Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recipe filters #1732

Merged
merged 7 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,15 @@ export const initialState = {
loadingIds: [],
updatingIds: [],
},
feastKeywords: {
data: {},
pagination: null,
lastError: null,
error: null,
lastSuccessfulFetchTimestamp: null,
loadingIds: [],
updatingIds: [],
},
};

export const finalStateWhenAddNewCollection = {
Expand Down Expand Up @@ -1530,6 +1539,15 @@ export const finalStateWhenAddNewCollection = {
loadingIds: [],
updatingIds: [],
},
feastKeywords: {
data: {},
pagination: null,
lastError: null,
error: null,
lastSuccessfulFetchTimestamp: null,
loadingIds: [],
updatingIds: [],
},
};

export const finalStateWhenRemoveACollection = {
Expand Down Expand Up @@ -2263,4 +2281,13 @@ export const finalStateWhenRemoveACollection = {
loadingIds: [],
updatingIds: [],
},
feastKeywords: {
data: {},
pagination: null,
lastError: null,
error: null,
lastSuccessfulFetchTimestamp: null,
loadingIds: [],
updatingIds: [],
},
};
97 changes: 97 additions & 0 deletions fronts-client/src/bundles/__tests__/feastKeywordBundle.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import fetchMock from 'fetch-mock';
import configureStore from '../../util/configureStore';
import { fetchKeywords, selectors } from '../feastKeywordBundle';

const quickTimeout = () =>
new Promise((resolve) => window.setTimeout(resolve, 10));

describe('feastKeywordBundle', () => {
beforeEach(() => fetchMock.reset());

it('should fetch celebrations and return them when asked', async () => {
const store = configureStore();
fetchMock.once('https://recipes.guardianapis.com/keywords/celebrations', {
celebrations: [
{
key: 'christmas',
doc_count: 3,
},
{
key: 'birthday',
doc_count: 2,
},
{
key: 'veganuary',
doc_count: 2,
},
{
key: 'bank-holiday',
doc_count: 1,
},
{
key: 'boxing-day',
doc_count: 1,
},
],
});

await store.dispatch(fetchKeywords('celebration') as any);
await quickTimeout(); //if we don't await again, the store has not been updated yet.

expect(selectors.selectCelebrationKeywords(store.getState())).toEqual([
'christmas',
'birthday',
'veganuary',
'bank-holiday',
'boxing-day',
]);
});

it('should fetch diets and return them when asked', async () => {
const store = configureStore();
fetchMock.once('https://recipes.guardianapis.com/keywords/diet-ids', {
'diet-ids': [
{
key: 'vegetarian',
doc_count: 66,
},
{
key: 'gluten-free',
doc_count: 37,
},
{
key: 'meat-free',
doc_count: 37,
},
{
key: 'dairy-free',
doc_count: 30,
},
{
key: 'pescatarian',
doc_count: 29,
},
{
key: 'vegan',
doc_count: 21,
},
{
key: '',
doc_count: 2,
},
],
});

await store.dispatch(fetchKeywords('diet') as any);
await quickTimeout(); //if we don't await again, the store has not been updated yet.

expect(selectors.selectDietKeywords(store.getState())).toEqual([
'vegetarian',
'gluten-free',
'meat-free',
'dairy-free',
'pescatarian',
'vegan',
]);
});
});
63 changes: 63 additions & 0 deletions fronts-client/src/bundles/feastKeywordBundle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import createAsyncResourceBundle, {
IPagination,
} from '../lib/createAsyncResourceBundle';
import { ThunkResult } from '../types/Store';
import { FeastKeywordType } from '../types/FeastKeyword';
import { liveRecipes } from '../services/recipeQuery';
import { createSelector } from 'reselect';
import { State } from '../types/State';

const bundle = createAsyncResourceBundle('feastKeywords', {
indexById: true,
selectLocalState: (state) => state.feastKeywords,
});

export const fetchKeywords =
(forType: FeastKeywordType): ThunkResult<void> =>
async (dispatch) => {
dispatch(actions.fetchStart(forType));

try {
const kwdata = await liveRecipes.keywords(forType);

const payload: {
ignoreOrder?: undefined;
pagination?: IPagination;
order?: string[];
} = {
pagination: {
pageSize: kwdata.length,
totalPages: 1,
currentPage: 1,
},
order: undefined,
};

dispatch(
actions.fetchSuccess(
kwdata.filter((kw) => !!kw.id),
payload,
),
);
} catch (err) {
console.error(`Unable to fetch keywords: `, err);
dispatch(actions.fetchError(err));
}
};

const selectAllKeywords = (state: State) => state.feastKeywords.data;
const makeKeywordSelector = (kwType: FeastKeywordType) =>
createSelector([selectAllKeywords], (kws) => {
return Object.keys(kws).filter((_) => kws[_].keywordType === kwType);
});
const selectCelebrationKeywords = makeKeywordSelector('celebration');
const selectDietKeywords = makeKeywordSelector('diet');

export const actionNames = bundle.actionNames;
export const actions = bundle.actions;
export const reducer = bundle.reducer;
export const selectors = {
...bundle.selectors,
selectCelebrationKeywords,
selectDietKeywords,
};
73 changes: 70 additions & 3 deletions fronts-client/src/components/feed/RecipeSearchContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import ClipboardHeader from 'components/ClipboardHeader';
import TextInput from 'components/inputs/TextInput';
import { styled } from 'constants/theme';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
fetchRecipes,
selectors as recipeSelectors,
} from 'bundles/recipesBundle';
import { selectors as feastKeywordsSelectors } from 'bundles/feastKeywordBundle';
import { fetchChefs, selectors as chefSelectors } from 'bundles/chefsBundle';
import { State } from 'types/State';
import { RecipeFeedItem } from './RecipeFeedItem';
Expand All @@ -19,10 +20,12 @@ import ScrollContainer from '../ScrollContainer';
import {
ChefSearchParams,
DateParamField,
RecipeSearchFilters,
RecipeSearchParams,
} from '../../services/recipeQuery';
import debounce from 'lodash/debounce';
import ButtonDefault from '../inputs/ButtonDefault';
import { fetchKeywords } from '../../bundles/feastKeywordBundle';

const InputContainer = styled.div`
margin-bottom: 10px;
Expand Down Expand Up @@ -70,6 +73,8 @@ export const RecipeSearchContainer = ({ rightHandContainer }: Props) => {

const [showAdvancedRecipes, setShowAdvancedRecipes] = useState(false);
const [dateField, setDateField] = useState<DateParamField>(undefined);
const [celebrationFilter, setCelebrationFilter] = useState<string>('');
const [dietFilter, setDietFilter] = useState<string>('');
const [orderingForce, setOrderingForce] = useState<string>('default');
const [forceDates, setForceDates] = useState(false);

Expand All @@ -94,14 +99,38 @@ export const RecipeSearchContainer = ({ rightHandContainer }: Props) => {
chefSelectors.selectLastFetchOrder(state),
);

const knownCelebrations = useSelector(
feastKeywordsSelectors.selectCelebrationKeywords,
);

const knownDiets = useSelector(feastKeywordsSelectors.selectDietKeywords);

const [page, setPage] = useState(1);

useEffect(() => {
dispatch(fetchKeywords('celebration'));
dispatch(fetchKeywords('diet'));
}, []);

const filters: RecipeSearchFilters | undefined = useMemo(() => {
if (celebrationFilter || dietFilter) {
return {
celebrations: celebrationFilter ? [celebrationFilter] : undefined,
diets: dietFilter ? [dietFilter] : undefined,
filterType: 'Post',
};
}
}, [celebrationFilter, dietFilter]);

useEffect(() => {
const dbf = debounce(() => runSearch(page), 750);
dbf();
return () => dbf.cancel();
}, [selectedOption, searchText, page, dateField, orderingForce]);
}, [searchText]);

useEffect(() => {
runSearch(page);
}, [page, dateField, orderingForce, filters, selectedOption]);
const chefsPagination: IPagination | null = useSelector((state: State) =>
chefSelectors.selectPagination(state),
);
Expand Down Expand Up @@ -140,12 +169,14 @@ export const RecipeSearchContainer = ({ rightHandContainer }: Props) => {
searchForRecipes({
queryText: searchText,
uprateByDate: dateField,
filters: filters,
uprateConfig: getUpdateConfig(),
limit: !!filters ? 300 : 100,
});
break;
}
},
[selectedOption, searchText, page, dateField, orderingForce],
[selectedOption, searchText, page, dateField, orderingForce, filters],
);

const renderTheFeed = () => {
Expand Down Expand Up @@ -191,6 +222,42 @@ export const RecipeSearchContainer = ({ rightHandContainer }: Props) => {

{showAdvancedRecipes && selectedOption === FeedType.recipes ? (
<>
<TopOptions>
<div>
<label htmlFor="celebrationSelector">Celebrations</label>
</div>
<div>
<select
style={{ textTransform: 'capitalize' }}
id="celebrationSelector"
value={celebrationFilter}
onChange={(evt) => setCelebrationFilter(evt.target.value)}
>
<option value={''}>Any</option>
{knownCelebrations.map((c) => (
<option value={c}>{c.replace(/-/g, ' ')}</option>
))}
</select>
</div>
</TopOptions>
<TopOptions>
<div>
<label htmlFor="dietSelector">Suitable for</label>{' '}
</div>
<div>
<select
style={{ textTransform: 'capitalize' }}
id="dietSelector"
value={dietFilter}
onChange={(evt) => setDietFilter(evt.target.value)}
>
<option value={''}>Any</option>
{knownDiets.map((d) => (
<option value={d}>{d.replace(/-/g, ' ')}</option>
))}
</select>
</div>
</TopOptions>
<TopOptions>
<div>
<label
Expand Down
1 change: 1 addition & 0 deletions fronts-client/src/fixtures/initialState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,7 @@ const state = {
},
notifications: { banners: [] },
chefs: emptyFeedBundle,
feastKeywords: emptyFeedBundle,
} as State;

export { state };
2 changes: 2 additions & 0 deletions fronts-client/src/reducers/rootReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { reducer as featureSwitches } from 'reducers/featureSwitchesReducer';
import { reducer as notificationsReducer } from 'bundles/notificationsBundle';
import { reducer as recipesReducer } from 'bundles/recipesBundle';
import { reducer as chefsReducer } from 'bundles/chefsBundle';
import { reducer as feastKeywordsReducer } from 'bundles/feastKeywordBundle';

const rootReducer = (state: any = { feed: {} }, action: any) => ({
fronts: fronts(state.fronts, action),
Expand Down Expand Up @@ -57,6 +58,7 @@ const rootReducer = (state: any = { feed: {} }, action: any) => ({
notifications: notificationsReducer(state.notifications, action),
recipes: recipesReducer(state.recipes, action),
chefs: chefsReducer(state.chefs, action),
feastKeywords: feastKeywordsReducer(state.feastKeywords, action),
});

export default rootReducer;
Loading
Loading