diff --git a/package-lock.json b/package-lock.json index 9f27cbb9b7b6cc84f3e54cdc2360fda1b501d6c5..8e0de9943e8605cc9c3bcd93f398c3618b0ec57a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,13 +11,13 @@ "dependencies": { "@headlessui/react": "^2", "@headlessui/tailwindcss": "^0.2.2", - "jotai": "^2.10.3", + "jotai": "^2.12.1", "next": "^15", "next-intl": "^3", "react": "^18", "react-dom": "^18", "react-paginate": "^8", - "sharp": "^0.33", + "sharp": "^0.33.5", "zod": "^3" }, "devDependencies": { @@ -601,9 +601,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.0.tgz", + "integrity": "sha512-RoV8Xs9eNwiDvhv7M+xcL4PWyRyIXRY/FLp3buU4h1EYfdF7unWUy3dOjPqb3C7rMUewIcqwW850PgS8h1o1yg==", "dev": true, "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -1989,12 +1989,12 @@ } }, "node_modules/@react-aria/focus": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.0.tgz", - "integrity": "sha512-KXZCwWzwnmtUo6xhnyV26ptxlxmqd0Reez7axduqqqeDDgDZOVscoo/5gFg71fdPZmnDC8MyUK1vxSbMhOTrGg==", + "version": "3.20.1", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.1.tgz", + "integrity": "sha512-lgYs+sQ1TtBrAXnAdRBQrBo0/7o5H6IrfDxec1j+VRpcXL0xyk0xPq+m3lZp8typzIghqDgpnKkJ5Jf4OrzPIw==", "dependencies": { - "@react-aria/interactions": "^3.24.0", - "@react-aria/utils": "^3.28.0", + "@react-aria/interactions": "^3.24.1", + "@react-aria/utils": "^3.28.1", "@react-types/shared": "^3.28.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" @@ -2005,12 +2005,12 @@ } }, "node_modules/@react-aria/interactions": { - "version": "3.24.0", - "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.24.0.tgz", - "integrity": "sha512-6Zdhp1pswyPgbwEWzvXARdKAWPjP7mACczoIUvlEQiMsX04fuizBiBLAA+W/5mPe17pbJYHA/rxZF5Y5m+M0Ng==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.24.1.tgz", + "integrity": "sha512-OWEcIC6UQfWq4Td5Ptuh4PZQ4LHLJr/JL2jGYvuNL6EgL3bWvzPrRYIF/R64YbfVxIC7FeZpPSkS07sZ93/NoA==", "dependencies": { "@react-aria/ssr": "^3.9.7", - "@react-aria/utils": "^3.28.0", + "@react-aria/utils": "^3.28.1", "@react-stately/flags": "^3.1.0", "@react-types/shared": "^3.28.0", "@swc/helpers": "^0.5.0" @@ -2035,9 +2035,9 @@ } }, "node_modules/@react-aria/utils": { - "version": "3.28.0", - "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.28.0.tgz", - "integrity": "sha512-FfpvpADk61OvEnFe37k6jF1zr5gtafIPN9ccJRnPCTqrzuExag01mGi+wX/hWyFK0zAe1OjWf1zFOX3FsFvikg==", + "version": "3.28.1", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.28.1.tgz", + "integrity": "sha512-mnHFF4YOVu9BRFQ1SZSKfPhg3z+lBRYoW5mLcYTQihbKhz48+I1sqRkP7ahMITr8ANH3nb34YaMME4XWmK2Mgg==", "dependencies": { "@react-aria/ssr": "^3.9.7", "@react-stately/flags": "^3.1.0", @@ -2085,9 +2085,9 @@ "dev": true }, "node_modules/@rushstack/eslint-patch": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.5.tgz", - "integrity": "sha512-kkKUDVlII2DQiKy7UstOR1ErJP8kUKAQ4oa+SQtM0K+lPdmmjj0YnnxBgtTVYH7mUKtbsxeFC9y0AmK7Yb78/A==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.11.0.tgz", + "integrity": "sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==", "dev": true }, "node_modules/@sinclair/typebox": { @@ -2489,16 +2489,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.0.tgz", - "integrity": "sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q==", + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz", + "integrity": "sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.26.0", - "@typescript-eslint/type-utils": "8.26.0", - "@typescript-eslint/utils": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0", + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/type-utils": "8.26.1", + "@typescript-eslint/utils": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -2518,15 +2518,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.0.tgz", - "integrity": "sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA==", + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.1.tgz", + "integrity": "sha512-w6HZUV4NWxqd8BdeFf81t07d7/YV9s7TCWrQQbG5uhuvGUAW+fq1usZ1Hmz9UPNLniFnD8GLSsDpjP0hm1S4lQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.26.0", - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/typescript-estree": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0", + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/typescript-estree": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", "debug": "^4.3.4" }, "engines": { @@ -2542,13 +2542,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.0.tgz", - "integrity": "sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==", + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.1.tgz", + "integrity": "sha512-6EIvbE5cNER8sqBu6V7+KeMZIC1664d2Yjt+B9EWUXrsyWpxx4lEZrmvxgSKRC6gX+efDL/UY9OpPZ267io3mg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0" + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2559,13 +2559,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.0.tgz", - "integrity": "sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==", + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.1.tgz", + "integrity": "sha512-Kcj/TagJLwoY/5w9JGEFV0dclQdyqw9+VMndxOJKtoFSjfZhLXhYjzsQEeyza03rwHx2vFEGvrJWJBXKleRvZg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "8.26.0", - "@typescript-eslint/utils": "8.26.0", + "@typescript-eslint/typescript-estree": "8.26.1", + "@typescript-eslint/utils": "8.26.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -2582,9 +2582,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.0.tgz", - "integrity": "sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==", + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.1.tgz", + "integrity": "sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2595,13 +2595,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.0.tgz", - "integrity": "sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==", + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.1.tgz", + "integrity": "sha512-yUwPpUHDgdrv1QJ7YQal3cMVBGWfnuCdKbXw1yyjArax3353rEJP1ZA+4F8nOlQ3RfS2hUN/wze3nlY+ZOhvoA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0", + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2673,15 +2673,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.0.tgz", - "integrity": "sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==", + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.1.tgz", + "integrity": "sha512-V4Urxa/XtSUroUrnI7q6yUTD3hDtfJ2jzVfeT3VK0ciizfK2q/zGC0iDh1lFMUZR8cImRrep6/q0xd/1ZGPQpg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.26.0", - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/typescript-estree": "8.26.0" + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/typescript-estree": "8.26.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2696,12 +2696,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.0.tgz", - "integrity": "sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==", + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.1.tgz", + "integrity": "sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.26.0", + "@typescript-eslint/types": "8.26.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -4521,9 +4521,9 @@ } }, "node_modules/eslint-import-resolver-typescript": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.8.3.tgz", - "integrity": "sha512-A0bu4Ks2QqDWNpeEgTQMPTngaMhuDu4yv6xpftBMAf+1ziXnpx+eSR1WRfoPTe2BAiAjHFZ7kSNx1fvr5g5pmQ==", + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.8.4.tgz", + "integrity": "sha512-vjTGvhr528DzCOLQnBxvoB9a2YuzegT1ogfrUwOqMXS/J6vNYQKSHDJxxDVU1gRuTiUK8N2wyp8Uik9JSPAygA==", "dev": true, "dependencies": { "@nolyfill/is-core-module": "1.0.39", @@ -7430,9 +7430,9 @@ } }, "node_modules/jotai": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.12.1.tgz", - "integrity": "sha512-VUW0nMPYIru5g89tdxwr9ftiVdc/nGV9jvHISN8Ucx+m1vI9dBeHemfqYzEuw5XSkmYjD/MEyApN9k6yrATsZQ==", + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.12.2.tgz", + "integrity": "sha512-oN8715y7MkjXlSrpyjlR887TOuc/NLZMs9gvgtfWH/JP47ChwO0lR2ijSwBvPMYyXRAPT+liIAhuBavluKGgtA==", "engines": { "node": ">=12.20.0" }, diff --git a/package.json b/package.json index 32f87676171214b205a04d1ae5cda9e35c49e4f3..3435c6016566f03e539bbe03fbbb5e309375f200 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "dependencies": { "@headlessui/react": "^2", "@headlessui/tailwindcss": "^0.2.2", - "jotai": "^2.10.3", + "jotai": "^2.12.1", "next": "^15", "next-intl": "^3", "react": "^18", diff --git a/src/app/[locale]/globals.css b/src/app/[locale]/globals.css index a22dd0deebcc2ee1d2a8abe159189079a6fb59ad..0d132dc096d6f695e13956cd16b318b0218f2031 100644 --- a/src/app/[locale]/globals.css +++ b/src/app/[locale]/globals.css @@ -9,4 +9,7 @@ #smallScreenSearch { @apply m-10 p-10 rounded-lg drop-shadow bg-left-bottom border-2 lg:hidden; } + #smallScreenDetailsSearch { + @apply mb-10 p-10 lg:hidden; + } } diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 6e66d1831731fb952d629e57b051b0b6dc52333f..591c7094bb12c6427dfc5c69d181b67a7421f54c 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -7,6 +7,7 @@ import React from "react"; import Banner from "@/components/layout/Banner"; import Footer from "@/components/layout/Footer"; import Header from "@/components/layout/Header"; +import { CategoryProvider } from "@/contexts/CategoryContext"; import { routing } from "@/i18n/routing"; const env = process.env.NEXT_PUBLIC_SHOW_BANNER; @@ -58,11 +59,14 @@ export default function RootLayout({ <noscript>{t("js_needed")}</noscript> <main className="flex flex-col"> <NextIntlClientProvider locale={locale} messages={messages}> - <header className="sticky top-0 w-full border-b-2 border-b-secondary-helmholtz-mint shadow-lg bg-primary-helmholtz-weiss py-5 "> - {showBanner && <Banner />} - <Header /> - </header> - <div className="grow flex flex-col">{children}</div> + <CategoryProvider> + <header className="sticky top-0 w-full border-b-2 border-b-secondary-helmholtz-mint shadow-lg bg-primary-helmholtz-weiss py-5 "> + {showBanner && <Banner />} + <Header /> + </header> + + <div className="grow flex flex-col">{children}</div> + </CategoryProvider> <div className="sticky bottom-0"> <Footer /> </div> diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx index 746a9fa9d0c9cdc4ebc74c04d7d735bc0499d1a7..7f701eedbf5cb2a886188e259203a2959f89e436 100644 --- a/src/app/[locale]/page.tsx +++ b/src/app/[locale]/page.tsx @@ -2,7 +2,7 @@ import Categories from "@/components/app/Categories"; import Intro from "@/components/app/Intro"; -import MobileSearch from "@/components/layout/MobileSearch"; +import MobileSearch from "@/components/layout/search/MobileSearch"; const Home = () => { return ( diff --git a/src/app/[locale]/results/details/page.tsx b/src/app/[locale]/results/details/page.tsx index 7411f9e7732d45d66ea371d0ee468a2e00243e79..443d6f7867d6500cda18aeb5b1bb93084170dc49 100644 --- a/src/app/[locale]/results/details/page.tsx +++ b/src/app/[locale]/results/details/page.tsx @@ -3,11 +3,13 @@ import React, { Suspense } from "react"; import BackBar from "@/components/app/results/BackBar"; import DetailsItemCard from "@/components/app/results/Details/DetailsItemCard"; import RelatedResults from "@/components/app/results/Details/RelatedResults"; +import MobileSearch from "@/components/layout/search/MobileSearch"; const DetailsComponent = () => { return ( <div className="flex flex-col md:px-10 lg:px-16 py-5 mb-8 bg-whitesmoke h-full"> <BackBar /> + <MobileSearch /> <DetailsItemCard /> <RelatedResults /> </div> diff --git a/src/app/[locale]/results/page.tsx b/src/app/[locale]/results/page.tsx index cd67949bf048b5d6c4bf927d363e67b75dcfdb76..a2e24e636dd934ac40641f677775c98ae833491f 100644 --- a/src/app/[locale]/results/page.tsx +++ b/src/app/[locale]/results/page.tsx @@ -1,56 +1,13 @@ "use client"; -import React, { Suspense, useState } from "react"; +import React, { Suspense } from "react"; -import BackBar from "@/components/app/results/BackBar"; -import CategoryBar from "@/components/app/results/CategoryBar"; -import ListResults from "@/components/app/results/ListResults"; -import Error from "@/components/layout/Error"; -import MobileSearch from "@/components/layout/MobileSearch"; -import Spinner from "@/components/layout/Spinner"; -import type { Category } from "@/types/types"; +import CategorizedResults from "@/components/app/results/CategorizedResults"; const ResultsComponent = () => { - const [hasBcError, setHasBcError] = useState(false); - const [hasSearchError, setHasSearchError] = useState(false); - const [categoryLoading, setCategoryLoading] = useState(true); - const [resultsLoading, setResultsLoading] = useState(true); - const [activeCategory, setActiveCategory] = useState<Category>(); - return ( <div className="grow flex flex-col justify-start"> - <> - <CategoryBar - activeCategory={activeCategory} - categoryLoading={categoryLoading} - setCategoryLoading={setCategoryLoading} - hasBcError={hasBcError} - hasSearchError={hasSearchError} - setActiveCategory={setActiveCategory} - setHasBcError={setHasBcError} - setHasSearchError={setHasSearchError} - inDetailsView={false} - /> - <div className="flex flex-col md:px-10 lg:px-16 py-5 mb-8 bg-whitesmoke h-full"> - {!categoryLoading && !resultsLoading ? ( - <> - <BackBar /> - <MobileSearch /> - </> - ) : null} - - {activeCategory && ( - <ListResults - activeCategory={activeCategory} - resultsLoading={resultsLoading} - setResultsLoading={setResultsLoading} - /> - )} - </div> - </> - - {(categoryLoading || resultsLoading) && <Spinner />} - {(hasBcError || hasSearchError) && <Error />} + <CategorizedResults /> </div> ); }; diff --git a/src/components/app/Categories.tsx b/src/components/app/Categories.tsx index 20d40bfb52912cb46ba86167d5a77e00bacb90ea..75ebe1f784b40de4684eb6e48eb39f971151b653 100644 --- a/src/components/app/Categories.tsx +++ b/src/components/app/Categories.tsx @@ -2,46 +2,17 @@ import Image from "next/image"; import { useTranslations } from "next-intl"; -import { useEffect, useState } from "react"; +import { useContext } from "react"; import Error from "@/components/layout/Error"; import Spinner from "@/components/layout/Spinner"; +import { CategoryContext } from "@/contexts/CategoryContext"; import { Link } from "@/i18n/routing"; -import type { Category } from "@/types/types"; -import { getAllCategories } from "@/utils/apiCall"; const Categories = () => { const t = useTranslations("Categories"); - const [allCategories, setCategories] = useState<Category[]>([]); - const [isLoading, setIsLoading] = useState<boolean>(true); - const [hasError, setHasError] = useState<boolean>(false); - - useEffect(() => { - const updateCategories = async () => { - try { - setIsLoading(true); - setHasError(false); - const categories = await getAllCategories(); - for (const category of categories) { - let iconImport; - - try { - iconImport = await import("@/resources/images/category/" + category.id + ".png"); - } catch { - iconImport = await import("@/resources/images/category/software.png"); - } - - category.icon = iconImport.default; - } - setCategories(categories); - setIsLoading(false); - } catch { - setIsLoading(false); - setHasError(true); - } - }; - updateCategories(); - }, []); + const { categoryIsLoading, categoryHasError, defaultCategories, setPrimaryActiveCategory } = + useContext(CategoryContext); const formatter = Intl.NumberFormat("en", { notation: "compact" }); /* traditional JS module that makes the badge numbers appear as strings representing exponents @@ -49,9 +20,9 @@ const Categories = () => { return ( <div className="grow flex flex-col"> - {isLoading ? ( + {categoryIsLoading ? ( <Spinner /> - ) : hasError ? ( + ) : categoryHasError ? ( <Error /> ) : ( <div className="grow container mx-auto flex flex-col justify-center pb-16 md:p-14 md:pt-2 lg:p-20 lg:pt-10 xl:pt-10 xl:pb-20 2xl:pb-16 3xl:py-0"> @@ -60,12 +31,13 @@ const Categories = () => { </div> {/* grid of categories */} <div className="px-10 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 3xl:grid-cols-5 gap-y-28 md:gap-x-16 lg:gap-x-32 xl:gap-x-46 content-center justify-center"> - {allCategories.map((o) => { + {defaultCategories.map((o) => { return ( <Link key={o.id} href={`/results?category=${o.id}`} className="group flex place-content-center transition ease-in-out hover:-translate-y-1 hover:scale-110" + onClick={() => setPrimaryActiveCategory(o)} > {/* icon for category */} <Image diff --git a/src/components/app/results/CategorizedResults.tsx b/src/components/app/results/CategorizedResults.tsx new file mode 100644 index 0000000000000000000000000000000000000000..06947b0109279fc08c215c601ab19236413977d2 --- /dev/null +++ b/src/components/app/results/CategorizedResults.tsx @@ -0,0 +1,104 @@ +"use client"; +import { usePathname, useSearchParams } from "next/navigation"; +import React, { useState, useContext, useEffect, Suspense } from "react"; + +import BackBar from "@/components/app/results/BackBar"; +import CategoryBar from "@/components/app/results/CategoryBar"; +import ListResults from "@/components/app/results/ListResults"; +import Error from "@/components/layout/Error"; +import Spinner from "@/components/layout/Spinner"; +import MobileSearch from "@/components/layout/search/MobileSearch"; +import { CategoryContext } from "@/contexts/CategoryContext"; +import { Category } from "@/types/types"; + +const CategorizedResultsComponent = () => { + const pathName = usePathname(); + const resultParams = useSearchParams(); + const paramsCategory = resultParams.get("category") ?? undefined; + const [relatedActiveCategory, setRelatedActiveCategory] = useState<Category | null>(null); + const [inDetailsView, setInDetailsView] = useState<boolean>(false); + + const { + categoryIsLoading, + categoryHasError, + categories, + primaryActiveCategory, + setPrimaryActiveCategory, + } = useContext(CategoryContext); + + // Figuring out which view is calling this component + useEffect(() => { + const pathParams = pathName.split(/(\/)+/g); + const hasResults = pathParams.includes("results"); + const hasDetails = pathParams.includes("details"); + + if (hasResults && hasDetails) { + setInDetailsView(true); + } else if (hasResults && !hasDetails) { + setInDetailsView(false); + } else { + setInDetailsView(false); + } + }, [pathName]); + + useEffect(() => { + if (!inDetailsView && paramsCategory) { + const chosenCategory = categories.filter((o) => o.id === paramsCategory)[0]; + setPrimaryActiveCategory(chosenCategory); + } else if (!inDetailsView && !primaryActiveCategory) { + setPrimaryActiveCategory(categories[0]); + } + }, [inDetailsView, primaryActiveCategory, paramsCategory, categories, setPrimaryActiveCategory]); + + useEffect(() => { + if (inDetailsView && !relatedActiveCategory) { + setRelatedActiveCategory(categories[0]); + } + }, [inDetailsView, relatedActiveCategory, categories]); + + return ( + <> + {categoryIsLoading || primaryActiveCategory === null ? ( + <Spinner /> + ) : categoryHasError ? ( + <Error /> + ) : ( + <> + {inDetailsView && relatedActiveCategory !== null ? ( + <> + <CategoryBar + activeCategory={relatedActiveCategory} + inDetailsView={inDetailsView} + setRelatedActiveCategory={setRelatedActiveCategory} + /> + <ListResults + activeCategory={relatedActiveCategory} + inDetailsView={inDetailsView} + setRelatedActiveCategory={setRelatedActiveCategory} + /> + </> + ) : ( + <> + <CategoryBar activeCategory={primaryActiveCategory} /> + <div className="flex flex-col md:px-10 lg:px-16 py-5 mb-8 bg-whitesmoke h-full"> + <> + <BackBar /> + <MobileSearch /> + </> + <ListResults activeCategory={primaryActiveCategory} /> + </div> + </> + )} + </> + )} + </> + ); +}; + +export default function CategorizedResults() { + return ( + <Suspense> + <CategorizedResultsComponent /> + </Suspense> + ); +} diff --git a/src/components/app/results/CategoryBar.tsx b/src/components/app/results/CategoryBar.tsx index c58ed4338c54bca82ea56652ec7c4fefbb72b373..4ee6d1c09e007ee74278be8da7266553327ebdad 100644 --- a/src/components/app/results/CategoryBar.tsx +++ b/src/components/app/results/CategoryBar.tsx @@ -2,39 +2,27 @@ import { useSearchParams } from "next/navigation"; import { useTranslations } from "next-intl"; -import { Dispatch, SetStateAction, Suspense, useEffect, useState, useRef } from "react"; +import { SetStateAction, Suspense, useContext, Dispatch, useEffect } from "react"; +import { CategoryContext } from "@/contexts/CategoryContext"; import { Link } from "@/i18n/routing"; import type { Category } from "@/types/types"; -import { getAllCategories, getCategoryCounts } from "@/utils/apiCall"; +import { getCategoryCounts } from "@/utils/apiCall"; type Props = { activeCategory?: Category; - categoryLoading: boolean; - hasBcError: boolean; - hasSearchError: boolean; inDetailsView?: boolean; - setActiveCategory: Dispatch<SetStateAction<Category | undefined>>; - setHasBcError: Dispatch<SetStateAction<boolean>>; - setHasSearchError: Dispatch<SetStateAction<boolean>>; - setCategoryLoading: Dispatch<SetStateAction<boolean>>; + setRelatedActiveCategory?: Dispatch<SetStateAction<Category | null>>; }; const CategoryBarComponent = ({ activeCategory, - categoryLoading, - hasBcError, - hasSearchError, inDetailsView, - setActiveCategory, - setCategoryLoading, - setHasBcError, - setHasSearchError, + setRelatedActiveCategory, }: Props) => { const c = useTranslations("Categories"); const resultParams = useSearchParams(); - const currentlyActivecategory = resultParams.get("category") ?? undefined; const searchText = resultParams.get("searchText") ?? undefined; const detailsText = resultParams.get("detailsText") ?? undefined; const details = detailsText ? `&detailsText=${detailsText}` : ""; @@ -42,112 +30,74 @@ const CategoryBarComponent = ({ const formatter = Intl.NumberFormat("en", { notation: "compact" }); - const [categories, setCategories] = useState<Category[]>([]); - const [baseCategories, setBaseCategories] = useState<Category[]>([]); - - const firstRender = useRef(true); - - useEffect(() => { - const loadCategories = async () => { - try { - const allCategories = await getAllCategories(); - if (inDetailsView && currentlyActivecategory) { - setBaseCategories(allCategories.filter((cat) => cat.id !== currentlyActivecategory)); - } else { - setBaseCategories(allCategories); - } - } catch { - setHasBcError(true); - } - }; - - loadCategories().catch(console.error); - }, [inDetailsView, currentlyActivecategory, setHasBcError]); - - useEffect(() => { - if (!activeCategory && categories.length) { - setActiveCategory(categories[0]); - } - }, [activeCategory, categories, setActiveCategory]); - - useEffect(() => { - categories.forEach((cat: Category) => - cat.id === currentlyActivecategory ? setActiveCategory(cat) : null - ); - inDetailsView && setActiveCategory(categories[0]); - }, [currentlyActivecategory, categories, inDetailsView, setActiveCategory]); + const { + categories, + defaultCategories, + primaryActiveCategory, + setCategories, + setPrimaryActiveCategory, + } = useContext(CategoryContext); useEffect(() => { - const updateCategoryCount = async () => { + const fetchAndUpdateCategoryBarCount = async () => { try { - setHasSearchError(false); const updatedSearchText = searchText || detailsText ? (searchText ? searchText : "") + " " + (detailsText ? detailsText : "") : undefined; const categoryCounts = await getCategoryCounts(updatedSearchText); - const categories = structuredClone(baseCategories); + const updatedCategories = structuredClone(defaultCategories); - categories.forEach((cat: Category) => (cat.count = categoryCounts[cat.id] ?? 0)); - setCategories(categories); + updatedCategories.forEach((cat: Category) => (cat.count = categoryCounts[cat.id] ?? 0)); + setCategories(updatedCategories); } catch (error) { - console.error("Failed to fetch category counts", error); - setHasSearchError(true); - } finally { - setCategoryLoading(false); - // console.log("here", categories); + // TODO: How to handle error in case of API failure in this effect? + console.error("Failed to fetch category counts for categorybar", error); } }; - if (firstRender.current) { - firstRender.current = false; - } else { - updateCategoryCount().catch(console.error); + if (defaultCategories.length !== 0) { + fetchAndUpdateCategoryBarCount(); } - }, [ - baseCategories, - searchText, - detailsText, - setCategoryLoading, - setHasSearchError, - setHasBcError, - ]); + }, [searchText, detailsText, defaultCategories, setCategories]); return ( <> - {categoryLoading || hasBcError || hasSearchError ? null : ( - <div className="bg-primary-helmholtz-dunkelblau text-primary-helmholtz-weiss grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:flex gap-y-2 gap-x-2 xl:gap-x-6 px-4 pt-2 pb-3 md:px-10 lg:px-16 xl:pt-4 xl:min-h-24"> - {categories.map((o, idx) => ( - <Link - href={ - inDetailsView - ? `/results/details?&category=${currentlyActivecategory}${search}${details}` - : `/results?category=${o.id}${search}` - } - shallow - replace={!!(inDetailsView && activeCategory?.id !== currentlyActivecategory)} - onClick={() => setActiveCategory(o)} - key={idx} - className={`${ - o.id === activeCategory?.id - ? "font-semibold underline decoration-secondary-helmholtz-mint underline-offset-[20px]" - : "text-blue-100 hover:rounded-full hover:bg-white/[0.12] hover:font-medium hover:py-3 hover:px-2" - } flex place-content-center cursor-pointer text-md w-full flex-1 py-2 font-normal text-primary-helmholtz-weiss `} - > - <div className="xl:basis-1/4 2xl:basis-1/5 mr-2 "> - {/* count indicator for category */} - <span className="inline-flex h-11 w-11 place-content-center rounded-full bg-primary-helmholtz-hellblau object-contain align-middle"> - <span className="self-center font-medium text-primary-helmholtz-weiss"> - {formatter.format(o.count)} - </span> + <div className="bg-primary-helmholtz-dunkelblau text-primary-helmholtz-weiss grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:flex gap-y-2 gap-x-2 xl:gap-x-6 px-4 pt-2 pb-3 md:px-10 lg:px-16 xl:pt-4 xl:min-h-24"> + {categories.map((o, idx) => ( + <Link + href={ + inDetailsView + ? `/results/details?&category=${primaryActiveCategory?.id}${search}${details}` + : `/results?category=${o.id}${search}` + } + shallow + replace={!!(inDetailsView && activeCategory?.id !== primaryActiveCategory?.id)} + onClick={() => + inDetailsView && setRelatedActiveCategory + ? setRelatedActiveCategory(o) + : setPrimaryActiveCategory(o) + } + key={idx} + className={`${ + o.id === activeCategory?.id + ? "font-semibold underline decoration-secondary-helmholtz-mint underline-offset-[20px]" + : "text-blue-100 hover:rounded-full hover:bg-white/[0.12] hover:font-medium hover:py-3 hover:px-2" + } flex place-content-center cursor-pointer text-md w-full flex-1 py-2 font-normal text-primary-helmholtz-weiss `} + > + <div className="xl:basis-1/4 2xl:basis-1/5 mr-2 "> + {/* count indicator for category */} + <span className="inline-flex h-11 w-11 place-content-center rounded-full bg-primary-helmholtz-hellblau object-contain align-middle"> + <span className="self-center font-medium text-primary-helmholtz-weiss"> + {formatter.format(o.count)} </span> - </div> - <div className="basis-2/3 self-center text-left">{c(o.id, { count: o.count })}</div> - </Link> - ))} - </div> - )} + </span> + </div> + <div className="basis-2/3 self-center text-left">{c(o.id, { count: o.count })}</div> + </Link> + ))} + </div> </> ); }; diff --git a/src/components/app/results/Details/DetailsItemCard.tsx b/src/components/app/results/Details/DetailsItemCard.tsx index 56ab0cd525ac3359defa42ae354c54d717a524f6..2e5266c7728e559adbf3bac938826e2b585f92c1 100644 --- a/src/components/app/results/Details/DetailsItemCard.tsx +++ b/src/components/app/results/Details/DetailsItemCard.tsx @@ -24,7 +24,7 @@ const DetailsItemCard = () => { <> {isResultItem(detailsItem) ? ( <div className="p-4"> - <ResultItem item={detailsItem} idx={detailsItem.id} inDetailsPage /> + <ResultItem item={detailsItem} idx={detailsItem.id} isDetailsCard /> </div> ) : null} </> diff --git a/src/components/app/results/Details/RelatedResults.tsx b/src/components/app/results/Details/RelatedResults.tsx index 451acf2ef30a2e15c0179f41191c70fa3d2850f9..586d1480cca3a33b2209031d0f43c5cd6f3ba316 100644 --- a/src/components/app/results/Details/RelatedResults.tsx +++ b/src/components/app/results/Details/RelatedResults.tsx @@ -1,51 +1,16 @@ -"use client"; import { useTranslations } from "next-intl"; -import React, { useState } from "react"; -import CategoryBar from "@/components/app/results/CategoryBar"; -import ListResults from "@/components/app/results/ListResults"; -import Error from "@/components/layout/Error"; -import Spinner from "@/components/layout/Spinner"; -import { Category } from "@/types/types"; +import CategorizedResults from "@/components/app/results/CategorizedResults"; const RelatedResults = () => { const d = useTranslations("DetailsView"); - const [categoryLoading, setCategoryLoading] = useState(true); - const [resultsLoading, setResultsLoading] = useState(true); - const [hasBcError, setHasBcError] = useState(false); - const [hasSearchError, setHasSearchError] = useState(false); - const [activeCategory, setActiveCategory] = useState<Category>(); return ( <div className="m-4 space-y-2 rounded-xl border-2 border-primary-helmholtz-hellblau/20 px-5 py-4 shadow-lg shadow-secondary-helmholtz-highlightblau bg-white"> - <> - {!categoryLoading && !resultsLoading ? ( - <p className="text-sm font-semibold text-primary-helmholtz-dunkelblau cursor-default"> - {d("other_items")} - </p> - ) : null} - <CategoryBar - activeCategory={activeCategory} - categoryLoading={categoryLoading} - setCategoryLoading={setCategoryLoading} - hasBcError={hasBcError} - hasSearchError={hasSearchError} - setActiveCategory={setActiveCategory} - setHasBcError={setHasBcError} - setHasSearchError={setHasSearchError} - inDetailsView - /> - {activeCategory && ( - <ListResults - activeCategory={activeCategory} - resultsLoading={resultsLoading} - setResultsLoading={setResultsLoading} - /> - )} - </> - - {(categoryLoading || resultsLoading) ?? <Spinner />} - {(hasBcError || hasSearchError) ?? <Error />} + <p className="text-sm font-semibold text-primary-helmholtz-dunkelblau cursor-default"> + {d("other_items")} + </p> + <CategorizedResults /> </div> ); }; diff --git a/src/components/app/results/ListResults.tsx b/src/components/app/results/ListResults.tsx index d58ca94dbd3389adbdae91376e611c0efa3f45d1..cf37ff336655d10dd88e06873515f1748b0b9da9 100644 --- a/src/components/app/results/ListResults.tsx +++ b/src/components/app/results/ListResults.tsx @@ -2,7 +2,7 @@ import { useSetAtom } from "jotai"; import { useSearchParams } from "next/navigation"; import { useTranslations } from "next-intl"; -import React, { Suspense, useEffect, useState, Dispatch, SetStateAction } from "react"; +import React, { Dispatch, SetStateAction, Suspense, useEffect, useState } from "react"; import Filters from "@/components/app/results/ListResults/Filters"; import NoResults from "@/components/app/results/ListResults/NoResults"; @@ -10,29 +10,36 @@ import Pagination from "@/components/app/results/ListResults/Pagination"; import ResultItem from "@/components/app/results/ListResults/ResultItem"; import SelectedFiltersBar from "@/components/app/results/ListResults/SelectedFiltersBar"; import Error from "@/components/layout/Error"; +import Spinner from "@/components/layout/Spinner"; import type { Category, FilterEntity, ResultItem as ResultItemType } from "@/types/types"; import { search, getRorInfo } from "@/utils/apiCall"; import { detailsItemAtom } from "@/utils/atoms"; type Props = { activeCategory: Category; - resultsLoading: boolean; - setResultsLoading: Dispatch<SetStateAction<boolean>>; + inDetailsView?: boolean; + setRelatedActiveCategory?: Dispatch<SetStateAction<Category | null>>; }; -const ListResultsComponent = ({ activeCategory, resultsLoading, setResultsLoading }: Props) => { +const ListResultsComponent = ({ + activeCategory, + inDetailsView, + setRelatedActiveCategory, +}: Props) => { const t = useTranslations("ListResults"); const s = useTranslations("Categories"); const setDetailsItem = useSetAtom(detailsItemAtom); - const [hasError, setHasError] = useState(false); const [start, setStart] = useState(0); const [results, setResults] = useState<ResultItemType[]>([]); const [searchCount, setSearchCount] = useState(0); const [availableFilters, setAvailableFilters] = useState<FilterEntity[]>([]); const [selectedFilters, setSelectedFilters] = useState<FilterEntity[]>([]); + const [isLoading, setIsLoading] = useState(false); + const [hasError, setHasError] = useState(false); + const resultParams = useSearchParams(); const searchText = resultParams.get("searchText") ?? undefined; const detailsText = resultParams.get("detailsText") ?? undefined; @@ -72,6 +79,7 @@ const ListResultsComponent = ({ activeCategory, resultsLoading, setResultsLoadin useEffect(() => { const updateResultList = async () => { + setIsLoading(true); const updatedSearchText = searchText || detailsText ? (searchText ? searchText : "") + " " + (detailsText ? detailsText : "") @@ -95,28 +103,23 @@ const ListResultsComponent = ({ activeCategory, resultsLoading, setResultsLoadin } setSearchCount(searchResult.totalCount); setAvailableFilters(searchResult.filters); + setHasError(false); } catch (error) { console.log("Error on updating result list", error); setHasError(true); } finally { - setResultsLoading(false); + setIsLoading(false); } }; updateResultList().catch(console.error); - }, [ - activeCategory, - searchText, - detailsText, - start, - resultsPerPage, - selectedFilters, - setResultsLoading, - ]); + }, [activeCategory, searchText, detailsText, start, resultsPerPage, selectedFilters]); return ( <> - {resultsLoading ? null : hasError ? ( + {isLoading ? ( + <Spinner /> + ) : hasError ? ( <Error /> ) : ( <div @@ -147,7 +150,7 @@ const ListResultsComponent = ({ activeCategory, resultsLoading, setResultsLoadin > <div className="py-5 text-primary-helmholtz-dunkelblau underline"> {t("result_count", { - count: activeCategory.count, + count: searchCount, category: s(activeCategory.id, { count: activeCategory.count }), })} </div> @@ -171,6 +174,8 @@ const ListResultsComponent = ({ activeCategory, resultsLoading, setResultsLoadin activeCategory={activeCategory} searchText={searchText} associatedToFilters={availableFilters.length !== 0} + inDetailsView={inDetailsView} + setRelatedActiveCategory={setRelatedActiveCategory} /> )) ) : ( diff --git a/src/components/app/results/ListResults/ResultItem.tsx b/src/components/app/results/ListResults/ResultItem.tsx index c63b21c63ac7feb9c6c655f748b1a65e8bca120c..aa125d21d887835373339351dcadb710d320ba6d 100644 --- a/src/components/app/results/ListResults/ResultItem.tsx +++ b/src/components/app/results/ListResults/ResultItem.tsx @@ -4,11 +4,12 @@ import { useSetAtom } from "jotai"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState, useContext, Dispatch, SetStateAction } from "react"; import { INSTITUTE_ROR_LOGOS } from "@/app/api/config/config"; import ExternalLink from "@/components/ExternalLink"; import Relationships from "@/components/app/results/ListResults/ResultItem/Relationships"; +import { CategoryContext } from "@/contexts/CategoryContext"; import { Link } from "@/i18n/routing"; import ExternalLinkIcon from "@/resources/images/svg/ExternalLinkIcon"; import RightArrowIcon from "@/resources/images/svg/RightArrowIcon"; @@ -20,8 +21,10 @@ type Props = { idx: string; searchText?: string; activeCategory?: Category; - inDetailsPage?: boolean; associatedToFilters?: boolean; + inDetailsView?: boolean; + isDetailsCard?: boolean; + setRelatedActiveCategory?: Dispatch<SetStateAction<Category | null>>; }; const ResultItem = ({ @@ -29,8 +32,10 @@ const ResultItem = ({ idx, searchText, activeCategory, - inDetailsPage, associatedToFilters, + isDetailsCard, + inDetailsView, + setRelatedActiveCategory, }: Props) => { const setDetailsItem = useSetAtom(detailsItemAtom); const router = useRouter(); @@ -43,6 +48,8 @@ const ResultItem = ({ const [titleIconHref, setTitleIconHref] = useState<string>(""); const [logoHref, setLogoHref] = useState<string>(""); + const { setPrimaryActiveCategory } = useContext(CategoryContext); + useEffect(() => { const element = descRef.current; if (element && element.offsetHeight && element.scrollHeight) { @@ -126,6 +133,14 @@ const ResultItem = ({ const handleDetailSearch = () => { setDetailsItem((prev) => [...prev, ...[item]]); + + if (inDetailsView && activeCategory && setRelatedActiveCategory) { + // setting currently chosen relatedActiveCategory, which in this case is the activeCategory, as the primaryActiveCategory + setPrimaryActiveCategory(activeCategory); + // cleaning the slate on relatedActiveCategory + setRelatedActiveCategory(null); + } + router.push( `/results/details?category=${activeCategory?.id}${searchText ? `&searchText=${searchText}` : ``}&detailsText=${item.name}` ); @@ -133,9 +148,9 @@ const ResultItem = ({ return ( <div - className={`flex my-2 space-y-2 rounded-xl border-2 border-primary-helmholtz-hellblau/20 p-5 shadow-lg shadow-secondary-helmholtz-highlightblau bg-white size-auto ${inDetailsPage && logoHref !== "" ? "xl:gap-x-10" : "xl:flex-col"}`} + className={`flex my-2 space-y-2 rounded-xl border-2 border-primary-helmholtz-hellblau/20 p-5 shadow-lg shadow-secondary-helmholtz-highlightblau bg-white size-auto ${isDetailsCard && logoHref !== "" ? "xl:gap-x-10" : "xl:flex-col"}`} > - {inDetailsPage && logoHref !== "" ? ( + {isDetailsCard && logoHref !== "" ? ( <div className="hidden xl:space-y-2 xl:p-5 xl:mt-2 xl:basis-1/3 xl:flex"> <Image src={logoHref} @@ -149,12 +164,12 @@ const ResultItem = ({ <div key={idx} - className={`flex flex-col my-2 space-y-2 p-5 ${inDetailsPage && logoHref !== "" ? "xl:basis-2/3" : ""} `} + className={`flex flex-col my-2 space-y-2 p-5 ${isDetailsCard && logoHref !== "" ? "xl:basis-2/3" : ""} `} > <button - className={`${inDetailsPage ? "" : "hover:transition-all hover:translate-x-3"} inline-flex align-middle items-center pb-2 text-primary-helmholtz-dunkelblau ${associatedToFilters ? "" : "inline-flex"} ${inDetailsPage ? "mb-5" : ""}`} + className={`${isDetailsCard ? "" : "hover:transition-all hover:translate-x-3"} inline-flex align-middle items-center pb-2 text-primary-helmholtz-dunkelblau ${associatedToFilters ? "" : "inline-flex"} ${isDetailsCard ? "mb-5" : ""}`} onClick={handleDetailSearch} - disabled={inDetailsPage} + disabled={isDetailsCard} > {titleIconHref ? ( <span className="mr-1"> @@ -168,12 +183,12 @@ const ResultItem = ({ </span> ) : null} <p - className={`${inDetailsPage ? "inline-flex text-2xl underline underline-offset-[6px] xl:underline-offset-[8px] decoration-primary-helmholtz-hellblau" : "truncate"} font-semibold text-left w-[98%]`} + className={`${isDetailsCard ? "inline-flex text-2xl underline underline-offset-[6px] xl:underline-offset-[8px] decoration-primary-helmholtz-hellblau" : "truncate"} font-semibold text-left w-[98%]`} rel="noreferrer" > {item.name} </p> - {inDetailsPage ?? <RightArrowIcon />} + {isDetailsCard ?? <RightArrowIcon />} </button> <div className="text-sm inline-flex flex-wrap items-center gap-x-1"> diff --git a/src/components/layout/Error.tsx b/src/components/layout/Error.tsx index 0b940c134a1c7db8d4f422e604169e15c1d523f0..690ac5b48199e04de2ec1c2e2d7bd987e13bec54 100644 --- a/src/components/layout/Error.tsx +++ b/src/components/layout/Error.tsx @@ -4,7 +4,7 @@ import React from "react"; import ErrorWarning from "@/resources/images/misc/error-warning.png"; const Error = () => { return ( - <div className="grow flex flex-col justify-center items-center p-10 bg-whitesmoke h-full"> + <div className="grow flex flex-col justify-center items-center p-10 h-full"> {/* TODO: change the font on the icon */} <Image src={ErrorWarning || ""} diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index 5f43cd4f9d42e0c714514296672d7acd34b36239..4b7f0a3b5b7a0257b0f8860344b509d88fa0a7cf 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -1,6 +1,6 @@ import Image from "next/image"; import Link from "next/link"; -import { useTranslations } from "next-intl"; +import { useTranslations, useFormatter } from "next-intl"; import ExternalLink from "@/components/ExternalLink"; import HMCLogoInv from "@/resources/images/logo/HMC_Logo_invert_S.png"; @@ -13,7 +13,10 @@ import MastodonLogo from "@/resources/images/svg/MastodonLogo"; import MattermostLogo from "@/resources/images/svg/MattermostLogo"; export default function Footer() { + const format = useFormatter(); + const dateTime = new Date(); const t = useTranslations("Footer"); + return ( <> <div className="text-primary-helmholtz-weiss w-full flex-wrap divide-y divide-secondary-helmholtz-webblassblau bg-primary-helmholtz-dunkelblau px-20 py-5 drop-shadow"> @@ -89,7 +92,7 @@ export default function Footer() { {/* copyright info */} <div className="pt-4 md:pt-0 md:basis-1/3 flex justify-end cursor-default text-sm"> - {t("copyright", { year: new Date().getFullYear() })} + {t("copyright", { year: format.dateTime(dateTime, { year: "numeric" }) })} </div> </div> </div> diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index fc902c433b45255699b76ebfaa7ac1c87d97d249..a018dc7bff707cf6c57b11f2b4ea2f8ddbccd772 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -1,7 +1,7 @@ import Image from "next/image"; import { useTranslations } from "next-intl"; -import Search from "@/components/layout/Search"; +import Search from "@/components/layout/search/Search"; import { Link } from "@/i18n/routing"; import logoHGF from "@/resources/images/logo/Logo_HGF-KG_text_brightback.png"; import unhideLogo from "@/resources/images/logo/unhide_header.png"; @@ -9,6 +9,7 @@ import unhideLogo from "@/resources/images/logo/unhide_header.png"; const Header = () => { const t = useTranslations("Search"); const s = useTranslations("Intro"); + return ( <div className="flex items-center justify-between px-4 md:px-12 lg:px-16"> <div className=" basis-1/3 md:basis-1/8"> @@ -28,7 +29,7 @@ const Header = () => { {/* secondary nav */} <div className="hidden lg:flex lg:basis-1/2 xl:basis-2/3 "> <div className="mx-auto flex grow items-center justify-end"> - <Search exampleTrigger={t("try")} placeholder={t("placeholder")} /> + <Search exampleTrigger={t("try")} /> </div> </div> diff --git a/src/components/layout/Search.tsx b/src/components/layout/Search.tsx deleted file mode 100644 index c5f5e2f9d8d9d872da0d9e7d4341ec6dda20c314..0000000000000000000000000000000000000000 --- a/src/components/layout/Search.tsx +++ /dev/null @@ -1,122 +0,0 @@ -"use client"; - -import { useSearchParams, useRouter } from "next/navigation"; -import { Suspense, useEffect, useState } from "react"; - -import { Link } from "@/i18n/routing"; -import ClearIcon from "@/resources/images/svg/ClearIcon"; -import RightArrowIcon from "@/resources/images/svg/RightArrowIcon"; -import SearchIcon from "@/resources/images/svg/SearchIcon"; - -type Props = { - exampleTrigger: string; - placeholder: string; -}; - -const SearchComponent = ({ exampleTrigger, placeholder }: Props) => { - const resultParams = useSearchParams(); - const router = useRouter(); - - const [searchQuery, setSearchQuery] = useState(resultParams.get("searchText") ?? ""); - const category = resultParams.get("category") ?? "documents"; - - // not really random but randomly enough for us… - const [exampleSearch, setExamples] = useState<string[]>([]); - - useEffect(() => { - setSearchQuery(resultParams.get("searchText") ?? ""); - }, [resultParams]); - - useEffect(() => { - setExamples( - [ - "GEOMAR", - "AWI", - "Forschungszentrum Jülich", - "Jülich Data", - "Climate", - "DESY", - "HZB", - "KIT", - "HZDR", - "Rodare", - "Pangaea", - "Datacite", - "Geofon", - ] - .sort(() => 0.5 - Math.random()) - .slice(0, 4) - ); - }, []); - - const submitSearch = (event: any) => { - event.preventDefault(); - router.push( - `/results?category=${category}` + (searchQuery ? `&searchText=${searchQuery}` : "") - ); - }; - - return ( - <div className=""> - <form - onSubmit={(e) => submitSearch(e)} - className="flex items-center py-1 border-b-2 border-b-primary-helmholtz-dunkelblau" - > - <div className="basis-1/3 md:basis-11/12 py-2"> - <input - className="pl-2 text-primary-helmholtz-dunkelblau placeholder:italic focus:outline-none bg-transparent" - placeholder={placeholder} - value={searchQuery} - onChange={(e) => setSearchQuery(e.target.value)} - /> - </div> - - {/* search option buttons */} - {/* TODO: refactor this into a single reusable component? */} - <div className="flex md:basis-1/12 justify-end divide-x-2"> - {searchQuery !== "" ? ( - <Link - className="-ml-12 pr-2 md:mt-1 xl:mt-1 flex items-center" - onClick={() => { - setSearchQuery(""); - }} - href={`/results?category=${category}`} - > - <ClearIcon /> - </Link> - ) : null} - - <div className="pl-2"> - <button className="float-right rounded-full bg-secondary-helmholtz-mint px-3 py-3 text-primary-helmholtz-dunkelblau shadow transition duration-300 hover:bg-primary-helmholtz-hellblau"> - <SearchIcon /> - </button> - </div> - </div> - </form> - - <div className="flex md:basis-11/12 space-x-2 pl-2 pt-3 items-center truncate"> - <div className="font-bold text-primary-helmholtz-dunkelblau pr-2">{exampleTrigger}</div> - <div className="flex flex-wrap gap-x-8 gap-y-2 flex-row md:flex-nowrap md:gap-x-10 md:grow md:justify-between h-5"> - {exampleSearch.map((query, ix) => ( - <Link - key={ix} - href={`/results?category=${category}&searchText=${query}`} - className="flex items-center text-info text-primary-helmholtz-dunkelblau hover:scale-110 hover:transition hover:delay-150 hover:translate-x-2 hover:ease-in-out hover:text-primary-helmholtz-hellblau" - > - <div className="basis-1">{query}</div> - <RightArrowIcon /> - </Link> - ))} - </div> - </div> - </div> - ); -}; - -export default function Search(props: Props) { - return ( - <Suspense> - <SearchComponent {...props} /> - </Suspense> - ); -} diff --git a/src/components/layout/Spinner.tsx b/src/components/layout/Spinner.tsx index b78807fc443343648c67ee7195d62a89a666cd71..fff90e0212e51abaefc1efd3b9dacc2cf4cc6cb7 100644 --- a/src/components/layout/Spinner.tsx +++ b/src/components/layout/Spinner.tsx @@ -1,5 +1,5 @@ const Spinner = () => ( - <div className="grow flex flex-col p-10 pt-5 bg-whitesmoke h-full"> + <div className="grow flex flex-col p-10 pt-5 h-full"> <svg className="m-auto align-middle max-h-20 max-w-20 animate-spin text-secondary-helmholtz-mint" xmlns="http://www.w3.org/2000/svg" diff --git a/src/components/layout/MobileSearch.tsx b/src/components/layout/search/MobileSearch.tsx similarity index 82% rename from src/components/layout/MobileSearch.tsx rename to src/components/layout/search/MobileSearch.tsx index aa8895892fd6c6972824dd3b3a62fe730ff30f4f..0a4f6630957f6f74f183ac4a876d7a72c620bd85 100644 --- a/src/components/layout/MobileSearch.tsx +++ b/src/components/layout/search/MobileSearch.tsx @@ -1,7 +1,7 @@ import { useTranslations } from "next-intl"; import React from "react"; -import Search from "@/components/layout/Search"; +import Search from "@/components/layout/search/Search"; import bg from "@/resources/images/misc/unhide_kg_tbg.png"; const MobileSearch = () => { @@ -17,7 +17,7 @@ const MobileSearch = () => { <div className="text-xl md:text-2xl text-primary-helmholtz-dunkelblau">{t("search")}</div> <div className=""> {/* search bar */} - <Search exampleTrigger={t("try")} placeholder={t("placeholder")} /> + <Search exampleTrigger={t("try")} /> </div> </div> ); diff --git a/src/components/layout/search/Search.tsx b/src/components/layout/search/Search.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6d6c4219c52553c34b8dce369854dcf9446538f3 --- /dev/null +++ b/src/components/layout/search/Search.tsx @@ -0,0 +1,17 @@ +import SearchExamples from "@/components/layout/search/SearchExamples"; +import SearchForm from "@/components/layout/search/SearchForm"; + +type Props = { + exampleTrigger: string; +}; + +const Search = ({ exampleTrigger }: Props) => { + return ( + <div> + <SearchForm /> + <SearchExamples exampleTrigger={exampleTrigger} /> + </div> + ); +}; + +export default Search; diff --git a/src/components/layout/search/SearchExamples.tsx b/src/components/layout/search/SearchExamples.tsx new file mode 100644 index 0000000000000000000000000000000000000000..144146f504d84a9e48a7e9814ef8f6a881603b35 --- /dev/null +++ b/src/components/layout/search/SearchExamples.tsx @@ -0,0 +1,76 @@ +"use client"; +import { useSearchParams } from "next/navigation"; +import { useEffect, useState, useContext } from "react"; + +import { CategoryContext } from "@/contexts/CategoryContext"; +import { Link } from "@/i18n/routing"; +import RightArrowIcon from "@/resources/images/svg/RightArrowIcon"; + +type Props = { + exampleTrigger: string; +}; + +const SearchExamples = ({ exampleTrigger }: Props) => { + const resultParams = useSearchParams(); + const paramsCategory = resultParams.get("category") ?? "datasets"; + const detailsText = resultParams.get("detailsText") ?? undefined; + const { categories, setPrimaryActiveCategory } = useContext(CategoryContext); + + // not really random but randomly enough for us… + const [exampleSearch, setExamples] = useState<string[]>([]); + + useEffect(() => { + setExamples( + [ + "GEOMAR", + "AWI", + "Forschungszentrum Jülich", + "Jülich Data", + "Climate", + "DESY", + "HZB", + "KIT", + "HZDR", + "Rodare", + "Pangaea", + "Datacite", + "Geofon", + ] + .sort(() => 0.5 - Math.random()) + .slice(0, 4) + ); + }, []); + + const handleExampleClick = () => { + if (detailsText) { + setPrimaryActiveCategory(categories.filter((o) => o.id === "datasets")[0]); + } + }; + + return ( + <> + <div className="flex md:basis-11/12 space-x-2 pl-2 pt-3 items-center truncate"> + <div className="font-bold text-primary-helmholtz-dunkelblau pr-2">{exampleTrigger}</div> + <div className="flex flex-wrap gap-x-8 gap-y-2 flex-row md:flex-nowrap md:gap-x-10 md:grow md:justify-between h-5"> + {exampleSearch.map((query, ix) => ( + <Link + href={ + detailsText + ? `/results?category=datasets&searchText=${query}` + : `/results?category=${paramsCategory}&searchText=${query}` + } + key={ix} + onClick={handleExampleClick} + className="flex items-center text-info text-primary-helmholtz-dunkelblau hover:scale-110 hover:transition hover:delay-150 hover:translate-x-2 hover:ease-in-out hover:text-primary-helmholtz-hellblau" + > + <div className="basis-1">{query}</div> + <RightArrowIcon /> + </Link> + ))} + </div> + </div> + </> + ); +}; + +export default SearchExamples; diff --git a/src/components/layout/search/SearchForm.tsx b/src/components/layout/search/SearchForm.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9fef4729e39737f521278070c8ecc91320634402 --- /dev/null +++ b/src/components/layout/search/SearchForm.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { useSearchParams, useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; +import { Suspense, useEffect, useState, useContext } from "react"; + +import { CategoryContext } from "@/contexts/CategoryContext"; +import { Link } from "@/i18n/routing"; +import ClearIcon from "@/resources/images/svg/ClearIcon"; +import SearchIcon from "@/resources/images/svg/SearchIcon"; + +const SearchFormComponent = () => { + const router = useRouter(); + const t = useTranslations("Search"); + const resultParams = useSearchParams(); + const paramsCategory = resultParams.get("category") ?? "datasets"; + const detailsText = resultParams.get("detailsText") ?? undefined; + const { categories, setPrimaryActiveCategory } = useContext(CategoryContext); + const [searchQuery, setSearchQuery] = useState(resultParams.get("searchText") ?? ""); + + useEffect(() => { + setSearchQuery(resultParams.get("searchText") ?? ""); + }, [resultParams]); + + const handleClear = () => { + if (detailsText) { + setPrimaryActiveCategory(categories.filter((o) => o.id === "datasets")[0]); + } + setSearchQuery(""); + }; + + const submitSearch = (event: any) => { + event.preventDefault(); + if (detailsText) { + setPrimaryActiveCategory(categories.filter((o) => o.id === "datasets")[0]); + } + router.push( + `/results?category=${detailsText ? "datasets" : paramsCategory}` + + (searchQuery ? `&searchText=${searchQuery}` : "") + ); + }; + + return ( + <div> + <form + onSubmit={(e) => submitSearch(e)} + className="flex items-center py-1 border-b-2 border-b-primary-helmholtz-dunkelblau justify-between" + > + <div className="basis-9/12 md:basis-10/12"> + <input + id="searchInput" + name="searchInput" + className="pl-2 text-primary-helmholtz-dunkelblau placeholder:italic focus:outline-none bg-transparent" + placeholder={t("placeholder")} + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} + /> + </div> + + <div className="flex basis-2/12 md:basis-2/12 justify-end divide-x-2"> + {searchQuery !== "" ? ( + <Link + className="-ml-12 pr-2 md:mt-1 xl:mt-1 flex items-center" + onClick={handleClear} + href={`/results?category=${detailsText ? "datasets" : paramsCategory}`} + > + <ClearIcon /> + </Link> + ) : null} + + <div className="pl-2"> + <button className="rounded-full bg-secondary-helmholtz-mint px-3 py-3 text-primary-helmholtz-dunkelblau shadow transition duration-300 hover:bg-primary-helmholtz-hellblau"> + <SearchIcon /> + </button> + </div> + </div> + </form> + </div> + ); +}; + +export default function SearchForm() { + return ( + <Suspense> + <SearchFormComponent /> + </Suspense> + ); +} diff --git a/src/contexts/CategoryContext.tsx b/src/contexts/CategoryContext.tsx new file mode 100644 index 0000000000000000000000000000000000000000..590d7aec383d66e389f09c4670a39909859387ca --- /dev/null +++ b/src/contexts/CategoryContext.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { createContext, useState, useEffect } from "react"; + +import { Category, CategoryContextType } from "@/types/types"; +import { getAllCategories } from "@/utils/apiCall"; + +export const DEFAULT_CATEGORY_CONTEXT: CategoryContextType = { + categories: [], + defaultCategories: [], + primaryActiveCategory: null, + setCategories: () => {}, + setPrimaryActiveCategory: () => {}, + categoryIsLoading: false, + categoryHasError: false, +}; + +// Creating a context for the category +export const CategoryContext = createContext<CategoryContextType>(DEFAULT_CATEGORY_CONTEXT); + +// Creating a provider for the category +export const CategoryProvider = ({ children }: { children: React.ReactNode }) => { + // The category state that will be shared + const [categories, setCategories] = useState<Category[]>([]); // list of categories whose counts will be updated as per search text updates + const [defaultCategories, setDefaultCategories] = useState<Category[]>([]); // default set of categories and counts + const [primaryActiveCategory, setPrimaryActiveCategory] = useState<Category | null>(null); + + // Main flags for loading and error states + const [categoryIsLoading, setCategoryIsLoading] = useState(false); + const [categoryHasError, setCategoryHasError] = useState(false); + + // Function to fetch categories via API call and set the category images + const fetchCategoriesAndSetImages = async () => { + const allCategories = await getAllCategories(); + const categoriesClone = structuredClone(allCategories); + for (const category of categoriesClone) { + let iconImport; + + try { + iconImport = await import("@/resources/images/category/" + category.id + ".png"); + } catch { + iconImport = await import("@/resources/images/category/software.png"); + } + + category.icon = iconImport.default; + } + return categoriesClone; + }; + + // Effect for loading categories + useEffect(() => { + const setDefaultCategoriesAndCounts = async () => { + try { + setCategoryIsLoading(true); + setCategoryHasError(false); + // get all categories + const allCategories = await fetchCategoriesAndSetImages(); + + // set default categories with default counts + setDefaultCategories(allCategories); + // and a copy of the default categories that will updated when the counts update for search + setCategories(allCategories); + } catch (error) { + console.error("Error fetching categories for landing page", error); + setCategoryHasError(true); + } finally { + setCategoryIsLoading(false); + } + }; + + if (defaultCategories.length === 0) { + setDefaultCategoriesAndCounts(); + } + }, [defaultCategories]); + + return ( + <CategoryContext.Provider + value={{ + categories, + defaultCategories, + primaryActiveCategory, + setCategories, + setPrimaryActiveCategory, + categoryIsLoading, + categoryHasError, + }} + > + {children} + </CategoryContext.Provider> + ); +}; diff --git a/src/i18n/routing.ts b/src/i18n/routing.ts index 7d9aa7223139fb3d69c083863a68c66580a4a876..14e403e0caefeb4922092da8eedc4d577072cad0 100644 --- a/src/i18n/routing.ts +++ b/src/i18n/routing.ts @@ -4,7 +4,9 @@ import { defineRouting, LocalePrefix } from "next-intl/routing"; export const i18nConfig = { locales: ["en", "de"], defaultLocale: "en", - localeDetection: false, + // the following setting causes surprising side effects of category bar not showing the right language in details view + // TODO: Why is this happening? Discuss why locale detection shoube be false + // localeDetection: false, localePrefix: "as-needed" as LocalePrefix, }; export const routing = defineRouting(i18nConfig); diff --git a/src/types/types.ts b/src/types/types.ts index 846c53715425955746b42db8dba9e59813fc063f..a3cb047a1768997be617bb4befe4f831eebeda90 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -1,4 +1,5 @@ import { StaticImport } from "next/dist/shared/lib/get-img-props"; +import { Dispatch, SetStateAction } from "react"; import { z } from "zod"; import { @@ -166,12 +167,22 @@ export type FacetFieldKeys = | "instruments"; export type Category = { - id: string; + id: FacetFieldKeys; text: string; icon?: StaticImport; count: number; }; +export interface CategoryContextType { + categories: Category[]; + defaultCategories: Category[]; + primaryActiveCategory: Category | null; + setCategories: Dispatch<SetStateAction<Category[]>>; + setPrimaryActiveCategory: Dispatch<SetStateAction<Category | null>>; + categoryIsLoading: boolean; + categoryHasError: boolean; +} + // -------- SEARCH API --------- export const searchSchema = z.object({ searchText: z.string().optional(), diff --git a/src/utils/apiCall.ts b/src/utils/apiCall.ts index 4a6de7da68176ba9f957610166af435827b05ed6..edab55b08d9c43fc8b56f511e471e66ee447f47c 100644 --- a/src/utils/apiCall.ts +++ b/src/utils/apiCall.ts @@ -47,7 +47,6 @@ export async function getAllCategories(): Promise<Category[]> { // You can return Date, Map, Set, etc. if (!res.ok) { - // This will activate the closest `error.js` Error Boundary throw new Error("Failed to fetch data"); } return res.json(); @@ -78,7 +77,6 @@ export async function search( try { return await searchAPI(searchText, documentType, rows, start, filters); } catch (error) { - // TODO: Error handling throw error; } } diff --git a/yarn.lock b/yarn.lock index 41356b17a5d9bc110a467b8ca0db9ee0b72c362c..281c1780dc332269b3d0c202ff37b8243cc3b068 100644 --- a/yarn.lock +++ b/yarn.lock @@ -426,13 +426,13 @@ __metadata: linkType: hard "@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0": - version: 4.4.1 - resolution: "@eslint-community/eslint-utils@npm:4.4.1" + version: 4.5.0 + resolution: "@eslint-community/eslint-utils@npm:4.5.0" dependencies: eslint-visitor-keys: "npm:^3.4.3" peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - checksum: 10c0/2aa0ac2fc50ff3f234408b10900ed4f1a0b19352f21346ad4cc3d83a1271481bdda11097baa45d484dd564c895e0762a27a8240be7a256b3ad47129e96528252 + checksum: 10c0/0fad96181bd73ef7254ba61dbbca460a41fe155880122db8852e75c1063617dc60005ff31a7354ecf9dbfcb46fce4349ceca5c1d24db036c5aa39a4bf622fafc languageName: node linkType: hard @@ -1271,34 +1271,34 @@ __metadata: linkType: hard "@react-aria/focus@npm:^3.17.1": - version: 3.20.0 - resolution: "@react-aria/focus@npm:3.20.0" + version: 3.20.1 + resolution: "@react-aria/focus@npm:3.20.1" dependencies: - "@react-aria/interactions": "npm:^3.24.0" - "@react-aria/utils": "npm:^3.28.0" + "@react-aria/interactions": "npm:^3.24.1" + "@react-aria/utils": "npm:^3.28.1" "@react-types/shared": "npm:^3.28.0" "@swc/helpers": "npm:^0.5.0" clsx: "npm:^2.0.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10c0/e7509e90c62f2db2a49997c4ed64782d1a27f4648c6421de3cd5082f376e0ca877f0422c3a1e2da5d7a5201f4457b0b1d797bd1885865bb7ff7089f35a03084a + checksum: 10c0/be982f6cff4531d12894f35b99c326835315d723bf736e36d044cbbffab3b35307620bdbcbd92454010f94a35d851e5976fa9318b4b38ad8d15b1dee447710d6 languageName: node linkType: hard -"@react-aria/interactions@npm:^3.21.3, @react-aria/interactions@npm:^3.24.0": - version: 3.24.0 - resolution: "@react-aria/interactions@npm:3.24.0" +"@react-aria/interactions@npm:^3.21.3, @react-aria/interactions@npm:^3.24.1": + version: 3.24.1 + resolution: "@react-aria/interactions@npm:3.24.1" dependencies: "@react-aria/ssr": "npm:^3.9.7" - "@react-aria/utils": "npm:^3.28.0" + "@react-aria/utils": "npm:^3.28.1" "@react-stately/flags": "npm:^3.1.0" "@react-types/shared": "npm:^3.28.0" "@swc/helpers": "npm:^0.5.0" peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10c0/8352eb4157a64d110b9ad984a03cab51f3b3f4a9db8e70498271928966c3ff6480ac1fd38b7b602160d0efd98c24b2031a7137e5ed749a89a8ae4e7544d6cead + checksum: 10c0/984c5782336eca52a09536733bd41e6b88b15ed648ebe471dbaccc4dabec7b8e3018d1f2c95054272c8ce723379384efd2908181209dee7e204ba5c663c80964 languageName: node linkType: hard @@ -1313,9 +1313,9 @@ __metadata: languageName: node linkType: hard -"@react-aria/utils@npm:^3.28.0": - version: 3.28.0 - resolution: "@react-aria/utils@npm:3.28.0" +"@react-aria/utils@npm:^3.28.1": + version: 3.28.1 + resolution: "@react-aria/utils@npm:3.28.1" dependencies: "@react-aria/ssr": "npm:^3.9.7" "@react-stately/flags": "npm:^3.1.0" @@ -1326,7 +1326,7 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10c0/4f712c7691beeab67aa393f12155b6f157fb99b82fabaf301924c617855bf1d38129a7fbc0a8bc7b7b4e2ecfd243d5571fd591c16a9ca4050072167d932a5dd8 + checksum: 10c0/dcda0e238b3bbd9cd6a59563a0491320cf68f27b0b1e2bd4ee540ab4d7aaa7483cf20d96bbcd0041b5746598f4990239d1a697a6d57348607ac4cc452f69a147 languageName: node linkType: hard @@ -1367,9 +1367,9 @@ __metadata: linkType: hard "@rushstack/eslint-patch@npm:^1.10.3": - version: 1.10.5 - resolution: "@rushstack/eslint-patch@npm:1.10.5" - checksum: 10c0/ea66e8be3a78a48d06e8ff33221cef5749386589236bbcd24013577d2b4e1035e3dc919740c6e0f16d44c1e0b62d06d00898508fc74cc2adb0ac6b667aa5a8e4 + version: 1.11.0 + resolution: "@rushstack/eslint-patch@npm:1.11.0" + checksum: 10c0/abea8d8cf2f4f50343f74abd6a8173c521ddd09b102021f5aa379ef373c40af5948b23db0e87eca1682e559e09d97d3f0c48ea71edad682c6bf72b840c8675b3 languageName: node linkType: hard @@ -1714,14 +1714,14 @@ __metadata: linkType: hard "@typescript-eslint/eslint-plugin@npm:^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/eslint-plugin@npm:^8.9.0": - version: 8.26.0 - resolution: "@typescript-eslint/eslint-plugin@npm:8.26.0" + version: 8.26.1 + resolution: "@typescript-eslint/eslint-plugin@npm:8.26.1" dependencies: "@eslint-community/regexpp": "npm:^4.10.0" - "@typescript-eslint/scope-manager": "npm:8.26.0" - "@typescript-eslint/type-utils": "npm:8.26.0" - "@typescript-eslint/utils": "npm:8.26.0" - "@typescript-eslint/visitor-keys": "npm:8.26.0" + "@typescript-eslint/scope-manager": "npm:8.26.1" + "@typescript-eslint/type-utils": "npm:8.26.1" + "@typescript-eslint/utils": "npm:8.26.1" + "@typescript-eslint/visitor-keys": "npm:8.26.1" graphemer: "npm:^1.4.0" ignore: "npm:^5.3.1" natural-compare: "npm:^1.4.0" @@ -1730,64 +1730,64 @@ __metadata: "@typescript-eslint/parser": ^8.0.0 || ^8.0.0-alpha.0 eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <5.9.0" - checksum: 10c0/b270467672c5cb7fb9085ae063364252af2910a424899f2a9f54cfbe84aba6ce80dbbf5027f1f33f17cc587da9883de212a4b3dc969f22ded30076889b499dd8 + checksum: 10c0/412f41aafd503a1faea91edd03a68717ca8a49ed6683700b8386115c67b86110c9826d10005d3a0341b78cdee41a6ef08842716ced2b58af03f91eb1b8cc929c languageName: node linkType: hard "@typescript-eslint/parser@npm:^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/parser@npm:^8.9.0": - version: 8.26.0 - resolution: "@typescript-eslint/parser@npm:8.26.0" + version: 8.26.1 + resolution: "@typescript-eslint/parser@npm:8.26.1" dependencies: - "@typescript-eslint/scope-manager": "npm:8.26.0" - "@typescript-eslint/types": "npm:8.26.0" - "@typescript-eslint/typescript-estree": "npm:8.26.0" - "@typescript-eslint/visitor-keys": "npm:8.26.0" + "@typescript-eslint/scope-manager": "npm:8.26.1" + "@typescript-eslint/types": "npm:8.26.1" + "@typescript-eslint/typescript-estree": "npm:8.26.1" + "@typescript-eslint/visitor-keys": "npm:8.26.1" debug: "npm:^4.3.4" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <5.9.0" - checksum: 10c0/b937a80aeca4e508a67cbf2e42dfd268316336de265aaf836d04e49008a6ff4d754e73ad30075c183d98756677d1f54061c34e618c97d5fb61a04903c65d4851 + checksum: 10c0/21fe4306b6017bf183d92cdd493edacd302816071e29e1400452f3ccd224ab8111b75892507b9731545e98e6e4d153e54dab568b3433f6c9596b6cb2f7af922f languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.26.0": - version: 8.26.0 - resolution: "@typescript-eslint/scope-manager@npm:8.26.0" +"@typescript-eslint/scope-manager@npm:8.26.1": + version: 8.26.1 + resolution: "@typescript-eslint/scope-manager@npm:8.26.1" dependencies: - "@typescript-eslint/types": "npm:8.26.0" - "@typescript-eslint/visitor-keys": "npm:8.26.0" - checksum: 10c0/f93b12daf6a4df3050ca3fc6db1f534b5c521861509ee09a45a8a17d97f2fbb20c2d34975f07291481d69998aac9f2975f8facad0d47f533db56ec8f70f533a0 + "@typescript-eslint/types": "npm:8.26.1" + "@typescript-eslint/visitor-keys": "npm:8.26.1" + checksum: 10c0/ecd30eb615c7384f01cea8f2c8e8dda7507ada52ad0d002d3701bdd9d06f6d14cefb31c6c26ef55708adfaa2045a01151e8685656240268231a4bac8f792afe4 languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.26.0": - version: 8.26.0 - resolution: "@typescript-eslint/type-utils@npm:8.26.0" +"@typescript-eslint/type-utils@npm:8.26.1": + version: 8.26.1 + resolution: "@typescript-eslint/type-utils@npm:8.26.1" dependencies: - "@typescript-eslint/typescript-estree": "npm:8.26.0" - "@typescript-eslint/utils": "npm:8.26.0" + "@typescript-eslint/typescript-estree": "npm:8.26.1" + "@typescript-eslint/utils": "npm:8.26.1" debug: "npm:^4.3.4" ts-api-utils: "npm:^2.0.1" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <5.9.0" - checksum: 10c0/840b7551dcea7304632564612a2460f869c5330c50661cf21ac5992359aba7539f1466ac7dbde6f2d0bd56f6f769c9f3fed8564045c82d4914a88745da846870 + checksum: 10c0/17553b4333246e1ffd447dab78a4cbc565c129c9baf32326387760c9790120a99d955acf84888b7ef96e73c82fc22a3e08e80f0bd65d21e3cf2fe002f977aba1 languageName: node linkType: hard -"@typescript-eslint/types@npm:8.26.0": - version: 8.26.0 - resolution: "@typescript-eslint/types@npm:8.26.0" - checksum: 10c0/b16c0f67d12092c204a5935b430854b3a41c80934b386a5a4526acc9c8a829d8ee4f78732e71587e605de7845fa9a801b59fff015471dab7bf33676ee68c0100 +"@typescript-eslint/types@npm:8.26.1": + version: 8.26.1 + resolution: "@typescript-eslint/types@npm:8.26.1" + checksum: 10c0/805b239b57854fc12eae9e2bec6ccab24bac1d30a762c455f22c73b777a5859c64c58b4750458bd0ab4aadd664eb95cbef091348a071975acac05b15ebea9f1b languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.26.0": - version: 8.26.0 - resolution: "@typescript-eslint/typescript-estree@npm:8.26.0" +"@typescript-eslint/typescript-estree@npm:8.26.1": + version: 8.26.1 + resolution: "@typescript-eslint/typescript-estree@npm:8.26.1" dependencies: - "@typescript-eslint/types": "npm:8.26.0" - "@typescript-eslint/visitor-keys": "npm:8.26.0" + "@typescript-eslint/types": "npm:8.26.1" + "@typescript-eslint/visitor-keys": "npm:8.26.1" debug: "npm:^4.3.4" fast-glob: "npm:^3.3.2" is-glob: "npm:^4.0.3" @@ -1796,32 +1796,32 @@ __metadata: ts-api-utils: "npm:^2.0.1" peerDependencies: typescript: ">=4.8.4 <5.9.0" - checksum: 10c0/898bf7ec8ee1f3454d0e38a0bb3d7bd3cbd39f530857c9b1851650ec1647bcb6997622e86d24332d81848afd9b65ce4c080437ab1c3c023b23915a745dd0b363 + checksum: 10c0/adc95e4735a8ded05ad35d7b4fae68c675afdd4b3531bc4a51eab5efe793cf80bc75f56dfc8022af4c0a5b316eec61f8ce6b77c2ead45fc675fea7e28cd52ade languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.26.0": - version: 8.26.0 - resolution: "@typescript-eslint/utils@npm:8.26.0" +"@typescript-eslint/utils@npm:8.26.1": + version: 8.26.1 + resolution: "@typescript-eslint/utils@npm:8.26.1" dependencies: "@eslint-community/eslint-utils": "npm:^4.4.0" - "@typescript-eslint/scope-manager": "npm:8.26.0" - "@typescript-eslint/types": "npm:8.26.0" - "@typescript-eslint/typescript-estree": "npm:8.26.0" + "@typescript-eslint/scope-manager": "npm:8.26.1" + "@typescript-eslint/types": "npm:8.26.1" + "@typescript-eslint/typescript-estree": "npm:8.26.1" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <5.9.0" - checksum: 10c0/594838a865d385ad5206c8b948678d4cb4010d0c9b826913968ce9e8af4d1c58b1f044de49f91d8dc36cda2ddb121ee7d2c5b53822a05f3e55002b10a42b3bfb + checksum: 10c0/a5cb3bdf253cc8e8474a2ed8666c0a6194abe56f44039c6623bef0459ed17d0276ed6e40c70d35bd8ec4d41bafc255e4d3025469f32ac692ba2d89e7579c2a26 languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.26.0": - version: 8.26.0 - resolution: "@typescript-eslint/visitor-keys@npm:8.26.0" +"@typescript-eslint/visitor-keys@npm:8.26.1": + version: 8.26.1 + resolution: "@typescript-eslint/visitor-keys@npm:8.26.1" dependencies: - "@typescript-eslint/types": "npm:8.26.0" + "@typescript-eslint/types": "npm:8.26.1" eslint-visitor-keys: "npm:^4.2.0" - checksum: 10c0/6428c1ba199d962060d43f06ba8a98b874ba6fe875a23b10e8f01550838d8be8ee689ae4da3e8b045d4c7bb01e38385e6a8ae17a9d566cf7cd21f7090b573f61 + checksum: 10c0/51b1016d06cd2b9eac0a213de418b0a26022fd3b71478014541bfcbc2a3c4d666552390eb9c209fa9e52c868710d9f1b21a2c789d35c650239438c366a27a239 languageName: node linkType: hard @@ -3313,8 +3313,8 @@ __metadata: linkType: hard "eslint-import-resolver-typescript@npm:^3.5.2": - version: 3.8.3 - resolution: "eslint-import-resolver-typescript@npm:3.8.3" + version: 3.8.4 + resolution: "eslint-import-resolver-typescript@npm:3.8.4" dependencies: "@nolyfill/is-core-module": "npm:1.0.39" debug: "npm:^4.3.7" @@ -3332,7 +3332,7 @@ __metadata: optional: true eslint-plugin-import-x: optional: true - checksum: 10c0/886ceeab4cad14958d7c7d3432ead2486374616c8ada7925ab96e55f919f2dbbbdbe7c3081d7d238231e84699849e82930417a66e05638bcc8202e1263edddeb + checksum: 10c0/d582536ab56091d6a0321ed35101ee315a7e101e5b360f47eb196b28ec9d3c9c6bd64d253d5935547490e97f70a47922bc9810e44b29964f9ec70e2251661a52 languageName: node linkType: hard @@ -5256,9 +5256,9 @@ __metadata: languageName: node linkType: hard -"jotai@npm:^2.10.3": - version: 2.12.1 - resolution: "jotai@npm:2.12.1" +"jotai@npm:^2.12.1": + version: 2.12.2 + resolution: "jotai@npm:2.12.2" peerDependencies: "@types/react": ">=17.0.0" react: ">=17.0.0" @@ -5267,7 +5267,7 @@ __metadata: optional: true react: optional: true - checksum: 10c0/8ef622b7d626919d87595d7382467cb81698a14925befdd27abc3b03d441077eca35328d51d10dc6fe3e91ee4d4e9ba7456bacbdf36940161bd2afeba861ac83 + checksum: 10c0/df135d8a8c635df5d34bdb30b2e24afa93292c9c0bdac589ed9df31da50cec8c4820f66d04f23fa05f6c0ed0726e86c83b0767ca9c6abae4d281a253526d2db3 languageName: node linkType: hard @@ -7892,7 +7892,7 @@ __metadata: isomorphic-fetch: "npm:^3.0.0" jest: "npm:^29.7.0" jest-environment-jsdom: "npm:^29.7.0" - jotai: "npm:^2.10.3" + jotai: "npm:^2.12.1" next: "npm:^15" next-intl: "npm:^3" node-mocks-http: "npm:^1.14.1"