feat: add firearm management features

- Implemented API for fetching firearms and firearm details.
- Created a new page for displaying the list of firearms with search and filter options.
- Added Redux slice for managing firearms state.
- Integrated Redux Persist for state persistence.
- Updated routing to include firearms page.
- Removed obsolete modification codes data.
- Enhanced UI with responsive grid layout for firearms display.
- Added utility functions for handling URL query parameters.
This commit is contained in:
2026-04-06 17:57:25 +08:00
parent 864895d932
commit a0a5c835aa
20 changed files with 789 additions and 389 deletions
-5
View File
@@ -1,5 +0,0 @@
{
"chat.tools.terminal.autoApprove": {
"pnpm": true
}
}
+5 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "react-template", "name": "delta-force-guide-web",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
@@ -12,13 +12,17 @@
"predeploy": "pnpm build" "predeploy": "pnpm build"
}, },
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^2.11.2",
"@tailwindcss/vite": "^4.2.2", "@tailwindcss/vite": "^4.2.2",
"@tanstack/react-virtual": "^3.13.23", "@tanstack/react-virtual": "^3.13.23",
"axios": "^1.14.0",
"dayjs": "^1.11.20", "dayjs": "^1.11.20",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"react-redux": "^9.2.0",
"react-router": "^7.13.2", "react-router": "^7.13.2",
"react-router-dom": "^7.13.2", "react-router-dom": "^7.13.2",
"redux-persist": "^6.0.0",
"tailwindcss": "^4.2.2" "tailwindcss": "^4.2.2"
}, },
"devDependencies": { "devDependencies": {
+321
View File
@@ -8,12 +8,18 @@ importers:
.: .:
dependencies: dependencies:
'@reduxjs/toolkit':
specifier: ^2.11.2
version: 2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4)
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.2.2 specifier: ^4.2.2
version: 4.2.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)) version: 4.2.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1))
'@tanstack/react-virtual': '@tanstack/react-virtual':
specifier: ^3.13.23 specifier: ^3.13.23
version: 3.13.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 3.13.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
axios:
specifier: ^1.14.0
version: 1.14.0
dayjs: dayjs:
specifier: ^1.11.20 specifier: ^1.11.20
version: 1.11.20 version: 1.11.20
@@ -23,12 +29,18 @@ importers:
react-dom: react-dom:
specifier: ^19.2.4 specifier: ^19.2.4
version: 19.2.4(react@19.2.4) version: 19.2.4(react@19.2.4)
react-redux:
specifier: ^9.2.0
version: 9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1)
react-router: react-router:
specifier: ^7.13.2 specifier: ^7.13.2
version: 7.13.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 7.13.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react-router-dom: react-router-dom:
specifier: ^7.13.2 specifier: ^7.13.2
version: 7.13.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 7.13.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
redux-persist:
specifier: ^6.0.0
version: 6.0.0(react@19.2.4)(redux@5.0.1)
tailwindcss: tailwindcss:
specifier: ^4.2.2 specifier: ^4.2.2
version: 4.2.2 version: 4.2.2
@@ -250,6 +262,17 @@ packages:
'@oxc-project/types@0.122.0': '@oxc-project/types@0.122.0':
resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==}
'@reduxjs/toolkit@2.11.2':
resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==}
peerDependencies:
react: ^16.9.0 || ^17.0.0 || ^18 || ^19
react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
peerDependenciesMeta:
react:
optional: true
react-redux:
optional: true
'@rolldown/binding-android-arm64@1.0.0-rc.12': '@rolldown/binding-android-arm64@1.0.0-rc.12':
resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
@@ -285,36 +308,42 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12':
resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12':
resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12':
resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12':
resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.0.0-rc.12': '@rolldown/binding-linux-x64-musl@1.0.0-rc.12':
resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.0.0-rc.12': '@rolldown/binding-openharmony-arm64@1.0.0-rc.12':
resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==}
@@ -345,6 +374,12 @@ packages:
'@rolldown/pluginutils@1.0.0-rc.7': '@rolldown/pluginutils@1.0.0-rc.7':
resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==}
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
'@standard-schema/utils@0.3.0':
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
'@tailwindcss/node@4.2.2': '@tailwindcss/node@4.2.2':
resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==} resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==}
@@ -383,24 +418,28 @@ packages:
engines: {node: '>= 20'} engines: {node: '>= 20'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.2.2': '@tailwindcss/oxide-linux-arm64-musl@4.2.2':
resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.2.2': '@tailwindcss/oxide-linux-x64-gnu@4.2.2':
resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.2.2': '@tailwindcss/oxide-linux-x64-musl@4.2.2':
resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.2.2': '@tailwindcss/oxide-wasm32-wasi@4.2.2':
resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==}
@@ -458,6 +497,9 @@ packages:
'@types/react@19.2.14': '@types/react@19.2.14':
resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
'@types/use-sync-external-store@0.0.6':
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
'@vitejs/plugin-react@6.0.1': '@vitejs/plugin-react@6.0.1':
resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
@@ -471,6 +513,20 @@ packages:
babel-plugin-react-compiler: babel-plugin-react-compiler:
optional: true optional: true
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
axios@1.14.0:
resolution: {integrity: sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
cookie@1.1.1: cookie@1.1.1:
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -481,14 +537,38 @@ packages:
dayjs@1.11.20: dayjs@1.11.20:
resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
detect-libc@2.1.2: detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
enhanced-resolve@5.20.1: enhanced-resolve@5.20.1:
resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
es-errors@1.3.0:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
es-set-tostringtag@2.1.0:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
esbuild@0.27.4: esbuild@0.27.4:
resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -503,18 +583,61 @@ packages:
picomatch: picomatch:
optional: true optional: true
follow-redirects@1.15.11:
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
form-data@4.0.5:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
fsevents@2.3.3: fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin] os: [darwin]
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
globals@17.4.0: globals@17.4.0:
resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==} resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==}
engines: {node: '>=18'} engines: {node: '>=18'}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
graceful-fs@4.2.11: graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
has-tostringtag@1.0.2:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
immer@11.1.4:
resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==}
jiti@2.6.1: jiti@2.6.1:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true hasBin: true
@@ -554,24 +677,28 @@ packages:
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0: lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.32.0: lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.32.0: lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.32.0: lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
@@ -592,6 +719,18 @@ packages:
magic-string@0.30.21: magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
nanoid@3.3.11: nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -613,11 +752,27 @@ packages:
engines: {node: '>=14'} engines: {node: '>=14'}
hasBin: true hasBin: true
proxy-from-env@2.1.0:
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
engines: {node: '>=10'}
react-dom@19.2.4: react-dom@19.2.4:
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
peerDependencies: peerDependencies:
react: ^19.2.4 react: ^19.2.4
react-redux@9.2.0:
resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==}
peerDependencies:
'@types/react': ^18.2.25 || ^19
react: ^18.0 || ^19
redux: ^5.0.0
peerDependenciesMeta:
'@types/react':
optional: true
redux:
optional: true
react-router-dom@7.13.2: react-router-dom@7.13.2:
resolution: {integrity: sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA==} resolution: {integrity: sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA==}
engines: {node: '>=20.0.0'} engines: {node: '>=20.0.0'}
@@ -639,6 +794,26 @@ packages:
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
redux-persist@6.0.0:
resolution: {integrity: sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==}
peerDependencies:
react: '>=16'
redux: '>4.0.0'
peerDependenciesMeta:
react:
optional: true
redux-thunk@3.1.0:
resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
peerDependencies:
redux: ^5.0.0
redux@5.0.1:
resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
reselect@5.1.1:
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
rolldown@1.0.0-rc.12: rolldown@1.0.0-rc.12:
resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
@@ -676,6 +851,11 @@ packages:
undici-types@6.21.0: undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
use-sync-external-store@1.6.0:
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
vite@8.0.3: vite@8.0.3:
resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
@@ -843,6 +1023,18 @@ snapshots:
'@oxc-project/types@0.122.0': {} '@oxc-project/types@0.122.0': {}
'@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4)':
dependencies:
'@standard-schema/spec': 1.1.0
'@standard-schema/utils': 0.3.0
immer: 11.1.4
redux: 5.0.1
redux-thunk: 3.1.0(redux@5.0.1)
reselect: 5.1.1
optionalDependencies:
react: 19.2.4
react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1)
'@rolldown/binding-android-arm64@1.0.0-rc.12': '@rolldown/binding-android-arm64@1.0.0-rc.12':
optional: true optional: true
@@ -897,6 +1089,10 @@ snapshots:
'@rolldown/pluginutils@1.0.0-rc.7': {} '@rolldown/pluginutils@1.0.0-rc.7': {}
'@standard-schema/spec@1.1.0': {}
'@standard-schema/utils@0.3.0': {}
'@tailwindcss/node@4.2.2': '@tailwindcss/node@4.2.2':
dependencies: dependencies:
'@jridgewell/remapping': 2.3.5 '@jridgewell/remapping': 2.3.5
@@ -990,24 +1186,68 @@ snapshots:
dependencies: dependencies:
csstype: 3.2.3 csstype: 3.2.3
'@types/use-sync-external-store@0.0.6': {}
'@vitejs/plugin-react@6.0.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1))': '@vitejs/plugin-react@6.0.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1))':
dependencies: dependencies:
'@rolldown/pluginutils': 1.0.0-rc.7 '@rolldown/pluginutils': 1.0.0-rc.7
vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1) vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)
asynckit@0.4.0: {}
axios@1.14.0:
dependencies:
follow-redirects: 1.15.11
form-data: 4.0.5
proxy-from-env: 2.1.0
transitivePeerDependencies:
- debug
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
function-bind: 1.1.2
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
cookie@1.1.1: {} cookie@1.1.1: {}
csstype@3.2.3: {} csstype@3.2.3: {}
dayjs@1.11.20: {} dayjs@1.11.20: {}
delayed-stream@1.0.0: {}
detect-libc@2.1.2: {} detect-libc@2.1.2: {}
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
es-errors: 1.3.0
gopd: 1.2.0
enhanced-resolve@5.20.1: enhanced-resolve@5.20.1:
dependencies: dependencies:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
tapable: 2.3.0 tapable: 2.3.0
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
es-set-tostringtag@2.1.0:
dependencies:
es-errors: 1.3.0
get-intrinsic: 1.3.0
has-tostringtag: 1.0.2
hasown: 2.0.2
esbuild@0.27.4: esbuild@0.27.4:
optionalDependencies: optionalDependencies:
'@esbuild/aix-ppc64': 0.27.4 '@esbuild/aix-ppc64': 0.27.4
@@ -1042,13 +1282,57 @@ snapshots:
optionalDependencies: optionalDependencies:
picomatch: 4.0.4 picomatch: 4.0.4
follow-redirects@1.15.11: {}
form-data@4.0.5:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.2
mime-types: 2.1.35
fsevents@2.3.3: fsevents@2.3.3:
optional: true optional: true
function-bind@1.1.2: {}
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
es-define-property: 1.0.1
es-errors: 1.3.0
es-object-atoms: 1.1.1
function-bind: 1.1.2
get-proto: 1.0.1
gopd: 1.2.0
has-symbols: 1.1.0
hasown: 2.0.2
math-intrinsics: 1.1.0
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
globals@17.4.0: {} globals@17.4.0: {}
gopd@1.2.0: {}
graceful-fs@4.2.11: {} graceful-fs@4.2.11: {}
has-symbols@1.1.0: {}
has-tostringtag@1.0.2:
dependencies:
has-symbols: 1.1.0
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
immer@11.1.4: {}
jiti@2.6.1: {} jiti@2.6.1: {}
lightningcss-android-arm64@1.32.0: lightningcss-android-arm64@1.32.0:
@@ -1104,6 +1388,14 @@ snapshots:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
math-intrinsics@1.1.0: {}
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
nanoid@3.3.11: {} nanoid@3.3.11: {}
picocolors@1.1.1: {} picocolors@1.1.1: {}
@@ -1118,11 +1410,22 @@ snapshots:
prettier@3.8.1: {} prettier@3.8.1: {}
proxy-from-env@2.1.0: {}
react-dom@19.2.4(react@19.2.4): react-dom@19.2.4(react@19.2.4):
dependencies: dependencies:
react: 19.2.4 react: 19.2.4
scheduler: 0.27.0 scheduler: 0.27.0
react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1):
dependencies:
'@types/use-sync-external-store': 0.0.6
react: 19.2.4
use-sync-external-store: 1.6.0(react@19.2.4)
optionalDependencies:
'@types/react': 19.2.14
redux: 5.0.1
react-router-dom@7.13.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): react-router-dom@7.13.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies: dependencies:
react: 19.2.4 react: 19.2.4
@@ -1139,6 +1442,20 @@ snapshots:
react@19.2.4: {} react@19.2.4: {}
redux-persist@6.0.0(react@19.2.4)(redux@5.0.1):
dependencies:
redux: 5.0.1
optionalDependencies:
react: 19.2.4
redux-thunk@3.1.0(redux@5.0.1):
dependencies:
redux: 5.0.1
redux@5.0.1: {}
reselect@5.1.1: {}
rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1):
dependencies: dependencies:
'@oxc-project/types': 0.122.0 '@oxc-project/types': 0.122.0
@@ -1185,6 +1502,10 @@ snapshots:
undici-types@6.21.0: {} undici-types@6.21.0: {}
use-sync-external-store@1.6.0(react@19.2.4):
dependencies:
react: 19.2.4
vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1): vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1):
dependencies: dependencies:
lightningcss: 1.32.0 lightningcss: 1.32.0
+38
View File
@@ -0,0 +1,38 @@
import { Direction, Firearm, FirearmType, Page, PageQueryParams } from "@/types"
import { WebClient } from "@/shared/web-client"
import { asUrlSearchParam } from "@/utils/query-param-utils.ts"
interface FirearmParams extends PageQueryParams {
type?: FirearmType
}
/**
* 查询武器列表
*
* @param params 分页查询参数¬
*/
export async function getFirearms(params?: FirearmParams): Promise<Page<Firearm>> {
let uri = "/firearms"
const urlSearchParam = asUrlSearchParam(params)
if (params?.type) {
urlSearchParam.append("type", params.type)
}
if (urlSearchParam.size > 0) {
uri = uri.concat("?", urlSearchParam.toString())
}
const { data } = await WebClient.get<Page<Firearm>>(uri)
return data
}
/**
* 根据 ID 查询武器
*
* @param id 武器 ID
*/
export async function getFirearm(id: number): Promise<Firearm> {
const { data } = await WebClient.get<Firearm>(`/firearms/${id}`)
return data
}
+2
View File
@@ -0,0 +1,2 @@
export * as FirearmApi from "./firearm-api"
export * as ModificationApi from "./modification-api"
+25
View File
@@ -0,0 +1,25 @@
import { Modification, Page, PageQueryParams } from "@/types"
import { WebClient } from "@/shared/web-client"
import { asUrlSearchParam } from "@/utils/query-param-utils.ts"
interface ModificationParams extends PageQueryParams {
firearmId?: string
}
export async function getModifications(params?: ModificationParams): Promise<Page<Modification>> {
let uri = "/modifications"
const urlSearchParams = asUrlSearchParam(params)
if (params?.firearmId) {
urlSearchParams.append("firearmId", "" + params.firearmId)
}
if (urlSearchParams.size > 0) {
uri = uri.concat("?", urlSearchParams.toString())
}
const { data } = await WebClient.get<Page<Modification>>(uri)
return data
}
export async function getModification(id: number): Promise<Modification> {
const { data } = await WebClient.get<Modification>(`/modifications/${id}`)
return data
}
-114
View File
@@ -1,114 +0,0 @@
[
{
"weapon": "SCAR-H战斗步枪",
"modification-code": "6JJE3180BB9B3KKCCH41C",
"mode": "烽火地带",
"tags": ["精锐制式券", "突击步枪", "稳定"],
"note": "精锐火力券提供的 SCAR-H",
"price": 445295
},
{
"weapon": "M4A1突击步枪",
"modification-code": "6JAS9U80BB9B3KKCCH41C",
"mode": "烽火地带",
"tags": ["稳定", "突击步枪", "倍镜"],
"note": "带有三倍镜的稳定版 M4A1,全套价格左右。",
"price": 400000
},
{
"weapon": "M4A1突击步枪",
"modification-code": "6J9D8SO0BB9B3KKCCH41C",
"mode": "烽火地带",
"tags": ["突击步枪", "性价比"],
"note": "性价比改装稳定版 M4A1。",
"price": 242638
},
{
"weapon": "MK4冲锋枪",
"modification-code": "6IVR0N40BB9B3KKCCH41C",
"mode": "烽火地带",
"tags": ["超绝腰射", "三连发"],
"note": "超绝三连发腰射 MK4,近距离火力十足",
"price": 538686
},
{
"weapon": "MK4冲锋枪",
"modification-code": "6J7283O0BB9B3KKCCH41C",
"mode": "烽火地带",
"tags": ["超绝腰射", "全自动"],
"note": "超绝全自动腰射 MK4,见到人瞄准好之后左键按到死",
"price": 466836
},
{
"weapon": "M7战斗步枪",
"modification-code": "6IVH3HC0BB9B3KKCCH41C",
"mode": "烽火地带",
"tags": ["半改", "蓝管", "稳定"],
"note": "半改稳定 M7",
"price": 620597
},
{
"weapon": "M7战斗步枪",
"modification-code": "6J2QBQK0BB9B3KKCCH41C",
"mode": "烽火地带",
"tags": ["倍镜", "职业选手", "消音"],
"note": "需要较高控枪水平的满改 M7",
"price": 891030
},
{
"weapon": "M7战斗步枪",
"modification-code": "6J2QC080BB9B3KKCCH41C",
"mode": "烽火地带",
"tags": ["稳定"],
"note": "S8 赛季幻影版满改稳定 M751操控",
"price": 1039234
},
{
"weapon": "M7战斗步枪",
"modification-code": "6J2QC4O0BB9B3KKCCH41C",
"mode": "烽火地带",
"tags": ["稳定"],
"note": "S8 赛季隐袭版满改稳定 M749操控",
"price": 1064418
},
{
"weapon": "MK47突击步枪",
"modification-code": "6JAV6A80BB9B3KKCCH41C",
"mode": "烽火地带",
"tags": ["突袭", "大口径", "消音"],
"note": "S8 赛季余烬MK47,携带消音器",
"price": 862384
},
{
"weapon": "MK47突击步枪",
"modification-code": "6JAV6SS0BB9B3KKCCH41C",
"mode": "烽火地带",
"tags": ["突袭", "稳定"],
"note": "幻影余烬主宰者MK47",
"price": 864227
},
{
"weapon": "AWM狙击步枪",
"modification-code": "6JAS7DG0BB9B3KKCCH41C",
"mode": "烽火地带",
"tags": [],
"note": "",
"price": 899872
},
{
"weapon": "MCX LT突击步枪",
"modification-code": "6JJGQFS0BB9B3KKCCH41C",
"mode": "烽火地带",
"tags": ["稳定", "常规"],
"note": "中距离机密局好用的突击步枪",
"price": 474581
},
{
"weapon": "KC17突击步枪",
"modification-code": "6JJGQQ40BB9B3KKCCH41C",
"mode": "烽火地带",
"tags": ["性价比"],
"note": "性价比很高",
"price": 373532
}
]
+23
View File
@@ -9,3 +9,26 @@ html, body {
#root { #root {
width: 100%; width: 100%;
} }
.mod-codes-grid {
display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr));
}
@media (min-width: 640px) {
.mod-codes-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 1024px) {
.mod-codes-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (min-width: 1280px) {
.mod-codes-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
+6
View File
@@ -21,6 +21,12 @@ export default function HeroLayout() {
</h1> </h1>
</div> </div>
<nav className="flex space-x-8"> <nav className="flex space-x-8">
<Link
to="/firearms"
className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
>
</Link>
<Link <Link
to="/mod-codes" to="/mod-codes"
className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium" className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
+7
View File
@@ -1,8 +1,11 @@
import { StrictMode } from "react" import { StrictMode } from "react"
import { createRoot } from "react-dom/client" import { createRoot } from "react-dom/client"
import { RouterProvider } from "react-router-dom" import { RouterProvider } from "react-router-dom"
import { Provider } from "react-redux"
import { PersistGate } from "redux-persist/integration/react"
import "@/init" import "@/init"
import router from "@/router" import router from "@/router"
import store, { persistor } from "@/store"
import "./index.css" import "./index.css"
/** /**
@@ -11,6 +14,10 @@ import "./index.css"
*/ */
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<RouterProvider router={router} /> <RouterProvider router={router} />
</PersistGate>
</Provider>
</StrictMode>, </StrictMode>,
) )
+187
View File
@@ -0,0 +1,187 @@
import { useEffect, useMemo, useState } from "react"
import { Link } from "react-router-dom"
import { FirearmApi } from "@/api"
import { Firearm, FirearmType } from "@/types"
import { setFirearmsPage } from "@/store/firearms-slice"
import { useAppDispatch, useAppSelector } from "@/store"
const firearmTypeText: Record<FirearmType, string> = {
RIFLE: "步枪",
SUB_MACHINE_GUN: "冲锋枪",
SHOTGUN: "霰弹枪",
LIGHT_MACHINE_GUN: "轻机枪",
DESIGNATED_MARKSMAN_RIFLE: "射手步枪",
SNIPER_RIFLE: "狙击步枪",
PISTOL: "手枪",
SPECIAL: "特殊",
}
export default function FirearmsPage() {
const pageSize = 12
const dispatch = useAppDispatch()
const firearmsState = useAppSelector((state) => state.firearms)
const firearms = firearmsState.items
const [isLoading, setIsLoading] = useState<boolean>(true)
const [isRefreshing, setIsRefreshing] = useState<boolean>(false)
const [loadError, setLoadError] = useState<string | null>(null)
const [keyword, setKeyword] = useState<string>("")
const [activeType, setActiveType] = useState<"全部" | FirearmType>("全部")
const [currentPage, setCurrentPage] = useState<number>(firearmsState.page)
const fetchFirearms = (page: number, forceRefresh = false) => {
if (!forceRefresh && firearms.length > 0 && page === firearmsState.page) {
setIsLoading(false)
setLoadError(null)
return
}
setIsLoading(true)
setLoadError(null)
if (forceRefresh) {
setIsRefreshing(true)
}
FirearmApi.getFirearms({
page,
size: pageSize,
sortBy: "name",
direction: "ASC",
})
.then((page) => {
dispatch(setFirearmsPage(page))
})
.catch(() => {
setLoadError("武器列表加载失败,请确认后端服务是否已启动。")
})
.finally(() => {
setIsLoading(false)
setIsRefreshing(false)
})
}
useEffect(() => {
fetchFirearms(currentPage, false)
}, [currentPage, dispatch])
useEffect(() => {
if (firearmsState.page !== currentPage) {
setCurrentPage(firearmsState.page)
}
}, [currentPage, firearmsState.page])
const filteredFirearms = useMemo(() => {
const trimmed = keyword.trim().toLowerCase()
return firearms.filter((item) => {
const matchKeyword = !trimmed || item.name.toLowerCase().includes(trimmed)
const matchType = activeType === "全部" || item.type === activeType
return matchKeyword && matchType
})
}, [activeType, firearms, keyword])
return (
<section className="space-y-6">
<div className="space-y-4 px-1">
<div className="flex flex-wrap items-center justify-between gap-3">
<p className="text-sm text-gray-600"></p>
<button
type="button"
onClick={() => fetchFirearms(currentPage, true)}
disabled={isRefreshing}
className="rounded-lg border border-gray-300 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-60"
>
{isRefreshing ? "刷新中..." : "强制刷新"}
</button>
</div>
{isLoading ? <p className="text-sm text-gray-500">...</p> : null}
{loadError ? <p className="text-sm text-red-600">{loadError}</p> : null}
<div className="grid gap-3 sm:grid-cols-2 max-w-2xl">
<label className="block">
<span className="block text-sm font-medium text-gray-700 mb-1"></span>
<input
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
placeholder="例如:M4A1"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-100 bg-white"
/>
</label>
<label className="block">
<span className="block text-sm font-medium text-gray-700 mb-1"></span>
<select
value={activeType}
onChange={(event) => setActiveType(event.target.value as "全部" | FirearmType)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-100 bg-white"
>
<option value="全部"></option>
{Object.entries(firearmTypeText).map(([type, text]) => (
<option key={type} value={type}>{text}</option>
))}
</select>
</label>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredFirearms.map((item) => (
<article key={item.id} className="bg-white border rounded-xl p-4 shadow-sm space-y-3">
<div className="flex items-start justify-between gap-3">
<h2 className="text-lg font-semibold text-gray-900">{item.name}</h2>
<span className="text-xs rounded-full bg-gray-100 text-gray-700 px-2 py-1">
{firearmTypeText[item.type]}
</span>
</div>
<div className="flex flex-wrap gap-2 text-xs text-gray-600">
{item.level ? (
<span className="rounded-full bg-gray-100 px-2 py-1">{item.level}</span>
) : null}
</div>
{item.review ? (
<p className="text-sm text-gray-600 whitespace-pre-line">{item.review}</p>
) : null}
<Link
to={`/mod-codes?firearmId=${encodeURIComponent(item.id)}`}
className="inline-flex items-center justify-center rounded-lg border border-blue-300 bg-blue-50 px-3 py-2 text-sm font-medium text-blue-700 hover:bg-blue-100">
</Link>
</article>
))}
</div>
{!isLoading && filteredFirearms.length === 0 ? (
<div className="bg-white border rounded-xl p-6 text-center text-gray-600">
</div>
) : null}
<div className="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-gray-200 bg-white px-3 py-2">
<p className="text-sm text-gray-600">
{firearmsState.page + 1} / {Math.max(firearmsState.totalPages, 1)} {firearmsState.totalElements}
</p>
<div className="flex items-center gap-2">
<button
type="button"
disabled={isLoading || currentPage <= 0}
onClick={() => setCurrentPage((page) => Math.max(page - 1, 0))}
className="rounded-lg border border-gray-300 px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-60"
>
</button>
<button
type="button"
disabled={isLoading || firearmsState.totalPages === 0 || currentPage >= firearmsState.totalPages - 1}
onClick={() => setCurrentPage((page) => page + 1)}
className="rounded-lg border border-gray-300 px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-60"
>
</button>
</div>
</div>
</section>
)
}
-267
View File
@@ -1,267 +0,0 @@
import { useMemo, useState, useEffect, useLayoutEffect, useRef } from "react"
import { useWindowVirtualizer } from "@tanstack/react-virtual"
import rawModCodes from "@/data/modification-codes.json"
function useColumnCount() {
const getCount = () => {
if (window.innerWidth >= 1024) return 4
if (window.innerWidth >= 768) return 3
if (window.innerWidth >= 640) return 2
return 1
}
const [cols, setCols] = useState(getCount)
useEffect(() => {
const handler = () => setCols(getCount())
window.addEventListener("resize", handler)
return () => window.removeEventListener("resize", handler)
}, [])
return cols
}
type ModCodeSource = {
weapon: string
"modification-code": string
mode?: string
tags?: string[]
note?: string
price?: number
}
type ModCode = {
id: string
weapon: string
code: string
mode?: string
tags: string[]
note?: string
price?: number
}
const MOD_CODES: ModCode[] = (rawModCodes as ModCodeSource[]).map((item, index) => ({
id: `mod-${index + 1}`,
weapon: item.weapon,
code: item["modification-code"],
mode: item.mode,
tags: item.tags ?? [],
note: item.note,
price: item.price,
}))
export default function ModCodes() {
const [activeTag, setActiveTag] = useState<string>("全部")
const [activeWeapon, setActiveWeapon] = useState<string>("全部")
const [activeMode, setActiveMode] = useState<string>("全部")
const [copiedId, setCopiedId] = useState<string | null>(null)
const [copyErrorId, setCopyErrorId] = useState<string | null>(null)
const handleCopy = async (item: ModCode) => {
try {
await navigator.clipboard.writeText(item.code)
setCopyErrorId(null)
setCopiedId(item.id)
window.setTimeout(() => {
setCopiedId((current) => (current === item.id ? null : current))
}, 1500)
} catch {
setCopiedId(null)
setCopyErrorId(item.id)
window.setTimeout(() => {
setCopyErrorId((current) => (current === item.id ? null : current))
}, 1500)
}
}
const allWeapons = useMemo(() => {
const weapons = new Set<string>()
MOD_CODES.forEach((item) => weapons.add(item.weapon))
return ["全部", ...Array.from(weapons)]
}, [])
const allModes = useMemo(() => {
const modes = new Set<string>()
MOD_CODES.forEach((item) => {
if (item.mode) {
modes.add(item.mode)
}
})
return ["全部", ...Array.from(modes)]
}, [])
const allTags = useMemo(() => {
const tags = new Set<string>()
MOD_CODES.forEach((item) => {
item.tags.forEach((tag) => tags.add(tag))
})
return ["全部", ...Array.from(tags)]
}, [])
const filtered = useMemo(() => {
return MOD_CODES.filter((item) => {
const matchWeapon = activeWeapon === "全部" || item.weapon === activeWeapon
const matchMode = activeMode === "全部" || item.mode === activeMode
const matchTag = activeTag === "全部" || item.tags.includes(activeTag)
return matchWeapon && matchMode && matchTag
})
}, [activeMode, activeWeapon, activeTag])
const colCount = useColumnCount()
const rows = useMemo<ModCode[][]>(() => {
const result: ModCode[][] = []
for (let i = 0; i < filtered.length; i += colCount) {
result.push(filtered.slice(i, i + colCount))
}
return result
}, [filtered, colCount])
const listRef = useRef<HTMLDivElement>(null)
const scrollMarginRef = useRef(0)
useLayoutEffect(() => {
scrollMarginRef.current = listRef.current?.offsetTop ?? 0
})
const rowVirtualizer = useWindowVirtualizer({
count: rows.length,
estimateSize: () => 220,
overscan: 3,
scrollMargin: scrollMarginRef.current,
})
return (
<section className="space-y-6">
<div className="space-y-4 px-1">
<p className="text-sm text-gray-600"> tag </p>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
<label className="block">
<span className="block text-sm font-medium text-gray-700 mb-1"></span>
<select
value={activeWeapon}
onChange={(event) => setActiveWeapon(event.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-100 bg-white"
>
{allWeapons.map((weapon) => (
<option key={weapon} value={weapon}>{weapon}</option>
))}
</select>
</label>
<label className="block">
<span className="block text-sm font-medium text-gray-700 mb-1"></span>
<select
value={activeMode}
onChange={(event) => setActiveMode(event.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-100 bg-white"
>
{allModes.map((mode) => (
<option key={mode} value={mode}>{mode}</option>
))}
</select>
</label>
<div className="flex items-end">
<button
type="button"
onClick={() => {
setActiveTag("全部")
setActiveWeapon("全部")
setActiveMode("全部")
}}
className="rounded-lg border border-gray-300 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50"
>
</button>
</div>
</div>
<div className="flex flex-wrap gap-2">
{allTags.map((tag) => {
const selected = tag === activeTag
return (
<button
key={tag}
type="button"
onClick={() => setActiveTag(tag)}
className={`rounded-full px-3 py-1 text-sm border transition ${
selected
? "border-blue-600 bg-blue-600 text-white"
: "border-gray-300 bg-white text-gray-700 hover:bg-gray-100"
}`}
>
#{tag}
</button>
)
})}
</div>
</div>
<div
ref={listRef}
style={{ height: `${rowVirtualizer.getTotalSize()}px`, position: "relative" }}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.index}
data-index={virtualRow.index}
ref={rowVirtualizer.measureElement}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
transform: `translateY(${virtualRow.start - rowVirtualizer.options.scrollMargin}px)`,
}}
>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 pb-4">
{rows[virtualRow.index].map((item) => (
<article key={item.id} className="bg-white border rounded-xl p-4 shadow-sm space-y-3">
<div className="flex items-center justify-between gap-3">
<h2 className="text-lg font-semibold text-gray-900">{item.weapon}</h2>
{item.mode ? (
<span className="text-xs rounded-full bg-gray-100 text-gray-700 px-2 py-1">
{item.mode}
</span>
) : (
<span className="text-xs text-gray-500">ID: {item.id}</span>
)}
</div>
<div className="rounded-lg bg-gray-900 text-green-300 px-3 py-2 font-mono text-sm break-all flex items-center justify-between gap-3">
<span>{item.code}</span>
<button
type="button"
onClick={() => handleCopy(item)}
className="shrink-0 rounded-md bg-green-400/10 border border-green-400/40 text-green-200 px-2 py-1 text-xs hover:bg-green-400/20"
>
{copiedId === item.id ? "已复制" : "复制改枪码"}
</button>
</div>
{copyErrorId === item.id ? (
<p className="text-xs text-red-600"></p>
) : null}
{item.price ? <p className="text-sm text-gray-900 font-medium">$ {item.price.toLocaleString()}</p> : null}
{item.note ? <p className="text-sm text-gray-600">{item.note}</p> : null}
<div className="flex flex-wrap gap-2">
{item.tags.map((tag) => (
<button
key={`${item.id}-${tag}`}
type="button"
onClick={() => setActiveTag(tag)}
className="text-xs rounded-full bg-blue-50 text-blue-700 px-2 py-1 hover:bg-blue-100"
>
#{tag}
</button>
))}
</div>
</article>
))}
</div>
</div>
))}
</div>
{filtered.length === 0 ? (
<div className="bg-white border rounded-xl p-6 text-center text-gray-600">
tag
</div>
) : null}
</section>
)
}
+5 -1
View File
@@ -25,7 +25,11 @@ const router = createBrowserRouter(
children: [ children: [
{ {
index: true, index: true,
lazy: lazy(() => import("@/page/mod-codes")), lazy: lazy(() => import("@/page/firearms")),
},
{
path: "firearms",
lazy: lazy(() => import("@/page/firearms")),
}, },
{ {
path: "mod-codes", path: "mod-codes",
+9
View File
@@ -0,0 +1,9 @@
import axios from "axios"
import dayjs from "dayjs"
const WebClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: dayjs.duration({ seconds: 10 }).asMilliseconds(),
})
export { WebClient }
+42
View File
@@ -0,0 +1,42 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"
import { Firearm, Page } from "@/types"
interface FirearmsState {
items: Firearm[]
page: number
size: number
totalPages: number
totalElements: number
}
const initialState: FirearmsState = {
items: [],
page: 0,
size: 12,
totalPages: 0,
totalElements: 0,
}
const firearmsSlice = createSlice({
name: "firearms",
initialState,
reducers: {
setFirearmsPage(state, action: PayloadAction<Page<Firearm>>) {
state.items = action.payload.items
state.page = action.payload.page
state.size = action.payload.size
state.totalPages = action.payload.totalPages
state.totalElements = action.payload.totalElements
},
clearFirearms(state) {
state.items = []
state.page = 0
state.size = 12
state.totalPages = 0
state.totalElements = 0
},
},
})
export const { setFirearmsPage, clearFirearms } = firearmsSlice.actions
export const firearmsReducer = firearmsSlice.reducer
+48
View File
@@ -0,0 +1,48 @@
import { configureStore, combineReducers } from "@reduxjs/toolkit"
import { useDispatch, useSelector } from "react-redux"
import {
persistStore,
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from "redux-persist"
import createWebStorage from "redux-persist/es/storage/createWebStorage"
import { firearmsReducer } from "./firearms-slice"
const storage = createWebStorage(import.meta.env.VITE_REDUX_STORAGE ?? "local")
const persistConfig = {
key: "root",
storage,
whitelist: ["firearms"],
}
const rootReducer = combineReducers({
firearms: firearmsReducer
})
const persistedReducer = persistReducer(persistConfig, rootReducer)
const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
})
export const persistor = persistStore(store)
export default store
export type RootState = ReturnType<typeof rootReducer>
export type AppDispatch = typeof store.dispatch
export type AppStore = typeof store
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
+45
View File
@@ -0,0 +1,45 @@
export type Direction = "ASC" | "DESC"
export type FirearmType =
| "RIFLE"
| "SUB_MACHINE_GUN"
| "SHOTGUN"
| "LIGHT_MACHINE_GUN"
| "DESIGNATED_MARKSMAN_RIFLE"
| "SNIPER_RIFLE"
| "PISTOL"
| "SPECIAL"
export interface Page<T> {
items: T[]
page: number
size: number
totalElements: number
totalPages: number
}
export interface Firearm {
id: number
name: string
type: FirearmType
level: string
review: string
}
export interface Modification {
id: number
firearmId: number
name: string
code: string
tags: string[]
note: string
author: string
videoUrl: string
}
export interface PageQueryParams {
page?: number
size?: number
sortBy?: string
direction?: Direction
}
+1
View File
@@ -0,0 +1 @@
export * as QueryParamUtils from "./query-param-utils"
+23
View File
@@ -0,0 +1,23 @@
import { PageQueryParams } from "@/types"
export function asUrlSearchParam(pageQueryParams?: PageQueryParams) {
const urlSearchParams = new URLSearchParams()
if (pageQueryParams?.page) {
urlSearchParams.append("page", "" + pageQueryParams.page)
}
if (pageQueryParams?.size) {
urlSearchParams.append("size", "" + pageQueryParams.size)
}
if (pageQueryParams?.sortBy) {
urlSearchParams.append("sortBy", pageQueryParams.sortBy)
}
if (pageQueryParams?.direction) {
urlSearchParams.append("direction", pageQueryParams.direction)
}
return urlSearchParams
}
+1
View File
@@ -1,6 +1,7 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
interface ImportMetaEnv { interface ImportMetaEnv {
readonly VITE_REDUX_STORAGE: "local" | "session" readonly VITE_REDUX_STORAGE: "local" | "session"
readonly VITE_API_BASE_URL: string
} }
interface ImportMeta { interface ImportMeta {