diff --git a/AGENTS.md b/AGENTS.md index 9d24f43..e4b2b30 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # GitNexus — Code Intelligence -This project is indexed by GitNexus as **36-character-flower** (2566 symbols, 4898 relationships, 220 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **36-character-flower** (2639 symbols, 5033 relationships, 226 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/CLAUDE.md b/CLAUDE.md index 9d24f43..e4b2b30 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # GitNexus — Code Intelligence -This project is indexed by GitNexus as **36-character-flower** (2566 symbols, 4898 relationships, 220 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **36-character-flower** (2639 symbols, 5033 relationships, 226 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/figma/img.jpg b/figma/img.jpg new file mode 100644 index 0000000..633cb60 Binary files /dev/null and b/figma/img.jpg differ diff --git a/public/favicon.svg b/public/favicon.svg index 6893eb1..1378682 100644 --- a/public/favicon.svg +++ b/public/favicon.svg @@ -1 +1,2988 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/system/chat.webp b/src/assets/system/chat.webp new file mode 100644 index 0000000..dc8ca70 Binary files /dev/null and b/src/assets/system/chat.webp differ diff --git a/src/components/app-boot-resource-gate.tsx b/src/components/app-boot-resource-gate.tsx index 6d2fc88..4f0debb 100644 --- a/src/components/app-boot-resource-gate.tsx +++ b/src/components/app-boot-resource-gate.tsx @@ -95,7 +95,7 @@ function AppLoadingOverlay({ progress }: { progress: number }) { role="status" aria-live="polite" aria-label="Loading" - className="fixed inset-0 z-[9999] flex items-center justify-center overflow-hidden bg-[#020913] text-[#D5FBFF]" + className="fixed inset-0 z-50 flex items-center justify-center overflow-hidden bg-[#020913] text-[#D5FBFF]" >
diff --git a/src/components/center-modal.tsx b/src/components/center-modal.tsx index 5260e95..4f528cf 100644 --- a/src/components/center-modal.tsx +++ b/src/components/center-modal.tsx @@ -1,10 +1,11 @@ -import { type ReactNode, useEffect } from 'react' +import { type ReactNode, useEffect, useRef } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import modalBg from '@/assets/system/modal-bg.webp' import modalClose from '@/assets/system/modal-close.webp' import modalNormalBg from '@/assets/system/modal-normal-bg.png' import { SmartBackground } from '@/components/smart-background.tsx' +import { acquireBodyScrollLock } from '@/lib/dom/body-scroll-lock' import { cn } from '@/lib/utils' interface CenterModalProps { @@ -16,6 +17,7 @@ interface CenterModalProps { isNormalBg?: boolean children?: ReactNode className?: string + backdropClassName?: string } const MODAL_HEADER_HEIGHT = 'calc(100% * 80 / 700)' @@ -30,44 +32,49 @@ export function CenterModal({ isNormalBg = false, children, className, + backdropClassName, }: CenterModalProps) { const { t } = useTranslation() + const onCloseRef = useRef(onClose) const handleClose = () => { onClose?.() } + useEffect(() => { + onCloseRef.current = onClose + }, [onClose]) + useEffect(() => { if (!open || typeof document === 'undefined') { return } - const previousOverflow = document.body.style.overflow + const releaseBodyScrollLock = acquireBodyScrollLock() const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { - onClose?.() + onCloseRef.current?.() } } - document.body.style.overflow = 'hidden' - - if (onClose) { - window.addEventListener('keydown', handleKeyDown) - } + window.addEventListener('keydown', handleKeyDown) return () => { - document.body.style.overflow = previousOverflow - if (onClose) { - window.removeEventListener('keydown', handleKeyDown) - } + releaseBodyScrollLock() + window.removeEventListener('keydown', handleKeyDown) } - }, [open, onClose]) + }, [open]) if (!open || typeof document === 'undefined') { return null } return createPortal( -
+
{ - document.body.style.overflow = previousOverflow - } + return acquireBodyScrollLock() }, [lockBodyScroll, open]) if (!open || !source || typeof document === 'undefined') { @@ -75,10 +69,9 @@ export function FullscreenLottieOverlay({ return createPortal(
+
+ ) +} + export function DesktopAuthSubmitError({ message, }: { diff --git a/src/features/auth/components/desktop-login-form-view.tsx b/src/features/auth/components/desktop-login-form-view.tsx index cad6b40..e9dd24e 100644 --- a/src/features/auth/components/desktop-login-form-view.tsx +++ b/src/features/auth/components/desktop-login-form-view.tsx @@ -6,7 +6,7 @@ import { Input } from '@/components/ui/input.tsx' import { DesktopAuthFieldRow, DesktopAuthInputError, - DesktopAuthSubmitError, + DesktopAuthPasswordInput, } from './desktop-auth-form-parts' interface DesktopLoginFormViewProps { @@ -20,7 +20,6 @@ interface DesktopLoginFormViewProps { onSwitchToRegister: () => void onUsernameChange: (value: string) => void password: string - submitError?: string | null username: string } @@ -32,7 +31,6 @@ export function DesktopLoginFormView({ onSwitchToRegister, onUsernameChange, password, - submitError, username, }: DesktopLoginFormViewProps) { const { t } = useTranslation() @@ -89,10 +87,9 @@ export function DesktopLoginFormView({ - onPasswordChange(event.target.value)} @@ -116,9 +113,6 @@ export function DesktopLoginFormView({
- state.setModalOpen) + const openExclusiveModal = useModalStore((state) => state.openExclusiveModal) const usernameField = useController({ control: form.control, name: 'username', @@ -22,8 +22,7 @@ export function DesktopLoginForm({ onSuccess }: DesktopLoginFormProps) { }) function handleSwitchToRegister() { - setModalOpen('desktopLogin', false) - setModalOpen('desktopRegister', true) + openExclusiveModal('desktopRegister') } return ( @@ -39,7 +38,6 @@ export function DesktopLoginForm({ onSuccess }: DesktopLoginFormProps) { onSubmit={onSubmit} onSwitchToRegister={handleSwitchToRegister} onUsernameChange={usernameField.field.onChange} - submitError={submitError} /> ) } diff --git a/src/features/auth/components/desktop-register-form-view.tsx b/src/features/auth/components/desktop-register-form-view.tsx index 3523ffd..ca00e6f 100644 --- a/src/features/auth/components/desktop-register-form-view.tsx +++ b/src/features/auth/components/desktop-register-form-view.tsx @@ -6,7 +6,7 @@ import { Input } from '@/components/ui/input.tsx' import { DesktopAuthFieldRow, DesktopAuthInputError, - DesktopAuthSubmitError, + DesktopAuthPasswordInput, } from './desktop-auth-form-parts' interface DesktopRegisterFormViewProps { @@ -26,7 +26,6 @@ interface DesktopRegisterFormViewProps { onUsernameChange: (value: string) => void password: string confirmPassword: string - submitError?: string | null username: string } @@ -42,7 +41,6 @@ export function DesktopRegisterFormView({ onSwitchToLogin, onUsernameChange, password, - submitError, username, }: DesktopRegisterFormViewProps) { const { t } = useTranslation() @@ -99,10 +97,9 @@ export function DesktopRegisterFormView({ - onPasswordChange(event.target.value)} @@ -128,10 +125,9 @@ export function DesktopRegisterFormView({ - onConfirmPasswordChange(event.target.value)} @@ -191,10 +187,6 @@ export function DesktopRegisterFormView({
- - state.setModalOpen) + const openExclusiveModal = useModalStore((state) => state.openExclusiveModal) const usernameField = useController({ control: form.control, name: 'username', @@ -30,8 +30,7 @@ export function DesktopRegisterForm({ onSuccess }: DesktopRegisterFormProps) { }) function handleSwitchToLogin() { - setModalOpen('desktopRegister', false) - setModalOpen('desktopLogin', true) + openExclusiveModal('desktopLogin') } return ( @@ -53,7 +52,6 @@ export function DesktopRegisterForm({ onSuccess }: DesktopRegisterFormProps) { onSubmit={onSubmit} onSwitchToLogin={handleSwitchToLogin} onUsernameChange={usernameField.field.onChange} - submitError={submitError} /> ) } diff --git a/src/features/auth/hooks/auth-error-key.ts b/src/features/auth/hooks/auth-error-key.ts index 4e55fe0..feea738 100644 --- a/src/features/auth/hooks/auth-error-key.ts +++ b/src/features/auth/hooks/auth-error-key.ts @@ -13,6 +13,22 @@ function fallbackKeyByContext(context: AuthSubmitContext) { : 'auth.register.errors.submitFailed' } +function hasServerMessage(error: ApiError) { + if (!error.data || typeof error.data !== 'object') { + return false + } + + const data = error.data + const serverMessage = + 'msg' in data && typeof data.msg === 'string' + ? data.msg + : 'message' in data && typeof data.message === 'string' + ? data.message + : null + + return Boolean(serverMessage && serverMessage === error.message) +} + export function toAuthSubmitErrorKey( error: unknown, context: AuthSubmitContext, @@ -28,6 +44,10 @@ export function toAuthSubmitErrorKey( return error.message } + if (hasServerMessage(error)) { + return error.message + } + if (error.status === 408) { return 'auth.errors.timeout' } diff --git a/src/features/auth/schema/auth-schema.ts b/src/features/auth/schema/auth-schema.ts index 1260af1..4c5b70b 100644 --- a/src/features/auth/schema/auth-schema.ts +++ b/src/features/auth/schema/auth-schema.ts @@ -1,12 +1,9 @@ import { z } from 'zod' -const malaysiaMobilePhonePattern = /^60\d{1,9}$/ - const usernameSchema = z .string() .trim() .min(1, 'auth.validation.username.required') - .regex(malaysiaMobilePhonePattern, 'auth.validation.username.invalidPhone') const passwordSchema = z .string() diff --git a/src/features/game/api/game-api.ts b/src/features/game/api/game-api.ts index 3a7f0cd..cc9361f 100644 --- a/src/features/game/api/game-api.ts +++ b/src/features/game/api/game-api.ts @@ -60,7 +60,9 @@ function unwrapGameEnvelope( message: typeof response.msg === 'string' && response.msg.length > 0 ? response.msg - : fallbackMessage, + : typeof response.message === 'string' && response.message.length > 0 + ? response.message + : fallbackMessage, }) } diff --git a/src/features/game/api/period-history-api.ts b/src/features/game/api/period-history-api.ts new file mode 100644 index 0000000..2b033ce --- /dev/null +++ b/src/features/game/api/period-history-api.ts @@ -0,0 +1,47 @@ +import { GAME_API_ENDPOINTS } from '@/constants' +import { api } from '@/lib/api/api-client' +import { ApiError } from '@/lib/api/api-error' +import type { ApiResponse } from '@/type' + +export interface GamePeriodHistoryItemDto { + open_time: number + period_no: string + result_number: number +} + +interface GamePeriodHistoryDto { + list: GamePeriodHistoryItemDto[] +} + +function unwrapPeriodHistoryEnvelope( + response: ApiResponse, +) { + if (response.code === 1) { + return response.data + } + + throw new ApiError({ + data: response, + message: + typeof response.msg === 'string' && response.msg.length > 0 + ? response.msg + : typeof response.message === 'string' && response.message.length > 0 + ? response.message + : 'Failed to load period history', + }) +} + +export async function getGamePeriodHistory(params: { limit?: number } = {}) { + const response = await api.get( + GAME_API_ENDPOINTS.periodHistory, + { + searchParams: { + limit: String(params.limit ?? 30), + }, + }, + ) + + return unwrapPeriodHistoryEnvelope( + response as ApiResponse, + ) +} diff --git a/src/features/game/components/desktop/desktop-animal-overlay.tsx b/src/features/game/components/desktop/desktop-animal-overlay.tsx index 62de547..5354242 100644 --- a/src/features/game/components/desktop/desktop-animal-overlay.tsx +++ b/src/features/game/components/desktop/desktop-animal-overlay.tsx @@ -346,7 +346,7 @@ export function DesktopAnimalOverlay({ return (
@@ -399,14 +399,14 @@ export function DesktopAnimalOverlay({ return (