Skip to content

Commit

Permalink
Merge pull request #1732 from guardian/ag/recipe-filters
Browse files Browse the repository at this point in the history
Recipe filters
  • Loading branch information
fredex42 authored Dec 12, 2024
2 parents 73f3d1f + ed75186 commit a1b08de
Show file tree
Hide file tree
Showing 9 changed files with 328 additions and 5 deletions.
27 changes: 27 additions & 0 deletions fronts-client/src/actions/__tests__/fixtures/Editions.fixture.ts
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

0 comments on commit a1b08de

Please sign in to comment.