-
Odds: 1:33
-
Streak: X2
-
Limit: 100
+
+
Odds: {oddsLabel}
+
Streak: {streakLabel}
+
Limit: {limitLabel}
{
console.log('countdown finished')
}}
/>
-
Round ID:20241026120
+
Round ID:{roundId}
-
(Menerima Taruhan)
+
{phaseDescription}
diff --git a/src/features/game/components/desktop/desktop-withdraw.tsx b/src/features/game/components/desktop/desktop-withdraw.tsx
index ae18492..4e0c7c7 100644
--- a/src/features/game/components/desktop/desktop-withdraw.tsx
+++ b/src/features/game/components/desktop/desktop-withdraw.tsx
@@ -1,5 +1,615 @@
+import { Minus, Plus } from 'lucide-react'
+import { type ReactNode, useState } from 'react'
+import lengthBlueBtn from '@/assets/system/length-blue-btn.webp'
+import lengthGreenBtn from '@/assets/system/length-green-btn.webp'
+import { SmartBackground } from '@/components/smart-background.tsx'
+import { Input } from '@/components/ui/input.tsx'
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select.tsx'
+import { cn } from '@/lib/utils'
+
+const AVAILABLE_BALANCE = 6628
+const MYR_PER_100_DIAMONDS = 1
+const USDT_TO_MYR_RATE = 4.049
+const VND_PER_DIAMOND = 10
+
+const QUICK_AMOUNTS = [
+ { diamonds: 210, preview: 'MYR 3' },
+ { diamonds: 2250, preview: 'MYR 30' },
+ { diamonds: 4000, preview: 'MYR 50' },
+ { diamonds: 8000, preview: 'MYR 100' },
+ { diamonds: 17000, preview: 'MYR 200' },
+ { diamonds: 45000, preview: 'MYR 500' },
+] as const
+
+const CURRENCY_OPTIONS = ['MYR'] as const
+
+const PAYMENT_CHANNELS = [
+ {
+ id: 'alipay-primary',
+ label: 'Alipay',
+ glyph: '支',
+ },
+ {
+ id: 'alipay-secondary',
+ label: 'Alipay',
+ glyph: '支',
+ },
+ {
+ id: 'alipay-third',
+ label: 'Alipay',
+ glyph: '支',
+ },
+] as const
+
+const BANK_OPTIONS = [
+ {
+ id: 'bca',
+ label: 'BCA',
+ brand: 'BCA',
+ subtitle: 'Bank Central Asia',
+ surface:
+ 'bg-[linear-gradient(180deg,rgba(251,252,255,0.98),rgba(224,239,255,0.96))] text-[#1E53A4]',
+ },
+ {
+ id: 'mandiri',
+ label: 'Mandiri',
+ brand: 'mandiri',
+ subtitle: 'Mandiri',
+ surface:
+ 'bg-[linear-gradient(180deg,rgba(26,53,93,0.98),rgba(9,22,43,0.96))] text-[#F5C247]',
+ },
+ {
+ id: 'bni',
+ label: 'BNI',
+ brand: 'BNI',
+ subtitle: 'BNI',
+ surface:
+ 'bg-[linear-gradient(180deg,rgba(254,253,252,0.98),rgba(239,242,247,0.96))] text-[#E1742B]',
+ },
+ {
+ id: 'bri',
+ label: 'BRI',
+ brand: 'BRI',
+ subtitle: 'BRI',
+ surface:
+ 'bg-[linear-gradient(180deg,rgba(253,254,255,0.98),rgba(234,243,255,0.96))] text-[#0E56A5]',
+ },
+] as const
+
+type PaymentChannelId = (typeof PAYMENT_CHANNELS)[number]['id']
+type BankId = (typeof BANK_OPTIONS)[number]['id']
+
+const numberFormatter = new Intl.NumberFormat('en-US')
+const fixedTwoFormatter = new Intl.NumberFormat('en-US', {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+})
+const fixedSixFormatter = new Intl.NumberFormat('en-US', {
+ minimumFractionDigits: 6,
+ maximumFractionDigits: 6,
+})
+
+const PANEL_CLASS =
+ 'rounded-md border border-[rgba(110,229,243,0.24)] bg-[linear-gradient(180deg,rgba(7,30,43,0.9),rgba(3,15,26,0.94))] shadow-[inset_0_0_calc(var(--design-unit)*14)_rgba(88,225,238,0.08),0_0_calc(var(--design-unit)*10)_rgba(32,163,186,0.12)]'
+
+const SELECTABLE_CARD_CLASS =
+ 'flex shrink-0 cursor-pointer flex-col items-center justify-between rounded-[calc(var(--design-unit)*6)] border px-design-8 py-design-8 transition'
+
+const SELECTABLE_CARD_ACTIVE_CLASS =
+ 'border-[#D18A43] bg-[linear-gradient(180deg,rgba(65,45,28,0.92),rgba(39,26,16,0.9))] shadow-[0_0_calc(var(--design-unit)*10)_rgba(209,138,67,0.18)]'
+
+const SELECTABLE_CARD_IDLE_CLASS =
+ 'border-[rgba(103,227,239,0.28)] bg-[linear-gradient(180deg,rgba(8,34,48,0.92),rgba(5,19,29,0.94))] hover:border-[rgba(170,247,255,0.7)]'
+
+function formatNumber(value: number) {
+ return numberFormatter.format(value)
+}
+
+function formatFixedTwo(value: number) {
+ return fixedTwoFormatter.format(value)
+}
+
+function formatFixedSix(value: number) {
+ return fixedSixFormatter.format(value)
+}
+
+function WithdrawField({
+ label,
+ children,
+ alignStart = true,
+}: {
+ label: string
+ children: ReactNode
+ alignStart?: boolean
+}) {
+ return (
+
+
+ {label}
+ :
+
+
+ {children}
+
+
+ )
+}
+
+function AmountShell({
+ amount,
+ onMinus,
+ onPlus,
+}: {
+ amount: number
+ onMinus: () => void
+ onPlus: () => void
+}) {
+ return (
+
+
+
+
+
+ {formatNumber(amount)}
+
+
+
+
+
+
+ Saldo Tersedia: {formatNumber(AVAILABLE_BALANCE)}
+
+
+ )
+}
+
+function QuickAmountCard({
+ amount,
+ preview,
+ active,
+ onClick,
+}: {
+ amount: number
+ preview: string
+ active: boolean
+ onClick: () => void
+}) {
+ return (
+
+ )
+}
+
+function PaymentCard({
+ active,
+ label,
+ glyph,
+ onClick,
+}: {
+ active: boolean
+ label: string
+ glyph: string
+ onClick: () => void
+}) {
+ return (
+
+ )
+}
+
+function BankCard({
+ active,
+ brand,
+ subtitle,
+ surface,
+ onClick,
+}: {
+ active: boolean
+ brand: string
+ subtitle: string
+ surface: string
+ onClick: () => void
+}) {
+ return (
+
+ )
+}
+
+function InputShell({
+ value,
+ onChange,
+ placeholder,
+ error,
+ errorMessage,
+ uppercase = false,
+}: {
+ value: string
+ onChange: (value: string) => void
+ placeholder: string
+ error?: boolean
+ errorMessage?: string
+ uppercase?: boolean
+}) {
+ return (
+
+
onChange(event.target.value)}
+ placeholder={placeholder}
+ className={cn(
+ 'h-design-42 rounded-[calc(var(--design-unit)*5)] border px-design-14 text-design-16',
+ uppercase && 'uppercase',
+ error
+ ? 'border-[#B93F44] bg-[rgba(34,13,16,0.78)] text-[#FCEEEE]'
+ : 'border-[rgba(103,227,239,0.24)] bg-[linear-gradient(180deg,rgba(10,47,57,0.84),rgba(5,23,32,0.92))] text-[#ACF1F6]',
+ )}
+ />
+ {error && errorMessage ? (
+
+ {errorMessage}
+
+ ) : null}
+
+ )
+}
+
+function PreviewRow({
+ label,
+ value,
+ highlight = false,
+}: {
+ label: string
+ value: ReactNode
+ highlight?: boolean
+}) {
+ return (
+
+
+ {label}
+
+
+ {value}
+
+
+ )
+}
+
function DesktopWithdraw() {
- return
DesktopWithdraw
+ const [amount, setAmount] = useState(6626)
+ const [currency, setCurrency] =
+ useState<(typeof CURRENCY_OPTIONS)[number]>('MYR')
+ const [paymentChannel, setPaymentChannel] =
+ useState
('alipay-primary')
+ const [bank, setBank] = useState('bca')
+ const [holderName, setHolderName] = useState('')
+ const [bankAccount, setBankAccount] = useState('')
+ const [receiverEmail, setReceiverEmail] = useState('')
+ const [receiverPhone, setReceiverPhone] = useState('')
+
+ const withdrawMyr = amount / 100
+ const withdrawVnd = amount * VND_PER_DIAMOND
+ const withdrawUsdt = withdrawMyr / USDT_TO_MYR_RATE
+
+ const selectedBank = BANK_OPTIONS.find((item) => item.id === bank)
+ const holderNameError = holderName.trim().length === 0
+ const bankAccountError = bankAccount.trim().length === 0
+
+ function handleAmountChange(nextAmount: number) {
+ setAmount(Math.max(0, nextAmount))
+ }
+
+ return (
+
+
+
+
+
+ handleAmountChange(amount - 1)}
+ onPlus={() => handleAmountChange(amount + 1)}
+ />
+
+
+
+
+
+
+
+
+
+ {QUICK_AMOUNTS.map((option) => (
+ handleAmountChange(option.diamonds)}
+ />
+ ))}
+
+
+
+
+
+ {PAYMENT_CHANNELS.map((channel) => (
+
setPaymentChannel(channel.id)}
+ />
+ ))}
+
+
+
+
+
+
+ {`014${selectedBank?.label ?? 'BCA'} (${selectedBank?.subtitle ?? 'BANK CENTRAL ASIA'}): 014`}
+
+
+ {BANK_OPTIONS.map((option) => (
+ setBank(option.id)}
+ />
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Pratinjau Penukaran
+
+
+
+
+
+
+ Nilai tukar berfungsi sebagai harga acuan; nilai tukar aktual yang
+ berlaku ditentukan pada saat penarikan.
+
+
+
+
+ Dompet Elektronik:{' '}
+ Minimal RM10
+
+
+ Bank: Minimal RM10
+
+
+ Waktu Pengerjaan:{' '}
+
+ Dana Tiba Hanya Dalam 9 Detik.
+
+
+
+ Melihat: Transaksi antara RM10 dan RM99,99 akan dikenakan biaya
+ penarikan minimum sebesar RM1.
+
+
+
+
+
+ Membatalkan
+
+
+ Konfirmasi
+
+ Penarikan
+
+
+
+
+
+
+ )
}
export default DesktopWithdraw
diff --git a/src/features/game/entry/mobile-entry.tsx b/src/features/game/entry/mobile-entry.tsx
index 51695a7..b129fa4 100644
--- a/src/features/game/entry/mobile-entry.tsx
+++ b/src/features/game/entry/mobile-entry.tsx
@@ -1,24 +1,3 @@
-import { DesktopAnimal } from '@/features/game/components/desktop/desktop-animal.tsx'
-import { DesktopGameHistory } from '@/features/game/components/desktop/desktop-game-history.tsx'
-import { DesktopTitle } from '@/features/game/components/desktop/desktop-title.tsx'
-
export function MobileEntry() {
- return (
- <>
-
-
-
-
-
-
-
-
- >
- )
+ return mobile component entry
}
diff --git a/src/features/game/entry/pc-entry.tsx b/src/features/game/entry/pc-entry.tsx
index 099d12b..cbba741 100644
--- a/src/features/game/entry/pc-entry.tsx
+++ b/src/features/game/entry/pc-entry.tsx
@@ -4,9 +4,7 @@ import { DesktopControl } from '@/features/game/components/desktop/desktop-contr
import { DesktopGameHistory } from '@/features/game/components/desktop/desktop-game-history.tsx'
import { DesktopStatusLine } from '@/features/game/components/desktop/desktop-status.tsx'
import DesktopAutoSettingModal from '@/features/game/modal/desktop/desktop-auto-setting-modal.tsx'
-import DesktopProceduresModal from '@/features/game/modal/desktop/desktop-procedures-modal.tsx'
import DesktopWithdrawTopupModal from '@/features/game/modal/desktop/desktop-withdraw-topup-modal.tsx'
-import DesktopRegisterModal from '../modal/desktop/desktop-register-modal'
export function PcEntry() {
return (
@@ -49,9 +47,9 @@ export function PcEntry() {
{/*公告弹窗*/}
{/**/}
{/*自动托管弹窗*/}
- {/* */}
+
{/* 充值提现前置选择弹窗*/}
-
+ {/**/}
{/* 充值和提现弹窗 */}
{/**/}
>
diff --git a/src/features/game/hooks/use-game-control-vm.ts b/src/features/game/hooks/use-game-control-vm.ts
new file mode 100644
index 0000000..b45a23a
--- /dev/null
+++ b/src/features/game/hooks/use-game-control-vm.ts
@@ -0,0 +1,42 @@
+import { useMemo } from 'react'
+import { CHIP_OPTIONS } from '@/constants'
+import { selectSelectionTotal, useGameRoundStore } from '@/store/game'
+
+const CHIP_IMAGE_MAP = new Map(
+ CHIP_OPTIONS.map((chip) => [chip.value, chip.src] as const),
+)
+
+export function useGameControlVm() {
+ const chips = useGameRoundStore((state) => state.chips)
+ const activeChipId = useGameRoundStore((state) => state.activeChipId)
+ const selections = useGameRoundStore((state) => state.selections)
+ const clearSelections = useGameRoundStore((state) => state.clearSelections)
+ const selectChip = useGameRoundStore((state) => state.selectChip)
+ const totalBetAmount = useGameRoundStore(selectSelectionTotal)
+
+ const chipItems = useMemo(
+ () =>
+ chips.map((chip) => ({
+ amount: chip.amount,
+ id: chip.id,
+ isSelected: chip.id === activeChipId,
+ src: CHIP_IMAGE_MAP.get(chip.amount) ?? CHIP_OPTIONS[0]?.src ?? '',
+ valueLabel: String(chip.amount),
+ })),
+ [activeChipId, chips],
+ )
+
+ const selectedChip =
+ chipItems.find((chip) => chip.id === activeChipId) ?? chipItems[0] ?? null
+
+ return {
+ canClear: selections.length > 0,
+ onChipSelect: selectChip,
+ onClearSelections: clearSelections,
+ selectedChipAmountLabel: selectedChip?.valueLabel ?? '--',
+ selectedChipId: activeChipId,
+ selectedCountLabel: `${selections.length}/5`,
+ totalBetAmountLabel: String(totalBetAmount),
+ chips: chipItems,
+ }
+}
diff --git a/src/features/game/hooks/use-game-history-vm.ts b/src/features/game/hooks/use-game-history-vm.ts
new file mode 100644
index 0000000..ac74c0e
--- /dev/null
+++ b/src/features/game/hooks/use-game-history-vm.ts
@@ -0,0 +1,43 @@
+import { useMemo } from 'react'
+import { useGameRoundStore } from '@/store/game'
+
+function formatSettledTime(iso: string) {
+ const date = new Date(iso)
+
+ if (Number.isNaN(date.getTime())) {
+ return '--'
+ }
+
+ return date.toLocaleString('zh-CN', {
+ hour12: false,
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ })
+}
+
+export function useGameHistoryVm() {
+ const history = useGameRoundStore((state) => state.history)
+
+ const items = useMemo(
+ () =>
+ history.map((entry) => ({
+ id: entry.roundId,
+ payoutMultiplierLabel: `${entry.payoutMultiplier}x`,
+ roundId: entry.roundId,
+ settledAtLabel: formatSettledTime(entry.settledAt),
+ statusLabel: 'settled',
+ totalPoolAmountLabel: entry.totalPoolAmount.toFixed(2),
+ winningCellIdLabel: String(entry.winningCellId),
+ })),
+ [history],
+ )
+
+ return {
+ emptyText: 'No history yet',
+ isEmpty: items.length === 0,
+ items,
+ }
+}
diff --git a/src/features/game/hooks/use-game-status-vm.ts b/src/features/game/hooks/use-game-status-vm.ts
new file mode 100644
index 0000000..f7c65b2
--- /dev/null
+++ b/src/features/game/hooks/use-game-status-vm.ts
@@ -0,0 +1,59 @@
+import { useMemo } from 'react'
+import { getRoundCountdownMs } from '@/features/game/shared/selectors'
+import { useGameRoundStore, useGameSessionStore } from '@/store/game'
+
+const PHASE_META = {
+ betting: {
+ description: '(Menerima Taruhan)',
+ label: 'OPEN',
+ toneClassName: 'text-[#78FF7F]',
+ },
+ locked: {
+ description: '(Taruhan Ditutup)',
+ label: 'LOCKED',
+ toneClassName: 'text-[#FFE375]',
+ },
+ revealing: {
+ description: '(Mengundi Hasil)',
+ label: 'DRAWING',
+ toneClassName: 'text-[#57E8FF]',
+ },
+ settled: {
+ description: '(Putaran Selesai)',
+ label: 'SETTLED',
+ toneClassName: 'text-[#FF9C6B]',
+ },
+ waiting: {
+ description: '(Menunggu Putaran Berikutnya)',
+ label: 'WAITING',
+ toneClassName: 'text-[#A7B6C7]',
+ },
+} as const
+
+export function useGameStatusVm() {
+ const cells = useGameRoundStore((state) => state.cells)
+ const round = useGameRoundStore((state) => state.round)
+ const trends = useGameRoundStore((state) => state.trends)
+ const dashboard = useGameSessionStore((state) => state.dashboard)
+
+ return useMemo(() => {
+ const oddsValue = cells[0]?.odds ?? '--'
+ const featuredTrend = trends.find(
+ (entry) => entry.cellId === dashboard.featuredCellId,
+ )
+ const phaseMeta = PHASE_META[round.phase]
+
+ return {
+ acceptingBets: round.phase === 'betting',
+ countdownMs: getRoundCountdownMs(round),
+ limitLabel: `${dashboard.tableLimitMin}-${dashboard.tableLimitMax}`,
+ oddsLabel: `1:${oddsValue}`,
+ phase: round.phase,
+ phaseDescription: phaseMeta.description,
+ phaseLabel: phaseMeta.label,
+ phaseToneClassName: phaseMeta.toneClassName,
+ roundId: round.id,
+ streakLabel: featuredTrend ? `X${featuredTrend.currentStreak}` : '--',
+ }
+ }, [cells, dashboard, round, trends])
+}
diff --git a/src/features/game/modal/desktop/desktop-auto-setting-modal.tsx b/src/features/game/modal/desktop/desktop-auto-setting-modal.tsx
index 7df392e..8322bed 100644
--- a/src/features/game/modal/desktop/desktop-auto-setting-modal.tsx
+++ b/src/features/game/modal/desktop/desktop-auto-setting-modal.tsx
@@ -2,6 +2,7 @@ import { useState } from 'react'
import lengthBlueBtn from '@/assets/system/length-blue-btn.webp'
import { CenterModal } from '@/components/center-modal.tsx'
import { SmartBackground } from '@/components/smart-background.tsx'
+import { Input } from '@/components/ui/input.tsx'
import { Switch } from '@/components/ui/switch.tsx'
const AUTO_STOP_ROWS = [
@@ -17,6 +18,7 @@ const AUTO_STOP_ROWS = [
},
{
label: 'Stop on any Jackpot',
+ // value: '50000',
checked: false,
},
] as const
@@ -66,7 +68,7 @@ function DesktopAutoSettingModal() {
'game-setting-input-shell flex h-design-58 w-design-410 items-center justify-between pl-design-18 pr-design-10'
}
>
-
Akun/TEL:
-