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() {