From 0c18eb00b587df15388c4052dfe0fbfee0b085f3 Mon Sep 17 00:00:00 2001 From: Alexandre Fauquette <45398769+alexfauquette@users.noreply.github.com> Date: Fri, 8 Jul 2022 22:58:01 +0200 Subject: [PATCH] feat: allows to save filters (#36) --- package.json | 1 + src/App.js | 3 +- src/components/QuestionCard.jsx | 51 +++++++++++++++- .../QuestionFilter/QuestionFilter.jsx | 23 +++++++- .../QuestionFilter/useFilterSearch.js | 28 ++++++++- src/localeStorageManager.js | 59 +++++++++++++++++++ src/pages/home/index.jsx | 29 +++++++++ src/pages/index.js | 1 + src/pages/questions/index.jsx | 5 +- yarn.lock | 5 ++ 10 files changed, 196 insertions(+), 9 deletions(-) create mode 100644 src/pages/home/index.jsx diff --git a/package.json b/package.json index 6b8e61b654..de8eed4917 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@mui/x-data-grid": "^5.12.2", "axios": "^0.27.2", "i18next": "^21.8.10", + "lodash.isequal": "^4.5.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^11.17.2", diff --git a/src/App.js b/src/App.js index 95e770ffa2..41fff35747 100644 --- a/src/App.js +++ b/src/App.js @@ -11,6 +11,7 @@ import { QuestionsPage, InsightsPage, NotFoundPage, + Home, } from "./pages"; import ResponsiveAppBar from "./components/ResponsiveAppBar"; @@ -24,7 +25,7 @@ export default function App() { {/* TODO: put a home page for root url */} - } /> + } /> } /> } /> } /> diff --git a/src/components/QuestionCard.jsx b/src/components/QuestionCard.jsx index 8f1493934b..b378f80fec 100644 --- a/src/components/QuestionCard.jsx +++ b/src/components/QuestionCard.jsx @@ -7,13 +7,16 @@ import CardContent from "@mui/material/CardContent"; import Typography from "@mui/material/Typography"; import Badge from "@mui/material/Badge"; import Link from "@mui/material/Link"; +import Stack from "@mui/material/Stack"; +import Chip from "@mui/material/Chip"; import robotoff from "../robotoff"; import { reformatValueTag } from "../utils"; import { getQuestionSearchParams } from "./QuestionFilter"; +import { useTranslation } from "react-i18next"; const QuestionCard = (props) => { - const { filterState, imageSrc, title } = props; + const { filterState, imageSrc, title, showFilterResume } = props; const { sortByPopularity, insightType, @@ -22,6 +25,8 @@ const QuestionCard = (props) => { countryFilter, } = filterState; + const { t } = useTranslation(); + const targetUrl = `/questions?${getQuestionSearchParams(filterState)}`; const [questionNumber, setQuestionNumber] = React.useState("?"); @@ -67,7 +72,7 @@ const QuestionCard = (props) => { : "success" } > - + { sx={{ objectFit: "contain" }} /> + {showFilterResume && ( + + {filterState?.insightType && ( + + )} + {filterState?.valueTag && ( + + )} + {filterState?.countryFilter && ( + + )} + {filterState?.brandFilter && ( + + )} + {filterState?.sortByPopularity && ( + + )} + + )} {title} diff --git a/src/components/QuestionFilter/QuestionFilter.jsx b/src/components/QuestionFilter/QuestionFilter.jsx index 7d1aa6d43d..84b7218220 100644 --- a/src/components/QuestionFilter/QuestionFilter.jsx +++ b/src/components/QuestionFilter/QuestionFilter.jsx @@ -13,14 +13,22 @@ import RadioGroup from "@mui/material/RadioGroup"; import FormControlLabel from "@mui/material/FormControlLabel"; import Box from "@mui/material/Box"; import Radio from "@mui/material/Radio"; +import IconButton from "@mui/material/IconButton"; import EditIcon from "@mui/icons-material/Edit"; +import StarIcon from "@mui/icons-material/Star"; +import StarBorderIcon from "@mui/icons-material/StarBorder"; import { useTranslation } from "react-i18next"; import brands from "../../assets/brands.json"; import { countryNames, insightTypesNames } from "./const"; -export const QuestionFilter = ({ filterState, setFilterState }) => { +export const QuestionFilter = ({ + filterState, + setFilterState, + isFavorite, + toggleFavorite, +}) => { const { t } = useTranslation(); const [isOpen, setIsOpen] = React.useState(false); @@ -104,7 +112,7 @@ export const QuestionFilter = ({ filterState, setFilterState }) => { {/* Chip indicating the current state of the filtering */} - + { }} /> )} + { + toggleFavorite(); + }} + > + {isFavorite ? ( + + ) : ( + + )} + diff --git a/src/components/QuestionFilter/useFilterSearch.js b/src/components/QuestionFilter/useFilterSearch.js index 90ef8a7ba4..0c56885921 100644 --- a/src/components/QuestionFilter/useFilterSearch.js +++ b/src/components/QuestionFilter/useFilterSearch.js @@ -2,6 +2,7 @@ import * as React from "react"; import { key2urlParam, DEFAULT_FILTER_STATE } from "./const"; import { useLocation } from "react-router-dom"; +import { localFavorites } from "../../localeStorageManager"; export const getQuestionSearchParams = (filterState) => { const urlParams = new URLSearchParams(window.location.search); @@ -40,8 +41,12 @@ const getSearchFromUrl = () => { export function useFilterSearch() { const { search } = useLocation(); - const [searchParams, setInternSearchParams] = React.useState(() => - getSearchFromUrl() + const initialParams = getSearchFromUrl(); + const [searchParams, setInternSearchParams] = React.useState( + () => initialParams + ); + const [isFavorite, setIsFavorite] = React.useState( + localFavorites.isSaved(initialParams) ); React.useEffect(() => { @@ -63,10 +68,27 @@ export function useFilterSearch() { return; } + setIsFavorite(localFavorites.isSaved(newState)); setInternSearchParams(newState); updateSearchSearchParams(newState); }, [searchParams] ); - return [searchParams, setSearchParams]; + + const toggleFavorite = React.useCallback( + (imageSrc = "", title = "") => { + const isSaved = localFavorites.isSaved(searchParams); + + if (isSaved) { + localFavorites.removeQuestion(searchParams); + } else { + localFavorites.addQuestion(searchParams, imageSrc, title); + } + + setIsFavorite(!isSaved); + }, + [searchParams] + ); + + return [searchParams, setSearchParams, isFavorite, toggleFavorite]; } diff --git a/src/localeStorageManager.js b/src/localeStorageManager.js index 15758d5fc7..b7086ed04d 100644 --- a/src/localeStorageManager.js +++ b/src/localeStorageManager.js @@ -1,3 +1,5 @@ +import isEqual from "lodash.isequal"; + const STORAGE_KEY = "hunger-game-settings"; export const localSettings = { @@ -26,3 +28,60 @@ export const getLang = () => { (navigator.language || navigator.userLanguage).split("-")[0] ); }; + +const FAVORITE_STORAGE_KEY = "hunger-game-favorites"; + +export const localFavorites = { + mem: null, + fetch: function () { + return JSON.parse(localStorage.getItem(FAVORITE_STORAGE_KEY) || "{}"); + }, + save: function (favorites) { + localStorage.setItem(FAVORITE_STORAGE_KEY, JSON.stringify(favorites)); + }, + addQuestion: function (filterState, imageSrc, title) { + if (this.mem == null) { + this.mem = this.fetch(); + } + if (!this.mem.questions) { + this.mem.questions = []; + } + const questionIndex = this.mem.questions.indexOf( + ({ filterState: memFilterState }) => isEqual(memFilterState, filterState) + ); + if (questionIndex < 0) { + this.mem.questions.push({ filterState, imageSrc, title }); + } else { + this.mem.questions[questionIndex] = { filterState, imageSrc, title }; + } + console.log(this.mem); + this.save(this.mem); + }, + removeQuestion: function (filterState) { + if (this.mem == null) { + this.mem = this.fetch(); + } + if (!this.mem.questions) { + return; + } + this.mem.questions = this.mem.questions.filter( + ({ filterState: memFilterState }) => !isEqual(memFilterState, filterState) + ); + this.save(this.mem); + }, + isSaved: function (filterState) { + if (this.mem == null) { + this.mem = this.fetch(); + } + + if (!this.mem.questions) { + return false; + } + + return ( + this.mem.questions.filter(({ filterState: memFilterState }) => + isEqual(memFilterState, filterState) + ).length > 0 + ); + }, +}; diff --git a/src/pages/home/index.jsx b/src/pages/home/index.jsx new file mode 100644 index 0000000000..a5510984fe --- /dev/null +++ b/src/pages/home/index.jsx @@ -0,0 +1,29 @@ +import * as React from "react"; + +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; + +import QuestionCard from "../../components/QuestionCard"; +import { localFavorites } from "../../localeStorageManager"; + +const Home = () => { + const [savedQuestions] = React.useState(() => { + return localFavorites.fetch().questions ?? []; + }); + + return ( + + Home + + {savedQuestions.map((props) => ( + + + + ))} + + + ); +}; + +export default Home; diff --git a/src/pages/index.js b/src/pages/index.js index 7ae1da2b10..296830073a 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -2,6 +2,7 @@ export { default as EcoScorePage } from "./eco-score"; export { default as SettingsPage } from "./settings"; export { default as QuestionsPage } from "./questions"; export { default as InsightsPage } from "./insights"; +export { default as Home } from "./home"; export { default as NotFoundPage } from "./not-found"; export { default as LogoAnnotationPage } from "./logos/LogoAnnotation"; diff --git a/src/pages/questions/index.jsx b/src/pages/questions/index.jsx index e9908f3f4e..12b1c01371 100644 --- a/src/pages/questions/index.jsx +++ b/src/pages/questions/index.jsx @@ -12,7 +12,8 @@ import Stack from "@mui/material/Stack"; import Grid from "@mui/material/Grid"; export default function Questions() { - const [filterState, setFilterState] = useFilterSearch(); + const [filterState, setFilterState, isFavorite, toggleFavorite] = + useFilterSearch(); const { buffer, answerQuestion, remainingQuestionNb, answers } = useQuestionBuffer(filterState); @@ -25,6 +26,8 @@ export default function Questions() {