docs(game): 添加游戏模块数据

- 新增 useGameBoardVm 数据层实施说明文档
- 添加 36字花核心玩法与前端规则摘要
- 创建游戏模块数据与界面分层第一阶段实施稿
- 定义四层架构:api/dto、store、view-model hooks、ui层
- 规范 PC 与 Mobile 共享业务逻辑的改造方案
- 明确各层职责边界和组件改造顺序
This commit is contained in:
JiaJun
2026-05-09 17:52:30 +08:00
parent 7622d4121f
commit 6aaf90a6ac
28 changed files with 2635 additions and 258 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 73 KiB

View File

@@ -0,0 +1,18 @@
import type * as React from 'react'
import { cn } from '@/lib/utils'
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
return (
<input
type={type}
data-slot="input"
className={cn(
'w-full min-w-0 rounded-md border border-transparent bg-[#135E65]/60 px-design-30 py-design-15 text-design-20 text-[#D9FFFF] outline-none transition placeholder:text-[rgba(116,173,175,0.72)] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 focus-visible:border-[rgba(110,255,255,0.72)] focus-visible:ring-0 focus-visible:shadow-[0_0_0_calc(var(--design-unit)*1.5)_rgba(110,255,255,0.16),0_0_calc(var(--design-unit)*8)_rgba(48,214,255,0.36),0_0_calc(var(--design-unit)*18)_rgba(18,162,255,0.22),inset_0_0_calc(var(--design-unit)*6)_rgba(110,255,255,0.08)] aria-invalid:border-[#DF5B5B] aria-invalid:bg-[rgba(78,17,23,0.45)] aria-invalid:text-[#FFF2F2] aria-invalid:focus-visible:shadow-none',
className,
)}
{...props}
/>
)
}
export { Input }

View File

@@ -0,0 +1,192 @@
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react'
import { Select as SelectPrimitive } from 'radix-ui'
import type * as React from 'react'
import { cn } from '@/lib/utils'
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return (
<SelectPrimitive.Group
data-slot="select-group"
className={cn('scroll-my-1 p-1', className)}
{...props}
/>
)
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = 'default',
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: 'sm' | 'default'
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = 'item-aligned',
align = 'center',
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
data-align-trigger={position === 'item-aligned'}
className={cn(
'relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className,
)}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
data-position={position}
className={cn(
'data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)',
position === 'popper' && '',
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn('px-1.5 py-1 text-xs text-muted-foreground', className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}
>
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="pointer-events-none" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn('pointer-events-none -mx-1 my-1 h-px bg-border', className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View File

@@ -11,48 +11,47 @@ import reduce from '@/assets/game/reduce.webp'
import totalBg from '@/assets/game/total-bg.webp'
import { SmartBackground } from '@/components/smart-background.tsx'
import { SmartImage } from '@/components/smart-image.tsx'
import { ACTION_OPTIONS, CHIP_OPTIONS } from '@/constants'
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 [chips, setChips] = useState(CHIP_OPTIONS)
const [selectedChipId, setSelectedChipId] = useState(
CHIP_OPTIONS[CHIP_OPTIONS.length - 1]?.id ?? '',
)
const {
canClear,
chips,
onChipSelect,
onClearSelections,
selectedChipAmountLabel,
selectedChipId,
selectedCountLabel,
totalBetAmountLabel,
} = useGameControlVm()
const [clickedId, setClickedId] = useState<string | null>(null)
const [hidingId, setHidingId] = useState<string | null>(null)
const [confirmClicked, setConfirmClicked] = useState(false)
const selectedChip =
chips.find((chip) => chip.id === selectedChipId) ?? CHIP_OPTIONS[0]
const handleChipClick = (chipId: string) => {
setSelectedChipId(chipId)
setChips((current) => {
const next = [...current]
const index = next.findIndex((chip) => chip.id === chipId)
if (index === -1 || index === next.length - 1) {
return next
}
const [selected] = next.splice(index, 1)
next.push(selected)
return next
})
onChipSelect(chipId)
}
const handleActionClick = useCallback((id: string) => {
setClickedId(id)
setTimeout(() => {
setClickedId(null)
setHidingId(id)
const handleActionClick = useCallback(
(id: string) => {
if (id === 'clear' && canClear) {
onClearSelections()
}
setClickedId(id)
setTimeout(() => {
setHidingId(null)
}, 180)
}, 200)
}, [])
setClickedId(null)
setHidingId(id)
setTimeout(() => {
setHidingId(null)
}, 180)
}, 200)
},
[canClear, onClearSelections],
)
const handleConfirmClick = useCallback(() => {
setConfirmClicked(true)
@@ -202,7 +201,7 @@ export function DesktopControl() {
>
<motion.img
src={chip.src}
alt={`chip-${chip.value}`}
alt={`chip-${chip.amount}`}
draggable={false}
className={'h-design-70 w-design-70 object-contain'}
/>
@@ -225,7 +224,7 @@ export function DesktopControl() {
<div
className={'w-design-80 h-full flex items-center justify-center'}
>
{selectedChip.value}
{selectedChipAmountLabel}
</div>
<SmartImage
src={reduce}
@@ -241,8 +240,8 @@ export function DesktopControl() {
'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'
}
>
<div>SELECTED:3/5</div>
<div>Total Bet150</div>
<div>SELECTED:{selectedCountLabel}</div>
<div>Total Bet{totalBetAmountLabel}</div>
</SmartBackground>
<SmartBackground
src={controlBg}

View File

@@ -1,119 +1,9 @@
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 data = [
{
order_no: 'BET202604290001',
period_no: '202604290101',
numbers: [3, 8, 12],
bet_amount: '100.00',
total_amount: '100.00',
result_number: 8,
win_amount: '330.00',
status: 'won',
create_time: 1745881200,
},
{
order_no: 'BET202604290002',
period_no: '202604290102',
numbers: [5],
bet_amount: '50.00',
total_amount: '50.00',
result_number: 11,
win_amount: '0.00',
status: 'lost',
create_time: 1745882100,
},
{
order_no: 'BET202604290003',
period_no: '202604290103',
numbers: [1, 7],
bet_amount: '88.00',
total_amount: '88.00',
result_number: 7,
win_amount: '176.00',
status: 'won',
create_time: 1745883000,
},
{
order_no: 'BET202604290004',
period_no: '202604290104',
numbers: [9, 10, 15],
bet_amount: '120.00',
total_amount: '120.00',
result_number: 4,
win_amount: '0.00',
status: 'settled',
create_time: 1745883900,
},
{
order_no: 'BET202604290005',
period_no: '202604290105',
numbers: [6],
bet_amount: '66.00',
total_amount: '66.00',
result_number: null,
win_amount: '0.00',
status: 'pending',
create_time: 1745884800,
},
{
order_no: 'BET202604290006',
period_no: '202604290106',
numbers: [2, 14],
bet_amount: '200.00',
total_amount: '200.00',
result_number: 14,
win_amount: '400.00',
status: 'won',
create_time: 1745885700,
},
{
order_no: 'BET202604290007',
period_no: '202604290107',
numbers: [13],
bet_amount: '30.00',
total_amount: '30.00',
result_number: 13,
win_amount: '99.00',
status: 'won',
create_time: 1745886600,
},
{
order_no: 'BET202604290008',
period_no: '202604290108',
numbers: [4, 16],
bet_amount: '150.00',
total_amount: '150.00',
result_number: 1,
win_amount: '0.00',
status: 'lost',
create_time: 1745887500,
},
{
order_no: 'BET202604290009',
period_no: '202604290109',
numbers: [11, 18, 20],
bet_amount: '300.00',
total_amount: '300.00',
result_number: null,
win_amount: '0.00',
status: 'pending',
create_time: 1745888400,
},
{
order_no: 'BET202604290010',
period_no: '202604290110',
numbers: [17],
bet_amount: '80.00',
total_amount: '80.00',
result_number: 17,
win_amount: '264.00',
status: 'won',
create_time: 1745889300,
},
]
const { emptyText, isEmpty, items } = useGameHistoryVm()
return (
<SmartBackground
@@ -133,49 +23,66 @@ export function DesktopGameHistory() {
'history-scroll-hidden z-10 flex min-h-0 flex-1 w-full flex-col gap-design-10 overflow-y-auto overflow-x-hidden px-design-20 py-design-20'
}
>
{data.map((item) => {
return (
<div
key={item.order_no}
className={
'common-neon-inset flex w-full flex-col items-center !p-0 text-[#FFE375]'
}
>
{isEmpty ? (
<div
className={
'flex w-full flex-1 items-center justify-center text-design-18 text-[#84A2A2]'
}
>
{emptyText}
</div>
) : (
items.map((item) => {
return (
<div
key={item.id}
className={
'common-neon-inset w-full !rounded-b-none text-center text-design-20'
'common-neon-inset flex w-full flex-col items-center !p-0 text-[#FFE375]'
}
>
{item.status}
</div>
<div
className={
'flex w-full flex-col gap-design-5 px-design-10 py-design-10 text-design-16'
}
>
<div>
<span className={'text-[#84A2A2]'}>Round ID: </span>
<span className={'text-[#C0E7EB]'}>{item.order_no}</span>
<div
className={
'common-neon-inset w-full !rounded-b-none text-center text-design-20'
}
>
{item.statusLabel}
</div>
<div>
<span className={'text-[#84A2A2]'}>Animals Bet: </span>
<span>{item.numbers.join(', ')}</span>
</div>
<div>
<span className={'text-[#84A2A2]'}>Total Bet Amount: </span>
<span className={'text-[#FFE375]'}>{item.bet_amount}</span>
</div>
<div>
<span className={'text-[#84A2A2]'}> Winning Result:</span>
<span className={'text-[#FF7575]'}>
{' '}
{item.result_number === null ? '--' : item.result_number}
</span>
<div
className={
'flex w-full flex-col gap-design-5 px-design-10 py-design-10 text-design-16'
}
>
<div>
<span className={'text-[#84A2A2]'}>Round ID: </span>
<span className={'text-[#C0E7EB]'}>{item.roundId}</span>
</div>
<div>
<span className={'text-[#84A2A2]'}>Settled At: </span>
<span>{item.settledAtLabel}</span>
</div>
<div>
<span className={'text-[#84A2A2]'}>
Total Pool Amount:{' '}
</span>
<span className={'text-[#FFE375]'}>
{item.totalPoolAmountLabel}
</span>
</div>
<div>
<span className={'text-[#84A2A2]'}>Winning Result: </span>
<span className={'text-[#FF7575]'}>
{item.winningCellIdLabel}
</span>
</div>
<div>
<span className={'text-[#84A2A2]'}>Payout: </span>
<span>{item.payoutMultiplierLabel}</span>
</div>
</div>
</div>
</div>
)
})}
)
})
)}
</div>
</SmartBackground>
)

View File

@@ -3,8 +3,20 @@ import statusLine from '@/assets/system/status-line.webp'
import { SmartBackground } from '@/components/smart-background.tsx'
import { DesktopCountdown } from '@/features/game/components/desktop/desktop-countdown.tsx'
import { DesktopTitle } from '@/features/game/components/desktop/desktop-title.tsx'
import { useGameStatusVm } from '@/features/game/hooks/use-game-status-vm.ts'
export function DesktopStatusLine() {
const {
countdownMs,
limitLabel,
oddsLabel,
phaseDescription,
phaseLabel,
phaseToneClassName,
roundId,
streakLabel,
} = useGameStatusVm()
return (
<div className={'relative w-full flex flex-col text-design-22'}>
<SmartBackground
@@ -12,10 +24,12 @@ export function DesktopStatusLine() {
size="100% 100%"
className="w-full h-design-60 bg-no-repeat bg-center flex items-center justify-center"
>
<div className={'flex-1 flex items-center justify-center'}>
<div>Odds: 1:33</div>
<div>Streak: X2</div>
<div>Limit: 100</div>
<div
className={'flex-1 flex items-center justify-center gap-design-24'}
>
<div>Odds: {oddsLabel}</div>
<div>Streak: {streakLabel}</div>
<div>Limit: {limitLabel}</div>
</div>
<SmartBackground
src={statusCenter}
@@ -23,22 +37,22 @@ export function DesktopStatusLine() {
size="contain"
>
<DesktopCountdown
initialSeconds={30}
initialMs={countdownMs}
onComplete={() => {
console.log('countdown finished')
}}
/>
</SmartBackground>
<div className={'flex-1 flex items-center justify-center gap-10'}>
<div>Round ID:20241026120</div>
<div>Round ID:{roundId}</div>
<div className={'flex items-center gap-2'}>
<div className={'flex items-center gap-2'}>
<div
className={'w-design-20 h-design-20 bg-[#78FF7F] rounded-[50%]'}
></div>
<div className={'text-[#78FF7F]'}>OPEN</div>
<div className={phaseToneClassName}>{phaseLabel}</div>
</div>
<div>(Menerima Taruhan)</div>
<div>{phaseDescription}</div>
</div>
</div>
</SmartBackground>

View File

@@ -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 (
<div className="flex gap-design-14">
<div className="flex w-design-108 shrink-0 items-center justify-end text-right text-design-16 font-medium uppercase leading-[1.15] tracking-[0.04em] text-[#6FD4DA]">
<span>{label}</span>
<span className="pl-design-4">:</span>
</div>
<div
className={cn(
'min-w-0 flex-1',
alignStart ? 'pt-design-2' : 'flex items-center',
)}
>
{children}
</div>
</div>
)
}
function AmountShell({
amount,
onMinus,
onPlus,
}: {
amount: number
onMinus: () => void
onPlus: () => void
}) {
return (
<div className="flex flex-col gap-design-6">
<div className="flex h-design-52 items-center gap-design-10 rounded-[calc(var(--design-unit)*6)] border border-[rgba(103,227,239,0.32)] bg-[linear-gradient(180deg,rgba(14,64,74,0.82),rgba(8,36,47,0.78))] px-design-10 shadow-[inset_0_0_calc(var(--design-unit)*12)_rgba(93,239,255,0.08)]">
<button
type="button"
onClick={onMinus}
className="flex h-design-34 w-design-34 shrink-0 items-center justify-center rounded-[calc(var(--design-unit)*4)] border border-[rgba(109,232,244,0.44)] bg-[rgba(37,115,123,0.32)] text-[#E1FEFF] transition hover:border-[rgba(170,247,255,0.82)] hover:bg-[rgba(66,146,151,0.35)]"
>
<Minus className="h-design-16 w-design-16" />
</button>
<div className="flex min-w-0 flex-1 items-center justify-center text-design-24 font-medium tracking-[0.04em] text-[#A1EBF3]">
{formatNumber(amount)}
</div>
<button
type="button"
onClick={onPlus}
className="flex h-design-34 w-design-34 shrink-0 items-center justify-center rounded-[calc(var(--design-unit)*4)] border border-[rgba(109,232,244,0.44)] bg-[rgba(37,115,123,0.32)] text-[#E1FEFF] transition hover:border-[rgba(170,247,255,0.82)] hover:bg-[rgba(66,146,151,0.35)]"
>
<Plus className="h-design-16 w-design-16" />
</button>
</div>
<div className="pl-design-8 text-design-14 text-[#6DAAB0]">
Saldo Tersedia: {formatNumber(AVAILABLE_BALANCE)}
</div>
</div>
)
}
function QuickAmountCard({
amount,
preview,
active,
onClick,
}: {
amount: number
preview: string
active: boolean
onClick: () => void
}) {
return (
<button
type="button"
onClick={onClick}
className={cn(
'flex h-design-68 w-design-104 shrink-0 cursor-pointer flex-col items-center justify-center rounded-[calc(var(--design-unit)*6)] border transition',
active
? 'border-[#D18A43] bg-[linear-gradient(180deg,rgba(84,48,24,0.92),rgba(60,34,18,0.88))] shadow-[0_0_calc(var(--design-unit)*10)_rgba(209,138,67,0.18)]'
: 'border-[rgba(103,227,239,0.32)] bg-[linear-gradient(180deg,rgba(10,44,58,0.84),rgba(5,21,32,0.92))] hover:border-[rgba(170,247,255,0.7)]',
)}
>
<div className="text-design-24 font-semibold leading-none text-[#FFE229]">
{amount}
</div>
<div className="pt-design-6 text-design-12 uppercase leading-none tracking-[0.04em] text-[#63AEB6]">
{preview}
</div>
</button>
)
}
function PaymentCard({
active,
label,
glyph,
onClick,
}: {
active: boolean
label: string
glyph: string
onClick: () => void
}) {
return (
<button
type="button"
onClick={onClick}
className={cn(
SELECTABLE_CARD_CLASS,
'h-design-92 w-design-86',
active ? SELECTABLE_CARD_ACTIVE_CLASS : SELECTABLE_CARD_IDLE_CLASS,
)}
>
<div
className={cn(
'flex h-design-58 w-full items-center justify-center rounded-[calc(var(--design-unit)*4)] text-design-42 font-semibold leading-none',
active
? 'bg-[linear-gradient(180deg,#1F9DE8,#0E6BCF)] text-white'
: 'bg-[linear-gradient(180deg,#1C96DF,#0B6ECF)] text-white',
)}
>
{glyph}
</div>
<div className="text-design-14 text-[#AEE8EE]">{label}</div>
</button>
)
}
function BankCard({
active,
brand,
subtitle,
surface,
onClick,
}: {
active: boolean
brand: string
subtitle: string
surface: string
onClick: () => void
}) {
return (
<button
type="button"
onClick={onClick}
className={cn(
SELECTABLE_CARD_CLASS,
'h-design-86 w-design-86',
active ? SELECTABLE_CARD_ACTIVE_CLASS : SELECTABLE_CARD_IDLE_CLASS,
)}
>
<div
className={cn(
'flex h-design-52 w-full items-center justify-center rounded-[calc(var(--design-unit)*4)] text-design-20 font-bold uppercase',
surface,
)}
>
{brand}
</div>
<div className="text-design-13 text-[#AEE8EE]">{subtitle}</div>
</button>
)
}
function InputShell({
value,
onChange,
placeholder,
error,
errorMessage,
uppercase = false,
}: {
value: string
onChange: (value: string) => void
placeholder: string
error?: boolean
errorMessage?: string
uppercase?: boolean
}) {
return (
<div className="flex flex-col gap-design-5">
<Input
value={value}
onChange={(event) => 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 ? (
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
{errorMessage}
</div>
) : null}
</div>
)
}
function PreviewRow({
label,
value,
highlight = false,
}: {
label: string
value: ReactNode
highlight?: boolean
}) {
return (
<div className="flex border-b border-[rgba(89,209,223,0.2)] last:border-b-0">
<div className="flex w-[44%] shrink-0 items-center border-r border-[rgba(89,209,223,0.2)] px-design-14 py-design-20 text-design-16 font-medium uppercase leading-[1.15] text-[#7CE3E8]">
{label}
</div>
<div
className={cn(
'flex min-w-0 flex-1 items-center justify-end px-design-14 py-design-20 text-right text-design-16 text-[#E6FFFF]',
highlight && 'text-design-18 font-semibold text-[#6DFF83]',
)}
>
{value}
</div>
</div>
)
}
function DesktopWithdraw() {
return <div>DesktopWithdraw</div>
const [amount, setAmount] = useState(6626)
const [currency, setCurrency] =
useState<(typeof CURRENCY_OPTIONS)[number]>('MYR')
const [paymentChannel, setPaymentChannel] =
useState<PaymentChannelId>('alipay-primary')
const [bank, setBank] = useState<BankId>('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 (
<div className="flex h-full min-h-0 w-full px-design-12 pb-design-12 text-[#D9FFFF]">
<div
className={cn(
PANEL_CLASS,
'flex h-full min-h-0 w-full min-w-0 overflow-y-auto',
)}
>
<div className="flex min-h-full min-w-0 flex-[1.7] flex-col px-design-16 py-design-14">
<div className="flex flex-col gap-design-12">
<WithdrawField label="Jumlah Penarikan Berlian">
<AmountShell
amount={amount}
onMinus={() => handleAmountChange(amount - 1)}
onPlus={() => handleAmountChange(amount + 1)}
/>
</WithdrawField>
<WithdrawField label="Jenis Mata Uang" alignStart={false}>
<Select
value={currency}
onValueChange={(value) =>
setCurrency(value as (typeof CURRENCY_OPTIONS)[number])
}
>
<SelectTrigger
className="h-design-52 w-full rounded-[calc(var(--design-unit)*6)] border-[rgba(103,227,239,0.3)] bg-[linear-gradient(180deg,rgba(12,61,72,0.82),rgba(6,28,39,0.9))] px-design-16 text-left text-design-20 font-semibold text-[#A5EDF4] shadow-[inset_0_0_calc(var(--design-unit)*12)_rgba(94,237,255,0.08)] data-[size=default]:h-design-52 [&_svg]:h-design-18 [&_svg]:w-design-18 [&_svg]:text-[#79DFEA]"
aria-label="Currency selection"
>
<SelectValue placeholder="Select currency" />
</SelectTrigger>
<SelectContent
position="popper"
className="min-w-(--radix-select-trigger-width) rounded-[calc(var(--design-unit)*6)] border border-[rgba(103,227,239,0.3)] bg-[linear-gradient(180deg,rgba(8,36,48,0.98),rgba(4,18,28,0.98))] text-[#CFFDFF] shadow-[0_0_calc(var(--design-unit)*16)_rgba(56,241,255,0.12)]"
>
{CURRENCY_OPTIONS.map((option) => (
<SelectItem
key={option}
value={option}
className="rounded-[calc(var(--design-unit)*4)] px-design-12 py-design-10 text-design-18 focus:bg-[rgba(53,154,171,0.2)] focus:text-white"
>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</WithdrawField>
<div className="flex gap-design-14">
<div className="w-design-108 shrink-0" />
<div className="flex min-w-0 flex-1 flex-wrap gap-design-10">
{QUICK_AMOUNTS.map((option) => (
<QuickAmountCard
key={option.diamonds}
amount={option.diamonds}
preview={option.preview}
active={option.diamonds === amount}
onClick={() => handleAmountChange(option.diamonds)}
/>
))}
</div>
</div>
<WithdrawField label="Saluran Pembayaran">
<div className="flex flex-wrap gap-design-10">
{PAYMENT_CHANNELS.map((channel) => (
<PaymentCard
key={channel.id}
active={channel.id === paymentChannel}
label={channel.label}
glyph={channel.glyph}
onClick={() => setPaymentChannel(channel.id)}
/>
))}
</div>
</WithdrawField>
<WithdrawField label="Kode Bank">
<div className="flex flex-col gap-design-10">
<div className="flex h-design-40 items-center rounded-[calc(var(--design-unit)*6)] border border-[rgba(103,227,239,0.28)] bg-[linear-gradient(180deg,rgba(12,61,72,0.78),rgba(6,28,39,0.88))] px-design-12 text-design-15 uppercase tracking-[0.02em] text-[#A4EAF2] shadow-[inset_0_0_calc(var(--design-unit)*10)_rgba(94,237,255,0.07)]">
{`014${selectedBank?.label ?? 'BCA'} (${selectedBank?.subtitle ?? 'BANK CENTRAL ASIA'}): 014`}
</div>
<div className="flex flex-wrap gap-design-10">
{BANK_OPTIONS.map((option) => (
<BankCard
key={option.id}
active={option.id === bank}
brand={option.brand}
subtitle={option.label}
surface={option.surface}
onClick={() => setBank(option.id)}
/>
))}
</div>
</div>
</WithdrawField>
<WithdrawField label="Nama Pemegang Kartu">
<InputShell
value={holderName}
onChange={setHolderName}
placeholder="Mohon masukkan nama pemegang kartu."
error={holderNameError}
errorMessage="Mohon masukkan nama pemegang kartu."
/>
</WithdrawField>
<WithdrawField label="Nomor Rekening Bank">
<InputShell
value={bankAccount}
onChange={setBankAccount}
placeholder="Silakan masukkan nomor rekening bank Anda."
error={bankAccountError}
errorMessage="Silakan masukkan nomor rekening bank Anda."
/>
</WithdrawField>
<WithdrawField label="Email Penerima" alignStart={false}>
<InputShell
value={receiverEmail}
onChange={setReceiverEmail}
placeholder="SILAKAN MASUKKAN ALAMAT EMAIL PENERIMA."
uppercase={true}
/>
</WithdrawField>
<WithdrawField label="Nomor Ponsel Penerima" alignStart={false}>
<InputShell
value={receiverPhone}
onChange={setReceiverPhone}
placeholder="SILAKAN MASUKKAN ALAMAT EMAIL PENERIMA."
uppercase={true}
/>
</WithdrawField>
</div>
</div>
<div className="w-px shrink-0 bg-[linear-gradient(180deg,rgba(89,209,223,0)_0%,rgba(89,209,223,0.4)_12%,rgba(89,209,223,0.5)_88%,rgba(89,209,223,0)_100%)]" />
<div className="flex min-h-full min-w-0 w-design-520 shrink-0 flex-col">
<div className="flex h-design-44 items-center border-b border-[rgba(89,209,223,0.2)] bg-[linear-gradient(90deg,rgba(18,99,110,0.8),rgba(7,68,79,0.9))] px-design-12 text-design-20 font-semibold uppercase tracking-[0.04em] text-[#9AF5FB]">
Pratinjau Penukaran
</div>
<div className="flex flex-1 flex-col gap-design-12 px-design-10 py-design-10">
<div className="overflow-hidden rounded-[calc(var(--design-unit)*4)] border border-[rgba(89,209,223,0.22)] bg-[rgba(4,19,28,0.58)]">
<PreviewRow label="Jumlah Berlian" value={formatNumber(amount)} />
<PreviewRow
label="Kurs (MYR)"
value={`${100 * MYR_PER_100_DIAMONDS} BERLIAN = 1 MYR`}
/>
<PreviewRow
label="Dapat Ditukarkan MYR"
value={`RM ${formatFixedTwo(withdrawMyr)}`}
highlight={true}
/>
<PreviewRow
label="Nilai Tukar USDT/MYR"
value={`1 USDT = RM ${USDT_TO_MYR_RATE}`}
/>
<PreviewRow
label="Nilai Tukar (VND)"
value={`${VND_PER_DIAMOND} BERLIAN = 1 VND`}
/>
<PreviewRow
label="Dapat Dikonversi ke VND"
value={`${formatNumber(withdrawVnd)} VND`}
highlight={true}
/>
<PreviewRow
label="Dapat Ditukarkan dengan USDT"
value={`${formatFixedSix(withdrawUsdt)} USDT`}
highlight={true}
/>
<PreviewRow
label="Jumlah Berlian Nilai Tukar Tetap"
value="0-0-0 0:0:0"
/>
</div>
<div className="rounded-[calc(var(--design-unit)*4)] border border-[rgba(240,175,66,0.2)] bg-[rgba(110,77,26,0.24)] px-design-12 py-design-10 text-design-16 leading-[1.35] text-[#F0B44A]">
Nilai tukar berfungsi sebagai harga acuan; nilai tukar aktual yang
berlaku ditentukan pada saat penarikan.
</div>
<div className="flex flex-col gap-design-8 px-design-2 text-design-16 uppercase leading-[1.35] text-[#7AD8E0]">
<div>
Dompet Elektronik:{' '}
<span className="text-[#B9F4F8]">Minimal RM10</span>
</div>
<div>
Bank: <span className="text-[#B9F4F8]">Minimal RM10</span>
</div>
<div>
Waktu Pengerjaan:{' '}
<span className="text-[#77FF76]">
Dana Tiba Hanya Dalam 9 Detik.
</span>
</div>
<div className="text-[#B9F4F8]">
Melihat: Transaksi antara RM10 dan RM99,99 akan dikenakan biaya
penarikan minimum sebesar RM1.
</div>
</div>
<div className="mt-auto flex items-end justify-between gap-design-10 pt-design-10">
<SmartBackground
as="button"
type="button"
src={lengthGreenBtn}
size="100% 100%"
className="flex h-design-64 w-design-200 shrink-0 cursor-pointer items-center justify-center pb-design-4 text-center text-design-18 font-bold uppercase tracking-[0.03em] text-[#F0FFFF] transition hover:scale-[1.02] active:scale-[0.98]"
>
Membatalkan
</SmartBackground>
<SmartBackground
as="button"
type="button"
src={lengthBlueBtn}
size="100% 100%"
className="flex h-design-64 w-design-200 shrink-0 cursor-pointer items-center justify-center pb-design-4 text-center text-design-17 font-bold uppercase leading-[1.05] tracking-[0.03em] text-[#F0FFFF] transition hover:scale-[1.02] active:scale-[0.98]"
>
Konfirmasi
<br />
Penarikan
</SmartBackground>
</div>
</div>
</div>
</div>
</div>
)
}
export default DesktopWithdraw

View File

@@ -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 (
<>
<div
className={'mx-auto my-design-10 w-[calc(100%-24*var(--design-unit))]'}
>
<DesktopTitle />
</div>
<div
className={
'mx-auto flex w-[calc(100%-24*var(--design-unit))] flex-col gap-design-10'
}
>
<DesktopGameHistory />
<DesktopAnimal />
</div>
</>
)
return <div>mobile component entry</div>
}

View File

@@ -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() {
{/*公告弹窗*/}
{/*<DesktopNoticeModal />*/}
{/*自动托管弹窗*/}
{/* <DesktopAutoSettingModal/>*/}
<DesktopAutoSettingModal />
{/* 充值提现前置选择弹窗*/}
<DesktopProceduresModal />
{/*<DesktopProceduresModal />*/}
{/* 充值和提现弹窗 */}
{/*<DesktopWithdrawTopupModal/>*/}
</>

View File

@@ -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,
}
}

View File

@@ -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,
}
}

View File

@@ -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])
}

View File

@@ -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'
}
>
<input
<Input
defaultValue={row.value}
className={
'game-setting-input h-full w-design-280 text-design-18'

View File

@@ -5,6 +5,7 @@ import rightImg from '@/assets/system/right.webp'
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'
function DesktopLoginModal() {
const [open, setOpen] = useState(true)
@@ -39,7 +40,7 @@ function DesktopLoginModal() {
>
Akun/TEL:
</div>
<input
<Input
className={'flex-1 text-left'}
placeholder={'Silakan masukkan akun atau nomor ponsel Anda.'}
/>
@@ -52,7 +53,7 @@ function DesktopLoginModal() {
>
Kata Sandi:
</div>
<input
<Input
className={'flex-1 text-left'}
placeholder={'Masukkan Kata Sandi'}
/>

View File

@@ -5,6 +5,7 @@ import rightImg from '@/assets/system/right.webp'
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'
function DesktopRegisterModal() {
const [open, setOpen] = useState(true)
@@ -37,7 +38,7 @@ function DesktopRegisterModal() {
>
Akun/TEL:
</div>
<input
<Input
className={'flex-1 text-left'}
placeholder={'Silakan masukkan akun atau nomor ponsel Anda.'}
/>
@@ -50,7 +51,7 @@ function DesktopRegisterModal() {
>
Kata Sandi:
</div>
<input
<Input
className={'flex-1 text-left'}
placeholder={'Masukkan Kata Sandi'}
/>
@@ -63,7 +64,7 @@ function DesktopRegisterModal() {
>
Kata Sandi:
</div>
<input
<Input
className={'flex-1 text-left'}
placeholder={'Masukkan Kata Sandi'}
/>
@@ -76,7 +77,7 @@ function DesktopRegisterModal() {
>
Kata Sandi:
</div>
<input
<Input
className={'flex-1 text-left'}
placeholder={'Masukkan Kata Sandi'}
/>

View File

@@ -7,7 +7,7 @@ type WithdrawType = 'withdraw' | 'topup'
function DesktopWithdrawTopupModal() {
const [open, setOpen] = useState(true)
const [type, setType] = useState<WithdrawType>('withdraw')
const [type] = useState<WithdrawType>('withdraw')
function handleSubmit() {
setOpen(false)
}
@@ -18,15 +18,16 @@ function DesktopWithdrawTopupModal() {
onClose={handleSubmit}
title={
<div className={'modal-title-glow text-design-26 uppercase'}>
{type}
{type === 'withdraw' ? '申请提现' : '申请充值'}
</div>
}
isShowClose={false}
isNormalBg={true}
titleAlign="left"
className={'w-design-835 h-design-500'}
className={'w-design-1200 h-design-700'}
>
<div>{type ? <DesktopWithdraw /> : <DesktopTopup />}</div>
<div className={'w-full h-[96%]'}>
{type === 'withdraw' ? <DesktopWithdraw /> : <DesktopTopup />}
</div>
</CenterModal>
)
}

View File

@@ -44,8 +44,6 @@ export const BET_SOURCES = ['local', 'server'] as const
export const TREND_DIRECTIONS = ['rising', 'steady', 'falling'] as const
export const DEFAULT_GAME_CHIP_AMOUNTS = [10, 25, 50, 100, 200, 500] as const
export const DEFAULT_GAME_CHIP_COLORS = [
'#1D4ED8',
'#0F766E',
@@ -55,7 +53,7 @@ export const DEFAULT_GAME_CHIP_COLORS = [
'#111827',
] as const
export const DEFAULT_ACTIVE_CHIP_ID = 'chip-50'
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

View File

@@ -1,7 +1,7 @@
import { CHIP_OPTIONS } from '@/constants'
import {
DEFAULT_ACTIVE_CHIP_ID,
DEFAULT_ANNOUNCEMENT_TTL_MS,
DEFAULT_GAME_CHIP_AMOUNTS,
DEFAULT_GAME_CHIP_COLORS,
GAME_GRID_COLUMNS,
GAME_TOTAL_CELLS,
@@ -41,12 +41,12 @@ export function createGameCells() {
}
export function createDefaultChips() {
return DEFAULT_GAME_CHIP_AMOUNTS.map((amount, index) => ({
amount,
return CHIP_OPTIONS.map((chip, index) => ({
amount: chip.value,
color: DEFAULT_GAME_CHIP_COLORS[index],
id: `chip-${amount}`,
isDefault: `chip-${amount}` === DEFAULT_ACTIVE_CHIP_ID,
label: amount >= 100 ? `${amount / 100}x` : String(amount),
id: chip.id,
isDefault: chip.id === DEFAULT_ACTIVE_CHIP_ID,
label: chip.value >= 100 ? `${chip.value / 100}x` : String(chip.value),
})) satisfies Chip[]
}

View File

@@ -190,25 +190,6 @@
linear-gradient(180deg, #07111f 0%, #040812 100%);
color: #f8fafc;
}
input {
@apply border border-transparent bg-[#135E65]/60 text-[#D9FFFF] py-design-15 px-design-30 text-design-20 rounded-md outline-none transition;
}
input::placeholder {
color: rgba(116, 173, 175, 0.72);
}
input:focus,
input:focus-visible {
border-color: rgba(110, 255, 255, 0.72);
outline: none;
box-shadow:
0 0 0 calc(var(--design-unit) * 1.5) rgba(110, 255, 255, 0.16),
0 0 calc(var(--design-unit) * 8) rgba(48, 214, 255, 0.36),
0 0 calc(var(--design-unit) * 18) rgba(18, 162, 255, 0.22),
inset 0 0 calc(var(--design-unit) * 6) rgba(110, 255, 255, 0.08);
}
}
@layer utilities {