mirror of
https://github.com/ershisan99/flashcards-example-project.git
synced 2025-12-16 20:59:27 +00:00
lesson 4 finished
This commit is contained in:
@@ -18,11 +18,14 @@
|
|||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@radix-ui/react-radio-group": "^1.1.3",
|
"@radix-ui/react-radio-group": "^1.1.3",
|
||||||
|
"@reduxjs/toolkit": "^1.9.6",
|
||||||
"@storybook/theming": "^7.2.1",
|
"@storybook/theming": "^7.2.1",
|
||||||
|
"async-mutex": "^0.4.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.45.2",
|
"react-hook-form": "^7.45.2",
|
||||||
|
"react-redux": "^8.1.2",
|
||||||
"react-router-dom": "^6.14.2",
|
"react-router-dom": "^6.14.2",
|
||||||
"react-toastify": "^9.1.3",
|
"react-toastify": "^9.1.3",
|
||||||
"remeda": "^1.24.0",
|
"remeda": "^1.24.0",
|
||||||
|
|||||||
112
pnpm-lock.yaml
generated
112
pnpm-lock.yaml
generated
@@ -20,9 +20,15 @@ dependencies:
|
|||||||
'@radix-ui/react-radio-group':
|
'@radix-ui/react-radio-group':
|
||||||
specifier: ^1.1.3
|
specifier: ^1.1.3
|
||||||
version: 1.1.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.1.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@reduxjs/toolkit':
|
||||||
|
specifier: ^1.9.6
|
||||||
|
version: 1.9.6(react-redux@8.1.2)(react@18.2.0)
|
||||||
'@storybook/theming':
|
'@storybook/theming':
|
||||||
specifier: ^7.2.1
|
specifier: ^7.2.1
|
||||||
version: 7.2.1(react-dom@18.2.0)(react@18.2.0)
|
version: 7.2.1(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
async-mutex:
|
||||||
|
specifier: ^0.4.0
|
||||||
|
version: 0.4.0
|
||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
@@ -35,6 +41,9 @@ dependencies:
|
|||||||
react-hook-form:
|
react-hook-form:
|
||||||
specifier: ^7.45.2
|
specifier: ^7.45.2
|
||||||
version: 7.45.2(react@18.2.0)
|
version: 7.45.2(react@18.2.0)
|
||||||
|
react-redux:
|
||||||
|
specifier: ^8.1.2
|
||||||
|
version: 8.1.2(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1)
|
||||||
react-router-dom:
|
react-router-dom:
|
||||||
specifier: ^6.14.2
|
specifier: ^6.14.2
|
||||||
version: 6.14.2(react-dom@18.2.0)(react@18.2.0)
|
version: 6.14.2(react-dom@18.2.0)(react@18.2.0)
|
||||||
@@ -2672,6 +2681,25 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.22.6
|
'@babel/runtime': 7.22.6
|
||||||
|
|
||||||
|
/@reduxjs/toolkit@1.9.6(react-redux@8.1.2)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-Gc4ikl90ORF4viIdAkY06JNUnODjKfGxZRwATM30EdHq8hLSVoSrwXne5dd739yenP5bJxAX7tLuOWK5RPGtrw==}
|
||||||
|
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
|
||||||
|
dependencies:
|
||||||
|
immer: 9.0.21
|
||||||
|
react: 18.2.0
|
||||||
|
react-redux: 8.1.2(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1)
|
||||||
|
redux: 4.2.1
|
||||||
|
redux-thunk: 2.4.2(redux@4.2.1)
|
||||||
|
reselect: 4.1.8
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@remix-run/router@1.7.2:
|
/@remix-run/router@1.7.2:
|
||||||
resolution: {integrity: sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==}
|
resolution: {integrity: sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -3951,6 +3979,13 @@ packages:
|
|||||||
'@types/node': 20.4.5
|
'@types/node': 20.4.5
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/hoist-non-react-statics@3.3.2:
|
||||||
|
resolution: {integrity: sha512-YIQtIg4PKr7ZyqNPZObpxfHsHEmuB8dXCxd6qVcGuQVDK2bpsF7bYNnBJ4Nn7giuACZg+WewExgrtAJ3XnA4Xw==}
|
||||||
|
dependencies:
|
||||||
|
'@types/react': 18.2.15
|
||||||
|
hoist-non-react-statics: 3.3.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/http-errors@2.0.1:
|
/@types/http-errors@2.0.1:
|
||||||
resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==}
|
resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==}
|
||||||
|
|
||||||
@@ -4073,6 +4108,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g==}
|
resolution: {integrity: sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/use-sync-external-store@0.0.3:
|
||||||
|
resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/yargs-parser@21.0.0:
|
/@types/yargs-parser@21.0.0:
|
||||||
resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
|
resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -4610,6 +4649,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
|
resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/async-mutex@0.4.0:
|
||||||
|
resolution: {integrity: sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==}
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.6.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/async@3.2.4:
|
/async@3.2.4:
|
||||||
resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==}
|
resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -6647,7 +6692,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
react-is: 16.13.1
|
react-is: 16.13.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/hosted-git-info@2.8.9:
|
/hosted-git-info@2.8.9:
|
||||||
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
||||||
@@ -6726,6 +6770,10 @@ packages:
|
|||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/immer@9.0.21:
|
||||||
|
resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/immutable@4.3.1:
|
/immutable@4.3.1:
|
||||||
resolution: {integrity: sha512-lj9cnmB/kVS0QHsJnYKD1uo3o39nrbKxszjnqS9Fr6NB7bZzW45U6WSGBPKXDL/CvDKqDNPA4r3DoDQ8GTxo2A==}
|
resolution: {integrity: sha512-lj9cnmB/kVS0QHsJnYKD1uo3o39nrbKxszjnqS9Fr6NB7bZzW45U6WSGBPKXDL/CvDKqDNPA4r3DoDQ8GTxo2A==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -8358,7 +8406,6 @@ packages:
|
|||||||
|
|
||||||
/react-is@16.13.1:
|
/react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/react-is@17.0.2:
|
/react-is@17.0.2:
|
||||||
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
||||||
@@ -8366,7 +8413,40 @@ packages:
|
|||||||
|
|
||||||
/react-is@18.1.0:
|
/react-is@18.1.0:
|
||||||
resolution: {integrity: sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==}
|
resolution: {integrity: sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==}
|
||||||
dev: true
|
|
||||||
|
/react-redux@8.1.2(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1):
|
||||||
|
resolution: {integrity: sha512-xJKYI189VwfsFc4CJvHqHlDrzyFTY/3vZACbE+rr/zQ34Xx1wQfB4OTOSeOSNrF6BDVe8OOdxIrAnMGXA3ggfw==}
|
||||||
|
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
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.22.6
|
||||||
|
'@types/hoist-non-react-statics': 3.3.2
|
||||||
|
'@types/react': 18.2.15
|
||||||
|
'@types/react-dom': 18.2.7
|
||||||
|
'@types/use-sync-external-store': 0.0.3
|
||||||
|
hoist-non-react-statics: 3.3.2
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
react-is: 18.1.0
|
||||||
|
redux: 4.2.1
|
||||||
|
use-sync-external-store: 1.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-refresh@0.14.0:
|
/react-refresh@0.14.0:
|
||||||
resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==}
|
resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==}
|
||||||
@@ -8565,6 +8645,20 @@ packages:
|
|||||||
strip-indent: 4.0.0
|
strip-indent: 4.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/redux-thunk@2.4.2(redux@4.2.1):
|
||||||
|
resolution: {integrity: sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==}
|
||||||
|
peerDependencies:
|
||||||
|
redux: ^4
|
||||||
|
dependencies:
|
||||||
|
redux: 4.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/redux@4.2.1:
|
||||||
|
resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.22.6
|
||||||
|
dev: false
|
||||||
|
|
||||||
/regenerate-unicode-properties@10.1.0:
|
/regenerate-unicode-properties@10.1.0:
|
||||||
resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==}
|
resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -8649,6 +8743,10 @@ packages:
|
|||||||
engines: {node: '>=0.10.5'}
|
engines: {node: '>=0.10.5'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/reselect@4.1.8:
|
||||||
|
resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/resolve-from@4.0.0:
|
/resolve-from@4.0.0:
|
||||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -9814,6 +9912,14 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
tslib: 2.6.1
|
tslib: 2.6.1
|
||||||
|
|
||||||
|
/use-sync-external-store@1.2.0(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/util-deprecate@1.0.2:
|
/util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
|
|||||||
11
src/App.tsx
11
src/App.tsx
@@ -1,3 +1,12 @@
|
|||||||
|
import { Provider } from 'react-redux'
|
||||||
|
|
||||||
|
import { Router } from '@/router.tsx'
|
||||||
|
import { store } from '@/services/store.ts'
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return <div>Hello</div>
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<Router />
|
||||||
|
</Provider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
45
src/base-query-with-reauth.ts
Normal file
45
src/base-query-with-reauth.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { fetchBaseQuery } from '@reduxjs/toolkit/query'
|
||||||
|
import type { BaseQueryFn, FetchArgs, FetchBaseQueryError } from '@reduxjs/toolkit/query'
|
||||||
|
import { Mutex } from 'async-mutex'
|
||||||
|
|
||||||
|
const mutex = new Mutex()
|
||||||
|
const baseQuery = fetchBaseQuery({
|
||||||
|
baseUrl: 'https://api.flashcards.andrii.es',
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const baseQueryWithReauth: BaseQueryFn<
|
||||||
|
string | FetchArgs,
|
||||||
|
unknown,
|
||||||
|
FetchBaseQueryError
|
||||||
|
> = async (args, api, extraOptions) => {
|
||||||
|
// wait until the mutex is available without locking it
|
||||||
|
await mutex.waitForUnlock()
|
||||||
|
let result = await baseQuery(args, api, extraOptions)
|
||||||
|
|
||||||
|
if (result.error && result.error.status === 401) {
|
||||||
|
// checking whether the mutex is locked
|
||||||
|
if (!mutex.isLocked()) {
|
||||||
|
const release = await mutex.acquire()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const refreshResult = await baseQuery(
|
||||||
|
{ url: '/v1/auth/refresh-token', method: 'POST' },
|
||||||
|
api,
|
||||||
|
extraOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!refreshResult.error) {
|
||||||
|
result = await baseQuery(args, api, extraOptions)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
release()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await mutex.waitForUnlock()
|
||||||
|
result = await baseQuery(args, api, extraOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
16
src/pages/login.tsx
Normal file
16
src/pages/login.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Navigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { SignIn } from '@/components'
|
||||||
|
import { useLoginMutation, useMeQuery } from '@/services/auth/auth.service.ts'
|
||||||
|
|
||||||
|
export const Login = () => {
|
||||||
|
const { isError, isLoading } = useMeQuery()
|
||||||
|
const isAuthenticated = !isError
|
||||||
|
const [logIn] = useLoginMutation()
|
||||||
|
|
||||||
|
if (isLoading) return <div>Loading...</div>
|
||||||
|
|
||||||
|
if (isAuthenticated) return <Navigate to={'/'} replace={true} />
|
||||||
|
|
||||||
|
return <SignIn onSubmit={logIn} />
|
||||||
|
}
|
||||||
155
src/router.tsx
Normal file
155
src/router.tsx
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
createBrowserRouter,
|
||||||
|
Navigate,
|
||||||
|
Outlet,
|
||||||
|
RouteObject,
|
||||||
|
RouterProvider,
|
||||||
|
} from 'react-router-dom'
|
||||||
|
|
||||||
|
import { Button, TextField } from '@/components'
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeadCell,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table'
|
||||||
|
import { Login } from '@/pages/login.tsx'
|
||||||
|
import {
|
||||||
|
useLogoutMutation,
|
||||||
|
useMeQuery,
|
||||||
|
useUpdateProfileMutation,
|
||||||
|
} from '@/services/auth/auth.service.ts'
|
||||||
|
import { useCreateDeckMutation, useDeleteDeckMutation, useGetDecksQuery } from '@/services/decks'
|
||||||
|
import { decksSlice } from '@/services/decks/decks.slice.ts'
|
||||||
|
import { useAppDispatch, useAppSelector } from '@/services/store.ts'
|
||||||
|
|
||||||
|
const publicRoutes: RouteObject[] = [
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
element: <Login />,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const privateRoutes: RouteObject[] = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
element: <Decks />,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
element: <PrivateRoutes />,
|
||||||
|
children: privateRoutes,
|
||||||
|
},
|
||||||
|
...publicRoutes,
|
||||||
|
{
|
||||||
|
path: '*',
|
||||||
|
element: <h1>404</h1>,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
export const Router = () => {
|
||||||
|
const [logOut] = useLogoutMutation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button onClick={() => logOut()}>log out</Button>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PrivateRoutes() {
|
||||||
|
const { isError, isLoading } = useMeQuery()
|
||||||
|
const isAuthenticated = !isError
|
||||||
|
|
||||||
|
if (isLoading) return <div>Loading...</div>
|
||||||
|
|
||||||
|
return isAuthenticated ? <Outlet /> : <Navigate to="/login" />
|
||||||
|
}
|
||||||
|
|
||||||
|
function Decks() {
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const [value, setValue] = useState<Blob | undefined>()
|
||||||
|
const searchByName = useAppSelector(state => state.decks.searchByName)
|
||||||
|
const currentPage = useAppSelector(state => state.decks.currentPage)
|
||||||
|
const setCurrentPage = (page: number) => dispatch(decksSlice.actions.setCurrentPage(page))
|
||||||
|
const setSearchByName = (name: string) => dispatch(decksSlice.actions.setSearchByName(name))
|
||||||
|
const { currentData: data } = useGetDecksQuery({ currentPage, name: searchByName })
|
||||||
|
const [createDeck] = useCreateDeckMutation()
|
||||||
|
const [deleteDeck] = useDeleteDeckMutation()
|
||||||
|
const [updateProfile] = useUpdateProfileMutation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input type={'file'} onChange={e => setValue(e.currentTarget.files?.[0])} />
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
if (value) formData.append('avatar', value)
|
||||||
|
if (value) formData.append('name', 'Inocencio')
|
||||||
|
console.log(formData.get('avatar'))
|
||||||
|
console.log(formData.get('name'))
|
||||||
|
updateProfile(formData)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Update avatar
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
createDeck({ name: 'new deck 123' })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create deck
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
value={searchByName}
|
||||||
|
onChange={event => {
|
||||||
|
setSearchByName(event.target.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeadCell>Name</TableHeadCell>
|
||||||
|
<TableHeadCell>Cards</TableHeadCell>
|
||||||
|
<TableHeadCell>Updated at</TableHeadCell>
|
||||||
|
<TableHeadCell>Created By</TableHeadCell>
|
||||||
|
<TableHeadCell>Actions</TableHeadCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{data?.items?.map(deck => (
|
||||||
|
<TableRow key={deck.id}>
|
||||||
|
<TableCell>{deck.name}</TableCell>
|
||||||
|
<TableCell>{deck.cardsCount}</TableCell>
|
||||||
|
<TableCell>{new Date(deck.updated).toLocaleDateString()}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<button onClick={() => deleteDeck({ id: deck.id })}>delete</button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap', marginTop: '24px' }}>
|
||||||
|
{[...Array(data?.pagination?.totalPages)].map((_, index) => (
|
||||||
|
<Button
|
||||||
|
key={index}
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentPage(index + 1)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{index + 1}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
48
src/services/auth/auth.service.ts
Normal file
48
src/services/auth/auth.service.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { LoginArgs } from '@/services/auth/auth.types.ts'
|
||||||
|
import { baseApi } from '@/services/base-api.ts'
|
||||||
|
|
||||||
|
export const authService = baseApi.injectEndpoints({
|
||||||
|
endpoints: builder => ({
|
||||||
|
me: builder.query<any, void>({
|
||||||
|
query: () => '/v1/auth/me',
|
||||||
|
providesTags: ['Me'],
|
||||||
|
}),
|
||||||
|
updateProfile: builder.mutation<any, any>({
|
||||||
|
query: body => ({
|
||||||
|
url: '/v1/auth/me',
|
||||||
|
method: 'PATCH',
|
||||||
|
body,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
login: builder.mutation<any, LoginArgs>({
|
||||||
|
query: body => ({
|
||||||
|
url: '/v1/auth/login',
|
||||||
|
method: 'POST',
|
||||||
|
body,
|
||||||
|
}),
|
||||||
|
invalidatesTags: ['Me'],
|
||||||
|
}),
|
||||||
|
logout: builder.mutation<void, void>({
|
||||||
|
query: () => ({
|
||||||
|
url: '/v1/auth/logout',
|
||||||
|
method: 'POST',
|
||||||
|
}),
|
||||||
|
// onQueryStarted: async (_, { getState, dispatch, queryFulfilled }) => {
|
||||||
|
// try {
|
||||||
|
// await queryFulfilled
|
||||||
|
// dispatch(
|
||||||
|
// authService.util.updateQueryData('me', undefined, draft => {
|
||||||
|
// return null
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
// } catch (e) {
|
||||||
|
// console.error(e)
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
invalidatesTags: ['Me'],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const { useLoginMutation, useMeQuery, useUpdateProfileMutation, useLogoutMutation } =
|
||||||
|
authService
|
||||||
5
src/services/auth/auth.types.ts
Normal file
5
src/services/auth/auth.types.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export type LoginArgs = {
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
rememberMe?: boolean
|
||||||
|
}
|
||||||
10
src/services/base-api.ts
Normal file
10
src/services/base-api.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { createApi } from '@reduxjs/toolkit/query/react'
|
||||||
|
|
||||||
|
import { baseQueryWithReauth } from '@/base-query-with-reauth.ts'
|
||||||
|
|
||||||
|
export const baseApi = createApi({
|
||||||
|
reducerPath: 'baseApi',
|
||||||
|
tagTypes: ['Decks', 'Cards', 'Users', 'Me'],
|
||||||
|
baseQuery: baseQueryWithReauth,
|
||||||
|
endpoints: () => ({}),
|
||||||
|
})
|
||||||
81
src/services/decks/decks.service.ts
Normal file
81
src/services/decks/decks.service.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { baseApi } from '../base-api'
|
||||||
|
|
||||||
|
import type { CreateDeckArgs, Deck, DecksResponse, DeleteDeckArgs } from './decks.types.ts'
|
||||||
|
import { GetDecksParams } from './decks.types.ts'
|
||||||
|
|
||||||
|
import { RootState } from '@/services/store.ts'
|
||||||
|
|
||||||
|
export const DecksService = baseApi.injectEndpoints({
|
||||||
|
endpoints: builder => {
|
||||||
|
return {
|
||||||
|
getDecks: builder.query<DecksResponse, GetDecksParams | void>({
|
||||||
|
query: params => {
|
||||||
|
return {
|
||||||
|
url: `v1/decks`,
|
||||||
|
params: params ?? {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
providesTags: ['Decks'],
|
||||||
|
}),
|
||||||
|
createDeck: builder.mutation<Deck, CreateDeckArgs>({
|
||||||
|
query: body => ({
|
||||||
|
url: `v1/decks`,
|
||||||
|
method: 'POST',
|
||||||
|
body,
|
||||||
|
}),
|
||||||
|
onQueryStarted: async (_, { getState, queryFulfilled, dispatch }) => {
|
||||||
|
const state = getState() as RootState
|
||||||
|
const { searchByName, currentPage } = state.decks
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await queryFulfilled
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
DecksService.util.updateQueryData(
|
||||||
|
'getDecks',
|
||||||
|
{ currentPage, name: searchByName },
|
||||||
|
draft => {
|
||||||
|
draft?.items?.unshift(result.data)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
invalidatesTags: ['Decks'],
|
||||||
|
}),
|
||||||
|
deleteDeck: builder.mutation<void, DeleteDeckArgs>({
|
||||||
|
query: ({ id }) => ({
|
||||||
|
url: `v1/decks/${id}`,
|
||||||
|
method: 'DELETE',
|
||||||
|
}),
|
||||||
|
|
||||||
|
onQueryStarted: async ({ id }, { getState, queryFulfilled, dispatch }) => {
|
||||||
|
const state = getState() as RootState
|
||||||
|
const { searchByName, currentPage } = state.decks
|
||||||
|
|
||||||
|
const patchResult = dispatch(
|
||||||
|
DecksService.util.updateQueryData(
|
||||||
|
'getDecks',
|
||||||
|
{ currentPage, name: searchByName },
|
||||||
|
draft => {
|
||||||
|
draft?.items?.splice(draft?.items?.findIndex(deck => deck.id === id), 1)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await queryFulfilled
|
||||||
|
} catch (e) {
|
||||||
|
patchResult.undo()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
invalidatesTags: ['Decks'],
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const { useGetDecksQuery, useCreateDeckMutation, useDeleteDeckMutation } = DecksService
|
||||||
17
src/services/decks/decks.slice.ts
Normal file
17
src/services/decks/decks.slice.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
|
|
||||||
|
export const decksSlice = createSlice({
|
||||||
|
name: 'decks',
|
||||||
|
initialState: {
|
||||||
|
searchByName: '',
|
||||||
|
currentPage: 1,
|
||||||
|
},
|
||||||
|
reducers: {
|
||||||
|
setSearchByName: (state, action: PayloadAction<string>) => {
|
||||||
|
state.searchByName = action.payload
|
||||||
|
},
|
||||||
|
setCurrentPage: (state, action: PayloadAction<number>) => {
|
||||||
|
state.currentPage = action.payload
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
42
src/services/decks/decks.types.ts
Normal file
42
src/services/decks/decks.types.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
export type DecksResponse = {
|
||||||
|
maxCardsCount: number
|
||||||
|
pagination: Pagination
|
||||||
|
items: Deck[]
|
||||||
|
}
|
||||||
|
export type Pagination = {
|
||||||
|
totalPages: number
|
||||||
|
currentPage: number
|
||||||
|
itemsPerPage: number
|
||||||
|
totalItems: number
|
||||||
|
}
|
||||||
|
export type Author = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
export type Deck = {
|
||||||
|
id: string
|
||||||
|
userId: string
|
||||||
|
name: string
|
||||||
|
isPrivate?: boolean
|
||||||
|
shots: number
|
||||||
|
cover?: string | null
|
||||||
|
rating: number
|
||||||
|
isDeleted?: any
|
||||||
|
isBlocked?: any
|
||||||
|
created: string
|
||||||
|
updated: string
|
||||||
|
cardsCount: number
|
||||||
|
author: Author
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateDeckArgs = Pick<Deck, 'name' | 'cover' | 'isPrivate'>
|
||||||
|
export type DeleteDeckArgs = Pick<Deck, 'id'>
|
||||||
|
type Direction = 'asc' | 'desc'
|
||||||
|
type Field = 'name' | 'updated'
|
||||||
|
|
||||||
|
export type GetDecksParams = {
|
||||||
|
name?: string
|
||||||
|
authorId?: string
|
||||||
|
orderBy?: `${Field}-${Direction}`
|
||||||
|
currentPage?: number
|
||||||
|
}
|
||||||
2
src/services/decks/index.ts
Normal file
2
src/services/decks/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './decks.service.ts'
|
||||||
|
export * from './decks.types.ts'
|
||||||
20
src/services/store.ts
Normal file
20
src/services/store.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { configureStore } from '@reduxjs/toolkit'
|
||||||
|
import { setupListeners } from '@reduxjs/toolkit/query'
|
||||||
|
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
|
import { baseApi } from '@/services/base-api.ts'
|
||||||
|
import { decksSlice } from '@/services/decks/decks.slice.ts'
|
||||||
|
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: {
|
||||||
|
[baseApi.reducerPath]: baseApi.reducer,
|
||||||
|
[decksSlice.name]: decksSlice.reducer,
|
||||||
|
},
|
||||||
|
middleware: getDefaultMiddleware => getDefaultMiddleware().concat(baseApi.middleware),
|
||||||
|
})
|
||||||
|
setupListeners(store.dispatch)
|
||||||
|
export type AppDispatch = typeof store.dispatch
|
||||||
|
export type RootState = ReturnType<typeof store.getState>
|
||||||
|
|
||||||
|
export const useAppDispatch: () => AppDispatch = useDispatch
|
||||||
|
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
|
||||||
@@ -6,6 +6,9 @@ import { defineConfig } from 'vite'
|
|||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: [{ find: '@', replacement: path.resolve(__dirname, 'src') }],
|
alias: [{ find: '@', replacement: path.resolve(__dirname, 'src') }],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user