From f54d49372e10b06c3638222ae09cd32ea19475a8 Mon Sep 17 00:00:00 2001 From: Onikiienko Bogdan Date: Mon, 14 Nov 2022 23:13:00 +0200 Subject: [PATCH] feat: support items filtering by tags on UI --- .gitignore | 1 + dist_scripts/src/model.js | 22 ++++- package-lock.json | 83 +++++++++++++++++ package.json | 2 + src/components/App.tsx | 31 ++++--- src/components/Header/Header.tsx | 58 +++++++++++- src/components/ItemTags/ItemTags.tsx | 17 ++++ src/components/Link/Link.tsx | 12 ++- src/components/PageItem/PageItem.tsx | 2 + .../PageItemMobile/PageItemMobile.tsx | 2 + src/components/TagsModal/TagsModal.tsx | 90 +++++++++++++++++++ src/components/TagsModal/tags-modal.scss | 44 +++++++++ src/hooks/use-search-param-state.tsx | 71 +++++++++++++++ src/icons/filter.svg | 7 ++ src/model.ts | 26 ++++++ src/styles/components/icon.scss | 4 + 16 files changed, 457 insertions(+), 15 deletions(-) create mode 100644 src/components/ItemTags/ItemTags.tsx create mode 100644 src/components/TagsModal/TagsModal.tsx create mode 100644 src/components/TagsModal/tags-modal.scss create mode 100644 src/hooks/use-search-param-state.tsx create mode 100644 src/icons/filter.svg diff --git a/.gitignore b/.gitignore index dc443cd..7ee6aee 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ .env.development.local .env.test.local .env.production.local +.vscode npm-debug.log* yarn-debug.log* diff --git a/dist_scripts/src/model.js b/dist_scripts/src/model.js index 07eabbc..9b4e122 100644 --- a/dist_scripts/src/model.js +++ b/dist_scripts/src/model.js @@ -20,7 +20,7 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { return to.concat(ar || Array.prototype.slice.call(from)); }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getFirstLetter = exports.groupByFirstLetter = exports.groupByQuadrants = exports.nonFeaturedOnly = exports.featuredOnly = exports.FlagType = exports.HomepageOption = void 0; +exports.getTags = exports.filteredOnly = exports.getFirstLetter = exports.groupByFirstLetter = exports.groupByQuadrants = exports.nonFeaturedOnly = exports.featuredOnly = exports.FlagType = exports.HomepageOption = void 0; var HomepageOption; (function (HomepageOption) { HomepageOption["chart"] = "chart"; @@ -78,3 +78,23 @@ var getFirstLetter = function (item) { return item.title.substr(0, 1).toUpperCase(); }; exports.getFirstLetter = getFirstLetter; +var filteredOnly = function (items, tags) { + return items.filter(function (item) { + var itemTags = item.tags; + if (typeof itemTags === "undefined") { + return false; + } + if (Array.isArray(tags)) { + return tags.every(function (tag) { return itemTags.includes(tag); }); + } + return itemTags.includes(tags); + }); +}; +exports.filteredOnly = filteredOnly; +var getTags = function (items) { + var tags = items.reduce(function (acc, item) { + return !item.tags ? acc : acc.concat(item.tags); + }, []).sort(); + return Array.from(new Set(tags)); +}; +exports.getTags = getTags; diff --git a/package-lock.json b/package-lock.json index c89b1b9..8da6c4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@types/jest": "29.1.2", "@types/react": "18.0.21", "@types/react-dom": "18.0.6", + "@types/react-modal": "3.13.1", "@types/sanitize-html": "2.6.2", "@types/walk": "2.3.1", "classnames": "2.3.2", @@ -33,6 +34,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-icons": "4.4.0", + "react-modal": "3.16.1", "react-router-dom": "6.4.2", "react-scripts": "5.0.1", "react-tooltip": "4.3.0", @@ -4213,6 +4215,14 @@ "@types/react": "*" } }, + "node_modules/@types/react-modal": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@types/react-modal/-/react-modal-3.13.1.tgz", + "integrity": "sha512-iY/gPvTDIy6Z+37l+ibmrY+GTV4KQTHcCyR5FIytm182RQS69G5ps4PH2FxtC7bAQ2QRHXMevsBgck7IQruHNg==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -8025,6 +8035,11 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" + }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -13496,6 +13511,29 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-modal": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz", + "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==", + "dependencies": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18", + "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18" + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -15371,6 +15409,14 @@ "makeerror": "1.0.12" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/watchpack": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz", @@ -19075,6 +19121,14 @@ "@types/react": "*" } }, + "@types/react-modal": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@types/react-modal/-/react-modal-3.13.1.tgz", + "integrity": "sha512-iY/gPvTDIy6Z+37l+ibmrY+GTV4KQTHcCyR5FIytm182RQS69G5ps4PH2FxtC7bAQ2QRHXMevsBgck7IQruHNg==", + "requires": { + "@types/react": "*" + } + }, "@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -21826,6 +21880,11 @@ "strip-final-newline": "^2.0.0" } }, + "exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" + }, "exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -25662,6 +25721,22 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "react-modal": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz", + "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==", + "requires": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + } + }, "react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -27035,6 +27110,14 @@ "makeerror": "1.0.12" } }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "watchpack": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz", diff --git a/package.json b/package.json index 522975b..6a213da 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@types/jest": "29.1.2", "@types/react": "18.0.21", "@types/react-dom": "18.0.6", + "@types/react-modal": "3.13.1", "@types/sanitize-html": "2.6.2", "@types/walk": "2.3.1", "classnames": "2.3.2", @@ -47,6 +48,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-icons": "4.4.0", + "react-modal": "3.16.1", "react-router-dom": "6.4.2", "react-scripts": "5.0.1", "react-tooltip": "4.3.0", diff --git a/src/components/App.tsx b/src/components/App.tsx index 5d4b295..ceda64c 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -5,13 +5,13 @@ import { Navigate, Route, Routes, - useLocation, useParams, } from "react-router-dom"; import { ConfigData } from "../config"; import { Messages, MessagesProvider } from "../context/MessagesContext"; -import { Item } from "../model"; +import { useSearchParamState } from "../hooks/use-search-param-state"; +import { Item, filteredOnly, getTags } from "../model"; import Footer from "./Footer/Footer"; import Header from "./Header/Header"; import Router from "./Router"; @@ -33,12 +33,17 @@ const useFetch = (url: string): D | undefined => { return data; }; -const useQuery = () => new URLSearchParams(useLocation().search); - const usePage = (params: Record) => { return (params["*"] || "").replace(".html", ""); }; +const useFilteredItems = ({ items }: { items: Item[] }) => { + const [searchParamState] = useSearchParamState(); + const { tags } = searchParamState; + + return tags ? filteredOnly(items, tags) : items; +}; + const RouterWithPageParam = ({ items, releases, @@ -49,29 +54,33 @@ const RouterWithPageParam = ({ config: ConfigData; }) => { const page = usePage(useParams()); - const query = useQuery(); + const [searchParamState] = useSearchParamState(); + const { search } = searchParamState; + const filteredItems = useFilteredItems({ items }); return ( ); }; -const HeaderWithPageParam = () => { +const HeaderWithPageParam = ({ items }: { items: Item[] }) => { const page = usePage(useParams()); + const tags = getTags(items); - return
; + return
; }; const FooterWithPageParam = ({ items }: { items: Item[] }) => { const page = usePage(useParams()); + const filteredItems = useFilteredItems({ items }); - return