From 507e264284a68bfb589c118d95ebf886e3322d67 Mon Sep 17 00:00:00 2001 From: sepulzera Date: Thu, 14 Nov 2024 14:02:08 +0100 Subject: [PATCH] Add field season See https://github.com/ownrecipes/OwnRecipes/issues/2 --- src/app/components/IntlMessagesCreator.tsx | 2 + src/app/components/messages/Seasons.ts | 22 +++++ src/browse/components/SearchMenu.tsx | 24 +++++- src/browse/containers/SearchMenuContainer.tsx | 13 ++- src/browse/css/filter.css | 13 ++- src/browse/store/FilterActions.ts | 21 ++++- src/browse/store/FilterReducer.ts | 4 +- src/browse/store/FilterTypes.ts | 18 ++-- src/browse/store/SearchActions.ts | 7 +- src/common/config.ts | 8 +- src/demo/store/browse.ts | 58 ++++++++++--- src/demo/store/config.ts | 11 ++- src/demo/store/recipe.ts | 86 +++++++++++++++++-- src/demo/store/search.ts | 21 ++++- src/demo/store/seasons.ts | 46 ++++++++++ src/header/css/header.css | 2 + src/locale/de.json | 7 ++ src/locale/en.json | 9 +- src/locale/es.json | 7 ++ src/locale/fr.json | 7 ++ src/random/components/RandomHeader.tsx | 8 +- src/random/components/SearchMenu.tsx | 32 ++++++- src/random/containers/RandomPage.tsx | 5 ++ src/recipe/components/RecipeFooter.tsx | 3 + src/recipe/components/RecipeHeader.tsx | 22 +++-- src/recipe/components/RecipeScheme.tsx | 3 +- src/recipe/css/recipe_header.css | 2 +- src/recipe/store/RecipeTypes.ts | 50 ++++------- src/recipe/tests/data.ts | 2 +- src/recipe_form/components/RecipeForm.tsx | 34 +++++--- .../containers/SeasonSelectContainer.tsx | 53 ++++++++++++ src/recipe_form/store/actions.ts | 5 +- src/recipe_groups/store/actions.ts | 23 ++++- src/recipe_groups/store/reducer.ts | 6 +- src/recipe_groups/store/types.ts | 7 +- 35 files changed, 521 insertions(+), 120 deletions(-) create mode 100644 src/app/components/messages/Seasons.ts create mode 100644 src/demo/store/seasons.ts create mode 100644 src/recipe_form/containers/SeasonSelectContainer.tsx diff --git a/src/app/components/IntlMessagesCreator.tsx b/src/app/components/IntlMessagesCreator.tsx index c869db1a..796f4df7 100644 --- a/src/app/components/IntlMessagesCreator.tsx +++ b/src/app/components/IntlMessagesCreator.tsx @@ -4,6 +4,7 @@ import { useIntl } from 'react-intl'; import initCourses from './messages/Courses'; import initCuisines from './messages/Cuisines'; import initMeasurements from './messages/Measurements'; +import initSeasons from './messages/Seasons'; import initTags from './messages/Tags'; import initValidations from './messages/Validations'; @@ -14,6 +15,7 @@ const IntlMessagesCreator = () => { initCourses(); initCuisines(); initMeasurements(); + initSeasons(); initTags(); initValidations(); }, [locale]); diff --git a/src/app/components/messages/Seasons.ts b/src/app/components/messages/Seasons.ts new file mode 100644 index 00000000..f6378cdf --- /dev/null +++ b/src/app/components/messages/Seasons.ts @@ -0,0 +1,22 @@ +import { defineMessages } from 'react-intl'; + +export default function initSeasons() { + defineMessages({ + season_spring: { + id: 'season.Spring', + defaultMessage: 'Spring', + }, + season_summer: { + id: 'season.Summer', + defaultMessage: 'Summer', + }, + season_autumn: { + id: 'season.Autumn', + defaultMessage: 'Autumn', + }, + season_winter: { + id: 'season.Winter', + defaultMessage: 'Winter', + }, + }); +} diff --git a/src/browse/components/SearchMenu.tsx b/src/browse/components/SearchMenu.tsx index 817939db..8968938a 100644 --- a/src/browse/components/SearchMenu.tsx +++ b/src/browse/components/SearchMenu.tsx @@ -9,6 +9,7 @@ import '../css/filter.css'; import Icon from '../../common/components/Icon'; import P from '../../common/components/P'; import Chip from '../../common/components/Chip'; +import Tooltip from '../../common/components/Tooltip'; import { CategoryCount, RatingCount } from '../store/FilterTypes'; import Filter from './Filter'; @@ -33,6 +34,11 @@ const messages = defineMessages({ description: 'Filter field rating', defaultMessage: 'Ratings', }, + filter_season: { + id: 'filter.filter_season', + description: 'Filter field season', + defaultMessage: 'Seasons', + }, filter_tag: { id: 'filter.filter_tag', description: 'Filter field tag', @@ -95,6 +101,7 @@ export interface ISearchMenuProps { courses: Array | undefined; cuisines: Array | undefined; ratings: Array | undefined; + seasons: Array | undefined; tags: Array | undefined; activeFilters: Record; @@ -106,7 +113,7 @@ export interface ISearchMenuProps { } const SearchMenu: React.FC = ({ - qs, courses, cuisines, ratings, tags, + qs, courses, cuisines, ratings, seasons, tags, activeFilters, resetFilterUrl, openFilters, setOpenFilters, buildUrl }: ISearchMenuProps) => { const { formatMessage } = useIntl(); @@ -153,9 +160,11 @@ const SearchMenu: React.FC = ({ {activeFiltersCount > 0 && ( <> {activeFiltersCount} - - - + + + + + )} @@ -190,6 +199,13 @@ const SearchMenu: React.FC = ({ multiSelect buildUrl = {buildUrl} sort = 'off' /> + = ({ qs, qsString, buildUrl }: ISearchMenuContainerProps) => { const dispatch = useDispatch(); - const courses = useSelector((state: RootState) => state.browse.browserFilter.courses.items); - const cuisines = useSelector((state: RootState) => state.browse.browserFilter.cuisines.items); - const ratings = useSelector((state: RootState) => state.browse.browserFilter.ratings.items); - const tags = useSelector((state: RootState) => state.browse.browserFilter.tags.items); + const courses = useSelector((state: RootState) => state.browse.browserFilter.filter_courses.items); + const cuisines = useSelector((state: RootState) => state.browse.browserFilter.filter_cuisines.items); + const ratings = useSelector((state: RootState) => state.browse.browserFilter.filter_ratings.items); + const seasons = useSelector((state: RootState) => state.browse.browserFilter.filter_seasons.items); + const tags = useSelector((state: RootState) => state.browse.browserFilter.filter_tags.items); const [openFilters, setOpenFilters] = useState>(Object.keys(qs)); @@ -40,6 +41,9 @@ const SearchMenuContainer: React.FC = ({ if (openFilters.includes('rating') && ratings?.[qsString] == null) { dispatchQueue.push(FilterActions.loadRatings(qsMergedDefaults)); } + if (openFilters.includes('season') && seasons?.[qsString] == null) { + dispatchQueue.push(FilterActions.loadSeasons(qsMergedDefaults)); + } if (openFilters.includes('tag') && tags?.[qsString] == null) { dispatchQueue.push(FilterActions.loadTags(qsMergedDefaults)); } @@ -74,6 +78,7 @@ const SearchMenuContainer: React.FC = ({ courses = {courses?.[qsString]} cuisines = {cuisines?.[qsString]} ratings = {ratings?.[qsString]} + seasons = {seasons?.[qsString]} tags = {tags?.[qsString]} qs = {qs} diff --git a/src/browse/css/filter.css b/src/browse/css/filter.css index 9777bbd9..5b890d0b 100644 --- a/src/browse/css/filter.css +++ b/src/browse/css/filter.css @@ -15,8 +15,12 @@ border: none; } .container.browser .sidebar .filter-group .list-group-title { - padding: 0 10px 0.25em 10px; + padding: 0 10px; font-weight: bold; + margin-bottom: 0.25em; + + min-height: 1.4em; + align-content: center; } .container.browser .sidebar .filter-group .list-group-title .chip { padding: 0.25em 0.5em; @@ -46,10 +50,12 @@ html[data-theme="dark"] .container.browser .sidebar .filter-group .list-group-ti .container.browser .sidebar .filter-group .list-group-item:hover { border: 0; border-radius: 4px; -} -.container.browser .sidebar .filter-group .list-group-item:hover { background-color: var(--hoverBg); color: var(--primaryMain); + box-shadow: 0 0 0 1px var(--primaryText) inset; +} +.container.browser .sidebar .filter-group .list-group-item:focus { + box-shadow: 0 0 0 2px var(--primaryText) inset; } .container.browser .sidebar .filter-group .list-group-item.active { background-color: transparent; @@ -77,6 +83,7 @@ html[data-theme="dark"] .container.browser .sidebar .filter-group .list-group-ti .container.browser .filter-panel .card-header h2 { font-size: inherit; + font-weight: bold; margin-bottom: 0; } diff --git a/src/browse/store/FilterActions.ts b/src/browse/store/FilterActions.ts index 281395a0..0046954d 100644 --- a/src/browse/store/FilterActions.ts +++ b/src/browse/store/FilterActions.ts @@ -6,7 +6,7 @@ import { ACTION } from '../../common/store/ReduxHelper'; import { handleError } from '../../common/requestUtils'; import { objToSearchString } from '../../common/utility'; import { toBasicAction } from '../../common/store/redux'; -import { BROWSE_FILTER_COURSE_STORE, BROWSE_FILTER_CUISINE_STORE, BROWSE_FILTER_RATING_STORE, BROWSE_FILTER_TAGS_STORE, FilterDispatch } from './FilterTypes'; +import { BROWSE_FILTER_COURSE_STORE, BROWSE_FILTER_CUISINE_STORE, BROWSE_FILTER_RATING_STORE, BROWSE_FILTER_SEASON_STORE, BROWSE_FILTER_TAGS_STORE, FilterDispatch } from './FilterTypes'; import { extractSearchStringToFields } from './SearchActions'; const parsedFilter = (filters: Record): Record => { @@ -72,6 +72,25 @@ export const loadRatings = (filter: Record) => (dispatch: Filter .catch(err => dispatch(handleError(err, BROWSE_FILTER_RATING_STORE))); }; +export const loadSeasons = (filters: Record) => (dispatch: FilterDispatch) => { + dispatch({ ...toBasicAction(BROWSE_FILTER_SEASON_STORE, ACTION.LOADING) }); + + request() + .get(serverURLs.season_count) + .query(parsedFilter(filters)) + .then(res => ( + dispatch({ + ...toBasicAction( + BROWSE_FILTER_SEASON_STORE, + ACTION.GET_SUCCESS + ), + id: objToSearchString(filters), + payload: res.body.results, + }) + )) + .catch(err => dispatch(handleError(err, BROWSE_FILTER_SEASON_STORE))); +}; + export const loadTags = (filters: Record) => (dispatch: FilterDispatch) => { dispatch({ ...toBasicAction(BROWSE_FILTER_TAGS_STORE, ACTION.LOADING) }); diff --git a/src/browse/store/FilterReducer.ts b/src/browse/store/FilterReducer.ts index 0f18c586..b25ed69d 100644 --- a/src/browse/store/FilterReducer.ts +++ b/src/browse/store/FilterReducer.ts @@ -1,11 +1,12 @@ import { combineReducers, Reducer } from 'redux'; import ReduxHelper from '../../common/store/ReduxHelper'; -import { BROWSE_FILTER_COURSE_STORE, BROWSE_FILTER_CUISINE_STORE, BROWSE_FILTER_RATING_STORE, BROWSE_FILTER_TAGS_STORE, CategoryCount, FilterAction, FilterState, RatingCount } from './FilterTypes'; +import { BROWSE_FILTER_COURSE_STORE, BROWSE_FILTER_CUISINE_STORE, BROWSE_FILTER_RATING_STORE, BROWSE_FILTER_SEASON_STORE, BROWSE_FILTER_TAGS_STORE, CategoryCount, FilterAction, FilterState, RatingCount } from './FilterTypes'; const defaultCourseState = ReduxHelper.getMapReducerDefaultState>(BROWSE_FILTER_COURSE_STORE); const defaultCuisineState = ReduxHelper.getMapReducerDefaultState>(BROWSE_FILTER_CUISINE_STORE); const defaultRatingsState = ReduxHelper.getMapReducerDefaultState>(BROWSE_FILTER_RATING_STORE); +const defaultSeasonState = ReduxHelper.getMapReducerDefaultState>(BROWSE_FILTER_SEASON_STORE); const defaultTagsState = ReduxHelper.getMapReducerDefaultState>(BROWSE_FILTER_TAGS_STORE); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -20,6 +21,7 @@ const filters: Reducer = combineReducers({ [BROWSE_FILTER_COURSE_STORE]: createFilterWithNamedType(defaultCourseState), [BROWSE_FILTER_CUISINE_STORE]: createFilterWithNamedType(defaultCuisineState), [BROWSE_FILTER_RATING_STORE]: createFilterWithNamedType(defaultRatingsState), + [BROWSE_FILTER_SEASON_STORE]: createFilterWithNamedType(defaultSeasonState), [BROWSE_FILTER_TAGS_STORE]: createFilterWithNamedType(defaultTagsState), }); diff --git a/src/browse/store/FilterTypes.ts b/src/browse/store/FilterTypes.ts index 64008559..65b5142b 100644 --- a/src/browse/store/FilterTypes.ts +++ b/src/browse/store/FilterTypes.ts @@ -17,10 +17,11 @@ export interface RatingCount { export const BROWSE_FILTER_STORE = 'browserFilter'; -export const BROWSE_FILTER_COURSE_STORE = 'courses'; -export const BROWSE_FILTER_CUISINE_STORE = 'cuisines'; -export const BROWSE_FILTER_RATING_STORE = 'ratings'; -export const BROWSE_FILTER_TAGS_STORE = 'tags'; +export const BROWSE_FILTER_COURSE_STORE = 'filter_courses'; +export const BROWSE_FILTER_CUISINE_STORE = 'filter_cuisines'; +export const BROWSE_FILTER_RATING_STORE = 'filter_ratings'; +export const BROWSE_FILTER_SEASON_STORE = 'filter_seasons'; +export const BROWSE_FILTER_TAGS_STORE = 'filter_tags'; export type CategoryCountState = MapReducerType>; export type RatingCountState = MapReducerType>; @@ -28,8 +29,9 @@ export type RatingCountState = MapReducerType>; export type FilterAction = GenericMapReducerAction> | GenericMapReducerAction>; export type FilterDispatch = ReduxDispatch; export interface FilterState { - courses: CategoryCountState; - cuisines: CategoryCountState; - ratings: RatingCountState; - tags: CategoryCountState; + filter_courses: CategoryCountState; + filter_cuisines: CategoryCountState; + filter_ratings: RatingCountState; + filter_seasons: CategoryCountState; + filter_tags: CategoryCountState; } diff --git a/src/browse/store/SearchActions.ts b/src/browse/store/SearchActions.ts index 7da352ac..4297dab0 100644 --- a/src/browse/store/SearchActions.ts +++ b/src/browse/store/SearchActions.ts @@ -8,13 +8,14 @@ import { BROWSER_SEARCH_STORE, SearchDispatch, SearchResultDto, toSearchResult } import { parseCSV } from '../utilts/utility'; const FILTER_QUERY_PARAMETER_MAPPING: Record = { - cuisine: 'cuisine__slug', + author: 'author__username', course: 'course__slug', + cuisine: 'cuisine__slug', + season: 'season__slug', tag: 'tag__slug', - author: 'author__username', }; -const FIELDS = ['author', 'cuisine', 'course', 'directions', 'info', 'ordering', 'rating', 'source', 'tag', 'title']; +const FIELDS = ['author', 'course', 'cuisine', 'directions', 'info', 'ordering', 'rating', 'season', 'source', 'tag', 'title']; export function extractSearchStringToFields(filters: Record): Record { if (!filters.search || !filters.search.includes(':')) return filters; diff --git a/src/common/config.ts b/src/common/config.ts index cfce57e2..7fdfe773 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -9,10 +9,12 @@ export const serverURLs = { revoke_token: `${apiUrl}/accounts/revoke-auth-token/`, browse: `${apiUrl}/recipe/recipes/?fields=id,slug,title,pub_date,rating,rating_count,tags,photo_thumbnail,info`, mini_browse: `${apiUrl}/recipe/mini-browse/?fields=id,slug,title,pub_date,rating,rating_count,tags,photo_thumbnail,info`, - cuisine_count: `${apiUrl}/recipe_groups/cuisine-count/`, - cuisine: `${apiUrl}/recipe_groups/cuisine/`, - course_count: `${apiUrl}/recipe_groups/course-count/`, course: `${apiUrl}/recipe_groups/course/`, + course_count: `${apiUrl}/recipe_groups/course-count/`, + cuisine: `${apiUrl}/recipe_groups/cuisine/`, + cuisine_count: `${apiUrl}/recipe_groups/cuisine-count/`, + season: `${apiUrl}/recipe_groups/season/`, + season_count: `${apiUrl}/recipe_groups/season-count/`, ratings: `${apiUrl}/rating/rating/`, rating_count: `${apiUrl}/rating/rating-count/`, tag: `${apiUrl}/recipe_groups/tag/`, diff --git a/src/demo/store/browse.ts b/src/demo/store/browse.ts index 132c22a5..1e0f92d4 100644 --- a/src/demo/store/browse.ts +++ b/src/demo/store/browse.ts @@ -1,10 +1,11 @@ /* eslint-disable func-names */ import { CategoryCount, RatingCount } from '../../browse/store/FilterTypes'; -import { CourseDto, CuisineDto, RecipeDto, TagDto } from '../../recipe/store/RecipeTypes'; +import { CourseDto, CuisineDto, RecipeDto, SeasonDto, TagDto } from '../../recipe/store/RecipeTypes'; import { demoCourses } from './courses'; import { demoCuisines } from './cuisines'; import { demoRecipes } from './recipe'; +import { demoSeasons } from './seasons'; import { demoFindSearchRecipes } from './search'; import { demoGetAllTags } from './tags'; import { ObjectIterator, toQueryParams } from './utils'; @@ -37,6 +38,41 @@ export const ratingCountConfig = { }, }; +export const courseCountConfig = { + pattern: '(.*)/recipe_groups/course-count/(.*)', + fixtures: function (match: Array) { + // console.log(`fixtures running for courseCountConfig. match=${JSON.stringify(match)}`); + + if (match.length < 3) return {}; + const queryParams: URLSearchParams = toQueryParams(match[2]); + const resultRecipes: Array = demoFindSearchRecipes(demoRecipes, queryParams); + + const allCourses: Array = demoCourses; + + const courseCounts: Array = []; + allCourses.forEach(c => { + courseCounts.push({ + id: c.id, + total: resultRecipes.filter(rec => rec.course?.title === c.title).length, + title: c.title, + slug: c.title, + }); + }); + + const result: ObjectIterator = { + count: courseCounts.length, + next: null, + previous: null, + results: courseCounts, + }; + + return result; + }, + get: function (_match: Array, data: Record) { + return { body : data }; + }, +}; + export const cuisineCountConfig = { pattern: '(.*)/recipe_groups/cuisine-count/(.*)', fixtures: function (match: Array) { @@ -72,32 +108,32 @@ export const cuisineCountConfig = { }, }; -export const courseCountConfig = { - pattern: '(.*)/recipe_groups/course-count/(.*)', +export const seasonCountConfig = { + pattern: '(.*)/recipe_groups/season-count/(.*)', fixtures: function (match: Array) { - // console.log(`fixtures running for courseCountConfig. match=${JSON.stringify(match)}`); + // console.log(`fixtures running for seasonCountConfig. match=${JSON.stringify(match)}`); if (match.length < 3) return {}; const queryParams: URLSearchParams = toQueryParams(match[2]); const resultRecipes: Array = demoFindSearchRecipes(demoRecipes, queryParams); - const allCourses: Array = demoCourses; + const allSeasons: Array = demoSeasons; - const courseCounts: Array = []; - allCourses.forEach(c => { - courseCounts.push({ + const seasonCounts: Array = []; + allSeasons.forEach(c => { + seasonCounts.push({ id: c.id, - total: resultRecipes.filter(rec => rec.course?.title === c.title).length, + total: resultRecipes.filter(rec => rec.season?.title === c.title).length, title: c.title, slug: c.title, }); }); const result: ObjectIterator = { - count: courseCounts.length, + count: seasonCounts.length, next: null, previous: null, - results: courseCounts, + results: seasonCounts, }; return result; diff --git a/src/demo/store/config.ts b/src/demo/store/config.ts index a3cdbd75..14ec0414 100644 --- a/src/demo/store/config.ts +++ b/src/demo/store/config.ts @@ -1,22 +1,25 @@ -import cuisines from './cuisines'; import courses from './courses'; +import cuisines from './cuisines'; import miniBrowse from './miniBrowse'; import ratings from './ratings'; import recipe from './recipe'; -import { cuisineCountConfig, courseCountConfig, ratingCountConfig, tagCountConfig } from './browse'; +import { cuisineCountConfig, courseCountConfig, ratingCountConfig, seasonCountConfig, tagCountConfig } from './browse'; import search from './search'; +import seasons from './seasons'; import tags from './tags'; const config = [ - cuisineCountConfig, - cuisines, courseCountConfig, courses, + cuisineCountConfig, + cuisines, miniBrowse, ratingCountConfig, ratings, recipe, search, + seasonCountConfig, + seasons, tagCountConfig, tags, ]; diff --git a/src/demo/store/recipe.ts b/src/demo/store/recipe.ts index 71ccd1f3..c82ec0be 100644 --- a/src/demo/store/recipe.ts +++ b/src/demo/store/recipe.ts @@ -106,13 +106,17 @@ export const demoRecipes: Array = [ "rating_count": 2, "public": true, "author": 1, + "course": { + "id": 4, + "title": "Main" + }, "cuisine": { "id": 1, "title": "American" }, - "course": { - "id": 4, - "title": "Main" + "season": { + "id": 2, + "title": "Summer" } }, { @@ -211,13 +215,17 @@ export const demoRecipes: Array = [ "rating_count": 132, "public": true, "author": 3, + "course": { + "id": 1, + "title": "Entry" + }, "cuisine": { "id": 1, "title": "Mexican" }, - "course": { - "id": 1, - "title": "Entry" + "season": { + "id": 2, + "title": "Summer" } }, { @@ -352,13 +360,17 @@ export const demoRecipes: Array = [ "rating_count": 42, "public": true, "author": 2, + "course": { + "id": 4, + "title": "Main" + }, "cuisine": { "id": 1, "title": "American" }, - "course": { - "id": 4, - "title": "Main" + "season": { + "id": 2, + "title": "Summer" } }, { @@ -484,6 +496,10 @@ export const demoRecipes: Array = [ "id": 10, "title": "German" }, + "season": { + "id": 2, + "title": "Summer" + }, "title": "Wild Garlic Potato Cake with Asparagus", "slug": "demo-wild-garlic-potato-cake-with-asparagus", "info": "", @@ -616,6 +632,10 @@ export const demoRecipes: Array = [ "id": 0, "title": "-" }, + "season": { + "id": 0, + "title": "-" + }, "title": "The best vegan banana bread", "slug": "demo-the-best-vegan-banana-bread", "info": "", @@ -760,6 +780,10 @@ export const demoRecipes: Array = [ "id": 11, "title": "European" }, + "season": { + "id": 2, + "title": "Summer" + }, "title": "Beef on prunes", "slug": "demo-beef-on-prunes", "info": "", @@ -926,6 +950,10 @@ export const demoRecipes: Array = [ "id": 11, "title": "European" }, + "season": { + "id": 3, + "title": "Autumn" + }, "title": "Creamy apple pie", "slug": "demo-creamy-apple-pie", "info": "", @@ -1043,6 +1071,10 @@ export const demoRecipes: Array = [ "id": 11, "title": "European" }, + "season": { + "id": 4, + "title": "Winter" + }, "title": "Crunchy muesli variety", "slug": "demo-crunchy-muesli-variety", "info": "", @@ -1163,6 +1195,10 @@ export const demoRecipes: Array = [ "id": 11, "title": "European" }, + "season": { + "id": 1, + "title": "Spring" + }, "title": "Chicken-roulade with dried plums", "slug": "demo-chicken-roulade-with-dried-plums", "info": "Not the usual beef roulade.", @@ -1268,6 +1304,10 @@ export const demoRecipes: Array = [ "id": 0, "title": "-" }, + "season": { + "id": 0, + "title": "-" + }, "title": "Tortilla couscous rolls", "slug": "demo-tortilla-couscous-rolls", "info": "Those delicious rolls are a nice addition to finger food, or even as standalone with a dip.", @@ -1385,6 +1425,10 @@ export const demoRecipes: Array = [ "id": 11, "title": "European" }, + "season": { + "id": 4, + "title": "Winter" + }, "title": "Simsalabim cheesecake with pudding", "slug": "demo-simsalabim-cheesecake-with-pudding", "info": "This blueberry cake is magical.", @@ -1486,6 +1530,10 @@ export const demoRecipes: Array = [ "id": 0, "title": "-" }, + "season": { + "id": 1, + "title": "Spring" + }, "title": "Gorgonzola asparagus", "slug": "demo-gorgonzola-asparagus", "info": "Asparagus and gorgonzola are a perfect team.", @@ -1594,6 +1642,10 @@ export const demoRecipes: Array = [ "id": 10, "title": "German" }, + "season": { + "id": 0, + "title": "-" + }, "title": "Balsamic mushrooms", "slug": "demo-balsamic-mushrooms", "info": "This appetizer is part of a magic menu, see subrecipes.", @@ -1713,6 +1765,10 @@ export const demoRecipes: Array = [ "id": 10, "title": "German" }, + "season": { + "id": 0, + "title": "-" + }, "title": "Herb fillet pocket", "slug": "demo-herb-fillet-pocket", "info": "This main course is part of a magic menu, see subrecipes.", @@ -1798,6 +1854,10 @@ export const demoRecipes: Array = [ "id": 10, "title": "German" }, + "season": { + "id": 4, + "title": "Winter" + }, "title": "Eggnog coffee", "slug": "demo-eggnog-coffee", "info": "This dessert is part of a magic menu, see subrecipes.", @@ -1854,6 +1914,10 @@ export const demoRecipes: Array = [ "id": 10, "title": "German" }, + "season": { + "id": 0, + "title": "-" + }, "title": "Magical Menu", "slug": "demo-magical-menu", "info": "Magic menu for special moments.", @@ -1961,6 +2025,10 @@ export const demoRecipes: Array = [ "id": 0, "title": "-" }, + "season": { + "id": 4, + "title": "Winter" + }, "title": "Beetroot cream soup with croutons", "slug": "demo-beetroot-cream-soup-with-croutons", "info": "", diff --git a/src/demo/store/search.ts b/src/demo/store/search.ts index 6858f7df..4252e16f 100644 --- a/src/demo/store/search.ts +++ b/src/demo/store/search.ts @@ -42,6 +42,21 @@ function demoFilterCuisine(resultRecipes: Array, queryParams: URLSear return resultRecipes; } +function demoDoFilterBySeason(resultRecipes: Array, seasons: Array): Array { + return resultRecipes.filter(r => r.season && seasons?.includes(r.season.title.toLocaleLowerCase())); +} +function demoFilterSeason(resultRecipes: Array, queryParams: URLSearchParams): Array { + if (queryParams.has('season')) { + const querySeasons = queryParams.get('season')?.split(',').map(c => c.toLocaleLowerCase()); + return demoDoFilterBySeason(resultRecipes, querySeasons ?? []); + } + if (queryParams.has('season__slug')) { + const querySeasons = queryParams.get('season__slug')?.split(',').map(c => c.toLocaleLowerCase()); + return demoDoFilterBySeason(resultRecipes, querySeasons ?? []); + } + return resultRecipes; +} + function demoDoFilterByTag(resultRecipes: Array, tags: Array): Array { return resultRecipes.filter(r => r.tags.find(t => tags?.includes(t.title.toLocaleLowerCase()))); } @@ -106,8 +121,9 @@ function demoFilterSearch(resultRecipes: Array, queryParams: URLSearc const querySearches = queryParams.get('search')?.split(' ').map(c => c.toLocaleLowerCase()); const resultRecipess: Set = new Set(); - demoDoFilterByCuisine(resultRecipes, querySearches ?? []).forEach(r => resultRecipess.add(r)); demoDoFilterByCourse(resultRecipes, querySearches ?? []).forEach(r => resultRecipess.add(r)); + demoDoFilterByCuisine(resultRecipes, querySearches ?? []).forEach(r => resultRecipess.add(r)); + demoDoFilterBySeason(resultRecipes, querySearches ?? []).forEach(r => resultRecipess.add(r)); demoDoFilterByTag(resultRecipes, querySearches ?? []).forEach(r => resultRecipess.add(r)); demoDoFilterByAuthor(resultRecipes, querySearches ?? []).forEach(r => resultRecipess.add(r)); demoDoFilterByTitle(resultRecipes, querySearches ?? []).forEach(r => resultRecipess.add(r)); @@ -155,8 +171,9 @@ export function demoFindSearchRecipes(allRecipes: Array, queryParams: let resultRecipes: Array = [...allRecipes]; resultRecipes = demoFilterRating(resultRecipes, queryParams); - resultRecipes = demoFilterCuisine(resultRecipes, queryParams); resultRecipes = demoFilterCourse(resultRecipes, queryParams); + resultRecipes = demoFilterCuisine(resultRecipes, queryParams); + resultRecipes = demoFilterSeason(resultRecipes, queryParams); resultRecipes = demoFilterTag(resultRecipes, queryParams); resultRecipes = demoFilterAuthor(resultRecipes, queryParams); resultRecipes = demoFilterSearch(resultRecipes, queryParams); diff --git a/src/demo/store/seasons.ts b/src/demo/store/seasons.ts new file mode 100644 index 00000000..a037c1ae --- /dev/null +++ b/src/demo/store/seasons.ts @@ -0,0 +1,46 @@ +/* eslint-disable func-names */ + +import { SeasonDto } from '../../recipe/store/RecipeTypes'; +import { ObjectIterator } from './utils'; + +/* eslint-disable quotes, quote-props, comma-dangle */ +export const demoSeasons: Array = [ + { + "id": 1, + "title": "Spring", + }, + { + "id": 2, + "title": "Summer", + }, + { + "id": 3, + "title": "Autumn", + }, + { + "id": 4, + "title": "Winter", + }, +]; +/* eslint-enable quotes, quote-props, comma-dangle */ + +const config = { + pattern: '(.*)/recipe_groups/season/', + fixtures: function () { + // console.log(`fixtures running for seasons.`); + + const result: ObjectIterator = { + count: demoSeasons.length, + next: null, + previous: null, + results: demoSeasons, + }; + + return result; + }, + get: function (_match: Array, data: Record) { + return { body : data }; + }, +}; + +export default config; diff --git a/src/header/css/header.css b/src/header/css/header.css index b78995cd..a8a6c1bd 100644 --- a/src/header/css/header.css +++ b/src/header/css/header.css @@ -203,6 +203,8 @@ border-color: white; } #header-navbar form .form-group .input-adornment-end .nav-link { + height: 100%; + align-content: center; padding-top: 0; padding-bottom: 0; } diff --git a/src/locale/de.json b/src/locale/de.json index e83a8c52..e759ad47 100644 --- a/src/locale/de.json +++ b/src/locale/de.json @@ -50,6 +50,7 @@ "filter.filter_cuisine": "Küchen", "filter.filter_ordering": "Sortieren nach", "filter.filter_rating": "Bewertung", + "filter.filter_season": "Saison", "filter.filter_tag": "Kennungen", "filter.filters": "Filter", "filter.hide_filters": "Filter ausblenden", @@ -254,6 +255,7 @@ "random.search.menu.filter_all": "(Alle)", "random.search.menu.filter_by_course_dropdown": "Gang: {course}", "random.search.menu.filter_by_cuisine_dropdown": "Küche: {cuisine}", + "random.search.menu.filter_by_season_dropdown": "Saison: {season}", "rating.edited_by": "Editiert von", "rating.form.create.title": "Dein neuer Kommentar", "rating.form.edit.title": "Kommentar von {username}", @@ -294,6 +296,7 @@ "recipe.create.prep_time_label": "Vorbereitungszeit (min)", "recipe.create.preview": "Vorschau", "recipe.create.public_label": "Öffentlich", + "recipe.create.season_label": "Saison", "recipe.create.servings_label": "Anz. Portionen", "recipe.create.source_label": "Verweis zum Original", "recipe.create.source_tooltip": "Wo das originale Rezept zu finden ist, z. B. Link.", @@ -333,6 +336,10 @@ "searchsummary.results": "{resultsCount, plural, one {# Ergebnis} other {# Ergebnisse}}", "searchsummary.results_pagination": "{offset}-{offsetLast} von {resultsCount} Ergebnisse", "searchsummary.sort_by": "Sortieren: {sort}", + "season.Autumn": "Herbst", + "season.Spring": "Frühling", + "season.Summer": "Sommer", + "season.Winter": "Winter", "select.no_options": "Keine Optionen", "settings.language.display": "Anzeigesprache", "settings.language.heading": "Sprache", diff --git a/src/locale/en.json b/src/locale/en.json index fa62831d..32d78cb4 100644 --- a/src/locale/en.json +++ b/src/locale/en.json @@ -50,6 +50,7 @@ "filter.filter_cuisine": "Cuisines", "filter.filter_ordering": "Ordering", "filter.filter_rating": "Ratings", + "filter.filter_season": "Seasons", "filter.filter_tag": "Tags", "filter.filters": "Filters", "filter.hide_filters": "Hide Filters", @@ -247,6 +248,7 @@ "random.search.menu.filter_all": "(All)", "random.search.menu.filter_by_course_dropdown": "Course: {course}", "random.search.menu.filter_by_cuisine_dropdown": "Cuisine: {cuisine}", + "random.search.menu.filter_by_season_dropdown": "Season: {season}", "rating.edited_by": "Edited by", "rating.form.create.title": "Your new rating", "rating.form.edit.title": "Comment by {username}", @@ -266,7 +268,7 @@ "recipe.add_to_menu_tooltip": "Add recipe to menu", "recipe.comments": "Comments", "recipe.comments.new_rating": "New rating", - "recipe.comments.title": "Comments ({count})", + "recipe.comments.title": "{count, plural, one {# Comment} other {# Comments}}", "recipe.confirm_delete": "Are you sure you want to delete this recipe?", "recipe.confirm_delete_accept": "Delete", "recipe.confirm_delete_title": "Confirm deletion", @@ -287,6 +289,7 @@ "recipe.create.prep_time_label": "Prep time (min)", "recipe.create.preview": "Preview", "recipe.create.public_label": "Public Recipe", + "recipe.create.season_label": "Season", "recipe.create.servings_label": "Servings", "recipe.create.source_label": "Source", "recipe.create.source_tooltip": "Where the original recipe is from.", @@ -326,6 +329,10 @@ "searchsummary.results": "{resultsCount, plural, one {# result} other {# results}}", "searchsummary.results_pagination": "{offset}-{offsetLast} of {resultsCount} results", "searchsummary.sort_by": "Sort by: {sort}", + "season.Autumn": "Autumn", + "season.Spring": "Spring", + "season.Summer": "Summer", + "season.Winter": "Winter", "select.no_options": "No options", "settings.language.display": "Display language", "settings.language.heading": "Language", diff --git a/src/locale/es.json b/src/locale/es.json index b6062618..7abaa056 100644 --- a/src/locale/es.json +++ b/src/locale/es.json @@ -50,6 +50,7 @@ "filter.filter_cuisine": "Cocinas", "filter.filter_ordering": "Ordenar por", "filter.filter_rating": "Evaluación", + "filter.filter_season": "Temporada", "filter.filter_tag": "Etiquetas", "filter.filters": "Filtros", "filter.hide_filters": "Esconder filtros", @@ -254,6 +255,7 @@ "random.search.menu.filter_all": "(Todos)", "random.search.menu.filter_by_course_dropdown": "Plato: {course}", "random.search.menu.filter_by_cuisine_dropdown": "Cocina: {cuisine}", + "random.search.menu.filter_by_season_dropdown": "Temporada: {season}", "rating.edited_by": "Editado por", "rating.form.create.title": "Tu nuevo comentario", "rating.form.edit.title": "Comentario de {username}", @@ -294,6 +296,7 @@ "recipe.create.prep_time_label": "Tiempo de preparación (min)", "recipe.create.preview": "Vista previa", "recipe.create.public_label": "Público", + "recipe.create.season_label": "Temporada", "recipe.create.servings_label": "Número de porciones", "recipe.create.source_label": "Referencia al original", "recipe.create.source_tooltip": "Origen de la receta, por ejemplo un link", @@ -333,6 +336,10 @@ "searchsummary.results": "{resultsCount, plural, one {# resultado} other {# resultados}}", "searchsummary.results_pagination": "{offset}-{offsetLast} de {resultsCount} resultados", "searchsummary.sort_by": "Ordenar por: {sort}", + "season.Autumn": "Otoño", + "season.Spring": "Primavera", + "season.Summer": "Verano", + "season.Winter": "Invierno", "select.no_options": "aucune option", "settings.language.display": "Idioma de la pantalla", "settings.language.heading": "Idioma", diff --git a/src/locale/fr.json b/src/locale/fr.json index 99ef76e5..4a24cce8 100644 --- a/src/locale/fr.json +++ b/src/locale/fr.json @@ -50,6 +50,7 @@ "filter.filter_cuisine": "Cuisines", "filter.filter_ordering": "Trier par", "filter.filter_rating": "Évaluation", + "filter.filter_season": "Saison", "filter.filter_tag": "Identifications", "filter.filters": "Filtre", "filter.hide_filters": "Masquer les filtres", @@ -254,6 +255,7 @@ "random.search.menu.filter_all": "(Tous)", "random.search.menu.filter_by_course_dropdown": "Cours: {course}", "random.search.menu.filter_by_cuisine_dropdown": "Cuisine: {cuisine}", + "random.search.menu.filter_by_season_dropdown": "Saison: {season}", "rating.edited_by": "Changé par", "rating.form.create.title": "Ton nouveau commentaire", "rating.form.edit.title": "Ton nouveau commentaire", @@ -294,6 +296,7 @@ "recipe.create.prep_time_label": "Temps de préparation (min)", "recipe.create.preview": "Aperçu", "recipe.create.public_label": "Public", + "recipe.create.season_label": "Saison", "recipe.create.servings_label": "Nombre de portions", "recipe.create.source_label": "Référence à l'original", "recipe.create.source_tooltip": "Où trouver la recette originale, par exemple Link.", @@ -333,6 +336,10 @@ "searchsummary.results": "{resultsCount, plural, one {# résultat} other {# résultats}}", "searchsummary.results_pagination": "{offset}-{offsetLast} de {resultsCount} résultats", "searchsummary.sort_by": "Trier: {sort}", + "season.Autumn": "Automne", + "season.Spring": "Printemps", + "season.Summer": "Été", + "season.Winter": "Hiver", "select.no_options": "sin option", "settings.language.display": "Langue d'affichage", "settings.language.heading": "Langue", diff --git a/src/random/components/RandomHeader.tsx b/src/random/components/RandomHeader.tsx index 1d55d620..b729c722 100644 --- a/src/random/components/RandomHeader.tsx +++ b/src/random/components/RandomHeader.tsx @@ -4,7 +4,7 @@ import { defineMessages, useIntl } from 'react-intl'; import P from '../../common/components/P'; import { SearchResult } from '../../browse/store/SearchTypes'; import SearchMenu from './SearchMenu'; -import { Course, Cuisine } from '../../recipe/store/RecipeTypes'; +import { Course, Cuisine, Season } from '../../recipe/store/RecipeTypes'; const messages = defineMessages({ random_heading: { @@ -21,8 +21,9 @@ const messages = defineMessages({ export interface IRandomHeaderProps { search: Record | undefined; - courses: Array| undefined; + courses: Array | undefined; cuisines: Array | undefined; + seasons: Array | undefined; qs: Record; qsString: string; @@ -30,7 +31,7 @@ export interface IRandomHeaderProps { } const RandomHeader: React.FC = ({ - search, courses, cuisines, qs, qsString, + search, courses, cuisines, seasons, qs, qsString, buildUrl }: IRandomHeaderProps) => { const { formatMessage } = useIntl(); @@ -47,6 +48,7 @@ const RandomHeader: React.FC = ({ search = {qsSearchResult} courses = {courses} cuisines = {cuisines} + seasons = {seasons} buildUrl = {buildUrl} /> diff --git a/src/random/components/SearchMenu.tsx b/src/random/components/SearchMenu.tsx index 1383bc73..feeba627 100644 --- a/src/random/components/SearchMenu.tsx +++ b/src/random/components/SearchMenu.tsx @@ -5,7 +5,7 @@ import { Link } from 'react-router-dom'; import { SearchResult } from '../../browse/store/SearchTypes'; import { optionallyFormatMessage } from '../../common/utility'; -import { Course, Cuisine } from '../../recipe/store/RecipeTypes'; +import { Course, Cuisine, Season } from '../../recipe/store/RecipeTypes'; const messages = defineMessages({ filter_by_course: { @@ -18,6 +18,11 @@ const messages = defineMessages({ description: 'Filter by indian/...', defaultMessage: 'Cuisine: {cuisine}', }, + filter_by_season: { + id: 'random.search.menu.filter_by_season_dropdown', + description: 'Filter by spring/...', + defaultMessage: 'Season: {season}', + }, filter_all: { id: 'random.search.menu.filter_all', description: 'Item to not filter at all', @@ -30,10 +35,11 @@ export interface ISearchMenuProps { qs: Record; courses: Array | undefined; cuisines: Array | undefined; + seasons: Array | undefined; buildUrl: (qsTitle: string, recipeSlug: string, multiSelect?: boolean) => string; } -const SearchMenu: React.FC = ({ search, qs, courses, cuisines, buildUrl }: ISearchMenuProps) => { +const SearchMenu: React.FC = ({ search, qs, courses, cuisines, seasons, buildUrl }: ISearchMenuProps) => { const intl = useIntl(); const { formatMessage } = intl; @@ -67,6 +73,20 @@ const SearchMenu: React.FC = ({ search, qs, courses, cuisines, )); + const currentSeason = qs.season__slug ?? ''; + const handleFilterSeasonClick = useCallback((event: React.MouseEvent, filterSeason: string) => { + if (currentSeason === filterSeason) { + event.preventDefault(); + } + }, [currentSeason]); + const seasonDropdownItems = seasons?.map(season => ({ key: season.title, value: optionallyFormatMessage(intl, 'season.', season.title) })); + seasonDropdownItems?.unshift({ key: '', value: filterAllText }); + const seasonDropdownItemsJsx = seasonDropdownItems?.map(item => ( + ) => handleFilterSeasonClick(event, item.key)}> + {item.value} + + )); + return ( @@ -85,6 +105,14 @@ const SearchMenu: React.FC = ({ search, qs, courses, cuisines, {cuisineDropdownItemsJsx} + + + {formatMessage(messages.filter_by_season, { season: currentSeason ? optionallyFormatMessage(intl, 'season.', currentSeason) : filterAllText })} + + + {seasonDropdownItemsJsx} + + ); }; diff --git a/src/random/containers/RandomPage.tsx b/src/random/containers/RandomPage.tsx index 407048e2..403f361f 100644 --- a/src/random/containers/RandomPage.tsx +++ b/src/random/containers/RandomPage.tsx @@ -39,6 +39,10 @@ const RandomPage: React.FC = () => { const cuisines = useSelector((state: RootState) => state.recipeGroups.cuisines.items); useSingle(fetchCuisines, cuisines); + const fetchSeasons = useCallback(() => { dispatch(RecipeGroupActions.fetchSeasons()); }, []); + const seasons = useSelector((state: RootState) => state.recipeGroups.seasons.items); + useSingle(fetchSeasons, seasons); + const reloadData = useCallback(() => { dispatch(SearchActions.loadRandomRecipes(qsMergedDefaults)); }, [qsMergedDefaults]); @@ -61,6 +65,7 @@ const RandomPage: React.FC = () => { search = {search} courses = {courses} cuisines = {cuisines} + seasons = {seasons} qs = {qs} qsString = {qsMergedString} buildUrl = {handleBuildUrl} diff --git a/src/recipe/components/RecipeFooter.tsx b/src/recipe/components/RecipeFooter.tsx index 8396f58c..7bfa1824 100644 --- a/src/recipe/components/RecipeFooter.tsx +++ b/src/recipe/components/RecipeFooter.tsx @@ -19,6 +19,9 @@ function getFilters(recipe: Recipe): Record | undefined { if (recipe.cuisine) { res.cuisine__slug = recipe.cuisine.title; } + if (recipe.season) { + res.season__slug = recipe.season.title; + } return Object.keys(res).length > 0 ? res : undefined; } diff --git a/src/recipe/components/RecipeHeader.tsx b/src/recipe/components/RecipeHeader.tsx index 458bcae4..84bc7f11 100644 --- a/src/recipe/components/RecipeHeader.tsx +++ b/src/recipe/components/RecipeHeader.tsx @@ -1,6 +1,7 @@ import { useCallback, useMemo, useState } from 'react'; import { Col, Row } from 'react-bootstrap'; import { defineMessages, useIntl } from 'react-intl'; +import classNames from 'classnames'; import '../css/recipe_header.css'; @@ -218,8 +219,8 @@ const RecipeHeader: React.FC = ({ ); const addToMenuButton = ( - ); @@ -232,7 +233,7 @@ const RecipeHeader: React.FC = ({ {deleteLink} )} - {!isDemoMode() && addToMenuButton} + {addToMenuButton} {printButton} @@ -256,18 +257,24 @@ const RecipeHeader: React.FC = ({ {` ${formatMessage(messages.minutes, { count: recipe.cookTime })}`} )} - {recipe.course != null && recipe.course.title != null && recipe.course.title.length > 0 && ( + {recipe.course?.title != null && recipe.course.title.length > 0 && ( - + {optionallyFormatMessage(intl, 'course.', recipe.course.title)} )} - {recipe.cuisine != null && recipe.cuisine.title != null && recipe.cuisine.title.length > 0 && ( + {recipe.cuisine?.title != null && recipe.cuisine.title.length > 0 && ( {optionallyFormatMessage(intl, 'cuisine.', recipe.cuisine.title)} )} + {recipe.season?.title != null && recipe.season.title.length > 0 && ( + + + {optionallyFormatMessage(intl, 'season.', recipe.season.title)} + + )}
@@ -293,7 +300,7 @@ const RecipeHeader: React.FC = ({ return ( <> -
+

{recipe?.title}

@@ -331,7 +338,6 @@ const RecipeHeader: React.FC = ({ {chips} {source} -
diff --git a/src/recipe/components/RecipeScheme.tsx b/src/recipe/components/RecipeScheme.tsx index 176bfb1c..5e5ca9a6 100644 --- a/src/recipe/components/RecipeScheme.tsx +++ b/src/recipe/components/RecipeScheme.tsx @@ -1,4 +1,5 @@ import { Col, Row } from 'react-bootstrap'; +import classNames from 'classnames'; import '../css/recipe.css'; @@ -20,7 +21,7 @@ interface IRecipeSchemeProps { } const RecipeScheme: React.FC = ({ recipe, recipeMeta, userId, editable, onEditRecipe, deleteRecipe, onAddToMenuClick }: IRecipeSchemeProps) => ( -
+
({ - id: dto.id, - title: dto.title, -}); - -export const toCourseDto = (obj: Course): CourseDto => ({ - id: obj.id, - title: obj.title, -}); +export type Course = CourseDto; +export const toCourse = (dto: CourseDto): Course => dto; +export const toCourseDto = (obj: Course): CourseDto => obj; export interface CuisineDto { id: number; title: string; } -export interface Cuisine { +export type Cuisine = CuisineDto; +export const toCuisine = (dto: CuisineDto): Cuisine => dto; +export const toCuisineDto = (obj: Cuisine): CuisineDto => obj; + +export interface SeasonDto { id: number; title: string; } -export const toCuisine = (dto: CuisineDto): Cuisine => ({ - id: dto.id, - title: dto.title, -}); - -export const toCuisineDto = (obj: Cuisine): CuisineDto => ({ - id: obj.id, - title: obj.title, -}); +export type Season = SeasonDto; +export const toSeason = (dto: SeasonDto): Season => dto; +export const toSeasonDto = (obj: Season): SeasonDto => obj; export interface TagDto { id: number; title: string; } -export interface Tag { - id: number; - title: string; -} +export type Tag = TagDto; -export const toTag = (dto: TagDto): Tag => ({ - id: dto.id, - title: dto.title, -}); +export const toTag = (dto: TagDto): Tag => dto; export interface RecipeListDto { id: number; @@ -213,6 +194,7 @@ export interface RecipeDto extends RecipeListDto { course?: Course; cuisine?: Cuisine; + season?: Season; tags: Array; photo?: string | null; @@ -240,6 +222,7 @@ export interface Recipe extends RecipeList { course?: Course; cuisine?: Cuisine; + season?: Season; tags: Array; oTags: TagObj; @@ -274,6 +257,7 @@ export const toRecipe = (dto: RecipeDto): Recipe => ({ course: (dto.course == null || dto.course.title === '-') ? undefined : toCourse(dto.course), cuisine: (dto.cuisine == null || dto.cuisine.title === '-') ? undefined : toCuisine(dto.cuisine), + season: (dto.season == null || dto.season.title === '-') ? undefined : toSeason(dto.season), tags: dto.tags.map(toTag), oTags: _.keyBy(dto.tags.map(toTag), 'title'), @@ -312,6 +296,7 @@ export interface RecipeRequest { tags: Array; course: CourseDto | null; cuisine: CuisineDto | null; + season: SeasonDto | null; subrecipes: Array; ingredient_groups: Array; @@ -360,6 +345,7 @@ export const toRecipeRequest = (obj: Recipe): RecipeRequest => ({ tags: obj.tags, course: obj.course ? toCourseDto(obj.course) : {} as CourseDto, cuisine: obj.cuisine ? toCuisineDto(obj.cuisine) : {} as CuisineDto, + season: obj.season ? toSeasonDto(obj.season) : {} as SeasonDto, subrecipes: obj.subrecipes?.map(toSubRecipeDto) ?? [], ingredient_groups: toIngredientGroupsDto(obj), diff --git a/src/recipe/tests/data.ts b/src/recipe/tests/data.ts index 41b49199..0e33b23a 100644 --- a/src/recipe/tests/data.ts +++ b/src/recipe/tests/data.ts @@ -1 +1 @@ -export default { id:1,photo:null,photo_thumbnail:null,ingredients:[{ id:1,title:'black pepper',quantity:'1',measurement:'dash',recipe:1 },{ id:2,title:'chili powder',quantity:'4',measurement:'tablespoons',recipe:1 },{ id:3,title:'cumin',quantity:'1',measurement:'tablespoon',recipe:1 },{ id:4,title:'dark kidney beans',quantity:'1',measurement:'can',recipe:1 },{ id:5,title:'diced tomatos',quantity:'2',measurement:'cans',recipe:1 },{ id:6,title:'green bell pepper',quantity:'1',measurement:'whole',recipe:1 },{ id:7,title:'ground pork',quantity:'1',measurement:'pound',recipe:1 },{ id:8,title:'ground sirloin',quantity:'1',measurement:'pound',recipe:1 },{ id:9,title:'kosher salt',quantity:'1',measurement:'dash',recipe:1 },{ id:10,title:'light kidney beans',quantity:'1',measurement:'can',recipe:1 },{ id:11,title:'serrano pepper ',quantity:'1',measurement:'whole',recipe:1 },{ id:12,title:'white onion',quantity:'1',measurement:'whole',recipe:1 }],ingredient_groups:[{ title:'Chili', ingredients:[{ id:1,title:'black pepper',quantity:'1',measurement:'dash',recipe:1 }] },{ title:'Spices', ingredients:[{ id:1,title:'black pepper',quantity:'1',measurement:'dash',recipe:1 }] },{ title:'', ingredients:[{ id:1,title:'black pepper',quantity:'1',measurement:'dash',recipe:1 }] }],subrecipes:[{ child_recipe_id:1,title:'black pepper',quantity:'1',measurement:'dash',recipe:1 },{ child_recipe_id:2,title:'chili powder',quantity:'4',measurement:'tablespoons',recipe:1 }],directions:'Brown the ground pork and ground sirlion in a medium pan. Add a teaspoon of sereano pepper while browning the meat. Season with kosher salt and pepper.\\nChop the onion, bell pepper and one Serrano pepper and place them in a large pot.\\nOpen up and drain both cans of kidney beans and add them to the large pot.\\nOpen up both cans of stewed chili style tomatoes and add them to the pot.\\nDrain the grease away from the browned meat and add the meat to the pot.\\nPour in the tomato juice over the meat mixture.\\nAdd kosher salt, black pepper, two table spoons of chili powder, and two teaspoons of ground cumin. Stir well.\\nCook slowly over medium low heat for an hour. If it starts to bubble turn down the heat. Taste during the cooking process to check the seasoning add more to taste.',tags:[],pub_username:'ryan',title:'Tasty Chili',info:'This chili is requested every winter by friends and family. I have been making this chili every since I was a small child and learned from my grandma',source:'',prep_time:60,cook_time:60,servings:8,rating:3,pub_date:'May 21, 2011',update_date:'May 21, 2011',author:1,cuisine:1,course:2 }; +export default { id:1,photo:null,photo_thumbnail:null,ingredients:[{ id:1,title:'black pepper',quantity:'1',measurement:'dash',recipe:1 },{ id:2,title:'chili powder',quantity:'4',measurement:'tablespoons',recipe:1 },{ id:3,title:'cumin',quantity:'1',measurement:'tablespoon',recipe:1 },{ id:4,title:'dark kidney beans',quantity:'1',measurement:'can',recipe:1 },{ id:5,title:'diced tomatos',quantity:'2',measurement:'cans',recipe:1 },{ id:6,title:'green bell pepper',quantity:'1',measurement:'whole',recipe:1 },{ id:7,title:'ground pork',quantity:'1',measurement:'pound',recipe:1 },{ id:8,title:'ground sirloin',quantity:'1',measurement:'pound',recipe:1 },{ id:9,title:'kosher salt',quantity:'1',measurement:'dash',recipe:1 },{ id:10,title:'light kidney beans',quantity:'1',measurement:'can',recipe:1 },{ id:11,title:'serrano pepper ',quantity:'1',measurement:'whole',recipe:1 },{ id:12,title:'white onion',quantity:'1',measurement:'whole',recipe:1 }],ingredient_groups:[{ title:'Chili', ingredients:[{ id:1,title:'black pepper',quantity:'1',measurement:'dash',recipe:1 }] },{ title:'Spices', ingredients:[{ id:1,title:'black pepper',quantity:'1',measurement:'dash',recipe:1 }] },{ title:'', ingredients:[{ id:1,title:'black pepper',quantity:'1',measurement:'dash',recipe:1 }] }],subrecipes:[{ child_recipe_id:1,title:'black pepper',quantity:'1',measurement:'dash',recipe:1 },{ child_recipe_id:2,title:'chili powder',quantity:'4',measurement:'tablespoons',recipe:1 }],directions:'Brown the ground pork and ground sirlion in a medium pan. Add a teaspoon of sereano pepper while browning the meat. Season with kosher salt and pepper.\\nChop the onion, bell pepper and one Serrano pepper and place them in a large pot.\\nOpen up and drain both cans of kidney beans and add them to the large pot.\\nOpen up both cans of stewed chili style tomatoes and add them to the pot.\\nDrain the grease away from the browned meat and add the meat to the pot.\\nPour in the tomato juice over the meat mixture.\\nAdd kosher salt, black pepper, two table spoons of chili powder, and two teaspoons of ground cumin. Stir well.\\nCook slowly over medium low heat for an hour. If it starts to bubble turn down the heat. Taste during the cooking process to check the seasoning add more to taste.',tags:[],pub_username:'ryan',title:'Tasty Chili',info:'This chili is requested every winter by friends and family. I have been making this chili every since I was a small child and learned from my grandma',source:'',prep_time:60,cook_time:60,servings:8,rating:3,pub_date:'May 21, 2011',update_date:'May 21, 2011',author:1,cuisine:1,course:2,season:3 }; diff --git a/src/recipe_form/components/RecipeForm.tsx b/src/recipe_form/components/RecipeForm.tsx index e8f05fe5..91dce053 100644 --- a/src/recipe_form/components/RecipeForm.tsx +++ b/src/recipe_form/components/RecipeForm.tsx @@ -10,6 +10,7 @@ import { Recipe } from '../../recipe/store/RecipeTypes'; import TagListContainer from '../containers/TagListContainer'; import CourseSelectContainer from '../containers/CourseSelectContainer'; import CuisineSelectContainer from '../containers/CuisineSelectContainer'; +import SeasonSelectContainer from '../containers/SeasonSelectContainer'; import RecipeFormToolbar, { SubmittingObserver, SubmittingObserverClass } from '../containers/RecipeFormToolbar'; import ReInput from '../../common/components/ReduxForm/ReInput'; import ReCheckbox from '../../common/components/ReduxForm/ReCheckbox'; @@ -38,6 +39,11 @@ const messages = defineMessages({ description: 'Cuisine label', defaultMessage: 'Cuisine', }, + season_label: { + id: 'recipe.create.season_label', + description: 'Season label', + defaultMessage: 'Season', + }, tags_label: { id: 'recipe.create.tags_label', description: 'Tags label', @@ -164,6 +170,23 @@ const RecipeForm: React.FC = ({ + + + + + + + + + = ({ label = {formatMessage(messages.cooking_time_label)} /> - - - - - = ({ + name, label }: ISeasonSelectContainerProps) => { + const intl = useIntl(); + const dispatch = useDispatch(); + + const fetchSeasons = useCallback(() => { dispatch(RecipeGroupActions.fetchSeasons()); }, []); + const seasons = useSelector((state: RootState) => state.recipeGroups.seasons.items); + useSingle(fetchSeasons, seasons); + + const data = useMemo(() => seasons + ?.map(c => ({ value: c.title, label: optionallyFormatMessage(intl, 'season.', c.title) })), [seasons, intl.locale]); + + const parser = useCallback((newValue: string | undefined): Season | undefined => { + if (newValue == null) { + return undefined; + } else { + return seasons?.find(c => c.title === newValue) ?? { title: newValue } as Season; + } + }, [seasons]); + + const formatter = useCallback((value: Array | Season): Array | string => ( + _.castArray(value).map(v => v.title) + ), []); + + return ( + + ); +}; + +export default SeasonSelectContainer; diff --git a/src/recipe_form/store/actions.ts b/src/recipe_form/store/actions.ts index 87c65bfa..8c508ca1 100644 --- a/src/recipe_form/store/actions.ts +++ b/src/recipe_form/store/actions.ts @@ -5,7 +5,7 @@ import { AnyDispatch, toBasicAction } from '../../common/store/redux'; import { AutocompleteListItem } from '../../common/components/Input/TextareaAutocomplete'; import { handleError, handleFormError } from '../../common/requestUtils'; import { Recipe, RecipeDto, toRecipe, toRecipeRequest } from '../../recipe/store/RecipeTypes'; -import { COURSES_STORE, CUISINES_STORE, TAGS_STORE } from '../../recipe_groups/store/types'; +import { COURSES_STORE, CUISINES_STORE, SEASONS_STORE, TAGS_STORE } from '../../recipe_groups/store/types'; import { getRecipeSuccess } from '../../recipe/store/RecipeActions'; import { RecipeFormDispatch, RECIPE_FORM_STORE } from './types'; @@ -100,6 +100,9 @@ export const invalidateCreatableLists = (oldRecipe: Recipe, savedRecipe: Recipe) if (oldRecipe.cuisine?.id !== savedRecipe.cuisine?.id) { dispatch({ ...toBasicAction(CUISINES_STORE, ACTION.RESET) }); } + if (oldRecipe.season?.id !== savedRecipe.season?.id) { + dispatch({ ...toBasicAction(SEASONS_STORE, ACTION.RESET) }); + } if (oldRecipe.tags?.map(t => t.id).join('/') !== savedRecipe.tags?.map(t => t.id).join('/')) { dispatch({ ...toBasicAction(TAGS_STORE, ACTION.RESET) }); } diff --git a/src/recipe_groups/store/actions.ts b/src/recipe_groups/store/actions.ts index 7790eb8e..dc90908c 100644 --- a/src/recipe_groups/store/actions.ts +++ b/src/recipe_groups/store/actions.ts @@ -1,10 +1,10 @@ import request from '../../common/CustomSuperagent'; import { serverURLs } from '../../common/config'; import { ACTION } from '../../common/store/ReduxHelper'; -import { CourseDto, toCourse, CuisineDto, toCuisine, toTag } from '../../recipe/store/RecipeTypes'; +import { CourseDto, toCourse, CuisineDto, toCuisine, SeasonDto, toSeason, toTag } from '../../recipe/store/RecipeTypes'; import { toBasicAction } from '../../common/store/redux'; import { handleError } from '../../common/requestUtils'; -import { COURSES_STORE, CUISINES_STORE, RecipeGroupsDispatch, TAGS_STORE } from './types'; +import { COURSES_STORE, CUISINES_STORE, RecipeGroupsDispatch, SEASONS_STORE, TAGS_STORE } from './types'; export const fetchCourses = () => (dispatch: RecipeGroupsDispatch) => { dispatch({ ...toBasicAction(COURSES_STORE, ACTION.GET_START) }); @@ -44,6 +44,25 @@ export const fetchCuisines = () => (dispatch: RecipeGroupsDispatch) => { .catch(err => dispatch(handleError(err, CUISINES_STORE))); }; +export const fetchSeasons = () => (dispatch: RecipeGroupsDispatch) => { + dispatch({ ...toBasicAction(SEASONS_STORE, ACTION.GET_START) }); + + request() + .get(serverURLs.season) + .then(res => { + dispatch({ + ...toBasicAction( + SEASONS_STORE, + ACTION.GET_SUCCESS + ), + payload: res.body.results + .filter((seasonDto: SeasonDto) => seasonDto.title !== '-') + .map(toSeason), + }); + }) + .catch(err => dispatch(handleError(err, CUISINES_STORE))); +}; + export const fetchTags = () => (dispatch: RecipeGroupsDispatch) => { dispatch({ ...toBasicAction(TAGS_STORE, ACTION.GET_START) }); diff --git a/src/recipe_groups/store/reducer.ts b/src/recipe_groups/store/reducer.ts index b55f7058..4046db79 100644 --- a/src/recipe_groups/store/reducer.ts +++ b/src/recipe_groups/store/reducer.ts @@ -1,11 +1,12 @@ import { combineReducers, Reducer } from 'redux'; import ReduxHelper from '../../common/store/ReduxHelper'; -import { Course, Cuisine, Tag } from '../../recipe/store/RecipeTypes'; -import { COURSES_STORE, CUISINES_STORE, RecipeGroupsAction, RecipeGroupsState, TAGS_STORE } from './types'; +import { Course, Cuisine, Season, Tag } from '../../recipe/store/RecipeTypes'; +import { COURSES_STORE, CUISINES_STORE, RecipeGroupsAction, RecipeGroupsState, SEASONS_STORE, TAGS_STORE } from './types'; const defaultCourseState = ReduxHelper.getArrayReducerDefaultState>(COURSES_STORE); const defaultCuisineState = ReduxHelper.getArrayReducerDefaultState>(CUISINES_STORE); +const defaultSeasonState = ReduxHelper.getArrayReducerDefaultState>(SEASONS_STORE); const defaultTagsState = ReduxHelper.getArrayReducerDefaultState>(TAGS_STORE); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -18,6 +19,7 @@ function createRecipeGroupsWithNamedType(defaultState: any): Reducer = combineReducers({ courses: createRecipeGroupsWithNamedType(defaultCourseState), cuisines: createRecipeGroupsWithNamedType(defaultCuisineState), + seasons: createRecipeGroupsWithNamedType(defaultSeasonState), tags: createRecipeGroupsWithNamedType(defaultTagsState), }); diff --git a/src/recipe_groups/store/types.ts b/src/recipe_groups/store/types.ts index edcb1d4e..65a270f9 100644 --- a/src/recipe_groups/store/types.ts +++ b/src/recipe_groups/store/types.ts @@ -2,22 +2,25 @@ import { Dispatch as ReduxDispatch } from 'redux'; import ArrayReducerType from '../../common/store/ArrayReducerType'; import { GenericArrayReducerAction } from '../../common/store/ReduxHelper'; -import { Course, Cuisine, Tag } from '../../recipe/store/RecipeTypes'; +import { Course, Cuisine, Season, Tag } from '../../recipe/store/RecipeTypes'; export const RECIPE_GROUPS_STORE = 'recipeGroups'; export const COURSES_STORE = 'courses'; export const CUISINES_STORE = 'cuisines'; +export const SEASONS_STORE = 'seasons'; export const TAGS_STORE = 'tags'; export type CoursesState = ArrayReducerType; export type CuisinesState = ArrayReducerType; +export type SeasonsState = ArrayReducerType; export type TagsState = ArrayReducerType; -export type RecipeGroupsAction = GenericArrayReducerAction; +export type RecipeGroupsAction = GenericArrayReducerAction; export type RecipeGroupsDispatch = ReduxDispatch; export interface RecipeGroupsState { [COURSES_STORE]: CoursesState, [CUISINES_STORE]: CuisinesState, + [SEASONS_STORE]: SeasonsState, [TAGS_STORE]: TagsState, }