feat: 项目接口联调
@@ -226,3 +226,4 @@ Before delivering UI code, verify these items:
|
|||||||
- [ ] Form inputs have labels
|
- [ ] Form inputs have labels
|
||||||
- [ ] Color is not the only indicator
|
- [ ] Color is not the only indicator
|
||||||
- [ ] `prefers-reduced-motion` respected
|
- [ ] `prefers-reduced-motion` respected
|
||||||
|
|
||||||
|
|||||||
@@ -137,16 +137,11 @@
|
|||||||
|
|
||||||
### 5.2 当前已观察到的 `SVG` 使用情况
|
### 5.2 当前已观察到的 `SVG` 使用情况
|
||||||
|
|
||||||
根据当前代码扫描,项目中已存在以下 `SVG` 来源:
|
根据当前代码扫描,当前项目中保留的 `SVG` 资源如下:
|
||||||
|
|
||||||
- `src/assets/account.svg`
|
|
||||||
- `src/assets/record.svg`
|
|
||||||
- `public/icons.svg`
|
|
||||||
- `public/favicon.svg`
|
- `public/favicon.svg`
|
||||||
|
|
||||||
当前在页面层级观察到的 `SVG` 资源使用位置如下:
|
当前页面层级的界面图标已迁移为 `lucide-react`,不再保留视图层级的 `SVG` 图标依赖。
|
||||||
|
|
||||||
- `src/views/home/index.tsx`
|
|
||||||
|
|
||||||
### 5.3 迁移规则
|
### 5.3 迁移规则
|
||||||
|
|
||||||
@@ -331,7 +326,6 @@
|
|||||||
- `src/views/account/index.tsx`
|
- `src/views/account/index.tsx`
|
||||||
- `src/views/record/index.tsx`
|
- `src/views/record/index.tsx`
|
||||||
- `src/components/modal/index.tsx`
|
- `src/components/modal/index.tsx`
|
||||||
- 其余与界面展示相关的 `SVG` 资源使用位置
|
|
||||||
|
|
||||||
以下文件或样式必须作为核心基线保留:
|
以下文件或样式必须作为核心基线保留:
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<link rel="preconnect" href="https://rsms.me" />
|
||||||
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>palyx</title>
|
<title>palyx</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -10,16 +10,22 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tanstack/react-query": "^5.96.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"element-china-area-data": "^6.1.0",
|
||||||
|
"ky": "^2.0.0",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
|
"province-city-china": "^8.5.8",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-router-dom": "^7.13.1",
|
"react-router-dom": "^7.13.1",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tailwindcss": "^4.2.1"
|
"tailwindcss": "^4.2.1",
|
||||||
|
"zustand": "^5.0.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.29.0",
|
"@babel/core": "^7.29.0",
|
||||||
|
"@capacitor/cli": "^8.2.0",
|
||||||
"@eslint/js": "^9.39.4",
|
"@eslint/js": "^9.39.4",
|
||||||
"@rolldown/plugin-babel": "^0.2.0",
|
"@rolldown/plugin-babel": "^0.2.0",
|
||||||
"@tailwindcss/vite": "^4.2.1",
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
|
|||||||
683
pnpm-lock.yaml
generated
@@ -8,12 +8,24 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@tanstack/react-query':
|
||||||
|
specifier: ^5.96.2
|
||||||
|
version: 5.96.2(react@19.2.4)
|
||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
element-china-area-data:
|
||||||
|
specifier: ^6.1.0
|
||||||
|
version: 6.1.0
|
||||||
|
ky:
|
||||||
|
specifier: ^2.0.0
|
||||||
|
version: 2.0.0
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.577.0
|
specifier: ^0.577.0
|
||||||
version: 0.577.0(react@19.2.4)
|
version: 0.577.0(react@19.2.4)
|
||||||
|
province-city-china:
|
||||||
|
specifier: ^8.5.8
|
||||||
|
version: 8.5.8
|
||||||
react:
|
react:
|
||||||
specifier: ^19.2.4
|
specifier: ^19.2.4
|
||||||
version: 19.2.4
|
version: 19.2.4
|
||||||
@@ -29,10 +41,16 @@ importers:
|
|||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.2.1
|
specifier: ^4.2.1
|
||||||
version: 4.2.1
|
version: 4.2.1
|
||||||
|
zustand:
|
||||||
|
specifier: ^5.0.12
|
||||||
|
version: 5.0.12(@types/react@19.2.14)(react@19.2.4)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@babel/core':
|
'@babel/core':
|
||||||
specifier: ^7.29.0
|
specifier: ^7.29.0
|
||||||
version: 7.29.0
|
version: 7.29.0
|
||||||
|
'@capacitor/cli':
|
||||||
|
specifier: ^8.2.0
|
||||||
|
version: 8.2.0
|
||||||
'@eslint/js':
|
'@eslint/js':
|
||||||
specifier: ^9.39.4
|
specifier: ^9.39.4
|
||||||
version: 9.39.4
|
version: 9.39.4
|
||||||
@@ -151,6 +169,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@capacitor/cli@8.2.0':
|
||||||
|
resolution: {integrity: sha512-1cMEk0d/I6tl1U+v/lnJR5Oylpx8ZBIHrvQxD5zK0MkjYOUyQAAGJgh97rkhGJqjAUvrGpa8H4BmyhNQN9a17A==}
|
||||||
|
engines: {node: '>=22.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
'@emnapi/core@1.9.0':
|
'@emnapi/core@1.9.0':
|
||||||
resolution: {integrity: sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==}
|
resolution: {integrity: sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==}
|
||||||
|
|
||||||
@@ -214,6 +237,42 @@ packages:
|
|||||||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||||
engines: {node: '>=18.18'}
|
engines: {node: '>=18.18'}
|
||||||
|
|
||||||
|
'@ionic/cli-framework-output@2.2.8':
|
||||||
|
resolution: {integrity: sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
|
'@ionic/utils-array@2.1.6':
|
||||||
|
resolution: {integrity: sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
|
'@ionic/utils-fs@3.1.7':
|
||||||
|
resolution: {integrity: sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
|
'@ionic/utils-object@2.1.6':
|
||||||
|
resolution: {integrity: sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
|
'@ionic/utils-process@2.1.12':
|
||||||
|
resolution: {integrity: sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
|
'@ionic/utils-stream@3.1.7':
|
||||||
|
resolution: {integrity: sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
|
'@ionic/utils-subprocess@3.0.1':
|
||||||
|
resolution: {integrity: sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
|
'@ionic/utils-terminal@2.3.5':
|
||||||
|
resolution: {integrity: sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
|
'@isaacs/fs-minipass@4.0.1':
|
||||||
|
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||||
|
|
||||||
@@ -240,6 +299,9 @@ packages:
|
|||||||
'@oxc-project/types@0.115.0':
|
'@oxc-project/types@0.115.0':
|
||||||
resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==}
|
resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==}
|
||||||
|
|
||||||
|
'@province-city-china/types@8.5.8':
|
||||||
|
resolution: {integrity: sha512-KZ3NyM8HsaBVcn5BRhWaOeZRhqEvm18PfB6HfRDuZfwwWhJLoTxB81mTENrBlONr2g8fy/fSbjsh44gvOj+/Lw==}
|
||||||
|
|
||||||
'@rolldown/binding-android-arm64@1.0.0-rc.9':
|
'@rolldown/binding-android-arm64@1.0.0-rc.9':
|
||||||
resolution: {integrity: sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==}
|
resolution: {integrity: sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -452,6 +514,14 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^5.2.0 || ^6 || ^7
|
vite: ^5.2.0 || ^6 || ^7
|
||||||
|
|
||||||
|
'@tanstack/query-core@5.96.2':
|
||||||
|
resolution: {integrity: sha512-hzI6cTVh4KNRk8UtoIBS7Lv9g6BnJPXvBKsvYH1aGWvv0347jT3BnSvztOE+kD76XGvZnRC/t6qdW1CaIfwCeA==}
|
||||||
|
|
||||||
|
'@tanstack/react-query@5.96.2':
|
||||||
|
resolution: {integrity: sha512-sYyzzJT4G0g02azzJ8o55VFFV31XvFpdUpG+unxS0vSaYsJnSPKGoI6WdPwUucJL1wpgGfwfmntNX/Ub1uOViA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^18 || ^19
|
||||||
|
|
||||||
'@tybys/wasm-util@0.10.1':
|
'@tybys/wasm-util@0.10.1':
|
||||||
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
||||||
|
|
||||||
@@ -470,6 +540,9 @@ packages:
|
|||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
|
|
||||||
|
'@types/fs-extra@8.1.5':
|
||||||
|
resolution: {integrity: sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==}
|
||||||
|
|
||||||
'@types/json-schema@7.0.15':
|
'@types/json-schema@7.0.15':
|
||||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
|
|
||||||
@@ -484,6 +557,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/slice-ansi@4.0.0':
|
||||||
|
resolution: {integrity: sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.57.0':
|
'@typescript-eslint/eslint-plugin@8.57.0':
|
||||||
resolution: {integrity: sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==}
|
resolution: {integrity: sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@@ -556,6 +632,10 @@ packages:
|
|||||||
babel-plugin-react-compiler:
|
babel-plugin-react-compiler:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@xmldom/xmldom@0.8.11':
|
||||||
|
resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
acorn-jsx@5.3.2:
|
acorn-jsx@5.3.2:
|
||||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -569,6 +649,10 @@ packages:
|
|||||||
ajv@6.14.0:
|
ajv@6.14.0:
|
||||||
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
|
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
|
||||||
|
|
||||||
|
ansi-regex@5.0.1:
|
||||||
|
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
ansi-styles@4.3.0:
|
ansi-styles@4.3.0:
|
||||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -576,6 +660,14 @@ packages:
|
|||||||
argparse@2.0.1:
|
argparse@2.0.1:
|
||||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||||
|
|
||||||
|
astral-regex@2.0.0:
|
||||||
|
resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
at-least-node@1.0.0:
|
||||||
|
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
|
||||||
|
engines: {node: '>= 4.0.0'}
|
||||||
|
|
||||||
babel-plugin-react-compiler@1.0.0:
|
babel-plugin-react-compiler@1.0.0:
|
||||||
resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==}
|
resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==}
|
||||||
|
|
||||||
@@ -586,11 +678,22 @@ packages:
|
|||||||
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
|
base64-js@1.5.1:
|
||||||
|
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||||
|
|
||||||
baseline-browser-mapping@2.10.8:
|
baseline-browser-mapping@2.10.8:
|
||||||
resolution: {integrity: sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==}
|
resolution: {integrity: sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
big-integer@1.6.52:
|
||||||
|
resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
|
||||||
|
engines: {node: '>=0.6'}
|
||||||
|
|
||||||
|
bplist-parser@0.3.2:
|
||||||
|
resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==}
|
||||||
|
engines: {node: '>= 5.10.0'}
|
||||||
|
|
||||||
brace-expansion@1.1.12:
|
brace-expansion@1.1.12:
|
||||||
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
|
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
|
||||||
|
|
||||||
@@ -603,6 +706,9 @@ packages:
|
|||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
buffer-crc32@0.2.13:
|
||||||
|
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
||||||
|
|
||||||
callsites@3.1.0:
|
callsites@3.1.0:
|
||||||
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -614,6 +720,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
china-division@2.7.0:
|
||||||
|
resolution: {integrity: sha512-4uUPAT+1WfqDh5jytq7omdCmHNk3j+k76zEG/2IqaGcYB90c2SwcixttcypdsZ3T/9tN1TTpBDoeZn+Yw/qBEA==}
|
||||||
|
|
||||||
|
chownr@3.0.0:
|
||||||
|
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
clsx@2.1.1:
|
clsx@2.1.1:
|
||||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -625,6 +738,10 @@ packages:
|
|||||||
color-name@1.1.4:
|
color-name@1.1.4:
|
||||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
|
commander@12.1.0:
|
||||||
|
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||||
|
|
||||||
@@ -654,6 +771,10 @@ packages:
|
|||||||
deep-is@0.1.4:
|
deep-is@0.1.4:
|
||||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||||
|
|
||||||
|
define-lazy-prop@2.0.0:
|
||||||
|
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
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'}
|
||||||
@@ -661,10 +782,24 @@ packages:
|
|||||||
electron-to-chromium@1.5.313:
|
electron-to-chromium@1.5.313:
|
||||||
resolution: {integrity: sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==}
|
resolution: {integrity: sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==}
|
||||||
|
|
||||||
|
element-china-area-data@6.1.0:
|
||||||
|
resolution: {integrity: sha512-IkpcjwQv2A/2AxFiSoaISZ+oMw1rZCPUSOg5sOCwT5jKc96TaawmKZeY81xfxXsO0QbKxU5LLc6AirhG52hUmg==}
|
||||||
|
|
||||||
|
elementtree@0.1.7:
|
||||||
|
resolution: {integrity: sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==}
|
||||||
|
engines: {node: '>= 0.4.0'}
|
||||||
|
|
||||||
|
emoji-regex@8.0.0:
|
||||||
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
|
|
||||||
enhanced-resolve@5.20.0:
|
enhanced-resolve@5.20.0:
|
||||||
resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==}
|
resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
|
env-paths@2.2.1:
|
||||||
|
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
escalade@3.2.0:
|
escalade@3.2.0:
|
||||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -739,6 +874,9 @@ packages:
|
|||||||
fast-levenshtein@2.0.6:
|
fast-levenshtein@2.0.6:
|
||||||
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||||
|
|
||||||
|
fd-slicer@1.1.0:
|
||||||
|
resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
|
||||||
|
|
||||||
fdir@6.5.0:
|
fdir@6.5.0:
|
||||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@@ -763,6 +901,14 @@ packages:
|
|||||||
flatted@3.4.1:
|
flatted@3.4.1:
|
||||||
resolution: {integrity: sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==}
|
resolution: {integrity: sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==}
|
||||||
|
|
||||||
|
fs-extra@11.3.4:
|
||||||
|
resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==}
|
||||||
|
engines: {node: '>=14.14'}
|
||||||
|
|
||||||
|
fs-extra@9.1.0:
|
||||||
|
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
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}
|
||||||
@@ -776,6 +922,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
|
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
|
glob@13.0.6:
|
||||||
|
resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
|
||||||
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
globals@14.0.0:
|
globals@14.0.0:
|
||||||
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
|
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -813,14 +963,34 @@ packages:
|
|||||||
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
||||||
engines: {node: '>=0.8.19'}
|
engines: {node: '>=0.8.19'}
|
||||||
|
|
||||||
|
inherits@2.0.4:
|
||||||
|
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||||
|
|
||||||
|
ini@4.1.3:
|
||||||
|
resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==}
|
||||||
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
|
|
||||||
|
is-docker@2.2.1:
|
||||||
|
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
is-extglob@2.1.1:
|
is-extglob@2.1.1:
|
||||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
is-fullwidth-code-point@3.0.0:
|
||||||
|
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
is-glob@4.0.3:
|
is-glob@4.0.3:
|
||||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
is-wsl@2.2.0:
|
||||||
|
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
isexe@2.0.0:
|
isexe@2.0.0:
|
||||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
|
|
||||||
@@ -854,9 +1024,24 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
jsonfile@6.2.0:
|
||||||
|
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||||
|
|
||||||
|
kleur@3.0.3:
|
||||||
|
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
kleur@4.1.5:
|
||||||
|
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
ky@2.0.0:
|
||||||
|
resolution: {integrity: sha512-KzI4Vz5AbZFAUFYGx28PCSfFWUo6/qj9Br/P6KRwDieE1xfdz0tIONepJcLw/1xLocN13GgvfJGasa+pfSkbHg==}
|
||||||
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
levn@0.4.1:
|
levn@0.4.1:
|
||||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -1016,6 +1201,10 @@ packages:
|
|||||||
lodash.merge@4.6.2:
|
lodash.merge@4.6.2:
|
||||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||||
|
|
||||||
|
lru-cache@11.2.7:
|
||||||
|
resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||||
|
|
||||||
@@ -1034,6 +1223,14 @@ packages:
|
|||||||
minimatch@3.1.5:
|
minimatch@3.1.5:
|
||||||
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
|
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
|
||||||
|
|
||||||
|
minipass@7.1.3:
|
||||||
|
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
|
||||||
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
|
minizlib@3.1.0:
|
||||||
|
resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -1042,12 +1239,21 @@ packages:
|
|||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
native-run@2.0.3:
|
||||||
|
resolution: {integrity: sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
natural-compare@1.4.0:
|
natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
|
|
||||||
node-releases@2.0.36:
|
node-releases@2.0.36:
|
||||||
resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==}
|
resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==}
|
||||||
|
|
||||||
|
open@8.4.2:
|
||||||
|
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
optionator@0.9.4:
|
optionator@0.9.4:
|
||||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -1060,6 +1266,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
package-json-from-dist@1.0.1:
|
||||||
|
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||||
|
|
||||||
parent-module@1.0.1:
|
parent-module@1.0.1:
|
||||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -1072,6 +1281,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
path-scurry@2.0.2:
|
||||||
|
resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
|
||||||
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
|
pend@1.2.0:
|
||||||
|
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@@ -1079,6 +1295,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
plist@3.1.0:
|
||||||
|
resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==}
|
||||||
|
engines: {node: '>=10.4.0'}
|
||||||
|
|
||||||
postcss@8.5.8:
|
postcss@8.5.8:
|
||||||
resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
|
resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
@@ -1087,6 +1307,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
prompts@2.4.2:
|
||||||
|
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
province-city-china@8.5.8:
|
||||||
|
resolution: {integrity: sha512-gUV5kSOWHVufemkq6lygb0ngNZ4snMcONmr3QzxHuj1MYOQPphiyjHplfmywcVGWdGQgim30RXia/mYB007eLg==}
|
||||||
|
|
||||||
punycode@2.3.1:
|
punycode@2.3.1:
|
||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -1117,15 +1344,34 @@ packages:
|
|||||||
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
|
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
readable-stream@3.6.2:
|
||||||
|
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
resolve-from@4.0.0:
|
resolve-from@4.0.0:
|
||||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
rimraf@6.1.3:
|
||||||
|
resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
rolldown@1.0.0-rc.9:
|
rolldown@1.0.0-rc.9:
|
||||||
resolution: {integrity: sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==}
|
resolution: {integrity: sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
safe-buffer@5.2.1:
|
||||||
|
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||||
|
|
||||||
|
sax@1.1.4:
|
||||||
|
resolution: {integrity: sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==}
|
||||||
|
|
||||||
|
sax@1.6.0:
|
||||||
|
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
|
||||||
|
engines: {node: '>=11.0.0'}
|
||||||
|
|
||||||
scheduler@0.27.0:
|
scheduler@0.27.0:
|
||||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||||
|
|
||||||
@@ -1149,10 +1395,35 @@ packages:
|
|||||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
signal-exit@3.0.7:
|
||||||
|
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
||||||
|
|
||||||
|
sisteransi@1.0.5:
|
||||||
|
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||||
|
|
||||||
|
slice-ansi@4.0.0:
|
||||||
|
resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
source-map-js@1.2.1:
|
source-map-js@1.2.1:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
split2@4.2.0:
|
||||||
|
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||||
|
engines: {node: '>= 10.x'}
|
||||||
|
|
||||||
|
string-width@4.2.3:
|
||||||
|
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
string_decoder@1.3.0:
|
||||||
|
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||||
|
|
||||||
|
strip-ansi@6.0.1:
|
||||||
|
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
strip-json-comments@3.1.1:
|
strip-json-comments@3.1.1:
|
||||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -1171,10 +1442,21 @@ packages:
|
|||||||
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
tar@7.5.12:
|
||||||
|
resolution: {integrity: sha512-9TsuLcdhOn4XztcQqhNyq1KOwOOED/3k58JAvtULiYqbO8B/0IBAAIE1hj0Svmm58k27TmcigyDI0deMlgG3uw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
through2@4.0.2:
|
||||||
|
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
|
||||||
|
|
||||||
tinyglobby@0.2.15:
|
tinyglobby@0.2.15:
|
||||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
tree-kill@1.2.2:
|
||||||
|
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
ts-api-utils@2.4.0:
|
ts-api-utils@2.4.0:
|
||||||
resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==}
|
resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==}
|
||||||
engines: {node: '>=18.12'}
|
engines: {node: '>=18.12'}
|
||||||
@@ -1203,6 +1485,14 @@ packages:
|
|||||||
undici-types@7.16.0:
|
undici-types@7.16.0:
|
||||||
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||||
|
|
||||||
|
universalify@2.0.1:
|
||||||
|
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
|
||||||
|
untildify@4.0.0:
|
||||||
|
resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
update-browserslist-db@1.2.3:
|
update-browserslist-db@1.2.3:
|
||||||
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
|
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -1212,6 +1502,9 @@ packages:
|
|||||||
uri-js@4.4.1:
|
uri-js@4.4.1:
|
||||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
|
|
||||||
|
util-deprecate@1.0.2:
|
||||||
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
vite@8.0.0:
|
vite@8.0.0:
|
||||||
resolution: {integrity: sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==}
|
resolution: {integrity: sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -1264,9 +1557,32 @@ packages:
|
|||||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
wrap-ansi@7.0.0:
|
||||||
|
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
xml2js@0.6.2:
|
||||||
|
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
|
||||||
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
||||||
|
xmlbuilder@11.0.1:
|
||||||
|
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
|
||||||
|
xmlbuilder@15.1.1:
|
||||||
|
resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==}
|
||||||
|
engines: {node: '>=8.0'}
|
||||||
|
|
||||||
yallist@3.1.1:
|
yallist@3.1.1:
|
||||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||||
|
|
||||||
|
yallist@5.0.0:
|
||||||
|
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
yauzl@2.10.0:
|
||||||
|
resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
|
||||||
|
|
||||||
yocto-queue@0.1.0:
|
yocto-queue@0.1.0:
|
||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -1280,6 +1596,24 @@ packages:
|
|||||||
zod@4.3.6:
|
zod@4.3.6:
|
||||||
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
|
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
|
||||||
|
|
||||||
|
zustand@5.0.12:
|
||||||
|
resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==}
|
||||||
|
engines: {node: '>=12.20.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '>=18.0.0'
|
||||||
|
immer: '>=9.0.6'
|
||||||
|
react: '>=18.0.0'
|
||||||
|
use-sync-external-store: '>=1.2.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
immer:
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
use-sync-external-store:
|
||||||
|
optional: true
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@babel/code-frame@7.29.0':
|
'@babel/code-frame@7.29.0':
|
||||||
@@ -1382,6 +1716,28 @@ snapshots:
|
|||||||
'@babel/helper-string-parser': 7.27.1
|
'@babel/helper-string-parser': 7.27.1
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.28.5
|
||||||
|
|
||||||
|
'@capacitor/cli@8.2.0':
|
||||||
|
dependencies:
|
||||||
|
'@ionic/cli-framework-output': 2.2.8
|
||||||
|
'@ionic/utils-subprocess': 3.0.1
|
||||||
|
'@ionic/utils-terminal': 2.3.5
|
||||||
|
commander: 12.1.0
|
||||||
|
debug: 4.4.3
|
||||||
|
env-paths: 2.2.1
|
||||||
|
fs-extra: 11.3.4
|
||||||
|
kleur: 4.1.5
|
||||||
|
native-run: 2.0.3
|
||||||
|
open: 8.4.2
|
||||||
|
plist: 3.1.0
|
||||||
|
prompts: 2.4.2
|
||||||
|
rimraf: 6.1.3
|
||||||
|
semver: 7.7.4
|
||||||
|
tar: 7.5.12
|
||||||
|
tslib: 2.8.1
|
||||||
|
xml2js: 0.6.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@emnapi/core@1.9.0':
|
'@emnapi/core@1.9.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/wasi-threads': 1.2.0
|
'@emnapi/wasi-threads': 1.2.0
|
||||||
@@ -1455,6 +1811,86 @@ snapshots:
|
|||||||
|
|
||||||
'@humanwhocodes/retry@0.4.3': {}
|
'@humanwhocodes/retry@0.4.3': {}
|
||||||
|
|
||||||
|
'@ionic/cli-framework-output@2.2.8':
|
||||||
|
dependencies:
|
||||||
|
'@ionic/utils-terminal': 2.3.5
|
||||||
|
debug: 4.4.3
|
||||||
|
tslib: 2.8.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@ionic/utils-array@2.1.6':
|
||||||
|
dependencies:
|
||||||
|
debug: 4.4.3
|
||||||
|
tslib: 2.8.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@ionic/utils-fs@3.1.7':
|
||||||
|
dependencies:
|
||||||
|
'@types/fs-extra': 8.1.5
|
||||||
|
debug: 4.4.3
|
||||||
|
fs-extra: 9.1.0
|
||||||
|
tslib: 2.8.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@ionic/utils-object@2.1.6':
|
||||||
|
dependencies:
|
||||||
|
debug: 4.4.3
|
||||||
|
tslib: 2.8.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@ionic/utils-process@2.1.12':
|
||||||
|
dependencies:
|
||||||
|
'@ionic/utils-object': 2.1.6
|
||||||
|
'@ionic/utils-terminal': 2.3.5
|
||||||
|
debug: 4.4.3
|
||||||
|
signal-exit: 3.0.7
|
||||||
|
tree-kill: 1.2.2
|
||||||
|
tslib: 2.8.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@ionic/utils-stream@3.1.7':
|
||||||
|
dependencies:
|
||||||
|
debug: 4.4.3
|
||||||
|
tslib: 2.8.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@ionic/utils-subprocess@3.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@ionic/utils-array': 2.1.6
|
||||||
|
'@ionic/utils-fs': 3.1.7
|
||||||
|
'@ionic/utils-process': 2.1.12
|
||||||
|
'@ionic/utils-stream': 3.1.7
|
||||||
|
'@ionic/utils-terminal': 2.3.5
|
||||||
|
cross-spawn: 7.0.6
|
||||||
|
debug: 4.4.3
|
||||||
|
tslib: 2.8.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@ionic/utils-terminal@2.3.5':
|
||||||
|
dependencies:
|
||||||
|
'@types/slice-ansi': 4.0.0
|
||||||
|
debug: 4.4.3
|
||||||
|
signal-exit: 3.0.7
|
||||||
|
slice-ansi: 4.0.0
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
untildify: 4.0.0
|
||||||
|
wrap-ansi: 7.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@isaacs/fs-minipass@4.0.1':
|
||||||
|
dependencies:
|
||||||
|
minipass: 7.1.3
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
@@ -1485,6 +1921,8 @@ snapshots:
|
|||||||
|
|
||||||
'@oxc-project/types@0.115.0': {}
|
'@oxc-project/types@0.115.0': {}
|
||||||
|
|
||||||
|
'@province-city-china/types@8.5.8': {}
|
||||||
|
|
||||||
'@rolldown/binding-android-arm64@1.0.0-rc.9':
|
'@rolldown/binding-android-arm64@1.0.0-rc.9':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -1612,6 +2050,13 @@ snapshots:
|
|||||||
tailwindcss: 4.2.1
|
tailwindcss: 4.2.1
|
||||||
vite: 8.0.0(@types/node@24.12.0)(jiti@2.6.1)
|
vite: 8.0.0(@types/node@24.12.0)(jiti@2.6.1)
|
||||||
|
|
||||||
|
'@tanstack/query-core@5.96.2': {}
|
||||||
|
|
||||||
|
'@tanstack/react-query@5.96.2(react@19.2.4)':
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/query-core': 5.96.2
|
||||||
|
react: 19.2.4
|
||||||
|
|
||||||
'@tybys/wasm-util@0.10.1':
|
'@tybys/wasm-util@0.10.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@@ -1640,6 +2085,10 @@ snapshots:
|
|||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
|
'@types/fs-extra@8.1.5':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 24.12.0
|
||||||
|
|
||||||
'@types/json-schema@7.0.15': {}
|
'@types/json-schema@7.0.15': {}
|
||||||
|
|
||||||
'@types/node@24.12.0':
|
'@types/node@24.12.0':
|
||||||
@@ -1654,6 +2103,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
csstype: 3.2.3
|
csstype: 3.2.3
|
||||||
|
|
||||||
|
'@types/slice-ansi@4.0.0': {}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.57.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/eslint-plugin@8.57.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
@@ -1753,6 +2204,8 @@ snapshots:
|
|||||||
'@rolldown/plugin-babel': 0.2.1(@babel/core@7.29.0)(rolldown@1.0.0-rc.9)(vite@8.0.0(@types/node@24.12.0)(jiti@2.6.1))
|
'@rolldown/plugin-babel': 0.2.1(@babel/core@7.29.0)(rolldown@1.0.0-rc.9)(vite@8.0.0(@types/node@24.12.0)(jiti@2.6.1))
|
||||||
babel-plugin-react-compiler: 1.0.0
|
babel-plugin-react-compiler: 1.0.0
|
||||||
|
|
||||||
|
'@xmldom/xmldom@0.8.11': {}
|
||||||
|
|
||||||
acorn-jsx@5.3.2(acorn@8.16.0):
|
acorn-jsx@5.3.2(acorn@8.16.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.16.0
|
acorn: 8.16.0
|
||||||
@@ -1766,12 +2219,18 @@ snapshots:
|
|||||||
json-schema-traverse: 0.4.1
|
json-schema-traverse: 0.4.1
|
||||||
uri-js: 4.4.1
|
uri-js: 4.4.1
|
||||||
|
|
||||||
|
ansi-regex@5.0.1: {}
|
||||||
|
|
||||||
ansi-styles@4.3.0:
|
ansi-styles@4.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-convert: 2.0.1
|
color-convert: 2.0.1
|
||||||
|
|
||||||
argparse@2.0.1: {}
|
argparse@2.0.1: {}
|
||||||
|
|
||||||
|
astral-regex@2.0.0: {}
|
||||||
|
|
||||||
|
at-least-node@1.0.0: {}
|
||||||
|
|
||||||
babel-plugin-react-compiler@1.0.0:
|
babel-plugin-react-compiler@1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.0
|
||||||
@@ -1780,8 +2239,16 @@ snapshots:
|
|||||||
|
|
||||||
balanced-match@4.0.4: {}
|
balanced-match@4.0.4: {}
|
||||||
|
|
||||||
|
base64-js@1.5.1: {}
|
||||||
|
|
||||||
baseline-browser-mapping@2.10.8: {}
|
baseline-browser-mapping@2.10.8: {}
|
||||||
|
|
||||||
|
big-integer@1.6.52: {}
|
||||||
|
|
||||||
|
bplist-parser@0.3.2:
|
||||||
|
dependencies:
|
||||||
|
big-integer: 1.6.52
|
||||||
|
|
||||||
brace-expansion@1.1.12:
|
brace-expansion@1.1.12:
|
||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 1.0.2
|
balanced-match: 1.0.2
|
||||||
@@ -1799,6 +2266,8 @@ snapshots:
|
|||||||
node-releases: 2.0.36
|
node-releases: 2.0.36
|
||||||
update-browserslist-db: 1.2.3(browserslist@4.28.1)
|
update-browserslist-db: 1.2.3(browserslist@4.28.1)
|
||||||
|
|
||||||
|
buffer-crc32@0.2.13: {}
|
||||||
|
|
||||||
callsites@3.1.0: {}
|
callsites@3.1.0: {}
|
||||||
|
|
||||||
caniuse-lite@1.0.30001779: {}
|
caniuse-lite@1.0.30001779: {}
|
||||||
@@ -1808,6 +2277,10 @@ snapshots:
|
|||||||
ansi-styles: 4.3.0
|
ansi-styles: 4.3.0
|
||||||
supports-color: 7.2.0
|
supports-color: 7.2.0
|
||||||
|
|
||||||
|
china-division@2.7.0: {}
|
||||||
|
|
||||||
|
chownr@3.0.0: {}
|
||||||
|
|
||||||
clsx@2.1.1: {}
|
clsx@2.1.1: {}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
@@ -1816,6 +2289,8 @@ snapshots:
|
|||||||
|
|
||||||
color-name@1.1.4: {}
|
color-name@1.1.4: {}
|
||||||
|
|
||||||
|
commander@12.1.0: {}
|
||||||
|
|
||||||
concat-map@0.0.1: {}
|
concat-map@0.0.1: {}
|
||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
@@ -1836,15 +2311,29 @@ snapshots:
|
|||||||
|
|
||||||
deep-is@0.1.4: {}
|
deep-is@0.1.4: {}
|
||||||
|
|
||||||
|
define-lazy-prop@2.0.0: {}
|
||||||
|
|
||||||
detect-libc@2.1.2: {}
|
detect-libc@2.1.2: {}
|
||||||
|
|
||||||
electron-to-chromium@1.5.313: {}
|
electron-to-chromium@1.5.313: {}
|
||||||
|
|
||||||
|
element-china-area-data@6.1.0:
|
||||||
|
dependencies:
|
||||||
|
china-division: 2.7.0
|
||||||
|
|
||||||
|
elementtree@0.1.7:
|
||||||
|
dependencies:
|
||||||
|
sax: 1.1.4
|
||||||
|
|
||||||
|
emoji-regex@8.0.0: {}
|
||||||
|
|
||||||
enhanced-resolve@5.20.0:
|
enhanced-resolve@5.20.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
tapable: 2.3.0
|
tapable: 2.3.0
|
||||||
|
|
||||||
|
env-paths@2.2.1: {}
|
||||||
|
|
||||||
escalade@3.2.0: {}
|
escalade@3.2.0: {}
|
||||||
|
|
||||||
escape-string-regexp@4.0.0: {}
|
escape-string-regexp@4.0.0: {}
|
||||||
@@ -1940,6 +2429,10 @@ snapshots:
|
|||||||
|
|
||||||
fast-levenshtein@2.0.6: {}
|
fast-levenshtein@2.0.6: {}
|
||||||
|
|
||||||
|
fd-slicer@1.1.0:
|
||||||
|
dependencies:
|
||||||
|
pend: 1.2.0
|
||||||
|
|
||||||
fdir@6.5.0(picomatch@4.0.3):
|
fdir@6.5.0(picomatch@4.0.3):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
@@ -1960,6 +2453,19 @@ snapshots:
|
|||||||
|
|
||||||
flatted@3.4.1: {}
|
flatted@3.4.1: {}
|
||||||
|
|
||||||
|
fs-extra@11.3.4:
|
||||||
|
dependencies:
|
||||||
|
graceful-fs: 4.2.11
|
||||||
|
jsonfile: 6.2.0
|
||||||
|
universalify: 2.0.1
|
||||||
|
|
||||||
|
fs-extra@9.1.0:
|
||||||
|
dependencies:
|
||||||
|
at-least-node: 1.0.0
|
||||||
|
graceful-fs: 4.2.11
|
||||||
|
jsonfile: 6.2.0
|
||||||
|
universalify: 2.0.1
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -1969,6 +2475,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
|
|
||||||
|
glob@13.0.6:
|
||||||
|
dependencies:
|
||||||
|
minimatch: 10.2.4
|
||||||
|
minipass: 7.1.3
|
||||||
|
path-scurry: 2.0.2
|
||||||
|
|
||||||
globals@14.0.0: {}
|
globals@14.0.0: {}
|
||||||
|
|
||||||
globals@17.4.0: {}
|
globals@17.4.0: {}
|
||||||
@@ -1994,12 +2506,24 @@ snapshots:
|
|||||||
|
|
||||||
imurmurhash@0.1.4: {}
|
imurmurhash@0.1.4: {}
|
||||||
|
|
||||||
|
inherits@2.0.4: {}
|
||||||
|
|
||||||
|
ini@4.1.3: {}
|
||||||
|
|
||||||
|
is-docker@2.2.1: {}
|
||||||
|
|
||||||
is-extglob@2.1.1: {}
|
is-extglob@2.1.1: {}
|
||||||
|
|
||||||
|
is-fullwidth-code-point@3.0.0: {}
|
||||||
|
|
||||||
is-glob@4.0.3:
|
is-glob@4.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-extglob: 2.1.1
|
is-extglob: 2.1.1
|
||||||
|
|
||||||
|
is-wsl@2.2.0:
|
||||||
|
dependencies:
|
||||||
|
is-docker: 2.2.1
|
||||||
|
|
||||||
isexe@2.0.0: {}
|
isexe@2.0.0: {}
|
||||||
|
|
||||||
jiti@2.6.1: {}
|
jiti@2.6.1: {}
|
||||||
@@ -2020,10 +2544,22 @@ snapshots:
|
|||||||
|
|
||||||
json5@2.2.3: {}
|
json5@2.2.3: {}
|
||||||
|
|
||||||
|
jsonfile@6.2.0:
|
||||||
|
dependencies:
|
||||||
|
universalify: 2.0.1
|
||||||
|
optionalDependencies:
|
||||||
|
graceful-fs: 4.2.11
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
json-buffer: 3.0.1
|
json-buffer: 3.0.1
|
||||||
|
|
||||||
|
kleur@3.0.3: {}
|
||||||
|
|
||||||
|
kleur@4.1.5: {}
|
||||||
|
|
||||||
|
ky@2.0.0: {}
|
||||||
|
|
||||||
levn@0.4.1:
|
levn@0.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
@@ -2133,6 +2669,8 @@ snapshots:
|
|||||||
|
|
||||||
lodash.merge@4.6.2: {}
|
lodash.merge@4.6.2: {}
|
||||||
|
|
||||||
|
lru-cache@11.2.7: {}
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
yallist: 3.1.1
|
yallist: 3.1.1
|
||||||
@@ -2153,14 +2691,42 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 1.1.12
|
brace-expansion: 1.1.12
|
||||||
|
|
||||||
|
minipass@7.1.3: {}
|
||||||
|
|
||||||
|
minizlib@3.1.0:
|
||||||
|
dependencies:
|
||||||
|
minipass: 7.1.3
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
|
native-run@2.0.3:
|
||||||
|
dependencies:
|
||||||
|
'@ionic/utils-fs': 3.1.7
|
||||||
|
'@ionic/utils-terminal': 2.3.5
|
||||||
|
bplist-parser: 0.3.2
|
||||||
|
debug: 4.4.3
|
||||||
|
elementtree: 0.1.7
|
||||||
|
ini: 4.1.3
|
||||||
|
plist: 3.1.0
|
||||||
|
split2: 4.2.0
|
||||||
|
through2: 4.0.2
|
||||||
|
tslib: 2.8.1
|
||||||
|
yauzl: 2.10.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
|
|
||||||
node-releases@2.0.36: {}
|
node-releases@2.0.36: {}
|
||||||
|
|
||||||
|
open@8.4.2:
|
||||||
|
dependencies:
|
||||||
|
define-lazy-prop: 2.0.0
|
||||||
|
is-docker: 2.2.1
|
||||||
|
is-wsl: 2.2.0
|
||||||
|
|
||||||
optionator@0.9.4:
|
optionator@0.9.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
deep-is: 0.1.4
|
deep-is: 0.1.4
|
||||||
@@ -2178,6 +2744,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
p-limit: 3.1.0
|
p-limit: 3.1.0
|
||||||
|
|
||||||
|
package-json-from-dist@1.0.1: {}
|
||||||
|
|
||||||
parent-module@1.0.1:
|
parent-module@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
callsites: 3.1.0
|
callsites: 3.1.0
|
||||||
@@ -2186,10 +2754,23 @@ snapshots:
|
|||||||
|
|
||||||
path-key@3.1.1: {}
|
path-key@3.1.1: {}
|
||||||
|
|
||||||
|
path-scurry@2.0.2:
|
||||||
|
dependencies:
|
||||||
|
lru-cache: 11.2.7
|
||||||
|
minipass: 7.1.3
|
||||||
|
|
||||||
|
pend@1.2.0: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@4.0.3: {}
|
picomatch@4.0.3: {}
|
||||||
|
|
||||||
|
plist@3.1.0:
|
||||||
|
dependencies:
|
||||||
|
'@xmldom/xmldom': 0.8.11
|
||||||
|
base64-js: 1.5.1
|
||||||
|
xmlbuilder: 15.1.1
|
||||||
|
|
||||||
postcss@8.5.8:
|
postcss@8.5.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
nanoid: 3.3.11
|
nanoid: 3.3.11
|
||||||
@@ -2198,6 +2779,15 @@ snapshots:
|
|||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
|
prompts@2.4.2:
|
||||||
|
dependencies:
|
||||||
|
kleur: 3.0.3
|
||||||
|
sisteransi: 1.0.5
|
||||||
|
|
||||||
|
province-city-china@8.5.8:
|
||||||
|
dependencies:
|
||||||
|
'@province-city-china/types': 8.5.8
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
react-dom@19.2.4(react@19.2.4):
|
react-dom@19.2.4(react@19.2.4):
|
||||||
@@ -2221,8 +2811,19 @@ snapshots:
|
|||||||
|
|
||||||
react@19.2.4: {}
|
react@19.2.4: {}
|
||||||
|
|
||||||
|
readable-stream@3.6.2:
|
||||||
|
dependencies:
|
||||||
|
inherits: 2.0.4
|
||||||
|
string_decoder: 1.3.0
|
||||||
|
util-deprecate: 1.0.2
|
||||||
|
|
||||||
resolve-from@4.0.0: {}
|
resolve-from@4.0.0: {}
|
||||||
|
|
||||||
|
rimraf@6.1.3:
|
||||||
|
dependencies:
|
||||||
|
glob: 13.0.6
|
||||||
|
package-json-from-dist: 1.0.1
|
||||||
|
|
||||||
rolldown@1.0.0-rc.9:
|
rolldown@1.0.0-rc.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@oxc-project/types': 0.115.0
|
'@oxc-project/types': 0.115.0
|
||||||
@@ -2244,6 +2845,12 @@ snapshots:
|
|||||||
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.9
|
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.9
|
||||||
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.9
|
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.9
|
||||||
|
|
||||||
|
safe-buffer@5.2.1: {}
|
||||||
|
|
||||||
|
sax@1.1.4: {}
|
||||||
|
|
||||||
|
sax@1.6.0: {}
|
||||||
|
|
||||||
scheduler@0.27.0: {}
|
scheduler@0.27.0: {}
|
||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
@@ -2258,8 +2865,34 @@ snapshots:
|
|||||||
|
|
||||||
shebang-regex@3.0.0: {}
|
shebang-regex@3.0.0: {}
|
||||||
|
|
||||||
|
signal-exit@3.0.7: {}
|
||||||
|
|
||||||
|
sisteransi@1.0.5: {}
|
||||||
|
|
||||||
|
slice-ansi@4.0.0:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
astral-regex: 2.0.0
|
||||||
|
is-fullwidth-code-point: 3.0.0
|
||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
split2@4.2.0: {}
|
||||||
|
|
||||||
|
string-width@4.2.3:
|
||||||
|
dependencies:
|
||||||
|
emoji-regex: 8.0.0
|
||||||
|
is-fullwidth-code-point: 3.0.0
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
|
string_decoder@1.3.0:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
|
strip-ansi@6.0.1:
|
||||||
|
dependencies:
|
||||||
|
ansi-regex: 5.0.1
|
||||||
|
|
||||||
strip-json-comments@3.1.1: {}
|
strip-json-comments@3.1.1: {}
|
||||||
|
|
||||||
supports-color@7.2.0:
|
supports-color@7.2.0:
|
||||||
@@ -2272,17 +2905,30 @@ snapshots:
|
|||||||
|
|
||||||
tapable@2.3.0: {}
|
tapable@2.3.0: {}
|
||||||
|
|
||||||
|
tar@7.5.12:
|
||||||
|
dependencies:
|
||||||
|
'@isaacs/fs-minipass': 4.0.1
|
||||||
|
chownr: 3.0.0
|
||||||
|
minipass: 7.1.3
|
||||||
|
minizlib: 3.1.0
|
||||||
|
yallist: 5.0.0
|
||||||
|
|
||||||
|
through2@4.0.2:
|
||||||
|
dependencies:
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
tinyglobby@0.2.15:
|
tinyglobby@0.2.15:
|
||||||
dependencies:
|
dependencies:
|
||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
|
|
||||||
|
tree-kill@1.2.2: {}
|
||||||
|
|
||||||
ts-api-utils@2.4.0(typescript@5.9.3):
|
ts-api-utils@2.4.0(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
tslib@2.8.1:
|
tslib@2.8.1: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2303,6 +2949,10 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@7.16.0: {}
|
undici-types@7.16.0: {}
|
||||||
|
|
||||||
|
universalify@2.0.1: {}
|
||||||
|
|
||||||
|
untildify@4.0.0: {}
|
||||||
|
|
||||||
update-browserslist-db@1.2.3(browserslist@4.28.1):
|
update-browserslist-db@1.2.3(browserslist@4.28.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.28.1
|
browserslist: 4.28.1
|
||||||
@@ -2313,6 +2963,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
|
|
||||||
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
vite@8.0.0(@types/node@24.12.0)(jiti@2.6.1):
|
vite@8.0.0(@types/node@24.12.0)(jiti@2.6.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@oxc-project/runtime': 0.115.0
|
'@oxc-project/runtime': 0.115.0
|
||||||
@@ -2332,8 +2984,30 @@ snapshots:
|
|||||||
|
|
||||||
word-wrap@1.2.5: {}
|
word-wrap@1.2.5: {}
|
||||||
|
|
||||||
|
wrap-ansi@7.0.0:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
|
xml2js@0.6.2:
|
||||||
|
dependencies:
|
||||||
|
sax: 1.6.0
|
||||||
|
xmlbuilder: 11.0.1
|
||||||
|
|
||||||
|
xmlbuilder@11.0.1: {}
|
||||||
|
|
||||||
|
xmlbuilder@15.1.1: {}
|
||||||
|
|
||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
|
|
||||||
|
yallist@5.0.0: {}
|
||||||
|
|
||||||
|
yauzl@2.10.0:
|
||||||
|
dependencies:
|
||||||
|
buffer-crc32: 0.2.13
|
||||||
|
fd-slicer: 1.1.0
|
||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
zod-validation-error@4.0.2(zod@4.3.6):
|
zod-validation-error@4.0.2(zod@4.3.6):
|
||||||
@@ -2341,3 +3015,8 @@ snapshots:
|
|||||||
zod: 4.3.6
|
zod: 4.3.6
|
||||||
|
|
||||||
zod@4.3.6: {}
|
zod@4.3.6: {}
|
||||||
|
|
||||||
|
zustand@5.0.12(@types/react@19.2.14)(react@19.2.4):
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
react: 19.2.4
|
||||||
|
|||||||
1
public/china-data/area.min.json
Normal file
1
public/china-data/city.min.json
Normal file
1
public/china-data/province.min.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[{"c":"110000","n":"北京市","p":"11"},{"c":"120000","n":"天津市","p":"12"},{"c":"130000","n":"河北省","p":"13"},{"c":"140000","n":"山西省","p":"14"},{"c":"150000","n":"内蒙古自治区","p":"15"},{"c":"210000","n":"辽宁省","p":"21"},{"c":"220000","n":"吉林省","p":"22"},{"c":"230000","n":"黑龙江省","p":"23"},{"c":"310000","n":"上海市","p":"31"},{"c":"320000","n":"江苏省","p":"32"},{"c":"330000","n":"浙江省","p":"33"},{"c":"340000","n":"安徽省","p":"34"},{"c":"350000","n":"福建省","p":"35"},{"c":"360000","n":"江西省","p":"36"},{"c":"370000","n":"山东省","p":"37"},{"c":"410000","n":"河南省","p":"41"},{"c":"420000","n":"湖北省","p":"42"},{"c":"430000","n":"湖南省","p":"43"},{"c":"440000","n":"广东省","p":"44"},{"c":"450000","n":"广西壮族自治区","p":"45"},{"c":"460000","n":"海南省","p":"46"},{"c":"500000","n":"重庆市","p":"50"},{"c":"510000","n":"四川省","p":"51"},{"c":"520000","n":"贵州省","p":"52"},{"c":"530000","n":"云南省","p":"53"},{"c":"540000","n":"西藏自治区","p":"54"},{"c":"610000","n":"陕西省","p":"61"},{"c":"620000","n":"甘肃省","p":"62"},{"c":"630000","n":"青海省","p":"63"},{"c":"640000","n":"宁夏回族自治区","p":"64"},{"c":"650000","n":"新疆维吾尔自治区","p":"65"},{"c":"710000","n":"台湾省","p":"71"},{"c":"810000","n":"香港特别行政区","p":"81"},{"c":"820000","n":"澳门特别行政区","p":"82"}]
|
||||||
|
Before Width: | Height: | Size: 3.4 MiB After Width: | Height: | Size: 243 KiB |
@@ -1,24 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
|
||||||
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
|
||||||
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="discord-icon" viewBox="0 0 20 19">
|
|
||||||
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
|
||||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
|
||||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
|
||||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="github-icon" viewBox="0 0 19 19">
|
|
||||||
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="social-icon" viewBox="0 0 20 20">
|
|
||||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
|
||||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="x-icon" viewBox="0 0 19 19">
|
|
||||||
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
|
||||||
</symbol>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.9 KiB |
98
src/App.tsx
@@ -1,16 +1,100 @@
|
|||||||
import { BrowserRouter, Route, Routes } from 'react-router-dom'
|
// import {useEffect} from 'react'
|
||||||
import HomePage from './views/home'
|
import {lazy, Suspense} from 'react'
|
||||||
import RecordPage from './views/record'
|
import {BrowserRouter, Route, Routes} from 'react-router-dom'
|
||||||
import AccountPage from './views/account'
|
import {AuthGuide} from '@/features/authGuide.tsx'
|
||||||
|
import {GlobalToast} from '@/features/notifications'
|
||||||
|
// import type { HostContextMessage } from '@/types'
|
||||||
|
|
||||||
|
const HomePage = lazy(() => import('./views/home'))
|
||||||
|
const RecordPage = lazy(() => import('./views/record'))
|
||||||
|
const AccountPage = lazy(() => import('./views/account'))
|
||||||
|
const GoodsPage = lazy(() => import('./views/goods'))
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
// useEffect(() => {
|
||||||
|
// const handleMessage = (event: MessageEvent<HostContextMessage>) => {
|
||||||
|
// const message = event.data
|
||||||
|
//
|
||||||
|
// if (!message || message.type !== 'IFRAME_CONTEXT' || !message.payload) {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// const {token, language} = message.payload
|
||||||
|
//
|
||||||
|
// console.log('postMessage', token, language)
|
||||||
|
//
|
||||||
|
// if (typeof token === 'string' && token.trim()) {
|
||||||
|
// sessionStorage.setItem('host_token', token)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (typeof language === 'string' && language.trim()) {
|
||||||
|
// sessionStorage.setItem('host_language', language)
|
||||||
|
// document.documentElement.lang = language
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// window.addEventListener('message', handleMessage)
|
||||||
|
//
|
||||||
|
// return () => {
|
||||||
|
// window.removeEventListener('message', handleMessage)
|
||||||
|
// }
|
||||||
|
// }, [])
|
||||||
|
|
||||||
|
// 本机测试
|
||||||
|
// window.postMessage(
|
||||||
|
// {
|
||||||
|
// type: 'IFRAME_CONTEXT',
|
||||||
|
// payload: {
|
||||||
|
// token: 'test-token-123',
|
||||||
|
// language: 'zh-CN',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// window.location.origin
|
||||||
|
// )
|
||||||
|
// 父页面发送iframe
|
||||||
|
// <iframe
|
||||||
|
// id="palyx-frame"
|
||||||
|
// src="https://your-iframe-app.example.com"
|
||||||
|
// style="width: 100%; border: 0;"
|
||||||
|
// ></iframe>
|
||||||
|
//
|
||||||
|
// <script>
|
||||||
|
// const iframe = document.getElementById('palyx-frame')
|
||||||
|
// const IFRAME_ORIGIN = 'https://your-iframe-app.example.com'
|
||||||
|
//
|
||||||
|
// iframe.addEventListener('load', () => {
|
||||||
|
// iframe.contentWindow.postMessage(
|
||||||
|
// {
|
||||||
|
// type: 'IFRAME_CONTEXT',
|
||||||
|
// payload: {
|
||||||
|
// token: 'token',
|
||||||
|
// language: 'zh-CN',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// window.location.origin
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
// </script>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
<GlobalToast/>
|
||||||
|
<AuthGuide>
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div className="flex min-h-screen items-center justify-center bg-[#08070E] px-6 text-center text-[14px] text-white/68">
|
||||||
|
Loading page...
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/" element={<HomePage/>}/>
|
||||||
<Route path="/record" element={<RecordPage />} />
|
<Route path="/goods" element={<GoodsPage/>}/>
|
||||||
<Route path="/account" element={<AccountPage />} />
|
<Route path="/record" element={<RecordPage/>}/>
|
||||||
|
<Route path="/account" element={<AccountPage/>}/>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
|
</AuthGuide>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
64
src/api/address.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import {http, objectToFormData} from "@/lib";
|
||||||
|
import type {
|
||||||
|
AddressAddResponse,
|
||||||
|
AddressInfo,
|
||||||
|
AddressListResponse,
|
||||||
|
} from "@/types/address.type.ts";
|
||||||
|
import type {GoodListResponse} from "@/types/business.type.ts";
|
||||||
|
|
||||||
|
/** @description 获取收货地址 */
|
||||||
|
export const addressList = (params: {
|
||||||
|
session_id: string
|
||||||
|
}): Promise<AddressListResponse> => {
|
||||||
|
return http.get<AddressListResponse>('v1/mall/addressList', {
|
||||||
|
searchParams: {
|
||||||
|
session_id: params.session_id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** @description 添加地址*/
|
||||||
|
export const addressAdd = (params: {
|
||||||
|
session_id: string
|
||||||
|
} & AddressInfo): Promise<AddressAddResponse> => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const {session_id: sessionId, ...rest} = params
|
||||||
|
const formData = objectToFormData(rest)
|
||||||
|
return http.post<AddressAddResponse>('v1/mall/addressAdd', {
|
||||||
|
body: formData,
|
||||||
|
searchParams: {
|
||||||
|
session_id: params.session_id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @description 修改地址(含设为默认) */
|
||||||
|
export const addressEdit = (params: {
|
||||||
|
session_id: string
|
||||||
|
id: string
|
||||||
|
} & AddressInfo): Promise<GoodListResponse> => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const {session_id: sessionId, ...rest} = params
|
||||||
|
const formData = objectToFormData(rest)
|
||||||
|
return http.post<GoodListResponse>('v1/mall/addressEdit', {
|
||||||
|
body: formData,
|
||||||
|
searchParams: {
|
||||||
|
session_id: params.session_id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @description 删除地址*/
|
||||||
|
export const addressDelete = (params: {
|
||||||
|
id: string,
|
||||||
|
session_id: string
|
||||||
|
}): Promise<GoodListResponse> => {
|
||||||
|
const formData = objectToFormData({id:params.id})
|
||||||
|
return http.post<GoodListResponse>('v1/mall/addressDelete', {
|
||||||
|
body: formData,
|
||||||
|
searchParams: {
|
||||||
|
session_id: params.session_id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
15
src/api/auth.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import {http, objectToFormData} from '@/lib'
|
||||||
|
import type {LoginParams, LoginResponse, ValidateTokenResponse} from "@/types/auth.type.ts";
|
||||||
|
|
||||||
|
/** @description 登录获取token */
|
||||||
|
export const login = (params: LoginParams) => http.get<LoginResponse>('v1/temLogin', {
|
||||||
|
searchParams: params,
|
||||||
|
})
|
||||||
|
|
||||||
|
/** @description Token 验证 */
|
||||||
|
export const validateToken = (token: string): Promise<ValidateTokenResponse> => {
|
||||||
|
const formData = objectToFormData({token})
|
||||||
|
return http.post<ValidateTokenResponse>('v1/mall/verifyToken', {
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
}
|
||||||
105
src/api/business.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import {http, objectToFormData} from "@/lib";
|
||||||
|
import type {goodsType, GoodListResponse, OrdersResponse, PointsLogsResponse} from "@/types/business.type.ts";
|
||||||
|
|
||||||
|
|
||||||
|
/** @description 商品列表 */
|
||||||
|
export const goodList = (params: {
|
||||||
|
type: goodsType
|
||||||
|
}): Promise<GoodListResponse> => http.get<GoodListResponse>('v1/mall/items', {
|
||||||
|
searchParams: params,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
/** @description 领取(Claim)
|
||||||
|
* @param params claim_request_id 领取请求ID使用 user_id + 时间戳生成
|
||||||
|
* */
|
||||||
|
export const claim = (params: {
|
||||||
|
claim_request_id: string,
|
||||||
|
session_id: string
|
||||||
|
}): Promise<GoodListResponse> => {
|
||||||
|
const formData = objectToFormData({claim_request_id: params.claim_request_id})
|
||||||
|
return http.post<GoodListResponse>('v1/mall/claim', {
|
||||||
|
body: formData,
|
||||||
|
searchParams: {
|
||||||
|
session_id: params.session_id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @description 红利兑换(Bonus Redeem)
|
||||||
|
* @param params item_id 商品ID
|
||||||
|
* @param params session_id 会话ID
|
||||||
|
* */
|
||||||
|
export const bonusRedeem = (params: {
|
||||||
|
item_id: string,
|
||||||
|
session_id: string
|
||||||
|
}): Promise<GoodListResponse> => {
|
||||||
|
const formData = objectToFormData({item_id: params.item_id})
|
||||||
|
return http.post<GoodListResponse>('v1/mall/bonusRedeem', {
|
||||||
|
body: formData,
|
||||||
|
searchParams: {
|
||||||
|
session_id: params.session_id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** @description 实物兑换(Physical Redeem)
|
||||||
|
* @param params item_id 商品id
|
||||||
|
* @param params session_id 会话ID
|
||||||
|
* @param params address_id 收货地址id
|
||||||
|
* */
|
||||||
|
export const physicalRedeem = (params: {
|
||||||
|
item_id: string,
|
||||||
|
session_id: string,
|
||||||
|
address_id: string,
|
||||||
|
}): Promise<GoodListResponse> => {
|
||||||
|
const formData = objectToFormData({item_id: params.item_id, address_id: params.address_id})
|
||||||
|
return http.post<GoodListResponse>('v1/mall/physicalRedeem', {
|
||||||
|
body: formData,
|
||||||
|
searchParams: {
|
||||||
|
session_id: params.session_id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @description 提现申请(Withdraw Apply)
|
||||||
|
* @param params item_id 商品id
|
||||||
|
* @param params session_id 会话ID
|
||||||
|
* */
|
||||||
|
export const withdrawApply = (params: {
|
||||||
|
item_id: string,
|
||||||
|
session_id: string
|
||||||
|
}): Promise<GoodListResponse> => {
|
||||||
|
const formData = objectToFormData({item_id: params.item_id})
|
||||||
|
return http.post<GoodListResponse>('v1/mall/withdrawApply', {
|
||||||
|
body: formData,
|
||||||
|
searchParams: {
|
||||||
|
session_id: params.session_id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** @description 订单列表 */
|
||||||
|
export const orders = (params: {
|
||||||
|
session_id: string
|
||||||
|
}): Promise<OrdersResponse> => {
|
||||||
|
return http.get<OrdersResponse>('v1/mall/orders', {
|
||||||
|
searchParams: {
|
||||||
|
session_id: params.session_id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @description 积分流水 */
|
||||||
|
export const pointsLogs = (params: {
|
||||||
|
session_id: string
|
||||||
|
}): Promise<PointsLogsResponse> => {
|
||||||
|
return http.get<PointsLogsResponse>('v1/mall/pointsLogs', {
|
||||||
|
searchParams: {
|
||||||
|
session_id: params.session_id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
8
src/api/user.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import type {UserAssetsParams, UserAssetsResponse} from "@/types/user.type.ts";
|
||||||
|
import {http} from "@/lib";
|
||||||
|
|
||||||
|
/** @description 用户资产*/
|
||||||
|
export const userAssets = (params: UserAssetsParams) => http.get<UserAssetsResponse>('v1/mall/assets', {
|
||||||
|
searchParams: params,
|
||||||
|
})
|
||||||
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<svg width="34" height="34" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<rect width="34" height="34" fill="url(#pattern0_18_206)"/>
|
|
||||||
<defs>
|
|
||||||
<pattern id="pattern0_18_206" patternContentUnits="objectBoundingBox" width="1" height="1">
|
|
||||||
<use xlink:href="#image0_18_206" transform="scale(0.0294118)"/>
|
|
||||||
</pattern>
|
|
||||||
<image id="image0_18_206" width="34" height="34" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAAiCAMAAAANmfvwAAAAAXNSR0IB2cksfwAAAblQTFRF//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//XrKAqIygAAAJN0Uk5TADWgz+Tq3siCGCy6//eUCVngHlvEuNPsHyL5hjmi29koU284EfILsHrKWvUKQocC6LwZ/qoE664X/cUboftgfZOlSE/Wn1iQRumyXTAdP27UCMmoDLEgIcOpxirS7b82/FJyKaMjN5KcmZuWRxDvYsJli4yJiIoySkCEfmjVrVZDOjvnByfLzHF1jWoNAfCDTOLx5sxRPAAAAg1JREFUeJxjZEAGjDDwHVkQic3FyIhQ+hGbEoE/SMpZGd9hKhH+CaY4GBm/gWhuxhfoSiQ/Awk+xn/PGRhkGN8D2UKP0JWwswINYnwA4Si+BhJi91CVKL8EssXvILtLkvEWihL1p0ALbiCcq/mYgUHuGooSuXcM8oxXEEp07zMwKF1CVqLPeIdB9TySp40YbzJonEFWYsp4jUH7JJISC8bLDJyvUSxi4WDQO4akxPoiA4PBERQlYt8YDG+/QCixO8fAYHwQRYkD0F7T/Qgl4l8ZGMz2oSgBGcxgvpcBSYfFHgYUJQyux4GE1S4Ix/0okLDZgaaE3+owyAmQ5ALSL/iYAU0JQyAjxAj1m2BSYS0DhpIQxu1IntZ9/ISBIWyL2CvflXAlDj8vMyADnxUMkZtBDD+okmjGDRDbdv7zOn2XDSSzLH4tskW6xmtA9n+xOgqO/zCeHw8050p8QVLicwBERjDORrGKIQ2aHYDMXMZ5QDJ5IgN2AFJSMAdoifEshGAhI+N/EM3E+J9paQxQScUUkEntCBVVk9BNMQOmv/xWJDGxb2hKnE4xMAQsQRar2YscRIWMDHV9DAzFjSj6JDIYGM4Zgz37/+ksRobmTgYtvxocvgFb1Ap0p+9yTHFPG3DiNu0tYexsZmCoK0NXofYMSXU30BmKl9GV9DQgKekt7WEsxHTABMYpEEbuVQAqHIIU/MvtggAAAABJRU5ErkJggg=="/>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
@@ -1,9 +0,0 @@
|
|||||||
<svg width="32" height="34" viewBox="0 0 32 34" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<rect width="32" height="34" fill="url(#pattern0_18_197)"/>
|
|
||||||
<defs>
|
|
||||||
<pattern id="pattern0_18_197" patternContentUnits="objectBoundingBox" width="1" height="1">
|
|
||||||
<use xlink:href="#image0_18_197" transform="scale(0.03125 0.0294118)"/>
|
|
||||||
</pattern>
|
|
||||||
<image id="image0_18_197" width="32" height="34" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAiCAMAAAAJbCvNAAAAAXNSR0IB2cksfwAAAVZQTFRF//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//Xr//XrBUhsGwAAAHJ0Uk5TPej/4SUA2Mfv5+Xccunb5LTQ2r/XNEhGRUcrdGwC+wGxnuJxkmvNKd0fOjk7NW39wAzBz69eBGINwgUHpAlzgX97NtHqBmp29Wdp9Atd1WNcCArUZFrZ02CrFfpmtyeaDsoqLC+mMdbueM5bmZaXfoOYXJMCGQAAAb5JREFUeJxjZGBkZMAJGIGA7Q9ueQYG1n+MHL/wKWD/w8j1g4EThyWMXxl4vjPyf2bg+4pdAc9HBoHPIAWCn7Ar4H9HVQUCjMjh8Z+J8TWqArHn6NqlXiIrkHiKYb7McwbGP3AFUoyP0BXIP2H4959B4THMDXJogcX4gIFB8Q4Dx298vlBifMTzHqZAhZEBagYj4w0UoyAKNBkvI8T0GK8ASQVeoM8vQBUYnkEx2/QcgzHjSRCL1RjqC6mTyAqEP5idhiYTK6gVFoiABIbiYWPu/UCW0z6EG1CBw0GQfpfvXLsZ3LAp0HnwEUi672Fw3cHgiUWB+3eQ+S77GTy2YrVCR3YLkPTdBpE3wlAA1++9CUgL2aIr8GVcDySDNjIErAXSIY/Ooilw514NMT94FZBm0b6C7oaQlUDCYzdcP7ojQ1cwMEQyrkTohyiIXgFTELmEgSF2GVQ/11IGqIJ4xh1Alifji53//zEwJP/8twzI5VG+AtbD6LIP7gAnpavHYezUn0shDMaMWQg/sv5j+w5hwfQDFWTORCiIXabx8QmIkfFtKUyMkTVtpwc4phn/MT/awiAa9of1z61/B+CaALF6szdHXa+5AAAAAElFTkSuQmCC"/>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,18 +1,19 @@
|
|||||||
import type { ButtonHTMLAttributes, PropsWithChildren } from "react";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import type { ButtonProps } from '@/types'
|
||||||
type ButtonProps = PropsWithChildren<ButtonHTMLAttributes<HTMLButtonElement>>;
|
|
||||||
|
|
||||||
export function Button({
|
export function Button({
|
||||||
children,
|
children,
|
||||||
className = "",
|
className = "",
|
||||||
type = "button",
|
type = "button",
|
||||||
|
variant = "orange",
|
||||||
...props
|
...props
|
||||||
}: ButtonProps) {
|
}: ButtonProps) {
|
||||||
|
const baseClassName = variant === "gray" ? "button-play-gray" : "button-play";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type={type}
|
type={type}
|
||||||
className={twMerge("button-play", className)}
|
className={twMerge(baseClassName, className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export function Card(){
|
|
||||||
return (<div className={'w-full h-[]'}>card</div>)
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
import type { ReactNode } from 'react'
|
import type { PageLayoutProps } from '@/types'
|
||||||
|
|
||||||
type PageLayoutProps = {
|
function PageLayout({
|
||||||
children: ReactNode
|
children,
|
||||||
contentClassName?: string
|
contentClassName = 'mx-auto flex min-h-screen w-full max-w-[1180px] flex-col px-4 pb-8 sm:px-6 lg:px-8',
|
||||||
}
|
}: PageLayoutProps) {
|
||||||
|
|
||||||
function PageLayout({ children, contentClassName = 'w-[90%] lg:w-[60%] h-full mx-auto flex flex-col' }: PageLayoutProps) {
|
|
||||||
return (
|
return (
|
||||||
<main className="text-white h-screen w-screen overflow-x-hidden bg-[url('/home-figma-frame.png')] bg-cover bg-center bg-no-repeat">
|
<main className="min-h-screen w-full overflow-x-hidden bg-[url('/home-figma-frame.png')] bg-cover bg-center bg-no-repeat text-white">
|
||||||
<div className={contentClassName}>{children}</div>
|
<div className={contentClassName}>{children}</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
import type { ReactNode } from 'react'
|
|
||||||
|
|
||||||
import { cn } from '@/lib'
|
import { cn } from '@/lib'
|
||||||
|
import type { ModalProps } from '@/types'
|
||||||
type ModalProps = {
|
|
||||||
open: boolean
|
|
||||||
title?: ReactNode
|
|
||||||
children: ReactNode
|
|
||||||
footer?: ReactNode
|
|
||||||
onClose?: () => void
|
|
||||||
closeOnOverlayClick?: boolean
|
|
||||||
className?: string
|
|
||||||
bodyClassName?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Modal({
|
export function Modal({
|
||||||
open,
|
open,
|
||||||
@@ -34,7 +22,7 @@ export function Modal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-[16px]">
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-[12px] sm:p-[16px]">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Close modal"
|
aria-label="Close modal"
|
||||||
@@ -46,16 +34,16 @@ export function Modal({
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative z-10 w-full max-w-[560px] overflow-hidden rounded-[14px] shadow-[0_18px_60px_rgba(0,0,0,0.45)]',
|
'relative z-10 flex max-h-[calc(100dvh-24px)] w-full max-w-[560px] flex-col overflow-hidden rounded-[14px] shadow-[0_18px_60px_rgba(0,0,0,0.45)] sm:max-h-[calc(100vh-32px)]',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex min-h-[58px] items-center justify-between bg-linear-to-r from-[#F96C02] to-[#FE9F00] px-[18px] py-[14px] text-white">
|
<div className="flex min-h-[58px] shrink-0 items-center justify-between bg-linear-to-r from-[#F96C02] to-[#FE9F00] px-[16px] py-[14px] text-white sm:px-[18px]">
|
||||||
<div className="text-[18px] leading-[1.2] font-bold">{title}</div>
|
<div className="pr-[12px] text-[16px] leading-[1.2] font-bold sm:text-[18px]">{title}</div>
|
||||||
{onClose ? (
|
{onClose ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex h-[30px] w-[30px] items-center justify-center rounded-full bg-black/15 text-[18px] leading-none text-white transition-colors hover:bg-black/25"
|
className="flex h-[30px] w-[30px] items-center justify-center rounded-full bg-black/15 text-[18px] leading-none text-white transition-colors hover:bg-black/25 focus-visible:ring-2 focus-visible:ring-white/85 focus-visible:ring-offset-2 focus-visible:ring-offset-[#F96C02]"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
@@ -63,12 +51,21 @@ export function Modal({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="liquid-glass-bg !rounded-t-none bg-[#08070E]/75">
|
<div className="liquid-glass-bg !rounded-t-none flex min-h-0 flex-1 flex-col bg-[#08070E]/75">
|
||||||
<div className={cn('px-[20px] py-[20px] text-white', bodyClassName)}>{children}</div>
|
<div
|
||||||
|
className={cn(
|
||||||
|
'min-h-0 flex-1 overflow-y-auto px-[16px] py-[16px] text-white sm:px-[20px] sm:py-[20px]',
|
||||||
|
bodyClassName,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
{footer ? (
|
{footer ? (
|
||||||
<div className="flex flex-wrap items-center justify-center gap-[60px] px-[20px] pb-[20px]">
|
<div className="shrink-0 border-t border-white/8 px-[16px] py-[14px] sm:px-[20px] sm:py-[16px]">
|
||||||
|
<div className="flex flex-wrap items-center justify-center gap-3 sm:gap-6">
|
||||||
{footer}
|
{footer}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,29 +1,18 @@
|
|||||||
import type { ReactNode } from 'react'
|
import type { BorderlessTableProps } from '@/types'
|
||||||
|
|
||||||
export type TableColumn<T extends Record<string, string>> = {
|
|
||||||
label: string
|
|
||||||
key: keyof T
|
|
||||||
render?: (value: T[keyof T], record: T, index: number) => ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
type BorderlessTableProps<T extends Record<string, string>> = {
|
|
||||||
columns: TableColumn<T>[]
|
|
||||||
dataSource: T[]
|
|
||||||
}
|
|
||||||
|
|
||||||
function BorderlessTable<T extends Record<string, string>>({
|
function BorderlessTable<T extends Record<string, string>>({
|
||||||
columns,
|
columns,
|
||||||
dataSource,
|
dataSource,
|
||||||
}: BorderlessTableProps<T>) {
|
}: BorderlessTableProps<T>) {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto rounded-[12px] bg-[#08070E]/55 p-[10px]">
|
<div className="overflow-x-auto rounded-[14px] border border-white/8 bg-[#08070E]/58 p-[10px] shadow-[0_12px_32px_rgba(0,0,0,0.22)]">
|
||||||
<table className="w-full min-w-[960px] table-fixed border-collapse text-left text-[14px] text-white">
|
<table className="w-full min-w-[860px] table-fixed border-collapse text-left text-[14px] text-white">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-linear-to-r from-[#F96C02] to-[#FE9F00] text-white">
|
<tr className="bg-linear-to-r from-[#F96C02] to-[#FE9F00] text-white">
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<th
|
<th
|
||||||
key={String(column.key)}
|
key={String(column.key)}
|
||||||
className="px-[16px] py-[14px] text-[13px] font-bold first:rounded-l-[10px] last:rounded-r-[10px]"
|
className="px-[16px] py-[14px] text-[12px] font-bold uppercase tracking-[0.08em] first:rounded-tl-[10px] last:rounded-tr-[10px]"
|
||||||
>
|
>
|
||||||
{column.label}
|
{column.label}
|
||||||
</th>
|
</th>
|
||||||
@@ -34,15 +23,22 @@ function BorderlessTable<T extends Record<string, string>>({
|
|||||||
{dataSource.map((record, index) => (
|
{dataSource.map((record, index) => (
|
||||||
<tr
|
<tr
|
||||||
key={`${record.name}-${index}`}
|
key={`${record.name}-${index}`}
|
||||||
className="bg-white/4 transition-colors hover:bg-white/8"
|
className="border-b border-white/6 bg-white/4 transition-colors last:border-b-0 hover:bg-white/8"
|
||||||
>
|
>
|
||||||
{columns.map((column) => {
|
{columns.map((column, columnIndex) => {
|
||||||
const value = record[column.key]
|
const value = record[column.key]
|
||||||
|
const isLastRow = index === dataSource.length - 1
|
||||||
|
const isFirstColumn = columnIndex === 0
|
||||||
|
const isLastColumn = columnIndex === columns.length - 1
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
key={String(column.key)}
|
key={String(column.key)}
|
||||||
className="px-[16px] py-[16px] align-top text-[13px] text-white/90"
|
className={`px-[16px] py-[16px] align-top text-[13px] leading-[1.55] text-white/90 ${
|
||||||
|
isLastRow && isFirstColumn ? 'rounded-bl-[10px]' : ''
|
||||||
|
} ${
|
||||||
|
isLastRow && isLastColumn ? 'rounded-br-[10px]' : ''
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{column.render ? column.render(value, record, index) : value}
|
{column.render ? column.render(value, record, index) : value}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
39
src/constant/index.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
ArrowRightLeft,
|
||||||
|
Gift,
|
||||||
|
Sparkles,
|
||||||
|
type LucideIcon,
|
||||||
|
} from 'lucide-react'
|
||||||
|
|
||||||
|
import type {goodsType} from '@/types/business.type.ts'
|
||||||
|
|
||||||
|
export type GoodCategoryMeta = {
|
||||||
|
name: string
|
||||||
|
ctaLabel: string
|
||||||
|
icon: LucideIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HOME_CATEGORY_META_MAP: Record<goodsType, GoodCategoryMeta> = {
|
||||||
|
WITHDRAW: {
|
||||||
|
name: 'Transfer to Platform',
|
||||||
|
ctaLabel: 'Transfer Now',
|
||||||
|
icon: ArrowRightLeft,
|
||||||
|
},
|
||||||
|
BONUS: {
|
||||||
|
name: 'Game Bonus',
|
||||||
|
ctaLabel: 'Redeem Bonus',
|
||||||
|
icon: Sparkles,
|
||||||
|
},
|
||||||
|
PHYSICAL: {
|
||||||
|
name: 'Physical Prizes',
|
||||||
|
ctaLabel: 'Claim Prize',
|
||||||
|
icon: Gift,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HOME_GOOD_TYPE_ORDER = ['WITHDRAW', 'BONUS', 'PHYSICAL'] as const satisfies readonly goodsType[]
|
||||||
|
|
||||||
|
export const API_ORIGIN = import.meta.env.VITE_API_ORIGIN ?? 'https://playx-api.cjdhr.top'
|
||||||
|
|
||||||
|
/**@description 待处理 / 已完成 / 已发货 / 已驳回 */
|
||||||
|
export const ORDER_STATUS = ['PENDING','COMPLETED','SHIPPED','REJECTED']
|
||||||
37
src/features/addressBook/addressValidation.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type {AddAddressForm} from '@/types'
|
||||||
|
|
||||||
|
type AddressValidationResult =
|
||||||
|
| { valid: true }
|
||||||
|
| { valid: false; message: string }
|
||||||
|
|
||||||
|
export function validateAddressFormSubmission(addressForm: AddAddressForm): AddressValidationResult {
|
||||||
|
if (!addressForm.name.trim()) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'Please enter the receiver name.',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!addressForm.phone.trim()) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'Please enter a reachable mobile number.',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addressForm.region.length !== 3) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'Please select province, city and district.',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!addressForm.detailedAddress.trim()) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'Please enter the detailed address.',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {valid: true}
|
||||||
|
}
|
||||||
2
src/features/addressBook/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './useAddressBook'
|
||||||
|
export * from './addressValidation'
|
||||||
187
src/features/addressBook/useAddressBook.ts
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import {useCallback, useState} from 'react'
|
||||||
|
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {addressAdd, addressDelete, addressEdit, addressList} from '@/api/address.ts'
|
||||||
|
import {validateAddressFormSubmission} from '@/features/addressBook/addressValidation'
|
||||||
|
import {notifyError} from '@/features/notifications'
|
||||||
|
import {queryKeys} from '@/lib/queryKeys.ts'
|
||||||
|
import {emptyAddressFormMock} from '@/mock'
|
||||||
|
import {useUserStore} from '@/store/user.ts'
|
||||||
|
import type {AddressListItem} from '@/types/address.type.ts'
|
||||||
|
import type {AddAddressForm, AddressOption} from '@/types'
|
||||||
|
|
||||||
|
type UseAddressBookOptions = {
|
||||||
|
autoLoad?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAddressText(item: AddressListItem) {
|
||||||
|
const regionText = item.region_text || item.region.map((part) => part.trim()).join(', ')
|
||||||
|
return [regionText, item.detail_address].filter(Boolean).join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapAddressToOption(item: AddressListItem): AddressOption {
|
||||||
|
return {
|
||||||
|
id: String(item.id),
|
||||||
|
name: item.receiver_name,
|
||||||
|
phone: item.phone,
|
||||||
|
address: getAddressText(item),
|
||||||
|
isDefault: item.default_setting === 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapAddressToForm(item: AddressListItem): AddAddressForm {
|
||||||
|
return {
|
||||||
|
name: item.receiver_name,
|
||||||
|
phone: item.phone,
|
||||||
|
region: item.region.map((part) => part.trim()).filter(Boolean).slice(0, 3),
|
||||||
|
detailedAddress: item.detail_address,
|
||||||
|
isDefault: item.default_setting === 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAddressBook(options?: UseAddressBookOptions) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const sessionId = useUserStore((state) => state.authInfo?.session_id ?? '')
|
||||||
|
const [addressForm, setAddressForm] = useState<AddAddressForm>(emptyAddressFormMock)
|
||||||
|
|
||||||
|
const addressListQuery = useQuery({
|
||||||
|
queryKey: queryKeys.addressList(sessionId),
|
||||||
|
enabled: Boolean(sessionId) && Boolean(options?.autoLoad),
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await addressList({session_id: sessionId!})
|
||||||
|
return response.data.list
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const saveAddressMutation = useMutation({
|
||||||
|
mutationFn: async (editingAddress?: AddressListItem | null) => {
|
||||||
|
const payload = {
|
||||||
|
session_id: sessionId!,
|
||||||
|
receiver_name: addressForm.name.trim(),
|
||||||
|
phone: addressForm.phone.trim(),
|
||||||
|
region: addressForm.region.join(', '),
|
||||||
|
detail_address: addressForm.detailedAddress.trim(),
|
||||||
|
default_setting: addressForm.isDefault ? '1' : '0',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
if (editingAddress) {
|
||||||
|
return await addressEdit({
|
||||||
|
...payload,
|
||||||
|
id: String(editingAddress.id),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return await addressAdd(payload)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteAddressMutation = useMutation({
|
||||||
|
mutationFn: async (addressId: string) => {
|
||||||
|
return await addressDelete({
|
||||||
|
id: addressId,
|
||||||
|
session_id: sessionId!,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const addresses = addressListQuery.data ?? []
|
||||||
|
const addressOptions = addresses.map(mapAddressToOption)
|
||||||
|
|
||||||
|
const isAddressFormValid = validateAddressFormSubmission(addressForm).valid
|
||||||
|
|
||||||
|
const loadAddresses = useCallback(async () => {
|
||||||
|
if (!sessionId) {
|
||||||
|
queryClient.removeQueries({queryKey: queryKeys.addressList(sessionId)})
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await queryClient.fetchQuery({
|
||||||
|
queryKey: queryKeys.addressList(sessionId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await addressList({session_id: sessionId})
|
||||||
|
return response.data.list
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}, [queryClient, sessionId])
|
||||||
|
|
||||||
|
const resetAddressForm = () => {
|
||||||
|
setAddressForm(emptyAddressFormMock)
|
||||||
|
saveAddressMutation.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
const fillAddressForm = (address?: AddressListItem | null) => {
|
||||||
|
setAddressForm(address ? mapAddressToForm(address) : emptyAddressFormMock)
|
||||||
|
saveAddressMutation.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeAddressForm = (field: keyof AddAddressForm, value: AddAddressForm[keyof AddAddressForm]) => {
|
||||||
|
setAddressForm((previous) => ({
|
||||||
|
...previous,
|
||||||
|
[field]: value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveAddress = async (editingAddress?: AddressListItem | null) => {
|
||||||
|
if (!sessionId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = validateAddressFormSubmission(addressForm)
|
||||||
|
if (!validation.valid) {
|
||||||
|
notifyError(validation.message)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await saveAddressMutation.mutateAsync(editingAddress)
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: queryKeys.addressList(sessionId),
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
addresses: await loadAddresses(),
|
||||||
|
response,
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeAddress = async (addressId: string) => {
|
||||||
|
if (!sessionId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await deleteAddressMutation.mutateAsync(addressId)
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: queryKeys.addressList(sessionId),
|
||||||
|
})
|
||||||
|
return response
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionId,
|
||||||
|
addresses,
|
||||||
|
addressOptions,
|
||||||
|
loading: addressListQuery.isPending,
|
||||||
|
addressForm,
|
||||||
|
isAddressFormValid,
|
||||||
|
submitLoading: saveAddressMutation.isPending,
|
||||||
|
deleteLoading: deleteAddressMutation.isPending,
|
||||||
|
loadAddresses,
|
||||||
|
resetAddressForm,
|
||||||
|
fillAddressForm,
|
||||||
|
changeAddressForm,
|
||||||
|
saveAddress,
|
||||||
|
removeAddress,
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/features/authGuide.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import {useQuery, useQueryClient} from '@tanstack/react-query'
|
||||||
|
import {type PropsWithChildren, useEffect} from 'react'
|
||||||
|
|
||||||
|
import {login, validateToken} from '@/api/auth.ts'
|
||||||
|
import {userAssets} from '@/api/user.ts'
|
||||||
|
import {queryKeys} from '@/lib/queryKeys.ts'
|
||||||
|
import {useUserStore} from '@/store/user.ts'
|
||||||
|
|
||||||
|
export function AuthGuide({children}: PropsWithChildren) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const setUserInfo = useUserStore((state) => state.setUserInfo)
|
||||||
|
const setAuthInfo = useUserStore((state) => state.setAuthInfo)
|
||||||
|
const setAssetsInfo = useUserStore((state) => state.setAssetsInfo)
|
||||||
|
const clearUserInfo = useUserStore((state) => state.clearUserInfo)
|
||||||
|
const authBootstrapQuery = useQuery({
|
||||||
|
queryKey: queryKeys.authBootstrap,
|
||||||
|
queryFn: async () => {
|
||||||
|
const loginResponse = await login({username: '+60777777777'})
|
||||||
|
const userInfo = loginResponse.data.userInfo
|
||||||
|
const validateResponse = await validateToken(userInfo.token)
|
||||||
|
const authInfo = validateResponse.data
|
||||||
|
const assetsResponse = await userAssets({
|
||||||
|
session_id: authInfo.session_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
userInfo,
|
||||||
|
authInfo,
|
||||||
|
assetsInfo: assetsResponse.data,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!authBootstrapQuery.data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setUserInfo(authBootstrapQuery.data.userInfo)
|
||||||
|
setAuthInfo(authBootstrapQuery.data.authInfo)
|
||||||
|
setAssetsInfo(authBootstrapQuery.data.assetsInfo)
|
||||||
|
queryClient.setQueryData(queryKeys.assets(authBootstrapQuery.data.authInfo.session_id), authBootstrapQuery.data.assetsInfo)
|
||||||
|
}, [authBootstrapQuery.data, queryClient, setAssetsInfo, setAuthInfo, setUserInfo])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!authBootstrapQuery.isError) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clearUserInfo()
|
||||||
|
}, [authBootstrapQuery.isError, clearUserInfo])
|
||||||
|
|
||||||
|
if (authBootstrapQuery.isPending) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center bg-[#08070E] px-6 text-center text-[14px] text-white/68">
|
||||||
|
Loading account data...
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authBootstrapQuery.isError) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center bg-[#08070E] px-6 text-center">
|
||||||
|
<div className="max-w-[420px] rounded-[14px] border border-white/10 bg-white/4 px-[18px] py-[16px]">
|
||||||
|
<div className="text-[16px] font-semibold text-white">Authentication failed</div>
|
||||||
|
<div className="mt-[8px] text-[13px] leading-[1.6] text-white/58">Please refresh and try again.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
131
src/features/goods/GoodsCategoryList.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import {useState} from 'react'
|
||||||
|
|
||||||
|
import {ChevronRight} from 'lucide-react'
|
||||||
|
|
||||||
|
import Button from '@/components/button'
|
||||||
|
import {HOME_CATEGORY_META_MAP} from '@/constant'
|
||||||
|
import type {ProductCategory, ProductItem} from '@/types'
|
||||||
|
|
||||||
|
type GoodsCategoryListProps = {
|
||||||
|
categories: ProductCategory[]
|
||||||
|
loading?: boolean
|
||||||
|
emptyText?: string
|
||||||
|
showMore?: boolean
|
||||||
|
onMoreClick?: (categoryId: ProductCategory['id']) => void
|
||||||
|
onRedeem: (product: ProductItem, categoryId: ProductCategory['id']) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function GoodsImage({
|
||||||
|
imageUrl,
|
||||||
|
}: {
|
||||||
|
imageUrl?: string
|
||||||
|
}) {
|
||||||
|
const [hasError, setHasError] = useState(false)
|
||||||
|
const showFallback = !imageUrl || hasError
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative h-[116px] w-full overflow-hidden rounded-t-[10px] sm:h-[128px]">
|
||||||
|
{showFallback ? (
|
||||||
|
<div className="absolute inset-0 flex flex-col items-center justify-center bg-[radial-gradient(circle_at_top,rgba(250,106,0,0.18),transparent_58%),linear-gradient(180deg,rgba(255,255,255,0.06),rgba(255,255,255,0.02))] px-[12px] text-center">
|
||||||
|
<div className="h-[30px] w-[30px] rounded-[10px] border border-white/10 bg-white/6"></div>
|
||||||
|
<div className="mt-[10px] text-[12px] tracking-[0.08em] text-white/42">NO IMAGE</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{imageUrl ? (
|
||||||
|
<img
|
||||||
|
src={imageUrl}
|
||||||
|
alt=""
|
||||||
|
className={`h-full w-full object-cover ${showFallback ? 'hidden' : 'block'}`}
|
||||||
|
onError={() => setHasError(true)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GoodsCategoryList({
|
||||||
|
categories,
|
||||||
|
loading = false,
|
||||||
|
emptyText = 'No goods available yet.',
|
||||||
|
showMore = false,
|
||||||
|
onMoreClick,
|
||||||
|
onRedeem,
|
||||||
|
}: GoodsCategoryListProps) {
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="pb-[24px] mt-[20px]">
|
||||||
|
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
||||||
|
Loading ...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!categories.length) {
|
||||||
|
return (
|
||||||
|
<div className="pb-[24px]">
|
||||||
|
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
||||||
|
{emptyText}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pb-[24px]">
|
||||||
|
{categories.map((category) => {
|
||||||
|
const CategoryIcon = HOME_CATEGORY_META_MAP[category.id].icon
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={category.id} className="mt-[20px]">
|
||||||
|
<div className="mb-[10px] flex items-center justify-between gap-[12px]">
|
||||||
|
<div className="flex min-w-0 items-center gap-[10px]">
|
||||||
|
<div
|
||||||
|
className="flex h-[36px] w-[36px] items-center justify-center rounded-[12px] bg-[#FA6A00]/15 text-[#FE9F00]">
|
||||||
|
<CategoryIcon className="h-[18px] w-[18px]" aria-hidden="true"/>
|
||||||
|
</div>
|
||||||
|
<div className="truncate text-[15px] font-semibold text-white">{category.name}</div>
|
||||||
|
</div>
|
||||||
|
{showMore && onMoreClick ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex shrink-0 items-center gap-[3px] text-[12px] font-light text-[#FA6A00] underline cursor-pointer"
|
||||||
|
onClick={() => onMoreClick(category.id)}
|
||||||
|
>
|
||||||
|
more
|
||||||
|
<ChevronRight className="h-[14px] w-[14px]" aria-hidden="true"/>
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||||
|
{category.items.map((product) => (
|
||||||
|
<div
|
||||||
|
key={product.id}
|
||||||
|
className="liquid-glass-bg flex min-h-[260px] w-full flex-col items-stretch justify-start overflow-hidden"
|
||||||
|
>
|
||||||
|
<GoodsImage imageUrl={product.imageUrl}/>
|
||||||
|
<div
|
||||||
|
className="flex flex-1 flex-col items-start justify-between gap-[12px] p-[12px] sm:p-[14px]"
|
||||||
|
>
|
||||||
|
<div className="space-y-[4px]">
|
||||||
|
<div className="text-[16px] font-medium text-white">{product.title}</div>
|
||||||
|
<div className="text-[13px] text-white/52">{product.subtitle}</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-[16px] font-semibold text-[#FA6A00]">{product.score}</div>
|
||||||
|
<Button
|
||||||
|
className="h-[36px] w-full text-[13px]"
|
||||||
|
onClick={() => onRedeem(product, category.id)}
|
||||||
|
>
|
||||||
|
{product.ctaLabel}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
354
src/features/goods/GoodsRedeemModal.tsx
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
import {
|
||||||
|
Check,
|
||||||
|
ChevronRight,
|
||||||
|
CirclePlus,
|
||||||
|
Gift,
|
||||||
|
MapPinHouse,
|
||||||
|
} from 'lucide-react'
|
||||||
|
|
||||||
|
import Button from '@/components/button'
|
||||||
|
import Modal from '@/components/modal'
|
||||||
|
import {RegionPicker} from '@/features/goods/RegionPicker'
|
||||||
|
import type {
|
||||||
|
AddAddressForm,
|
||||||
|
AddressOption,
|
||||||
|
ModalMode,
|
||||||
|
SelectedProductState,
|
||||||
|
} from '@/types'
|
||||||
|
|
||||||
|
function getNumericValue(value: string) {
|
||||||
|
const matched = value.match(/\d+/)
|
||||||
|
return matched ? matched[0] : value
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTurnoverRequirement(subtitle: string) {
|
||||||
|
const matched = subtitle.match(/\d+/)
|
||||||
|
return matched ? `${matched[0]}x` : subtitle
|
||||||
|
}
|
||||||
|
|
||||||
|
type GoodsRedeemModalProps = {
|
||||||
|
selectedProduct: SelectedProductState | null
|
||||||
|
modalMode: ModalMode
|
||||||
|
addressOptions: AddressOption[]
|
||||||
|
selectedAddressId: string
|
||||||
|
addressForm: AddAddressForm
|
||||||
|
addressLoading: boolean
|
||||||
|
isAddAddressFormValid: boolean
|
||||||
|
submitLoading: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onConfirm: () => void
|
||||||
|
onOpenAddAddress: () => void
|
||||||
|
onBackToSelectAddress: () => void
|
||||||
|
onSelectAddress: (addressId: string) => void
|
||||||
|
onChangeAddressForm: (field: keyof AddAddressForm, value: AddAddressForm[keyof AddAddressForm]) => void
|
||||||
|
forceOpen?: boolean
|
||||||
|
formOnly?: boolean
|
||||||
|
titleOverride?: string
|
||||||
|
confirmText?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GoodsRedeemModal({
|
||||||
|
selectedProduct,
|
||||||
|
modalMode,
|
||||||
|
addressOptions,
|
||||||
|
selectedAddressId,
|
||||||
|
addressForm,
|
||||||
|
addressLoading,
|
||||||
|
isAddAddressFormValid,
|
||||||
|
submitLoading,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
onOpenAddAddress,
|
||||||
|
onBackToSelectAddress,
|
||||||
|
onSelectAddress,
|
||||||
|
onChangeAddressForm,
|
||||||
|
forceOpen = false,
|
||||||
|
formOnly = false,
|
||||||
|
titleOverride,
|
||||||
|
confirmText = 'Confirm',
|
||||||
|
}: GoodsRedeemModalProps) {
|
||||||
|
const selectedCategoryId = selectedProduct?.categoryId
|
||||||
|
const selectedProductData = selectedProduct?.product ?? null
|
||||||
|
const isPhysicalPrize = selectedCategoryId === 'PHYSICAL'
|
||||||
|
const isTransferToPlatform = selectedCategoryId === 'WITHDRAW'
|
||||||
|
const isGameBonus = selectedCategoryId === 'BONUS'
|
||||||
|
const isPhysicalPrizeSelection = modalMode === 'select-address' && isPhysicalPrize
|
||||||
|
const modalTitle = modalMode === 'add-address'
|
||||||
|
? 'Add Shipping Address'
|
||||||
|
: isTransferToPlatform
|
||||||
|
? 'Confirm Withdrawal'
|
||||||
|
: isGameBonus
|
||||||
|
? 'Confirm Bonus Redemption'
|
||||||
|
: 'Confirm Physical Reward'
|
||||||
|
const modalMaxWidthClassName = isTransferToPlatform
|
||||||
|
? 'max-w-[620px]'
|
||||||
|
: isGameBonus
|
||||||
|
? 'max-w-[460px]'
|
||||||
|
: 'max-w-[680px]'
|
||||||
|
const resolvedTitle = titleOverride ?? modalTitle
|
||||||
|
const isAddressFormMode = formOnly || isPhysicalPrize
|
||||||
|
|
||||||
|
const addressFormContent = (
|
||||||
|
<div
|
||||||
|
className="rounded-[12px] bg-[#171313]/88 px-[14px] py-[10px] shadow-[0_10px_30px_rgba(0,0,0,0.25)]">
|
||||||
|
<div
|
||||||
|
className="flex flex-col gap-[10px] border-b border-white/10 px-[6px] pb-[16px] sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div className="flex items-center gap-[8px] text-[18px] font-medium text-white">
|
||||||
|
<MapPinHouse className="h-[18px] w-[18px] text-[#FE9F00]" aria-hidden="true"/>
|
||||||
|
<span>Address Info</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-[8px] text-[14px] text-white/85"
|
||||||
|
onClick={() => onChangeAddressForm('isDefault', !addressForm.isDefault)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`flex h-[16px] w-[16px] items-center justify-center rounded-full border ${
|
||||||
|
addressForm.isDefault ? 'border-[#FA6A00]' : 'border-white/35'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`h-[8px] w-[8px] rounded-full ${
|
||||||
|
addressForm.isDefault ? 'bg-[#FA6A00]' : 'bg-transparent'
|
||||||
|
}`}
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
<span>Default Address</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="divide-y divide-white/10">
|
||||||
|
<div
|
||||||
|
className="grid grid-cols-1 gap-[10px] px-[6px] py-[16px] sm:grid-cols-[170px_1fr] sm:items-center">
|
||||||
|
<label className="text-[14px] text-white/92">
|
||||||
|
Name<span className="text-[#FA6A00]">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
value={addressForm.name}
|
||||||
|
onChange={(event) => onChangeAddressForm('name', event.target.value)}
|
||||||
|
placeholder="Enter receiver's full name"
|
||||||
|
className="bg-transparent text-[14px] text-white outline-none placeholder:text-white/35"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="grid grid-cols-1 gap-[10px] px-[6px] py-[16px] sm:grid-cols-[170px_1fr] sm:items-center">
|
||||||
|
<label className="text-[14px] text-white/92">
|
||||||
|
Phone Number<span className="text-[#FA6A00]">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
value={addressForm.phone}
|
||||||
|
type="number"
|
||||||
|
onChange={(event) => onChangeAddressForm('phone', event.target.value)}
|
||||||
|
placeholder="Enter a reachable mobile number"
|
||||||
|
className="bg-transparent text-[14px] text-white outline-none placeholder:text-white/35"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="grid grid-cols-1 gap-[10px] px-[6px] py-[16px] sm:grid-cols-[170px_1fr] sm:items-center">
|
||||||
|
<label className="text-[14px] text-white/92">
|
||||||
|
Region<span className="text-[#FA6A00]">*</span>
|
||||||
|
</label>
|
||||||
|
<RegionPicker
|
||||||
|
value={addressForm.region}
|
||||||
|
onChange={(value) => onChangeAddressForm('region', value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="grid grid-cols-1 gap-[10px] px-[6px] py-[16px] sm:grid-cols-[170px_1fr] sm:items-center">
|
||||||
|
<label className="text-[14px] text-white/92">
|
||||||
|
Detailed Address<span className="text-[#FA6A00]">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
value={addressForm.detailedAddress}
|
||||||
|
onChange={(event) => onChangeAddressForm('detailedAddress', event.target.value)}
|
||||||
|
placeholder="Street, building, unit and room number"
|
||||||
|
className="bg-transparent text-[14px] text-white outline-none placeholder:text-white/35"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={forceOpen || Boolean(selectedProduct)}
|
||||||
|
title={resolvedTitle}
|
||||||
|
onClose={onClose}
|
||||||
|
className={formOnly ? 'max-w-[680px]' : modalMaxWidthClassName}
|
||||||
|
bodyClassName="space-y-[18px]"
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="gray"
|
||||||
|
className={`h-[38px] text-[14px] ${
|
||||||
|
isPhysicalPrizeSelection ? 'w-[124px]' : 'w-full sm:w-auto'
|
||||||
|
}`}
|
||||||
|
onClick={modalMode === 'add-address' ? onBackToSelectAddress : onClose}
|
||||||
|
disabled={submitLoading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className={`h-[38px] ${
|
||||||
|
isPhysicalPrizeSelection ? 'w-[124px]' : 'w-full sm:w-auto'
|
||||||
|
} ${modalMode === 'add-address' && !isAddAddressFormValid ? 'opacity-50' : ''}`}
|
||||||
|
onClick={onConfirm}
|
||||||
|
disabled={submitLoading || (modalMode === 'add-address' && !isAddAddressFormValid)}
|
||||||
|
>
|
||||||
|
{submitLoading ? 'Processing...' : confirmText}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{formOnly ? (
|
||||||
|
addressFormContent
|
||||||
|
) : selectedProductData ? (
|
||||||
|
<>
|
||||||
|
{isTransferToPlatform ? (
|
||||||
|
<div
|
||||||
|
className="rounded-[14px] bg-[#1C1818]/80 px-[18px] py-[14px] shadow-[0_10px_30px_rgba(0,0,0,0.22)]">
|
||||||
|
<div className="divide-y divide-white/8">
|
||||||
|
<div className="flex items-center justify-between py-[14px] text-white">
|
||||||
|
<div className="text-[18px]">Withdrawal Amount</div>
|
||||||
|
<div className="text-[18px]">{getNumericValue(selectedProductData.title)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between py-[14px] text-white">
|
||||||
|
<div className="text-[18px]">Points Required</div>
|
||||||
|
<div className="text-[18px]">{getNumericValue(selectedProductData.score)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between py-[14px] text-white">
|
||||||
|
<div className="text-[18px] underline decoration-[#1E90FF] underline-offset-[3px]">
|
||||||
|
Turnover Requirement
|
||||||
|
</div>
|
||||||
|
<div className="text-[18px]">{getTurnoverRequirement(selectedProductData.subtitle)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="pt-[10px] text-center text-[18px] text-white/45">
|
||||||
|
Submit withdrawal request?
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : isGameBonus ? (
|
||||||
|
<div
|
||||||
|
className="rounded-[10px] bg-[#1C1818]/80 px-[16px] py-[8px] shadow-[0_10px_30px_rgba(0,0,0,0.22)]">
|
||||||
|
<div className="divide-y divide-white/8">
|
||||||
|
<div className="flex items-center justify-between py-[14px] text-white">
|
||||||
|
<div className="text-[13px] text-white/78">Item</div>
|
||||||
|
<div className="text-[14px]">{selectedProductData.title}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between py-[14px] text-white">
|
||||||
|
<div className="text-[13px] text-white/78">Points Required</div>
|
||||||
|
<div className="text-[14px]">{getNumericValue(selectedProductData.score)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between py-[14px] text-white">
|
||||||
|
<div className="text-[13px] text-white/78">Turnover Requirement</div>
|
||||||
|
<div className="text-[14px]">{getTurnoverRequirement(selectedProductData.subtitle)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : modalMode === 'select-address' && isPhysicalPrize ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="rounded-[12px] bg-[#1F1B1B]/82 px-[14px] py-[10px] shadow-[0_10px_28px_rgba(0,0,0,0.2)]">
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-between gap-[12px] border-b border-white/8 py-[12px]">
|
||||||
|
<div className="text-[13px] text-white/82">Item</div>
|
||||||
|
<div className="flex items-center gap-[10px]">
|
||||||
|
<div className="text-right text-[15px] text-white">{selectedProductData.title}</div>
|
||||||
|
<div className="h-[54px] w-[54px] overflow-hidden rounded-[10px] shadow-[0_10px_18px_rgba(0,0,0,0.2)]">
|
||||||
|
{selectedProductData.imageUrl ? (
|
||||||
|
<img
|
||||||
|
src={selectedProductData.imageUrl}
|
||||||
|
alt={selectedProductData.title}
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Gift className="h-[24px] w-[24px] text-white" aria-hidden="true"/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between py-[12px]">
|
||||||
|
<div className="text-[13px] text-white/82">Points Cost</div>
|
||||||
|
<div className="text-[15px] text-white">{getNumericValue(selectedProductData.score)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="mb-[12px] text-[14px] text-white/40">
|
||||||
|
Please select the address information to fill in.
|
||||||
|
</div>
|
||||||
|
<div className="overflow-hidden rounded-[12px] bg-[#1F1B1B]/82 shadow-[0_10px_28px_rgba(0,0,0,0.2)]">
|
||||||
|
{addressLoading ? (
|
||||||
|
<div className="px-[14px] py-[16px] text-[14px] text-white/60">
|
||||||
|
Loading address list...
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{addressOptions.map((address) => {
|
||||||
|
const isSelected = selectedAddressId === address.id
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={address.id}
|
||||||
|
type="button"
|
||||||
|
className={`flex w-full items-start gap-[12px] border-b border-white/6 px-[14px] py-[14px] text-left transition-colors last:border-b-0 hover:bg-white/4 ${
|
||||||
|
isSelected ? 'bg-white/[0.03]' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => onSelectAddress(address.id)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mt-[2px] flex h-[20px] w-[20px] shrink-0 items-center justify-center rounded-full">
|
||||||
|
{isSelected ? (
|
||||||
|
<span
|
||||||
|
className="flex h-[20px] w-[20px] items-center justify-center rounded-full bg-[#FA8A00] text-white shadow-[0_0_12px_rgba(250,138,0,0.45)]">
|
||||||
|
<Check className="h-[12px] w-[12px]" aria-hidden="true"/>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="h-[20px] w-[20px] rounded-full border border-white/40"></span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center gap-[8px]">
|
||||||
|
<div className="text-[14px] font-bold text-white">{address.name}</div>
|
||||||
|
<div className="text-[12px] text-white/45">{address.phone}</div>
|
||||||
|
{address.isDefault ? (
|
||||||
|
<div
|
||||||
|
className="rounded-[4px] bg-[#FF7F7F] px-[5px] py-[1px] text-[10px] leading-none text-white">
|
||||||
|
Default
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="mt-[6px] text-[13px] leading-[1.5] text-white/52">
|
||||||
|
{address.address}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex w-full items-center justify-between px-[14px] py-[16px] text-left transition-colors hover:bg-white/4 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]"
|
||||||
|
onClick={onOpenAddAddress}
|
||||||
|
disabled={addressLoading}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-[10px]">
|
||||||
|
<span
|
||||||
|
className="flex h-[16px] w-[16px] items-center justify-center rounded-full border border-white/70 text-white">
|
||||||
|
<CirclePlus className="h-[12px] w-[12px]" aria-hidden="true"/>
|
||||||
|
</span>
|
||||||
|
<div className="text-[14px] text-white/82">Add Address</div>
|
||||||
|
</div>
|
||||||
|
<ChevronRight className="h-[16px] w-[16px] text-white/45" aria-hidden="true"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : isAddressFormMode ? addressFormContent : null}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
158
src/features/goods/RegionPicker.tsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import {useEffect, useState} from 'react'
|
||||||
|
|
||||||
|
type ProvinceNode = {
|
||||||
|
c: string
|
||||||
|
n: string
|
||||||
|
p: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CityNode = {
|
||||||
|
c: string
|
||||||
|
n: string
|
||||||
|
p: string
|
||||||
|
y: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AreaNode = {
|
||||||
|
c: string
|
||||||
|
n: string
|
||||||
|
p: string
|
||||||
|
y: string
|
||||||
|
a: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegionPickerProps = {
|
||||||
|
value: string[]
|
||||||
|
onChange: (value: string[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegionDataset = {
|
||||||
|
province: ProvinceNode[]
|
||||||
|
city: CityNode[]
|
||||||
|
area: AreaNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function RegionSelect({
|
||||||
|
value,
|
||||||
|
placeholder,
|
||||||
|
options,
|
||||||
|
onChange,
|
||||||
|
disabled = false,
|
||||||
|
}: {
|
||||||
|
value: string
|
||||||
|
placeholder: string
|
||||||
|
options: Array<{c: string; n: string}>
|
||||||
|
onChange: (value: string) => void
|
||||||
|
disabled?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<select
|
||||||
|
value={value}
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
disabled={disabled}
|
||||||
|
className="h-[42px] w-full appearance-none rounded-[10px] border border-white/10 bg-[#0E0B12] px-[12px] pr-[36px] text-[14px] text-white outline-none transition-colors focus:border-[#FA6A00] disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<option value="">{placeholder}</option>
|
||||||
|
{options.map((option) => (
|
||||||
|
<option key={option.c} value={option.n}>
|
||||||
|
{option.n}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div className="pointer-events-none absolute right-[12px] top-1/2 -translate-y-1/2 text-[12px] text-white/38">
|
||||||
|
v
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RegionPicker({value, onChange}: RegionPickerProps) {
|
||||||
|
const [regionData, setRegionData] = useState<RegionDataset | null>(null)
|
||||||
|
const [loadFailed, setLoadFailed] = useState(false)
|
||||||
|
const [province = '', city = '', district = ''] = value
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
|
const loadRegionData = async () => {
|
||||||
|
try {
|
||||||
|
const [provinceResponse, cityResponse, areaResponse] = await Promise.all([
|
||||||
|
fetch(`${import.meta.env.BASE_URL}china-data/province.min.json`),
|
||||||
|
fetch(`${import.meta.env.BASE_URL}china-data/city.min.json`),
|
||||||
|
fetch(`${import.meta.env.BASE_URL}china-data/area.min.json`),
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!provinceResponse.ok || !cityResponse.ok || !areaResponse.ok) {
|
||||||
|
throw new Error('Failed to load region data')
|
||||||
|
}
|
||||||
|
|
||||||
|
const [provinceList, cityList, areaList] = await Promise.all([
|
||||||
|
provinceResponse.json() as Promise<ProvinceNode[]>,
|
||||||
|
cityResponse.json() as Promise<CityNode[]>,
|
||||||
|
areaResponse.json() as Promise<AreaNode[]>,
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!cancelled) {
|
||||||
|
setRegionData({
|
||||||
|
province: provinceList,
|
||||||
|
city: cityList,
|
||||||
|
area: areaList,
|
||||||
|
})
|
||||||
|
setLoadFailed(false)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (!cancelled) {
|
||||||
|
setLoadFailed(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadRegionData()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const provinceList = regionData?.province ?? []
|
||||||
|
const cityList = regionData?.city ?? []
|
||||||
|
const areaList = regionData?.area ?? []
|
||||||
|
|
||||||
|
const selectedProvince = provinceList.find((item) => item.n === province)
|
||||||
|
const selectedCity = cityList.find((item) => item.n === city && item.p === selectedProvince?.p)
|
||||||
|
|
||||||
|
const cities = selectedProvince
|
||||||
|
? cityList.filter((item) => item.p === selectedProvince.p)
|
||||||
|
: []
|
||||||
|
|
||||||
|
const districts = selectedCity
|
||||||
|
? areaList.filter((item) => item.p === selectedCity.p && item.y === selectedCity.y)
|
||||||
|
: []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 gap-[10px] sm:grid-cols-3">
|
||||||
|
<RegionSelect
|
||||||
|
value={province}
|
||||||
|
placeholder={loadFailed ? 'failed to load' : 'province'}
|
||||||
|
options={provinceList}
|
||||||
|
onChange={(nextProvince) => onChange(nextProvince ? [nextProvince] : [])}
|
||||||
|
disabled={loadFailed || !regionData}
|
||||||
|
/>
|
||||||
|
<RegionSelect
|
||||||
|
value={city}
|
||||||
|
placeholder="city"
|
||||||
|
options={cities}
|
||||||
|
disabled={loadFailed || !regionData || !province}
|
||||||
|
onChange={(nextCity) => onChange(nextCity ? [province, nextCity] : [province])}
|
||||||
|
/>
|
||||||
|
<RegionSelect
|
||||||
|
value={district}
|
||||||
|
placeholder="district"
|
||||||
|
options={districts}
|
||||||
|
disabled={loadFailed || !regionData || !city}
|
||||||
|
onChange={(nextDistrict) => onChange(nextDistrict ? [province, city, nextDistrict] : [province, city])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
7
src/features/goods/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export * from './GoodsCategoryList'
|
||||||
|
export * from './GoodsRedeemModal'
|
||||||
|
export * from './RegionPicker'
|
||||||
|
export * from './useAssetsRefresh'
|
||||||
|
export * from './useAssetsQuery'
|
||||||
|
export * from './useGoodsCatalog'
|
||||||
|
export * from './useGoodsRedeem'
|
||||||
61
src/features/goods/redeemValidation.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import type {SelectedProductState} from '@/types'
|
||||||
|
|
||||||
|
type RedeemValidationParams = {
|
||||||
|
sessionId?: string
|
||||||
|
selectedProduct: SelectedProductState | null
|
||||||
|
selectedAddressId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RedeemValidationResult =
|
||||||
|
| { valid: true }
|
||||||
|
| { valid: false; message: string }
|
||||||
|
|
||||||
|
export function validateRedeemSubmission({
|
||||||
|
sessionId,
|
||||||
|
selectedProduct,
|
||||||
|
selectedAddressId,
|
||||||
|
}: RedeemValidationParams): RedeemValidationResult {
|
||||||
|
if (!sessionId) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'Session expired. Please log in again.',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedProduct) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'No product selected.',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedProduct.categoryId === 'PHYSICAL' && !selectedAddressId) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'Please select a shipping address.',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddAddressValidationParams = {
|
||||||
|
isAddAddressFormValid: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddAddressValidationResult =
|
||||||
|
| { valid: true }
|
||||||
|
| { valid: false; message: string }
|
||||||
|
|
||||||
|
export function validateAddAddressSubmission({
|
||||||
|
isAddAddressFormValid,
|
||||||
|
}: AddAddressValidationParams): AddAddressValidationResult {
|
||||||
|
if (!isAddAddressFormValid) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'Please complete all required address fields.',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {valid: true}
|
||||||
|
}
|
||||||
39
src/features/goods/useAssetsQuery.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import {useEffect} from 'react'
|
||||||
|
import {useQuery} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {userAssets} from '@/api/user.ts'
|
||||||
|
import {queryKeys} from '@/lib/queryKeys.ts'
|
||||||
|
import {useUserStore} from '@/store/user.ts'
|
||||||
|
|
||||||
|
export function useAssetsQuery() {
|
||||||
|
const sessionId = useUserStore((state) => state.authInfo?.session_id ?? '')
|
||||||
|
const setAssetsInfo = useUserStore((state) => state.setAssetsInfo)
|
||||||
|
const fallbackAssetsInfo = useUserStore((state) => state.assetsInfo)
|
||||||
|
|
||||||
|
const assetsQuery = useQuery({
|
||||||
|
queryKey: queryKeys.assets(sessionId),
|
||||||
|
enabled: Boolean(sessionId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await userAssets({
|
||||||
|
session_id: sessionId,
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
initialData: fallbackAssetsInfo ?? undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!assetsQuery.data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setAssetsInfo(assetsQuery.data)
|
||||||
|
}, [assetsQuery.data, setAssetsInfo])
|
||||||
|
|
||||||
|
return {
|
||||||
|
assetsInfo: assetsQuery.data ?? null,
|
||||||
|
assetsLoading: assetsQuery.isPending,
|
||||||
|
assetsError: assetsQuery.error instanceof Error ? assetsQuery.error.message : null,
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/features/goods/useAssetsRefresh.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {queryKeys} from '@/lib/queryKeys.ts'
|
||||||
|
import {useUserStore} from '@/store/user.ts'
|
||||||
|
|
||||||
|
export function useAssetsRefresh() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const sessionId = useUserStore((state) => state.authInfo?.session_id ?? '')
|
||||||
|
|
||||||
|
const invalidateAssets = async () => {
|
||||||
|
if (!sessionId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: queryKeys.assets(sessionId),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
invalidateAssets,
|
||||||
|
}
|
||||||
|
}
|
||||||
89
src/features/goods/useGoodsCatalog.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import {useQuery, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {goodList} from '@/api/business.ts'
|
||||||
|
import {
|
||||||
|
API_ORIGIN,
|
||||||
|
HOME_CATEGORY_META_MAP,
|
||||||
|
HOME_GOOD_TYPE_ORDER,
|
||||||
|
} from '@/constant'
|
||||||
|
import {queryKeys} from '@/lib/queryKeys.ts'
|
||||||
|
import type {ProductCategory, ProductItem} from '@/types'
|
||||||
|
import type {GoodsItem, goodsType} from '@/types/business.type.ts'
|
||||||
|
|
||||||
|
function getProductImageUrl(image?: string) {
|
||||||
|
if (!image) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^https?:\/\//.test(image)) {
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
|
return new URL(image, API_ORIGIN).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapGoodItemToProductItem(item: GoodsItem, type: goodsType): ProductItem {
|
||||||
|
const categoryMeta = HOME_CATEGORY_META_MAP[type]
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: String(item.id),
|
||||||
|
title: item.title,
|
||||||
|
subtitle: item.description,
|
||||||
|
score: `${item.score} Points`,
|
||||||
|
ctaLabel: categoryMeta.ctaLabel,
|
||||||
|
imageUrl: getProductImageUrl(item.image),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProductCategories(groups: Record<goodsType, GoodsItem[]>): ProductCategory[] {
|
||||||
|
return HOME_GOOD_TYPE_ORDER.map((type) => ({
|
||||||
|
id: type,
|
||||||
|
name: HOME_CATEGORY_META_MAP[type].name,
|
||||||
|
items: groups[type].map((item) => mapGoodItemToProductItem(item, type)),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isGoodsType(value: string | null | undefined): value is goodsType {
|
||||||
|
return value === 'WITHDRAW' || value === 'BONUS' || value === 'PHYSICAL'
|
||||||
|
}
|
||||||
|
|
||||||
|
type UseGoodsCatalogOptions = {
|
||||||
|
types?: readonly goodsType[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGoodsCatalog(options?: UseGoodsCatalogOptions) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const types = options?.types?.length ? [...options.types] : HOME_GOOD_TYPE_ORDER
|
||||||
|
const goodsCatalogQuery = useQuery({
|
||||||
|
queryKey: queryKeys.goodsCatalog(types),
|
||||||
|
queryFn: async () => {
|
||||||
|
const responses = await Promise.all(
|
||||||
|
types.map(async (type) => {
|
||||||
|
const response = await goodList({type})
|
||||||
|
return [type, response.data.list] as const
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const groups = responses.reduce<Record<goodsType, GoodsItem[]>>(
|
||||||
|
(result, [type, items]) => {
|
||||||
|
result[type] = items
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
{
|
||||||
|
WITHDRAW: [],
|
||||||
|
BONUS: [],
|
||||||
|
PHYSICAL: [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return buildProductCategories(groups)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
productCategories: goodsCatalogQuery.data ?? [],
|
||||||
|
loading: goodsCatalogQuery.isPending,
|
||||||
|
error: goodsCatalogQuery.error instanceof Error ? goodsCatalogQuery.error.message : null,
|
||||||
|
invalidateGoods: () => queryClient.invalidateQueries({queryKey: ['goods-catalog']}),
|
||||||
|
}
|
||||||
|
}
|
||||||
156
src/features/goods/useGoodsRedeem.ts
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||||
|
import {useState} from 'react'
|
||||||
|
|
||||||
|
import {bonusRedeem, physicalRedeem, withdrawApply} from '@/api/business.ts'
|
||||||
|
import {useAddressBook} from '@/features/addressBook'
|
||||||
|
import {notifyError, notifySuccess} from '@/features/notifications'
|
||||||
|
import {queryKeys} from '@/lib/queryKeys.ts'
|
||||||
|
import {validateAddAddressSubmission, validateRedeemSubmission} from '@/features/goods/redeemValidation'
|
||||||
|
import type {
|
||||||
|
ModalMode,
|
||||||
|
ProductCategory,
|
||||||
|
ProductItem,
|
||||||
|
SelectedProductState,
|
||||||
|
} from '@/types'
|
||||||
|
|
||||||
|
export function useGoodsRedeem() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const addressBook = useAddressBook()
|
||||||
|
const [selectedProduct, setSelectedProduct] = useState<SelectedProductState | null>(null)
|
||||||
|
const [modalMode, setModalMode] = useState<ModalMode>('select-address')
|
||||||
|
const [selectedAddressId, setSelectedAddressId] = useState<string>('')
|
||||||
|
const redeemMutation = useMutation({
|
||||||
|
mutationFn: async (selectedProductState: SelectedProductState) => {
|
||||||
|
if (selectedProductState.categoryId === 'BONUS') {
|
||||||
|
return await bonusRedeem({
|
||||||
|
item_id: selectedProductState.product.id,
|
||||||
|
session_id: addressBook.sessionId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedProductState.categoryId === 'PHYSICAL') {
|
||||||
|
return await physicalRedeem({
|
||||||
|
item_id: selectedProductState.product.id,
|
||||||
|
session_id: addressBook.sessionId,
|
||||||
|
address_id: selectedAddressId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return await withdrawApply({
|
||||||
|
item_id: selectedProductState.product.id,
|
||||||
|
session_id: addressBook.sessionId,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const openRedeemModal = async (product: ProductItem, categoryId: ProductCategory['id']) => {
|
||||||
|
setSelectedProduct({
|
||||||
|
product,
|
||||||
|
categoryId,
|
||||||
|
})
|
||||||
|
addressBook.resetAddressForm()
|
||||||
|
|
||||||
|
if (categoryId === 'PHYSICAL') {
|
||||||
|
const addresses = await addressBook.loadAddresses()
|
||||||
|
const defaultAddress = addresses.find((item) => item.default_setting === 1) ?? addresses[0]
|
||||||
|
setSelectedAddressId(defaultAddress ? String(defaultAddress.id) : '')
|
||||||
|
setModalMode('select-address')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setModalMode('select-address')
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeRedeemModal = () => {
|
||||||
|
setSelectedProduct(null)
|
||||||
|
setModalMode('select-address')
|
||||||
|
addressBook.resetAddressForm()
|
||||||
|
redeemMutation.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
const openAddAddress = () => {
|
||||||
|
setModalMode('add-address')
|
||||||
|
}
|
||||||
|
|
||||||
|
const backToSelectAddress = () => {
|
||||||
|
setModalMode('select-address')
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAddAddressFormValid = addressBook.isAddressFormValid
|
||||||
|
|
||||||
|
const confirmRedeem = async () => {
|
||||||
|
if (modalMode === 'add-address') {
|
||||||
|
const addAddressValidation = validateAddAddressSubmission({
|
||||||
|
isAddAddressFormValid,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!addAddressValidation.valid) {
|
||||||
|
notifyError(addAddressValidation.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedAddress = await addressBook.saveAddress()
|
||||||
|
if (savedAddress) {
|
||||||
|
const defaultOption = savedAddress.addresses
|
||||||
|
.map((item) => ({
|
||||||
|
id: String(item.id),
|
||||||
|
isDefault: item.default_setting === 1,
|
||||||
|
}))
|
||||||
|
.find((item) => item.isDefault)
|
||||||
|
?? (savedAddress.addresses[0]
|
||||||
|
? {
|
||||||
|
id: String(savedAddress.addresses[0].id),
|
||||||
|
isDefault: savedAddress.addresses[0].default_setting === 1,
|
||||||
|
}
|
||||||
|
: null)
|
||||||
|
setSelectedAddressId(defaultOption?.id ?? '')
|
||||||
|
setModalMode('select-address')
|
||||||
|
notifySuccess(savedAddress.response, 'Address added successfully.')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const redeemValidation = validateRedeemSubmission({
|
||||||
|
sessionId: addressBook.sessionId,
|
||||||
|
selectedProduct,
|
||||||
|
selectedAddressId,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!redeemValidation.valid) {
|
||||||
|
notifyError(redeemValidation.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await redeemMutation.mutateAsync(selectedProduct!)
|
||||||
|
await Promise.all([
|
||||||
|
queryClient.invalidateQueries({queryKey: ['goods-catalog']}),
|
||||||
|
addressBook.sessionId
|
||||||
|
? queryClient.invalidateQueries({queryKey: queryKeys.assets(addressBook.sessionId)})
|
||||||
|
: Promise.resolve(),
|
||||||
|
])
|
||||||
|
notifySuccess(response, 'Redeem request submitted successfully.')
|
||||||
|
closeRedeemModal()
|
||||||
|
} catch {
|
||||||
|
// mutation error is surfaced via redeemMutation.error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedProduct,
|
||||||
|
modalMode,
|
||||||
|
addressOptions: addressBook.addressOptions,
|
||||||
|
selectedAddressId,
|
||||||
|
addressForm: addressBook.addressForm,
|
||||||
|
addressLoading: addressBook.loading,
|
||||||
|
isAddAddressFormValid,
|
||||||
|
submitLoading: modalMode === 'add-address' ? addressBook.submitLoading : redeemMutation.isPending,
|
||||||
|
openRedeemModal,
|
||||||
|
closeRedeemModal,
|
||||||
|
openAddAddress,
|
||||||
|
backToSelectAddress,
|
||||||
|
setSelectedAddressId,
|
||||||
|
changeAddressForm: addressBook.changeAddressForm,
|
||||||
|
confirmRedeem,
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/features/home/claimValidation.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type {ValidateTokenData} from '@/types/auth.type.ts'
|
||||||
|
|
||||||
|
type ClaimValidationResult =
|
||||||
|
| { valid: true }
|
||||||
|
| { valid: false; message: string }
|
||||||
|
|
||||||
|
export function validateClaimSubmission(authInfo: ValidateTokenData | null): ClaimValidationResult {
|
||||||
|
if (!authInfo?.session_id || !authInfo.user_id) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'Session expired. Please log in again.',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {valid: true}
|
||||||
|
}
|
||||||
32
src/features/notifications/GlobalToast.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import {CircleAlert, CircleCheckBig} from 'lucide-react'
|
||||||
|
|
||||||
|
import {useToastStore} from './store'
|
||||||
|
|
||||||
|
export function GlobalToast() {
|
||||||
|
const toast = useToastStore((state) => state.toast)
|
||||||
|
|
||||||
|
if (!toast?.visible) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const isError = toast.type === 'error'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pointer-events-none fixed right-[16px] top-[16px] z-[60] max-w-[320px]">
|
||||||
|
<div
|
||||||
|
className={`flex items-start gap-[10px] rounded-[14px] border px-[14px] py-[12px] text-white shadow-[0_18px_40px_rgba(0,0,0,0.35)] backdrop-blur-[8px] ${
|
||||||
|
isError
|
||||||
|
? 'border-[#FF9BA4]/35 bg-[#3A1318]/94'
|
||||||
|
: 'border-[#9BFFC0]/35 bg-[#11311E]/94'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isError ? (
|
||||||
|
<CircleAlert className="mt-[1px] h-[18px] w-[18px] shrink-0 text-[#FF9BA4]" aria-hidden="true"/>
|
||||||
|
) : (
|
||||||
|
<CircleCheckBig className="mt-[1px] h-[18px] w-[18px] shrink-0 text-[#9BFFC0]" aria-hidden="true"/>
|
||||||
|
)}
|
||||||
|
<div className="text-[13px] leading-[1.55] text-white/92">{toast.message}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
2
src/features/notifications/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './GlobalToast'
|
||||||
|
export * from './store'
|
||||||
79
src/features/notifications/store.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import {create} from 'zustand'
|
||||||
|
|
||||||
|
export type ToastType = 'success' | 'error'
|
||||||
|
|
||||||
|
type ToastState = {
|
||||||
|
message: string
|
||||||
|
type: ToastType
|
||||||
|
visible: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type ToastStore = {
|
||||||
|
toast: ToastState | null
|
||||||
|
showToast: (message: string, type?: ToastType) => void
|
||||||
|
clearToast: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
let toastTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
export function resolveToastMessage(source: unknown, fallback = '') {
|
||||||
|
if (typeof source === 'string' && source.trim()) {
|
||||||
|
return source.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof source === 'object' && source !== null && 'msg' in source) {
|
||||||
|
const message = (source as {msg?: unknown}).msg
|
||||||
|
if (typeof message === 'string' && message.trim()) {
|
||||||
|
return message.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useToastStore = create<ToastStore>((set) => ({
|
||||||
|
toast: null,
|
||||||
|
showToast: (message, type = 'success') => {
|
||||||
|
if (toastTimer) {
|
||||||
|
clearTimeout(toastTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
set({
|
||||||
|
toast: {
|
||||||
|
message,
|
||||||
|
type,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
toastTimer = setTimeout(() => {
|
||||||
|
set({toast: null})
|
||||||
|
toastTimer = null
|
||||||
|
}, 2000)
|
||||||
|
},
|
||||||
|
clearToast: () => {
|
||||||
|
if (toastTimer) {
|
||||||
|
clearTimeout(toastTimer)
|
||||||
|
toastTimer = null
|
||||||
|
}
|
||||||
|
set({toast: null})
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
export function notifySuccess(source: unknown, fallback = '') {
|
||||||
|
const message = resolveToastMessage(source, fallback)
|
||||||
|
if (!message) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
useToastStore.getState().showToast(message, 'success')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function notifyError(source: unknown, fallback = '') {
|
||||||
|
const message = resolveToastMessage(source, fallback)
|
||||||
|
if (!message) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
useToastStore.getState().showToast(message, 'error')
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 363 KiB |
@@ -1,5 +1,11 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
font-family: "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.button-play {
|
.button-play {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -16,7 +22,7 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
font-family: "JetBrains Mono",monospace;
|
font-family: "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
@@ -59,6 +65,64 @@
|
|||||||
transform: translateY(2px);
|
transform: translateY(2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-play-gray {
|
||||||
|
align-items: center;
|
||||||
|
appearance: none;
|
||||||
|
background-image: radial-gradient(100% 100% at 100% 0, #77716d 0, #66615d 45%, #55524f 100%);
|
||||||
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow:
|
||||||
|
rgba(85, 82, 79, .42) 0 0 18px,
|
||||||
|
rgba(52, 49, 47, .35) 0 2px 4px,
|
||||||
|
rgba(52, 49, 47, .28) 0 7px 13px -3px,
|
||||||
|
#474440 0 -3px 0 inset;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
font-family: "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1;
|
||||||
|
list-style: none;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
position: relative;
|
||||||
|
text-align: left;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: box-shadow .15s,transform .15s;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
touch-action: manipulation;
|
||||||
|
white-space: nowrap;
|
||||||
|
will-change: box-shadow,transform;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-play-gray:focus {
|
||||||
|
box-shadow:
|
||||||
|
#474440 0 0 0 1.5px inset,
|
||||||
|
rgba(113, 108, 104, .5) 0 0 22px,
|
||||||
|
rgba(52, 49, 47, .35) 0 2px 4px,
|
||||||
|
rgba(52, 49, 47, .28) 0 7px 13px -3px,
|
||||||
|
#474440 0 -3px 0 inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-play-gray:hover {
|
||||||
|
box-shadow:
|
||||||
|
rgba(113, 108, 104, .58) 0 0 24px,
|
||||||
|
rgba(52, 49, 47, .35) 0 4px 8px,
|
||||||
|
rgba(52, 49, 47, .28) 0 7px 13px -3px,
|
||||||
|
#474440 0 -3px 0 inset;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-play-gray:active {
|
||||||
|
box-shadow: #474440 0 3px 7px inset, rgba(113, 108, 104, .28) 0 0 12px;
|
||||||
|
transform: translateY(2px);
|
||||||
|
}
|
||||||
|
|
||||||
.liquid-glass-bg {
|
.liquid-glass-bg {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -71,25 +135,3 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
width: 100%;
|
|
||||||
height: 10px;
|
|
||||||
margin: 10px 0 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: linear-gradient(180deg, rgba(18, 14, 10, 0.92) 0%, rgba(34, 22, 14, 0.96) 100%);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.45);
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar__fill {
|
|
||||||
height: 100%;
|
|
||||||
border-radius: inherit;
|
|
||||||
background: linear-gradient(90deg, #fe9c00 0%, #fa6d02 100%);
|
|
||||||
box-shadow:
|
|
||||||
inset 0 0 0 1px rgba(255, 220, 160, 0.35),
|
|
||||||
0 0 8px rgba(254, 156, 0, 0.55),
|
|
||||||
0 0 16px rgba(250, 109, 2, 0.4),
|
|
||||||
0 0 24px rgba(250, 109, 2, 0.22);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { clsx, type ClassValue } from "clsx"
|
import { clsx, type ClassValue } from "clsx"
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export * from './request'
|
||||||
|
export * from './tool'
|
||||||
|
|||||||
15
src/lib/query.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import {QueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
export const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
retry: false,
|
||||||
|
staleTime: 0,
|
||||||
|
gcTime: 0,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
retry: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
10
src/lib/queryKeys.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type {goodsType} from '@/types/business.type.ts'
|
||||||
|
|
||||||
|
export const queryKeys = {
|
||||||
|
authBootstrap: ['auth-bootstrap'] as const,
|
||||||
|
goodsCatalog: (types?: readonly goodsType[]) => ['goods-catalog', ...(types ?? ['all'])] as const,
|
||||||
|
assets: (sessionId: string) => ['assets', sessionId] as const,
|
||||||
|
addressList: (sessionId: string) => ['address-list', sessionId] as const,
|
||||||
|
orders: (sessionId: string) => ['orders', sessionId] as const,
|
||||||
|
pointsLogs: (sessionId: string) => ['points-logs', sessionId] as const,
|
||||||
|
}
|
||||||
183
src/lib/request.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import ky, {HTTPError, type Input, type KyInstance, type Options} from 'ky'
|
||||||
|
|
||||||
|
import {notifyError, resolveToastMessage} from '@/features/notifications'
|
||||||
|
import {useUserStore} from '@/store/user.ts'
|
||||||
|
|
||||||
|
type RequestMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'
|
||||||
|
type ResponseType = 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData' | 'response'
|
||||||
|
type TokenFormatter = (token: string) => string
|
||||||
|
|
||||||
|
export type RequestResult<T, R extends ResponseType = 'json'> =
|
||||||
|
R extends 'text' ? string
|
||||||
|
: R extends 'blob' ? Blob
|
||||||
|
: R extends 'arrayBuffer' ? ArrayBuffer
|
||||||
|
: R extends 'formData' ? FormData
|
||||||
|
: R extends 'response' ? Response
|
||||||
|
: T
|
||||||
|
|
||||||
|
export type RequestConfig<R extends ResponseType = 'json'> = Omit<Options, 'hooks' | 'method'> & {
|
||||||
|
responseType?: R
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApiEnvelope = {
|
||||||
|
code: number
|
||||||
|
msg?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RequestError<T = unknown> extends Error {
|
||||||
|
status?: number
|
||||||
|
data?: T
|
||||||
|
|
||||||
|
constructor(message: string, options?: {status?: number; data?: T; cause?: unknown}) {
|
||||||
|
super(message, {cause: options?.cause})
|
||||||
|
this.name = 'RequestError'
|
||||||
|
this.status = options?.status
|
||||||
|
this.data = options?.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isApiEnvelope = (value: unknown): value is ApiEnvelope =>
|
||||||
|
typeof value === 'object' &&
|
||||||
|
value !== null &&
|
||||||
|
'code' in value &&
|
||||||
|
typeof (value as {code?: unknown}).code === 'number'
|
||||||
|
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? '/api/'
|
||||||
|
const REQUEST_TIMEOUT = 10_000
|
||||||
|
|
||||||
|
let accessTokenFormatter: TokenFormatter = (token) => `Bearer ${token}`
|
||||||
|
|
||||||
|
export const setAccessTokenFormatter = (formatter?: TokenFormatter) => {
|
||||||
|
accessTokenFormatter = formatter ?? ((token) => `Bearer ${token}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestClient = ky.create({
|
||||||
|
baseUrl: API_BASE_URL,
|
||||||
|
timeout: REQUEST_TIMEOUT,
|
||||||
|
retry: 0,
|
||||||
|
headers: {
|
||||||
|
lang: 'zh',
|
||||||
|
},
|
||||||
|
hooks: {
|
||||||
|
beforeRequest: [
|
||||||
|
({request}) => {
|
||||||
|
const token = useUserStore.getState().userInfo?.token
|
||||||
|
if (!token) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
request.headers.set('Authorization', accessTokenFormatter(token))
|
||||||
|
},
|
||||||
|
],
|
||||||
|
beforeError: [
|
||||||
|
async ({error}) => {
|
||||||
|
if (error instanceof HTTPError) {
|
||||||
|
const message = resolveToastMessage(
|
||||||
|
error.data,
|
||||||
|
typeof error.data === 'string'
|
||||||
|
? error.data
|
||||||
|
: error.response.statusText || 'Request failed',
|
||||||
|
)
|
||||||
|
|
||||||
|
notifyError(message)
|
||||||
|
|
||||||
|
return new RequestError(message, {
|
||||||
|
status: error.response.status,
|
||||||
|
data: error.data,
|
||||||
|
cause: error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = error.message || 'Network request failed'
|
||||||
|
notifyError(message)
|
||||||
|
return new RequestError(message, {
|
||||||
|
cause: error,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const isEmptyResponse = (response: Response) =>
|
||||||
|
response.status === 204 ||
|
||||||
|
response.status === 205 ||
|
||||||
|
response.headers.get('content-length') === '0'
|
||||||
|
|
||||||
|
async function parseResponse<T, R extends ResponseType>(
|
||||||
|
response: Response,
|
||||||
|
responseType: R,
|
||||||
|
): Promise<RequestResult<T, R>> {
|
||||||
|
if (responseType === 'response') {
|
||||||
|
return response as RequestResult<T, R>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEmptyResponse(response)) {
|
||||||
|
return undefined as RequestResult<T, R>
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (responseType) {
|
||||||
|
case 'text':
|
||||||
|
return (await response.text()) as RequestResult<T, R>
|
||||||
|
case 'blob':
|
||||||
|
return (await response.blob()) as RequestResult<T, R>
|
||||||
|
case 'arrayBuffer':
|
||||||
|
return (await response.arrayBuffer()) as RequestResult<T, R>
|
||||||
|
case 'formData':
|
||||||
|
return (await response.formData()) as RequestResult<T, R>
|
||||||
|
case 'json':
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
const payload = await response.json()
|
||||||
|
|
||||||
|
if (isApiEnvelope(payload) && payload.code !== 1 && payload.code !== 200) {
|
||||||
|
const message = payload.msg?.trim() || 'Request failed'
|
||||||
|
notifyError(message)
|
||||||
|
throw new RequestError(message, {
|
||||||
|
status: response.status,
|
||||||
|
data: payload,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload as RequestResult<T, R>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function request<T = unknown, R extends ResponseType = 'json'>(
|
||||||
|
method: RequestMethod,
|
||||||
|
input: Input,
|
||||||
|
config?: RequestConfig<R>,
|
||||||
|
): Promise<RequestResult<T, R>> {
|
||||||
|
const responseType = (config?.responseType ?? 'json') as R
|
||||||
|
const options = config ? {...config} : {}
|
||||||
|
delete options.responseType
|
||||||
|
const response = await requestClient(input, {
|
||||||
|
...options,
|
||||||
|
method,
|
||||||
|
})
|
||||||
|
|
||||||
|
return parseResponse<T, R>(response, responseType)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const http = {
|
||||||
|
get: <T = unknown, R extends ResponseType = 'json'>(input: Input, config?: RequestConfig<R>): Promise<RequestResult<T, R>> =>
|
||||||
|
request<T, R>('get', input, config),
|
||||||
|
post: <T = unknown, R extends ResponseType = 'json'>(input: Input, config?: RequestConfig<R>): Promise<RequestResult<T, R>> =>
|
||||||
|
request<T, R>('post', input, config),
|
||||||
|
put: <T = unknown, R extends ResponseType = 'json'>(input: Input, config?: RequestConfig<R>): Promise<RequestResult<T, R>> =>
|
||||||
|
request<T, R>('put', input, config),
|
||||||
|
patch: <T = unknown, R extends ResponseType = 'json'>(input: Input, config?: RequestConfig<R>): Promise<RequestResult<T, R>> =>
|
||||||
|
request<T, R>('patch', input, config),
|
||||||
|
delete: <T = unknown, R extends ResponseType = 'json'>(input: Input, config?: RequestConfig<R>): Promise<RequestResult<T, R>> =>
|
||||||
|
request<T, R>('delete', input, config),
|
||||||
|
request: <T = unknown, R extends ResponseType = 'json'>(
|
||||||
|
method: RequestMethod,
|
||||||
|
input: Input,
|
||||||
|
config?: RequestConfig<R>,
|
||||||
|
): Promise<RequestResult<T, R>> => request<T, R>(method, input, config),
|
||||||
|
create: (options?: Options): KyInstance => requestClient.extend(options ?? {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export {requestClient}
|
||||||
|
|
||||||
|
export default http
|
||||||
28
src/lib/tool.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
type FormDataValue = string | number | boolean | Blob
|
||||||
|
|
||||||
|
type FormDataRecordValue =
|
||||||
|
| FormDataValue
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
| FormDataValue[]
|
||||||
|
|
||||||
|
export function objectToFormData(data: Record<string, FormDataRecordValue>): FormData {
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
Object.entries(data).forEach(([key, value]) => {
|
||||||
|
if (value == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((item) => {
|
||||||
|
formData.append(key, item instanceof Blob ? item : String(item))
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
formData.append(key, value instanceof Blob ? value : String(value))
|
||||||
|
})
|
||||||
|
|
||||||
|
return formData
|
||||||
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
|
import { QueryClientProvider } from '@tanstack/react-query'
|
||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
|
import { queryClient } from '@/lib/query.ts'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
<App />
|
<App />
|
||||||
|
</QueryClientProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
162
src/mock/index.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import type {
|
||||||
|
AccountTableRow,
|
||||||
|
AddressOption,
|
||||||
|
AddAddressForm,
|
||||||
|
OrderRecord,
|
||||||
|
PointsRecord,
|
||||||
|
PointsRecordTone,
|
||||||
|
} from '@/types'
|
||||||
|
|
||||||
|
export const accountAddressRowsMock: AccountTableRow[] = [
|
||||||
|
{
|
||||||
|
name: 'Jia Jun',
|
||||||
|
phone: '+86 138 0000 1288',
|
||||||
|
address: 'No. 88 Century Avenue, Pudong New Area, Shanghai',
|
||||||
|
code: '200120',
|
||||||
|
action: 'Edit',
|
||||||
|
setting: 'Default',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Alicia Tan',
|
||||||
|
phone: '+65 9123 4567',
|
||||||
|
address: '18 Robinson Road, Singapore',
|
||||||
|
code: '048547',
|
||||||
|
action: 'Edit',
|
||||||
|
setting: 'Optional',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Marcus Lee',
|
||||||
|
phone: '+60 12 778 9911',
|
||||||
|
address: '27 Jalan Bukit Bintang, Kuala Lumpur',
|
||||||
|
code: '55100',
|
||||||
|
action: 'Edit',
|
||||||
|
setting: 'Optional',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const initialAddressOptionsMock: AddressOption[] = [
|
||||||
|
{
|
||||||
|
id: 'address-shanghai',
|
||||||
|
name: 'Jia Jun',
|
||||||
|
phone: '+86 138 0000 1288',
|
||||||
|
address: 'No. 88 Century Avenue, Pudong New Area, Shanghai',
|
||||||
|
isDefault: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'address-singapore',
|
||||||
|
name: 'Alicia Tan',
|
||||||
|
phone: '+65 9123 4567',
|
||||||
|
address: '18 Robinson Road, Singapore',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'address-kuala-lumpur',
|
||||||
|
name: 'Marcus Lee',
|
||||||
|
phone: '+60 12 778 9911',
|
||||||
|
address: '27 Jalan Bukit Bintang, Kuala Lumpur',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const emptyAddressFormMock: AddAddressForm = {
|
||||||
|
name: '',
|
||||||
|
phone: '',
|
||||||
|
region: [],
|
||||||
|
detailedAddress: '',
|
||||||
|
isDefault: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const orderRecordsMock: OrderRecord[] = [
|
||||||
|
{
|
||||||
|
id: 'order-1',
|
||||||
|
date: '2025-03-04',
|
||||||
|
time: '10:20',
|
||||||
|
category: 'Bonus',
|
||||||
|
title: 'Daily Rebate 50',
|
||||||
|
status: 'Issued',
|
||||||
|
points: '-500 points',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'order-2',
|
||||||
|
date: '2025-03-03',
|
||||||
|
time: '14:00',
|
||||||
|
category: 'Physical',
|
||||||
|
title: 'Weekly Bonus 200',
|
||||||
|
trackingNumber: 'SF1234567890',
|
||||||
|
status: 'Shipped',
|
||||||
|
points: '-1200 points',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'order-3',
|
||||||
|
date: '2025-03-02',
|
||||||
|
time: '09:15',
|
||||||
|
category: 'Withdrawal',
|
||||||
|
title: 'Wireless Earbuds',
|
||||||
|
status: 'Issued',
|
||||||
|
points: '-1000 points',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'order-4',
|
||||||
|
date: '2025-03-01',
|
||||||
|
time: '16:30',
|
||||||
|
category: 'Bonus',
|
||||||
|
title: 'Fitness Tracker',
|
||||||
|
status: 'Pending',
|
||||||
|
points: '-1800 points',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'order-5',
|
||||||
|
date: '2025-02-28',
|
||||||
|
time: '11:00',
|
||||||
|
category: 'Physical',
|
||||||
|
title: 'Withdraw 100',
|
||||||
|
status: 'Rejected',
|
||||||
|
points: '-2500 points',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const pointsRecordsMock: PointsRecord[] = [
|
||||||
|
{
|
||||||
|
id: 'points-1',
|
||||||
|
title: 'Bonus Redemption - Daily Rewards 50',
|
||||||
|
date: '2025-03-04',
|
||||||
|
time: '10:20',
|
||||||
|
amount: '-500',
|
||||||
|
tone: 'negative',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'points-2',
|
||||||
|
title: "Claim Yesterday's Protection Funds",
|
||||||
|
date: '2025-03-04',
|
||||||
|
time: '09:20',
|
||||||
|
amount: '+800',
|
||||||
|
tone: 'positive',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'points-3',
|
||||||
|
title: 'Physical Item Redemption - Bluetooth Headphones',
|
||||||
|
date: '2025-03-03',
|
||||||
|
time: '14:00',
|
||||||
|
amount: '-1200',
|
||||||
|
tone: 'negative',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'points-4',
|
||||||
|
title: "Claim Yesterday's Protection Funds",
|
||||||
|
date: '2025-03-03',
|
||||||
|
time: '09:00',
|
||||||
|
amount: '+700',
|
||||||
|
tone: 'positive',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'points-5',
|
||||||
|
title: 'Withdraw to Platform - 100',
|
||||||
|
date: '2025-03-02',
|
||||||
|
time: '09:15',
|
||||||
|
amount: '-1000',
|
||||||
|
tone: 'negative',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const pointsRecordToneClassNameMock: Record<PointsRecordTone, string> = {
|
||||||
|
positive: 'bg-[#9BFFC0] text-[#176640]',
|
||||||
|
negative: 'bg-[#FF9BA4] text-[#7B2634]',
|
||||||
|
}
|
||||||
38
src/store/user.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import {create} from 'zustand'
|
||||||
|
import {createJSONStorage, persist} from 'zustand/middleware'
|
||||||
|
|
||||||
|
import type {LoginUserInfo, ValidateTokenData} from '@/types/auth.type.ts'
|
||||||
|
import type {UserAssetsData} from '@/types/user.type.ts'
|
||||||
|
|
||||||
|
type UserState = {
|
||||||
|
userInfo: LoginUserInfo | null
|
||||||
|
setUserInfo: (userInfo: LoginUserInfo) => void
|
||||||
|
authInfo: ValidateTokenData | null
|
||||||
|
setAuthInfo: (authInfo: ValidateTokenData) => void
|
||||||
|
assetsInfo: UserAssetsData | null
|
||||||
|
setAssetsInfo: (assetsInfo: UserAssetsData) => void
|
||||||
|
clearUserInfo: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUserStore = create<UserState>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
userInfo: null,
|
||||||
|
setUserInfo: (userInfo) => set({userInfo}),
|
||||||
|
authInfo: null,
|
||||||
|
setAuthInfo: (authInfo) => set({authInfo}),
|
||||||
|
assetsInfo: null,
|
||||||
|
setAssetsInfo: (assetsInfo) => set({assetsInfo}),
|
||||||
|
clearUserInfo: () => set({userInfo: null, authInfo: null, assetsInfo: null}),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'playx-user',
|
||||||
|
storage: createJSONStorage(() => localStorage),
|
||||||
|
partialize: (state) => ({
|
||||||
|
userInfo: state.userInfo,
|
||||||
|
authInfo: state.authInfo,
|
||||||
|
assetsInfo: state.assetsInfo,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
37
src/types/address.type.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
export interface AddressInfo {
|
||||||
|
/**@description 收件人姓名 */
|
||||||
|
receiver_name: string,
|
||||||
|
/**@description 电话 */
|
||||||
|
phone: string,
|
||||||
|
/**@description 地区(数组或逗号分隔字符串) */
|
||||||
|
region: string,
|
||||||
|
/**@description 详细地址 */
|
||||||
|
detail_address: string,
|
||||||
|
/**@description 1 设为默认地址 */
|
||||||
|
default_setting: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AddressListItem = {
|
||||||
|
id: number
|
||||||
|
phone: string
|
||||||
|
region: string[]
|
||||||
|
detail_address: string
|
||||||
|
default_setting: 0 | 1
|
||||||
|
create_time: number
|
||||||
|
update_time: number
|
||||||
|
playx_user_asset_id: number
|
||||||
|
receiver_name: string
|
||||||
|
region_text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AddressListData = {
|
||||||
|
list: AddressListItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AddressListResponse = ApiResponse<AddressListData>
|
||||||
|
|
||||||
|
export type AddressAddData = {
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AddressAddResponse = ApiResponse<AddressAddData>
|
||||||
31
src/types/auth.type.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
|
||||||
|
export type LoginParams = {
|
||||||
|
username?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LoginUserInfo = {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
nickname: string
|
||||||
|
playx_user_id: string
|
||||||
|
token: string
|
||||||
|
refresh_token: string
|
||||||
|
expires_in: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LoginData = {
|
||||||
|
userInfo: LoginUserInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LoginResponse = ApiResponse<LoginData>
|
||||||
|
|
||||||
|
|
||||||
|
export interface ValidateTokenData {
|
||||||
|
session_id:string;
|
||||||
|
user_id:string;
|
||||||
|
name:string;
|
||||||
|
token_expire_at:string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type ValidateTokenResponse = ApiResponse<ValidateTokenData>
|
||||||
108
src/types/business.type.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
export type goodsType = 'BONUS' | 'PHYSICAL' | 'WITHDRAW'
|
||||||
|
|
||||||
|
export type GoodsItem = {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
remark: string
|
||||||
|
score: number
|
||||||
|
amount: number
|
||||||
|
multiplier: number
|
||||||
|
category: string
|
||||||
|
category_title: string
|
||||||
|
type: string
|
||||||
|
image: string
|
||||||
|
stock: number | null
|
||||||
|
admin_id: number
|
||||||
|
sort: number
|
||||||
|
create_time: number
|
||||||
|
update_time: number
|
||||||
|
status: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GoodsListData = {
|
||||||
|
list: GoodsItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GoodListResponse = ApiResponse<GoodsListData>
|
||||||
|
|
||||||
|
export type OrderItem = {
|
||||||
|
id: number | string
|
||||||
|
user_id?: string
|
||||||
|
order_no?: string
|
||||||
|
external_transaction_id?: string
|
||||||
|
playx_transaction_id?: string
|
||||||
|
type?: goodsType | string
|
||||||
|
type_title?: string
|
||||||
|
category?: string
|
||||||
|
category_title?: string
|
||||||
|
title?: string
|
||||||
|
item_title?: string
|
||||||
|
mall_item_id?: number | string | null
|
||||||
|
mall_address_id?: number | string | null
|
||||||
|
points_cost?: number | string
|
||||||
|
amount?: number | string
|
||||||
|
multiplier?: number | string
|
||||||
|
tracking_no?: string | null
|
||||||
|
logistics_no?: string | null
|
||||||
|
shipping_company?: string | null
|
||||||
|
shipping_no?: string | null
|
||||||
|
receiver_name?: string | null
|
||||||
|
receiver_phone?: string | null
|
||||||
|
receiver_address?: string | null
|
||||||
|
fail_reason?: string | null
|
||||||
|
reject_reason?: string | null
|
||||||
|
grant_status?: string | null
|
||||||
|
retry_count?: number
|
||||||
|
status?: string | number
|
||||||
|
score?: number | string
|
||||||
|
points?: number | string
|
||||||
|
create_time?: number | string
|
||||||
|
created_at?: string
|
||||||
|
update_time?: number | string
|
||||||
|
start_time?: number | string | null
|
||||||
|
end_time?: number | string | null
|
||||||
|
mallItem?: Partial<GoodsItem> | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OrdersData = {
|
||||||
|
list: OrderItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OrdersResponse = ApiResponse<OrdersData>
|
||||||
|
|
||||||
|
export type PointsLogItem = {
|
||||||
|
id: number | string
|
||||||
|
biz_type?: string
|
||||||
|
direction?: 'IN' | 'OUT' | string
|
||||||
|
ts?: number | string
|
||||||
|
ref_id?: string
|
||||||
|
order_no?: string
|
||||||
|
order_status?: string
|
||||||
|
item_id?: number | string | null
|
||||||
|
item_type?: string
|
||||||
|
item_score?: number | string
|
||||||
|
cursor?: string
|
||||||
|
type?: string
|
||||||
|
title?: string
|
||||||
|
item_title?: string
|
||||||
|
description?: string
|
||||||
|
remark?: string
|
||||||
|
amount?: number | string
|
||||||
|
points?: number | string
|
||||||
|
score?: number | string
|
||||||
|
points_cost?: number | string
|
||||||
|
points_change?: number | string
|
||||||
|
change_points?: number | string
|
||||||
|
create_time?: number | string
|
||||||
|
created_at?: string
|
||||||
|
update_time?: number | string
|
||||||
|
mallItem?: Partial<GoodsItem> | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PointsLogsData = {
|
||||||
|
list: PointsLogItem[]
|
||||||
|
next_cursor?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PointsLogsResponse = ApiResponse<PointsLogsData>
|
||||||
6
src/types/global.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
type ApiResponse<T> = {
|
||||||
|
code: number
|
||||||
|
msg: string
|
||||||
|
time: number
|
||||||
|
data: T
|
||||||
|
}
|
||||||
@@ -1 +1,139 @@
|
|||||||
|
import type { ButtonHTMLAttributes, PropsWithChildren, ReactNode } from 'react'
|
||||||
|
import type { LucideIcon } from 'lucide-react'
|
||||||
|
import type {goodsType} from './business.type.ts'
|
||||||
|
|
||||||
export type RecordButtonType = 'order' | 'record'
|
export type RecordButtonType = 'order' | 'record'
|
||||||
|
|
||||||
|
export type HostContextMessage = {
|
||||||
|
type: 'IFRAME_CONTEXT'
|
||||||
|
payload?: {
|
||||||
|
token?: string
|
||||||
|
language?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ButtonVariant = 'orange' | 'gray'
|
||||||
|
|
||||||
|
export type ButtonProps = PropsWithChildren<ButtonHTMLAttributes<HTMLButtonElement>> & {
|
||||||
|
variant?: ButtonVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ModalProps = {
|
||||||
|
open: boolean
|
||||||
|
title?: ReactNode
|
||||||
|
children: ReactNode
|
||||||
|
footer?: ReactNode
|
||||||
|
onClose?: () => void
|
||||||
|
closeOnOverlayClick?: boolean
|
||||||
|
className?: string
|
||||||
|
bodyClassName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PageLayoutProps = {
|
||||||
|
children: ReactNode
|
||||||
|
contentClassName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TableColumn<T extends Record<string, string>> = {
|
||||||
|
label: string
|
||||||
|
key: keyof T
|
||||||
|
render?: (value: T[keyof T], record: T, index: number) => ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BorderlessTableProps<T extends Record<string, string>> = {
|
||||||
|
columns: TableColumn<T>[]
|
||||||
|
dataSource: T[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QuickNavCardProps = {
|
||||||
|
icon: LucideIcon
|
||||||
|
label: string
|
||||||
|
to: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProductItem = {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
subtitle: string
|
||||||
|
score: string
|
||||||
|
ctaLabel: string
|
||||||
|
imageClassName?: string
|
||||||
|
imageUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProductCategory = {
|
||||||
|
id: goodsType
|
||||||
|
name: string
|
||||||
|
items: ProductItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SelectedProductState = {
|
||||||
|
categoryId: ProductCategory['id']
|
||||||
|
product: ProductItem
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AddressOption = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
phone: string
|
||||||
|
address: string
|
||||||
|
isDefault?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ModalMode = 'select-address' | 'add-address'
|
||||||
|
|
||||||
|
export type AddAddressForm = {
|
||||||
|
name: string
|
||||||
|
phone: string
|
||||||
|
region: string[]
|
||||||
|
detailedAddress: string
|
||||||
|
isDefault: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AccountTableRow = {
|
||||||
|
name: string
|
||||||
|
phone: string
|
||||||
|
address: string
|
||||||
|
code: string
|
||||||
|
action: string
|
||||||
|
setting: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PointsRecordTone = 'positive' | 'negative'
|
||||||
|
|
||||||
|
export type OrderRecord = {
|
||||||
|
id: string
|
||||||
|
orderNumber?: string
|
||||||
|
date: string
|
||||||
|
time: string
|
||||||
|
category: string
|
||||||
|
title: string
|
||||||
|
trackingNumber?: string
|
||||||
|
status: string
|
||||||
|
points: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PointsRecord = {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
date: string
|
||||||
|
time: string
|
||||||
|
amount: string
|
||||||
|
tone: PointsRecordTone
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TabButtonProps = {
|
||||||
|
active: boolean
|
||||||
|
label: string
|
||||||
|
icon: LucideIcon
|
||||||
|
onClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OrderCardProps = {
|
||||||
|
record: OrderRecord
|
||||||
|
onOpenDetails: (record: OrderRecord) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PointsCardProps = {
|
||||||
|
record: PointsRecord
|
||||||
|
}
|
||||||
|
|||||||
18
src/types/user.type.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export type UserAssetsParams = {
|
||||||
|
session_id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserAssetsData {
|
||||||
|
/**@description 可用积分 */
|
||||||
|
available_points: number;
|
||||||
|
/**@description 待领取积分 */
|
||||||
|
locked_points: number;
|
||||||
|
/**@description 今日已领取 */
|
||||||
|
today_claimed: number;
|
||||||
|
/**@description 今日可领取上限 */
|
||||||
|
today_limit: number;
|
||||||
|
/**@description 可提现现金(积分按配置比例换算) */
|
||||||
|
withdrawable_cash: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserAssetsResponse = ApiResponse<UserAssetsData>
|
||||||
@@ -1,98 +1,289 @@
|
|||||||
import PageLayout from '@/components/layout'
|
import {useState} from 'react'
|
||||||
import BorderlessTable, { type TableColumn } from '@/components/table'
|
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
|
|
||||||
type AccountTableRow = {
|
import PageLayout from '@/components/layout'
|
||||||
|
import BorderlessTable from '@/components/table'
|
||||||
|
import Modal from '@/components/modal'
|
||||||
|
import Button from '@/components/button'
|
||||||
|
import {Link} from 'react-router-dom'
|
||||||
|
import {ArrowLeft, BadgeCheck, MapPinHouse, PencilLine, Plus, Trash2} from 'lucide-react'
|
||||||
|
import {useAddressBook} from '@/features/addressBook'
|
||||||
|
import {GoodsRedeemModal} from '@/features/goods'
|
||||||
|
import {notifySuccess} from '@/features/notifications'
|
||||||
|
import type {AddressListItem} from '@/types/address.type.ts'
|
||||||
|
import type {TableColumn} from '@/types'
|
||||||
|
|
||||||
|
type AddressTableRow = {
|
||||||
|
id: string
|
||||||
name: string
|
name: string
|
||||||
phone: string
|
phone: string
|
||||||
address: string
|
address: string
|
||||||
code: string
|
|
||||||
action: string
|
action: string
|
||||||
setting: string
|
setting: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function AccountPage() {
|
function AccountPage() {
|
||||||
const columns: TableColumn<AccountTableRow>[] = [
|
const addressBook = useAddressBook({autoLoad: true})
|
||||||
|
const [addressModalOpen, setAddressModalOpen] = useState(false)
|
||||||
|
const [editingAddress, setEditingAddress] = useState<AddressListItem | null>(null)
|
||||||
|
const [deleteTarget, setDeleteTarget] = useState<AddressListItem | null>(null)
|
||||||
|
|
||||||
|
const rows: AddressTableRow[] = addressBook.addresses.map((item) => ({
|
||||||
|
id: String(item.id),
|
||||||
|
name: item.receiver_name,
|
||||||
|
phone: item.phone,
|
||||||
|
address: addressBook.addressOptions.find((option) => option.id === String(item.id))?.address ?? '',
|
||||||
|
action: 'Edit',
|
||||||
|
setting: item.default_setting === 1 ? 'Default' : 'Optional',
|
||||||
|
}))
|
||||||
|
|
||||||
|
const isAddressFormValid = addressBook.isAddressFormValid
|
||||||
|
|
||||||
|
const handleOpenAddAddress = () => {
|
||||||
|
setEditingAddress(null)
|
||||||
|
addressBook.resetAddressForm()
|
||||||
|
setAddressModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpenEditAddress = (address: AddressListItem) => {
|
||||||
|
setEditingAddress(address)
|
||||||
|
addressBook.fillAddressForm(address)
|
||||||
|
setAddressModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCloseAddressModal = () => {
|
||||||
|
setAddressModalOpen(false)
|
||||||
|
setEditingAddress(null)
|
||||||
|
addressBook.resetAddressForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmitAddress = async () => {
|
||||||
|
const saved = await addressBook.saveAddress(editingAddress)
|
||||||
|
if (saved) {
|
||||||
|
handleCloseAddressModal()
|
||||||
|
notifySuccess(saved.response, editingAddress ? 'Address updated successfully.' : 'Address added successfully.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirmDelete = async () => {
|
||||||
|
if (!deleteTarget) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleted = await addressBook.removeAddress(String(deleteTarget.id))
|
||||||
|
if (deleted) {
|
||||||
|
setDeleteTarget(null)
|
||||||
|
notifySuccess(deleted, 'Address deleted successfully.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: TableColumn<AddressTableRow>[] = [
|
||||||
{
|
{
|
||||||
label: 'Name',
|
label: 'Name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
render: (value: string) => <div>{value}</div>
|
render: (value: string) => <div className="font-medium text-white">{value}</div>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Phone / Mobile',
|
label: 'Phone / Mobile',
|
||||||
key: 'phone',
|
key: 'phone',
|
||||||
render: (value: string) => <div>{value}</div>
|
render: (value: string) => <div className="text-white/72">{value}</div>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Address',
|
label: 'Address',
|
||||||
key: 'address',
|
key: 'address',
|
||||||
render: (value: string) => <div>{value}</div>
|
render: (value: string) => <div className="max-w-[280px] text-white/72">{value}</div>,
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Postal Code',
|
|
||||||
key: 'code',
|
|
||||||
render: (value: string) => <div>{value}</div>
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Action',
|
label: 'Action',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
render: (value: string) => <div>{value}</div>
|
render: (_value: string, _record: AddressTableRow, index: number) => (
|
||||||
|
<div className="flex items-center gap-[8px]">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center gap-[6px] rounded-full bg-white/6 px-[10px] py-[6px] text-[12px] text-white/82 transition-colors hover:bg-white/10 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]"
|
||||||
|
onClick={() => handleOpenEditAddress(addressBook.addresses[index])}
|
||||||
|
>
|
||||||
|
<PencilLine className="h-[12px] w-[12px]" aria-hidden="true" />
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center gap-[6px] rounded-full bg-[#4B1818]/70 px-[10px] py-[6px] text-[12px] text-[#FFB1B1] transition-colors hover:bg-[#612121]/80 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]"
|
||||||
|
onClick={() => setDeleteTarget(addressBook.addresses[index])}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-[12px] w-[12px]" aria-hidden="true" />
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Default Setting',
|
label: 'Default Setting',
|
||||||
key: 'setting',
|
key: 'setting',
|
||||||
render: (value: string) => <div>{value}</div>
|
render: (value: string) => (
|
||||||
}
|
<div
|
||||||
]
|
className={`inline-flex rounded-full px-[10px] py-[5px] text-[12px] ${
|
||||||
|
value === 'Default'
|
||||||
const dataSource: AccountTableRow[] = [
|
? 'bg-[#FA6A00]/14 text-[#FFB36D]'
|
||||||
{
|
: 'bg-white/6 text-white/62'
|
||||||
name: 'Jia Jun',
|
}`}
|
||||||
phone: '+86 138 0000 1288',
|
>
|
||||||
address: 'No. 88 Century Avenue, Pudong New Area, Shanghai',
|
{value}
|
||||||
code: '200120',
|
</div>
|
||||||
action: 'Edit',
|
),
|
||||||
setting: 'Default',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Alicia Tan',
|
|
||||||
phone: '+65 9123 4567',
|
|
||||||
address: '18 Robinson Road, Singapore',
|
|
||||||
code: '048547',
|
|
||||||
action: 'Edit',
|
|
||||||
setting: 'Optional',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Marcus Lee',
|
|
||||||
phone: '+60 12 778 9911',
|
|
||||||
address: '27 Jalan Bukit Bintang, Kuala Lumpur',
|
|
||||||
code: '55100',
|
|
||||||
action: 'Edit',
|
|
||||||
setting: 'Optional',
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout contentClassName="min-h-screen">
|
<PageLayout contentClassName="mx-auto flex min-h-screen w-full max-w-[1120px] flex-col px-4 pb-8 sm:px-6 lg:px-8">
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
className={'relative text-[#F56E10] flex h-[40px] w-full items-center justify-center bg-[#08070E]/70'}
|
className="mt-[12px] flex h-[44px] items-center justify-between rounded-[12px] bg-[#08070E]/72 px-[14px] text-[#F56E10] transition-colors hover:bg-[#0D0A14]/80 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E] sm:mt-[16px]"
|
||||||
>
|
>
|
||||||
<div className={'absolute left-[16px]'}> < </div>
|
<div className="flex items-center gap-[8px]">
|
||||||
<div>Account</div>
|
<ArrowLeft className="h-[16px] w-[16px]" aria-hidden="true" />
|
||||||
|
<span className="text-[14px] font-medium text-white/92">Back</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-[15px] font-semibold text-[#F56E10]">Account</div>
|
||||||
|
<div className="w-[52px]"></div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className={'mt-[20px] w-full flex items-center justify-center'}>
|
<div className="mx-auto mt-[20px] w-full max-w-[1000px]">
|
||||||
|
<div className="mb-[14px] flex flex-col gap-[12px] sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className={'w-[80%]'}>
|
<div className="flex items-center gap-[10px]">
|
||||||
<div className={'w-full mb-[10px] flex items-center justify-between'}>
|
<div className="flex h-[38px] w-[38px] items-center justify-center rounded-[12px] bg-[#FA6A00]/15 text-[#FE9F00]">
|
||||||
<div>My Shipping Address</div>
|
<MapPinHouse className="h-[18px] w-[18px]" aria-hidden="true" />
|
||||||
<div className={'liquid-glass-bg px-[10px] py-[5px] text-sm'}>Add Address</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[16px] font-semibold text-white">My Shipping Address</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="liquid-glass-bg inline-flex h-[40px] items-center justify-center gap-[8px] px-[14px] text-sm text-white transition-colors hover:bg-white/28 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]"
|
||||||
|
onClick={handleOpenAddAddress}
|
||||||
|
>
|
||||||
|
<Plus className="h-[14px] w-[14px]" aria-hidden="true" />
|
||||||
|
Add Address
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BorderlessTable columns={columns} dataSource={dataSource} />
|
{addressBook.loading ? (
|
||||||
|
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
||||||
|
Loading address list...
|
||||||
|
</div>
|
||||||
|
) : !addressBook.addresses.length ? (
|
||||||
|
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
||||||
|
No shipping address found. Add one to start redeeming physical rewards.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="space-y-[12px] lg:hidden">
|
||||||
|
{rows.map((item, index) => (
|
||||||
|
<div key={item.id} className="liquid-glass-bg p-[14px]">
|
||||||
|
<div className="flex items-start justify-between gap-[12px]">
|
||||||
|
<div>
|
||||||
|
<div className="text-[16px] font-semibold text-white">{item.name}</div>
|
||||||
|
<div className="mt-[4px] text-[13px] text-white/62">{item.phone}</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`inline-flex rounded-full px-[10px] py-[5px] text-[12px] ${
|
||||||
|
item.setting === 'Default'
|
||||||
|
? 'bg-[#FA6A00]/14 text-[#FFB36D]'
|
||||||
|
: 'bg-white/6 text-white/62'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.setting === 'Default' ? (
|
||||||
|
<span className="inline-flex items-center gap-[5px]">
|
||||||
|
<BadgeCheck className="h-[12px] w-[12px]" aria-hidden="true" />
|
||||||
|
{item.setting}
|
||||||
|
</span>
|
||||||
|
) : item.setting}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-[12px] rounded-[10px] bg-black/12 p-[12px]">
|
||||||
|
<div className="text-[12px] uppercase tracking-[0.08em] text-white/44">Address</div>
|
||||||
|
<div className="mt-[6px] text-[13px] leading-[1.6] text-white/78">{item.address}</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mt-[12px] inline-flex items-center gap-[6px] rounded-full bg-white/6 px-[10px] py-[6px] text-[12px] text-white/82 transition-colors hover:bg-white/10 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]"
|
||||||
|
onClick={() => handleOpenEditAddress(addressBook.addresses[index])}
|
||||||
|
>
|
||||||
|
<PencilLine className="h-[12px] w-[12px]" aria-hidden="true" />
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mt-[10px] inline-flex items-center gap-[6px] rounded-full bg-[#4B1818]/70 px-[10px] py-[6px] text-[12px] text-[#FFB1B1] transition-colors hover:bg-[#612121]/80 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]"
|
||||||
|
onClick={() => setDeleteTarget(addressBook.addresses[index])}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-[12px] w-[12px]" aria-hidden="true" />
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden lg:block">
|
||||||
|
<BorderlessTable columns={columns} dataSource={rows} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<GoodsRedeemModal
|
||||||
|
selectedProduct={null}
|
||||||
|
modalMode="add-address"
|
||||||
|
addressOptions={[]}
|
||||||
|
selectedAddressId=""
|
||||||
|
addressForm={addressBook.addressForm}
|
||||||
|
addressLoading={false}
|
||||||
|
isAddAddressFormValid={isAddressFormValid}
|
||||||
|
submitLoading={addressBook.submitLoading}
|
||||||
|
onClose={handleCloseAddressModal}
|
||||||
|
onConfirm={handleSubmitAddress}
|
||||||
|
onOpenAddAddress={handleOpenAddAddress}
|
||||||
|
onBackToSelectAddress={handleCloseAddressModal}
|
||||||
|
onSelectAddress={() => {}}
|
||||||
|
onChangeAddressForm={addressBook.changeAddressForm}
|
||||||
|
forceOpen={addressModalOpen}
|
||||||
|
formOnly
|
||||||
|
titleOverride={editingAddress ? 'Edit Shipping Address' : 'Add Shipping Address'}
|
||||||
|
confirmText={editingAddress ? 'Save Changes' : 'Add Address'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={Boolean(deleteTarget)}
|
||||||
|
title="Delete Address"
|
||||||
|
onClose={() => setDeleteTarget(null)}
|
||||||
|
className="max-w-[420px]"
|
||||||
|
bodyClassName="space-y-[18px]"
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="gray"
|
||||||
|
className="h-[38px] w-full sm:w-auto sm:min-w-[120px]"
|
||||||
|
onClick={() => setDeleteTarget(null)}
|
||||||
|
disabled={addressBook.deleteLoading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="h-[38px] w-full sm:w-auto sm:min-w-[120px]"
|
||||||
|
onClick={handleConfirmDelete}
|
||||||
|
disabled={addressBook.deleteLoading}
|
||||||
|
>
|
||||||
|
{addressBook.deleteLoading ? 'Deleting...' : 'Delete'}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="rounded-[12px] bg-[#1C1818]/82 px-[14px] py-[18px] text-[15px] leading-[1.65] text-white/92 shadow-[0_10px_30px_rgba(0,0,0,0.2)]">
|
||||||
|
{deleteTarget ? `Delete the address for ${deleteTarget.receiver_name}? ` : ''}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
68
src/views/goods/index.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import {ArrowLeft} from 'lucide-react'
|
||||||
|
import {Link, useSearchParams} from 'react-router-dom'
|
||||||
|
|
||||||
|
import PageLayout from '@/components/layout'
|
||||||
|
import {HOME_GOOD_TYPE_ORDER} from '@/constant'
|
||||||
|
import {
|
||||||
|
GoodsCategoryList,
|
||||||
|
GoodsRedeemModal,
|
||||||
|
isGoodsType,
|
||||||
|
useGoodsCatalog,
|
||||||
|
useGoodsRedeem,
|
||||||
|
} from '@/features/goods'
|
||||||
|
|
||||||
|
function GoodsPage() {
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
const queryType = searchParams.get('type')
|
||||||
|
const selectedType = isGoodsType(queryType) ? queryType : HOME_GOOD_TYPE_ORDER[0]
|
||||||
|
const {productCategories, loading} = useGoodsCatalog({types: [selectedType]})
|
||||||
|
const redeem = useGoodsRedeem()
|
||||||
|
const visibleCategories = productCategories.filter((category) => category.id === selectedType)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageLayout contentClassName="mx-auto flex min-h-screen w-full max-w-[1180px] flex-col px-4 pb-8 sm:px-6 lg:px-8">
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="mt-[12px] flex h-[44px] items-center justify-between rounded-[12px] bg-[#08070E]/72 px-[14px] text-[#F56E10] transition-colors hover:bg-[#0D0A14]/80 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E] sm:mt-[16px]"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-[8px]">
|
||||||
|
<ArrowLeft className="h-[16px] w-[16px]" aria-hidden="true"/>
|
||||||
|
<span className="text-[14px] font-medium text-white/92">Back</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-[15px] font-semibold text-[#F56E10]">{queryType}</div>
|
||||||
|
<div className="w-[52px]"></div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="mx-auto w-full max-w-[1120px] pt-[18px] pb-[24px]">
|
||||||
|
<div className="h-px bg-white/16"></div>
|
||||||
|
<div className="mt-[14px]">
|
||||||
|
<GoodsCategoryList
|
||||||
|
categories={visibleCategories}
|
||||||
|
loading={loading}
|
||||||
|
emptyText="No goods found for this category."
|
||||||
|
onRedeem={redeem.openRedeemModal}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<GoodsRedeemModal
|
||||||
|
selectedProduct={redeem.selectedProduct}
|
||||||
|
modalMode={redeem.modalMode}
|
||||||
|
addressOptions={redeem.addressOptions}
|
||||||
|
selectedAddressId={redeem.selectedAddressId}
|
||||||
|
addressForm={redeem.addressForm}
|
||||||
|
addressLoading={redeem.addressLoading}
|
||||||
|
isAddAddressFormValid={redeem.isAddAddressFormValid}
|
||||||
|
submitLoading={redeem.submitLoading}
|
||||||
|
onClose={redeem.closeRedeemModal}
|
||||||
|
onConfirm={redeem.confirmRedeem}
|
||||||
|
onOpenAddAddress={redeem.openAddAddress}
|
||||||
|
onBackToSelectAddress={redeem.backToSelectAddress}
|
||||||
|
onSelectAddress={redeem.setSelectedAddressId}
|
||||||
|
onChangeAddressForm={redeem.changeAddressForm}
|
||||||
|
/>
|
||||||
|
</PageLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GoodsPage
|
||||||
@@ -1,258 +1,87 @@
|
|||||||
import { useState } from 'react'
|
import {useState} from 'react'
|
||||||
|
|
||||||
|
import {useMutation} from '@tanstack/react-query'
|
||||||
|
|
||||||
import recordSvg from '@/assets/record.svg'
|
|
||||||
import accountSvg from '@/assets/account.svg'
|
|
||||||
import PageLayout from '@/components/layout'
|
import PageLayout from '@/components/layout'
|
||||||
import Modal from '@/components/modal'
|
import Modal from '@/components/modal'
|
||||||
import { Link } from 'react-router-dom'
|
import Button from '@/components/button'
|
||||||
|
import {Link, useNavigate} from 'react-router-dom'
|
||||||
|
import {
|
||||||
|
ChevronRight,
|
||||||
|
Coins,
|
||||||
|
Gauge,
|
||||||
|
History,
|
||||||
|
UserRound,
|
||||||
|
Wallet,
|
||||||
|
} from 'lucide-react'
|
||||||
|
import type {
|
||||||
|
ProductCategory,
|
||||||
|
QuickNavCardProps,
|
||||||
|
} from '@/types'
|
||||||
|
import {
|
||||||
|
GoodsCategoryList,
|
||||||
|
GoodsRedeemModal,
|
||||||
|
useAssetsQuery,
|
||||||
|
useAssetsRefresh,
|
||||||
|
useGoodsCatalog,
|
||||||
|
useGoodsRedeem,
|
||||||
|
} from '@/features/goods'
|
||||||
|
import {validateClaimSubmission} from '@/features/home/claimValidation'
|
||||||
|
import {claim} from '@/api/business.ts'
|
||||||
|
import {notifyError, notifySuccess} from '@/features/notifications'
|
||||||
|
import {useUserStore} from "@/store/user.ts";
|
||||||
|
|
||||||
type QuickNavCardProps = {
|
function QuickNavCard({icon: Icon, label, to}: QuickNavCardProps) {
|
||||||
icon: string
|
|
||||||
label: string
|
|
||||||
to: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProductItem = {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
subtitle: string
|
|
||||||
points: string
|
|
||||||
ctaLabel: string
|
|
||||||
imageClassName: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProductCategory = {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
items: ProductItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type SelectedProductState = {
|
|
||||||
categoryId: ProductCategory['id']
|
|
||||||
product: ProductItem
|
|
||||||
}
|
|
||||||
|
|
||||||
type AddressOption = {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
phone: string
|
|
||||||
address: string
|
|
||||||
postalCode: string
|
|
||||||
isDefault?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type ModalMode = 'select-address' | 'add-address'
|
|
||||||
|
|
||||||
type AddAddressForm = {
|
|
||||||
name: string
|
|
||||||
phone: string
|
|
||||||
region: string
|
|
||||||
detailedAddress: string
|
|
||||||
postalCode: string
|
|
||||||
isDefault: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
function QuickNavCard({ icon, label, to }: QuickNavCardProps) {
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={to}
|
to={to}
|
||||||
className={'liquid-glass-bg rounded-[10px] flex items-center pr-[10px] cursor-pointer'}
|
className="liquid-glass-bg flex items-center justify-between gap-[12px] rounded-[12px] px-[12px] py-[8px] text-[13px] text-white/88 transition-colors hover:bg-white/28 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]"
|
||||||
>
|
>
|
||||||
<div className={'flex px-[10px] py-[5px] items-center gap-[5px]'}>
|
<div className="flex min-w-0 items-center gap-[10px]">
|
||||||
<div className={'p-[5px] rounded-[10px] bg-linear-to-b from-[#FB8001] to-[#FCAA2C]'}>
|
<div
|
||||||
<img src={icon} className={'w-[16px] h-[16px]'} />
|
className="flex h-[32px] w-[32px] items-center justify-center rounded-[10px] bg-linear-to-b from-[#FB8001] to-[#FCAA2C] text-white shadow-[0_8px_18px_rgba(250,109,2,0.24)]">
|
||||||
|
<Icon className="h-[16px] w-[16px] shrink-0" aria-hidden="true"/>
|
||||||
</div>
|
</div>
|
||||||
<div>{label}</div>
|
<div className="truncate font-medium capitalize">{label}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>></div>
|
<ChevronRight className="h-[16px] w-[16px] shrink-0 text-white/70" aria-hidden="true"/>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const productCategories: ProductCategory[] = [
|
function getProgressPercent(current = 0, total = 0) {
|
||||||
{
|
if (total <= 0) {
|
||||||
id: 'transfer-to-platform',
|
return 0
|
||||||
name: 'Transfer to Platform',
|
}
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'transfer-100',
|
|
||||||
title: 'Transfer 100',
|
|
||||||
subtitle: '1 x Turnover',
|
|
||||||
points: '1000 Points',
|
|
||||||
ctaLabel: 'Transfer Now',
|
|
||||||
imageClassName: 'bg-[linear-gradient(135deg,_#5B1D00_0%,_#FA6A00_55%,_#FFBC6D_100%)]',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'transfer-300',
|
|
||||||
title: 'Transfer 300',
|
|
||||||
subtitle: '2 x Turnover',
|
|
||||||
points: '2800 Points',
|
|
||||||
ctaLabel: 'Transfer Now',
|
|
||||||
imageClassName: 'bg-[linear-gradient(135deg,_#1C1A48_0%,_#5050D8_55%,_#8AB6FF_100%)]',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'transfer-500',
|
|
||||||
title: 'Transfer 500',
|
|
||||||
subtitle: '3 x Turnover',
|
|
||||||
points: '4200 Points',
|
|
||||||
ctaLabel: 'Transfer Now',
|
|
||||||
imageClassName: 'bg-[linear-gradient(135deg,_#153A35_0%,_#1F9D8B_55%,_#8BF3D8_100%)]',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'transfer-1000',
|
|
||||||
title: 'Transfer 1000',
|
|
||||||
subtitle: '5 x Turnover',
|
|
||||||
points: '7800 Points',
|
|
||||||
ctaLabel: 'Transfer Now',
|
|
||||||
imageClassName: 'bg-[linear-gradient(135deg,_#421515_0%,_#C93D3D_52%,_#FF9B9B_100%)]',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'game-bonus',
|
|
||||||
name: 'Game Bonus',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'bonus-spin',
|
|
||||||
title: 'Lucky Spin Pack',
|
|
||||||
subtitle: 'Bonus Voucher',
|
|
||||||
points: '850 Points',
|
|
||||||
ctaLabel: 'Redeem Bonus',
|
|
||||||
imageClassName: 'bg-[linear-gradient(135deg,_#28103D_0%,_#8E3DD1_50%,_#F1A8FF_100%)]',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'bonus-chip',
|
|
||||||
title: 'Chip Booster',
|
|
||||||
subtitle: 'Casino Special',
|
|
||||||
points: '1600 Points',
|
|
||||||
ctaLabel: 'Redeem Bonus',
|
|
||||||
imageClassName: 'bg-[linear-gradient(135deg,_#2D2407_0%,_#C79200_52%,_#FFE58A_100%)]',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'bonus-cashback',
|
|
||||||
title: 'Cashback Card',
|
|
||||||
subtitle: 'Weekly Reward',
|
|
||||||
points: '2400 Points',
|
|
||||||
ctaLabel: 'Redeem Bonus',
|
|
||||||
imageClassName: 'bg-[linear-gradient(135deg,_#062A2F_0%,_#1296A5_52%,_#6DE2F0_100%)]',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'bonus-vip',
|
|
||||||
title: 'VIP Match Bonus',
|
|
||||||
subtitle: 'Limited Access',
|
|
||||||
points: '5000 Points',
|
|
||||||
ctaLabel: 'Redeem Bonus',
|
|
||||||
imageClassName: 'bg-[linear-gradient(135deg,_#35120A_0%,_#DD6C2F_52%,_#FFC09A_100%)]',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'physical-prizes',
|
|
||||||
name: 'Physical Prizes',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'prize-headset',
|
|
||||||
title: 'Gaming Headset',
|
|
||||||
subtitle: 'Physical Delivery',
|
|
||||||
points: '6800 Points',
|
|
||||||
ctaLabel: 'Claim Prize',
|
|
||||||
imageClassName: 'bg-[linear-gradient(135deg,_#111827_0%,_#374151_50%,_#A3AAB8_100%)]',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'prize-mouse',
|
|
||||||
title: 'Wireless Mouse',
|
|
||||||
subtitle: 'Physical Delivery',
|
|
||||||
points: '4200 Points',
|
|
||||||
ctaLabel: 'Claim Prize',
|
|
||||||
imageClassName: 'bg-[linear-gradient(135deg,_#19212F_0%,_#3A5B84_50%,_#98B8E3_100%)]',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'prize-speaker',
|
|
||||||
title: 'Bluetooth Speaker',
|
|
||||||
subtitle: 'Physical Delivery',
|
|
||||||
points: '7300 Points',
|
|
||||||
ctaLabel: 'Claim Prize',
|
|
||||||
imageClassName: 'bg-[linear-gradient(135deg,_#32120F_0%,_#A73F2F_50%,_#F0A28D_100%)]',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'prize-keyboard',
|
|
||||||
title: 'Mechanical Keyboard',
|
|
||||||
subtitle: 'Physical Delivery',
|
|
||||||
points: '9500 Points',
|
|
||||||
ctaLabel: 'Claim Prize',
|
|
||||||
imageClassName: 'bg-[linear-gradient(135deg,_#142C17_0%,_#2B7A38_52%,_#8DE69B_100%)]',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const initialAddressOptions: AddressOption[] = [
|
return Math.min((current / total) * 100, 100)
|
||||||
{
|
|
||||||
id: 'address-shanghai',
|
|
||||||
name: 'Jia Jun',
|
|
||||||
phone: '+86 138 0000 1288',
|
|
||||||
address: 'No. 88 Century Avenue, Pudong New Area, Shanghai',
|
|
||||||
postalCode: '200120',
|
|
||||||
isDefault: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'address-singapore',
|
|
||||||
name: 'Alicia Tan',
|
|
||||||
phone: '+65 9123 4567',
|
|
||||||
address: '18 Robinson Road, Singapore',
|
|
||||||
postalCode: '048547',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'address-kuala-lumpur',
|
|
||||||
name: 'Marcus Lee',
|
|
||||||
phone: '+60 12 778 9911',
|
|
||||||
address: '27 Jalan Bukit Bintang, Kuala Lumpur',
|
|
||||||
postalCode: '55100',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const emptyAddressForm: AddAddressForm = {
|
|
||||||
name: '',
|
|
||||||
phone: '',
|
|
||||||
region: '',
|
|
||||||
detailedAddress: '',
|
|
||||||
postalCode: '',
|
|
||||||
isDefault: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNumericValue(value: string) {
|
|
||||||
const matched = value.match(/\d+/)
|
|
||||||
return matched ? matched[0] : value
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTurnoverRequirement(subtitle: string) {
|
|
||||||
const matched = subtitle.match(/\d+/)
|
|
||||||
return matched ? `${matched[0]}x` : subtitle
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function HomePage() {
|
function HomePage() {
|
||||||
const [selectedProduct, setSelectedProduct] = useState<SelectedProductState | null>(null)
|
|
||||||
const [claimModalOpen, setClaimModalOpen] = useState(false)
|
const [claimModalOpen, setClaimModalOpen] = useState(false)
|
||||||
const [modalMode, setModalMode] = useState<ModalMode>('select-address')
|
const navigate = useNavigate()
|
||||||
const [addressOptions, setAddressOptions] = useState<AddressOption[]>(initialAddressOptions)
|
const authInfo = useUserStore(state => state.authInfo)
|
||||||
const [selectedAddressId, setSelectedAddressId] = useState<string>(initialAddressOptions[0]?.id ?? '')
|
const {productCategories, loading} = useGoodsCatalog()
|
||||||
const [addressForm, setAddressForm] = useState<AddAddressForm>(emptyAddressForm)
|
const {invalidateAssets} = useAssetsRefresh()
|
||||||
|
const redeem = useGoodsRedeem()
|
||||||
const handleOpenRedeemModal = (product: ProductItem, categoryId: ProductCategory['id']) => {
|
const claimMutation = useMutation({
|
||||||
setSelectedProduct({
|
mutationFn: async (claimRequestId: string) => {
|
||||||
product,
|
return await claim({
|
||||||
categoryId,
|
claim_request_id: claimRequestId,
|
||||||
|
session_id: authInfo!.session_id,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const syncBalanceMutation = useMutation({
|
||||||
|
mutationFn: invalidateAssets,
|
||||||
})
|
})
|
||||||
setSelectedAddressId(addressOptions[0]?.id ?? '')
|
|
||||||
setModalMode('select-address')
|
|
||||||
setAddressForm(emptyAddressForm)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCloseRedeemModal = () => {
|
const {assetsInfo} = useAssetsQuery()
|
||||||
setSelectedProduct(null)
|
const claimProgress = getProgressPercent(assetsInfo?.today_claimed, assetsInfo?.today_limit)
|
||||||
setModalMode('select-address')
|
const previewCategories: ProductCategory[] = productCategories.map((category) => ({
|
||||||
setAddressForm(emptyAddressForm)
|
...category,
|
||||||
}
|
items: category.items.slice(0, 4),
|
||||||
|
}))
|
||||||
|
|
||||||
const handleOpenClaimModal = () => {
|
const handleOpenClaimModal = () => {
|
||||||
setClaimModalOpen(true)
|
setClaimModalOpen(true)
|
||||||
@@ -260,392 +89,158 @@ function HomePage() {
|
|||||||
|
|
||||||
const handleCloseClaimModal = () => {
|
const handleCloseClaimModal = () => {
|
||||||
setClaimModalOpen(false)
|
setClaimModalOpen(false)
|
||||||
|
claimMutation.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOpenAddAddress = () => {
|
const handleSyncBalance = async () => {
|
||||||
setModalMode('add-address')
|
try {
|
||||||
|
await syncBalanceMutation.mutateAsync()
|
||||||
|
notifySuccess('Balance synced successfully.')
|
||||||
|
} catch {
|
||||||
|
// request interceptor handles interface error toast
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChangeAddressForm = (field: keyof AddAddressForm, value: string | boolean) => {
|
const handleConfirmClaim = async () => {
|
||||||
setAddressForm((previous) => ({
|
const claimValidation = validateClaimSubmission(authInfo)
|
||||||
...previous,
|
if (!claimValidation.valid) {
|
||||||
[field]: value,
|
notifyError(claimValidation.message)
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAddAddressFormValid = [
|
|
||||||
addressForm.name,
|
|
||||||
addressForm.phone,
|
|
||||||
addressForm.region,
|
|
||||||
addressForm.detailedAddress,
|
|
||||||
].every((value) => value.trim())
|
|
||||||
|
|
||||||
const handleConfirm = () => {
|
|
||||||
if (modalMode === 'add-address') {
|
|
||||||
if (!isAddAddressFormValid) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newAddress: AddressOption = {
|
try {
|
||||||
id: `address-${Date.now()}`,
|
const response = await claimMutation.mutateAsync(`${authInfo!.user_id}${Date.now()}`)
|
||||||
name: addressForm.name.trim(),
|
await invalidateAssets()
|
||||||
phone: addressForm.phone.trim(),
|
notifySuccess(response, 'Claim submitted successfully.')
|
||||||
address: `${addressForm.region.trim()}, ${addressForm.detailedAddress.trim()}`,
|
setClaimModalOpen(false)
|
||||||
postalCode: addressForm.postalCode.trim() || 'N/A',
|
} catch {
|
||||||
isDefault: addressForm.isDefault,
|
// request errors are surfaced by the shared request toast
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setAddressOptions((previous) => {
|
const handleMoreClick = (type: ProductCategory['id']) => {
|
||||||
const normalizedPrevious = addressForm.isDefault
|
navigate(`/goods?type=${type}`)
|
||||||
? previous.map((item) => ({ ...item, isDefault: false }))
|
|
||||||
: previous
|
|
||||||
|
|
||||||
return [...normalizedPrevious, newAddress]
|
|
||||||
})
|
|
||||||
setSelectedAddressId(newAddress.id)
|
|
||||||
setAddressForm(emptyAddressForm)
|
|
||||||
setModalMode('select-address')
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
handleCloseRedeemModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedCategoryId = selectedProduct?.categoryId
|
|
||||||
const selectedProductData = selectedProduct?.product ?? null
|
|
||||||
const isPhysicalPrize = selectedCategoryId === 'physical-prizes'
|
|
||||||
const isTransferToPlatform = selectedCategoryId === 'transfer-to-platform'
|
|
||||||
const isGameBonus = selectedCategoryId === 'game-bonus'
|
|
||||||
const modalTitle = modalMode === 'add-address'
|
|
||||||
? 'Add Shipping Address'
|
|
||||||
: isTransferToPlatform
|
|
||||||
? 'Confirm Withdrawal'
|
|
||||||
: isGameBonus
|
|
||||||
? 'Confirm Bonus Redemption'
|
|
||||||
: 'Redeem Product'
|
|
||||||
const modalMaxWidthClassName = isTransferToPlatform
|
|
||||||
? 'max-w-[620px]'
|
|
||||||
: isGameBonus
|
|
||||||
? 'max-w-[460px]'
|
|
||||||
: 'max-w-[720px]'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className={'flex justify-end gap-2 py-[10px]'}>
|
<div
|
||||||
<QuickNavCard to="/record" icon={recordSvg} label="record" />
|
className="grid grid-cols-2 gap-2 py-[14px] sm:ml-auto sm:flex sm:w-auto sm:grid-cols-none sm:justify-end">
|
||||||
<QuickNavCard to="/account" icon={accountSvg} label="account" />
|
<QuickNavCard to="/record" icon={History} label="record"/>
|
||||||
|
<QuickNavCard to="/account" icon={UserRound} label="account"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-[4px]">
|
||||||
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-stretch">
|
||||||
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:w-[544px] lg:shrink-0">
|
||||||
|
<div className="liquid-glass-bg flex min-h-[167px] flex-col justify-between p-[14px]">
|
||||||
|
<div className="flex items-start justify-between gap-[12px]">
|
||||||
<div>
|
<div>
|
||||||
<div className={'flex gap-[5px]'}>
|
<div className="text-[13px] uppercase tracking-[0.16em] text-white/58">Claimable
|
||||||
<div className={'liquid-glass-bg h-[167px] w-[267px] p-[10px] flex flex-col justify-start'}>
|
Points
|
||||||
<div>Claimable Points</div>
|
|
||||||
<div>2,880</div>
|
|
||||||
<div>Yesterday's losses
|
|
||||||
have been converted
|
|
||||||
to points.Claim to use.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className={'liquid-glass-bg h-[167px] w-[267px] p-[10px] flex flex-col justify-start'}>
|
|
||||||
<div>Daily Claim Limit</div>
|
|
||||||
<div>1,500</div>
|
|
||||||
<div
|
<div
|
||||||
className={'progress-bar'}
|
className="mt-[10px] text-[34px] font-semibold leading-none text-white">{assetsInfo?.locked_points || 0}</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex h-[38px] w-[38px] items-center justify-center rounded-[12px] bg-[#FA6A00]/16 text-[#FE9F00]">
|
||||||
|
<Coins className="h-[18px] w-[18px]" aria-hidden="true"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="max-w-[28ch] text-[13px] leading-[1.6] text-white/68">
|
||||||
|
Yesterday's losses have been converted into points. Claim them to use in rewards.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="liquid-glass-bg flex min-h-[167px] flex-col justify-around p-[14px]">
|
||||||
|
<div className="flex items-start justify-between gap-[12px]">
|
||||||
|
<div>
|
||||||
|
<div className="text-[13px] uppercase tracking-[0.16em] text-white/58">Daily Claim
|
||||||
|
Limit
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="mt-[10px] text-[34px] font-semibold leading-none text-white">{assetsInfo?.locked_points}</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex h-[38px] w-[38px] items-center justify-center rounded-[12px] bg-[#FA6A00]/16 text-[#FE9F00]">
|
||||||
|
<Gauge className="h-[18px] w-[18px]" aria-hidden="true"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="mt-[14px] h-[10px] w-full overflow-hidden rounded-full border border-white/8 bg-[linear-gradient(180deg,rgba(18,14,10,0.92)_0%,rgba(34,22,14,0.96)_100%)] shadow-[inset_0_1px_2px_rgba(0,0,0,0.45)]"
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
aria-valuemin={0}
|
aria-valuemin={0}
|
||||||
aria-valuemax={1500}
|
aria-valuemax={assetsInfo?.today_limit || 0}
|
||||||
aria-valuenow={800}
|
aria-valuenow={assetsInfo?.today_claimed || 0}
|
||||||
>
|
>
|
||||||
<div className={'progress-bar__fill'} style={{ width: '53.33%' }}></div>
|
<div
|
||||||
|
className="h-full rounded-full bg-linear-to-r from-[#FE9C00] to-[#FA6D02] shadow-[inset_0_0_0_1px_rgba(255,220,160,0.35),0_0_8px_rgba(254,156,0,0.55),0_0_16px_rgba(250,109,2,0.4),0_0_24px_rgba(250,109,2,0.22)]"
|
||||||
|
style={{width: `${claimProgress}%`}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="mt-[10px] text-[13px] text-white/68">Claimed: <span className={'text-[#FE9C00]'}>{assetsInfo?.today_claimed || 0}</span> / {assetsInfo?.today_limit || 0}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>Claimed: 800 / 1500</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'flex flex-col gap-[5px]'}>
|
<div className="flex min-w-0 flex-1 flex-col gap-3">
|
||||||
<div className={'liquid-glass-bg flex-1'}>
|
<div
|
||||||
<div>Available for Withdrawal (Cash)</div>
|
className="liquid-glass-bg flex min-h-[109px] flex-col justify-between p-[14px] sm:p-[16px]">
|
||||||
<div>152 CNY</div>
|
<div className="flex items-start justify-between gap-[12px]">
|
||||||
|
<div>
|
||||||
|
<div className="text-[13px] uppercase tracking-[0.16em] text-white/58">Available for
|
||||||
|
Withdrawal
|
||||||
</div>
|
</div>
|
||||||
<div className={'h-[54px] w-[564px] liquid-glass-bg flex gap-[10px] p-[5px]'}>
|
<div
|
||||||
<button className={'button-play flex-1'} onClick={handleOpenClaimModal}>
|
className="mt-[10px] text-[32px] font-semibold leading-none text-white">{assetsInfo?.withdrawable_cash || 0} CNY</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex h-[38px] w-[38px] items-center justify-center rounded-[12px] bg-[#FA6A00]/16 text-[#FE9F00]">
|
||||||
|
<Wallet className="h-[18px] w-[18px]" aria-hidden="true"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="liquid-glass-bg grid grid-cols-2 gap-[10px] p-[5px]">
|
||||||
|
<Button className="h-[44px] w-full text-[13px]" onClick={handleOpenClaimModal}>
|
||||||
Claim Now
|
Claim Now
|
||||||
</button>
|
</Button>
|
||||||
<button className={'button-play flex-1'}>Sync Balance</button>
|
<Button
|
||||||
|
variant={'gray'}
|
||||||
|
className="h-[44px] w-full text-[13px]"
|
||||||
|
onClick={handleSyncBalance}
|
||||||
|
disabled={syncBalanceMutation.isPending}
|
||||||
|
>
|
||||||
|
{syncBalanceMutation.isPending ? 'Syncing...' : 'Sync Balance'}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<GoodsCategoryList
|
||||||
{
|
categories={previewCategories}
|
||||||
productCategories.map((category) => (<div key={category.id} className={'mt-[20px]'}>
|
loading={loading}
|
||||||
<div className={'flex items-center justify-between mb-[5px]'}>
|
emptyText="No goods available yet."
|
||||||
<div className={'font-bold text-[14px]'}>{category.name}</div>
|
showMore
|
||||||
<div
|
onMoreClick={handleMoreClick}
|
||||||
className={'text-[#FA6A00] text-[12px] font-light underline cursor-pointer'}>more
|
onRedeem={redeem.openRedeemModal}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={'grid grid-cols-1 gap-4 md:grid-cols-4 xl:grid-cols-4'}>
|
|
||||||
{
|
|
||||||
category.items.map((product) => (
|
|
||||||
<div
|
|
||||||
key={product.id}
|
|
||||||
className={'liquid-glass-bg aspect-[215/260] w-full flex flex-col items-stretch justify-start'}>
|
|
||||||
<div className={`${product.imageClassName} w-full h-[40%] rounded-t-[10px]`}></div>
|
|
||||||
<div
|
|
||||||
className={'p-[10px] w-full flex-1 flex flex-col justify-around items-start'}>
|
|
||||||
<div>{product.title}</div>
|
|
||||||
<div className={'text-neutral-500'}>{product.subtitle}</div>
|
|
||||||
<div className={'text-[#FA6A00]'}>{product.points}</div>
|
|
||||||
<button
|
|
||||||
className={'button-play w-full h-[30px]'}
|
|
||||||
onClick={() => handleOpenRedeemModal(product, category.id)}
|
|
||||||
>
|
|
||||||
{product.ctaLabel}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
open={Boolean(selectedProduct)}
|
|
||||||
title={modalTitle}
|
|
||||||
onClose={handleCloseRedeemModal}
|
|
||||||
className={modalMaxWidthClassName}
|
|
||||||
bodyClassName="space-y-[18px]"
|
|
||||||
footer={
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="h-[38px] rounded-[8px] border border-white/15 bg-white/5 px-[18px] text-[14px] text-white transition-colors hover:bg-white/10"
|
|
||||||
onClick={modalMode === 'add-address' ? () => setModalMode('select-address') : handleCloseRedeemModal}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`button-play h-[38px] ${modalMode === 'add-address' && !isAddAddressFormValid ? 'opacity-50' : ''}`}
|
|
||||||
onClick={handleConfirm}
|
|
||||||
disabled={modalMode === 'add-address' && !isAddAddressFormValid}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{selectedProductData ? (
|
|
||||||
<>
|
|
||||||
{isTransferToPlatform ? (
|
|
||||||
<div className="rounded-[14px] bg-[#1C1818]/80 px-[18px] py-[14px] shadow-[0_10px_30px_rgba(0,0,0,0.22)]">
|
|
||||||
<div className="divide-y divide-white/8">
|
|
||||||
<div className="flex items-center justify-between py-[14px] text-white">
|
|
||||||
<div className="text-[18px]">Withdrawal Amount</div>
|
|
||||||
<div className="text-[18px]">{getNumericValue(selectedProductData.title)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between py-[14px] text-white">
|
|
||||||
<div className="text-[18px]">Points Required</div>
|
|
||||||
<div className="text-[18px]">{getNumericValue(selectedProductData.points)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between py-[14px] text-white">
|
|
||||||
<div className="text-[18px] underline decoration-[#1E90FF] underline-offset-[3px]">
|
|
||||||
Turnover Requirement
|
|
||||||
</div>
|
|
||||||
<div className="text-[18px]">{getTurnoverRequirement(selectedProductData.subtitle)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="pt-[10px] text-center text-[18px] text-white/45">
|
|
||||||
Submit withdrawal request?
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : isGameBonus ? (
|
|
||||||
<div className="rounded-[10px] bg-[#1C1818]/80 px-[16px] py-[8px] shadow-[0_10px_30px_rgba(0,0,0,0.22)]">
|
|
||||||
<div className="divide-y divide-white/8">
|
|
||||||
<div className="flex items-center justify-between py-[14px] text-white">
|
|
||||||
<div className="text-[13px] text-white/78">Item</div>
|
|
||||||
<div className="text-[14px]">{selectedProductData.title}</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between py-[14px] text-white">
|
|
||||||
<div className="text-[13px] text-white/78">Points Required</div>
|
|
||||||
<div className="text-[14px]">{getNumericValue(selectedProductData.points)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between py-[14px] text-white">
|
|
||||||
<div className="text-[13px] text-white/78">Turnover Requirement</div>
|
|
||||||
<div className="text-[14px]">{getTurnoverRequirement(selectedProductData.subtitle)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : modalMode === 'select-address' && isPhysicalPrize ? (
|
|
||||||
<>
|
|
||||||
<div className="flex gap-[14px] rounded-[12px] bg-white/5 p-[12px]">
|
|
||||||
<div
|
|
||||||
className={`${selectedProductData.imageClassName} h-[110px] w-[140px] shrink-0 rounded-[10px]`}
|
|
||||||
></div>
|
|
||||||
<div className="flex min-w-0 flex-1 flex-col justify-between">
|
|
||||||
<div>
|
|
||||||
<div className="text-[20px] font-bold text-white">{selectedProductData.title}</div>
|
|
||||||
<div className="mt-[6px] text-[13px] text-white/60">{selectedProductData.subtitle}</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-[18px] font-bold text-[#FA6A00]">{selectedProductData.points}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="mb-[10px] text-[14px] font-bold text-white">Select Shipping Address</div>
|
|
||||||
<div className="space-y-[10px]">
|
|
||||||
{addressOptions.map((address) => {
|
|
||||||
const isSelected = selectedAddressId === address.id
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={address.id}
|
|
||||||
type="button"
|
|
||||||
className={`flex w-full items-start gap-[12px] rounded-[12px] border px-[14px] py-[14px] text-left transition-colors ${
|
|
||||||
isSelected
|
|
||||||
? 'border-[#FA6A00] bg-[#FA6A00]/12'
|
|
||||||
: 'border-white/10 bg-white/4 hover:bg-white/8'
|
|
||||||
}`}
|
|
||||||
onClick={() => setSelectedAddressId(address.id)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`mt-[3px] h-[16px] w-[16px] rounded-full border ${
|
|
||||||
isSelected
|
|
||||||
? 'border-[#FA6A00] bg-[#FA6A00] shadow-[0_0_12px_rgba(250,106,0,0.45)]'
|
|
||||||
: 'border-white/30'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`m-auto mt-[3px] h-[6px] w-[6px] rounded-full bg-white ${
|
|
||||||
isSelected ? 'block' : 'hidden'
|
|
||||||
}`}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="flex items-center gap-[8px]">
|
|
||||||
<div className="text-[14px] font-bold text-white">{address.name}</div>
|
|
||||||
<div className="text-[13px] text-white/60">{address.phone}</div>
|
|
||||||
{address.isDefault ? (
|
|
||||||
<div className="rounded-full bg-[#FA6A00]/18 px-[8px] py-[2px] text-[11px] text-[#FFB36D]">
|
|
||||||
Default
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="mt-[6px] text-[13px] leading-[1.5] text-white/75">
|
|
||||||
{address.address}
|
|
||||||
</div>
|
|
||||||
<div className="mt-[4px] text-[12px] text-white/45">
|
|
||||||
Postal Code: {address.postalCode}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex w-full items-center gap-[12px] rounded-[12px] border border-dashed border-[#FA6A00]/55 bg-[#FA6A00]/8 px-[14px] py-[16px] text-left transition-colors hover:bg-[#FA6A00]/12"
|
|
||||||
onClick={handleOpenAddAddress}
|
|
||||||
>
|
|
||||||
<div className="flex h-[18px] w-[18px] items-center justify-center rounded-full bg-[#FA6A00] text-[14px] leading-none text-white">
|
|
||||||
+
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-[14px] font-bold text-white">Add New Address</div>
|
|
||||||
<div className="mt-[4px] text-[12px] text-white/55">
|
|
||||||
Create a shipping address for this redemption
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : isPhysicalPrize ? (
|
|
||||||
<div className="rounded-[12px] bg-[#171313]/88 px-[14px] py-[10px] shadow-[0_10px_30px_rgba(0,0,0,0.25)]">
|
|
||||||
<div className="flex items-center justify-between border-b border-white/10 px-[6px] pb-[16px]">
|
|
||||||
<div className="text-[18px] font-medium text-white">Address Info</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex items-center gap-[8px] text-[14px] text-white/85"
|
|
||||||
onClick={() => handleChangeAddressForm('isDefault', !addressForm.isDefault)}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`flex h-[16px] w-[16px] items-center justify-center rounded-full border ${
|
|
||||||
addressForm.isDefault ? 'border-[#FA6A00]' : 'border-white/35'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`h-[8px] w-[8px] rounded-full ${
|
|
||||||
addressForm.isDefault ? 'bg-[#FA6A00]' : 'bg-transparent'
|
|
||||||
}`}
|
|
||||||
></span>
|
|
||||||
</span>
|
|
||||||
<span>Default Address</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="divide-y divide-white/10">
|
|
||||||
<div className="grid grid-cols-[170px_1fr] items-center gap-[10px] px-[6px] py-[16px]">
|
|
||||||
<label className="text-[14px] text-white/92">
|
|
||||||
Name<span className="text-[#FA6A00]">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
value={addressForm.name}
|
|
||||||
onChange={(event) => handleChangeAddressForm('name', event.target.value)}
|
|
||||||
placeholder="Full Name"
|
|
||||||
className="bg-transparent text-[14px] text-white outline-none placeholder:text-white/35"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-[170px_1fr] items-center gap-[10px] px-[6px] py-[16px]">
|
<GoodsRedeemModal
|
||||||
<label className="text-[14px] text-white/92">
|
selectedProduct={redeem.selectedProduct}
|
||||||
Phone Number<span className="text-[#FA6A00]">*</span>
|
modalMode={redeem.modalMode}
|
||||||
</label>
|
addressOptions={redeem.addressOptions}
|
||||||
<input
|
selectedAddressId={redeem.selectedAddressId}
|
||||||
value={addressForm.phone}
|
addressForm={redeem.addressForm}
|
||||||
onChange={(event) => handleChangeAddressForm('phone', event.target.value)}
|
addressLoading={redeem.addressLoading}
|
||||||
placeholder="Phone Number"
|
isAddAddressFormValid={redeem.isAddAddressFormValid}
|
||||||
className="bg-transparent text-[14px] text-white outline-none placeholder:text-white/35"
|
submitLoading={redeem.submitLoading}
|
||||||
|
onClose={redeem.closeRedeemModal}
|
||||||
|
onConfirm={redeem.confirmRedeem}
|
||||||
|
onOpenAddAddress={redeem.openAddAddress}
|
||||||
|
onBackToSelectAddress={redeem.backToSelectAddress}
|
||||||
|
onSelectAddress={redeem.setSelectedAddressId}
|
||||||
|
onChangeAddressForm={redeem.changeAddressForm}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-[170px_1fr] items-center gap-[10px] px-[6px] py-[16px]">
|
|
||||||
<label className="text-[14px] text-white/92">
|
|
||||||
Region<span className="text-[#FA6A00]">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
value={addressForm.region}
|
|
||||||
onChange={(event) => handleChangeAddressForm('region', event.target.value)}
|
|
||||||
placeholder="State, City, District"
|
|
||||||
className="bg-transparent text-[14px] text-white outline-none placeholder:text-white/35"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-[170px_1fr] items-center gap-[10px] px-[6px] py-[16px]">
|
|
||||||
<label className="text-[14px] text-white/92">
|
|
||||||
Detailed Address<span className="text-[#FA6A00]">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
value={addressForm.detailedAddress}
|
|
||||||
onChange={(event) => handleChangeAddressForm('detailedAddress', event.target.value)}
|
|
||||||
placeholder="Apt, Suite, Bldg, etc"
|
|
||||||
className="bg-transparent text-[14px] text-white outline-none placeholder:text-white/35"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-[170px_1fr] items-center gap-[10px] px-[6px] py-[16px]">
|
|
||||||
<label className="text-[14px] text-white/92">Postal Code</label>
|
|
||||||
<input
|
|
||||||
value={addressForm.postalCode}
|
|
||||||
onChange={(event) => handleChangeAddressForm('postalCode', event.target.value)}
|
|
||||||
placeholder="Postal Code"
|
|
||||||
className="bg-transparent text-[14px] text-white outline-none placeholder:text-white/35"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
open={claimModalOpen}
|
open={claimModalOpen}
|
||||||
@@ -655,22 +250,23 @@ function HomePage() {
|
|||||||
bodyClassName="space-y-[18px]"
|
bodyClassName="space-y-[18px]"
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<button
|
<Button type="button" variant={'gray'} className="h-[38px] w-full sm:w-auto sm:min-w-[130px]"
|
||||||
type="button"
|
|
||||||
className="h-[38px] min-w-[120px] rounded-[8px] border border-white/10 bg-white/10 px-[18px] text-[14px] text-white/75 transition-colors hover:bg-white/14"
|
|
||||||
onClick={handleCloseClaimModal}
|
onClick={handleCloseClaimModal}
|
||||||
>
|
disabled={claimMutation.isPending}>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</Button>
|
||||||
<button type="button" className="button-play h-[38px] min-w-[130px]" onClick={handleCloseClaimModal}>
|
|
||||||
Confirm
|
<Button type="button" className="h-[38px] w-full sm:w-auto sm:min-w-[130px]"
|
||||||
</button>
|
onClick={handleConfirmClaim}
|
||||||
|
disabled={claimMutation.isPending}>
|
||||||
|
{claimMutation.isPending ? 'Processing...' : 'Confirm'}
|
||||||
|
</Button>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="rounded-[12px] bg-[#1C1818]/82 px-[14px] py-[18px] text-[17px] leading-[1.65] text-white/92 shadow-[0_10px_30px_rgba(0,0,0,0.2)]">
|
<div
|
||||||
Once pending points are transferred to your available balance, they can be redeemed or withdrawn.
|
className="rounded-[12px] bg-[#1C1818]/82 px-[14px] py-[18px] text-[17px] leading-[1.65] text-white/92 shadow-[0_10px_30px_rgba(0,0,0,0.2)]">
|
||||||
Confirm claim?
|
After converting the points to be collected into usable points, they can be redeemed or withdrawn. Are you sure to claim it?
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
|||||||
@@ -1,131 +1,246 @@
|
|||||||
import { useState } from 'react'
|
import {useEffect, useState} from 'react'
|
||||||
|
import {useQuery} from '@tanstack/react-query'
|
||||||
|
|
||||||
import PageLayout from '@/components/layout'
|
import PageLayout from '@/components/layout'
|
||||||
|
import {ORDER_STATUS} from '@/constant'
|
||||||
import { cn } from '@/lib'
|
import { cn } from '@/lib'
|
||||||
import Modal from '@/components/modal'
|
import Modal from '@/components/modal'
|
||||||
import type { RecordButtonType } from '@/types'
|
import Button from '@/components/button'
|
||||||
|
import type { OrderCardProps, OrderRecord, PointsCardProps, RecordButtonType, TabButtonProps } from '@/types'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
|
import { ArrowLeft, ChevronRight, Coins, PackageSearch } from 'lucide-react'
|
||||||
|
import {orders, pointsLogs} from '@/api/business.ts'
|
||||||
|
import {queryKeys} from '@/lib/queryKeys.ts'
|
||||||
|
import { useUserStore } from '@/store/user.ts'
|
||||||
|
import type {OrderItem, PointsLogItem} from '@/types/business.type.ts'
|
||||||
|
|
||||||
type OrderRecord = {
|
const pointsRecordToneClassName = {
|
||||||
id: string
|
|
||||||
date: string
|
|
||||||
time: string
|
|
||||||
category: string
|
|
||||||
title: string
|
|
||||||
trackingNumber?: string
|
|
||||||
status: string
|
|
||||||
points: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type PointsRecord = {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
date: string
|
|
||||||
time: string
|
|
||||||
amount: string
|
|
||||||
tone: 'positive' | 'negative'
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderRecords: OrderRecord[] = [
|
|
||||||
{
|
|
||||||
id: 'order-1',
|
|
||||||
date: '2025-03-04',
|
|
||||||
time: '10:20',
|
|
||||||
category: 'Bonus',
|
|
||||||
title: 'Daily Rebate 50',
|
|
||||||
status: 'Issued',
|
|
||||||
points: '-500 points',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'order-2',
|
|
||||||
date: '2025-03-03',
|
|
||||||
time: '14:00',
|
|
||||||
category: 'Physical',
|
|
||||||
title: 'Weekly Bonus 200',
|
|
||||||
trackingNumber: 'SF1234567890',
|
|
||||||
status: 'Shipped',
|
|
||||||
points: '-1200 points',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'order-3',
|
|
||||||
date: '2025-03-02',
|
|
||||||
time: '09:15',
|
|
||||||
category: 'Withdrawal',
|
|
||||||
title: 'Wireless Earbuds',
|
|
||||||
status: 'Issued',
|
|
||||||
points: '-1000 points',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'order-4',
|
|
||||||
date: '2025-03-01',
|
|
||||||
time: '16:30',
|
|
||||||
category: 'Bonus',
|
|
||||||
title: 'Fitness Tracker',
|
|
||||||
status: 'Pending',
|
|
||||||
points: '-1800 points',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'order-5',
|
|
||||||
date: '2025-02-28',
|
|
||||||
time: '11:00',
|
|
||||||
category: 'Physical',
|
|
||||||
title: 'Withdraw 100',
|
|
||||||
status: 'Rejected',
|
|
||||||
points: '-2500 points',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const pointsRecords: PointsRecord[] = [
|
|
||||||
{
|
|
||||||
id: 'points-1',
|
|
||||||
title: 'Bonus Redemption - Daily Rewards 50',
|
|
||||||
date: '2025-03-04',
|
|
||||||
time: '10:20',
|
|
||||||
amount: '-500',
|
|
||||||
tone: 'negative',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'points-2',
|
|
||||||
title: "Claim Yesterday's Protection Funds",
|
|
||||||
date: '2025-03-04',
|
|
||||||
time: '09:20',
|
|
||||||
amount: '+800',
|
|
||||||
tone: 'positive',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'points-3',
|
|
||||||
title: 'Physical Item Redemption - Bluetooth Headphones',
|
|
||||||
date: '2025-03-03',
|
|
||||||
time: '14:00',
|
|
||||||
amount: '-1200',
|
|
||||||
tone: 'negative',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'points-4',
|
|
||||||
title: "Claim Yesterday's Protection Funds",
|
|
||||||
date: '2025-03-03',
|
|
||||||
time: '09:00',
|
|
||||||
amount: '+700',
|
|
||||||
tone: 'positive',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'points-5',
|
|
||||||
title: 'Withdraw to Platform - 100',
|
|
||||||
date: '2025-03-02',
|
|
||||||
time: '09:15',
|
|
||||||
amount: '-1000',
|
|
||||||
tone: 'negative',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const amountToneClassName: Record<PointsRecord['tone'], string> = {
|
|
||||||
positive: 'bg-[#9BFFC0] text-[#176640]',
|
positive: 'bg-[#9BFFC0] text-[#176640]',
|
||||||
negative: 'bg-[#FF9BA4] text-[#7B2634]',
|
negative: 'bg-[#FF9BA4] text-[#7B2634]',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
function formatDatePart(value: number) {
|
||||||
|
return String(value).padStart(2, '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDateTimeParts(value?: number | string) {
|
||||||
|
if (value == null) {
|
||||||
|
return { date: '--', time: '--:--' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const numericValue = typeof value === 'string' && /^\d+$/.test(value) ? Number(value) : value
|
||||||
|
const date = new Date(
|
||||||
|
typeof numericValue === 'number'
|
||||||
|
? String(numericValue).length === 13
|
||||||
|
? numericValue
|
||||||
|
: numericValue * 1000
|
||||||
|
: numericValue,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return { date: '--', time: '--:--' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
date: `${date.getFullYear()}-${formatDatePart(date.getMonth() + 1)}-${formatDatePart(date.getDate())}`,
|
||||||
|
time: `${formatDatePart(date.getHours())}:${formatDatePart(date.getMinutes())}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toTitleCase(value: string) {
|
||||||
|
return value
|
||||||
|
.toLowerCase()
|
||||||
|
.split(/[\s_-]+/)
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((part) => part[0]?.toUpperCase() + part.slice(1))
|
||||||
|
.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrderStatus(status?: string | number) {
|
||||||
|
if (typeof status === 'string') {
|
||||||
|
const normalizedStatus = status.trim().toUpperCase()
|
||||||
|
const matchedStatus = ORDER_STATUS.find((item) => item === normalizedStatus)
|
||||||
|
if (matchedStatus) {
|
||||||
|
switch (matchedStatus) {
|
||||||
|
case 'PENDING':
|
||||||
|
return 'Pending'
|
||||||
|
case 'COMPLETED':
|
||||||
|
return 'Completed'
|
||||||
|
case 'SHIPPED':
|
||||||
|
return 'Shipped'
|
||||||
|
case 'REJECTED':
|
||||||
|
return 'Rejected'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedStatus = typeof status === 'string' && /^\d+$/.test(status)
|
||||||
|
? Number(status)
|
||||||
|
: status
|
||||||
|
|
||||||
|
if (typeof normalizedStatus === 'number') {
|
||||||
|
switch (normalizedStatus) {
|
||||||
|
case 0:
|
||||||
|
return 'Pending'
|
||||||
|
case 1:
|
||||||
|
return 'Completed'
|
||||||
|
case 2:
|
||||||
|
return 'Shipped'
|
||||||
|
case 3:
|
||||||
|
return 'Rejected'
|
||||||
|
default:
|
||||||
|
return String(normalizedStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!normalizedStatus) {
|
||||||
|
return 'Pending'
|
||||||
|
}
|
||||||
|
|
||||||
|
return toTitleCase(normalizedStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrderCategory(item: OrderItem) {
|
||||||
|
if (item.type?.trim()) {
|
||||||
|
return item.type.trim().toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type_title) {
|
||||||
|
return item.type_title
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.category_title) {
|
||||||
|
return item.category_title
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type) {
|
||||||
|
switch (item.type) {
|
||||||
|
case 'BONUS':
|
||||||
|
return 'Bonus'
|
||||||
|
case 'PHYSICAL':
|
||||||
|
return 'Physical'
|
||||||
|
case 'WITHDRAW':
|
||||||
|
return 'Transfer to Platform'
|
||||||
|
default:
|
||||||
|
return toTitleCase(item.type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.category) {
|
||||||
|
return toTitleCase(item.category)
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Order'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrderPoints(item: OrderItem) {
|
||||||
|
const rawValue = item.points_cost
|
||||||
|
|
||||||
|
if (rawValue == null || rawValue === '') {
|
||||||
|
return {display: '--'}
|
||||||
|
}
|
||||||
|
|
||||||
|
const numericValue = typeof rawValue === 'string' ? Number(rawValue) : rawValue
|
||||||
|
if (Number.isNaN(numericValue)) {
|
||||||
|
const textValue = String(rawValue)
|
||||||
|
return {display: `${textValue} points`}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
display: `${numericValue} points`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTrackingNumber(item: OrderItem) {
|
||||||
|
const shippingNumber = item.shipping_no?.trim()
|
||||||
|
const logisticsNumber = item.logistics_no?.trim()
|
||||||
|
const trackingNumber = item.tracking_no?.trim()
|
||||||
|
const shippingCompany = item.shipping_company?.trim()
|
||||||
|
const resolvedNumber = shippingNumber || logisticsNumber || trackingNumber
|
||||||
|
|
||||||
|
if (!resolvedNumber) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return shippingCompany ? `${shippingCompany} ${resolvedNumber}` : resolvedNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapOrderItemToRecord(item: OrderItem): OrderRecord {
|
||||||
|
const { date, time } = getDateTimeParts(item.create_time ?? item.created_at)
|
||||||
|
const orderNumber = item.external_transaction_id?.trim() || item.order_no ? String(item.external_transaction_id?.trim() || item.order_no) : undefined
|
||||||
|
const points = getOrderPoints(item)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: String(item.id),
|
||||||
|
orderNumber,
|
||||||
|
date,
|
||||||
|
time,
|
||||||
|
category: getOrderCategory(item),
|
||||||
|
title: item.item_title ?? item.title ?? item.mallItem?.title ?? 'Untitled Order',
|
||||||
|
trackingNumber: getTrackingNumber(item),
|
||||||
|
status: getOrderStatus(item.status),
|
||||||
|
points: points.display,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPointsRecordAmount(item: PointsLogItem) {
|
||||||
|
const rawValue = item.points
|
||||||
|
|
||||||
|
if (rawValue == null || rawValue === '') {
|
||||||
|
return {
|
||||||
|
amount: '--',
|
||||||
|
tone: 'negative' as const,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const numericValue = typeof rawValue === 'string' ? Number(rawValue) : rawValue
|
||||||
|
if (Number.isNaN(numericValue)) {
|
||||||
|
const textValue = String(rawValue).trim()
|
||||||
|
return {
|
||||||
|
amount: item.direction === 'IN'
|
||||||
|
? textValue.startsWith('+') ? textValue : `+${textValue.replace(/^[+-]/, '')}`
|
||||||
|
: item.direction === 'OUT'
|
||||||
|
? textValue.startsWith('-') ? textValue : `-${textValue.replace(/^[+-]/, '')}`
|
||||||
|
: textValue,
|
||||||
|
tone: item.direction === 'IN' ? 'positive' as const : 'negative' as const,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
amount: `${item.direction === 'IN' ? '+' : item.direction === 'OUT' ? '-' : ''}${Math.abs(numericValue)}`,
|
||||||
|
tone: item.direction === 'IN' ? 'positive' as const : 'negative' as const,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPointsRecordTitle(item: PointsLogItem) {
|
||||||
|
return [
|
||||||
|
item.item_title,
|
||||||
|
item.title,
|
||||||
|
item.mallItem?.title,
|
||||||
|
item.biz_type,
|
||||||
|
item.type,
|
||||||
|
item.description,
|
||||||
|
item.remark?.split('\n')[0],
|
||||||
|
].find((value) => typeof value === 'string' && value.trim())?.trim() ?? 'Points Record'
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapPointsLogItemToRecord(item: PointsLogItem) {
|
||||||
|
const {date, time} = getDateTimeParts(item.ts ?? item.create_time ?? item.created_at ?? item.update_time)
|
||||||
|
const amount = getPointsRecordAmount(item)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: String(item.id),
|
||||||
|
title: getPointsRecordTitle(item),
|
||||||
|
date,
|
||||||
|
time,
|
||||||
|
amount: amount.amount,
|
||||||
|
tone: amount.tone,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOrderStatusClassName(status: string) {
|
function getOrderStatusClassName(status: string) {
|
||||||
switch (status.toLowerCase()) {
|
switch (status.toLowerCase()) {
|
||||||
case 'issued':
|
case 'completed':
|
||||||
return 'bg-[#9BFFC0] text-[#176640]'
|
return 'bg-[#9BFFC0] text-[#176640]'
|
||||||
case 'shipped':
|
case 'shipped':
|
||||||
return 'bg-[#95F0FF] text-[#116A79]'
|
return 'bg-[#95F0FF] text-[#116A79]'
|
||||||
@@ -138,29 +253,19 @@ function getOrderStatusClassName(status: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type TabButtonProps = {
|
function TabButton({ active, label, icon: Icon, onClick }: TabButtonProps) {
|
||||||
active: boolean
|
|
||||||
label: string
|
|
||||||
onClick: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
type OrderCardProps = {
|
|
||||||
record: OrderRecord
|
|
||||||
onOpenDetails: (record: OrderRecord) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabButton({ active, label, onClick }: TabButtonProps) {
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
'min-w-[92px] cursor-pointer rounded-[6px] border px-[14px] py-[7px] text-[13px] transition-colors',
|
'inline-flex min-w-[140px] cursor-pointer items-center justify-center gap-[8px] rounded-[10px] border px-[14px] py-[9px] text-[13px] transition-colors focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]',
|
||||||
active
|
active
|
||||||
? 'border-[#F99A0B] bg-linear-to-r from-[#F96C02] to-[#FE9F00] text-white shadow-[0_0_16px_rgba(249,108,2,0.22)]'
|
? 'border-[#F99A0B] bg-linear-to-r from-[#F96C02] to-[#FE9F00] text-white shadow-[0_0_16px_rgba(249,108,2,0.22)]'
|
||||||
: 'border-white/35 bg-white/3 text-[#B8B1AA] hover:bg-white/6',
|
: 'border-white/35 bg-white/3 text-[#B8B1AA] hover:bg-white/6',
|
||||||
)}
|
)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
|
<Icon className="h-[15px] w-[15px]" aria-hidden="true" />
|
||||||
{label}
|
{label}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
@@ -168,12 +273,12 @@ function TabButton({ active, label, onClick }: TabButtonProps) {
|
|||||||
|
|
||||||
function OrderCard({ record, onOpenDetails }: OrderCardProps) {
|
function OrderCard({ record, onOpenDetails }: OrderCardProps) {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden rounded-[10px] shadow-[0_10px_30px_rgba(0,0,0,0.24)]">
|
<div className="overflow-hidden rounded-[12px] shadow-[0_10px_30px_rgba(0,0,0,0.24)]">
|
||||||
<div className="bg-linear-to-r from-[#F96C02] to-[#FE9F00] px-[12px] py-[8px] text-[14px] text-white">
|
<div className="bg-linear-to-r from-[#F96C02] to-[#FE9F00] px-[12px] py-[9px] text-[13px] text-white sm:text-[14px]">
|
||||||
{record.date} {record.time} • {record.category}
|
{record.date} {record.time} • {record.category}
|
||||||
</div>
|
</div>
|
||||||
<div className="liquid-glass-bg !rounded-t-none flex items-center justify-between px-[12px] py-[12px]">
|
<div className="liquid-glass-bg !rounded-t-none flex flex-col gap-[14px] px-[12px] py-[14px] sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="min-w-0 flex-1 pr-[16px]">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="text-[16px] font-medium text-white">{record.title}</div>
|
<div className="text-[16px] font-medium text-white">{record.title}</div>
|
||||||
{record.trackingNumber ? (
|
{record.trackingNumber ? (
|
||||||
<div className="mt-[4px] text-[13px] text-white/45">
|
<div className="mt-[4px] text-[13px] text-white/45">
|
||||||
@@ -182,14 +287,15 @@ function OrderCard({ record, onOpenDetails }: OrderCardProps) {
|
|||||||
) : null}
|
) : null}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="mt-[8px] text-[13px] text-[#FA6A00]"
|
className="mt-[10px] inline-flex items-center gap-[5px] text-[13px] text-[#FA6A00] focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]"
|
||||||
onClick={() => onOpenDetails(record)}
|
onClick={() => onOpenDetails(record)}
|
||||||
>
|
>
|
||||||
Check the details
|
Check the details
|
||||||
|
<ChevronRight className="h-[14px] w-[14px]" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex shrink-0 flex-col items-end gap-[10px]">
|
<div className="flex shrink-0 items-center justify-between gap-[10px] sm:flex-col sm:items-end">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-[6px] px-[8px] py-[3px] text-[11px] leading-none',
|
'rounded-[6px] px-[8px] py-[3px] text-[11px] leading-none',
|
||||||
@@ -198,27 +304,27 @@ function OrderCard({ record, onOpenDetails }: OrderCardProps) {
|
|||||||
>
|
>
|
||||||
{record.status}
|
{record.status}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[13px] text-white/85">{record.points}</div>
|
<div className="text-[13px] text-white/85">{record.points.replace(/^-/, '')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function PointsCard({ record }: { record: PointsRecord }) {
|
function PointsCard({ record }: PointsCardProps) {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden rounded-[10px] shadow-[0_10px_30px_rgba(0,0,0,0.24)]">
|
<div className="overflow-hidden rounded-[12px] shadow-[0_10px_30px_rgba(0,0,0,0.24)]">
|
||||||
<div className="bg-linear-to-r from-[#F96C02] to-[#FE9F00] px-[12px] py-[8px] text-[15px] text-white">
|
<div className="bg-linear-to-r from-[#F96C02] to-[#FE9F00] px-[12px] py-[9px] text-[14px] text-white sm:text-[15px]">
|
||||||
{record.title}
|
{record.title}
|
||||||
</div>
|
</div>
|
||||||
<div className="liquid-glass-bg !rounded-t-none flex items-center justify-between px-[12px] py-[22px]">
|
<div className="liquid-glass-bg !rounded-t-none flex flex-col gap-[10px] px-[12px] py-[18px] sm:flex-row sm:items-center sm:justify-between sm:py-[22px]">
|
||||||
<div className="text-[13px] text-white/40">
|
<div className="text-[13px] text-white/40">
|
||||||
{record.date} {record.time}
|
{record.date} {record.time}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-[6px] px-[10px] py-[3px] text-[12px] leading-none',
|
'inline-flex w-fit rounded-[6px] px-[10px] py-[3px] text-[12px] leading-none',
|
||||||
amountToneClassName[record.tone],
|
pointsRecordToneClassName[record.tone],
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{record.amount}
|
{record.amount}
|
||||||
@@ -228,7 +334,92 @@ function PointsCard({ record }: { record: PointsRecord }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function OrdersTabContent({
|
||||||
|
sessionId,
|
||||||
|
onOpenDetails,
|
||||||
|
}: {
|
||||||
|
sessionId: string
|
||||||
|
onOpenDetails: (record: OrderRecord) => void
|
||||||
|
}) {
|
||||||
|
const ordersQuery = useQuery({
|
||||||
|
queryKey: queryKeys.orders(sessionId),
|
||||||
|
enabled: Boolean(sessionId),
|
||||||
|
gcTime: 0,
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await orders({
|
||||||
|
session_id: sessionId,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (response.data.list ?? []).map(mapOrderItemToRecord)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const orderRecords = ordersQuery.data ?? []
|
||||||
|
|
||||||
|
if (ordersQuery.isPending) {
|
||||||
|
return (
|
||||||
|
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!orderRecords.length) {
|
||||||
|
return (
|
||||||
|
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
||||||
|
No Data
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{orderRecords.map((record) => (
|
||||||
|
<OrderCard key={record.id} record={record} onOpenDetails={onOpenDetails} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PointsTabContent({sessionId}: {sessionId: string}) {
|
||||||
|
const pointsLogsQuery = useQuery({
|
||||||
|
queryKey: queryKeys.pointsLogs(sessionId),
|
||||||
|
enabled: Boolean(sessionId),
|
||||||
|
gcTime: 0,
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await pointsLogs({
|
||||||
|
session_id: sessionId,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (response.data.list ?? []).map(mapPointsLogItemToRecord)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const pointsRecords = pointsLogsQuery.data ?? []
|
||||||
|
|
||||||
|
if (pointsLogsQuery.isPending) {
|
||||||
|
return (
|
||||||
|
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pointsRecords.length) {
|
||||||
|
return (
|
||||||
|
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
||||||
|
No Data
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{pointsRecords.map((record) => <PointsCard key={record.id} record={record} />)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function RecordPage() {
|
function RecordPage() {
|
||||||
|
const sessionId = useUserStore((state) => state.authInfo?.session_id ?? '')
|
||||||
const [tab, setTab] = useState<RecordButtonType>('order')
|
const [tab, setTab] = useState<RecordButtonType>('order')
|
||||||
const [selectedOrder, setSelectedOrder] = useState<OrderRecord | null>(null)
|
const [selectedOrder, setSelectedOrder] = useState<OrderRecord | null>(null)
|
||||||
|
|
||||||
@@ -236,30 +427,50 @@ function RecordPage() {
|
|||||||
setSelectedOrder(null)
|
setSelectedOrder(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedOrder(null)
|
||||||
|
}, [tab])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout contentClassName="min-h-screen">
|
<PageLayout contentClassName="mx-auto flex min-h-screen w-full max-w-[980px] flex-col px-4 pb-8 sm:px-6 lg:px-8">
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
className="relative flex h-[40px] w-full items-center justify-center bg-[#08070E]/70 text-[#F56E10]"
|
className="mt-[12px] flex h-[44px] items-center justify-between rounded-[12px] bg-[#08070E]/72 px-[14px] text-[#F56E10] transition-colors hover:bg-[#0D0A14]/80 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E] sm:mt-[16px]"
|
||||||
>
|
>
|
||||||
<div className="absolute left-[16px]"><</div>
|
<div className="flex items-center gap-[8px]">
|
||||||
<div>Record</div>
|
<ArrowLeft className="h-[16px] w-[16px]" aria-hidden="true" />
|
||||||
|
<span className="text-[14px] font-medium text-white/92">Back</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-[15px] font-semibold text-[#F56E10]">Record</div>
|
||||||
|
<div className="w-[52px]"></div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="mx-auto w-[60%] pt-[14px] pb-[24px]">
|
<div className="mx-auto w-full max-w-[860px] pt-[18px] pb-[24px]">
|
||||||
|
<div className="mb-[12px] flex justify-end">
|
||||||
<div className="flex gap-[8px]">
|
<div className="flex gap-[8px]">
|
||||||
<TabButton active={tab === 'order'} label="My Orders" onClick={() => setTab('order')} />
|
<TabButton
|
||||||
<TabButton active={tab === 'record'} label="Points Record" onClick={() => setTab('record')} />
|
active={tab === 'order'}
|
||||||
|
icon={PackageSearch}
|
||||||
|
label="My Orders"
|
||||||
|
onClick={() => setTab('order')}
|
||||||
|
/>
|
||||||
|
<TabButton
|
||||||
|
active={tab === 'record'}
|
||||||
|
icon={Coins}
|
||||||
|
label="Points Record"
|
||||||
|
onClick={() => setTab('record')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-[10px] border-t border-white/20"></div>
|
<div className="h-px bg-white/16"></div>
|
||||||
|
|
||||||
<div className="mt-[12px] flex flex-col gap-[12px]">
|
<div className="mt-[14px] flex flex-col gap-[12px]">
|
||||||
{tab === 'order'
|
{tab === 'order' ? (
|
||||||
? orderRecords.map((record) => (
|
<OrdersTabContent key="order" sessionId={sessionId} onOpenDetails={setSelectedOrder} />
|
||||||
<OrderCard key={record.id} record={record} onOpenDetails={setSelectedOrder} />
|
) : (
|
||||||
))
|
<PointsTabContent key="record" sessionId={sessionId} />
|
||||||
: pointsRecords.map((record) => <PointsCard key={record.id} record={record} />)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -267,22 +478,25 @@ function RecordPage() {
|
|||||||
open={Boolean(selectedOrder)}
|
open={Boolean(selectedOrder)}
|
||||||
title="Order Details"
|
title="Order Details"
|
||||||
onClose={handleCloseDetails}
|
onClose={handleCloseDetails}
|
||||||
className="max-w-[380px]"
|
className="max-w-[420px]"
|
||||||
bodyClassName="pt-[0px]"
|
bodyClassName="pt-[0px]"
|
||||||
footer={
|
footer={
|
||||||
<button type="button" className="button-play h-[36px] min-w-[94px]" onClick={handleCloseDetails}>
|
<Button type="button" className="h-[36px] w-full sm:min-w-[94px] sm:w-auto" onClick={handleCloseDetails}>
|
||||||
Close
|
Close
|
||||||
</button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{selectedOrder ? (
|
{selectedOrder ? (
|
||||||
<div className="rounded-[8px] bg-[#1C1818]/78 px-[12px] py-[6px]">
|
<div className="rounded-[10px] bg-[#1C1818]/78 px-[12px] py-[6px]">
|
||||||
{[
|
{[
|
||||||
{ label: 'Order Number', value: `ORD${selectedOrder.date.replaceAll('-', '')}${selectedOrder.time.replace(':', '')}001` },
|
{ label: 'Order Number', value: selectedOrder.orderNumber ?? '--' },
|
||||||
{ label: 'Order Time', value: `${selectedOrder.date} ${selectedOrder.time}` },
|
{ label: 'Order Time', value: `${selectedOrder.date} ${selectedOrder.time}` },
|
||||||
{ label: 'Order Type', value: selectedOrder.category },
|
{ label: 'Order Type', value: selectedOrder.category },
|
||||||
{ label: 'Item Name', value: selectedOrder.title },
|
{ label: 'Item Name', value: selectedOrder.title },
|
||||||
{ label: 'Points', value: selectedOrder.points.replace('-', '') },
|
...(selectedOrder.trackingNumber
|
||||||
|
? [{ label: 'Tracking Number', value: selectedOrder.trackingNumber }]
|
||||||
|
: []),
|
||||||
|
{ label: 'Points', value: selectedOrder.points.replace(/^-/, '') },
|
||||||
{ label: 'Status', value: selectedOrder.status },
|
{ label: 'Status', value: selectedOrder.status },
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
<div key={item.label} className="border-b border-white/8 py-[10px] last:border-b-0">
|
<div key={item.label} className="border-b border-white/8 py-[10px] last:border-b-0">
|
||||||
|
|||||||
@@ -1,11 +1,50 @@
|
|||||||
import { defineConfig } from 'vite'
|
import {defineConfig} from 'vite'
|
||||||
import react, { reactCompilerPreset } from '@vitejs/plugin-react'
|
import react, {reactCompilerPreset} from '@vitejs/plugin-react'
|
||||||
import babel from '@rolldown/plugin-babel'
|
import babel from '@rolldown/plugin-babel'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
import { fileURLToPath, URL } from 'node:url'
|
import {fileURLToPath, URL} from 'node:url'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
base: './',
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks(id) {
|
||||||
|
if (!id.includes('node_modules')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id.includes('/react-router-dom/')) {
|
||||||
|
return 'router'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id.includes('/@tanstack/react-query/') || id.includes('/zustand/') || id.includes('/ky/')) {
|
||||||
|
return 'data'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id.includes('/lucide-react/')) {
|
||||||
|
return 'icons'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id.includes('/react/') || id.includes('/react-dom/')) {
|
||||||
|
return 'react'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'vendor'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'https://playx-api.cjdhr.top',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
@@ -14,6 +53,6 @@ export default defineConfig({
|
|||||||
plugins: [
|
plugins: [
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
react(),
|
react(),
|
||||||
babel({ presets: [reactCompilerPreset()] })
|
babel({presets: [reactCompilerPreset()]})
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||