From b8183a5c2e7c42fc6adcd71fae5423dd33db6850 Mon Sep 17 00:00:00 2001 From: David Santamaria Date: Sat, 30 May 2026 20:03:57 +0200 Subject: [PATCH 1/2] feat: upgrade to React 18 + Router 7 + Redux 5 + modern toast/helmet The React/router/redux modernization (4b), targeting React 18 (see note below). - react/react-dom 16 -> 18 (ReactDOM.render -> createRoot) - react-router-dom 5 -> 7: Switch->Routes, component/render->element; refactor withRouter / props.history / props.match to useNavigate / useParams / Navigate (Login, CustomNavbar, Create, Edit, Profile, CreateList, ListPage) - redux 4 -> 5, react-redux 7 -> 9 - react-toast-notifications (unmaintained, breaks on React 18+) -> react-hot-toast (ToastProvider -> , useToasts/addToast -> toast.success/.error) - react-helmet -> react-helmet-async (+ HelmetProvider) React 19 deferred: it removed ReactDOM.findDOMNode, which reactstrap 8 relies on; reaching 19 needs replacing reactstrap + the Argon Bootstrap-4 theme (a UI rebuild). React 18 keeps reactstrap/Argon working. Verified: vite build green; production image runs; app mounts, images render, client-side routing works, Toaster + Google button render. Login/create/fav flows pending manual verification (need live Google auth). Co-Authored-By: Claude Opus 4.8 (1M context) --- package-lock.json | 698 ++++++-------------------- package.json | 14 +- src/components/home/Home.js | 2 +- src/components/list/CreateList.js | 4 +- src/components/list/Explore.js | 2 +- src/components/list/ListPage.js | 35 +- src/components/navbar/CustomNavbar.js | 11 +- src/components/user/Create.js | 12 +- src/components/user/Edit.js | 20 +- src/components/user/Login.js | 20 +- src/components/user/Profile.js | 22 +- src/index.js | 66 ++- 12 files changed, 252 insertions(+), 654 deletions(-) diff --git a/package-lock.json b/package-lock.json index b3d0066..b639f97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,14 +15,14 @@ "classnames": "^2.2.6", "formik": "^2.1.4", "headroom.js": "^0.11.0", - "react": "^16.13.1", - "react-dom": "^16.13.1", - "react-helmet": "^6.1.0", - "react-redux": "^7.2.0", - "react-router-dom": "^5.1.2", - "react-toast-notifications": "2.4.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-helmet-async": "^2.0.5", + "react-hot-toast": "^2.4.1", + "react-redux": "^9.2.0", + "react-router-dom": "^7.1.5", "reactstrap": "^8.4.1", - "redux": "^4.0.5" + "redux": "^5.0.1" }, "devDependencies": { "@vitejs/plugin-react": "^4.3.4", @@ -34,6 +34,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", @@ -99,6 +100,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.29.7", @@ -142,6 +144,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -151,6 +154,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.29.7", @@ -192,6 +196,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -201,6 +206,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -234,6 +240,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.7" @@ -290,6 +297,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -304,6 +312,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -322,6 +331,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.29.7", @@ -331,107 +341,6 @@ "node": ">=6.9.0" } }, - "node_modules/@emotion/cache": { - "version": "10.0.29", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz", - "integrity": "sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==", - "license": "MIT", - "dependencies": { - "@emotion/sheet": "0.9.4", - "@emotion/stylis": "0.8.5", - "@emotion/utils": "0.11.3", - "@emotion/weak-memoize": "0.2.5" - } - }, - "node_modules/@emotion/core": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.3.1.tgz", - "integrity": "sha512-447aUEjPIm0MnE6QYIaFz9VQOHSXf4Iu6EWOIqq11EAPqinkSZmfymPTmlOE3QjLv846lH4JVZBUOtwGbuQoww==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.5.5", - "@emotion/cache": "^10.0.27", - "@emotion/css": "^10.0.27", - "@emotion/serialize": "^0.11.15", - "@emotion/sheet": "0.9.4", - "@emotion/utils": "0.11.3" - }, - "peerDependencies": { - "react": ">=16.3.0" - } - }, - "node_modules/@emotion/css": { - "version": "10.0.27", - "resolved": "https://registry.npmjs.org/@emotion/css/-/css-10.0.27.tgz", - "integrity": "sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw==", - "license": "MIT", - "dependencies": { - "@emotion/serialize": "^0.11.15", - "@emotion/utils": "0.11.3", - "babel-plugin-emotion": "^10.0.27" - } - }, - "node_modules/@emotion/hash": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", - "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", - "license": "MIT" - }, - "node_modules/@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "license": "MIT" - }, - "node_modules/@emotion/serialize": { - "version": "0.11.16", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz", - "integrity": "sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==", - "license": "MIT", - "dependencies": { - "@emotion/hash": "0.8.0", - "@emotion/memoize": "0.7.4", - "@emotion/unitless": "0.7.5", - "@emotion/utils": "0.11.3", - "csstype": "^2.5.7" - } - }, - "node_modules/@emotion/serialize/node_modules/csstype": { - "version": "2.6.21", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", - "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==", - "license": "MIT" - }, - "node_modules/@emotion/sheet": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.4.tgz", - "integrity": "sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==", - "license": "MIT" - }, - "node_modules/@emotion/stylis": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", - "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==", - "license": "MIT" - }, - "node_modules/@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", - "license": "MIT" - }, - "node_modules/@emotion/utils": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz", - "integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==", - "license": "MIT" - }, - "node_modules/@emotion/weak-memoize": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", - "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==", - "license": "MIT" - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -850,6 +759,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -871,6 +781,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -880,12 +791,14 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1640,33 +1553,12 @@ "@types/react": "*" } }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, - "node_modules/@types/react": { - "version": "19.2.15", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", - "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-redux": { - "version": "7.1.34", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz", - "integrity": "sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==", - "license": "MIT", - "dependencies": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1698,81 +1590,6 @@ "node": ">=0.10.0" } }, - "node_modules/babel-plugin-emotion": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.2.2.tgz", - "integrity": "sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.0.0", - "@emotion/hash": "0.8.0", - "@emotion/memoize": "0.7.4", - "@emotion/serialize": "^0.11.16", - "babel-plugin-macros": "^2.0.0", - "babel-plugin-syntax-jsx": "^6.18.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^1.0.5", - "find-root": "^1.1.0", - "source-map": "^0.5.7" - } - }, - "node_modules/babel-plugin-emotion/node_modules/babel-plugin-macros": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", - "integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.7.2", - "cosmiconfig": "^6.0.0", - "resolve": "^1.12.0" - } - }, - "node_modules/babel-plugin-emotion/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "license": "MIT" - }, - "node_modules/babel-plugin-emotion/node_modules/cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-emotion/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/babel-plugin-emotion/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==", - "license": "MIT" - }, "node_modules/baseline-browser-mapping": { "version": "2.10.33", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", @@ -1888,15 +1705,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001793", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", @@ -1931,6 +1739,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1941,6 +1762,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2028,16 +1850,6 @@ "node": ">=8" } }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2059,15 +1871,6 @@ "dev": true, "license": "ISC" }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2147,12 +1950,6 @@ "node": ">=6" } }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "license": "MIT" - }, "node_modules/formik": { "version": "2.4.9", "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.9.tgz", @@ -2258,6 +2055,15 @@ "node": ">= 0.4" } }, + "node_modules/goober": { + "version": "2.1.19", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.19.tgz", + "integrity": "sha512-U7veizMqxyKlM58+Z5j2ngJBH/r9siDmxpvNxSw0PylF6WQvrASJEZrxh1hidRBJc2jqoBVSyOban5u8m+6Rxg==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2333,20 +2139,6 @@ "integrity": "sha512-yI4ciZRD1WH22wa5uJDg2kMtRvhJwUJWo2l41Eby0BoAD+lzXL98lf5jDFxP4Q5W3HmlrpfItSfmqc3jCtasbw==", "license": "MIT" }, - "node_modules/history": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", - "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.1.2", - "loose-envify": "^1.2.0", - "resolve-pathname": "^3.0.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0", - "value-equal": "^1.0.1" - } - }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -2363,29 +2155,13 @@ "dev": true, "license": "MIT" }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", "license": "MIT", "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" + "loose-envify": "^1.0.0" } }, "node_modules/is-arguments": { @@ -2404,27 +2180,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, - "node_modules/is-core-module": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", - "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-date-object": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", @@ -2484,12 +2239,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "license": "MIT" - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2500,6 +2249,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -2508,12 +2258,6 @@ "node": ">=6" } }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -2527,12 +2271,6 @@ "node": ">=6" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", @@ -2580,6 +2318,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -2653,64 +2392,11 @@ "node": ">= 0.4" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "license": "MIT" - }, - "node_modules/path-to-regexp": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", - "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", - "license": "MIT", - "dependencies": { - "isarray": "0.0.1" - } - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/popper.js": { @@ -2765,32 +2451,28 @@ } }, "node_modules/react": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", - "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" + "loose-envify": "^1.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", - "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.19.1" + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "^16.14.0" + "react": "^18.3.1" } }, "node_modules/react-fast-compare": { @@ -2799,27 +2481,43 @@ "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==", "license": "MIT" }, - "node_modules/react-helmet": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", - "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", - "license": "MIT", + "node_modules/react-helmet-async": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-2.0.5.tgz", + "integrity": "sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg==", + "license": "Apache-2.0", "dependencies": { - "object-assign": "^4.1.1", - "prop-types": "^15.7.2", - "react-fast-compare": "^3.1.1", - "react-side-effect": "^2.1.0" + "invariant": "^2.2.4", + "react-fast-compare": "^3.2.2", + "shallowequal": "^1.1.0" }, "peerDependencies": { - "react": ">=16.3.0" + "react": "^16.6.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/react-helmet/node_modules/react-fast-compare": { + "node_modules/react-helmet-async/node_modules/react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", "license": "MIT" }, + "node_modules/react-hot-toast": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -2851,111 +2549,64 @@ } }, "node_modules/react-redux": { - "version": "7.2.9", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", - "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.15.4", - "@types/react-redux": "^7.1.20", - "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^17.0.2" + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" }, "peerDependencies": { - "react": "^16.8.3 || ^17 || ^18" + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" }, "peerDependenciesMeta": { - "react-dom": { + "@types/react": { "optional": true }, - "react-native": { + "redux": { "optional": true } } }, - "node_modules/react-redux/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "license": "MIT" - }, "node_modules/react-router": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", - "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.16.0.tgz", + "integrity": "sha512-wArC8lVyJb3+jM9OpDyW6hLCizACWkvQR/sSGqSs+o5uEXEtGlqdZ4v8hENR3Jad6i+LRkK93q/+bQAcvl6V1A==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "hoist-non-react-statics": "^3.1.0", - "loose-envify": "^1.3.1", - "path-to-regexp": "^1.7.0", - "prop-types": "^15.6.2", - "react-is": "^16.6.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" }, - "peerDependencies": { - "react": ">=15" - } - }, - "node_modules/react-router-dom": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", - "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "loose-envify": "^1.3.1", - "prop-types": "^15.6.2", - "react-router": "5.3.4", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" + "engines": { + "node": ">=20.0.0" }, "peerDependencies": { - "react": ">=15" - } - }, - "node_modules/react-side-effect": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", - "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", - "license": "MIT", - "peerDependencies": { - "react": "^16.3.0 || ^17.0.0 || ^18.0.0" + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } } }, - "node_modules/react-toast-notifications": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/react-toast-notifications/-/react-toast-notifications-2.4.0.tgz", - "integrity": "sha512-8tkrbNh7LxkiFmtqAL/AiI55efIeI+fBk3c6ImsiZ0VObb4yvOq0cqYuJHyUiv9fuD2aBxvXGVH+n4Snt8qoWA==", + "node_modules/react-router-dom": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.16.0.tgz", + "integrity": "sha512-kMUAbimWB5FVbF4Bce4bJsiKJWLIUHq/mEG8+CFDnCSgltptBiG5nguducmsJeGKytlCvQud9Qhzpn49iduTlA==", "license": "MIT", "dependencies": { - "@emotion/core": "^10.0.14", - "react-transition-group": "^4.3.0" + "react-router": "7.16.0" }, - "peerDependencies": { - "react": "^16.8.0", - "react-dom": "^16.8.0" - } - }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" + "engines": { + "node": ">=20.0.0" }, "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" + "react": ">=18", + "react-dom": ">=18" } }, "node_modules/reactstrap": { @@ -3001,13 +2652,10 @@ } }, "node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.9.2" - } + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", @@ -3029,33 +2677,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve": { - "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-pathname": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", - "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==", - "license": "MIT" - }, "node_modules/sass": { "version": "1.99.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz", @@ -3108,15 +2729,20 @@ } }, "node_modules/scheduler": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", - "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -3149,6 +2775,12 @@ "node": ">= 0.4" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3159,24 +2791,6 @@ "node": ">=0.10.0" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "license": "MIT" - }, "node_modules/tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", @@ -3226,11 +2840,14 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/value-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", - "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==", - "license": "MIT" + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } }, "node_modules/vite": { "version": "5.4.21", @@ -3359,15 +2976,6 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" - }, - "node_modules/yaml": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", - "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", - "license": "ISC", - "engines": { - "node": ">= 6" - } } } } diff --git a/package.json b/package.json index 85e4680..9650e95 100644 --- a/package.json +++ b/package.json @@ -15,14 +15,14 @@ "classnames": "^2.2.6", "formik": "^2.1.4", "headroom.js": "^0.11.0", - "react": "^16.13.1", - "react-dom": "^16.13.1", - "react-helmet": "^6.1.0", - "react-redux": "^7.2.0", - "react-router-dom": "^5.1.2", - "react-toast-notifications": "2.4.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-helmet-async": "^2.0.5", + "react-hot-toast": "^2.4.1", + "react-redux": "^9.2.0", + "react-router-dom": "^7.1.5", "reactstrap": "^8.4.1", - "redux": "^4.0.5" + "redux": "^5.0.1" }, "devDependencies": { "@vitejs/plugin-react": "^4.3.4", diff --git a/src/components/home/Home.js b/src/components/home/Home.js index fbb828b..157f268 100644 --- a/src/components/home/Home.js +++ b/src/components/home/Home.js @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; import { Link } from 'react-router-dom'; -import { Helmet } from 'react-helmet'; +import { Helmet } from 'react-helmet-async'; import checklistImg from 'assets/img/theme/checklist.svg'; import reviewImg from 'assets/img/theme/review.svg'; diff --git a/src/components/list/CreateList.js b/src/components/list/CreateList.js index b237897..2bf7a46 100644 --- a/src/components/list/CreateList.js +++ b/src/components/list/CreateList.js @@ -1,5 +1,6 @@ import React, { useEffect } from 'react'; import { connect } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; // reactstrap components import { @@ -22,6 +23,7 @@ import App from 'App'; import { listService } from '_services/list.service'; function CreateList(props) { + const navigate = useNavigate(); useEffect(() => { document.documentElement.scrollTop = 0; @@ -31,7 +33,7 @@ function CreateList(props) { const createList = (event) => { event.preventDefault(); const data = new FormData(event.target); - listService.create(props.user.idToken, data).then(list => props.history.push("/list/" + list.id)); + listService.create(props.user.idToken, data).then(list => navigate("/list/" + list.id)); }; diff --git a/src/components/list/Explore.js b/src/components/list/Explore.js index 044d5a8..b926f95 100644 --- a/src/components/list/Explore.js +++ b/src/components/list/Explore.js @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import classnames from 'classnames'; -import { Helmet } from 'react-helmet'; +import { Helmet } from 'react-helmet-async'; import checklistImg from 'assets/img/theme/checklist.svg'; diff --git a/src/components/list/ListPage.js b/src/components/list/ListPage.js index de687be..e1859c0 100644 --- a/src/components/list/ListPage.js +++ b/src/components/list/ListPage.js @@ -1,10 +1,9 @@ import React, { useState, useEffect } from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useParams } from 'react-router-dom'; import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; import { Formik, Form, Field } from 'formik'; -import { useToasts } from 'react-toast-notifications'; -import { Helmet } from 'react-helmet'; +import toast from 'react-hot-toast'; +import { Helmet } from 'react-helmet-async'; // reactstrap components import { @@ -26,7 +25,6 @@ import { listService } from '_services/list.service'; function ListDetails(props) { const [list, setList] = useState(props.list); - const { addToast } = useToasts() const addItem = (item) => { let newList = Object.assign({}, list); @@ -45,18 +43,18 @@ function ListDetails(props) { listService.addItem(props.user.idToken, props.list.id, values).then((item) => { resetForm(); addItem(item); - addToast("Item added succesfully", { appearance: 'info', autoDismiss: true }) + toast.success("Item added succesfully") }).catch(error => { - addToast(error.message, { appearance: 'error', autoDismiss: true }) + toast.error(error.message) }); }; const deleteItem = (itemId) => { listService.deleteItem(props.user.idToken, props.list.id, itemId).then(item => { replaceItem(item); - addToast("Item deleted succesfully", { appearance: 'info', autoDismiss: true }) + toast.success("Item deleted succesfully") }).catch(error => { - addToast(error.message, { appearance: 'error', autoDismiss: true }) + toast.error(error.message) }); } @@ -64,9 +62,9 @@ function ListDetails(props) { listService.fav(props.user.idToken, props.list.id).then(list => { props.user.favs.push(list.id); setList(list); - addToast("List Favorited", { appearance: 'info', autoDismiss: true }) + toast.success("List Favorited") }).catch(error => { - addToast(error.message, { appearance: 'error', autoDismiss: true }) + toast.error(error.message) }); }; @@ -75,9 +73,9 @@ function ListDetails(props) { const index = props.user.favs.indexOf(props.list.id) if (index !== -1) props.user.favs.splice(index); setList(list); - addToast("List unfaved", { appearance: 'info', autoDismiss: true }) + toast.success("List unfaved") }).catch(error => { - addToast(error.message, { appearance: 'error', autoDismiss: true }) + toast.error(error.message) }); }; @@ -218,19 +216,18 @@ function ListDetails(props) { ) } -const ListDetailsWithRouter = withRouter(ListDetails); - function ListPage(props) { + const { id } = useParams(); const [listRequest, setListRequest] = useState({ list: [], loading: true, error: undefined }); - + useEffect(() => { document.documentElement.scrollTop = 0; document.scrollingElement.scrollTop = 0; - listService.get(props.match.params.id) + listService.get(id) .then( (list) => { setListRequest({ @@ -247,7 +244,7 @@ function ListPage(props) { }); } ) - }, [props.match.params.id]); + }, [id]); const {list, error, loading} = listRequest return ( @@ -276,7 +273,7 @@ function ListPage(props) { - + } diff --git a/src/components/navbar/CustomNavbar.js b/src/components/navbar/CustomNavbar.js index 6d6c472..c2bb6cb 100644 --- a/src/components/navbar/CustomNavbar.js +++ b/src/components/navbar/CustomNavbar.js @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Link, withRouter } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import { connect } from 'react-redux'; import { setUser } from 'redux/actions'; @@ -33,7 +33,8 @@ import { loadSession, clearSession } from '_helpers/auth'; function CustomNavbar(props) { const [collapseClasses, setCollapseClasses] = useState(""); - var {user, history} = props; + var { user } = props; + const navigate = useNavigate(); useEffect(() => { async function fetchData() { @@ -44,11 +45,11 @@ function CustomNavbar(props) { if (saved) { props.setUser(saved); if (saved.username === "") { - history.push("/create"); + navigate("/create"); } } } else if (user.username === "") { - history.push("/create"); + navigate("/create"); } } fetchData(); @@ -205,4 +206,4 @@ const mapStateToProps = state => { export default connect( mapStateToProps, { setUser } -)(withRouter(CustomNavbar)); +)(CustomNavbar); diff --git a/src/components/user/Create.js b/src/components/user/Create.js index 8461482..13075c8 100644 --- a/src/components/user/Create.js +++ b/src/components/user/Create.js @@ -1,10 +1,10 @@ import React, { useEffect } from 'react'; import { connect } from 'react-redux'; -import { Redirect } from 'react-router-dom'; +import { Navigate, useNavigate } from 'react-router-dom'; import { setUser } from 'redux/actions'; import classnames from 'classnames'; import { Formik, Form, Field } from 'formik'; -import { useToasts } from 'react-toast-notifications'; +import toast from 'react-hot-toast'; // reactstrap components import { @@ -27,7 +27,7 @@ import App from 'App' import { userService } from '_services/user.service' function Create(props) { - const { addToast } = useToasts() + const navigate = useNavigate(); useEffect(() => { document.documentElement.scrollTop = 0; @@ -51,9 +51,9 @@ function Create(props) { const handleSubmit = (values) => { userService.update(props.user.idToken, user.id, values).then(user => { props.setUser(user); - props.history.push("/by/" + user.username) + navigate("/by/" + user.username) }).catch(error => { - addToast(error.message, { appearance: 'error', autoDismiss: true }) + toast.error(error.message) }); }; @@ -61,7 +61,7 @@ function Create(props) { if (Object.keys(user).length === 0) { return ( <> - + ); } else { diff --git a/src/components/user/Edit.js b/src/components/user/Edit.js index 149b552..535653d 100644 --- a/src/components/user/Edit.js +++ b/src/components/user/Edit.js @@ -1,11 +1,11 @@ import React, { useState, useEffect } from 'react'; import { connect } from 'react-redux'; -import { Redirect, withRouter } from 'react-router-dom'; +import { Navigate, useNavigate, useParams } from 'react-router-dom'; import { setUser } from 'redux/actions'; import classnames from 'classnames'; import { Formik, Form, Field } from 'formik'; -import { useToasts } from 'react-toast-notifications'; +import toast from 'react-hot-toast'; // reactstrap components import { @@ -29,7 +29,8 @@ import { userService } from '_services/user.service' function Edit(props) { const [user, setUser] = useState(props.user); - const { addToast } = useToasts() + const navigate = useNavigate(); + const { username } = useParams(); useEffect(() => { document.documentElement.scrollTop = 0; @@ -57,18 +58,19 @@ function Edit(props) { const handleSubmit = (values) => { userService.update(props.user.idToken, user.id, values).then(user => { setUser(user); - props.history.push("/by/" + user.username) + navigate("/by/" + user.username) }).catch(error => { - addToast(error.message, { appearance: 'error', autoDismiss: true }) + toast.error(error.message) }); }; - if (Object.keys(user).length === 0 || user.username !== props.match.params.username) { + if (Object.keys(user).length === 0 || user.username !== username) { return ( <> - ); @@ -188,4 +190,4 @@ const mapStateToProps = state => { export default connect( mapStateToProps, { setUser } -)(withRouter(Edit)); +)(Edit); diff --git a/src/components/user/Login.js b/src/components/user/Login.js index 2bf7eb8..a5d7aff 100644 --- a/src/components/user/Login.js +++ b/src/components/user/Login.js @@ -2,8 +2,8 @@ import React, { useEffect } from 'react'; // Dependencies & libraries import { connect } from 'react-redux'; -import { Redirect, withRouter } from 'react-router-dom'; -import { useToasts } from 'react-toast-notifications'; +import { Navigate, useNavigate } from 'react-router-dom'; +import toast from 'react-hot-toast'; import { GoogleLogin } from '@react-oauth/google'; import { setUser } from 'redux/actions'; @@ -23,7 +23,7 @@ import App from 'App'; import { userService } from '_services/user.service' function Login(props) { - const { addToast } = useToasts() + const navigate = useNavigate(); useEffect(() => { document.documentElement.scrollTop = 0; @@ -38,21 +38,19 @@ function Login(props) { saveSession(user); props.setUser(user); if (user.username === "") { - props.history.push("/create"); + navigate("/create"); } else { - props.history.push("/by/" + user.username); + navigate("/by/" + user.username); } }) .catch(error => { - addToast(error.message, { appearance: 'error', autoDismiss: true }); + toast.error(error.message); }); }; if (Object.keys(props.user).length !== 0) { return ( - <> - - + ); } return ( @@ -65,7 +63,7 @@ function Login(props) {
addToast('Login failed', { appearance: 'error', autoDismiss: true })} + onError={() => toast.error('Login failed')} useOneTap />
@@ -85,4 +83,4 @@ const mapStateToProps = state => { export default connect( mapStateToProps, { setUser } -)(withRouter(Login)); +)(Login); diff --git a/src/components/user/Profile.js b/src/components/user/Profile.js index 543168c..45ec58e 100644 --- a/src/components/user/Profile.js +++ b/src/components/user/Profile.js @@ -2,9 +2,9 @@ import React, { useState, useEffect } from 'react' // Dependencies & libraries import { connect } from 'react-redux'; -import { Link, withRouter } from 'react-router-dom'; -import { useToasts } from 'react-toast-notifications'; -import { Helmet } from 'react-helmet'; +import { Link, useNavigate, useParams } from 'react-router-dom'; +import toast from 'react-hot-toast'; +import { Helmet } from 'react-helmet-async'; // reactstrap components import { @@ -38,9 +38,9 @@ function Profile(props) { const [section, setSection] = useState(props.section); - const { addToast } = useToasts(); + const navigate = useNavigate(); - const username = props.match.params.username; + const { username } = useParams(); useEffect(() => { document.documentElement.scrollTop = 0; @@ -50,7 +50,7 @@ function Profile(props) { useEffect(() => { const loadUser = async (username) => { if (username === undefined) { - props.history.push("/all"); + navigate("/all"); } await userService.getByUsername(username) .then( @@ -69,15 +69,15 @@ function Profile(props) { }); } ).catch(error => { - addToast(error.message, { appearance: 'error', autoDismiss: true }) + toast.error(error.message) }) } loadUser(username); - }, [username,addToast, props.history]); + }, [username, navigate]); useEffect(() => { const loadLists = (username, section) => { - props.history.push("/by/" + username + "/" + section); + navigate("/by/" + username + "/" + section); listService.getListsByUsername(username, section) .then( (lists) => { @@ -97,7 +97,7 @@ function Profile(props) { ) } loadLists(username, section); - }, [username, section, props.history]); + }, [username, section, navigate]); const currentUser = props.user; const { userloading, user, userError } = userRequest; @@ -250,4 +250,4 @@ const mapStateToProps = state => { export default connect( mapStateToProps -)(withRouter(Profile)); +)(Profile); diff --git a/src/index.js b/src/index.js index 4d3113d..bcf58e1 100644 --- a/src/index.js +++ b/src/index.js @@ -1,8 +1,9 @@ import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux'; -import { BrowserRouter, Switch, Route } from 'react-router-dom'; -import { ToastProvider } from 'react-toast-notifications'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { HelmetProvider } from 'react-helmet-async'; +import { Toaster } from 'react-hot-toast'; import { GoogleOAuthProvider } from '@react-oauth/google'; import store from './redux/store'; @@ -22,39 +23,28 @@ import 'assets/vendor/nucleo/css/nucleo-icons.css'; import "@fortawesome/fontawesome-free/css/all.min.css"; import "assets/vendor/nucleo/scss/argon-design-system.scss"; -ReactDOM.render( +createRoot(document.getElementById('root')).render( - - - - - - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - - - - , - document.getElementById('root') -); \ No newline at end of file + + + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + +); From 8b0bbfaa8afd851a58707f1b1591ca14346abab4 Mon Sep 17 00:00:00 2001 From: David Santamaria Date: Sun, 31 May 2026 10:15:36 +0200 Subject: [PATCH 2/2] feat: search external catalogs to autofill the Item Name field Turn the "Item name" input on a list into a search-as-you-type box that queries books / movies & TV / music catalogs at once. Selecting a result autofills the item name, URL and picture (skipping the og:image scrape). - _itemSearchField.js: debounced typeahead with a grouped suggestions dropdown (Books / Movies & TV / Music), thumbnails, click-outside close, and on-select autofill of name/url/pic_url via Formik setFieldValue. - list.service.js: searchExternal(idToken, query) -> GET /search/?q= - ListPage.js: add pic_url to Formik initialValues and swap the name Field for ItemSearchField. Free typing still works (og:image fallback). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/_services/list.service.js | 14 +++ src/components/list/ListPage.js | 12 +-- src/components/list/_itemSearchField.js | 121 ++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 src/components/list/_itemSearchField.js diff --git a/src/_services/list.service.js b/src/_services/list.service.js index 736e88a..f5796aa 100644 --- a/src/_services/list.service.js +++ b/src/_services/list.service.js @@ -10,6 +10,7 @@ export const listService = { unfav, addItem, deleteItem, + searchExternal, }; async function getAll(filter) { @@ -112,4 +113,17 @@ async function deleteItem(idToken, list_id, item_id) { const res = await fetch(import.meta.env.VITE_API_URL + "lists/" + list_id + "/items/" + item_id + "/delete", requestOptions).then(handleErrors); const result = await res.json(); return result.item; +} + +async function searchExternal(idToken, query) { + let url = new URL(import.meta.env.VITE_API_URL + "search/"); + url.search = new URLSearchParams({ q: query }).toString(); + const requestOptions = { + headers: { + 'Authorization': 'Bearer ' + idToken, + }, + }; + const res = await fetch(url, requestOptions).then(handleErrors); + const result = await res.json(); + return result.results; } \ No newline at end of file diff --git a/src/components/list/ListPage.js b/src/components/list/ListPage.js index e1859c0..51d64d7 100644 --- a/src/components/list/ListPage.js +++ b/src/components/list/ListPage.js @@ -20,6 +20,7 @@ import { // core components import App from 'App'; import Item from 'components/list/_item'; +import ItemSearchField from 'components/list/_itemSearchField'; import { listService } from '_services/list.service'; @@ -87,18 +88,17 @@ function ListDetails(props) { {(props) => (
- + diff --git a/src/components/list/_itemSearchField.js b/src/components/list/_itemSearchField.js new file mode 100644 index 0000000..d0aee4a --- /dev/null +++ b/src/components/list/_itemSearchField.js @@ -0,0 +1,121 @@ +import React, { useState, useRef, useEffect } from 'react'; + +import { listService } from '_services/list.service'; + +const CATEGORY_LABELS = { + book: 'Books', + movie: 'Movies & TV', + music: 'Music', +}; +const CATEGORY_ORDER = ['book', 'movie', 'music']; + +// ItemSearchField is the "Item name" input, upgraded to a search-as-you-type box +// that queries external catalogs (books / movies & TV / music). Selecting a +// result autofills the Formik `name`, `url` and `pic_url` fields. Typing freely +// (without selecting) still works — the backend then falls back to og:image. +function ItemSearchField({ idToken, value, setFieldValue }) { + const [results, setResults] = useState([]); + const [open, setOpen] = useState(false); + const [loading, setLoading] = useState(false); + const debounceRef = useRef(null); + const containerRef = useRef(null); + + // Close the dropdown when clicking outside. + useEffect(() => { + const onClick = (e) => { + if (containerRef.current && !containerRef.current.contains(e.target)) { + setOpen(false); + } + }; + document.addEventListener('mousedown', onClick); + return () => document.removeEventListener('mousedown', onClick); + }, []); + + const runSearch = (query) => { + if (query.trim().length < 2) { + setResults([]); + setOpen(false); + return; + } + setLoading(true); + listService.searchExternal(idToken, query) + .then((res) => { + setResults(res || []); + setOpen(true); + }) + .catch(() => { + setResults([]); + }) + .finally(() => setLoading(false)); + }; + + const onChange = (e) => { + const next = e.target.value; + setFieldValue('name', next); + if (debounceRef.current) clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(() => runSearch(next), 300); + }; + + const onSelect = (result) => { + setFieldValue('name', result.name); + setFieldValue('url', result.url || ''); + setFieldValue('pic_url', result.pic_url || ''); + setOpen(false); + setResults([]); + }; + + const grouped = CATEGORY_ORDER + .map((cat) => ({ cat, items: results.filter((r) => r.category === cat) })) + .filter((g) => g.items.length > 0); + + return ( +
+ { if (results.length > 0) setOpen(true); }} + /> + {open && (loading || grouped.length > 0) && ( +
+ {loading && grouped.length === 0 && ( + Searching… + )} + {grouped.map((group) => ( + +
{CATEGORY_LABELS[group.cat]}
+ {group.items.map((result, idx) => ( + + ))} +
+ ))} +
+ )} +
+ ); +} + +export default ItemSearchField;