diff --git a/.eslintrc.json b/.eslintrc.json index 94cbfd15580063d9b7ce1313d8af37d2e955d2e2..658e31dfcf75661ecaca6dd9c363d6322e793bd7 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -65,7 +65,9 @@ "**/jest.setup.ts", // jest setup "**/setupTests.ts" ], - "optionalDependencies": false + "optionalDependencies": false, + "peerDependencies": false, + "packageDir": "./" } ], "indent": ["error", 2], @@ -83,6 +85,12 @@ "rules": { "@typescript-eslint/explicit-function-return-type": "error" } + }, + { + // feel free to replace with your preferred file pattern - eg. 'src/**/*Slice.ts' + "files": ["src/**/*.slice.ts", "src/**/*.reducers.ts"], + // avoid state param assignment + "rules": { "no-param-reassign": ["error", { "props": false }] } } ], "settings": { diff --git a/package-lock.json b/package-lock.json index 32205168fb58eae02fa3d4a9f2c7aa851396f44f..1a168c993612d2b08165f53978c6de6cb80047f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,23 +10,29 @@ "hasInstallScript": true, "dependencies": { "@next/font": "^13.5.2", + "@reduxjs/toolkit": "^1.9.6", "@types/node": "20.6.2", "@types/react": "18.2.21", "@types/react-dom": "18.2.7", "autoprefixer": "10.4.15", + "axios": "^1.5.1", "eslint-config-next": "13.4.19", "next": "13.4.19", "postcss": "8.4.29", "react": "18.2.0", "react-dom": "18.2.0", + "react-redux": "^8.1.2", "tailwind-merge": "^1.14.0", - "tailwindcss": "3.3.3" + "tailwindcss": "3.3.3", + "zod": "^3.22.2" }, "devDependencies": { "@commitlint/cli": "^17.7.1", "@commitlint/config-conventional": "^17.7.0", "@testing-library/jest-dom": "^6.1.3", "@testing-library/react": "^14.0.0", + "@types/jest": "^29.5.5", + "@types/react-redux": "^7.1.26", "@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/parser": "^6.7.0", "cypress": "^13.2.0", @@ -2129,6 +2135,29 @@ "node": ">= 8" } }, + "node_modules/@reduxjs/toolkit": { + "version": "1.9.6", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.6.tgz", + "integrity": "sha512-Gc4ikl90ORF4viIdAkY06JNUnODjKfGxZRwATM30EdHq8hLSVoSrwXne5dd739yenP5bJxAX7tLuOWK5RPGtrw==", + "dependencies": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.0.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rushstack/eslint-patch": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.4.0.tgz", @@ -2355,6 +2384,15 @@ "@types/node": "*" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-YIQtIg4PKr7ZyqNPZObpxfHsHEmuB8dXCxd6qVcGuQVDK2bpsF7bYNnBJ4Nn7giuACZg+WewExgrtAJ3XnA4Xw==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -2384,8 +2422,6 @@ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.5.tgz", "integrity": "sha512-ebylz2hnsWR9mYvmBFbXJXr+33UPc4+ZdxyDXh5w0FlPBTfCVN3wPL+kuOiQt3xvrK419v7XWeAs+AeOksafXg==", "dev": true, - "optional": true, - "peer": true, "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" @@ -2396,8 +2432,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, - "optional": true, - "peer": true, "engines": { "node": ">=10" }, @@ -2410,8 +2444,6 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, - "optional": true, - "peer": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -2425,9 +2457,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/@types/jsdom": { "version": "20.0.1", @@ -2491,6 +2521,18 @@ "@types/react": "*" } }, + "node_modules/@types/react-redux": { + "version": "7.1.27", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.27.tgz", + "integrity": "sha512-xj7d9z32p1K/eBmO+OEy+qfaWXtcPlN8f1Xk3Ne0p/ZRQ867RI5bQ/bpBtxbqU1AHNhKJSgGvld/P2myU2uYkg==", + "dev": true, + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "node_modules/@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", @@ -2526,6 +2568,11 @@ "integrity": "sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==", "dev": true }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "node_modules/@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", @@ -3118,8 +3165,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/at-least-node": { "version": "1.0.0", @@ -3200,6 +3246,21 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", + "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -3914,7 +3975,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -4920,7 +4980,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -6337,6 +6396,25 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" }, + "node_modules/follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -6358,7 +6436,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -6798,6 +6875,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -6945,6 +7030,15 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -9449,7 +9543,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -9458,7 +9551,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -9644,6 +9736,14 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/next/node_modules/zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -10583,6 +10683,49 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-redux": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.2.tgz", + "integrity": "sha512-xJKYI189VwfsFc4CJvHqHlDrzyFTY/3vZACbE+rr/zQ34Xx1wQfB4OTOSeOSNrF6BDVe8OOdxIrAnMGXA3ggfw==", + "dependencies": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^16.8 || ^17.0 || ^18.0", + "@types/react-dom": "^16.8 || ^17.0 || ^18.0", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0", + "react-native": ">=0.59", + "redux": "^4 || ^5.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -10758,6 +10901,22 @@ "node": ">=8" } }, + "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==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "peerDependencies": { + "redux": "^4" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -10831,6 +10990,11 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "node_modules/resolve": { "version": "1.22.6", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", @@ -12137,6 +12301,14 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -12627,9 +12799,9 @@ } }, "node_modules/zod": { - "version": "3.21.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", - "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.2.tgz", + "integrity": "sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index a840cf2fad07c115fec8d24c65a9e063e32b8bdd..30e182e0c278104441384872a10a7c858e2ff27c 100644 --- a/package.json +++ b/package.json @@ -28,23 +28,29 @@ }, "dependencies": { "@next/font": "^13.5.2", + "@reduxjs/toolkit": "^1.9.6", "@types/node": "20.6.2", "@types/react": "18.2.21", "@types/react-dom": "18.2.7", "autoprefixer": "10.4.15", + "axios": "^1.5.1", "eslint-config-next": "13.4.19", "next": "13.4.19", "postcss": "8.4.29", "react": "18.2.0", "react-dom": "18.2.0", + "react-redux": "^8.1.2", "tailwind-merge": "^1.14.0", - "tailwindcss": "3.3.3" + "tailwindcss": "3.3.3", + "zod": "^3.22.2" }, "devDependencies": { "@commitlint/cli": "^17.7.1", "@commitlint/config-conventional": "^17.7.0", "@testing-library/jest-dom": "^6.1.3", "@testing-library/react": "^14.0.0", + "@types/jest": "^29.5.5", + "@types/react-redux": "^7.1.26", "@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/parser": "^6.7.0", "cypress": "^13.2.0", diff --git a/pages/_app.tsx b/pages/_app.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f0e8ed0cf72767f1f7198d9eb7c5ed36575ded60 --- /dev/null +++ b/pages/_app.tsx @@ -0,0 +1,10 @@ +import { AppWrapper } from '@/components/AppWrapper'; +import type { AppProps } from 'next/app'; + +const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => ( + <AppWrapper> + <Component {...pageProps} /> + </AppWrapper> +); + +export default MyApp; diff --git a/pages/redux-api-poc.tsx b/pages/redux-api-poc.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4a195e6e0c7a488c2d633aa0ed792d534c2bf622 --- /dev/null +++ b/pages/redux-api-poc.tsx @@ -0,0 +1,28 @@ +import { useSelector } from 'react-redux'; +import { selectSearchValue } from '@/redux/search/search.selectors'; +import { setSearchValue } from '@/redux/search/search.slice'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { getProjectById } from '@/redux/project/project.thunks'; + +const ReduxPage = (): JSX.Element => { + const dispatch = useAppDispatch(); + const searchValue = useSelector(selectSearchValue); + + const triggerSyncUpdate = (): void => { + // eslint-disable-next-line prefer-template + const newValue = searchValue + 'test'; + dispatch(setSearchValue(newValue)); + dispatch(getProjectById('pd_map_winter_23')); + }; + + return ( + <div> + {searchValue} + <button type="button" onClick={triggerSyncUpdate}> + sync update + </button> + </div> + ); +}; + +export default ReduxPage; diff --git a/src/components/AppWrapper/AppWrapper.component.tsx b/src/components/AppWrapper/AppWrapper.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2bb7e192c61b37fef97dafaf58cd0bfc1d94395d --- /dev/null +++ b/src/components/AppWrapper/AppWrapper.component.tsx @@ -0,0 +1,11 @@ +import { ReactNode } from 'react'; +import { Provider } from 'react-redux'; +import { store } from '@/redux/store'; + +interface AppWrapperProps { + children: ReactNode; +} + +export const AppWrapper = ({ children }: AppWrapperProps): JSX.Element => ( + <Provider store={store}>{children}</Provider> +); diff --git a/src/components/AppWrapper/index.ts b/src/components/AppWrapper/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..fed05f3d6bfceba57c760c1a873640b3f7c496f7 --- /dev/null +++ b/src/components/AppWrapper/index.ts @@ -0,0 +1 @@ +export { AppWrapper } from './AppWrapper.component'; diff --git a/src/components/FunctionalArea/TopBar/UserAvatar/UserAvatar.component.tsx b/src/components/FunctionalArea/TopBar/UserAvatar/UserAvatar.component.tsx index f2eedb817c632a0a97398310a5a6d75f92efaca1..3dfa2e6cfd26f0b5c7d82aa1049e518b76994e08 100644 --- a/src/components/FunctionalArea/TopBar/UserAvatar/UserAvatar.component.tsx +++ b/src/components/FunctionalArea/TopBar/UserAvatar/UserAvatar.component.tsx @@ -3,6 +3,6 @@ import avatarImg from '@/assets/images/user-avatar.png'; export const UserAvatar = (): JSX.Element => ( <div className="w-8 h-8 mr-7" data-testid="user-avatar"> - <Image src={avatarImg} alt="user avatar" height={32} width={32} /> + <Image src={avatarImg} alt="user avatar" width={32} height={32} /> </div> ); diff --git a/src/constants/.gitkeep b/src/constants/.gitkeep deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/constants/api.ts b/src/constants/api.ts new file mode 100644 index 0000000000000000000000000000000000000000..f3ebe0ec039ef618048a28016238eb1733bbf817 --- /dev/null +++ b/src/constants/api.ts @@ -0,0 +1 @@ +export const BASE_API_URL = 'https://corsproxy.io/?https://pdmap.uni.lu/minerva/api'; diff --git a/src/models/disease.ts b/src/models/disease.ts new file mode 100644 index 0000000000000000000000000000000000000000..65079e66740c8eb2f4488a9f5798339cc9a4abe4 --- /dev/null +++ b/src/models/disease.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +export const disease = z.object({ + link: z.string(), + type: z.string(), + resource: z.string(), + id: z.number(), + annotatorClassName: z.string(), +}); diff --git a/src/models/organism.ts b/src/models/organism.ts new file mode 100644 index 0000000000000000000000000000000000000000..4b003eefff187e85f7b27831e55d5c09ea3b5c62 --- /dev/null +++ b/src/models/organism.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +export const organism = z.object({ + link: z.string(), + type: z.string(), + resource: z.string(), + id: z.number(), + annotatorClassName: z.string(), +}); diff --git a/src/models/project.ts b/src/models/project.ts new file mode 100644 index 0000000000000000000000000000000000000000..051e5ca197d0bf239e1f525e8a460dfa499467cb --- /dev/null +++ b/src/models/project.ts @@ -0,0 +1,47 @@ +import { z } from 'zod'; +import { disease } from './disease'; +import { organism } from './organism'; + +export const projectSchema = z.object({ + version: z.string(), + disease, + organism, + idObject: z.number(), + status: z.string(), + directory: z.string(), + progress: z.number(), + notifyEmail: z.string(), + logEntries: z.boolean(), + name: z.string(), + sharedInMinervaNet: z.boolean(), + owner: z.string(), + projectId: z.string(), + creationDate: z.string(), + mapCanvasType: z.string(), + overviewImageViews: z.array( + z.object({ + idObject: z.number(), + filename: z.string(), + width: z.number(), + height: z.number(), + links: z.array( + z.union([ + z.object({ + idObject: z.number(), + polygon: z.array(z.object({ x: z.number(), y: z.number() })), + imageLinkId: z.number(), + type: z.string(), + }), + z.object({ + idObject: z.number(), + polygon: z.array(z.object({ x: z.number(), y: z.number() })), + zoomLevel: z.number(), + modelPoint: z.object({ x: z.number(), y: z.number() }), + modelLinkId: z.number(), + type: z.string(), + }), + ]), + ), + }), + ), +}); diff --git a/src/redux/hooks/useAppDispatch.ts b/src/redux/hooks/useAppDispatch.ts new file mode 100644 index 0000000000000000000000000000000000000000..a5d7fc0984e330c9467772db4596ca4adeb74b18 --- /dev/null +++ b/src/redux/hooks/useAppDispatch.ts @@ -0,0 +1,4 @@ +import { useDispatch } from 'react-redux'; +import { AppDispatch } from '@/redux/store'; + +export const useAppDispatch: () => AppDispatch = useDispatch; diff --git a/src/redux/hooks/useAppSelector.ts b/src/redux/hooks/useAppSelector.ts new file mode 100644 index 0000000000000000000000000000000000000000..953f1a353aa1fd916034f92e0911fe6ab855f9d5 --- /dev/null +++ b/src/redux/hooks/useAppSelector.ts @@ -0,0 +1,6 @@ +import { useSelector } from 'react-redux'; +import type { TypedUseSelectorHook } from 'react-redux'; +import { RootState } from '@/redux/store'; + +// Use throughout your app instead of plain `useDispatch` and `useSelector` +export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; diff --git a/src/redux/project/project.reducers.ts b/src/redux/project/project.reducers.ts new file mode 100644 index 0000000000000000000000000000000000000000..aee885ae235dace1b0345e3ee4f6a7d42ad8b3e8 --- /dev/null +++ b/src/redux/project/project.reducers.ts @@ -0,0 +1,10 @@ +import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; +import { ProjectState } from '@/redux/project/project.types'; +import { getProjectById } from '@/redux/project/project.thunks'; + +export const getProjectByIdReducer = (builder: ActionReducerMapBuilder<ProjectState>): void => { + builder.addCase(getProjectById.fulfilled, (state, action) => { + state.data = action.payload; + state.loading = 'succeeded'; + }); +}; diff --git a/src/redux/project/project.slice.ts b/src/redux/project/project.slice.ts new file mode 100644 index 0000000000000000000000000000000000000000..994cb48464988a1ea7b97d4baf788e55dc623b8e --- /dev/null +++ b/src/redux/project/project.slice.ts @@ -0,0 +1,20 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { ProjectState } from '@/redux/project/project.types'; +import { getProjectByIdReducer } from './project.reducers'; + +const initialState: ProjectState = { + data: [], + loading: 'idle', + error: { name: '', message: '' }, +}; + +const projectSlice = createSlice({ + name: 'project', + initialState, + reducers: {}, + extraReducers: builder => { + getProjectByIdReducer(builder); + }, +}); + +export default projectSlice.reducer; diff --git a/src/redux/project/project.thunks.ts b/src/redux/project/project.thunks.ts new file mode 100644 index 0000000000000000000000000000000000000000..d26bbdda8ce975ac42a703712aeed1df28e69d2e --- /dev/null +++ b/src/redux/project/project.thunks.ts @@ -0,0 +1,16 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { axiosInstance } from '@/services/api/utils/axiosInstance'; +import { Project } from '@/types/api'; +import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; +import { projectSchema } from '@/models/project'; + +export const getProjectById = createAsyncThunk( + 'project/getUsersByIdStatus', + async (id: string): Promise<Project | undefined> => { + const response = await axiosInstance.get<Project>(`projects/${id}`); + + const isDataValid = validateDataUsingZodSchema(response.data, projectSchema); + + return isDataValid ? response.data : undefined; + }, +); diff --git a/src/redux/project/project.types.ts b/src/redux/project/project.types.ts new file mode 100644 index 0000000000000000000000000000000000000000..b5ace9d7c0214a4de80316f4b4acb2addeccf6aa --- /dev/null +++ b/src/redux/project/project.types.ts @@ -0,0 +1,7 @@ +import { Project } from '@/types/api'; + +export type ProjectState = { + data: Project | undefined | []; + loading: 'idle' | 'pending' | 'succeeded' | 'failed'; + error: Error; +}; diff --git a/src/redux/search/search.reducers.ts b/src/redux/search/search.reducers.ts new file mode 100644 index 0000000000000000000000000000000000000000..4f6f747d357f19c9a48c43041c484c4052c21600 --- /dev/null +++ b/src/redux/search/search.reducers.ts @@ -0,0 +1,7 @@ +// updating state +import { SearchState } from '@/redux/search/search.types'; +import { PayloadAction } from '@reduxjs/toolkit'; + +export const setSearchValueReducer = (state: SearchState, action: PayloadAction<string>): void => { + state.searchValue = action.payload; +}; diff --git a/src/redux/search/search.selectors.ts b/src/redux/search/search.selectors.ts new file mode 100644 index 0000000000000000000000000000000000000000..c845eecd0c4220dc245e95335f575ece55ecb804 --- /dev/null +++ b/src/redux/search/search.selectors.ts @@ -0,0 +1,4 @@ +import type { RootState } from '@/redux/store'; + +// THIS IS EXAMPLE, it's not memoised!!!! Check redux-tookit docs. +export const selectSearchValue = (state: RootState): string => state.search.searchValue; diff --git a/src/redux/search/search.slice.ts b/src/redux/search/search.slice.ts new file mode 100644 index 0000000000000000000000000000000000000000..92357f8c943fea34aae15b7978266df4c89ae760 --- /dev/null +++ b/src/redux/search/search.slice.ts @@ -0,0 +1,23 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { SearchState } from '@/redux/search/search.types'; +import { setSearchValueReducer } from '@/redux/search/search.reducers'; + +const initialState: SearchState = { + searchValue: '', + searchResult: { + content: '', + drugs: '', + }, +}; + +export const searchSlice = createSlice({ + name: 'search', + initialState, + reducers: { + setSearchValue: setSearchValueReducer, + }, +}); + +export const { setSearchValue } = searchSlice.actions; + +export default searchSlice.reducer; diff --git a/src/redux/search/search.types.ts b/src/redux/search/search.types.ts new file mode 100644 index 0000000000000000000000000000000000000000..6b6316a881f83aed58588d03b75c2f0ca0d5d9a1 --- /dev/null +++ b/src/redux/search/search.types.ts @@ -0,0 +1,9 @@ +export interface SearchResult { + content: string; + drugs: string; +} + +export interface SearchState { + searchValue: string; + searchResult: SearchResult; +} diff --git a/src/redux/store.ts b/src/redux/store.ts new file mode 100644 index 0000000000000000000000000000000000000000..d1335018a8dc60c21274dfa9f543fdedaad9ec72 --- /dev/null +++ b/src/redux/store.ts @@ -0,0 +1,16 @@ +import { configureStore } from '@reduxjs/toolkit'; +import searchReducer from '@/redux/search/search.slice'; +import projectSlice from '@/redux/project/project.slice'; + +export const store = configureStore({ + reducer: { + search: searchReducer, + project: projectSlice, + }, + devTools: true, +}); + +// Infer the `RootState` and `AppDispatch` types from the store itself +export type RootState = ReturnType<typeof store.getState>; +// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} +export type AppDispatch = typeof store.dispatch; diff --git a/src/services/.gitkeep b/src/services/.gitkeep deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/services/api/utils/axiosInstance.ts b/src/services/api/utils/axiosInstance.ts new file mode 100644 index 0000000000000000000000000000000000000000..5e50fba77c865c1e222f51a4c451c9d14197242e --- /dev/null +++ b/src/services/api/utils/axiosInstance.ts @@ -0,0 +1,6 @@ +import axios from 'axios'; +import { BASE_API_URL } from '@/constants/api'; + +export const axiosInstance = axios.create({ + baseURL: BASE_API_URL, +}); diff --git a/src/services/api/utils/useApiQuery.ts b/src/services/api/utils/useApiQuery.ts new file mode 100644 index 0000000000000000000000000000000000000000..d150d65626e1f3f7d086ef39a5a12bc9adc73c0c --- /dev/null +++ b/src/services/api/utils/useApiQuery.ts @@ -0,0 +1,39 @@ +import { BASE_API_URL } from '@/constants/api'; +import { QueryOptions } from '@/types/api'; + +import useAxios, { UseAxiosResult } from 'axios-hooks'; +import { useMemo } from 'react'; +import { AnyZodObject, ZodError } from 'zod'; + +type UseApiQuery = <TResponse extends AnyZodObject>( + queryOptions: QueryOptions<TResponse>, +) => UseAxiosResult<TResponse>; + +export const useApiQuery: UseApiQuery = <TResponse extends AnyZodObject>({ + method, + path, + response, +}: QueryOptions<TResponse>) => { + const [{ data: fetchData, loading, error }, refetch, cancelRequest] = useAxios<TResponse>({ + method, + url: `${BASE_API_URL}${path}`, + }); + + const dataValidation: { success: boolean; error?: ZodError<TResponse> } = useMemo(() => { + if (!fetchData) { + return { success: false, error: undefined }; + } + + return { + error: undefined, + ...response.safeParse(fetchData), + }; + }, [fetchData, response]); + + const data = useMemo( + () => (dataValidation.success ? fetchData : undefined), + [dataValidation.success, fetchData], + ); + + return [{ data, loading, error }, refetch, cancelRequest]; +}; diff --git a/src/types/.gitkeep b/src/types/.gitkeep deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/types/api.ts b/src/types/api.ts new file mode 100644 index 0000000000000000000000000000000000000000..98a130766a65dbf3c7297c141a5776d741a8eaf0 --- /dev/null +++ b/src/types/api.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; +import { disease } from '@/models/disease'; +import { organism } from '@/models/organism'; +import { projectSchema } from '@/models/project'; + +export interface QueryOptions<Response> { + method: 'GET' | 'POST'; + path: string; + response: Response; +} + +export interface Query<Params, Response> { + (params: Params): QueryOptions<Response>; +} + +export type Project = z.infer<typeof projectSchema>; +export type Organism = z.infer<typeof organism>; +export type Disease = z.infer<typeof disease>; diff --git a/src/utils/.gitkeep b/src/utils/.gitkeep deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/utils/validateDataUsingZodSchema.ts b/src/utils/validateDataUsingZodSchema.ts new file mode 100644 index 0000000000000000000000000000000000000000..a7b0cf0858cc2e62842047cfea56bb5066324457 --- /dev/null +++ b/src/utils/validateDataUsingZodSchema.ts @@ -0,0 +1,15 @@ +import { ZodSchema } from 'zod'; + +type IsApiResponseValid = <TData>(data: TData, schema: ZodSchema) => boolean; + +export const validateDataUsingZodSchema: IsApiResponseValid = (data, schema: ZodSchema) => { + const validationResults = schema.safeParse(data); + + if (validationResults.success === false) { + // TODO - probably need to rething way of handling parsing errors, for now let's leave it to console.log + // eslint-disable-next-line no-console + console.error('Error on parsing data', validationResults.error); + } + + return validationResults.success; +};