From 5dd4e31db4438743b3b4f3d55f362d51a4e5e90e Mon Sep 17 00:00:00 2001 From: JiaJun <2394389886@qq.com> Date: Sat, 16 May 2026 09:03:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E9=9B=86=E6=88=90=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E6=8E=88=E6=9D=83=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96API=E5=AE=A2=E6=88=B7=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现了完整的登录注册认证流程,包括密码验证和用户资料获取 - 集成了JWT令牌管理和自动刷新机制,支持设备ID生成和管理 - 添加了WebSocket连接配置和API基础URL环境变量设置 - 实现了API客户端的请求拦截器,包括令牌验证和错误处理逻辑 - 集成了MD5加密和认证令牌缓存机制,提升安全性 - 添加了多语言国际化支持,包括英语、中文、马来语和印尼语 - 实现了认证状态管理和本地存储持久化功能 - 添加了表单验证schema和错误处理机制,增强用户体验 --- .env.development | 5 +- .env.example | 5 +- .env.production | 5 +- package.json | 7 +- pnpm-lock.yaml | 59 ++- src/assets/game/chip1.webp | Bin 30070 -> 2914 bytes src/assets/game/chip2.webp | Bin 31030 -> 3046 bytes src/assets/game/chip3.webp | Bin 30420 -> 2840 bytes src/assets/game/chip4.webp | Bin 31054 -> 3084 bytes src/assets/game/chip5.webp | Bin 32298 -> 3102 bytes src/assets/game/chip6.webp | Bin 25276 -> 2564 bytes src/components/center-modal.tsx | 10 +- src/components/language-link.tsx | 8 +- src/components/ui/toaster.tsx | 71 +++ src/constants/index.ts | 54 +- src/features/auth/api/auth-api.ts | 179 +++++++ src/features/auth/api/types.ts | 140 ++++++ .../components/desktop-auth-form-parts.tsx | 88 ++++ .../components/desktop-login-form-view.tsx | 107 ++++ .../auth/components/desktop-login-form.tsx | 37 ++ .../components/desktop-register-form-view.tsx | 149 ++++++ .../auth/components/desktop-register-form.tsx | 51 ++ src/features/auth/hooks/auth-error-key.ts | 54 ++ src/features/auth/hooks/use-auth.ts | 11 + src/features/auth/hooks/use-login-form.ts | 47 ++ src/features/auth/hooks/use-register-form.ts | 56 +++ src/features/auth/hooks/zod-form-resolver.ts | 66 +++ src/features/auth/schema/auth-schema.ts | 38 ++ src/features/game/api/game-api.ts | 360 +++++++++++++- src/features/game/api/types.ts | 110 +++++ .../components/desktop/desktop-animal.tsx | 231 ++++++++- .../components/desktop/desktop-control.tsx | 75 ++- .../desktop/desktop-game-history.tsx | 196 ++++++-- .../components/desktop/desktop-header.tsx | 406 ++++++++++++--- .../components/desktop/desktop-status.tsx | 18 +- .../game/components/desktop/desktop-title.tsx | 8 +- .../game/components/desktop/desktop-topup.tsx | 6 +- .../components/desktop/desktop-withdraw.tsx | 135 +++-- src/features/game/entry/entry-page.tsx | 106 +++- src/features/game/entry/mobile-entry.tsx | 6 +- src/features/game/entry/pc-entry.tsx | 24 +- .../game/hooks/use-game-control-vm.ts | 48 +- .../game/hooks/use-game-history-vm.ts | 84 +++- .../game/hooks/use-game-realtime-sync.ts | 465 ++++++++++++++++++ src/features/game/hooks/use-game-status-vm.ts | 44 +- .../desktop/desktop-auto-setting-modal.tsx | 25 +- .../modal/desktop/desktop-login-modal.tsx | 96 +--- .../modal/desktop/desktop-notice-modal.tsx | 23 +- .../desktop/desktop-procedures-modal.tsx | 34 +- .../modal/desktop/desktop-register-modal.tsx | 122 +---- .../modal/desktop/desktop-userInfo-modal.tsx | 42 +- .../desktop/desktop-withdraw-topup-modal.tsx | 18 +- src/features/game/shared/constants.ts | 2 +- src/features/game/shared/mock-data.ts | 44 +- src/features/game/shared/types.ts | 1 + src/i18n/index.ts | 32 +- src/lib/api/api-client.ts | 171 ++++++- src/lib/api/api-error.ts | 7 +- src/lib/api/types.ts | 6 - src/lib/auth/auth-session.ts | 10 + src/lib/crypto/md5.ts | 5 + src/lib/notify.ts | 113 +++++ src/lib/utils.ts | 103 ++++ src/lib/ws/game-socket-client.ts | 297 +++++++++++ src/locales/en-US/common.ts | 285 +++++++++++ src/locales/id-ID/common.ts | 415 ++++++++++++++++ src/locales/ms-MY/common.ts | 418 ++++++++++++++++ src/locales/zh-CN/common.ts | 279 +++++++++++ src/main.tsx | 24 +- src/routeTree.gen.ts | 24 +- src/routes/$lang/ws-test.tsx | 120 +++++ src/store/auth/auth-store.ts | 128 ++++- src/store/game/game-round-store.ts | 31 +- src/store/game/game-session-store.ts | 15 +- src/store/index.ts | 1 + src/store/modal/index.ts | 1 + src/store/modal/modal-store.ts | 60 +++ src/style/index.css | 168 +++++++ src/type/index.ts | 17 + src/vite-env.d.ts | 1 + vite.config.ts | 6 + 81 files changed, 6086 insertions(+), 627 deletions(-) create mode 100644 src/components/ui/toaster.tsx create mode 100644 src/features/auth/api/auth-api.ts create mode 100644 src/features/auth/api/types.ts create mode 100644 src/features/auth/components/desktop-auth-form-parts.tsx create mode 100644 src/features/auth/components/desktop-login-form-view.tsx create mode 100644 src/features/auth/components/desktop-login-form.tsx create mode 100644 src/features/auth/components/desktop-register-form-view.tsx create mode 100644 src/features/auth/components/desktop-register-form.tsx create mode 100644 src/features/auth/hooks/auth-error-key.ts create mode 100644 src/features/auth/hooks/use-auth.ts create mode 100644 src/features/auth/hooks/use-login-form.ts create mode 100644 src/features/auth/hooks/use-register-form.ts create mode 100644 src/features/auth/hooks/zod-form-resolver.ts create mode 100644 src/features/auth/schema/auth-schema.ts create mode 100644 src/features/game/hooks/use-game-realtime-sync.ts delete mode 100644 src/lib/api/types.ts create mode 100644 src/lib/crypto/md5.ts create mode 100644 src/lib/notify.ts create mode 100644 src/lib/ws/game-socket-client.ts create mode 100644 src/locales/id-ID/common.ts create mode 100644 src/locales/ms-MY/common.ts create mode 100644 src/routes/$lang/ws-test.tsx create mode 100644 src/store/modal/index.ts create mode 100644 src/store/modal/modal-store.ts create mode 100644 src/type/index.ts diff --git a/.env.development b/.env.development index 49980cb..7fe86fb 100644 --- a/.env.development +++ b/.env.development @@ -1,4 +1,7 @@ VITE_APP_ENV=development -VITE_API_BASE_URL=http://localhost:3000 +VITE_API_BASE_URL=/ +VITE_WEBSOCKET_URL=wss://zihua-api.h55555game.top/ws/ VITE_ENABLE_QUERY_DEVTOOLS=true VITE_ENABLE_REQUEST_LOG=true +# 客户端密钥 +VITE_AUTH_TOKEN_SECRET=564d14asdasd113e46542asd6das1a2a diff --git a/.env.example b/.env.example index 49980cb..44fef08 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,7 @@ VITE_APP_ENV=development -VITE_API_BASE_URL=http://localhost:3000 +VITE_API_BASE_URL=https://zihua-api.h55555game.top/ +VITE_WEBSOCKET_URL=wss://zihua-api.h55555game.top/ws/ VITE_ENABLE_QUERY_DEVTOOLS=true VITE_ENABLE_REQUEST_LOG=true +# 客户端密钥 +VITE_AUTH_TOKEN_SECRET=564d14asdasd113e46542asd6das1a2a diff --git a/.env.production b/.env.production index 32fb8d6..52b64b0 100644 --- a/.env.production +++ b/.env.production @@ -1,4 +1,7 @@ VITE_APP_ENV=production -VITE_API_BASE_URL=https://api.example.com +VITE_API_BASE_URL=https://zihua-api.h55555game.top/ +VITE_WEBSOCKET_URL=wss://zihua-api.h55555game.top/ws/ VITE_ENABLE_QUERY_DEVTOOLS=false VITE_ENABLE_REQUEST_LOG=false +# 客户端密钥 +VITE_AUTH_TOKEN_SECRET=564d14asdasd113e46542asd6das1a2a diff --git a/package.json b/package.json index b9f4788..6ffcc25 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "@tanstack/react-query": "^5.99.0", "@tanstack/react-query-devtools": "^5.99.0", "@tanstack/react-router": "^1.168.22", + "@tanstack/react-virtual": "^3.13.24", + "@types/md5": "^2.3.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dayjs": "^1.11.20", @@ -37,10 +39,11 @@ "ky": "^2.0.1", "lottie-web": "^5.13.0", "lucide-react": "^1.9.0", + "md5": "^2.3.0", "motion": "^12.38.0", "radix-ui": "^1.4.3", - "react": "^19.2.4", - "react-dom": "^19.2.4", + "react": "19.2.5", + "react-dom": "19.2.5", "react-hook-form": "^7.75.0", "react-i18next": "^17.0.3", "shadcn": "^4.7.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30f73a8..c9374d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,12 @@ importers: '@tanstack/react-router': specifier: ^1.168.22 version: 1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tanstack/react-virtual': + specifier: ^3.13.24 + version: 3.13.24(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@types/md5': + specifier: ^2.3.6 + version: 2.3.6 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -44,6 +50,9 @@ importers: lucide-react: specifier: ^1.9.0 version: 1.9.0(react@19.2.5) + md5: + specifier: ^2.3.0 + version: 2.3.0 motion: specifier: ^12.38.0 version: 12.38.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -51,10 +60,10 @@ importers: specifier: ^1.4.3 version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react: - specifier: ^19.2.4 + specifier: 19.2.5 version: 19.2.5 react-dom: - specifier: ^19.2.4 + specifier: 19.2.5 version: 19.2.5(react@19.2.5) react-hook-form: specifier: ^7.75.0 @@ -1686,6 +1695,12 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/react-virtual@3.13.24': + resolution: {integrity: sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/router-cli@1.166.33': resolution: {integrity: sha512-gCWBbCVkfT2OzgxQVV275BjRYKvfh7SEKD73ATHWyLE8ifm8/O2700roObVHUy+Y0jJT91Am0UkjsES0O2jqzw==} engines: {node: '>=20.19'} @@ -1729,6 +1744,9 @@ packages: '@tanstack/store@0.9.3': resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==} + '@tanstack/virtual-core@3.14.0': + resolution: {integrity: sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==} + '@tanstack/virtual-file-routes@1.161.7': resolution: {integrity: sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ==} engines: {node: '>=20.19'} @@ -1752,6 +1770,9 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/md5@2.3.6': + resolution: {integrity: sha512-WD69gNXtRBnpknfZcb4TRQ0XJQbUPZcai/Qdhmka3sxUR3Et8NrXoeAoknG/LghYHTf4ve795rInVYHBTQdNVA==} + '@types/node@24.12.2': resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==} @@ -1956,6 +1977,9 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -2104,6 +2128,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -2604,6 +2631,9 @@ packages: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2902,6 +2932,9 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + md5@2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -5317,6 +5350,12 @@ snapshots: react-dom: 19.2.5(react@19.2.5) use-sync-external-store: 1.6.0(react@19.2.5) + '@tanstack/react-virtual@3.13.24(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@tanstack/virtual-core': 3.14.0 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + '@tanstack/router-cli@1.166.33': dependencies: '@tanstack/router-generator': 1.166.32 @@ -5382,6 +5421,8 @@ snapshots: '@tanstack/store@0.9.3': {} + '@tanstack/virtual-core@3.14.0': {} + '@tanstack/virtual-file-routes@1.161.7': {} '@ts-morph/common@0.27.0': @@ -5416,6 +5457,8 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@types/md5@2.3.6': {} + '@types/node@24.12.2': dependencies: undici-types: 7.16.0 @@ -5613,6 +5656,8 @@ snapshots: chardet@0.7.0: {} + charenc@0.0.2: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -5761,6 +5806,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypt@0.0.2: {} + cssesc@3.0.0: {} csstype@3.2.3: {} @@ -6295,6 +6342,8 @@ snapshots: dependencies: binary-extensions: 2.3.0 + is-buffer@1.1.6: {} + is-docker@3.0.0: {} is-extglob@2.1.1: {} @@ -6510,6 +6559,12 @@ snapshots: math-intrinsics@1.1.0: {} + md5@2.3.0: + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + media-typer@1.1.0: {} meow@13.2.0: {} diff --git a/src/assets/game/chip1.webp b/src/assets/game/chip1.webp index 081eee468ade50069cc7fe453fa0b3d21b4a25ec..9d6e83b0fa43e2b0f07416e1a4774328cb2b19da 100644 GIT binary patch literal 2914 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 MQd*5n6951J0Iz}CJ^%m! literal 30070 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 literal 31030 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 DIZp2& diff --git a/src/assets/game/chip4.webp b/src/assets/game/chip4.webp index 9967c80af5c01405d61915d1865acf636283b3e8..e7953c26b3993b7fdede9442fbc5aa3dc8a6797a 100644 GIT binary patch literal 3084 zcmV+n4D<6+Nk&El3;+OEMM6+kP&il$0000G0000&002P%06|PpNXG#H00EFKNw#TQ zeSEHM+fTM_+qP}nwr$(CZQI5=dt5BcImY~eh?oFqO`NY*+wR@FHmg}ARR9ErsxtgjM|OQbFjNd3DZu{i{Hc|&@?PKv6n#7)xwx|zS0hzp75 zp~M<8-y*JWD=Mq?o%s3&`>E;dBi0K7RCFQkOZ>bw@v)<6o=RIJu(yX~??|Fm%vI=F zlKCoun}H;Cvx_u8#873Yy(ITZ1iJzxxwqYZQLiKZmz`v{`frx7GXTPU6h@hVDkOb4 zFjL$m;GQ^&lZfP3DrJCP9SyK#rU6c_F~Gf@26%nk06)$cfDRkr^JW7)n{9w|-3+j< zi~)wHFhDgtmX`<%#N7sJi<X12xy5O6 z02CK_&gOdysE@OQ$(4-I>DS~fgP()LL38vm-!+A;N(qJuMt{)ob5SNRjo4tXrRH46 z*x($q>eQ=Y?jEmdkqiPOWUbzz_lW5u`*o?CJ34d%09H^qAQlP$05B{7odGIC073vh zDG-K2A)yO~=C%w0LR$baLfRn}^FC*FZ|z@%`Gw4XXxR<*d->nk7xg3kFI3Mmf3W|w zzUqJNKUh7>`T&19{>9c~=qLK;>8bk{@LT&!`(^MI`!oOlG?%pR@E#KQqw?Rd9&Ozv zAH{hZ`_1_8%RX0qLwXEWBG{_r5m%`t|_+Km9xXzxWTy z&x#N3KgfPYdqn)h`=|Rq@ZXZZV86zHn*WXd;r=WBzxhx9{uuvgJ&?atSv^vWh|)cg zfB%Cme4GD!GPd1I9gkJwHrwGEj4SzgxdLOyjz9rYe^c~pl6u3f#*Wvk-ANwS^!a3{ zYMQm2B*tCs`RT*uPl1*xQtBC*#OFGW>^<6cBR8T%f$(=7eqpVB7>z7zzRI7WS*{{QiA2lDZre$;xM z<1sD1>hpe8%+a^ubGvreH(xp3f)nMlz2W<-gN=X)U#_h$g5`c}B7dK^=2<8whpj-= zEUp;VPzDrl6kA7a+!vNz$k7&y58JN!=iG<;XNcxO9H4QE6!m!1d(X|f{z7dW%TIl# z5weS!)zg6ViTrGpb^GK9(qOMSb0-L{M!r?K_|x_h$i{GNsUN4FROd(++9~h(Pp(k9pxAbfFidDwc@J2;0i7T)P?JCs5>7mBtvo= zfg~&AI7CVXth)W_-ftqI@saZQL zm{wM1&8IMHWNLYDMPWp!Um%a;^V)%mJb_9)Nz$*nWng)nRt~)pCf|7n%VASNm%X|l z+Ej~XY43Mm_*!G+-)tn^R#4wTYj|UKh`rnw3>(t_sQdrICk&-8QyFsSDK~^ng99B$ z2qNV=G`mSptTso*U!6i)iG2yESNt8JK=@aNHsS+tS~dZHn!)tAo6;5rf4_1cWyL=J z)42ic(bRU;Yt8MZryB&Px?^t*GMy*Ycla(moz*~#?7c~2Ds?+q^Zn%5k@17_PU;Qg z|DI10Es}uE7XqHy+{QS>NdN8u!#{x%IjJrXFKAuFr@{4obBlGx#Cb8(%(}9S2YhlU zYHbh-a1+epeU?kXBu0dEqf9}?T8U3I%c^h`+>%rlVnpXUA$hJ+YkQ9}& zZd===4{OFYFVs5%`1hpt%S6IXTE6+I<2_eF5y2Rm&It47>B&sJg9AONkrUerwXwsR zl}Me5WEuf2^VoMDL*qzp#LpuGFV8f@&A?wf#{aor#>eE3~NK(e?2< zO4;%#y!cy}9&iU;r5E)(bu!tT-qY{?k=jO&*Sg)j{>sZjT2XNp`1)Jwy7xEqwF#+J z-onr_XB|eMg-Hutzt8U0pEvdY;=b*k^R?%u>F~mHMF0Ue^Hx5_v(P8b#3f;bg`+Jo z+lzq549DVB!NB)tJM}W(7qA+`u9z8vA?;8Q2oI?_cr_548MF-++qcBd_j|X}fJoP? zbaDTaG+NYUQ-~n@m-_nKirRoIyzljz&C4tCPmA!f(0dWgL1i}?Y_Tmq3U^{-hAf_L zJ7bjk>vl#DSsgU1k@KF9nZ0l0_@aU4Fxs>EiCmo47n;IpgS}r)vQY-dP|mTmfApr( zhtLFh=M&nT=<;xwW`iZR==IDh2p7W~SJe{8y+*wVHw#Z*T6R^V&f++|Cg9{auH(zMdHuQPs$E6}hf=)C-pD$&b5EMHhZPm3yX zDKeG7FVL=?VsiV}wI@IAWxk2b(-_nNKlpnO7_cHi0N9ax`a?eV%B5AcHZzJDYyOw%I@s#g4RFMu z2lp^QZ7<8}6eG4E63%AS%#+biekFh3B|ZmG9H>LUF?x9a>YV>o8=0B8otlh|Y`-wk zVvGN(P4dg?wWiFsn&MHvmhuih7Oe=QDnZ-SSGIR`5g?8(hd6?&YE zY$={X*`YNC-S6lyGacwj-kcXaIDJ5!CuQSQ9WAy}+fhe2&m19kN%~4*~g0@3;8oRN9Y|yjgP=Xra}~$=O3S){KScBT->Uu_UL!t zPqvugNc%3Cl0Q64cqRrlp5FlY8iGTi;&+oI!Lp(y95;#It(x^bYv^Vdc5C77J zJXL+Hsv}ozv&a7``i#dk;29h+#sO6VlQP>@3D&8kKmrO-9EJ&XB|(F7S`^LLz#a(o zN?4lS@xA|m59or;O{D3kKhO3)e{K7_i;Vkrc~tj* z#4F9TZ~h8`>(=8^oXma+t@BX+kK0a^WHPi9^GF4wb_K*vTD>5{U@f&aqjUYKnM-1e zojdxz2u0r1f*#p+RPNfg6c#(n@jFqd1hww2sm(60Vbfo1r8x}Nz>O7M#$RKPn}wGY z5+lv|fBxz6F#4*;8+Oq%<5J?g@*YHQXvJ=-2^d#QgV`u>xgtC2BIE{PU75YIu6fFm zLfGb{7;#XK@U|TX2s1hkXQ7ZB)CZ#d{vjRZ`%bfLfs*_}Bmvw&PZ9_l;G7tL=xTY% aOie?X&Zvn{2r*YW1t|w!Q~&?~0000&eJ>mU literal 31054 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 literal 32298 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 QhgTm`_?H0LA8!o+05EDROaK4? diff --git a/src/assets/game/chip6.webp b/src/assets/game/chip6.webp index 444456c5b601bb5968621c3529d77f3ed2709198..aa773b0032e85e00ce1030d7558d7c3738b61f65 100644 GIT binary patch literal 2564 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≪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 { onClose?.() } @@ -63,7 +65,11 @@ export function CenterModal({ , + info: , + loading: ( + + ), + success: , + warning: , +} as const + +const TOAST_TONE_CLASS_BY_TYPE = { + error: 'game-toast-error', + info: 'game-toast-info', + loading: 'game-toast-loading', + success: 'game-toast-success', + warning: 'game-toast-warning', +} as const + +export function AppToaster() { + const toasts = useNotificationStore((state) => state.toasts) + + return ( +
+ {toasts.map((toast) => ( +
+ + +
+
{toast.message}
+ {toast.description ? ( +
{toast.description}
+ ) : null} +
+ + +
+ ))} +
+ ) +} diff --git a/src/constants/index.ts b/src/constants/index.ts index e9a32e5..f8c3c62 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -22,6 +22,9 @@ export const APP_DEFAULT_DESCRIPTION = /** @description 认证状态持久化到浏览器时使用的存储键。 */ export const AUTH_STORAGE_KEY = 'auth-session' +/** @description 应用偏好持久化到浏览器时使用的存储键。 */ +export const APP_PREFERENCES_STORAGE_KEY = 'app-preferences' + /** @description 接口请求的默认超时时间,单位为毫秒。 */ export const DEFAULT_REQUEST_TIMEOUT_MS = 15_000 @@ -48,23 +51,48 @@ export const QUERY_RETRYABLE_STATUS_CODES = [ 408, 429, 500, 502, 503, 504, ] as const -/** @description 国际化语言设置持久化到浏览器时使用的存储键。 */ -export const I18N_LANGUAGE_STORAGE_KEY = 'app-language' - /** @description 桌面端布局切换起始断点。 */ export const DESKTOP_LAYOUT_MIN_WIDTH_PX = 1024 -export const CHIP_OPTIONS = [ - { id: 'chip-1', value: 1, src: chip1 }, - { id: 'chip-2', value: 5, src: chip2 }, - { id: 'chip-3', value: 10, src: chip3 }, - { id: 'chip-4', value: 25, src: chip4 }, - { id: 'chip-5', value: 50, src: chip5 }, - { id: 'chip-6', value: 100, src: chip6 }, +export const CHIP_IMAGE_OPTIONS = [ + { id: 'chip-1', src: chip1 }, + { id: 'chip-2', src: chip2 }, + { id: 'chip-3', src: chip3 }, + { id: 'chip-4', src: chip4 }, + { id: 'chip-5', src: chip5 }, + { id: 'chip-6', src: chip6 }, ] +export const CHIP_IMAGE_MAP = new Map( + CHIP_IMAGE_OPTIONS.map((chip) => [chip.id, chip.src] as const), +) + +export const DEFAULT_CHIP_AMOUNTS = [ + { amount: 1, id: 'chip-1' }, + { amount: 5, id: 'chip-2' }, + { amount: 10, id: 'chip-3' }, + { amount: 25, id: 'chip-4' }, + { amount: 50, id: 'chip-5' }, + { amount: 100, id: 'chip-6' }, +] as const + export const ACTION_OPTIONS = [ - { id: 'clear', label: 'Clear', Icon: Trash2, bg: controlLeft }, - { id: 'repeat', label: 'Repeat', Icon: Repeat2, bg: controlMid }, - { id: 'auto-spin', label: 'Auto-Spin', Icon: Settings, bg: controlRight }, + { + id: 'clear', + labelKey: 'gameDesktop.control.actions.clear', + Icon: Trash2, + bg: controlLeft, + }, + { + id: 'repeat', + labelKey: 'gameDesktop.control.actions.repeat', + Icon: Repeat2, + bg: controlMid, + }, + { + id: 'auto-spin', + labelKey: 'gameDesktop.control.actions.auto-spin', + Icon: Settings, + bg: controlRight, + }, ] diff --git a/src/features/auth/api/auth-api.ts b/src/features/auth/api/auth-api.ts new file mode 100644 index 0000000..288dc54 --- /dev/null +++ b/src/features/auth/api/auth-api.ts @@ -0,0 +1,179 @@ +import { api } from '@/lib/api/api-client' +import { ApiError } from '@/lib/api/api-error' +import type { AuthSessionInput } from '@/store/auth' +import { getAuthDeviceId } from '@/store/auth' +import type { ApiResponse } from '@/type' +import type { + AuthSessionDto, + AuthUserProfileDto, + LoginPayload, + LoginRequestDto, + RefreshTokenDto, + RefreshTokenRequestDto, + RegisterPayload, + RegisterRequestDto, +} from './types' +import { + mergeAuthUsers, + normalizeAuthSession, + normalizeAuthUserProfile, + normalizeRefreshAuthSession, +} from './types' + +const AUTH_ENDPOINTS = { + login: 'api/user/login', + profile: 'api/user/profile', + refreshToken: 'api/user/refreshToken', + register: 'api/user/register', +} as const + +const shouldLogAuthLifecycle = + import.meta.env.VITE_ENABLE_REQUEST_LOG === 'true' + +function unwrapEnvelope( + response: ApiResponse, + fallbackErrorKey = 'auth.errors.requestFailed', +) { + if (response.code === 1) { + return response.data + } + + throw new ApiError({ + data: response, + message: fallbackErrorKey, + }) +} + +function logAuthSessionExpiry(action: string, session: AuthSessionInput) { + if (!shouldLogAuthLifecycle || !session.accessTokenExpiresAt) { + return + } + + console.info( + `[auth] ${action} user-token expires at ${new Date( + session.accessTokenExpiresAt, + ).toISOString()} (${session.accessTokenExpiresAt})`, + ) +} + +async function getCurrentUserProfileByToken(userToken: string) { + const response = await api.post(AUTH_ENDPOINTS.profile, { + headers: { + Authorization: `Bearer ${userToken}`, + 'user-token': userToken, + }, + }) + + return normalizeAuthUserProfile( + unwrapEnvelope( + response as ApiResponse, + 'auth.errors.requestFailed', + ), + ) +} + +async function buildEnrichedAuthSession(dto: AuthSessionDto) { + const session = normalizeAuthSession(dto) + + try { + const profileUser = await getCurrentUserProfileByToken(session.accessToken) + + return { + ...session, + currentUser: mergeAuthUsers(session.currentUser, profileUser), + } satisfies AuthSessionInput + } catch { + return session + } +} + +export async function loginWithPassword( + payload: LoginPayload, +): Promise { + const response = await api.post( + AUTH_ENDPOINTS.login, + { + json: { + device_id: getAuthDeviceId(), + password: payload.password, + username: payload.username, + }, + }, + ) + + const session = await buildEnrichedAuthSession( + unwrapEnvelope( + response as ApiResponse, + 'auth.login.errors.submitFailed', + ), + ) + + logAuthSessionExpiry('login', session) + + return session +} + +export async function registerWithPassword( + payload: RegisterPayload, +): Promise { + const response = await api.post( + AUTH_ENDPOINTS.register, + { + json: { + device_id: getAuthDeviceId(), + invite_code: payload.inviteCode, + password: payload.password, + username: payload.username, + }, + }, + ) + + const session = await buildEnrichedAuthSession( + unwrapEnvelope( + response as ApiResponse, + 'auth.register.errors.submitFailed', + ), + ) + + logAuthSessionExpiry('register', session) + + return session +} + +export async function getCurrentUserProfile() { + const response = await api.post(AUTH_ENDPOINTS.profile) + + return normalizeAuthUserProfile( + unwrapEnvelope( + response as ApiResponse, + 'auth.errors.requestFailed', + ), + ) +} + +export async function refreshAuthSession( + refreshToken: string, +): Promise { + const response = await api.post( + AUTH_ENDPOINTS.refreshToken, + { + context: { + skipAuthRefresh: true, + }, + json: { + refresh_token: refreshToken, + }, + }, + ) + + const session = normalizeRefreshAuthSession( + unwrapEnvelope( + response as ApiResponse, + 'auth.errors.requestFailed', + ), + ) + + logAuthSessionExpiry('refresh', session) + + return session +} diff --git a/src/features/auth/api/types.ts b/src/features/auth/api/types.ts new file mode 100644 index 0000000..e7a13ec --- /dev/null +++ b/src/features/auth/api/types.ts @@ -0,0 +1,140 @@ +import type { AuthSessionInput, AuthUser } from '@/store/auth' + +export interface AuthApiEnvelope { + code: number + data: T + message?: string + msg?: string +} + +export interface AuthTokenDto { + auth_token: string + expires_in: number + server_time: number +} + +export interface AuthUserDto { + channel_id: number + coin: string + phone?: string + risk_flags: number + username: string + uuid: string +} + +export interface AuthSessionDto { + expires_in: number + refresh_token?: string | null + user: AuthUserDto + 'user-token': string +} + +export interface RefreshTokenDto { + expires_in: number + refresh_token?: string | null + 'user-token': string +} + +export interface AuthUserProfileDto { + channel_id: number + coin: string + create_time: number + current_streak: number + email: string + head_image: string + last_bet_period_no: string + phone: string + register_invite_code: string + risk_flags: number + username: string + uuid: string +} + +export interface LoginRequestDto { + device_id?: string + password: string + username: string +} + +export interface RegisterRequestDto extends LoginRequestDto { + invite_code: string +} + +export interface RefreshTokenRequestDto { + refresh_token: string +} + +export interface LoginPayload { + password: string + username: string +} + +export interface RegisterPayload extends LoginPayload { + inviteCode: string +} + +export function normalizeAuthUser(dto: AuthUserDto): AuthUser { + return { + channelId: dto.channel_id, + coin: dto.coin, + id: dto.uuid, + name: dto.username, + phone: dto.phone, + riskFlags: dto.risk_flags, + username: dto.username, + uuid: dto.uuid, + } +} + +export function normalizeAuthUserProfile(dto: AuthUserProfileDto): AuthUser { + return { + channelId: dto.channel_id, + coin: dto.coin, + createTime: dto.create_time, + currentStreak: dto.current_streak, + email: dto.email, + headImage: dto.head_image, + id: dto.uuid, + lastBetPeriodNo: dto.last_bet_period_no, + name: dto.username, + phone: dto.phone, + registerInviteCode: dto.register_invite_code, + riskFlags: dto.risk_flags, + username: dto.username, + uuid: dto.uuid, + } +} + +export function mergeAuthUsers( + baseUser: AuthUser | null | undefined, + profileUser: AuthUser | null | undefined, +): AuthUser | null { + if (!baseUser && !profileUser) { + return null + } + + return { + ...baseUser, + ...profileUser, + id: profileUser?.id ?? baseUser?.id ?? '', + } +} + +export function normalizeAuthSession(dto: AuthSessionDto): AuthSessionInput { + return { + accessToken: dto['user-token'], + accessTokenExpiresAt: Date.now() + dto.expires_in * 1000, + currentUser: normalizeAuthUser(dto.user), + refreshToken: dto.refresh_token ?? null, + } +} + +export function normalizeRefreshAuthSession( + dto: RefreshTokenDto, +): AuthSessionInput { + return { + accessToken: dto['user-token'], + accessTokenExpiresAt: Date.now() + dto.expires_in * 1000, + refreshToken: dto.refresh_token ?? null, + } +} diff --git a/src/features/auth/components/desktop-auth-form-parts.tsx b/src/features/auth/components/desktop-auth-form-parts.tsx new file mode 100644 index 0000000..e309399 --- /dev/null +++ b/src/features/auth/components/desktop-auth-form-parts.tsx @@ -0,0 +1,88 @@ +import type { ReactNode } from 'react' +import { useTranslation } from 'react-i18next' +import rightImg from '@/assets/system/right.webp' +import { SmartImage } from '@/components/smart-image.tsx' +import { cn } from '@/lib/utils' + +export function DesktopAuthFieldRow({ + label, + children, + labelClassName, +}: { + children: ReactNode + label: string + labelClassName?: string +}) { + return ( +
+
+
+ {label} +
+
{children}
+
+
+ ) +} + +export function DesktopAuthInputError({ message }: { message?: string }) { + if (!message) { + return null + } + + return ( +
{message}
+ ) +} + +export function DesktopAuthFooterLinks({ + primaryLabel, + secondaryLabel, +}: { + primaryLabel: string + secondaryLabel: string +}) { + const { t } = useTranslation() + + return ( +
+ {[primaryLabel, secondaryLabel].map((label) => ( +
+
+ +
+
{label}
+
+ ))} +
+ ) +} + +export function DesktopAuthSubmitError({ + message, +}: { + message?: string | null +}) { + if (!message) { + return null + } + + return ( +
+ {message} +
+ ) +} diff --git a/src/features/auth/components/desktop-login-form-view.tsx b/src/features/auth/components/desktop-login-form-view.tsx new file mode 100644 index 0000000..890d366 --- /dev/null +++ b/src/features/auth/components/desktop-login-form-view.tsx @@ -0,0 +1,107 @@ +import { motion } from 'motion/react' +import { useTranslation } from 'react-i18next' +import loginBg from '@/assets/system/login-bg.webp' +import { SmartBackground } from '@/components/smart-background.tsx' +import { Input } from '@/components/ui/input.tsx' +import { + DesktopAuthFieldRow, + DesktopAuthFooterLinks, + DesktopAuthInputError, + DesktopAuthSubmitError, +} from './desktop-auth-form-parts' + +interface DesktopLoginFormViewProps { + errors: { + password?: string + username?: string + } + isSubmitting: boolean + onPasswordChange: (value: string) => void + onSubmit: () => void + onUsernameChange: (value: string) => void + password: string + submitError?: string | null + username: string +} + +export function DesktopLoginFormView({ + errors, + isSubmitting, + onPasswordChange, + onSubmit, + onUsernameChange, + password, + submitError, + username, +}: DesktopLoginFormViewProps) { + const { t } = useTranslation() + + return ( +
{ + event.preventDefault() + onSubmit() + }} + className={ + 'flex flex-col items-center justify-between gap-design-20 px-design-20' + } + > +
+ + onUsernameChange(event.target.value)} + placeholder={t('auth.login.fields.username.placeholder')} + aria-invalid={Boolean(errors.username)} + className={'h-design-58 text-left'} + /> + + + + + onPasswordChange(event.target.value)} + placeholder={t('auth.login.fields.password.placeholder')} + aria-invalid={Boolean(errors.password)} + className={'h-design-58 text-left'} + /> + + + + + +
+ + + {isSubmitting + ? t('auth.common.actions.submitting') + : t('auth.login.actions.submit')} + +
+ ) +} diff --git a/src/features/auth/components/desktop-login-form.tsx b/src/features/auth/components/desktop-login-form.tsx new file mode 100644 index 0000000..4c41b6a --- /dev/null +++ b/src/features/auth/components/desktop-login-form.tsx @@ -0,0 +1,37 @@ +import { useController } from 'react-hook-form' +import { useLoginForm } from '../hooks/use-login-form' +import { DesktopLoginFormView } from './desktop-login-form-view' + +interface DesktopLoginFormProps { + onSuccess?: () => void +} + +export function DesktopLoginForm({ onSuccess }: DesktopLoginFormProps) { + const { form, isSubmitting, onSubmit, submitError } = useLoginForm({ + onSuccess, + }) + const usernameField = useController({ + control: form.control, + name: 'username', + }) + const passwordField = useController({ + control: form.control, + name: 'password', + }) + + return ( + + ) +} diff --git a/src/features/auth/components/desktop-register-form-view.tsx b/src/features/auth/components/desktop-register-form-view.tsx new file mode 100644 index 0000000..d00091b --- /dev/null +++ b/src/features/auth/components/desktop-register-form-view.tsx @@ -0,0 +1,149 @@ +import { motion } from 'motion/react' +import { useTranslation } from 'react-i18next' +import loginBg from '@/assets/system/login-bg.webp' +import { SmartBackground } from '@/components/smart-background.tsx' +import { Input } from '@/components/ui/input.tsx' +import { + DesktopAuthFieldRow, + DesktopAuthFooterLinks, + DesktopAuthInputError, + DesktopAuthSubmitError, +} from './desktop-auth-form-parts' + +interface DesktopRegisterFormViewProps { + errors: { + confirmPassword?: string + inviteCode?: string + password?: string + username?: string + } + inviteCode: string + isSubmitting: boolean + onConfirmPasswordChange: (value: string) => void + onInviteCodeChange: (value: string) => void + onPasswordChange: (value: string) => void + onSubmit: () => void + onUsernameChange: (value: string) => void + password: string + confirmPassword: string + submitError?: string | null + username: string +} + +export function DesktopRegisterFormView({ + confirmPassword, + errors, + inviteCode, + isSubmitting, + onConfirmPasswordChange, + onInviteCodeChange, + onPasswordChange, + onSubmit, + onUsernameChange, + password, + submitError, + username, +}: DesktopRegisterFormViewProps) { + const { t } = useTranslation() + + return ( +
{ + event.preventDefault() + onSubmit() + }} + className={'flex flex-col items-center justify-between px-design-20'} + > +
+ + onUsernameChange(event.target.value)} + placeholder={t('auth.register.fields.username.placeholder')} + aria-invalid={Boolean(errors.username)} + className={'h-design-58 text-left'} + /> + + + + + onPasswordChange(event.target.value)} + placeholder={t('auth.register.fields.password.placeholder')} + aria-invalid={Boolean(errors.password)} + className={'h-design-58 text-left'} + /> + + + + + onConfirmPasswordChange(event.target.value)} + placeholder={t('auth.register.fields.confirmPassword.placeholder')} + aria-invalid={Boolean(errors.confirmPassword)} + className={'h-design-58 text-left'} + /> + + + + + onInviteCodeChange(event.target.value)} + placeholder={t('auth.register.fields.inviteCode.placeholder')} + aria-invalid={Boolean(errors.inviteCode)} + className={'h-design-58 max-w-design-520 text-left'} + /> + + + + + +
+ + + {isSubmitting + ? t('auth.common.actions.submitting') + : t('auth.register.actions.submit')} + +
+ ) +} diff --git a/src/features/auth/components/desktop-register-form.tsx b/src/features/auth/components/desktop-register-form.tsx new file mode 100644 index 0000000..4917574 --- /dev/null +++ b/src/features/auth/components/desktop-register-form.tsx @@ -0,0 +1,51 @@ +import { useController } from 'react-hook-form' +import { useRegisterForm } from '../hooks/use-register-form' +import { DesktopRegisterFormView } from './desktop-register-form-view' + +interface DesktopRegisterFormProps { + onSuccess?: () => void +} + +export function DesktopRegisterForm({ onSuccess }: DesktopRegisterFormProps) { + const { form, isSubmitting, onSubmit, submitError } = useRegisterForm({ + onSuccess, + }) + const usernameField = useController({ + control: form.control, + name: 'username', + }) + const passwordField = useController({ + control: form.control, + name: 'password', + }) + const confirmPasswordField = useController({ + control: form.control, + name: 'confirmPassword', + }) + const inviteCodeField = useController({ + control: form.control, + name: 'inviteCode', + }) + + return ( + + ) +} diff --git a/src/features/auth/hooks/auth-error-key.ts b/src/features/auth/hooks/auth-error-key.ts new file mode 100644 index 0000000..d0d8a44 --- /dev/null +++ b/src/features/auth/hooks/auth-error-key.ts @@ -0,0 +1,54 @@ +import { ApiError } from '@/lib/api/api-error' + +type AuthSubmitContext = 'login' | 'register' + +const AUTH_ERROR_KEY_PREFIX = 'auth.' + +function isTranslationKey(value: unknown): value is string { + return typeof value === 'string' && value.startsWith(AUTH_ERROR_KEY_PREFIX) +} + +function fallbackKeyByContext(context: AuthSubmitContext) { + return context === 'login' + ? 'auth.login.errors.submitFailed' + : 'auth.register.errors.submitFailed' +} + +export function toAuthSubmitErrorKey( + error: unknown, + context: AuthSubmitContext, +) { + if (!error) { + return null + } + + const fallbackKey = fallbackKeyByContext(context) + + if (error instanceof ApiError) { + if (isTranslationKey(error.message)) { + return error.message + } + + if (error.status === 408) { + return 'auth.errors.timeout' + } + + if (error.status === 401) { + return context === 'login' + ? 'auth.login.errors.invalidCredentials' + : 'auth.register.errors.unauthorized' + } + + if (typeof error.status === 'number' && error.status >= 500) { + return 'auth.errors.serviceUnavailable' + } + + return fallbackKey + } + + if (error instanceof Error && isTranslationKey(error.message)) { + return error.message + } + + return fallbackKey +} diff --git a/src/features/auth/hooks/use-auth.ts b/src/features/auth/hooks/use-auth.ts new file mode 100644 index 0000000..b774590 --- /dev/null +++ b/src/features/auth/hooks/use-auth.ts @@ -0,0 +1,11 @@ +import { type ModalKey, useModalStore } from '@/store' + +export function useAuth() { + const setModalOpen = useModalStore((state) => state.setModalOpen) + + const handleLogin = (modalKey: ModalKey, open: boolean) => { + setModalOpen(modalKey, open) + } + + return { handleLogin } +} diff --git a/src/features/auth/hooks/use-login-form.ts b/src/features/auth/hooks/use-login-form.ts new file mode 100644 index 0000000..a36ff2d --- /dev/null +++ b/src/features/auth/hooks/use-login-form.ts @@ -0,0 +1,47 @@ +import { useMutation } from '@tanstack/react-query' +import { useForm } from 'react-hook-form' +import i18n from '@/i18n' +import { notify } from '@/lib/notify' +import { useAuthStore } from '@/store/auth' +import { loginWithPassword } from '../api/auth-api' +import { type LoginFormValues, loginFormSchema } from '../schema/auth-schema' +import { toAuthSubmitErrorKey } from './auth-error-key' +import { createZodResolver } from './zod-form-resolver' + +interface UseLoginFormOptions { + onSuccess?: () => void +} + +export function useLoginForm({ onSuccess }: UseLoginFormOptions = {}) { + const startSession = useAuthStore((state) => state.startSession) + const form = useForm({ + defaultValues: { + password: '', + username: '', + }, + mode: 'onBlur', + resolver: createZodResolver(loginFormSchema), + }) + const mutation = useMutation({ + mutationFn: loginWithPassword, + onError: (error) => { + const errorKey = toAuthSubmitErrorKey(error, 'login') + + if (errorKey) { + notify.error(i18n.t(errorKey)) + } + }, + onSuccess: (session) => { + startSession(session) + notify.success(i18n.t('commonUi.toast.loginSuccess')) + onSuccess?.() + }, + }) + + return { + form, + isSubmitting: mutation.isPending, + onSubmit: form.handleSubmit((values) => mutation.mutateAsync(values)), + submitError: toAuthSubmitErrorKey(mutation.error, 'login'), + } +} diff --git a/src/features/auth/hooks/use-register-form.ts b/src/features/auth/hooks/use-register-form.ts new file mode 100644 index 0000000..ffc337a --- /dev/null +++ b/src/features/auth/hooks/use-register-form.ts @@ -0,0 +1,56 @@ +import { useMutation } from '@tanstack/react-query' +import { useForm } from 'react-hook-form' +import i18n from '@/i18n' +import { notify } from '@/lib/notify' +import { useAuthStore } from '@/store/auth' +import { registerWithPassword } from '../api/auth-api' +import { + type RegisterFormValues, + registerFormSchema, +} from '../schema/auth-schema' +import { toAuthSubmitErrorKey } from './auth-error-key' +import { createZodResolver } from './zod-form-resolver' + +interface UseRegisterFormOptions { + onSuccess?: () => void +} + +export function useRegisterForm({ onSuccess }: UseRegisterFormOptions = {}) { + const startSession = useAuthStore((state) => state.startSession) + const form = useForm({ + defaultValues: { + confirmPassword: '', + inviteCode: '', + password: '', + username: '', + }, + mode: 'onBlur', + resolver: createZodResolver(registerFormSchema), + }) + const mutation = useMutation({ + mutationFn: (values: RegisterFormValues) => { + const { confirmPassword: _confirmPassword, ...payload } = values + + return registerWithPassword(payload) + }, + onError: (error) => { + const errorKey = toAuthSubmitErrorKey(error, 'register') + + if (errorKey) { + notify.error(i18n.t(errorKey)) + } + }, + onSuccess: (session) => { + startSession(session) + notify.success(i18n.t('commonUi.toast.registerSuccess')) + onSuccess?.() + }, + }) + + return { + form, + isSubmitting: mutation.isPending, + onSubmit: form.handleSubmit((values) => mutation.mutateAsync(values)), + submitError: toAuthSubmitErrorKey(mutation.error, 'register'), + } +} diff --git a/src/features/auth/hooks/zod-form-resolver.ts b/src/features/auth/hooks/zod-form-resolver.ts new file mode 100644 index 0000000..f9c9776 --- /dev/null +++ b/src/features/auth/hooks/zod-form-resolver.ts @@ -0,0 +1,66 @@ +import type { + FieldErrors, + FieldValues, + Resolver, + ResolverResult, +} from 'react-hook-form' +import type { ZodType } from 'zod' + +function setNestedError( + errors: FieldErrors, + path: Array, + message: string, +) { + const [head, ...rest] = path + + if (head === undefined) { + return + } + + if (rest.length === 0) { + errors[String(head)] = { + message, + type: 'manual', + } + + return + } + + const key = String(head) + const next = (errors[key] as FieldErrors | undefined) ?? {} + errors[key] = next + setNestedError(next, rest, message) +} + +export function createZodResolver( + schema: ZodType, +): Resolver { + return async (values): Promise> => { + const result = await schema.safeParseAsync(values) + + if (result.success) { + return { + errors: {}, + values: result.data, + } satisfies ResolverResult + } + + const errors: FieldErrors = {} + + for (const issue of result.error.issues) { + setNestedError( + errors, + issue.path.filter( + (segment): segment is string | number => + typeof segment === 'string' || typeof segment === 'number', + ), + issue.message, + ) + } + + return { + errors, + values: {} as TValues, + } as ResolverResult + } +} diff --git a/src/features/auth/schema/auth-schema.ts b/src/features/auth/schema/auth-schema.ts new file mode 100644 index 0000000..baf136f --- /dev/null +++ b/src/features/auth/schema/auth-schema.ts @@ -0,0 +1,38 @@ +import { z } from 'zod' + +const mobilePhonePattern = /^1[3-9]\d{9}$/ + +const usernameSchema = z + .string() + .trim() + .min(1, 'auth.validation.username.required') + .regex(mobilePhonePattern, 'auth.validation.username.invalidPhone') + +const passwordSchema = z + .string() + .min(6, 'auth.validation.password.min') + .max(32, 'auth.validation.password.max') + +export const loginFormSchema = z.object({ + password: passwordSchema, + username: usernameSchema, +}) + +export const registerFormSchema = z + .object({ + confirmPassword: passwordSchema, + inviteCode: z + .string() + .trim() + .min(1, 'auth.validation.inviteCode.required') + .max(32, 'auth.validation.inviteCode.max'), + password: passwordSchema, + username: usernameSchema, + }) + .refine((value) => value.password === value.confirmPassword, { + message: 'auth.validation.confirmPassword.mismatch', + path: ['confirmPassword'], + }) + +export type LoginFormValues = z.infer +export type RegisterFormValues = z.infer diff --git a/src/features/game/api/game-api.ts b/src/features/game/api/game-api.ts index 8cf4e6f..42e0bb1 100644 --- a/src/features/game/api/game-api.ts +++ b/src/features/game/api/game-api.ts @@ -1,4 +1,6 @@ import { api } from '@/lib/api/api-client' +import { ApiError } from '@/lib/api/api-error' +import type { ApiResponse } from '@/type' import type { AnnouncementItem, @@ -9,10 +11,17 @@ import type { GameBootstrapSnapshot, GameCell, HistoryEntry, + RoundPhase, RoundSnapshot, TrendEntry, } from '../shared' -import { createMockGameBootstrapSnapshot } from '../shared' +import { + createMockGameBootstrapSnapshot, + DEFAULT_GAME_CHIP_COLORS, + deriveTrendEntries, + GAME_GRID_COLUMNS, + GAME_MAX_SELECTION_CELLS, +} from '../shared' import type { AnnouncementStateDto, BetSelectionDto, @@ -20,20 +29,72 @@ import type { ConnectionStateDto, DashboardStateDto, GameAnnouncementsDto, + GameBetOrdersDto, GameBootstrapDto, GameCellDto, + GameLobbyInitDto, + GameLobbyPeriodDto, + GamePeriodTickDto, GameRoundFeedDto, HistoryEntryDto, + NoticeConfirmDto, + NoticeDetailDto, + NoticeListDto, RoundSnapshotDto, TrendEntryDto, } from './types' +function unwrapGameEnvelope( + response: ApiResponse, + fallbackMessage = 'Game request failed', +) { + if (response.code === 1) { + return response.data + } + + throw new ApiError({ + data: response, + message: + typeof response.msg === 'string' && response.msg.length > 0 + ? response.msg + : fallbackMessage, + }) +} + +function assertLobbyInitDto( + dto: GameLobbyInitDto, +): asserts dto is GameLobbyInitDto { + if ( + !Number.isFinite(dto.server_time) || + !Array.isArray(dto.dictionary) || + !dto.bet_config || + !Number.isFinite(dto.bet_config.default_bet_chip_id) + ) { + throw new ApiError({ + data: dto, + message: 'Invalid game lobby init payload', + }) + } +} + export const GAME_API_ENDPOINTS = { announcements: 'game/announcements', + betMyOrders: 'api/game/betMyOrders', bootstrap: 'game/bootstrap', + lobbyInit: 'api/game/lobbyInit', + noticeConfirm: 'api/notice/noticeConfirm', + noticeDetail: 'api/notice/noticeDetail', + noticeList: 'api/notice/noticeList', roundFeed: 'game/round-feed', } as const +export interface GameLobbyInitResult { + runtimeEnabled: boolean + serverTime: number + snapshot: GameBootstrapSnapshot + userSnapshot: GameLobbyInitDto['user_snapshot'] +} + function normalizeGameCell(dto: GameCellDto) { return dto satisfies GameCell } @@ -136,6 +197,193 @@ function normalizeConnectionState(dto: ConnectionStateDto) { } satisfies ConnectionState } +function toIsoFromUnixSeconds(seconds: number) { + const timestamp = Number(seconds) + const date = new Date(timestamp * 1000) + + if (!Number.isFinite(timestamp) || Number.isNaN(date.valueOf())) { + throw new ApiError({ + data: { seconds }, + message: 'Invalid unix timestamp', + }) + } + + return date.toISOString() +} + +export function normalizeLobbyRoundPhase( + status: GameLobbyPeriodDto['status'], + runtimeEnabled: boolean, +): RoundPhase { + if (!runtimeEnabled && status === 'betting') { + return 'locked' + } + + switch (status) { + case 'betting': + return 'betting' + case 'locked': + return 'locked' + case 'settling': + return 'revealing' + case 'payouting': + case 'finished': + case 'void': + return 'settled' + default: + return 'waiting' + } +} + +function normalizeLobbyChips( + chips: Record, + defaultBetChipId: number, +) { + return Object.entries(chips) + .sort(([leftId], [rightId]) => Number(leftId) - Number(rightId)) + .map(([chipId, chipAmount], index) => { + const amount = Number(chipAmount) + + return { + amount: Number.isFinite(amount) ? amount : 0, + color: + DEFAULT_GAME_CHIP_COLORS[index % DEFAULT_GAME_CHIP_COLORS.length] ?? + DEFAULT_GAME_CHIP_COLORS[0], + id: `chip-${chipId}`, + isDefault: Number(chipId) === defaultBetChipId, + label: chipAmount, + } + }) +} + +function normalizeLobbyCells(dictionary: GameLobbyInitDto['dictionary']) { + return [...dictionary] + .sort((left, right) => left.number - right.number) + .map( + (item, index) => + ({ + column: (index % GAME_GRID_COLUMNS) + 1, + id: item.number, + label: item.name, + odds: 36, + row: Math.floor(index / GAME_GRID_COLUMNS) + 1, + }) satisfies GameCell, + ) +} + +export function normalizeLobbyRound( + lobbyInit: Pick< + GameLobbyInitDto, + 'period' | 'runtime_enabled' | 'server_time' + >, +) { + if (!lobbyInit.period) { + return { + bettingClosesAt: toIsoFromUnixSeconds(lobbyInit.server_time), + id: '', + phase: 'waiting', + revealingAt: toIsoFromUnixSeconds(lobbyInit.server_time), + settledAt: null, + startedAt: toIsoFromUnixSeconds(lobbyInit.server_time), + winningCellId: null, + } satisfies RoundSnapshot + } + + return { + bettingClosesAt: toIsoFromUnixSeconds(lobbyInit.period.lock_at), + id: lobbyInit.period.period_no, + phase: normalizeLobbyRoundPhase( + lobbyInit.period.status, + lobbyInit.runtime_enabled, + ), + revealingAt: toIsoFromUnixSeconds(lobbyInit.period.open_at), + settledAt: toIsoFromUnixSeconds(lobbyInit.period.open_at), + startedAt: toIsoFromUnixSeconds(lobbyInit.server_time), + winningCellId: null, + } satisfies RoundSnapshot +} + +export function normalizePeriodTickRound( + period: GamePeriodTickDto, + previousRound?: Pick | null, +) { + const startedAt = + previousRound?.id === period.period_no + ? previousRound.startedAt + : toIsoFromUnixSeconds(period.server_time) + const countdownSeconds = Math.max(0, period.countdown) + const betCloseSeconds = Math.max(0, period.bet_close_in) + const phase = normalizeLobbyRoundPhase(period.status, period.runtime_enabled) + const nextPhaseAt = toIsoFromUnixSeconds( + period.server_time + countdownSeconds, + ) + + return { + bettingClosesAt: toIsoFromUnixSeconds(period.server_time + betCloseSeconds), + id: period.period_no, + phase, + revealingAt: nextPhaseAt, + settledAt: nextPhaseAt, + startedAt, + winningCellId: + typeof period.result_number === 'number' ? period.result_number : null, + } satisfies RoundSnapshot +} + +export function normalizeGameLobbyInit(dto: GameLobbyInitDto) { + const baseIso = toIsoFromUnixSeconds(dto.server_time) + const template = createMockGameBootstrapSnapshot(baseIso) + const cells = normalizeLobbyCells(dto.dictionary) + const chips = normalizeLobbyChips( + dto.bet_config.chips, + dto.bet_config.default_bet_chip_id, + ) + const round = normalizeLobbyRound({ + period: null, + runtime_enabled: dto.runtime_enabled, + server_time: dto.server_time, + }) + const trends = deriveTrendEntries([]) + + return { + announcements: { + activeAnnouncementId: null, + items: [], + lastUpdatedAt: null, + } satisfies AnnouncementState, + cells, + chips: chips.length > 0 ? chips : template.chips, + connection: { + ...template.connection, + connectedAt: null, + lastError: null, + lastMessageAt: null, + latencyMs: null, + reconnectAttempt: 0, + status: 'idle', + transport: 'polling', + }, + dashboard: { + countdownMs: 0, + featuredCellId: null, + onlinePlayers: 0, + tableLimitMax: Number(dto.bet_config.max_bet_per_number) || 0, + tableLimitMin: Number(dto.bet_config.min_bet_per_number) || 0, + totalPoolAmount: 0, + updatedAt: baseIso, + } satisfies DashboardState, + history: [], + maxSelectionCount: + Number.isFinite(dto.bet_config.pick_max_number_count) && + dto.bet_config.pick_max_number_count > 0 + ? Math.min(36, Math.floor(dto.bet_config.pick_max_number_count)) + : GAME_MAX_SELECTION_CELLS, + round, + selections: [], + trends, + } satisfies GameBootstrapSnapshot +} + export function normalizeGameBootstrap(dto: GameBootstrapDto) { return { announcements: normalizeAnnouncementState(dto.announcements), @@ -144,6 +392,7 @@ export function normalizeGameBootstrap(dto: GameBootstrapDto) { connection: normalizeConnectionState(dto.connection), dashboard: normalizeDashboardState(dto.dashboard), history: dto.history.map(normalizeHistoryEntry), + maxSelectionCount: GAME_MAX_SELECTION_CELLS, round: normalizeRoundSnapshot(dto.round), selections: dto.selections.map(normalizeBetSelection), trends: dto.trends.map(normalizeTrendEntry), @@ -164,22 +413,125 @@ export function normalizeGameRoundFeed(dto: GameRoundFeedDto) { export async function getGameBootstrap() { const response = await api.get(GAME_API_ENDPOINTS.bootstrap) + const dto = unwrapGameEnvelope( + response as ApiResponse, + 'Failed to load game bootstrap', + ) - return normalizeGameBootstrap(response.data) + return normalizeGameBootstrap(dto) } export async function getGameRoundFeed() { const response = await api.get(GAME_API_ENDPOINTS.roundFeed) + const dto = unwrapGameEnvelope( + response as ApiResponse, + 'Failed to load game round feed', + ) - return normalizeGameRoundFeed(response.data) + return normalizeGameRoundFeed(dto) } export async function getGameAnnouncements() { const response = await api.get( GAME_API_ENDPOINTS.announcements, ) + const dto = unwrapGameEnvelope( + response as ApiResponse, + 'Failed to load game announcements', + ) - return normalizeAnnouncementState(response.data.announcements) + return normalizeAnnouncementState(dto.announcements) +} + +export async function getGameLobbyInit() { + const response = await api.post( + GAME_API_ENDPOINTS.lobbyInit, + ) + const dto = unwrapGameEnvelope( + response as ApiResponse, + 'Failed to load game lobby init', + ) + assertLobbyInitDto(dto) + + return { + runtimeEnabled: dto.runtime_enabled, + serverTime: dto.server_time, + snapshot: normalizeGameLobbyInit(dto), + userSnapshot: dto.user_snapshot, + } satisfies GameLobbyInitResult +} + +export async function getNoticeList(params?: { + page?: number + pageSize?: number +}) { + const response = await api.get(GAME_API_ENDPOINTS.noticeList, { + searchParams: { + page: String(params?.page ?? 1), + page_size: String(params?.pageSize ?? 20), + }, + }) + const dto = unwrapGameEnvelope( + response as ApiResponse, + 'Failed to load notice list', + ) + + return dto +} + +export async function getNoticeDetail(id: number) { + const response = await api.get( + GAME_API_ENDPOINTS.noticeDetail, + { + searchParams: { + id: String(id), + }, + }, + ) + const dto = unwrapGameEnvelope( + response as ApiResponse, + 'Failed to load notice detail', + ) + + return dto +} + +export async function confirmNotice(noticeId: number) { + const response = await api.get( + GAME_API_ENDPOINTS.noticeConfirm, + { + searchParams: { + notice_id: String(noticeId), + }, + }, + ) + const dto = unwrapGameEnvelope( + response as ApiResponse, + 'Failed to confirm notice', + ) + + return dto +} + +export async function getGameBetMyOrders(params: { + page?: number + pageSize?: number +}) { + const response = await api.post( + GAME_API_ENDPOINTS.betMyOrders, + { + json: { + page: params.page ?? 1, + page_size: params.pageSize ?? 20, + }, + }, + ) + const dto = unwrapGameEnvelope( + response as ApiResponse, + 'Failed to load bet orders', + ) + + return dto } export async function getMockGameBootstrap(latencyMs = 120) { diff --git a/src/features/game/api/types.ts b/src/features/game/api/types.ts index 270299f..831e787 100644 --- a/src/features/game/api/types.ts +++ b/src/features/game/api/types.ts @@ -123,6 +123,116 @@ export interface GameAnnouncementsDto { announcements: AnnouncementStateDto } +export interface NoticeListItemDto { + is_read: boolean + notice_id: number + notice_type: 'silent' | 'popout' + publish_time: number + title: string +} + +export interface NoticeListDto { + list: NoticeListItemDto[] +} + +export interface NoticeDetailDto { + content: string + must_confirm: boolean + notice_id: number + notice_type: 'silent' | 'popout' + publish_time: number + title: string +} + +export interface NoticeConfirmDto { + confirm_time: number + confirmed: boolean + notice_id: number +} + +export type GamePeriodStatus = + | 'betting' + | 'locked' + | 'settling' + | 'payouting' + | 'finished' + | 'void' + | (string & {}) + +export interface GameLobbyPeriodDto { + countdown: number + lock_at: number + open_at: number + period_no: string + status: GamePeriodStatus +} + +export interface GameLobbyBetConfigDto { + chips: Record + default_bet_chip_id: number + max_bet_per_number: string + min_bet_per_number: string + pick_max_number_count: number +} + +export interface GameLobbyDictionaryItemDto { + category: string + icon: string + name: string + number: number +} + +export interface GameLobbyUserSnapshotDto { + coin: string + current_streak: number + is_jackpot?: boolean + odds_factor?: number + streak_level?: number +} + +export interface GameLobbyInitDto { + bet_config: GameLobbyBetConfigDto + dictionary: GameLobbyDictionaryItemDto[] + period?: GameLobbyPeriodDto | null + runtime_enabled: boolean + server_time: number + user_snapshot: GameLobbyUserSnapshotDto +} + +export interface GamePeriodTickDto { + bet_close_in: number + countdown: number + period_id: number | null + period_no: string + result_number: number | null + runtime_enabled: boolean + server_time: number + status: GamePeriodStatus +} + +export interface GameBetOrderDto { + bet_amount: string + create_time: number + numbers: number[] + order_no: string + period_no: string + result_number: number | null + status: string + total_amount: string + win_amount: string +} + +export interface GameBetOrdersPaginationDto { + page: number + page_size: number + total: number +} + +export interface GameBetOrdersDto { + list: GameBetOrderDto[] + pagination: GameBetOrdersPaginationDto +} + export type { AnnouncementState, Chip, diff --git a/src/features/game/components/desktop/desktop-animal.tsx b/src/features/game/components/desktop/desktop-animal.tsx index b2f851b..8bd05ba 100644 --- a/src/features/game/components/desktop/desktop-animal.tsx +++ b/src/features/game/components/desktop/desktop-animal.tsx @@ -1,5 +1,11 @@ +import { useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import diamondIcon from '@/assets/system/diamond.webp' import { SmartImage } from '@/components/smart-image' +import { notify } from '@/lib/notify' import { cn } from '@/lib/utils' +import { useAuthStore, useModalStore } from '@/store' +import { useGameRoundStore, useGameSessionStore } from '@/store/game' const animalModules = import.meta.glob('../../../../assets/animal/*.webp', { eager: true, @@ -18,6 +24,37 @@ const animalImageList = Object.entries(animalModules) .filter((item) => item.id > 0) .sort((left, right) => left.id - right.id) +function getNextMarqueeId(currentId: number | null) { + if (animalImageList.length === 0) { + return null + } + + if (animalImageList.length === 1) { + return animalImageList[0]?.id ?? null + } + + let nextId = currentId + + while (nextId === currentId) { + nextId = + animalImageList[Math.floor(Math.random() * animalImageList.length)]?.id ?? + currentId + } + + return nextId +} + +function formatSelectedLog( + selectionByCell: Record, +) { + return Object.entries(selectionByCell) + .map(([cellId, value]) => ({ + 字花: String(cellId).padStart(2, '0'), + 筹码: value.amount, + })) + .sort((left, right) => Number(left.字花) - Number(right.字花)) +} + interface DesktopAnimalProps { activeId?: number | null className?: string @@ -33,40 +70,220 @@ export function DesktopAnimal({ imageClassName, onSelect, }: DesktopAnimalProps) { + const { t } = useTranslation() + const authStatus = useAuthStore((state) => state.status) + const setModalOpen = useModalStore((state) => state.setModalOpen) + const activeChipId = useGameRoundStore((state) => state.activeChipId) + const chips = useGameRoundStore((state) => state.chips) + const clearSelections = useGameRoundStore((state) => state.clearSelections) + const maxSelectionCount = useGameRoundStore( + (state) => state.maxSelectionCount, + ) + const placeBet = useGameRoundStore((state) => state.placeBet) + const removeSelectionsForCell = useGameRoundStore( + (state) => state.removeSelectionsForCell, + ) + const selections = useGameRoundStore((state) => state.selections) + const connection = useGameSessionStore((state) => state.connection) + const requestRealtimeConnection = useGameSessionStore( + (state) => state.requestRealtimeConnection, + ) + const shouldConnectRealtime = useGameSessionStore( + (state) => state.shouldConnectRealtime, + ) + const [marqueeId, setMarqueeId] = useState(() => + getNextMarqueeId(null), + ) + const activeChip = useMemo( + () => chips.find((chip) => chip.id === activeChipId) ?? chips[0] ?? null, + [activeChipId, chips], + ) + const selectionByCell = useMemo(() => { + return selections.reduce>( + (accumulator, selection) => { + const current = accumulator[selection.cellId] ?? { amount: 0, count: 0 } + + accumulator[selection.cellId] = { + amount: current.amount + selection.amount, + count: current.count + 1, + } + + return accumulator + }, + {}, + ) + }, [selections]) + + const isRealtimeConnected = connection.status === 'connected' + const isRealtimeConnecting = + shouldConnectRealtime && + (connection.status === 'connecting' || connection.status === 'reconnecting') + const showStandbyState = !shouldConnectRealtime || !isRealtimeConnected + const lockInteraction = showStandbyState + const isSelectedCell = (animalId: number) => + Boolean(selectionByCell[animalId]) + const selectedCellCount = Object.keys(selectionByCell).length + + const handleStart = () => { + if (authStatus !== 'authenticated') { + notify.warning(t('commonUi.toast.loginRequired')) + setModalOpen('desktopLogin', true) + return + } + + clearSelections() + requestRealtimeConnection() + } + + const handleSelect = (animalId: number) => { + if (showStandbyState) { + return + } + + if (onSelect) { + onSelect(animalId) + return + } + + if (isSelectedCell(animalId)) { + const nextSelectionByCell = { ...selectionByCell } + delete nextSelectionByCell[animalId] + console.log('已选', formatSelectedLog(nextSelectionByCell)) + removeSelectionsForCell(animalId) + return + } + + if (selectedCellCount >= maxSelectionCount) { + return + } + + console.log( + '已选', + formatSelectedLog({ + ...selectionByCell, + [animalId]: { + amount: activeChip?.amount ?? 0, + count: 1, + }, + }), + ) + placeBet(animalId) + } + + useEffect(() => { + if (!showStandbyState) { + setMarqueeId(null) + return + } + + setMarqueeId((currentId) => getNextMarqueeId(currentId)) + + let timerId = 0 + + const loop = () => { + setMarqueeId((currentId) => getNextMarqueeId(currentId)) + timerId = window.setTimeout(loop, 180 + Math.floor(Math.random() * 220)) + } + + timerId = window.setTimeout(loop, 220) + + return () => { + window.clearTimeout(timerId) + } + }, [showStandbyState]) + return (
{animalImageList.map((item) => { - const isActive = item.id === activeId + const selectionMeta = selectionByCell[item.id] + const hasPlacedSelection = Boolean(selectionMeta) + const isActive = item.id === activeId || hasPlacedSelection + const isMarqueeActive = showStandbyState && item.id === marqueeId return ( ) })} + + {showStandbyState ? ( + + ) : null}
) } diff --git a/src/features/game/components/desktop/desktop-control.tsx b/src/features/game/components/desktop/desktop-control.tsx index 44a7437..e7bdd3b 100644 --- a/src/features/game/components/desktop/desktop-control.tsx +++ b/src/features/game/components/desktop/desktop-control.tsx @@ -1,5 +1,6 @@ import { motion } from 'motion/react' import { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' import add from '@/assets/game/add.webp' import arrow from '@/assets/game/arrow.webp' import chipBg from '@/assets/game/chip-bg.webp' @@ -9,16 +10,18 @@ import controlBg from '@/assets/game/control-bg.png' import leftBottomBg from '@/assets/game/left-bg.webp' import reduce from '@/assets/game/reduce.webp' import totalBg from '@/assets/game/total-bg.webp' +import diamond from '@/assets/system/diamond.webp' import { SmartBackground } from '@/components/smart-background.tsx' import { SmartImage } from '@/components/smart-image.tsx' import { ACTION_OPTIONS } from '@/constants' import { useGameControlVm } from '@/features/game/hooks/use-game-control-vm.ts' import { cn } from '@/lib/utils' - export function DesktopControl() { + const { t } = useTranslation() const { canClear, chips, + maxSelectionCountLabel, onChipSelect, onClearSelections, selectedChipAmountLabel, @@ -26,7 +29,6 @@ export function DesktopControl() { selectedCountLabel, totalBetAmountLabel, } = useGameControlVm() - const [clickedId, setClickedId] = useState(null) const [hidingId, setHidingId] = useState(null) const [confirmClicked, setConfirmClicked] = useState(false) @@ -74,8 +76,8 @@ export function DesktopControl() { } >
-
TREBD
-
MAP
+
{t('gameDesktop.control.trend')}
+
{t('gameDesktop.control.map')}
+ + {chip.valueLabel} + + + {chip.valueLabel} + + + {chip.valueLabel} + ) @@ -237,11 +261,26 @@ export function DesktopControl() { src={totalBg} size="100% 100%" className={ - 'desktop-control-total relative flex flex-col items-center justify-center z-10 h-full w-design-435 shrink-0 bg-center bg-no-repeat' + 'desktop-control-total relative flex items-center justify-center text-design-20 gap-design-40 z-10 h-full w-design-435 shrink-0 bg-center bg-no-repeat' } > -
SELECTED:{selectedCountLabel}
-
Total Bet:{totalBetAmountLabel}
+
+ {t('gameDesktop.control.selected')}:{' '} + {selectedCountLabel} /{' '} + {maxSelectionCountLabel} +
+
+
{t('gameDesktop.control.totalBet')}:
+ +
+ +
{totalBetAmountLabel}
+
+
- {ACTION_OPTIONS.map(({ id, label, Icon, bg }) => { + {ACTION_OPTIONS.map(({ id, labelKey, Icon, bg }) => { const isClicked = clickedId === id const isHiding = hidingId === id const showBg = isClicked || isHiding @@ -315,7 +354,7 @@ export function DesktopControl() { className={showBg ? 'text-[#D9FEFF]' : 'text-[#37D5CB]'} />
- {label} + {t(labelKey)}
@@ -351,7 +390,7 @@ export function DesktopControl() { transition={{ duration: 0.15 }} className="relative" > - confirm + {t('gameDesktop.control.confirm')}
diff --git a/src/features/game/components/desktop/desktop-game-history.tsx b/src/features/game/components/desktop/desktop-game-history.tsx index c59562c..0bb262e 100644 --- a/src/features/game/components/desktop/desktop-game-history.tsx +++ b/src/features/game/components/desktop/desktop-game-history.tsx @@ -1,9 +1,54 @@ +import { useVirtualizer } from '@tanstack/react-virtual' +import { useEffect, useRef } from 'react' +import { useTranslation } from 'react-i18next' import historyBg from '@/assets/system/history-bg.png' import { SmartBackground } from '@/components/smart-background.tsx' import { useGameHistoryVm } from '@/features/game/hooks/use-game-history-vm.ts' export function DesktopGameHistory() { - const { emptyText, isEmpty, items } = useGameHistoryVm() + const { t } = useTranslation() + const { + emptyText, + endText, + fetchNextPage, + hasNextPage, + isEmpty, + isFetchingNextPage, + isInitialLoading, + items, + loadingText, + } = useGameHistoryVm() + const parentRef = useRef(null) + + const rowCount = hasNextPage ? items.length + 1 : items.length + const virtualizer = useVirtualizer({ + count: rowCount, + estimateSize: () => 196, + getScrollElement: () => parentRef.current, + overscan: 4, + }) + + useEffect(() => { + const virtualItems = virtualizer.getVirtualItems() + const lastItem = virtualItems[virtualItems.length - 1] + + if ( + !lastItem || + !hasNextPage || + isFetchingNextPage || + lastItem.index < items.length - 1 + ) { + return + } + + void fetchNextPage() + }, [ + fetchNextPage, + hasNextPage, + isFetchingNextPage, + items.length, + virtualizer, + ]) return ( - History + {t('gameDesktop.history.title')}
- {isEmpty ? ( + {isInitialLoading ? ( +
+ {loadingText} +
+ ) : isEmpty ? (
) : ( - items.map((item) => { - return ( -
+
+ {virtualizer.getVirtualItems().map((virtualRow) => { + const item = items[virtualRow.index] + + return (
- {item.statusLabel} + {item ? ( +
+
+ {item.statusLabel} +
+
+
+ + {t('gameDesktop.history.orderNo')}:{' '} + + + {item.orderNo} + +
+
+ + {t('gameDesktop.history.roundId')}:{' '} + + + {item.periodNo} + +
+
+ + {t('gameDesktop.history.numbers')}:{' '} + + {item.numbersLabel} +
+
+ + {t('gameDesktop.history.settledAt')}:{' '} + + {item.createdAtLabel} +
+
+ + {t('gameDesktop.history.totalPoolAmount')}:{' '} + + + {item.amountLabel} + +
+
+ + {t('gameDesktop.history.winningResult')}:{' '} + + + {item.resultNumberLabel} + +
+
+ + {t('gameDesktop.history.payout')}:{' '} + + {item.winAmountLabel} +
+
+
+ ) : ( +
+ {isFetchingNextPage ? loadingText : endText} +
+ )}
-
-
- Round ID: - {item.roundId} -
-
- Settled At: - {item.settledAtLabel} -
-
- - Total Pool Amount:{' '} - - - {item.totalPoolAmountLabel} - -
-
- Winning Result: - - {item.winningCellIdLabel} - -
-
- Payout: - {item.payoutMultiplierLabel} -
-
-
- ) - }) + ) + })} +
)}
diff --git a/src/features/game/components/desktop/desktop-header.tsx b/src/features/game/components/desktop/desktop-header.tsx index fe02dab..b483b90 100644 --- a/src/features/game/components/desktop/desktop-header.tsx +++ b/src/features/game/components/desktop/desktop-header.tsx @@ -1,10 +1,260 @@ -import { CircleAlert, Mail, Volume2 } from 'lucide-react' +import { CircleAlert, Mail, Maximize, Minimize, Volume2 } from 'lucide-react' +import { useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' import avatar from '@/assets/system/avatar.webp' +import diamond from '@/assets/system/diamond.webp' import logo from '@/assets/system/logo.webp' -import wifi from '@/assets/system/wifi.webp' import { SmartImage } from '@/components/smart-image.tsx' +import { + isDesktopFullscreen, + subscribeDesktopFullscreenChange, + toggleDesktopFullscreen, +} from '@/lib/utils' +import { useAuthStore, useGameSessionStore, useModalStore } from '@/store' + +type BrowserNetworkInformation = { + addEventListener?: (type: 'change', listener: () => void) => void + downlink?: number + effectiveType?: string + removeEventListener?: (type: 'change', listener: () => void) => void + rtt?: number +} + +type SignalPresentation = { + activeBars: number + latencyLabel: string + toneClassName: string +} + +function formatTimezoneOffset(date: Date) { + const offsetMinutes = -date.getTimezoneOffset() + const sign = offsetMinutes >= 0 ? '+' : '-' + const absoluteMinutes = Math.abs(offsetMinutes) + const hours = String(Math.floor(absoluteMinutes / 60)).padStart(2, '0') + const minutes = String(absoluteMinutes % 60).padStart(2, '0') + + return `GMT${sign}${hours}${minutes === '00' ? '' : `:${minutes}`}` +} + +function formatHeaderTime(date: Date) { + const hours = String(date.getHours()).padStart(2, '0') + const minutes = String(date.getMinutes()).padStart(2, '0') + const seconds = String(date.getSeconds()).padStart(2, '0') + + return `${hours}:${minutes}:${seconds} ${formatTimezoneOffset(date)}` +} + +function getBrowserNetworkInformation() { + if (typeof navigator === 'undefined') { + return null + } + + return (navigator as Navigator & { connection?: BrowserNetworkInformation }) + .connection +} + +function resolveSignalPresentation(input: { + isOnline: boolean + latencyMs: number | null + status: string +}) { + if (!input.isOnline || input.status === 'disconnected') { + return { + activeBars: 0, + latencyLabel: '--', + toneClassName: 'text-[#FF6B6B]', + } satisfies SignalPresentation + } + + if (input.latencyMs === null) { + return { + activeBars: input.status === 'connected' ? 2 : 1, + latencyLabel: '--', + toneClassName: 'text-[#7F8EA3]', + } satisfies SignalPresentation + } + + if (input.latencyMs <= 80) { + return { + activeBars: 4, + latencyLabel: String(input.latencyMs), + toneClassName: 'text-[#74FF69]', + } satisfies SignalPresentation + } + + if (input.latencyMs <= 150) { + return { + activeBars: 3, + latencyLabel: String(input.latencyMs), + toneClassName: 'text-[#B7FF6A]', + } satisfies SignalPresentation + } + + if (input.latencyMs <= 300) { + return { + activeBars: 2, + latencyLabel: String(input.latencyMs), + toneClassName: 'text-[#FFD76A]', + } satisfies SignalPresentation + } + + return { + activeBars: 1, + latencyLabel: String(input.latencyMs), + toneClassName: 'text-[#FF8A6A]', + } satisfies SignalPresentation +} + +function SignalBars({ + activeBars, + toneClassName, +}: { + activeBars: number + toneClassName: string +}) { + const barHeights = ['h-[6px]', 'h-[10px]', 'h-[14px]', 'h-[18px]'] as const + + return ( + diff --git a/src/features/game/modal/desktop/desktop-procedures-modal.tsx b/src/features/game/modal/desktop/desktop-procedures-modal.tsx index 10a47e1..6dc49a8 100644 --- a/src/features/game/modal/desktop/desktop-procedures-modal.tsx +++ b/src/features/game/modal/desktop/desktop-procedures-modal.tsx @@ -1,15 +1,27 @@ -import { useState } from 'react' +import { useTranslation } from 'react-i18next' import proceduresBg from '@/assets/system/procedures-bg.webp' import topupBtnBg from '@/assets/system/topup.webp' import withdrawBtnBg from '@/assets/system/withdraw.webp' import { CenterModal } from '@/components/center-modal.tsx' import { SmartBackground } from '@/components/smart-background.tsx' +import { useModalStore } from '@/store' function DesktopProceduresModal() { - const [open, setOpen] = useState(true) + const { t } = useTranslation() + const open = useModalStore((state) => state.modals.desktopProcedures) + const setModalOpen = useModalStore((state) => state.setModalOpen) + const setWithdrawTopupType = useModalStore( + (state) => state.setWithdrawTopupType, + ) function handleSubmit() { - setOpen(false) + setModalOpen('desktopProcedures', false) + } + + function handleOpenWithdrawTopup(type: 'withdraw' | 'topup') { + setModalOpen('desktopProcedures', false) + setWithdrawTopupType(type) + setModalOpen('desktopWithdrawTopup', true) } return ( @@ -18,7 +30,7 @@ function DesktopProceduresModal() { onClose={handleSubmit} title={
- Biomond Balance + {t('game.modals.procedures.title')}
} isNormalBg={true} @@ -33,23 +45,27 @@ function DesktopProceduresModal() { 'h-[95%] w-full rounded-md flex flex-col items-center justify-between' } > -
111
+
+ {t('game.modals.procedures.contentPlaceholder')} +
handleOpenWithdrawTopup('withdraw')} className={ - 'w-design-400 h-design-195 flex items-center justify-center pb-design-10 text-design-32 font-bold' + 'w-design-400 h-design-195 flex cursor-pointer items-center justify-center pb-design-10 text-design-32 font-bold' } > - 提 现 + {t('game.modals.procedures.withdraw')} handleOpenWithdrawTopup('topup')} className={ - 'w-design-400 h-design-195 flex items-center justify-center pb-design-20 text-design-32 font-bold' + 'w-design-400 h-design-195 flex cursor-pointer items-center justify-center pb-design-20 text-design-32 font-bold' } > - 充 值 + {t('game.modals.procedures.topup')}
diff --git a/src/features/game/modal/desktop/desktop-register-modal.tsx b/src/features/game/modal/desktop/desktop-register-modal.tsx index 78e9b7d..260f1dd 100644 --- a/src/features/game/modal/desktop/desktop-register-modal.tsx +++ b/src/features/game/modal/desktop/desktop-register-modal.tsx @@ -1,124 +1,30 @@ -import { motion } from 'motion/react' -import { useState } from 'react' -import loginBg from '@/assets/system/login-bg.webp' -import rightImg from '@/assets/system/right.webp' +import { useTranslation } from 'react-i18next' import { CenterModal } from '@/components/center-modal.tsx' -import { SmartBackground } from '@/components/smart-background.tsx' -import { SmartImage } from '@/components/smart-image.tsx' -import { Input } from '@/components/ui/input.tsx' +import { DesktopRegisterForm } from '@/features/auth/components/desktop-register-form' +import { useModalStore } from '@/store' function DesktopRegisterModal() { - const [open, setOpen] = useState(true) + const { t } = useTranslation() + const open = useModalStore((state) => state.modals.desktopRegister) + const setModalOpen = useModalStore((state) => state.setModalOpen) function handleSubmit() { - setOpen(false) + setModalOpen('desktopRegister', false) } return ( {}} - title={
注册
} + onClose={() => setModalOpen('desktopRegister', false)} + title={ +
+ {t('game.modals.register.title')} +
+ } titleAlign="center" className={'w-design-980 h-design-740'} > -
-
-
-
- Akun/TEL: -
- -
-
-
- Kata Sandi: -
- -
-
-
- Kata Sandi: -
- -
-
-
- Kata Sandi: -
- -
-
-
-
- -
-
Daftar Akun
-
-
-
- -
-
Ingat Kata Sandi
-
-
-
- - - MASUK - -
+
) } diff --git a/src/features/game/modal/desktop/desktop-userInfo-modal.tsx b/src/features/game/modal/desktop/desktop-userInfo-modal.tsx index ec1b0f0..33ffc77 100644 --- a/src/features/game/modal/desktop/desktop-userInfo-modal.tsx +++ b/src/features/game/modal/desktop/desktop-userInfo-modal.tsx @@ -1,5 +1,6 @@ import { CircleUserRound, Mail } from 'lucide-react' import { useState } from 'react' +import { useTranslation } from 'react-i18next' import avatar from '@/assets/system/avatar.webp' import blueBtnBg from '@/assets/system/blue-btn.webp' import lengthBtnBg from '@/assets/system/length-blue-btn.webp' @@ -8,32 +9,35 @@ import { CenterModal } from '@/components/center-modal.tsx' import { SmartBackground } from '@/components/smart-background.tsx' import { SmartImage } from '@/components/smart-image.tsx' import { cn } from '@/lib/utils' +import { useModalStore } from '@/store' type UserInfoTabKey = 'profile' | 'message' const USER_INFO_TABS: Array<{ key: UserInfoTabKey - label: string + labelKey: string icon: typeof CircleUserRound }> = [ { key: 'profile', - label: '个人信息', + labelKey: 'game.modals.userInfo.tabs.profile', icon: CircleUserRound, }, { key: 'message', - label: '站内消息', + labelKey: 'game.modals.userInfo.tabs.message', icon: Mail, }, ] function DesktopUserInfoModal() { - const [open, setOpen] = useState(true) + const { t } = useTranslation() + const open = useModalStore((state) => state.modals.desktopUserInfo) + const setModalOpen = useModalStore((state) => state.setModalOpen) const [activeTab, setActiveTab] = useState('profile') function handleSubmit() { - setOpen(false) + setModalOpen('desktopUserInfo', false) } return ( @@ -41,7 +45,9 @@ function DesktopUserInfoModal() { open={open} onClose={handleSubmit} title={ -
Biomond Balance
+
+ {t('game.modals.userInfo.title')} +
} isNormalBg={true} titleAlign="left" @@ -96,7 +102,7 @@ function DesktopUserInfoModal() { isActive && 'modal-title-gold-glow', )} > - {tab.label} + {t(tab.labelKey)}
) @@ -119,8 +125,12 @@ function DesktopUserInfoModal() { alt={'avatar'} />
-
NAMA :Biomond Balance
-
TEL :12345678901
+
+ {t('game.modals.userInfo.profile.name')} :Biomond Balance +
+
+ {t('game.modals.userInfo.profile.tel')} :12345678901 +
@@ -128,7 +138,7 @@ function DesktopUserInfoModal() {
{[1, 2, 3, 4].map((item) => (
- Tanggal Pendaftaran : + {t('game.modals.userInfo.profile.registeredAt')} : 2022-10-06 23:36 @@ -140,8 +150,7 @@ function DesktopUserInfoModal() { 'w-design-600 h-design-120 text-design-18 rounded-md bg-[#000000]/40 flex items-center justify-center' } > - Tanda tangan pribadi saya persis seperti jiwa saya—unik dan - mus + {t('game.modals.userInfo.profile.signature')}
@@ -166,10 +175,7 @@ function DesktopUserInfoModal() {
2026-10-10 08:32:56
-
- [Event Bonus Isi Ulang] Dari tanggal 1 hingga 7 Oktober - 2026, dapatkan pengembalian ... -
+
{t('game.modals.userInfo.message.eventBonus')}
- Memeriksa + {t('game.modals.userInfo.message.check')} ))} @@ -196,7 +202,7 @@ function DesktopUserInfoModal() { 'w-design-275 h-design-65 flex items-center justify-center text-design-22 font-bold' } > - 删除记录 + {t('game.modals.userInfo.message.deleteRecords')} diff --git a/src/features/game/modal/desktop/desktop-withdraw-topup-modal.tsx b/src/features/game/modal/desktop/desktop-withdraw-topup-modal.tsx index 1af504e..9dcc161 100644 --- a/src/features/game/modal/desktop/desktop-withdraw-topup-modal.tsx +++ b/src/features/game/modal/desktop/desktop-withdraw-topup-modal.tsx @@ -1,15 +1,17 @@ -import { useState } from 'react' +import { useTranslation } from 'react-i18next' import { CenterModal } from '@/components/center-modal.tsx' import DesktopTopup from '@/features/game/components/desktop/desktop-topup.tsx' import DesktopWithdraw from '@/features/game/components/desktop/desktop-withdraw.tsx' - -type WithdrawType = 'withdraw' | 'topup' +import { useModalStore } from '@/store' function DesktopWithdrawTopupModal() { - const [open, setOpen] = useState(true) - const [type] = useState('withdraw') + const { t } = useTranslation() + const open = useModalStore((state) => state.modals.desktopWithdrawTopup) + const type = useModalStore((state) => state.withdrawTopupType) + const setModalOpen = useModalStore((state) => state.setModalOpen) + function handleSubmit() { - setOpen(false) + setModalOpen('desktopWithdrawTopup', false) } return ( @@ -18,7 +20,9 @@ function DesktopWithdrawTopupModal() { onClose={handleSubmit} title={
- {type === 'withdraw' ? '申请提现' : '申请充值'} + {type === 'withdraw' + ? t('game.modals.withdrawTopup.applyWithdraw') + : t('game.modals.withdrawTopup.applyTopup')}
} isNormalBg={true} diff --git a/src/features/game/shared/constants.ts b/src/features/game/shared/constants.ts index d2ad1fc..a549321 100644 --- a/src/features/game/shared/constants.ts +++ b/src/features/game/shared/constants.ts @@ -56,4 +56,4 @@ export const DEFAULT_GAME_CHIP_COLORS = [ export const DEFAULT_ACTIVE_CHIP_ID = 'chip-5' export const DEFAULT_ANNOUNCEMENT_TTL_MS = 90_000 export const GAME_RECENT_HISTORY_LIMIT = 12 -export const GAME_BOARD_COLUMNS = GAME_GRID_COLUMNS +export const GAME_MAX_SELECTION_CELLS = 5 diff --git a/src/features/game/shared/mock-data.ts b/src/features/game/shared/mock-data.ts index f396036..f3052de 100644 --- a/src/features/game/shared/mock-data.ts +++ b/src/features/game/shared/mock-data.ts @@ -1,9 +1,10 @@ -import { CHIP_OPTIONS } from '@/constants' +import { DEFAULT_CHIP_AMOUNTS } from '@/constants' import { DEFAULT_ACTIVE_CHIP_ID, DEFAULT_ANNOUNCEMENT_TTL_MS, DEFAULT_GAME_CHIP_COLORS, GAME_GRID_COLUMNS, + GAME_MAX_SELECTION_CELLS, GAME_TOTAL_CELLS, } from './constants' import { deriveTrendEntries, getRoundCountdownMs } from './selectors' @@ -41,12 +42,12 @@ export function createGameCells() { } export function createDefaultChips() { - return CHIP_OPTIONS.map((chip, index) => ({ - amount: chip.value, + return DEFAULT_CHIP_AMOUNTS.map((chip, index) => ({ + amount: chip.amount, color: DEFAULT_GAME_CHIP_COLORS[index], id: chip.id, isDefault: chip.id === DEFAULT_ACTIVE_CHIP_ID, - label: chip.value >= 100 ? `${chip.value / 100}x` : String(chip.value), + label: chip.amount >= 100 ? `${chip.amount / 100}x` : String(chip.amount), })) satisfies Chip[] } @@ -76,36 +77,8 @@ export function createMockRoundSnapshot(baseIso = MOCK_GAME_BASE_TIME) { } satisfies RoundSnapshot } -export function createMockBetSelections(chips = createDefaultChips()) { - const defaultChip = - chips.find((chip) => chip.id === DEFAULT_ACTIVE_CHIP_ID) ?? chips[0] - - return [ - { - amount: defaultChip.amount, - cellId: 8, - chipId: defaultChip.id, - id: 'bet-local-1', - placedAt: offsetIso(MOCK_GAME_BASE_TIME, 4_000), - source: 'local', - }, - { - amount: chips[1]?.amount ?? defaultChip.amount, - cellId: 12, - chipId: chips[1]?.id ?? defaultChip.id, - id: 'bet-server-2', - placedAt: offsetIso(MOCK_GAME_BASE_TIME, 7_000), - source: 'server', - }, - { - amount: chips[3]?.amount ?? defaultChip.amount, - cellId: 17, - chipId: chips[3]?.id ?? defaultChip.id, - id: 'bet-local-3', - placedAt: offsetIso(MOCK_GAME_BASE_TIME, 10_000), - source: 'local', - }, - ] satisfies BetSelection[] +export function createMockBetSelections() { + return [] satisfies BetSelection[] } export function createMockAnnouncementState(baseIso = MOCK_GAME_BASE_TIME) { @@ -177,8 +150,9 @@ export function createMockGameBootstrapSnapshot(baseIso = MOCK_GAME_BASE_TIME) { connection: createMockConnectionState(baseIso), dashboard: createMockDashboardState(baseIso, round, history), history, + maxSelectionCount: GAME_MAX_SELECTION_CELLS, round, - selections: createMockBetSelections(chips), + selections: createMockBetSelections(), trends: deriveTrendEntries(history), } satisfies GameBootstrapSnapshot } diff --git a/src/features/game/shared/types.ts b/src/features/game/shared/types.ts index 7a1f815..260d4ad 100644 --- a/src/features/game/shared/types.ts +++ b/src/features/game/shared/types.ts @@ -112,6 +112,7 @@ export interface GameBootstrapSnapshot { connection: ConnectionState dashboard: DashboardState history: HistoryEntry[] + maxSelectionCount: number round: RoundSnapshot selections: BetSelection[] trends: TrendEntry[] diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 61e0cb6..c304b9c 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -1,11 +1,13 @@ import i18n from 'i18next' import { initReactI18next } from 'react-i18next' -import { I18N_LANGUAGE_STORAGE_KEY } from '@/constants' import enUSCommon from '@/locales/en-US/common' +import idIDCommon from '@/locales/id-ID/common' +import msMYCommon from '@/locales/ms-MY/common' import zhCNCommon from '@/locales/zh-CN/common' +import { getStoredAppLanguage, setStoredAppLanguage } from '@/store/auth' -export const supportedLanguages = ['zh-CN', 'en-US'] as const +export const supportedLanguages = ['zh-CN', 'en-US', 'ms-MY', 'id-ID'] as const export type AppLanguage = (typeof supportedLanguages)[number] const defaultLanguage: AppLanguage = 'zh-CN' @@ -39,6 +41,14 @@ function detectBrowserLanguage() { if (normalizedLanguage.startsWith('en')) { return 'en-US' } + + if (normalizedLanguage.startsWith('ms')) { + return 'ms-MY' + } + + if (normalizedLanguage.startsWith('id')) { + return 'id-ID' + } } return defaultLanguage @@ -46,13 +56,7 @@ function detectBrowserLanguage() { /** @description 获取应用启动时应使用的初始语言。 */ function getInitialLanguage() { - if (typeof window === 'undefined') { - return defaultLanguage - } - - const persistedLanguage = window.localStorage.getItem( - I18N_LANGUAGE_STORAGE_KEY, - ) + const persistedLanguage = getStoredAppLanguage() if (isSupportedLanguage(persistedLanguage)) { return persistedLanguage @@ -91,6 +95,12 @@ void i18n.use(initReactI18next).init({ 'en-US': { common: enUSCommon, }, + 'ms-MY': { + common: msMYCommon, + }, + 'id-ID': { + common: idIDCommon, + }, }, defaultNS: 'common', }) @@ -101,8 +111,8 @@ function syncLanguageState(language: string) { document.documentElement.lang = language } - if (typeof window !== 'undefined' && isSupportedLanguage(language)) { - window.localStorage.setItem(I18N_LANGUAGE_STORAGE_KEY, language) + if (isSupportedLanguage(language)) { + setStoredAppLanguage(language) } } diff --git a/src/lib/api/api-client.ts b/src/lib/api/api-client.ts index 13366c6..6d8cabb 100644 --- a/src/lib/api/api-client.ts +++ b/src/lib/api/api-client.ts @@ -4,14 +4,15 @@ import { DEFAULT_REQUEST_ACCEPT_HEADER, DEFAULT_REQUEST_TIMEOUT_MS, } from '@/constants' +import type { AuthTokenDto } from '@/features/auth/api/types' +import { ApiError } from '@/lib/api/api-error.ts' import { handleUnauthorizedSession, tryRefreshAuthSession, } from '@/lib/auth/auth-session' -import { useAuthStore } from '@/store/auth' - -import { ApiError } from './api-error' -import type { ApiResponse } from './types' +import { md5 } from '@/lib/crypto/md5' +import { getAuthDeviceId, useAuthStore } from '@/store/auth' +import type { ApiResponse } from '@/type' type RequestOptions = Omit type JsonRequestOptions = RequestOptions & { @@ -20,7 +21,12 @@ type JsonRequestOptions = RequestOptions & { const AUTH_REFRESH_ATTEMPTED_CONTEXT_KEY = 'authRefreshAttempted' const AUTH_SKIP_REFRESH_CONTEXT_KEY = 'skipAuthRefresh' +const AUTH_TOKEN_ENDPOINT = 'api/v1/authToken' +const AUTH_REFRESH_ENDPOINT = 'api/user/refreshToken' +const ACCESS_TOKEN_REFRESH_SKEW_MS = 60_000 +const AUTH_TOKEN_CACHE_SKEW_MS = 30_000 const appEnv = import.meta.env.VITE_APP_ENV +const authSecret = import.meta.env.VITE_AUTH_TOKEN_SECRET?.trim() const shouldLogRequests = import.meta.env.VITE_ENABLE_REQUEST_LOG === 'true' function normalizeApiBaseUrl(baseUrl: string | undefined) { @@ -96,6 +102,15 @@ async function toApiError(error: unknown) { export const apiBaseUrl = normalizeApiBaseUrl(import.meta.env.VITE_API_BASE_URL) +const authTokenClient = ky.create({ + prefix: apiBaseUrl, + retry: 0, + timeout: DEFAULT_REQUEST_TIMEOUT_MS, + headers: { + Accept: DEFAULT_REQUEST_ACCEPT_HEADER, + }, +}) + const apiClient = ky.create({ prefix: apiBaseUrl, retry: 0, @@ -109,6 +124,7 @@ const apiClient = ky.create({ if (token) { request.headers.set('Authorization', `Bearer ${token}`) + request.headers.set('user-token', token) } if (shouldLogRequests) { @@ -128,9 +144,153 @@ const apiClient = ky.create({ }, }) +function shouldAttachAuthToken(input: string) { + return input !== AUTH_TOKEN_ENDPOINT +} + +function shouldTryRefreshAccessToken(input: string, options?: Options) { + if ( + input === AUTH_REFRESH_ENDPOINT || + options?.context?.[AUTH_SKIP_REFRESH_CONTEXT_KEY] === true + ) { + return false + } + + const authState = useAuthStore.getState() + + return Boolean( + authState.accessToken && + authState.accessTokenExpiresAt && + authState.accessTokenExpiresAt <= + Date.now() + ACCESS_TOKEN_REFRESH_SKEW_MS, + ) +} + +function unwrapEnvelopeData(response: ApiResponse) { + if (response.code === 1) { + return response.data + } + + throw new ApiError({ + data: response, + message: + 'msg' in response && typeof response.msg === 'string' + ? response.msg + : 'message' in response && typeof response.message === 'string' + ? response.message + : API_ERROR_MESSAGES.unexpected, + }) +} + +async function fetchAuthToken() { + try { + const authState = useAuthStore.getState() + + if ( + authState.apiAuthToken && + authState.apiAuthTokenExpiresAt && + authState.apiAuthTokenExpiresAt > Date.now() + AUTH_TOKEN_CACHE_SKEW_MS + ) { + return authState.apiAuthToken + } + + if (!authSecret) { + throw new ApiError({ + message: 'auth.errors.authTokenConfigMissing', + }) + } + + const deviceId = getAuthDeviceId() + const timestamp = Math.floor(Date.now() / 1000) + const signature = md5( + `device_id=${deviceId}&secret=${authSecret}×tamp=${timestamp}`, + ).toUpperCase() + + const response = await authTokenClient + .get(AUTH_TOKEN_ENDPOINT, { + searchParams: { + device_id: deviceId, + secret: authSecret, + signature, + timestamp: String(timestamp), + }, + }) + .json>() + + const data = unwrapEnvelopeData(response) + const expiresAt = Date.now() + data.expires_in * 1000 + + useAuthStore.getState().setApiAuthToken({ + expiresAt, + serverTime: data.server_time, + value: data.auth_token, + }) + + return data.auth_token + } catch (error) { + throw await toApiError(error) + } +} + +export async function prefetchAuthToken() { + await fetchAuthToken() +} + +function createHeaders(headersInit?: Options['headers']) { + const headers = new Headers() + + if (!headersInit) { + return headers + } + + if (headersInit instanceof Headers) { + headersInit.forEach((value, key) => { + headers.set(key, value) + }) + + return headers + } + + if (Array.isArray(headersInit)) { + for (const [key, value] of headersInit) { + headers.set(key, value) + } + + return headers + } + + for (const [key, value] of Object.entries(headersInit)) { + if (typeof value === 'string') { + headers.set(key, value) + } + } + + return headers +} + +async function buildRequestOptions(input: string, options?: Options) { + const headers = createHeaders(options?.headers) + + if (shouldAttachAuthToken(input) && !headers.has('auth-token')) { + headers.set('auth-token', await fetchAuthToken()) + } + + return { + ...options, + headers, + } satisfies Options +} + async function request(input: string, options?: Options) { try { - const response = await apiClient(input, options) + if (shouldTryRefreshAccessToken(input, options)) { + await tryRefreshAuthSession() + } + + const response = await apiClient( + input, + await buildRequestOptions(input, options), + ) const data = await parseResponseBody(response) return data as TResponse @@ -138,6 +298,7 @@ async function request(input: string, options?: Options) { if ( error instanceof HTTPError && error.response.status === 401 && + input !== AUTH_REFRESH_ENDPOINT && options?.context?.[AUTH_SKIP_REFRESH_CONTEXT_KEY] !== true && options?.context?.[AUTH_REFRESH_ATTEMPTED_CONTEXT_KEY] !== true ) { diff --git a/src/lib/api/api-error.ts b/src/lib/api/api-error.ts index e2fddd6..e4b4273 100644 --- a/src/lib/api/api-error.ts +++ b/src/lib/api/api-error.ts @@ -1,9 +1,4 @@ -interface ApiErrorOptions { - message: string - status?: number - data?: unknown - url?: string -} +import type { ApiErrorOptions } from '@/type' export class ApiError extends Error { status: number | null diff --git a/src/lib/api/types.ts b/src/lib/api/types.ts deleted file mode 100644 index b2ead25..0000000 --- a/src/lib/api/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** @description 后端统一响应体结构。 */ -export interface ApiResponse { - code: number - msg: string - data: T -} diff --git a/src/lib/auth/auth-session.ts b/src/lib/auth/auth-session.ts index 735c722..3cadaef 100644 --- a/src/lib/auth/auth-session.ts +++ b/src/lib/auth/auth-session.ts @@ -61,6 +61,14 @@ export async function initializeAuthSession() { return authInitializationPromise } +export async function hydrateCurrentUser(initializer: CurrentUserInitializer) { + const currentUser = await initializer() + + useAuthStore.getState().setCurrentUser(currentUser) + + return currentUser +} + export async function tryRefreshAuthSession() { if (refreshSessionPromise) { return refreshSessionPromise @@ -86,6 +94,8 @@ export async function tryRefreshAuthSession() { useAuthStore.getState().startSession({ accessToken: nextSession.accessToken, + accessTokenExpiresAt: + nextSession.accessTokenExpiresAt ?? snapshot.accessTokenExpiresAt, currentUser: nextSession.currentUser ?? snapshot.currentUser, refreshToken: nextSession.refreshToken ?? snapshot.refreshToken, }) diff --git a/src/lib/crypto/md5.ts b/src/lib/crypto/md5.ts new file mode 100644 index 0000000..f045f5d --- /dev/null +++ b/src/lib/crypto/md5.ts @@ -0,0 +1,5 @@ +import md5Hash from 'md5' + +export function md5(value: string) { + return md5Hash(value) +} diff --git a/src/lib/notify.ts b/src/lib/notify.ts new file mode 100644 index 0000000..e490f1f --- /dev/null +++ b/src/lib/notify.ts @@ -0,0 +1,113 @@ +import { create } from 'zustand' + +const DEFAULT_TOAST_DURATION_MS = 3200 + +type NotificationType = 'success' | 'error' | 'warning' | 'info' | 'loading' + +export interface NotifyOptions { + description?: string + duration?: number +} + +interface NotificationToast { + description?: string + duration: number + id: string + message: string + type: NotificationType +} + +interface NotificationStoreState { + dismissToast: (id: string) => void + pushToast: (toast: NotificationToast) => void + toasts: NotificationToast[] +} + +const toastTimers = new Map() + +export const useNotificationStore = create()((set) => ({ + dismissToast: (id) => { + const timerId = toastTimers.get(id) + + if (timerId) { + window.clearTimeout(timerId) + toastTimers.delete(id) + } + + set((state) => ({ + toasts: state.toasts.filter((toast) => toast.id !== id), + })) + }, + pushToast: (toast) => { + set((state) => ({ + toasts: [...state.toasts.filter((item) => item.id !== toast.id), toast], + })) + }, + toasts: [], +})) + +function createToastId() { + return `toast-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` +} + +function showToast( + type: NotificationType, + message: string, + options?: NotifyOptions, +) { + const id = createToastId() + const duration = options?.duration ?? DEFAULT_TOAST_DURATION_MS + + useNotificationStore.getState().pushToast({ + description: options?.description, + duration, + id, + message, + type, + }) + + if (duration > 0) { + const timerId = window.setTimeout(() => { + useNotificationStore.getState().dismissToast(id) + }, duration) + + toastTimers.set(id, timerId) + } + + return id +} + +export const notify = { + dismiss(id?: string) { + if (id) { + useNotificationStore.getState().dismissToast(id) + return id + } + + const { toasts } = useNotificationStore.getState() + + for (const toast of toasts) { + useNotificationStore.getState().dismissToast(toast.id) + } + + return null + }, + error(message: string, options?: NotifyOptions) { + return showToast('error', message, options) + }, + info(message: string, options?: NotifyOptions) { + return showToast('info', message, options) + }, + loading(message: string, options?: NotifyOptions) { + return showToast('loading', message, options) + }, + message(message: string, options?: NotifyOptions) { + return showToast('info', message, options) + }, + success(message: string, options?: NotifyOptions) { + return showToast('success', message, options) + }, + warning(message: string, options?: NotifyOptions) { + return showToast('warning', message, options) + }, +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index d32b0fe..c4cddf1 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -4,3 +4,106 @@ import { twMerge } from 'tailwind-merge' export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } + +type FullscreenCapableElement = HTMLElement & { + mozRequestFullScreen?: () => Promise | void + msRequestFullscreen?: () => Promise | void + webkitRequestFullscreen?: () => Promise | void +} + +type FullscreenCapableDocument = Document & { + mozCancelFullScreen?: () => Promise | void + mozFullScreenElement?: Element | null + msExitFullscreen?: () => Promise | void + msFullscreenElement?: Element | null + webkitExitFullscreen?: () => Promise | void + webkitFullscreenElement?: Element | null +} + +const FULLSCREEN_CHANGE_EVENTS = [ + 'fullscreenchange', + 'webkitfullscreenchange', + 'mozfullscreenchange', + 'MSFullscreenChange', +] as const + +export function isDesktopFullscreen() { + if (typeof document === 'undefined') { + return false + } + + const fullscreenDocument = document as FullscreenCapableDocument + + return Boolean( + document.fullscreenElement || + fullscreenDocument.webkitFullscreenElement || + fullscreenDocument.mozFullScreenElement || + fullscreenDocument.msFullscreenElement, + ) +} + +export async function exitDesktopFullscreen() { + if (typeof document === 'undefined') { + return false + } + + const fullscreenDocument = document as FullscreenCapableDocument + + await Promise.resolve( + document.exitFullscreen?.() ?? + fullscreenDocument.webkitExitFullscreen?.() ?? + fullscreenDocument.mozCancelFullScreen?.() ?? + fullscreenDocument.msExitFullscreen?.(), + ) + + return !isDesktopFullscreen() +} + +export function subscribeDesktopFullscreenChange(listener: () => void) { + if (typeof document === 'undefined') { + return () => {} + } + + for (const eventName of FULLSCREEN_CHANGE_EVENTS) { + document.addEventListener(eventName, listener) + } + + return () => { + for (const eventName of FULLSCREEN_CHANGE_EVENTS) { + document.removeEventListener(eventName, listener) + } + } +} + +export async function requestDesktopFullscreen( + target: HTMLElement = document.documentElement, +) { + if (typeof window === 'undefined' || typeof document === 'undefined') { + return false + } + + if (isDesktopFullscreen()) { + return true + } + + const fullscreenTarget = target as FullscreenCapableElement + + await Promise.resolve( + fullscreenTarget.requestFullscreen?.() ?? + fullscreenTarget.webkitRequestFullscreen?.() ?? + fullscreenTarget.mozRequestFullScreen?.() ?? + fullscreenTarget.msRequestFullscreen?.(), + ) + + return isDesktopFullscreen() +} + +export async function toggleDesktopFullscreen( + target: HTMLElement = document.documentElement, +) { + if (isDesktopFullscreen()) { + return exitDesktopFullscreen() + } + + return requestDesktopFullscreen(target) +} diff --git a/src/lib/ws/game-socket-client.ts b/src/lib/ws/game-socket-client.ts new file mode 100644 index 0000000..2390013 --- /dev/null +++ b/src/lib/ws/game-socket-client.ts @@ -0,0 +1,297 @@ +type GameSocketContext = { + authToken: string + deviceId: string + lang: string + token: string +} + +type GameSocketConnectedMessage = { + connection_id?: string + event: 'ws.connected' + heartbeat_interval?: number + server_time?: number +} + +type GameSocketErrorMessage = { + code?: number + event: 'ws.error' + message?: string +} + +type GameSocketPongMessage = { + action?: 'pong' + event?: 'pong' + server_time?: number + topic?: 'pong' +} + +export type GameSocketMessage = + | GameSocketConnectedMessage + | GameSocketErrorMessage + | GameSocketPongMessage + | ({ event?: string } & Record) + +type GameSocketStatus = + | 'idle' + | 'connecting' + | 'connected' + | 'reconnecting' + | 'disconnected' + +type GameSocketClientOptions = { + getContext: () => Promise + getUrl: () => string | null + onError?: (message: GameSocketErrorMessage | Error) => void + onLatencyChange?: (latencyMs: number | null) => void + onMessage?: (message: GameSocketMessage) => void + onStatusChange?: (status: GameSocketStatus, reconnectAttempt: number) => void +} + +const MAX_RECONNECT_DELAY_MS = 10_000 +const LATENCY_PROBE_INTERVAL_MS = 3_000 +const LATENCY_PROBE_TIMEOUT_MS = 10_000 + +function toQueryString(context: GameSocketContext) { + const params = new URLSearchParams({ + token: context.token, + auth_token: context.authToken, + device_id: context.deviceId, + lang: context.lang, + }) + + return params.toString() +} + +export class GameSocketClient { + private heartbeatTimerId: number | null = null + private latencyProbeTimerId: number | null = null + private manualClose = false + private readonly options: GameSocketClientOptions + private pendingPingSentAt: number | null = null + private reconnectAttempt = 0 + private reconnectTimerId: number | null = null + private socket: WebSocket | null = null + private readonly subscribedTopics = new Set() + + constructor(options: GameSocketClientOptions) { + this.options = options + } + + async connect() { + if (this.socket && this.socket.readyState === WebSocket.OPEN) { + return + } + + this.clearReconnectTimer() + this.clearHeartbeatTimer() + this.clearLatencyProbeTimer() + + const url = this.options.getUrl() + const context = await this.options.getContext() + + if (!url || !context) { + this.setStatus('disconnected') + return + } + + this.manualClose = false + this.setStatus(this.reconnectAttempt > 0 ? 'reconnecting' : 'connecting') + + const socketUrl = new URL(url) + + socketUrl.search = toQueryString(context) + + const socket = new WebSocket(socketUrl.toString()) + + this.socket = socket + + socket.addEventListener('open', () => { + this.flushSubscriptions() + }) + + socket.addEventListener('message', (event) => { + this.handleMessage(event.data) + }) + + socket.addEventListener('error', () => { + this.options.onError?.(new Error('WebSocket connection error')) + }) + + socket.addEventListener('close', () => { + this.socket = null + this.clearHeartbeatTimer() + this.clearLatencyProbeTimer() + this.pendingPingSentAt = null + this.options.onLatencyChange?.(null) + + if (this.manualClose) { + this.setStatus('disconnected') + return + } + + this.scheduleReconnect() + }) + } + + disconnect() { + this.manualClose = true + this.clearReconnectTimer() + this.clearHeartbeatTimer() + this.clearLatencyProbeTimer() + this.pendingPingSentAt = null + this.options.onLatencyChange?.(null) + this.socket?.close() + this.socket = null + this.setStatus('disconnected') + } + + // Topics are de-duplicated locally and re-sent automatically after reconnect. + subscribe(topics: string[]) { + for (const topic of topics) { + this.subscribedTopics.add(topic) + } + + this.send({ + action: 'subscribe', + topics: [...this.subscribedTopics], + }) + } + + send(payload: Record) { + if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { + return + } + + this.socket.send(JSON.stringify(payload)) + } + + private clearHeartbeatTimer() { + if (this.heartbeatTimerId !== null) { + window.clearInterval(this.heartbeatTimerId) + this.heartbeatTimerId = null + } + } + + private clearReconnectTimer() { + if (this.reconnectTimerId !== null) { + window.clearTimeout(this.reconnectTimerId) + this.reconnectTimerId = null + } + } + + private clearLatencyProbeTimer() { + if (this.latencyProbeTimerId !== null) { + window.clearInterval(this.latencyProbeTimerId) + this.latencyProbeTimerId = null + } + } + + private flushSubscriptions() { + if (this.subscribedTopics.size === 0) { + return + } + + this.send({ + action: 'subscribe', + topics: [...this.subscribedTopics], + }) + } + + private sendPing() { + const now = performance.now() + + if ( + this.pendingPingSentAt !== null && + now - this.pendingPingSentAt < LATENCY_PROBE_TIMEOUT_MS + ) { + return + } + + this.pendingPingSentAt = performance.now() + this.send({ action: 'ping' }) + } + + private handlePongMessage() { + if (this.pendingPingSentAt === null) { + return + } + + const latencyMs = Math.max( + 0, + Math.round(performance.now() - this.pendingPingSentAt), + ) + + this.pendingPingSentAt = null + this.options.onLatencyChange?.(latencyMs) + } + + private handleConnectedMessage(message: GameSocketConnectedMessage) { + this.reconnectAttempt = 0 + this.setStatus('connected') + this.options.onLatencyChange?.(null) + this.clearLatencyProbeTimer() + + if (message.heartbeat_interval && message.heartbeat_interval > 0) { + this.clearHeartbeatTimer() + this.heartbeatTimerId = window.setInterval(() => { + this.sendPing() + }, message.heartbeat_interval * 1000) + } + + this.latencyProbeTimerId = window.setInterval(() => { + this.sendPing() + }, LATENCY_PROBE_INTERVAL_MS) + + this.flushSubscriptions() + this.sendPing() + } + + private handleMessage(raw: string) { + if (raw.trim() === 'pong') { + this.handlePongMessage() + return + } + + let message: GameSocketMessage + + try { + message = JSON.parse(raw) as GameSocketMessage + } catch { + this.options.onError?.(new Error('WebSocket message parse failed')) + return + } + + if (message.event === 'ws.connected') { + this.handleConnectedMessage(message as GameSocketConnectedMessage) + } else if (message.event === 'ws.error') { + this.options.onError?.(message as GameSocketErrorMessage) + } else if ( + message.event === 'pong' || + ('action' in message && message.action === 'pong') || + ('topic' in message && message.topic === 'pong') + ) { + this.handlePongMessage() + } + + this.options.onMessage?.(message) + } + + private scheduleReconnect() { + this.reconnectAttempt += 1 + this.setStatus('reconnecting') + + const delay = Math.min( + 1000 * 2 ** Math.max(0, this.reconnectAttempt - 1), + MAX_RECONNECT_DELAY_MS, + ) + + this.clearReconnectTimer() + this.reconnectTimerId = window.setTimeout(() => { + void this.connect() + }, delay) + } + + private setStatus(status: GameSocketStatus) { + this.options.onStatusChange?.(status, this.reconnectAttempt) + } +} diff --git a/src/locales/en-US/common.ts b/src/locales/en-US/common.ts index 22e7a94..106c2a1 100644 --- a/src/locales/en-US/common.ts +++ b/src/locales/en-US/common.ts @@ -42,6 +42,8 @@ export default { label: 'Language', zhCN: '中文', enUS: 'English', + msMY: 'Bahasa Melayu', + idID: 'Bahasa Indonesia', }, game: { metaTitle: 'Game Lobby', @@ -109,6 +111,59 @@ export default { 'This will later connect to the real announcement body, confirmation checkbox, and persistence flow.', line2: 'For now it validates the shared modal structure.', }, + modals: { + login: { + title: 'Login', + }, + register: { + title: 'Register', + }, + notice: { + title: 'Event Notice', + content: + 'This area will later load the real event announcement body, rich media, and a longer scrollable message. The current version focuses on shared multilingual modal wiring.', + check: 'View', + }, + procedures: { + title: 'Top Up / Withdraw', + contentPlaceholder: 'Choose the action you want to continue with', + withdraw: 'Withdraw', + topup: 'Top Up', + }, + autoSetting: { + title: 'Auto Spin', + startAutoSpin: 'Start Auto Spin', + rows: { + stopIfBalanceLowerThan: 'Stop if balance is lower than', + stopIfSingleWinExceeds: 'Stop if a single win exceeds', + stopOnAnyJackpot: 'Stop on any jackpot', + }, + }, + userInfo: { + title: 'User Info', + tabs: { + profile: 'Profile', + message: 'Messages', + }, + profile: { + name: 'Name', + tel: 'Phone', + registeredAt: 'Registered at', + signature: + 'My signature is as unique as my personality. This area will later display the real profile summary.', + }, + message: { + eventBonus: + '[Top-up Bonus Event] From October 1 to October 7, 2026, claim your rebate rewards...', + check: 'View', + deleteRecords: 'Delete records', + }, + }, + withdrawTopup: { + applyWithdraw: 'Apply for Withdrawal', + applyTopup: 'Apply for Top Up', + }, + }, autoSpin: { eyebrow: 'Auto spin', title: 'Auto spin running', @@ -128,4 +183,234 @@ export default { maxBet: 'Max bet', }, }, + commonUi: { + modal: { + close: 'Close modal', + defaultAriaLabel: 'Modal', + }, + toast: { + lobbyInitFailed: 'Failed to load the game lobby', + loginRequired: 'Please log in before entering the game', + loginSuccess: 'Login successful', + registerSuccess: 'Registration successful', + }, + }, + auth: { + common: { + arrowIconAlt: 'Arrow', + actions: { + submitting: 'Submitting...', + }, + }, + login: { + actions: { + submit: 'Log In', + }, + fields: { + username: { + label: 'Account / Phone:', + placeholder: 'Enter account or mobile number', + }, + password: { + label: 'Password:', + placeholder: 'Enter password', + }, + }, + footer: { + registerAccount: 'Create account', + forgotPassword: 'Forgot password', + }, + errors: { + submitFailed: 'Login failed. Please try again later.', + invalidCredentials: 'Incorrect account or password.', + }, + }, + register: { + actions: { + submit: 'Register', + }, + fields: { + username: { + label: 'Account / Phone:', + placeholder: 'Enter account or mobile number', + }, + password: { + label: 'Password:', + placeholder: 'Enter password', + }, + confirmPassword: { + label: 'Confirm Password:', + placeholder: 'Re-enter password', + }, + inviteCode: { + label: 'Invite Code:', + placeholder: 'Enter invite code', + }, + }, + footer: { + alreadyHaveAccount: 'Already have an account', + needHelp: 'Need help', + }, + errors: { + submitFailed: 'Registration failed. Please try again later.', + unauthorized: 'Registration is not authorized. Please try again later.', + }, + }, + validation: { + username: { + required: 'Please enter your mobile number.', + invalidPhone: 'Please enter a valid mobile number.', + }, + password: { + min: 'Password must be at least 6 characters.', + max: 'Password must be at most 32 characters.', + }, + inviteCode: { + required: 'Please enter the invite code.', + max: 'Invite code must be at most 32 characters.', + }, + confirmPassword: { + mismatch: 'The two passwords do not match.', + }, + }, + errors: { + requestFailed: 'Request failed. Please try again later.', + authTokenConfigMissing: + 'Authentication configuration is missing. Please contact support.', + timeout: 'Request timed out. Please try again later.', + serviceUnavailable: + 'Service is temporarily unavailable. Please try again later.', + }, + }, + gameDesktop: { + header: { + systemTime: 'System Time', + rules: 'Rules', + message: 'Message', + bgm: 'BGM', + id: 'ID', + fullscreen: 'Full Screen', + login: 'Login', + register: 'Register', + }, + control: { + trend: 'Trend', + map: 'Map', + selected: 'Selected', + totalBet: 'Total Bet', + confirm: 'Confirm', + actions: { + clear: 'Clear', + repeat: 'Repeat', + 'auto-spin': 'Auto Spin', + }, + }, + status: { + odds: 'Odds', + streak: 'Streak', + limit: 'Limit', + roundId: 'Round', + phase: { + betting: { + label: 'Open', + description: '(Accepting Bets)', + }, + locked: { + label: 'Locked', + description: '(Betting Closed)', + }, + revealing: { + label: 'Drawing', + description: '(Revealing Result)', + }, + settled: { + label: 'Settled', + description: '(Round Complete)', + }, + waiting: { + label: 'Waiting', + description: '(Waiting for Next Round)', + }, + }, + }, + title: { + announcement: 'Announcement', + }, + animal: { + loading: 'Loading', + tapToEnter: 'Tap To Enter', + getStart: 'Get Start', + }, + history: { + title: 'History', + orderNo: 'Order No.', + roundId: 'Round ID', + numbers: 'Bet Numbers', + settledAt: 'Settled At', + totalPoolAmount: 'Bet Amount', + winningResult: 'Winning Result', + payout: 'Win Amount', + empty: 'No history yet', + end: 'No more records', + loading: 'Loading...', + settled: 'Settled', + }, + topup: { + placeholder: 'Top-up content is under construction', + }, + mobile: { + placeholder: 'Mobile entry is under construction', + }, + withdraw: { + availableBalance: 'Available balance: {{amount}}', + currencySelection: 'Currency selection', + selectCurrency: 'Select currency', + exchangeRateNotice: + 'Exchange rates and final payout amounts follow the platform real-time settlement.', + wallet: 'Wallet', + bank: 'Bank', + minimumRm10: 'Minimum RM 10', + processingTime: 'Processing time', + fundsArrivalTime: 'Expected within 1-15 minutes', + feeNotice: + 'Please confirm the receiving information carefully. It cannot be changed after submission.', + cancel: 'Cancel', + confirm: 'Confirm', + withdrawal: 'Withdrawal', + fields: { + diamondWithdrawalAmount: 'Diamond Withdrawal Amount', + currencyType: 'Currency Type', + paymentChannel: 'Payment Channel', + bankCode: 'Bank Code', + cardHolderName: 'Card Holder Name', + bankAccountNumber: 'Bank Account Number', + receiverEmail: 'Receiver Email', + receiverPhone: 'Receiver Phone', + }, + placeholders: { + cardHolderName: 'Enter card holder name', + bankAccountNumber: 'Enter bank account number', + receiverEmail: 'Enter receiver email', + receiverPhone: 'Enter receiver phone number', + }, + errors: { + cardHolderNameRequired: 'Please enter the card holder name.', + bankAccountRequired: 'Please enter the bank account number.', + }, + preview: { + title: 'Exchange Preview', + diamondAmount: 'Diamond Amount', + rateMyr: 'MYR Rate', + rateMyrValue: '{{diamonds}} diamonds = 1 MYR', + convertibleMyr: 'Convertible MYR', + usdtMyrRate: 'USDT / MYR Rate', + usdtMyrRateValue: '1 USDT = {{rate}} MYR', + rateVnd: 'VND Rate', + rateVndValue: '1 diamond = {{diamonds}} VND', + convertibleVnd: 'Convertible VND', + convertibleUsdt: 'Convertible USDT', + fixedExchangeDiamondAmount: 'Fixed Exchange Diamond Amount', + }, + }, + }, } as const diff --git a/src/locales/id-ID/common.ts b/src/locales/id-ID/common.ts new file mode 100644 index 0000000..c0b8824 --- /dev/null +++ b/src/locales/id-ID/common.ts @@ -0,0 +1,415 @@ +export default { + nav: { + home: 'Beranda', + game: 'Game', + }, + shell: { + eyebrow: '36 Character Flower', + subtitle: 'Frontend game undian real-time untuk mobile dan desktop', + }, + notFound: { + eyebrow: '404', + title: 'Halaman yang kamu minta tidak ditemukan.', + description: 'Rute ini tidak ada. Kembali ke halaman utama scaffold.', + home: 'Kembali ke beranda', + }, + home: { + eyebrow: 'Shell game sedang dibangun', + title: 'Framework game dual-device 36-character-flower sedang dibangun.', + description: + 'Proyek ini sudah melewati tahap scaffold umum. Sekarang strukturnya dibangun dengan rute game bersama, state bersama, serta tampilan mobile dan desktop terpisah untuk pengalaman betting real-time.', + cards: { + routingMode: 'Routing', + dataLayer: 'Model state', + transport: 'Real-time', + auth: 'Produk', + metadata: 'Fokus saat ini', + }, + values: { + routingMode: 'URL bersama + tampilan device terpisah', + dataLayer: 'Round / Bet / User / UI / Connection', + transport: 'HTTP + WebSocket', + auth: 'Gameplay live draw 36-grid', + metadata: 'Bangun struktur dulu sebelum polishing state machine', + }, + footnote: + 'Berikutnya: rute utama game, model bisnis bersama, dan shell halaman mobile serta desktop.', + primaryAction: 'Masuk lobby game', + secondaryAction: 'Lihat struktur proyek', + }, + language: { + label: 'Bahasa', + zhCN: '中文', + enUS: 'English', + msMY: 'Bahasa Melayu', + idID: 'Bahasa Indonesia', + }, + game: { + metaTitle: 'Lobby Game', + metaDescription: 'Lobby game live 36-character-flower.', + lobbyTitle: 'Lobby 36 Character Flower', + lobbySubtitle: + 'Dalam satu rute bisnis bersama, mobile dan desktop memasang tampilan berbeda di atas data dan state game yang sama.', + status: { + roundState: 'Status ronde', + currentRound: 'Ronde saat ini {{id}}', + tablePool: 'Pool meja', + onlineCount: '{{count}} online', + activeChip: 'Chip aktif', + announcementsRead: '{{read}}/{{total}} pengumuman dibaca', + connection: 'Koneksi', + connectionHealthy: 'Sinkronisasi stabil', + connectionRecovering: 'Menunggu pemulihan', + synced: 'Tersinkron', + degraded: 'Menurun', + }, + board: { + historyTitle: 'Riwayat ronde', + historySubtitle: 'Jejak undian dan payout terbaru', + trendTitle: 'Radar tren', + trendSubtitle: 'Ringkasan momentum dan miss streak', + stageTitle: 'Panggung undian', + stageSubtitle: + 'Panggung ini menampung papan utama dan struktur kontrol sebelum integrasi penuh state machine dan animasi.', + currentPhase: 'Fase saat ini', + selectedBet: 'Bet {{amount}}', + hitCount: '{{count}} hit', + hitBadge: '{{count}}x', + badgeWin: 'Menang', + badgeBet: 'Bet', + cellLabel: 'Sel {{id}}', + winningCell: 'Sel pemenang {{id}}', + missedRounds: 'Miss {{count}} ronde', + rising: 'Naik', + falling: 'Turun', + steady: 'Stabil', + hitTotal: '{{count}} hit', + }, + phases: { + betting: 'Betting', + locked: 'Terkunci', + revealing: 'Mengungkap', + settled: 'Selesai', + }, + actions: { + unifiedBetHint: 'Bet seragam', + totalBet: 'Total bet', + canBet: 'Bisa bet', + yes: 'Ya', + no: 'Tidak', + quickBet: 'Quick bet 08', + clearPending: 'Hapus pending', + autoModeDemo: 'Demo mode auto', + stopAuto: 'Stop auto', + }, + modal: { + eyebrow: 'Pengumuman', + acknowledge: 'Saya paham', + later: 'Nanti', + line1: + 'Ini nantinya akan terhubung ke konten pengumuman nyata, checkbox konfirmasi, dan alur penyimpanan status.', + line2: 'Untuk sekarang ini memvalidasi struktur modal bersama.', + }, + modals: { + login: { + title: 'Masuk', + }, + register: { + title: 'Daftar', + }, + notice: { + title: 'Pengumuman Acara', + content: + 'Bagian ini nantinya akan memuat konten pengumuman acara yang sebenarnya, materi visual, dan pesan panjang yang dapat digulir. Versi saat ini fokus pada sambungan modal multibahasa.', + check: 'Lihat', + }, + procedures: { + title: 'Isi Ulang / Tarik Dana', + contentPlaceholder: 'Pilih tindakan yang ingin kamu lanjutkan', + withdraw: 'Tarik Dana', + topup: 'Isi Ulang', + }, + autoSetting: { + title: 'Auto Spin', + startAutoSpin: 'Mulai Auto Spin', + rows: { + stopIfBalanceLowerThan: 'Berhenti jika saldo lebih rendah dari', + stopIfSingleWinExceeds: 'Berhenti jika kemenangan tunggal melebihi', + stopOnAnyJackpot: 'Berhenti pada jackpot apa pun', + }, + }, + userInfo: { + title: 'Info Pengguna', + tabs: { + profile: 'Profil', + message: 'Pesan', + }, + profile: { + name: 'Nama', + tel: 'Telepon', + registeredAt: 'Tanggal daftar', + signature: + 'Tanda tanganku seunik diriku. Bagian ini nantinya akan menampilkan ringkasan profil yang sebenarnya.', + }, + message: { + eventBonus: + '[Event Bonus Isi Ulang] Dari 1 Oktober hingga 7 Oktober 2026, klaim hadiah rebate kamu...', + check: 'Lihat', + deleteRecords: 'Hapus riwayat', + }, + }, + withdrawTopup: { + applyWithdraw: 'Ajukan Penarikan', + applyTopup: 'Ajukan Isi Ulang', + }, + }, + autoSpin: { + eyebrow: 'Auto spin', + title: 'Auto spin berjalan', + description: + 'Mode auto akan menutupi board sambil mempertahankan fokus sel target dan progres.', + trailingLabel: 'Input manual terkunci', + }, + footer: { + implementationTitle: 'Implementasi saat ini', + implementationSubtitle: + 'Iterasi ini memprioritaskan shell dual-device, model bersama, dan wiring bisnis.', + implementationBody: + 'Langkah berikutnya adalah API nyata, WebSocket, UI store penuh, dan state machine siklus ronde.', + limitsTitle: 'Batas meja', + limitsSubtitle: 'Berasal dari data mock dashboard', + minBet: 'Bet minimum', + maxBet: 'Bet maksimum', + }, + }, + commonUi: { + modal: { + close: 'Tutup modal', + defaultAriaLabel: 'Modal', + }, + toast: { + lobbyInitFailed: 'Gagal memuat lobby game', + loginRequired: 'Silakan masuk sebelum memasuki game', + loginSuccess: 'Berhasil masuk', + registerSuccess: 'Pendaftaran berhasil', + }, + }, + auth: { + common: { + arrowIconAlt: 'Panah', + actions: { + submitting: 'Mengirim...', + }, + }, + login: { + actions: { + submit: 'Masuk', + }, + fields: { + username: { + label: 'Akun / Telepon:', + placeholder: 'Masukkan akun atau nomor ponsel', + }, + password: { + label: 'Kata Sandi:', + placeholder: 'Masukkan kata sandi', + }, + }, + footer: { + registerAccount: 'Daftar akun', + forgotPassword: 'Lupa kata sandi', + }, + errors: { + submitFailed: 'Login gagal. Silakan coba lagi nanti.', + invalidCredentials: 'Akun atau kata sandi salah.', + }, + }, + register: { + actions: { + submit: 'Daftar', + }, + fields: { + username: { + label: 'Akun / Telepon:', + placeholder: 'Masukkan akun atau nomor ponsel', + }, + password: { + label: 'Kata Sandi:', + placeholder: 'Masukkan kata sandi', + }, + confirmPassword: { + label: 'Konfirmasi Kata Sandi:', + placeholder: 'Masukkan ulang kata sandi', + }, + inviteCode: { + label: 'Kode Undangan:', + placeholder: 'Masukkan kode undangan', + }, + }, + footer: { + alreadyHaveAccount: 'Sudah punya akun', + needHelp: 'Butuh bantuan', + }, + errors: { + submitFailed: 'Pendaftaran gagal. Silakan coba lagi nanti.', + unauthorized: 'Pendaftaran tidak diizinkan. Silakan coba lagi nanti.', + }, + }, + validation: { + username: { + required: 'Silakan masukkan nomor ponsel.', + invalidPhone: 'Silakan masukkan nomor ponsel yang valid.', + }, + password: { + min: 'Kata sandi minimal 6 karakter.', + max: 'Kata sandi maksimal 32 karakter.', + }, + inviteCode: { + required: 'Silakan masukkan kode undangan.', + max: 'Kode undangan maksimal 32 karakter.', + }, + confirmPassword: { + mismatch: 'Kedua kata sandi tidak sama.', + }, + }, + errors: { + requestFailed: 'Permintaan gagal. Silakan coba lagi nanti.', + authTokenConfigMissing: + 'Konfigurasi autentikasi tidak ada. Silakan hubungi dukungan.', + timeout: 'Permintaan habis waktu. Silakan coba lagi nanti.', + serviceUnavailable: + 'Layanan sedang tidak tersedia. Silakan coba lagi nanti.', + }, + }, + gameDesktop: { + header: { + systemTime: 'Waktu Sistem', + rules: 'Aturan', + message: 'Pesan', + bgm: 'BGM', + id: 'ID', + fullscreen: 'Layar Penuh', + login: 'Masuk', + register: 'Daftar', + }, + control: { + trend: 'Tren', + map: 'Peta', + selected: 'Dipilih', + totalBet: 'Total Bet', + confirm: 'Konfirmasi', + actions: { + clear: 'Hapus', + repeat: 'Ulang', + 'auto-spin': 'Auto Spin', + }, + }, + status: { + odds: 'Odds', + streak: 'Streak', + limit: 'Batas', + roundId: 'Ronde', + phase: { + betting: { + label: 'Buka', + description: '(Menerima Bet)', + }, + locked: { + label: 'Terkunci', + description: '(Bet Ditutup)', + }, + revealing: { + label: 'Drawing', + description: '(Mengungkap Hasil)', + }, + settled: { + label: 'Selesai', + description: '(Ronde Selesai)', + }, + waiting: { + label: 'Menunggu', + description: '(Menunggu Ronde Berikutnya)', + }, + }, + }, + title: { + announcement: 'Pengumuman', + }, + animal: { + loading: 'Memuat', + tapToEnter: 'Ketuk Untuk Masuk', + getStart: 'Mulai', + }, + history: { + title: 'Riwayat', + orderNo: 'No. Order', + roundId: 'ID Ronde', + numbers: 'Nomor Taruhan', + settledAt: 'Waktu Selesai', + totalPoolAmount: 'Jumlah Taruhan', + winningResult: 'Hasil Menang', + payout: 'Jumlah Menang', + empty: 'Belum ada riwayat', + end: 'Tidak ada catatan lagi', + loading: 'Memuat...', + settled: 'Selesai', + }, + topup: { + placeholder: 'Konten isi ulang sedang dibangun', + }, + mobile: { + placeholder: 'Halaman mobile sedang dibangun', + }, + withdraw: { + availableBalance: 'Saldo tersedia: {{amount}}', + currencySelection: 'Pilihan mata uang', + selectCurrency: 'Pilih mata uang', + exchangeRateNotice: + 'Kurs dan jumlah akhir mengikuti penyelesaian real-time platform.', + wallet: 'Dompet', + bank: 'Bank', + minimumRm10: 'Minimum RM 10', + processingTime: 'Waktu proses', + fundsArrivalTime: 'Diperkirakan masuk dalam 1-15 menit', + feeNotice: + 'Pastikan informasi penerima benar. Data tidak dapat diubah setelah dikirim.', + cancel: 'Batal', + confirm: 'Konfirmasi', + withdrawal: 'Penarikan', + fields: { + diamondWithdrawalAmount: 'Jumlah Berlian Ditarik', + currencyType: 'Jenis Mata Uang', + paymentChannel: 'Saluran Pembayaran', + bankCode: 'Kode Bank', + cardHolderName: 'Nama Pemilik Rekening', + bankAccountNumber: 'Nomor Rekening Bank', + receiverEmail: 'Email Penerima', + receiverPhone: 'Telepon Penerima', + }, + placeholders: { + cardHolderName: 'Masukkan nama pemilik rekening', + bankAccountNumber: 'Masukkan nomor rekening bank', + receiverEmail: 'Masukkan email penerima', + receiverPhone: 'Masukkan nomor telepon penerima', + }, + errors: { + cardHolderNameRequired: 'Silakan masukkan nama pemilik rekening.', + bankAccountRequired: 'Silakan masukkan nomor rekening bank.', + }, + preview: { + title: 'Pratinjau Penukaran', + diamondAmount: 'Jumlah Berlian', + rateMyr: 'Kurs MYR', + rateMyrValue: '{{diamonds}} berlian = 1 MYR', + convertibleMyr: 'Bisa Ditukar ke MYR', + usdtMyrRate: 'Kurs USDT / MYR', + usdtMyrRateValue: '1 USDT = {{rate}} MYR', + rateVnd: 'Kurs VND', + rateVndValue: '1 berlian = {{diamonds}} VND', + convertibleVnd: 'Bisa Ditukar ke VND', + convertibleUsdt: 'Bisa Ditukar ke USDT', + fixedExchangeDiamondAmount: 'Jumlah Berlian Tukar Tetap', + }, + }, + }, +} as const diff --git a/src/locales/ms-MY/common.ts b/src/locales/ms-MY/common.ts new file mode 100644 index 0000000..947d628 --- /dev/null +++ b/src/locales/ms-MY/common.ts @@ -0,0 +1,418 @@ +export default { + nav: { + home: 'Laman Utama', + game: 'Permainan', + }, + shell: { + eyebrow: '36 Character Flower', + subtitle: + 'Antara muka permainan cabutan masa nyata untuk mudah alih dan desktop', + }, + notFound: { + eyebrow: '404', + title: 'Halaman yang anda minta tidak ditemui.', + description: + 'Laluan ini tidak wujud. Kembali ke halaman utama rangka kerja.', + home: 'Kembali ke utama', + }, + home: { + eyebrow: 'Rangka permainan sedang dibina', + title: + 'Rangka permainan dwi-peranti 36-character-flower sedang dibangunkan.', + description: + 'Projek ini telah melepasi peringkat rangka asas. Kini ia disusun dengan laluan permainan dikongsi, keadaan dikongsi, serta paparan berasingan untuk mudah alih dan desktop bagi pengalaman pertaruhan masa nyata.', + cards: { + routingMode: 'Laluan', + dataLayer: 'Model keadaan', + transport: 'Masa nyata', + auth: 'Produk', + metadata: 'Fokus semasa', + }, + values: { + routingMode: 'URL dikongsi + paparan peranti berasingan', + dataLayer: 'Round / Bet / User / UI / Connection', + transport: 'HTTP + WebSocket', + auth: 'Permainan cabutan langsung grid 36', + metadata: 'Bina struktur dahulu sebelum kemasan state machine', + }, + footnote: + 'Seterusnya: laluan utama permainan, model perniagaan dikongsi, dan rangka halaman mudah alih serta desktop.', + primaryAction: 'Masuk lobi permainan', + secondaryAction: 'Lihat struktur projek', + }, + language: { + label: 'Bahasa', + zhCN: '中文', + enUS: 'English', + msMY: 'Bahasa Melayu', + idID: 'Bahasa Indonesia', + }, + game: { + metaTitle: 'Lobi Permainan', + metaDescription: 'Lobi permainan langsung 36-character-flower.', + lobbyTitle: 'Lobi 36 Character Flower', + lobbySubtitle: + 'Di bawah satu laluan perniagaan yang dikongsi, mudah alih dan desktop memaparkan antara muka berbeza di atas data dan keadaan permainan yang sama.', + status: { + roundState: 'Keadaan pusingan', + currentRound: 'Pusingan semasa {{id}}', + tablePool: 'Dana meja', + onlineCount: '{{count}} dalam talian', + activeChip: 'Cip aktif', + announcementsRead: '{{read}}/{{total}} pengumuman dibaca', + connection: 'Sambungan', + connectionHealthy: 'Penyegerakan stabil', + connectionRecovering: 'Menunggu pemulihan', + synced: 'Disegerakkan', + degraded: 'Terganggu', + }, + board: { + historyTitle: 'Sejarah pusingan', + historySubtitle: 'Rekod cabutan dan pembayaran terkini', + trendTitle: 'Radar trend', + trendSubtitle: 'Ringkasan momentum dan kekerapan miss', + stageTitle: 'Pentas cabutan', + stageSubtitle: + 'Pentas ini memuatkan papan utama dan struktur kawalan sebelum integrasi penuh state machine serta animasi.', + currentPhase: 'Fasa semasa', + selectedBet: 'Pertaruhan {{amount}}', + hitCount: '{{count}} kena', + hitBadge: '{{count}}x', + badgeWin: 'Menang', + badgeBet: 'Taruhan', + cellLabel: 'Sel {{id}}', + winningCell: 'Sel menang {{id}}', + missedRounds: 'Terlepas {{count}} pusingan', + rising: 'Meningkat', + falling: 'Menurun', + steady: 'Stabil', + hitTotal: '{{count}} kena', + }, + phases: { + betting: 'Taruhan', + locked: 'Dikunci', + revealing: 'Cabutan', + settled: 'Selesai', + }, + actions: { + unifiedBetHint: 'Taruhan seragam', + totalBet: 'Jumlah taruhan', + canBet: 'Boleh taruhan', + yes: 'Ya', + no: 'Tidak', + quickBet: 'Taruhan cepat 08', + clearPending: 'Kosongkan belum sah', + autoModeDemo: 'Demo mod auto', + stopAuto: 'Henti auto', + }, + modal: { + eyebrow: 'Pengumuman', + acknowledge: 'Faham', + later: 'Nanti', + line1: + 'Ini akan disambungkan kepada kandungan pengumuman sebenar, kotak pengesahan, dan aliran penyimpanan status.', + line2: 'Buat masa ini, ia mengesahkan struktur modal yang dikongsi.', + }, + modals: { + login: { + title: 'Log Masuk', + }, + register: { + title: 'Daftar', + }, + notice: { + title: 'Notis Acara', + content: + 'Bahagian ini akan memuatkan kandungan notis acara sebenar, bahan visual, dan mesej boleh skrol yang lebih panjang. Versi semasa memfokuskan sambungan modal pelbagai bahasa.', + check: 'Semak', + }, + procedures: { + title: 'Tambah Nilai / Pengeluaran', + contentPlaceholder: 'Pilih tindakan yang ingin anda teruskan', + withdraw: 'Keluarkan', + topup: 'Tambah Nilai', + }, + autoSetting: { + title: 'Putaran Auto', + startAutoSpin: 'Mula Putaran Auto', + rows: { + stopIfBalanceLowerThan: 'Henti jika baki lebih rendah daripada', + stopIfSingleWinExceeds: 'Henti jika kemenangan tunggal melebihi', + stopOnAnyJackpot: 'Henti pada sebarang jackpot', + }, + }, + userInfo: { + title: 'Maklumat Pengguna', + tabs: { + profile: 'Profil', + message: 'Mesej', + }, + profile: { + name: 'Nama', + tel: 'Telefon', + registeredAt: 'Tarikh daftar', + signature: + 'Tandatangan saya unik seperti personaliti saya. Bahagian ini akan memaparkan ringkasan profil sebenar kemudian.', + }, + message: { + eventBonus: + '[Acara Bonus Tambah Nilai] Dari 1 Oktober hingga 7 Oktober 2026, tuntut ganjaran rebat anda...', + check: 'Semak', + deleteRecords: 'Padam rekod', + }, + }, + withdrawTopup: { + applyWithdraw: 'Mohon Pengeluaran', + applyTopup: 'Mohon Tambah Nilai', + }, + }, + autoSpin: { + eyebrow: 'Putaran auto', + title: 'Putaran auto sedang berjalan', + description: + 'Mod auto akan menutup papan sambil mengekalkan fokus sel sasaran dan kemajuan.', + trailingLabel: 'Input manual dikunci', + }, + footer: { + implementationTitle: 'Pelaksanaan semasa', + implementationSubtitle: + 'Iterasi ini mengutamakan shell dwi-peranti, model dikongsi, dan sambungan logik perniagaan.', + implementationBody: + 'Langkah seterusnya ialah API sebenar, WebSocket, UI store penuh, dan state machine kitaran pusingan.', + limitsTitle: 'Had meja', + limitsSubtitle: 'Diambil daripada data mock dashboard', + minBet: 'Taruhan minimum', + maxBet: 'Taruhan maksimum', + }, + }, + commonUi: { + modal: { + close: 'Tutup modal', + defaultAriaLabel: 'Modal', + }, + toast: { + lobbyInitFailed: 'Gagal memuatkan lobi permainan', + loginRequired: 'Sila log masuk sebelum memasuki permainan', + loginSuccess: 'Log masuk berjaya', + registerSuccess: 'Pendaftaran berjaya', + }, + }, + auth: { + common: { + arrowIconAlt: 'Anak panah', + actions: { + submitting: 'Menghantar...', + }, + }, + login: { + actions: { + submit: 'Log Masuk', + }, + fields: { + username: { + label: 'Akaun / Telefon:', + placeholder: 'Masukkan akaun atau nombor telefon', + }, + password: { + label: 'Kata Laluan:', + placeholder: 'Masukkan kata laluan', + }, + }, + footer: { + registerAccount: 'Daftar akaun', + forgotPassword: 'Lupa kata laluan', + }, + errors: { + submitFailed: 'Log masuk gagal. Sila cuba lagi kemudian.', + invalidCredentials: 'Akaun atau kata laluan tidak betul.', + }, + }, + register: { + actions: { + submit: 'Daftar', + }, + fields: { + username: { + label: 'Akaun / Telefon:', + placeholder: 'Masukkan akaun atau nombor telefon', + }, + password: { + label: 'Kata Laluan:', + placeholder: 'Masukkan kata laluan', + }, + confirmPassword: { + label: 'Sahkan Kata Laluan:', + placeholder: 'Masukkan semula kata laluan', + }, + inviteCode: { + label: 'Kod Jemputan:', + placeholder: 'Masukkan kod jemputan', + }, + }, + footer: { + alreadyHaveAccount: 'Sudah ada akaun', + needHelp: 'Perlukan bantuan', + }, + errors: { + submitFailed: 'Pendaftaran gagal. Sila cuba lagi kemudian.', + unauthorized: 'Pendaftaran tidak dibenarkan. Sila cuba lagi kemudian.', + }, + }, + validation: { + username: { + required: 'Sila masukkan nombor telefon anda.', + invalidPhone: 'Sila masukkan nombor telefon yang sah.', + }, + password: { + min: 'Kata laluan mesti sekurang-kurangnya 6 aksara.', + max: 'Kata laluan mesti maksimum 32 aksara.', + }, + inviteCode: { + required: 'Sila masukkan kod jemputan.', + max: 'Kod jemputan mesti maksimum 32 aksara.', + }, + confirmPassword: { + mismatch: 'Kedua-dua kata laluan tidak sepadan.', + }, + }, + errors: { + requestFailed: 'Permintaan gagal. Sila cuba lagi kemudian.', + authTokenConfigMissing: + 'Konfigurasi pengesahan tiada. Sila hubungi sokongan.', + timeout: 'Permintaan tamat masa. Sila cuba lagi kemudian.', + serviceUnavailable: + 'Perkhidmatan tidak tersedia buat sementara waktu. Sila cuba lagi kemudian.', + }, + }, + gameDesktop: { + header: { + systemTime: 'Masa Sistem', + rules: 'Peraturan', + message: 'Mesej', + bgm: 'BGM', + id: 'ID', + fullscreen: 'Skrin Penuh', + login: 'Log Masuk', + register: 'Daftar', + }, + control: { + trend: 'Trend', + map: 'Peta', + selected: 'Dipilih', + totalBet: 'Jumlah Taruhan', + confirm: 'Sahkan', + actions: { + clear: 'Kosongkan', + repeat: 'Ulang', + 'auto-spin': 'Putaran Auto', + }, + }, + status: { + odds: 'Peluang', + streak: 'Streak', + limit: 'Had', + roundId: 'Pusingan', + phase: { + betting: { + label: 'Buka', + description: '(Menerima Taruhan)', + }, + locked: { + label: 'Dikunci', + description: '(Taruhan Ditutup)', + }, + revealing: { + label: 'Cabutan', + description: '(Mendedahkan Hasil)', + }, + settled: { + label: 'Selesai', + description: '(Pusingan Tamat)', + }, + waiting: { + label: 'Menunggu', + description: '(Menunggu Pusingan Seterusnya)', + }, + }, + }, + title: { + announcement: 'Pengumuman', + }, + animal: { + loading: 'Memuatkan', + tapToEnter: 'Ketik Untuk Masuk', + getStart: 'Mula', + }, + history: { + title: 'Sejarah', + orderNo: 'No. Pesanan', + roundId: 'ID Pusingan', + numbers: 'Nombor Pertaruhan', + settledAt: 'Masa Selesai', + totalPoolAmount: 'Jumlah Pertaruhan', + winningResult: 'Keputusan Menang', + payout: 'Jumlah Menang', + empty: 'Belum ada sejarah', + end: 'Tiada lagi rekod', + loading: 'Memuatkan...', + settled: 'Selesai', + }, + topup: { + placeholder: 'Kandungan tambah nilai sedang dibina', + }, + mobile: { + placeholder: 'Halaman mudah alih sedang dibina', + }, + withdraw: { + availableBalance: 'Baki tersedia: {{amount}}', + currencySelection: 'Pilihan mata wang', + selectCurrency: 'Pilih mata wang', + exchangeRateNotice: + 'Kadar pertukaran dan jumlah akhir tertakluk kepada penyelesaian masa nyata platform.', + wallet: 'Dompet', + bank: 'Bank', + minimumRm10: 'Minimum RM 10', + processingTime: 'Masa pemprosesan', + fundsArrivalTime: 'Dijangka tiba dalam 1-15 minit', + feeNotice: + 'Sila pastikan maklumat penerima adalah tepat. Ia tidak boleh diubah selepas dihantar.', + cancel: 'Batal', + confirm: 'Sahkan', + withdrawal: 'Pengeluaran', + fields: { + diamondWithdrawalAmount: 'Jumlah Berlian Dikeluarkan', + currencyType: 'Jenis Mata Wang', + paymentChannel: 'Saluran Pembayaran', + bankCode: 'Kod Bank', + cardHolderName: 'Nama Pemegang Kad', + bankAccountNumber: 'Nombor Akaun Bank', + receiverEmail: 'E-mel Penerima', + receiverPhone: 'Telefon Penerima', + }, + placeholders: { + cardHolderName: 'Masukkan nama pemegang kad', + bankAccountNumber: 'Masukkan nombor akaun bank', + receiverEmail: 'Masukkan e-mel penerima', + receiverPhone: 'Masukkan nombor telefon penerima', + }, + errors: { + cardHolderNameRequired: 'Sila masukkan nama pemegang kad.', + bankAccountRequired: 'Sila masukkan nombor akaun bank.', + }, + preview: { + title: 'Pratonton Pertukaran', + diamondAmount: 'Jumlah Berlian', + rateMyr: 'Kadar MYR', + rateMyrValue: '{{diamonds}} berlian = 1 MYR', + convertibleMyr: 'Boleh Tukar MYR', + usdtMyrRate: 'Kadar USDT / MYR', + usdtMyrRateValue: '1 USDT = {{rate}} MYR', + rateVnd: 'Kadar VND', + rateVndValue: '1 berlian = {{diamonds}} VND', + convertibleVnd: 'Boleh Tukar VND', + convertibleUsdt: 'Boleh Tukar USDT', + fixedExchangeDiamondAmount: 'Jumlah Berlian Tukaran Tetap', + }, + }, + }, +} as const diff --git a/src/locales/zh-CN/common.ts b/src/locales/zh-CN/common.ts index 657dacc..c5eb2da 100644 --- a/src/locales/zh-CN/common.ts +++ b/src/locales/zh-CN/common.ts @@ -41,6 +41,8 @@ export default { label: '语言', zhCN: '中文', enUS: 'English', + msMY: 'Bahasa Melayu', + idID: 'Bahasa Indonesia', }, game: { metaTitle: '游戏大厅', @@ -106,6 +108,57 @@ export default { line1: '这里后续会接真实公告图文、勾选确认和已读状态。', line2: '当前先用共享弹窗骨架验证结构。', }, + modals: { + login: { + title: '登录', + }, + register: { + title: '注册', + }, + notice: { + title: '活动公告', + content: + '这里后续将接入真实的活动公告内容、图文素材和滚动阅读区域。当前版本先用多语言结构完成桌面端公告弹窗能力。', + check: '查看', + }, + procedures: { + title: '充值 / 提现', + contentPlaceholder: '请选择你要进行的操作', + withdraw: '提现', + topup: '充值', + }, + autoSetting: { + title: '自动托管', + startAutoSpin: '开始自动托管', + rows: { + stopIfBalanceLowerThan: '余额低于时停止', + stopIfSingleWinExceeds: '单次盈利超过时停止', + stopOnAnyJackpot: '出现任意 Jackpot 时停止', + }, + }, + userInfo: { + title: '用户信息', + tabs: { + profile: '个人信息', + message: '站内消息', + }, + profile: { + name: '姓名', + tel: '电话', + registeredAt: '注册时间', + signature: '我的个性签名和灵魂一样独特,后续这里会接真实资料字段。', + }, + message: { + eventBonus: '[充值活动] 10 月 1 日至 10 月 7 日期间可获得返利奖励……', + check: '查看', + deleteRecords: '删除记录', + }, + }, + withdrawTopup: { + applyWithdraw: '申请提现', + applyTopup: '申请充值', + }, + }, autoSpin: { eyebrow: '自动托管', title: '自动托管运行中', @@ -124,4 +177,230 @@ export default { maxBet: '最高下注', }, }, + commonUi: { + modal: { + close: '关闭弹窗', + defaultAriaLabel: '弹窗', + }, + toast: { + lobbyInitFailed: '游戏大厅加载失败', + loginRequired: '请先登录后进入游戏', + loginSuccess: '登录成功', + registerSuccess: '注册成功', + }, + }, + auth: { + common: { + arrowIconAlt: '箭头', + actions: { + submitting: '提交中...', + }, + }, + login: { + actions: { + submit: '登录', + }, + fields: { + username: { + label: '账号/电话:', + placeholder: '请输入账号或手机号', + }, + password: { + label: '密码:', + placeholder: '请输入密码', + }, + }, + footer: { + registerAccount: '注册账号', + forgotPassword: '忘记密码', + }, + errors: { + submitFailed: '登录失败,请稍后重试', + invalidCredentials: '账号或密码错误', + }, + }, + register: { + actions: { + submit: '注册', + }, + fields: { + username: { + label: '账号/电话:', + placeholder: '请输入账号或手机号', + }, + password: { + label: '密码:', + placeholder: '请输入密码', + }, + confirmPassword: { + label: '确认密码:', + placeholder: '请再次输入密码', + }, + inviteCode: { + label: '邀请码:', + placeholder: '请输入邀请码', + }, + }, + footer: { + alreadyHaveAccount: '已有账号', + needHelp: '需要帮助', + }, + errors: { + submitFailed: '注册失败,请稍后重试', + unauthorized: '注册未授权,请稍后重试', + }, + }, + validation: { + username: { + required: '请输入手机号', + invalidPhone: '请输入正确的手机号', + }, + password: { + min: '密码至少 6 位', + max: '密码最多 32 位', + }, + inviteCode: { + required: '请输入邀请码', + max: '邀请码最多 32 位', + }, + confirmPassword: { + mismatch: '两次输入的密码不一致', + }, + }, + errors: { + requestFailed: '请求失败,请稍后重试', + authTokenConfigMissing: '认证配置缺失,请联系管理员', + timeout: '请求超时,请稍后重试', + serviceUnavailable: '服务暂不可用,请稍后重试', + }, + }, + gameDesktop: { + header: { + systemTime: '系统时间', + rules: '规则', + message: '消息', + bgm: '音乐', + id: '编号', + fullscreen: '全屏', + login: '登录', + register: '注册', + }, + control: { + trend: '走势', + map: '地图', + selected: '已选', + totalBet: '总下注', + confirm: '确认', + actions: { + clear: '清空', + repeat: '重复', + 'auto-spin': '自动托管', + }, + }, + status: { + odds: '赔率', + streak: '连中', + limit: '限额', + roundId: '期号', + phase: { + betting: { + label: '下注中', + description: '(接受下注)', + }, + locked: { + label: '已封盘', + description: '(停止下注)', + }, + revealing: { + label: '开奖中', + description: '(正在开奖)', + }, + settled: { + label: '已结算', + description: '(本轮结束)', + }, + waiting: { + label: '等待中', + description: '(等待下一轮)', + }, + }, + }, + title: { + announcement: '公告栏', + }, + animal: { + loading: '加载中', + tapToEnter: '点击进入', + getStart: '开始游戏', + }, + history: { + title: '历史记录', + orderNo: '订单号', + roundId: '期号', + numbers: '下注号码', + settledAt: '结算时间', + totalPoolAmount: '下注金额', + winningResult: '中奖字花', + payout: '中奖金额', + empty: '暂无历史记录', + end: '没有更多记录了', + loading: '加载中...', + settled: '已结算', + }, + topup: { + placeholder: '充值内容建设中', + }, + mobile: { + placeholder: '移动端页面建设中', + }, + withdraw: { + availableBalance: '可用余额:{{amount}}', + currencySelection: '币种选择', + selectCurrency: '请选择币种', + exchangeRateNotice: '汇率与到账金额以平台实时结算为准。', + wallet: '钱包', + bank: '银行卡', + minimumRm10: '最低 RM 10', + processingTime: '处理时间', + fundsArrivalTime: '预计 1-15 分钟到账', + feeNotice: '请确认收款信息准确无误,提交后不可修改。', + cancel: '取消', + confirm: '确认', + withdrawal: '提现', + fields: { + diamondWithdrawalAmount: '提取钻石数量', + currencyType: '币种类型', + paymentChannel: '付款渠道', + bankCode: '银行代码', + cardHolderName: '持卡人姓名', + bankAccountNumber: '银行账号', + receiverEmail: '收款邮箱', + receiverPhone: '收款手机', + }, + placeholders: { + cardHolderName: '请输入持卡人姓名', + bankAccountNumber: '请输入银行账号', + receiverEmail: '请输入收款邮箱', + receiverPhone: '请输入收款手机号', + }, + errors: { + cardHolderNameRequired: '请输入持卡人姓名', + bankAccountRequired: '请输入银行账号', + }, + preview: { + title: '兑换预览', + diamondAmount: '钻石数量', + rateMyr: '马币汇率', + rateMyrValue: '{{diamonds}} 钻石 = 1 MYR', + convertibleMyr: '可兑换 MYR', + usdtMyrRate: 'USDT / MYR 汇率', + usdtMyrRateValue: '1 USDT = {{rate}} MYR', + rateVnd: '越南盾汇率', + rateVndValue: '1 钻石 = {{diamonds}} VND', + convertibleVnd: '可兑换 VND', + convertibleUsdt: '可兑换 USDT', + fixedExchangeDiamondAmount: '固定兑换钻石金额', + }, + }, + }, } as const diff --git a/src/main.tsx b/src/main.tsx index 3e6a43c..72839f9 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,9 +3,19 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { RouterProvider } from '@tanstack/react-router' import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' +import { AppToaster } from '@/components/ui/toaster' import { APP_ROOT_ELEMENT_ID } from '@/constants' +import { + getCurrentUserProfile, + refreshAuthSession, +} from '@/features/auth/api/auth-api' import '@/i18n' -import { initializeAuthSession } from '@/lib/auth/auth-session' +import { prefetchAuthToken } from '@/lib/api/api-client' +import { + initializeAuthSession, + registerCurrentUserInitializer, + registerRefreshSessionHandler, +} from '@/lib/auth/auth-session' import { queryClient } from '@/lib/query/query-client' import { router } from '@/router' import './style/index.css' @@ -19,12 +29,22 @@ if (!rootElement) { throw new Error('Root element not found') } -void initializeAuthSession() +registerCurrentUserInitializer(getCurrentUserProfile) +registerRefreshSessionHandler(refreshAuthSession) + +void initializeAuthSession().then(async () => { + try { + await prefetchAuthToken() + } catch (error) { + console.error('Failed to prefetch auth token', error) + } +}) createRoot(rootElement).render( + {shouldShowQueryDevtools && } , diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index ec7bbd6..5cd285c 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -12,6 +12,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as LangRouteRouteImport } from './routes/$lang/route' import { Route as IndexRouteImport } from './routes/index' import { Route as LangIndexRouteImport } from './routes/$lang/index' +import { Route as LangWsTestRouteImport } from './routes/$lang/ws-test' const LangRouteRoute = LangRouteRouteImport.update({ id: '/$lang', @@ -28,28 +29,36 @@ const LangIndexRoute = LangIndexRouteImport.update({ path: '/', getParentRoute: () => LangRouteRoute, } as any) +const LangWsTestRoute = LangWsTestRouteImport.update({ + id: '/ws-test', + path: '/ws-test', + getParentRoute: () => LangRouteRoute, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute '/$lang': typeof LangRouteRouteWithChildren + '/$lang/ws-test': typeof LangWsTestRoute '/$lang/': typeof LangIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute + '/$lang/ws-test': typeof LangWsTestRoute '/$lang': typeof LangIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/$lang': typeof LangRouteRouteWithChildren + '/$lang/ws-test': typeof LangWsTestRoute '/$lang/': typeof LangIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/$lang' | '/$lang/' + fullPaths: '/' | '/$lang' | '/$lang/ws-test' | '/$lang/' fileRoutesByTo: FileRoutesByTo - to: '/' | '/$lang' - id: '__root__' | '/' | '/$lang' | '/$lang/' + to: '/' | '/$lang/ws-test' | '/$lang' + id: '__root__' | '/' | '/$lang' | '/$lang/ws-test' | '/$lang/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -80,14 +89,23 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LangIndexRouteImport parentRoute: typeof LangRouteRoute } + '/$lang/ws-test': { + id: '/$lang/ws-test' + path: '/ws-test' + fullPath: '/$lang/ws-test' + preLoaderRoute: typeof LangWsTestRouteImport + parentRoute: typeof LangRouteRoute + } } } interface LangRouteRouteChildren { + LangWsTestRoute: typeof LangWsTestRoute LangIndexRoute: typeof LangIndexRoute } const LangRouteRouteChildren: LangRouteRouteChildren = { + LangWsTestRoute: LangWsTestRoute, LangIndexRoute: LangIndexRoute, } diff --git a/src/routes/$lang/ws-test.tsx b/src/routes/$lang/ws-test.tsx new file mode 100644 index 0000000..cb2173a --- /dev/null +++ b/src/routes/$lang/ws-test.tsx @@ -0,0 +1,120 @@ +import { createFileRoute } from '@tanstack/react-router' +import { useEffect, useState } from 'react' + +const TEST_WS_URL = + 'wss://zihua-api.h55555game.top/ws/?token=d77371f4-d053-475a-9c53-bfa11eb921c2&auth_token=708df54d-c647-46fc-b6ee-4339298d1ed4&device_id=web_0bc09c22-9157-4398-b4b3-57584ece9da9&lang=zh' + +const TEST_TOPICS = [ + 'period.tick', + 'user.streak', + 'period.opened', + 'period.locked', + 'period.payout', + 'bet.accepted', + 'wallet.changed', + 'auto.spin.progress', + 'admin.live.snapshot', + 'admin.live.opened', + 'jackpot.hit', +] as const + +type WsTestLog = { + at: string + id: string + message: string +} + +export const Route = createFileRoute('/$lang/ws-test')({ + component: WsTestPage, +}) + +function WsTestPage() { + const [logs, setLogs] = useState([]) + const [status, setStatus] = useState('idle') + + useEffect(() => { + const appendLog = (message: string) => { + setLogs((current) => [ + ...current, + { + at: new Date().toISOString(), + id: crypto.randomUUID(), + message, + }, + ]) + } + + appendLog(`creating websocket: ${TEST_WS_URL}`) + setStatus('connecting') + + const socket = new WebSocket(TEST_WS_URL) + + socket.addEventListener('open', () => { + appendLog('open') + socket.send( + JSON.stringify({ + action: 'subscribe', + topics: [...TEST_TOPICS], + }), + ) + appendLog(`subscribe: ${TEST_TOPICS.join(', ')}`) + setStatus('open') + }) + + socket.addEventListener('message', (event) => { + appendLog(`message: ${String(event.data)}`) + }) + + socket.addEventListener('error', () => { + appendLog('error') + setStatus('error') + }) + + socket.addEventListener('close', (event) => { + appendLog( + `close code=${event.code} reason=${event.reason || '(empty)'} wasClean=${event.wasClean}`, + ) + setStatus('closed') + }) + + return () => { + socket.close() + } + }, []) + + return ( +
+

WS Test

+
+
Status
+
{status}
+
+
+
URL
+
+ {TEST_WS_URL} +
+
+
+
Topics
+
+ {TEST_TOPICS.join(', ')} +
+
+
+
Logs
+
+ {logs.length === 0 ? ( +
No logs yet
+ ) : ( + logs.map((log) => ( +
+ [{log.at}] {log.message} +
+ )) + )} +
+
+
+ ) +} diff --git a/src/store/auth/auth-store.ts b/src/store/auth/auth-store.ts index 6f34b31..ee3ef42 100644 --- a/src/store/auth/auth-store.ts +++ b/src/store/auth/auth-store.ts @@ -1,42 +1,77 @@ import { create } from 'zustand' import { createJSONStorage, persist } from 'zustand/middleware' -import { AUTH_STORAGE_KEY } from '@/constants' +import { APP_PREFERENCES_STORAGE_KEY, AUTH_STORAGE_KEY } from '@/constants' +/**@description 未登录 | 已登录 | 正在从存储恢复数据 */ export type AuthStatus = 'anonymous' | 'authenticated' | 'restoring' export interface AuthUser { + createTime?: number + channelId?: number + coin?: string + currentStreak?: number email?: string + headImage?: string id: string + isJackpot?: boolean + lastBetPeriodNo?: string name?: string + oddsFactor?: number + phone?: string + registerInviteCode?: string + riskFlags?: number roles?: string[] + streakLevel?: number + username?: string + uuid?: string } export interface AuthSessionInput { accessToken: string + accessTokenExpiresAt?: number | null currentUser?: AuthUser | null refreshToken?: string | null } interface PersistedAuthState { accessToken: string | null + /** @description 用户登录态 `user-token` 的绝对过期时间戳(毫秒)。 */ + accessTokenExpiresAt: number | null + /** @description `/api/v1/authToken` 返回的服务端时间戳(秒),用于后续校时或服务端时间基准判断。 */ + apiAuthServerTime: number | null + apiAuthToken: string | null + /** @description 接口鉴权 `auth-token` 的绝对过期时间戳(毫秒)。 */ + apiAuthTokenExpiresAt: number | null currentUser: AuthUser | null refreshToken: string | null } +interface PersistedAppPreferenceState { + appLanguage: string | null + deviceId: string | null +} + interface AuthState extends PersistedAuthState { + clearApiAuthToken: () => void clearAccessToken: () => void clearSession: () => void finishHydration: () => void isHydrated: boolean lastUnauthorizedAt: string | null markUnauthorized: () => void + setApiAuthToken: (token: { + expiresAt: number + serverTime: number + value: string + }) => void setAccessToken: (token: string) => void setCurrentUser: (user: AuthUser | null) => void startSession: (session: AuthSessionInput) => void status: AuthStatus updateTokens: (tokens: { accessToken: string + accessTokenExpiresAt?: number | null refreshToken?: string | null }) => void } @@ -47,10 +82,25 @@ function resolveAuthStatus(accessToken: string | null): AuthStatus { const initialPersistedState: PersistedAuthState = { accessToken: null, + accessTokenExpiresAt: null, + apiAuthServerTime: null, + apiAuthToken: null, + apiAuthTokenExpiresAt: null, refreshToken: null, currentUser: null, } +function generateDeviceId() { + if ( + typeof crypto !== 'undefined' && + typeof crypto.randomUUID === 'function' + ) { + return `web_${crypto.randomUUID()}` + } + + return `web_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}` +} + export const useAuthStore = create()( persist( (set) => ({ @@ -58,9 +108,24 @@ export const useAuthStore = create()( status: 'restoring', isHydrated: false, lastUnauthorizedAt: null, + setApiAuthToken: ({ expiresAt, serverTime, value }) => { + set({ + apiAuthServerTime: serverTime, + apiAuthToken: value, + apiAuthTokenExpiresAt: expiresAt, + }) + }, + clearApiAuthToken: () => { + set({ + apiAuthServerTime: null, + apiAuthToken: null, + apiAuthTokenExpiresAt: null, + }) + }, setAccessToken: (token) => { set({ accessToken: token, + accessTokenExpiresAt: null, status: 'authenticated', isHydrated: true, }) @@ -73,20 +138,24 @@ export const useAuthStore = create()( }, startSession: ({ accessToken, + accessTokenExpiresAt = null, currentUser = null, refreshToken = null, }) => { set({ accessToken, + accessTokenExpiresAt, currentUser, refreshToken, status: 'authenticated', isHydrated: true, }) }, - updateTokens: ({ accessToken, refreshToken }) => { + updateTokens: ({ accessToken, accessTokenExpiresAt, refreshToken }) => { set((state) => ({ accessToken, + accessTokenExpiresAt: + accessTokenExpiresAt ?? state.accessTokenExpiresAt, refreshToken: refreshToken ?? state.refreshToken, status: 'authenticated', isHydrated: true, @@ -101,6 +170,7 @@ export const useAuthStore = create()( clearAccessToken: () => { set({ accessToken: null, + accessTokenExpiresAt: null, status: 'anonymous', isHydrated: true, }) @@ -126,6 +196,10 @@ export const useAuthStore = create()( storage: createJSONStorage(() => sessionStorage), partialize: (state) => ({ accessToken: state.accessToken, + accessTokenExpiresAt: state.accessTokenExpiresAt, + apiAuthServerTime: state.apiAuthServerTime, + apiAuthToken: state.apiAuthToken, + apiAuthTokenExpiresAt: state.apiAuthTokenExpiresAt, currentUser: state.currentUser, refreshToken: state.refreshToken, }), @@ -141,3 +215,53 @@ export const useAuthStore = create()( }, ), ) + +interface AppPreferenceStoreState extends PersistedAppPreferenceState { + getOrCreateDeviceId: () => string + setAppLanguage: (language: string) => void +} + +export const useAppPreferenceStore = create()( + persist( + (set, get) => ({ + appLanguage: null, + deviceId: null, + getOrCreateDeviceId: () => { + const deviceId = get().deviceId + + if (deviceId) { + return deviceId + } + + const nextDeviceId = generateDeviceId() + + set({ deviceId: nextDeviceId }) + + return nextDeviceId + }, + setAppLanguage: (language) => { + set({ appLanguage: language }) + }, + }), + { + name: APP_PREFERENCES_STORAGE_KEY, + storage: createJSONStorage(() => localStorage), + partialize: (state) => ({ + appLanguage: state.appLanguage, + deviceId: state.deviceId, + }), + }, + ), +) + +export function getAuthDeviceId() { + return useAppPreferenceStore.getState().getOrCreateDeviceId() +} + +export function getStoredAppLanguage() { + return useAppPreferenceStore.getState().appLanguage +} + +export function setStoredAppLanguage(language: string) { + useAppPreferenceStore.getState().setAppLanguage(language) +} diff --git a/src/store/game/game-round-store.ts b/src/store/game/game-round-store.ts index 214341e..d339df6 100644 --- a/src/store/game/game-round-store.ts +++ b/src/store/game/game-round-store.ts @@ -22,7 +22,13 @@ import { type GameRoundSlice = Pick< GameBootstrapSnapshot, - 'cells' | 'chips' | 'history' | 'round' | 'selections' | 'trends' + | 'cells' + | 'chips' + | 'history' + | 'maxSelectionCount' + | 'round' + | 'selections' + | 'trends' > export interface GameRoundStoreState extends GameRoundSlice { @@ -47,6 +53,7 @@ function createInitialRoundState(): GameRoundSlice & { activeChipId: string } { cells: snapshot.cells, chips: snapshot.chips, history: snapshot.history, + maxSelectionCount: snapshot.maxSelectionCount, round: snapshot.round, selections: snapshot.selections, trends: snapshot.trends, @@ -68,6 +75,7 @@ export const useGameRoundStore = create()((set) => ({ cells: snapshot.cells, chips: snapshot.chips, history: snapshot.history, + maxSelectionCount: snapshot.maxSelectionCount, round: snapshot.round, selections: snapshot.selections, trends: snapshot.trends, @@ -79,8 +87,19 @@ export const useGameRoundStore = create()((set) => ({ getChipById(state.chips, state.activeChipId) ?? state.chips.find((chip) => chip.isDefault) ?? state.chips[0] + const hasExistingSelection = state.selections.some( + (selection) => selection.cellId === cellId, + ) + const selectedCellCount = new Set( + state.selections.map((selection) => selection.cellId), + ).size - if (!activeChip || state.round.phase !== 'betting') { + if ( + !activeChip || + state.round.phase !== 'betting' || + hasExistingSelection || + selectedCellCount >= state.maxSelectionCount + ) { return state } @@ -165,7 +184,13 @@ export const selectSelectionsByCell = (state: GameRoundStoreState) => export type GameRoundStore = typeof useGameRoundStore export type GameRoundStoreData = Pick< GameRoundStoreState, - 'cells' | 'chips' | 'history' | 'round' | 'selections' | 'trends' + | 'cells' + | 'chips' + | 'history' + | 'maxSelectionCount' + | 'round' + | 'selections' + | 'trends' > export type { BetSelection, GameCell, HistoryEntry, RoundSnapshot, TrendEntry } diff --git a/src/store/game/game-session-store.ts b/src/store/game/game-session-store.ts index 1b27495..844c4c3 100644 --- a/src/store/game/game-session-store.ts +++ b/src/store/game/game-session-store.ts @@ -22,6 +22,9 @@ export interface GameSessionStoreState extends GameSessionSlice { dismissAnnouncement: (announcementId: string) => void hydrateSession: (snapshot: GameSessionSlice) => void markAnnouncementRead: (announcementId: string) => void + requestRealtimeConnection: () => void + resetRealtimeConnectionRequest: () => void + shouldConnectRealtime: boolean setConnectionLatency: (latencyMs: number | null) => void setConnectionStatus: (status: ConnectionStatus) => void syncConnection: (patch: Partial) => void @@ -40,6 +43,7 @@ function createInitialSessionState(): GameSessionSlice { export const useGameSessionStore = create()((set) => ({ ...createInitialSessionState(), + shouldConnectRealtime: false, dismissAnnouncement: (announcementId) => { set((state) => ({ announcements: { @@ -57,7 +61,10 @@ export const useGameSessionStore = create()((set) => ({ })) }, hydrateSession: (snapshot) => { - set(snapshot) + set((state) => ({ + ...snapshot, + shouldConnectRealtime: state.shouldConnectRealtime, + })) }, markAnnouncementRead: (announcementId) => { set((state) => ({ @@ -69,6 +76,12 @@ export const useGameSessionStore = create()((set) => ({ }, })) }, + requestRealtimeConnection: () => { + set({ shouldConnectRealtime: true }) + }, + resetRealtimeConnectionRequest: () => { + set({ shouldConnectRealtime: false }) + }, setConnectionLatency: (latencyMs) => { set((state) => ({ connection: { diff --git a/src/store/index.ts b/src/store/index.ts index 6f9fed7..1a1232f 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,2 +1,3 @@ export * from './auth' export * from './game' +export * from './modal' diff --git a/src/store/modal/index.ts b/src/store/modal/index.ts new file mode 100644 index 0000000..1a4ebe2 --- /dev/null +++ b/src/store/modal/index.ts @@ -0,0 +1 @@ +export * from './modal-store' diff --git a/src/store/modal/modal-store.ts b/src/store/modal/modal-store.ts new file mode 100644 index 0000000..94346ef --- /dev/null +++ b/src/store/modal/modal-store.ts @@ -0,0 +1,60 @@ +import { create } from 'zustand' +import type { WithdrawTopupType } from '@/type' + +export const MODAL_KEYS = [ + /**@description 桌面端登录弹窗*/ + 'desktopLogin', + /**@description 桌面端注册弹窗*/ + 'desktopRegister', + /**@description 桌面端用户信息弹窗*/ + 'desktopUserInfo', + /**@description 桌面端公告弹窗*/ + 'desktopNotice', + /**@description 桌面端自动托管弹窗*/ + 'desktopAutoSetting', + /**@description 桌面端充值提现前置选择弹窗*/ + 'desktopProcedures', + /**@description 桌面端充值/提现弹窗*/ + 'desktopWithdrawTopup', +] as const + +export type ModalKey = (typeof MODAL_KEYS)[number] + +type ModalVisibilityMap = Record + +const INITIAL_MODAL_VISIBILITY: ModalVisibilityMap = { + desktopLogin: false, + desktopRegister: false, + desktopUserInfo: false, + desktopNotice: false, + desktopAutoSetting: false, + desktopProcedures: false, + desktopWithdrawTopup: false, +} + +export interface ModalStoreState { + modals: ModalVisibilityMap + withdrawTopupType: WithdrawTopupType + closeAllModals: () => void + setModalOpen: (key: ModalKey, open: boolean) => void + setWithdrawTopupType: (type: WithdrawTopupType) => void +} + +export const useModalStore = create()((set) => ({ + modals: INITIAL_MODAL_VISIBILITY, + withdrawTopupType: 'withdraw', + closeAllModals: () => { + set({ modals: INITIAL_MODAL_VISIBILITY }) + }, + setModalOpen: (key, open) => { + set((state) => ({ + modals: { + ...state.modals, + [key]: open, + }, + })) + }, + setWithdrawTopupType: (type) => { + set({ withdrawTopupType: type }) + }, +})) diff --git a/src/style/index.css b/src/style/index.css index 470ebe0..e567c18 100644 --- a/src/style/index.css +++ b/src/style/index.css @@ -198,6 +198,7 @@ border-radius: 5px; padding: calc(var(--design-unit) * 8) calc(var(--design-unit) * 10); box-shadow: inset 0 0 8px rgba(128, 223, 231, 0.65); + color: #d5fbff; } .common-neon-inset-glow { @@ -346,6 +347,173 @@ height: 0; display: none; } + + .game-toaster { + --width: min( + calc(100vw - calc(var(--design-unit) * 32)), + calc(var(--design-unit) * 520) + ); + } + + .game-toast { + width: min( + calc(100vw - calc(var(--design-unit) * 32)), + calc(var(--design-unit) * 520) + ); + display: grid; + grid-template-columns: auto minmax(0, 1fr); + column-gap: calc(var(--design-unit) * 14); + align-items: center; + min-height: calc(var(--design-unit) * 60); + padding: calc(var(--design-unit) * 16) calc(var(--design-unit) * 20); + border-radius: calc(var(--design-unit) * 16); + border: 1px solid rgba(128, 223, 231, 0.68); + background: + linear-gradient(180deg, rgba(15, 35, 49, 0.98), rgba(6, 16, 24, 0.98)), + radial-gradient(circle at top, rgba(124, 232, 255, 0.22), transparent 58%); + box-shadow: + inset 0 0 calc(var(--design-unit) * 16) rgba(128, 223, 231, 0.18), + 0 0 calc(var(--design-unit) * 24) rgba(29, 190, 219, 0.26), + 0 calc(var(--design-unit) * 18) calc(var(--design-unit) * 52) + rgba(2, 8, 16, 0.42); + backdrop-filter: blur(14px); + position: relative; + overflow: hidden; + } + + .game-toast::before { + content: ""; + position: absolute; + inset: 1px; + border-radius: inherit; + border: 1px solid rgba(255, 255, 255, 0.04); + border-top-color: rgba(215, 250, 255, 0.32); + pointer-events: none; + } + + .game-toast-success { + border-color: rgba(79, 220, 155, 0.72); + box-shadow: + inset 0 0 calc(var(--design-unit) * 16) rgba(79, 220, 155, 0.16), + 0 0 calc(var(--design-unit) * 24) rgba(79, 220, 155, 0.24), + 0 calc(var(--design-unit) * 18) calc(var(--design-unit) * 52) + rgba(2, 8, 16, 0.42); + } + + .game-toast-error { + border-color: rgba(255, 94, 122, 0.68); + box-shadow: + inset 0 0 calc(var(--design-unit) * 16) rgba(255, 94, 122, 0.16), + 0 0 calc(var(--design-unit) * 24) rgba(255, 94, 122, 0.24), + 0 calc(var(--design-unit) * 18) calc(var(--design-unit) * 52) + rgba(2, 8, 16, 0.42); + } + + .game-toast-warning { + border-color: rgba(255, 214, 110, 0.72); + box-shadow: + inset 0 0 calc(var(--design-unit) * 16) rgba(255, 214, 110, 0.16), + 0 0 calc(var(--design-unit) * 24) rgba(255, 214, 110, 0.22), + 0 calc(var(--design-unit) * 18) calc(var(--design-unit) * 52) + rgba(2, 8, 16, 0.42); + } + + .game-toast-info, + .game-toast-loading, + .game-toast-default { + border-color: rgba(128, 223, 231, 0.52); + } + + .game-toast-icon { + display: flex; + align-items: center; + justify-content: center; + width: calc(var(--design-unit) * 30); + height: calc(var(--design-unit) * 30); + border-radius: 999px; + background: rgba(255, 255, 255, 0.04); + box-shadow: + inset 0 0 calc(var(--design-unit) * 8) rgba(255, 255, 255, 0.04), + 0 0 calc(var(--design-unit) * 12) rgba(124, 232, 255, 0.12); + } + + .game-toast-content { + min-width: 0; + padding-right: calc(var(--design-unit) * 22); + } + + .game-toast-title { + font-size: calc(var(--design-unit) * 17); + line-height: 1.3; + font-weight: 800; + letter-spacing: 0.03em; + color: #f2fdff; + text-shadow: + 0 0 calc(var(--design-unit) * 8) rgba(124, 232, 255, 0.18), + 0 0 calc(var(--design-unit) * 16) rgba(124, 232, 255, 0.08); + } + + .game-toast-description { + margin-top: calc(var(--design-unit) * 5); + font-size: calc(var(--design-unit) * 13); + line-height: 1.45; + color: rgba(213, 251, 255, 0.84); + } + + .game-toast-close { + position: absolute; + top: 50%; + right: calc(var(--design-unit) * 14); + transform: translateY(-50%); + display: flex; + align-items: center; + justify-content: center; + width: calc(var(--design-unit) * 28); + height: calc(var(--design-unit) * 28); + border-radius: 999px; + border: 1px solid rgba(128, 223, 231, 0.34); + background: rgba(4, 18, 27, 0.82); + box-shadow: + inset 0 0 calc(var(--design-unit) * 6) rgba(255, 255, 255, 0.04), + 0 0 calc(var(--design-unit) * 10) rgba(124, 232, 255, 0.14); + transition: + border-color 180ms ease, + background-color 180ms ease, + transform 180ms ease, + box-shadow 180ms ease; + cursor: pointer; + } + + .game-toast-close:hover { + transform: translateY(-50%) scale(1.04); + border-color: rgba(128, 223, 231, 0.58); + background: rgba(10, 28, 40, 0.96); + box-shadow: + inset 0 0 calc(var(--design-unit) * 8) rgba(255, 255, 255, 0.06), + 0 0 calc(var(--design-unit) * 14) rgba(124, 232, 255, 0.22); + } + + .game-toast-action, + .game-toast-cancel { + margin-top: calc(var(--design-unit) * 10); + border-radius: calc(var(--design-unit) * 999); + padding: calc(var(--design-unit) * 6) calc(var(--design-unit) * 12); + font-size: calc(var(--design-unit) * 12); + font-weight: 600; + cursor: pointer; + } + + .game-toast-action { + border: 1px solid rgba(128, 223, 231, 0.52); + background: rgba(10, 34, 47, 0.9); + color: #d5fbff; + } + + .game-toast-cancel { + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.06); + color: rgba(213, 251, 255, 0.74); + } } @theme inline { diff --git a/src/type/index.ts b/src/type/index.ts new file mode 100644 index 0000000..8e695bf --- /dev/null +++ b/src/type/index.ts @@ -0,0 +1,17 @@ +export type WithdrawTopupType = 'withdraw' | 'topup' + +/** @description 后端统一响应体结构。 */ +export interface ApiResponse { + code: number + msg?: string + data: T + message?: string +} + +/** @description 后端统一错误响应体结构。 */ +export interface ApiErrorOptions { + message: string + status?: number + data?: unknown + url?: string +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 89854c8..e102625 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -3,6 +3,7 @@ interface ImportMetaEnv { readonly VITE_APP_ENV: 'development' | 'production' | 'test' readonly VITE_API_BASE_URL: string + readonly VITE_WEBSOCKET_URL?: string readonly VITE_ENABLE_QUERY_DEVTOOLS: 'true' | 'false' readonly VITE_ENABLE_REQUEST_LOG: 'true' | 'false' } diff --git a/vite.config.ts b/vite.config.ts index 61bc0b8..2b05666 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -16,6 +16,12 @@ export default defineConfig({ port: 9999, host: '0.0.0.0', allowedHosts: ['darlena-nonexpiring-cathie.ngrok-free.dev'], + proxy: { + '/api': { + target: 'https://zihua-api.h55555game.top', + changeOrigin: true, + }, + }, }, plugins: [ tanstackRouter({