From 7bf0309808c56214ede111ca8203ae5e8bad6de4 Mon Sep 17 00:00:00 2001 From: Nabil Ahmed Date: Wed, 22 Oct 2025 22:06:12 -0300 Subject: [PATCH 1/7] Add favorites section to the individual meal plan page --- .../src/pages/MealPlans/Favorites.tsx | 118 ++++++++ .../src/pages/MealPlans/MealPlan.tsx | 2 + .../FavoritesMealPlanQuery.graphql.ts | 274 ++++++++++++++++++ 3 files changed, 394 insertions(+) create mode 100644 mealplanner-ui/src/pages/MealPlans/Favorites.tsx create mode 100644 mealplanner-ui/src/pages/MealPlans/__generated__/FavoritesMealPlanQuery.graphql.ts diff --git a/mealplanner-ui/src/pages/MealPlans/Favorites.tsx b/mealplanner-ui/src/pages/MealPlans/Favorites.tsx new file mode 100644 index 00000000..38720a8c --- /dev/null +++ b/mealplanner-ui/src/pages/MealPlans/Favorites.tsx @@ -0,0 +1,118 @@ +import { + Box, + Button, + Typography, + useTheme, +} from "@mui/material"; +import React from "react"; +import { useRefetchableFragment, useLazyLoadQuery } from "react-relay"; +import { getCurrentPerson, clearSelectedMeal, setSelectedMeal } from "../../state/state"; +import { FavoriteMealsFragment } from "../Meals/PersonFavoriteMeals"; +import { graphql } from "babel-plugin-relay/macro"; +import { FavoritesMealPlanQuery } from "./__generated__/FavoritesMealPlanQuery.graphql"; + +export const Favorites: React.FC = () => { + const theme = useTheme(); + const slug = getCurrentPerson().personSlug; + const combinedQuery = graphql` + query FavoritesMealPlanQuery($slug: String!) { + gqLocalState { + selectedMeal { + nameEn + rowId + id + } + } + ...PersonFavoriteMeals_favorites @arguments(slug: $slug) + } + `; + + const data = useLazyLoadQuery( + combinedQuery, + { slug: slug }, + { fetchPolicy: "store-or-network" } + ); + const [meals] = useRefetchableFragment(FavoriteMealsFragment, data as any); + const favMeals = meals.people?.nodes[0].favoriteMeals.nodes; + const sortedFavMeals = (favMeals || []).slice().sort((a: any, b: any) => { + const aName = (a?.meal?.nameEn || "").toLowerCase(); + const bName = (b?.meal?.nameEn || "").toLowerCase(); + if (aName < bName) return -1; + if (aName > bName) return 1; + return 0; + }); + + return ( + +
+ + + Favorites + + + {data.gqLocalState?.selectedMeal?.nameEn ? ( + + {data.gqLocalState.selectedMeal.nameEn} + + + + ) : null} + + + {sortedFavMeals.map((fav: any) => { + const meal = fav.meal; + return ( + + ); + })} + + +
+
+ ); +}; \ No newline at end of file diff --git a/mealplanner-ui/src/pages/MealPlans/MealPlan.tsx b/mealplanner-ui/src/pages/MealPlans/MealPlan.tsx index c94666c6..13524455 100644 --- a/mealplanner-ui/src/pages/MealPlans/MealPlan.tsx +++ b/mealplanner-ui/src/pages/MealPlans/MealPlan.tsx @@ -6,6 +6,7 @@ import { useParams } from "react-router-dom"; import { Calendar } from "./Calendar"; import { MealPlanHeader } from "./MealPlanHeader"; import { SearchMeal } from "./SearchMeal"; +import { Favorites } from "./Favorites"; import { MealPlanQuery } from "./__generated__/MealPlanQuery.graphql"; /* Meal plan query */ @@ -60,6 +61,7 @@ export const MealPlan = () => {
+ diff --git a/mealplanner-ui/src/pages/MealPlans/__generated__/FavoritesMealPlanQuery.graphql.ts b/mealplanner-ui/src/pages/MealPlans/__generated__/FavoritesMealPlanQuery.graphql.ts new file mode 100644 index 00000000..3c2d9502 --- /dev/null +++ b/mealplanner-ui/src/pages/MealPlans/__generated__/FavoritesMealPlanQuery.graphql.ts @@ -0,0 +1,274 @@ +/** + * @generated SignedSource<<1ce334861d2916529459e623749a8005>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest, Query } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type FavoritesMealPlanQuery$variables = { + slug: string; +}; +export type FavoritesMealPlanQuery$data = { + readonly gqLocalState: { + readonly selectedMeal: { + readonly nameEn: string; + readonly rowId: any; + readonly id: string; + } | null; + }; + readonly " $fragmentSpreads": FragmentRefs<"PersonFavoriteMeals_favorites">; +}; +export type FavoritesMealPlanQuery = { + variables: FavoritesMealPlanQuery$variables; + response: FavoritesMealPlanQuery$data; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "slug" + } +], +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "nameEn", + "storageKey": null +}, +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "rowId", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v4 = { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "GQLocalState", + "kind": "LinkedField", + "name": "gqLocalState", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "SelectedMeal", + "kind": "LinkedField", + "name": "selectedMeal", + "plural": false, + "selections": [ + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/) + ], + "storageKey": null + } + ], + "storageKey": null + } + ] +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "FavoritesMealPlanQuery", + "selections": [ + { + "args": [ + { + "kind": "Variable", + "name": "slug", + "variableName": "slug" + } + ], + "kind": "FragmentSpread", + "name": "PersonFavoriteMeals_favorites" + }, + (v4/*: any*/) + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "FavoritesMealPlanQuery", + "selections": [ + { + "alias": null, + "args": [ + { + "fields": [ + { + "fields": [ + { + "kind": "Variable", + "name": "equalTo", + "variableName": "slug" + } + ], + "kind": "ObjectValue", + "name": "slug" + } + ], + "kind": "ObjectValue", + "name": "filter" + }, + { + "kind": "Literal", + "name": "first", + "value": 1 + } + ], + "concreteType": "PeopleConnection", + "kind": "LinkedField", + "name": "people", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Person", + "kind": "LinkedField", + "name": "nodes", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "FavoriteMealsConnection", + "kind": "LinkedField", + "name": "favoriteMeals", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "FavoriteMeal", + "kind": "LinkedField", + "name": "nodes", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Meal", + "kind": "LinkedField", + "name": "meal", + "plural": false, + "selections": [ + (v2/*: any*/), + (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "nameFr", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "descriptionEn", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "descriptionFr", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "categories", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "tags", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "code", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "photoUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "videoUrl", + "storageKey": null + }, + (v3/*: any*/) + ], + "storageKey": null + }, + (v3/*: any*/) + ], + "storageKey": null + } + ], + "storageKey": null + }, + (v3/*: any*/) + ], + "storageKey": null + } + ], + "storageKey": null + }, + (v4/*: any*/) + ] + }, + "params": { + "cacheID": "0a5ae228b61b0ceb2f36799537d7d73c", + "id": null, + "metadata": {}, + "name": "FavoritesMealPlanQuery", + "operationKind": "query", + "text": "query FavoritesMealPlanQuery(\n $slug: String!\n) {\n ...PersonFavoriteMeals_favorites_20J5Pl\n}\n\nfragment PersonFavoriteMeals_favorites_20J5Pl on Query {\n people(filter: {slug: {equalTo: $slug}}, first: 1) {\n nodes {\n favoriteMeals {\n nodes {\n meal {\n rowId\n nameEn\n nameFr\n descriptionEn\n descriptionFr\n categories\n tags\n code\n photoUrl\n videoUrl\n id\n }\n id\n }\n }\n id\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "694be900db8a05d29cdbb0c8f994727f"; + +export default node; From bcb4ac63fe405d1c3d1050563fb8f0f8647eb8c9 Mon Sep 17 00:00:00 2001 From: Nabil Ahmed Date: Thu, 23 Oct 2025 11:13:50 -0300 Subject: [PATCH 2/7] Add search bar to favorites section --- .../src/pages/MealPlans/Favorites.tsx | 88 ++++++++++++++++--- 1 file changed, 78 insertions(+), 10 deletions(-) diff --git a/mealplanner-ui/src/pages/MealPlans/Favorites.tsx b/mealplanner-ui/src/pages/MealPlans/Favorites.tsx index 38720a8c..f0f6ebdd 100644 --- a/mealplanner-ui/src/pages/MealPlans/Favorites.tsx +++ b/mealplanner-ui/src/pages/MealPlans/Favorites.tsx @@ -1,10 +1,15 @@ +import SearchIcon from "@mui/icons-material/Search"; import { Box, Button, + FormControl, + InputAdornment, + InputLabel, + OutlinedInput, Typography, useTheme, } from "@mui/material"; -import React from "react"; +import React, { useState } from "react"; import { useRefetchableFragment, useLazyLoadQuery } from "react-relay"; import { getCurrentPerson, clearSelectedMeal, setSelectedMeal } from "../../state/state"; import { FavoriteMealsFragment } from "../Meals/PersonFavoriteMeals"; @@ -33,14 +38,29 @@ export const Favorites: React.FC = () => { { fetchPolicy: "store-or-network" } ); const [meals] = useRefetchableFragment(FavoriteMealsFragment, data as any); - const favMeals = meals.people?.nodes[0].favoriteMeals.nodes; - const sortedFavMeals = (favMeals || []).slice().sort((a: any, b: any) => { - const aName = (a?.meal?.nameEn || "").toLowerCase(); - const bName = (b?.meal?.nameEn || "").toLowerCase(); - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - }); + const favMeals = meals.people?.nodes[0].favoriteMeals.nodes || []; + + let [searchText, setSearchText] = useState(""); + let search = (searchText: string) => { + const mapped = favMeals + .map((f: any) => f.meal) + .filter((m: any) => m != null); + + let sortedMeals = mapped.slice().sort((a: any, b: any) => { + const aName = (a?.nameEn || "").toLowerCase(); + const bName = (b?.nameEn || "").toLowerCase(); + if (aName < bName) return -1; + if (aName > bName) return 1; + return 0; + }); + + if (searchText === "") { + return sortedMeals; + } + return sortedMeals.filter((m: any) => + (m.nameEn || "").match(new RegExp(searchText, "i")) + ); + }; return ( @@ -67,6 +87,31 @@ export const Favorites: React.FC = () => { Favorites + + + Search for meals + + + + + } + value={searchText} + onChange={(e) => { + setSearchText(e.target.value); + }} + /> + + {data.gqLocalState?.selectedMeal?.nameEn ? ( {data.gqLocalState.selectedMeal.nameEn} @@ -87,7 +132,7 @@ export const Favorites: React.FC = () => { ) : null} - {sortedFavMeals.map((fav: any) => { + {/* {sortedFavMeals.map((fav: any) => { const meal = fav.meal; return ( ); + })} */} + { + search(searchText).map((m: any) => { + return ( + + ); })} From e730c04f05c538ba150147ba09ba8a78281f25ca Mon Sep 17 00:00:00 2001 From: Nabil Ahmed Date: Mon, 27 Oct 2025 12:53:36 -0300 Subject: [PATCH 3/7] Add filter by tag option in favorites section of individual meal plan --- .../src/pages/MealPlans/Favorites.tsx | 131 ++++++++++-------- 1 file changed, 76 insertions(+), 55 deletions(-) diff --git a/mealplanner-ui/src/pages/MealPlans/Favorites.tsx b/mealplanner-ui/src/pages/MealPlans/Favorites.tsx index f0f6ebdd..fd59bad9 100644 --- a/mealplanner-ui/src/pages/MealPlans/Favorites.tsx +++ b/mealplanner-ui/src/pages/MealPlans/Favorites.tsx @@ -5,7 +5,9 @@ import { FormControl, InputAdornment, InputLabel, + MenuItem, OutlinedInput, + Select, Typography, useTheme, } from "@mui/material"; @@ -39,9 +41,16 @@ export const Favorites: React.FC = () => { ); const [meals] = useRefetchableFragment(FavoriteMealsFragment, data as any); const favMeals = meals.people?.nodes[0].favoriteMeals.nodes || []; + const favMealTags: string[] = Array.from( + new Set( + (favMeals || []).flatMap((f: any) => f?.meal?.tags || []) + ) + ); let [searchText, setSearchText] = useState(""); - let search = (searchText: string) => { + let [selectedTag, setSelectedTag] = useState(""); + + let search = (searchText: string, tag: string) => { const mapped = favMeals .map((f: any) => f.meal) .filter((m: any) => m != null); @@ -53,13 +62,20 @@ export const Favorites: React.FC = () => { if (aName > bName) return 1; return 0; }); - - if (searchText === "") { - return sortedMeals; + + if (searchText !== "") { + sortedMeals = sortedMeals.filter((m: any) => + (m.nameEn || "").match(new RegExp(searchText, "i")) + ); + } + + if (tag !== "") { + sortedMeals = sortedMeals.filter((m: any) => + (m.tags || []).includes(tag) + ); } - return sortedMeals.filter((m: any) => - (m.nameEn || "").match(new RegExp(searchText, "i")) - ); + + return sortedMeals; }; return ( @@ -87,30 +103,58 @@ export const Favorites: React.FC = () => { Favorites - - - Search for meals - - - - - } - value={searchText} - onChange={(e) => { - setSearchText(e.target.value); - }} - /> - + + + + Search for meals + + + + + } + value={searchText} + onChange={(e) => { + setSearchText(e.target.value); + }} + /> + + + + + Filter by tag + + + + {data.gqLocalState?.selectedMeal?.nameEn ? ( @@ -132,31 +176,8 @@ export const Favorites: React.FC = () => { ) : null} - {/* {sortedFavMeals.map((fav: any) => { - const meal = fav.meal; - return ( - - ); - })} */} { - search(searchText).map((m: any) => { + search(searchText, selectedTag).map((m: any) => { return (