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:
Vendored
-5
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"chat.tools.terminal.autoApprove": {
|
||||
"pnpm": true
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "react-template",
|
||||
"name": "delta-force-guide-web",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
@@ -12,13 +12,17 @@
|
||||
"predeploy": "pnpm build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@tanstack/react-virtual": "^3.13.23",
|
||||
"axios": "^1.14.0",
|
||||
"dayjs": "^1.11.20",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router": "^7.13.2",
|
||||
"react-router-dom": "^7.13.2",
|
||||
"redux-persist": "^6.0.0",
|
||||
"tailwindcss": "^4.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Generated
+321
@@ -8,12 +8,18 @@ importers:
|
||||
|
||||
.:
|
||||
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':
|
||||
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))
|
||||
'@tanstack/react-virtual':
|
||||
specifier: ^3.13.23
|
||||
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:
|
||||
specifier: ^1.11.20
|
||||
version: 1.11.20
|
||||
@@ -23,12 +29,18 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^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:
|
||||
specifier: ^7.13.2
|
||||
version: 7.13.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
react-router-dom:
|
||||
specifier: ^7.13.2
|
||||
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:
|
||||
specifier: ^4.2.2
|
||||
version: 4.2.2
|
||||
@@ -250,6 +262,17 @@ packages:
|
||||
'@oxc-project/types@0.122.0':
|
||||
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':
|
||||
resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -285,36 +308,42 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.12':
|
||||
resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12':
|
||||
resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12':
|
||||
resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.12':
|
||||
resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.0.0-rc.12':
|
||||
resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.0.0-rc.12':
|
||||
resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==}
|
||||
@@ -345,6 +374,12 @@ packages:
|
||||
'@rolldown/pluginutils@1.0.0-rc.7':
|
||||
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':
|
||||
resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==}
|
||||
|
||||
@@ -383,24 +418,28 @@ packages:
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.2.2':
|
||||
resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.2.2':
|
||||
resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.2.2':
|
||||
resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.2.2':
|
||||
resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==}
|
||||
@@ -458,6 +497,9 @@ packages:
|
||||
'@types/react@19.2.14':
|
||||
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':
|
||||
resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -471,6 +513,20 @@ packages:
|
||||
babel-plugin-react-compiler:
|
||||
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:
|
||||
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -481,14 +537,38 @@ packages:
|
||||
dayjs@1.11.20:
|
||||
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:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
enhanced-resolve@5.20.1:
|
||||
resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -503,18 +583,61 @@ packages:
|
||||
picomatch:
|
||||
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:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
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:
|
||||
resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
gopd@1.2.0:
|
||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
graceful-fs@4.2.11:
|
||||
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:
|
||||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||
hasBin: true
|
||||
@@ -554,24 +677,28 @@ packages:
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.32.0:
|
||||
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.32.0:
|
||||
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.32.0:
|
||||
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.32.0:
|
||||
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
|
||||
@@ -592,6 +719,18 @@ packages:
|
||||
magic-string@0.30.21:
|
||||
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:
|
||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
@@ -613,11 +752,27 @@ packages:
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
proxy-from-env@2.1.0:
|
||||
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
react-dom@19.2.4:
|
||||
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
|
||||
peerDependencies:
|
||||
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:
|
||||
resolution: {integrity: sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
@@ -639,6 +794,26 @@ packages:
|
||||
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
|
||||
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:
|
||||
resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -676,6 +851,11 @@ packages:
|
||||
undici-types@6.21.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -843,6 +1023,18 @@ snapshots:
|
||||
|
||||
'@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':
|
||||
optional: true
|
||||
|
||||
@@ -897,6 +1089,10 @@ snapshots:
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-rc.7': {}
|
||||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
|
||||
'@standard-schema/utils@0.3.0': {}
|
||||
|
||||
'@tailwindcss/node@4.2.2':
|
||||
dependencies:
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
@@ -990,24 +1186,68 @@ snapshots:
|
||||
dependencies:
|
||||
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))':
|
||||
dependencies:
|
||||
'@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)
|
||||
|
||||
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: {}
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
dayjs@1.11.20: {}
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
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:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
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:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.27.4
|
||||
@@ -1042,13 +1282,57 @@ snapshots:
|
||||
optionalDependencies:
|
||||
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:
|
||||
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: {}
|
||||
|
||||
gopd@1.2.0: {}
|
||||
|
||||
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: {}
|
||||
|
||||
lightningcss-android-arm64@1.32.0:
|
||||
@@ -1104,6 +1388,14 @@ snapshots:
|
||||
dependencies:
|
||||
'@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: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
@@ -1118,11 +1410,22 @@ snapshots:
|
||||
|
||||
prettier@3.8.1: {}
|
||||
|
||||
proxy-from-env@2.1.0: {}
|
||||
|
||||
react-dom@19.2.4(react@19.2.4):
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
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):
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
@@ -1139,6 +1442,20 @@ snapshots:
|
||||
|
||||
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):
|
||||
dependencies:
|
||||
'@oxc-project/types': 0.122.0
|
||||
@@ -1185,6 +1502,10 @@ snapshots:
|
||||
|
||||
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):
|
||||
dependencies:
|
||||
lightningcss: 1.32.0
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * as FirearmApi from "./firearm-api"
|
||||
export * as ModificationApi from "./modification-api"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 赛季幻影版满改稳定 M7,51操控",
|
||||
"price": 1039234
|
||||
},
|
||||
{
|
||||
"weapon": "M7战斗步枪",
|
||||
"modification-code": "6J2QC4O0BB9B3KKCCH41C",
|
||||
"mode": "烽火地带",
|
||||
"tags": ["稳定"],
|
||||
"note": "S8 赛季隐袭版满改稳定 M7,49操控",
|
||||
"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
|
||||
}
|
||||
]
|
||||
@@ -9,3 +9,26 @@ html, body {
|
||||
#root {
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,12 @@ export default function HeroLayout() {
|
||||
</h1>
|
||||
</div>
|
||||
<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
|
||||
to="/mod-codes"
|
||||
className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { StrictMode } from "react"
|
||||
import { createRoot } from "react-dom/client"
|
||||
import { RouterProvider } from "react-router-dom"
|
||||
import { Provider } from "react-redux"
|
||||
import { PersistGate } from "redux-persist/integration/react"
|
||||
import "@/init"
|
||||
import router from "@/router"
|
||||
import store, { persistor } from "@/store"
|
||||
import "./index.css"
|
||||
|
||||
/**
|
||||
@@ -11,6 +14,10 @@ import "./index.css"
|
||||
*/
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<RouterProvider router={router} />
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -25,7 +25,11 @@ const router = createBrowserRouter(
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
lazy: lazy(() => import("@/page/mod-codes")),
|
||||
lazy: lazy(() => import("@/page/firearms")),
|
||||
},
|
||||
{
|
||||
path: "firearms",
|
||||
lazy: lazy(() => import("@/page/firearms")),
|
||||
},
|
||||
{
|
||||
path: "mod-codes",
|
||||
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
@@ -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>()
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * as QueryParamUtils from "./query-param-utils"
|
||||
@@ -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
|
||||
}
|
||||
Vendored
+1
@@ -1,6 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_REDUX_STORAGE: "local" | "session"
|
||||
readonly VITE_API_BASE_URL: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
||||
Reference in New Issue
Block a user