Compare commits
12 Commits
20a6fc5175
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf9300eefc | ||
|
|
1b7557e2cc | ||
|
|
2a76f1d722 | ||
|
|
c841eb6688 | ||
|
|
596d435d52 | ||
|
|
20f31ff0a5 | ||
|
|
9ade40c29b | ||
|
|
fa40c23720 | ||
|
|
fe3b839297 | ||
|
|
2c1a69dd9a | ||
|
|
2d88e4a204 | ||
|
|
6fd9862019 |
12
.cloudbase-sites/preview.json
Normal file
12
.cloudbase-sites/preview.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"pid": 24641,
|
||||||
|
"port": 17175,
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"base": "/",
|
||||||
|
"framework": "vite-react",
|
||||||
|
"command": "/Users/jiaunun/.nvm/versions/node/v24.15.0/bin/node /Users/jiaunun/Desktop/36-character-flower/node_modules/vite/bin/vite.js --host 0.0.0.0 --port 17175 --strictPort --base /",
|
||||||
|
"internalUrl": "http://127.0.0.1:17175/",
|
||||||
|
"logPath": "/Users/jiaunun/Desktop/36-character-flower/.cloudbase-sites/logs/preview-1784622161040.log",
|
||||||
|
"cwd": "/Users/jiaunun/Desktop/36-character-flower",
|
||||||
|
"startedAt": "2026-07-21T08:22:42.070Z"
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<!-- gitnexus:start -->
|
<!-- gitnexus:start -->
|
||||||
# GitNexus — Code Intelligence
|
# GitNexus — Code Intelligence
|
||||||
|
|
||||||
This project is indexed by GitNexus as **36-character-flower** (3095 symbols, 6119 relationships, 264 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** (3365 symbols, 6609 relationships, 288 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.
|
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<!-- gitnexus:start -->
|
<!-- gitnexus:start -->
|
||||||
# GitNexus — Code Intelligence
|
# GitNexus — Code Intelligence
|
||||||
|
|
||||||
This project is indexed by GitNexus as **36-character-flower** (3095 symbols, 6119 relationships, 264 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** (3365 symbols, 6609 relationships, 288 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.
|
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/geist": "^5.2.8",
|
"@fontsource-variable/geist": "^5.2.8",
|
||||||
|
"@fontsource-variable/oxanium": "^5.2.8",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@tanstack/react-query": "^5.99.0",
|
"@tanstack/react-query": "^5.99.0",
|
||||||
"@tanstack/react-query-devtools": "^5.99.0",
|
"@tanstack/react-query-devtools": "^5.99.0",
|
||||||
|
|||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
|||||||
'@fontsource-variable/geist':
|
'@fontsource-variable/geist':
|
||||||
specifier: ^5.2.8
|
specifier: ^5.2.8
|
||||||
version: 5.2.9
|
version: 5.2.9
|
||||||
|
'@fontsource-variable/oxanium':
|
||||||
|
specifier: ^5.2.8
|
||||||
|
version: 5.2.8
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^5.2.2
|
specifier: ^5.2.2
|
||||||
version: 5.4.0(react-hook-form@7.75.0(react@19.2.5))
|
version: 5.4.0(react-hook-form@7.75.0(react@19.2.5))
|
||||||
@@ -635,6 +638,9 @@ packages:
|
|||||||
'@fontsource-variable/geist@5.2.9':
|
'@fontsource-variable/geist@5.2.9':
|
||||||
resolution: {integrity: sha512-TP+QSBG3wxKGPE33CbMy/L0Nu3qvJ6Fy81Yc4LnQ95xH+i+cfEp8fyU8/kfV14YwszxIFPhnoMTbjL71waVpyQ==}
|
resolution: {integrity: sha512-TP+QSBG3wxKGPE33CbMy/L0Nu3qvJ6Fy81Yc4LnQ95xH+i+cfEp8fyU8/kfV14YwszxIFPhnoMTbjL71waVpyQ==}
|
||||||
|
|
||||||
|
'@fontsource-variable/oxanium@5.2.8':
|
||||||
|
resolution: {integrity: sha512-W3HWxRLXVB6yox3dgm1DIGOp98pz8KglwiM6/2BsMopvZrg98mXI9citFWz2pUX0FOOLwf8fmHxX+KrIn+6BoA==}
|
||||||
|
|
||||||
'@hono/node-server@1.19.14':
|
'@hono/node-server@1.19.14':
|
||||||
resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==}
|
resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==}
|
||||||
engines: {node: '>=18.14.1'}
|
engines: {node: '>=18.14.1'}
|
||||||
@@ -4785,6 +4791,8 @@ snapshots:
|
|||||||
|
|
||||||
'@fontsource-variable/geist@5.2.9': {}
|
'@fontsource-variable/geist@5.2.9': {}
|
||||||
|
|
||||||
|
'@fontsource-variable/oxanium@5.2.8': {}
|
||||||
|
|
||||||
'@hono/node-server@1.19.14(hono@4.12.18)':
|
'@hono/node-server@1.19.14(hono@4.12.18)':
|
||||||
dependencies:
|
dependencies:
|
||||||
hono: 4.12.18
|
hono: 4.12.18
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ function logAuthSessionExpiry(action: string, session: AuthSessionInput) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getCurrentUserProfileByToken(userToken: string) {
|
async function getCurrentUserProfileByToken(userToken: string) {
|
||||||
const response = await api.post<AuthUserProfileDto>(AUTH_ENDPOINTS.profile, {
|
const response = await api.get<AuthUserProfileDto>(AUTH_ENDPOINTS.profile, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${userToken}`,
|
Authorization: `Bearer ${userToken}`,
|
||||||
'user-token': userToken,
|
'user-token': userToken,
|
||||||
@@ -145,16 +145,18 @@ export async function logoutWithPassword(
|
|||||||
export async function registerWithPassword(
|
export async function registerWithPassword(
|
||||||
payload: RegisterPayload,
|
payload: RegisterPayload,
|
||||||
): Promise<AuthSessionInput> {
|
): Promise<AuthSessionInput> {
|
||||||
|
const inviteCode = payload.inviteCode?.trim() ?? ''
|
||||||
|
const registerRequest: RegisterRequestDto = {
|
||||||
|
captcha: payload.captcha,
|
||||||
|
device_id: getAuthDeviceId(),
|
||||||
|
password: payload.password,
|
||||||
|
username: payload.mobile,
|
||||||
|
...(inviteCode ? { invite_code: inviteCode } : {}),
|
||||||
|
}
|
||||||
const response = await api.post<AuthSessionDto, RegisterRequestDto>(
|
const response = await api.post<AuthSessionDto, RegisterRequestDto>(
|
||||||
AUTH_ENDPOINTS.register,
|
AUTH_ENDPOINTS.register,
|
||||||
{
|
{
|
||||||
json: {
|
json: registerRequest,
|
||||||
captcha: payload.captcha,
|
|
||||||
device_id: getAuthDeviceId(),
|
|
||||||
invite_code: payload.inviteCode,
|
|
||||||
password: payload.password,
|
|
||||||
username: payload.mobile,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -195,7 +197,7 @@ export async function sendSmsCode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getCurrentUserProfile() {
|
export async function getCurrentUserProfile() {
|
||||||
const response = await api.post<AuthUserProfileDto>(AUTH_ENDPOINTS.profile)
|
const response = await api.get<AuthUserProfileDto>(AUTH_ENDPOINTS.profile)
|
||||||
|
|
||||||
return normalizeAuthUserProfile(
|
return normalizeAuthUserProfile(
|
||||||
unwrapEnvelope(
|
unwrapEnvelope(
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ function normalizePayChannel(dto: FinancePayChannelDto) {
|
|||||||
currencyCodes: Array.isArray(dto.currency_codes) ? dto.currency_codes : [],
|
currencyCodes: Array.isArray(dto.currency_codes) ? dto.currency_codes : [],
|
||||||
depositBanks: (dto.deposit_banks ?? []).map(normalizeWithdrawBank),
|
depositBanks: (dto.deposit_banks ?? []).map(normalizeWithdrawBank),
|
||||||
feeNote: dto.fee_note ?? '',
|
feeNote: dto.fee_note ?? '',
|
||||||
|
handlingFee: dto.handling_fee ?? '0.00',
|
||||||
methods: (dto.methods ?? []).map(normalizeDepositMethod),
|
methods: (dto.methods ?? []).map(normalizeDepositMethod),
|
||||||
minBank: dto.min_bank ?? '',
|
minBank: dto.min_bank ?? '',
|
||||||
minEwallet: dto.min_ewallet ?? '',
|
minEwallet: dto.min_ewallet ?? '',
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import type {
|
|||||||
GameBootstrapSnapshot,
|
GameBootstrapSnapshot,
|
||||||
GameCell,
|
GameCell,
|
||||||
GameCellDto,
|
GameCellDto,
|
||||||
|
GameJackpotPoolDto,
|
||||||
GameLobbyInitDto,
|
GameLobbyInitDto,
|
||||||
GameLobbyInitResult,
|
GameLobbyInitResult,
|
||||||
GameLobbyPeriodDto,
|
GameLobbyPeriodDto,
|
||||||
@@ -40,6 +41,7 @@ import type {
|
|||||||
GameRoundFeedDto,
|
GameRoundFeedDto,
|
||||||
HistoryEntry,
|
HistoryEntry,
|
||||||
HistoryEntryDto,
|
HistoryEntryDto,
|
||||||
|
JackpotPoolState,
|
||||||
NoticeConfirmDto,
|
NoticeConfirmDto,
|
||||||
NoticeDetailDto,
|
NoticeDetailDto,
|
||||||
NoticeListDto,
|
NoticeListDto,
|
||||||
@@ -164,10 +166,33 @@ function normalizeAnnouncementState(dto: AnnouncementStateDto) {
|
|||||||
} satisfies AnnouncementState
|
} satisfies AnnouncementState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeJackpotPoolState(
|
||||||
|
dto: GameJackpotPoolDto | null | undefined,
|
||||||
|
updatedAt: string | null,
|
||||||
|
) {
|
||||||
|
if (!dto) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = Number(dto.current)
|
||||||
|
const speed = Number(dto.speed)
|
||||||
|
|
||||||
|
if (!Number.isFinite(current) || !Number.isFinite(speed)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
current,
|
||||||
|
speedPerSecond: Math.max(0, speed),
|
||||||
|
updatedAt,
|
||||||
|
} satisfies JackpotPoolState
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeDashboardState(dto: DashboardStateDto) {
|
function normalizeDashboardState(dto: DashboardStateDto) {
|
||||||
return {
|
return {
|
||||||
countdownMs: dto.countdown_ms,
|
countdownMs: dto.countdown_ms,
|
||||||
featuredCellId: dto.featured_cell_id,
|
featuredCellId: dto.featured_cell_id,
|
||||||
|
jackpotPool: normalizeJackpotPoolState(dto.jackpot_pool, dto.updated_at),
|
||||||
onlinePlayers: dto.online_players,
|
onlinePlayers: dto.online_players,
|
||||||
tableLimitMax: dto.table_limit_max,
|
tableLimitMax: dto.table_limit_max,
|
||||||
tableLimitMin: dto.table_limit_min,
|
tableLimitMin: dto.table_limit_min,
|
||||||
@@ -334,6 +359,7 @@ export function normalizeGameLobbyInit(dto: GameLobbyInitDto) {
|
|||||||
dashboard: {
|
dashboard: {
|
||||||
countdownMs: 0,
|
countdownMs: 0,
|
||||||
featuredCellId: null,
|
featuredCellId: null,
|
||||||
|
jackpotPool: normalizeJackpotPoolState(dto.jackpot_pool, baseIso),
|
||||||
onlinePlayers: 0,
|
onlinePlayers: 0,
|
||||||
tableLimitMax: Number(dto.bet_config.max_bet_per_number) || 0,
|
tableLimitMax: Number(dto.bet_config.max_bet_per_number) || 0,
|
||||||
tableLimitMin: Number(dto.bet_config.min_bet_per_number) || 0,
|
tableLimitMin: Number(dto.bet_config.min_bet_per_number) || 0,
|
||||||
|
|||||||
BIN
src/assets/system/jackpot-bg.png
Normal file
BIN
src/assets/system/jackpot-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 231 KiB |
BIN
src/assets/system/jackpot-title.png
Normal file
BIN
src/assets/system/jackpot-title.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
224
src/components/jackpot-pool-ticker.tsx
Normal file
224
src/components/jackpot-pool-ticker.tsx
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import diamond from '@/assets/system/diamond.webp'
|
||||||
|
import jackpotBg from '@/assets/system/jackpot-bg.png'
|
||||||
|
import jackpotTitle from '@/assets/system/jackpot-title.png'
|
||||||
|
import { SmartImage } from '@/components/smart-image.tsx'
|
||||||
|
import { cn } from '@/lib/utils.ts'
|
||||||
|
import { useGameSessionStore } from '@/store/game'
|
||||||
|
|
||||||
|
const JACKPOT_POOL_TICK_MS = 1000
|
||||||
|
const DEFAULT_JACKPOT_POOL_AMOUNT = 1_000_000
|
||||||
|
const jackpotPoolFormatter = new Intl.NumberFormat('en-US', {
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
type JackpotPoolTickerProps = {
|
||||||
|
className?: string
|
||||||
|
variant?: 'hud' | 'inline' | 'standalone'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function JackpotPoolTicker({
|
||||||
|
className,
|
||||||
|
variant = 'standalone',
|
||||||
|
}: JackpotPoolTickerProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const jackpotPool = useGameSessionStore(
|
||||||
|
(state) => state.dashboard.jackpotPool,
|
||||||
|
)
|
||||||
|
const poolCurrent = jackpotPool?.current ?? DEFAULT_JACKPOT_POOL_AMOUNT
|
||||||
|
const poolSpeedPerSecond = jackpotPool?.speedPerSecond ?? 0
|
||||||
|
const poolUpdatedAt = jackpotPool?.updatedAt ?? null
|
||||||
|
const [nowMs, setNowMs] = useState(() => Date.now())
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNowMs(Date.now())
|
||||||
|
|
||||||
|
if (poolSpeedPerSecond <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const timerId = window.setInterval(() => {
|
||||||
|
setNowMs(Date.now())
|
||||||
|
}, JACKPOT_POOL_TICK_MS)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearInterval(timerId)
|
||||||
|
}
|
||||||
|
}, [poolSpeedPerSecond])
|
||||||
|
|
||||||
|
const displayAmount = useMemo(() => {
|
||||||
|
const updatedAtMs = poolUpdatedAt ? Date.parse(poolUpdatedAt) : Number.NaN
|
||||||
|
const baseTimeMs = Number.isFinite(updatedAtMs) ? updatedAtMs : nowMs
|
||||||
|
const elapsedSeconds = Math.max(0, (nowMs - baseTimeMs) / 1000)
|
||||||
|
|
||||||
|
return poolCurrent + elapsedSeconds * poolSpeedPerSecond
|
||||||
|
}, [nowMs, poolCurrent, poolSpeedPerSecond, poolUpdatedAt])
|
||||||
|
|
||||||
|
const amountLabel = jackpotPoolFormatter.format(displayAmount)
|
||||||
|
const isHud = variant === 'hud'
|
||||||
|
const isInline = variant === 'inline'
|
||||||
|
const amountCharacterCounts = new Map<string, number>()
|
||||||
|
const amountCharacterItems = Array.from(amountLabel).map((character) => {
|
||||||
|
const nextCount = (amountCharacterCounts.get(character) ?? 0) + 1
|
||||||
|
|
||||||
|
amountCharacterCounts.set(character, nextCount)
|
||||||
|
|
||||||
|
return {
|
||||||
|
character,
|
||||||
|
key: `${character}-${nextCount}`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isHud) {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
aria-label={t('game.jackpotPool.ariaLabel', { amount: amountLabel })}
|
||||||
|
className={cn(
|
||||||
|
'relative min-w-0 shrink-0 overflow-hidden whitespace-nowrap bg-center bg-no-repeat',
|
||||||
|
'h-design-55 w-design-126 px-design-7 pb-design-4 pt-design-3',
|
||||||
|
'md:h-design-150 md:w-design-340 md:px-design-22 md:pb-design-14 md:pt-design-12',
|
||||||
|
'[background-size:100%_100%]',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
style={{ backgroundImage: `url(${jackpotBg})` }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none absolute left-[12%] top-[25%] h-[2px] w-[23%] bg-gradient-to-r from-transparent via-[#27DFFF] to-transparent shadow-[0_0_calc(var(--design-unit)*8)_rgba(39,223,255,0.9)]"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none absolute right-[12%] top-[25%] h-[2px] w-[23%] bg-gradient-to-r from-transparent via-[#27DFFF] to-transparent shadow-[0_0_calc(var(--design-unit)*8)_rgba(39,223,255,0.9)]"
|
||||||
|
/>
|
||||||
|
<div className="absolute left-1/2 top-[9%] z-10 flex -translate-x-1/2 flex-col items-center leading-none">
|
||||||
|
<span className="text-design-4 font-black uppercase tracking-[0.16em] text-[#EAF8FC] [text-shadow:0_calc(var(--design-unit)*1)_0_rgba(4,35,51,0.96),0_0_calc(var(--design-unit)*5)_rgba(174,237,255,0.52)] md:text-design-15 md:tracking-[0.22em]">
|
||||||
|
{t('game.jackpotPool.promoLine1')}
|
||||||
|
</span>
|
||||||
|
<span className="mt-design-1 text-design-4 font-black uppercase tracking-[0.16em] text-[#EAF8FC] [text-shadow:0_calc(var(--design-unit)*1)_0_rgba(4,35,51,0.96),0_0_calc(var(--design-unit)*5)_rgba(174,237,255,0.52)] md:text-design-13 md:tracking-[0.22em]">
|
||||||
|
{t('game.jackpotPool.promoLine2')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none absolute inset-x-[6%] top-[30%] z-10 h-auto w-[88%] object-contain drop-shadow-[0_calc(var(--design-unit)*4)_0_rgba(0,45,68,0.95)]"
|
||||||
|
src={jackpotTitle}
|
||||||
|
/>
|
||||||
|
<div className="absolute left-1/2 top-[66%] z-10 flex h-[25%] w-[82%] -translate-x-1/2 items-center justify-center">
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none absolute inset-0 h-full w-full"
|
||||||
|
focusable="false"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
viewBox="0 0 1000 180"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<filter
|
||||||
|
colorInterpolationFilters="sRGB"
|
||||||
|
height="160%"
|
||||||
|
id="jackpotAmountFrameGlow"
|
||||||
|
width="116%"
|
||||||
|
x="-8%"
|
||||||
|
y="-30%"
|
||||||
|
>
|
||||||
|
<feDropShadow
|
||||||
|
dx="0"
|
||||||
|
dy="0"
|
||||||
|
floodColor="#28f8ff"
|
||||||
|
floodOpacity="0.58"
|
||||||
|
stdDeviation="1"
|
||||||
|
/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<polygon
|
||||||
|
filter="url(#jackpotAmountFrameGlow)"
|
||||||
|
fill="rgba(0, 14, 24, 0.44)"
|
||||||
|
points="32,1 968,1 999,32 999,148 968,179 32,179 1,148 1,32"
|
||||||
|
stroke="#29F5FF"
|
||||||
|
strokeWidth="1"
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none absolute z-10 translate-y-[1%] font-jackpot-amount text-design-14 font-normal leading-none tabular-nums tracking-[0.025em] md:text-design-37"
|
||||||
|
style={{
|
||||||
|
WebkitTextFillColor: '#72F9FF',
|
||||||
|
WebkitTextStroke: 'calc(var(--design-unit) * 0.5) #C9FFFF',
|
||||||
|
fontVariationSettings: '"wght" 450',
|
||||||
|
filter:
|
||||||
|
'drop-shadow(0 0 calc(var(--design-unit) * 1.8) rgba(45, 255, 255, 0.9)) drop-shadow(0 0 calc(var(--design-unit) * 4) rgba(31, 235, 255, 0.5))',
|
||||||
|
textShadow:
|
||||||
|
'0 calc(var(--design-unit) * 1) 0 rgba(0, 38, 48, 0.82)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
${amountLabel}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
aria-label={t('game.jackpotPool.ariaLabel', { amount: amountLabel })}
|
||||||
|
className={cn(
|
||||||
|
'relative flex w-full min-w-0 items-center overflow-hidden whitespace-nowrap',
|
||||||
|
isInline
|
||||||
|
? 'h-design-20 rounded-[3px] border border-[#B86A13]/55 bg-[rgba(32,18,3,0.72)] px-design-4 md:h-design-52 md:rounded-[4px] md:px-design-10'
|
||||||
|
: 'h-design-34 !rounded-[4px] border border-[#B86A13]/70 bg-[rgba(32,18,3,0.82)] !px-design-10 !py-0 md:h-design-48 md:!rounded-[5px] md:!px-design-16',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SmartImage
|
||||||
|
alt={t('game.jackpotPool.iconAlt')}
|
||||||
|
className={cn(
|
||||||
|
'shrink-0',
|
||||||
|
isInline
|
||||||
|
? 'mr-design-3 h-design-10 w-design-10 md:mr-design-8 md:h-design-24 md:w-design-24'
|
||||||
|
: 'mr-design-5 h-design-14 w-design-14 md:mr-design-10 md:h-design-26 md:w-design-26',
|
||||||
|
)}
|
||||||
|
src={diamond}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'shrink-0 font-bold tracking-[0.08em] text-[#D8AA49]',
|
||||||
|
isInline
|
||||||
|
? 'mr-[20px] text-design-6 md:text-design-12'
|
||||||
|
: 'mr-[20px] text-design-8 md:text-design-14',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{t('game.jackpotPool.label')}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'min-w-0 shrink truncate font-jackpot-amount font-extrabold leading-none tabular-nums [font-variation-settings:"wght"_760]',
|
||||||
|
isInline
|
||||||
|
? 'text-design-10 md:text-design-27'
|
||||||
|
: 'text-design-16 md:text-design-30',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{amountCharacterItems.map(({ character, key }) => {
|
||||||
|
const isNumber = /\d/.test(character)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn(
|
||||||
|
'inline-block text-[#FFD15A]',
|
||||||
|
isNumber
|
||||||
|
? '[text-shadow:0_0_calc(var(--design-unit)*3)_rgba(255,240,166,0.88),0_0_calc(var(--design-unit)*7)_rgba(255,209,90,0.64),0_calc(var(--design-unit)*1)_0_rgba(92,43,2,0.92)]'
|
||||||
|
: 'mx-[0.03em] text-[#D49322]',
|
||||||
|
)}
|
||||||
|
key={key}
|
||||||
|
>
|
||||||
|
{character}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { AnimatePresence, motion, useReducedMotion } from 'motion/react'
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import broadcast from '@/assets/system/broadcast.webp'
|
import broadcast from '@/assets/system/broadcast.webp'
|
||||||
|
import { JackpotPoolTicker } from '@/components/jackpot-pool-ticker.tsx'
|
||||||
import { SmartImage } from '@/components/smart-image.tsx'
|
import { SmartImage } from '@/components/smart-image.tsx'
|
||||||
import { cn } from '@/lib/utils.ts'
|
import { cn } from '@/lib/utils.ts'
|
||||||
import { useGameSessionStore } from '@/store/game'
|
import { useGameSessionStore } from '@/store/game'
|
||||||
@@ -20,9 +21,13 @@ function formatWinAmount(value: string) {
|
|||||||
|
|
||||||
type MessageBroadcastProps = {
|
type MessageBroadcastProps = {
|
||||||
className?: string
|
className?: string
|
||||||
|
showJackpotPool?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MessageBroadcast({ className }: MessageBroadcastProps) {
|
export function MessageBroadcast({
|
||||||
|
className,
|
||||||
|
showJackpotPool = true,
|
||||||
|
}: MessageBroadcastProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const prefersReducedMotion = useReducedMotion()
|
const prefersReducedMotion = useReducedMotion()
|
||||||
const jackpotBroadcasts = useGameSessionStore(
|
const jackpotBroadcasts = useGameSessionStore(
|
||||||
@@ -103,65 +108,76 @@ export function MessageBroadcast({ className }: MessageBroadcastProps) {
|
|||||||
aria-label={t('game.jackpotBroadcast.ariaLabel')}
|
aria-label={t('game.jackpotBroadcast.ariaLabel')}
|
||||||
className={cn(
|
className={cn(
|
||||||
'common-neon-inset flex w-full min-w-0 items-center overflow-hidden',
|
'common-neon-inset flex w-full min-w-0 items-center overflow-hidden',
|
||||||
'h-design-24 gap-design-5 !rounded-[3px] !px-design-7 !py-0 text-design-8',
|
'h-design-24 gap-design-4 !rounded-[3px] !px-design-5 !py-0 text-design-8',
|
||||||
'md:h-design-65 md:gap-design-10 md:!rounded-[5px] md:!px-design-20 md:text-design-16',
|
'md:h-design-65 md:gap-design-12 md:!rounded-[5px] md:!px-design-20 md:text-design-16',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SmartImage
|
<div className="flex min-w-0 flex-1 items-center gap-design-4 md:gap-design-10">
|
||||||
className="h-design-12 w-design-12 shrink-0 md:h-design-24 md:w-design-24"
|
<SmartImage
|
||||||
alt={t('game.jackpotBroadcast.iconAlt')}
|
className="h-design-12 w-design-12 shrink-0 md:h-design-24 md:w-design-24"
|
||||||
src={broadcast}
|
alt={t('game.jackpotBroadcast.iconAlt')}
|
||||||
/>
|
src={broadcast}
|
||||||
<div className="relative h-design-16 min-w-0 flex-1 overflow-hidden md:h-design-28">
|
/>
|
||||||
<AnimatePresence>
|
<div className="relative h-design-16 min-w-0 flex-1 overflow-hidden md:h-design-28">
|
||||||
{activeBroadcast ? (
|
<AnimatePresence mode="wait">
|
||||||
<motion.div
|
{activeBroadcast ? (
|
||||||
aria-label={activeBroadcastLabel}
|
<motion.div
|
||||||
className="absolute inset-0 flex min-w-max items-center whitespace-nowrap font-semibold"
|
aria-label={activeBroadcastLabel}
|
||||||
key={activeBroadcast.id}
|
className="absolute inset-0 flex min-w-max items-center whitespace-nowrap font-semibold"
|
||||||
initial={
|
key={activeBroadcast.id}
|
||||||
prefersReducedMotion
|
initial={
|
||||||
? { opacity: 0 }
|
prefersReducedMotion
|
||||||
: { opacity: 1, x: '108%' }
|
? { opacity: 0 }
|
||||||
}
|
: { opacity: 1, x: '108%' }
|
||||||
animate={
|
}
|
||||||
prefersReducedMotion
|
animate={
|
||||||
? { opacity: 1 }
|
prefersReducedMotion
|
||||||
: { opacity: 1, x: '-108%' }
|
? { opacity: 1 }
|
||||||
}
|
: { opacity: 1, x: '-108%' }
|
||||||
exit={{ opacity: 0 }}
|
}
|
||||||
transition={{
|
exit={{
|
||||||
duration: prefersReducedMotion
|
opacity: 0,
|
||||||
? 0.18
|
transition: { duration: 0.15, ease: 'easeOut' },
|
||||||
: MESSAGE_BROADCAST_ANIMATION_MS / 1000,
|
}}
|
||||||
ease: prefersReducedMotion ? [0.16, 1, 0.3, 1] : 'linear',
|
transition={{
|
||||||
}}
|
duration: prefersReducedMotion
|
||||||
>
|
? 0.18
|
||||||
<span className="text-[#D5F7FF]">
|
: MESSAGE_BROADCAST_ANIMATION_MS / 1000,
|
||||||
{t('game.jackpotBroadcast.prefix')}
|
ease: prefersReducedMotion ? [0.16, 1, 0.3, 1] : 'linear',
|
||||||
</span>
|
}}
|
||||||
<span className="ml-design-4 text-[#4BFFFE] md:ml-design-8">
|
>
|
||||||
{activeBroadcast.nickname}
|
<span className="text-[#D5F7FF]">
|
||||||
</span>
|
{t('game.jackpotBroadcast.prefix')}
|
||||||
<span className="mx-design-4 text-[#D5F7FF] md:mx-design-8">
|
</span>
|
||||||
{t('game.jackpotBroadcast.separator')}
|
<span className="ml-design-4 text-[#4BFFFE] md:ml-design-8">
|
||||||
</span>
|
{activeBroadcast.nickname}
|
||||||
<span className="text-[#78FF7F]">
|
</span>
|
||||||
{t('game.jackpotBroadcast.streak', {
|
<span className="mx-design-4 text-[#D5F7FF] md:mx-design-8">
|
||||||
count: activeBroadcast.currentStreak,
|
{t('game.jackpotBroadcast.separator')}
|
||||||
})}
|
</span>
|
||||||
</span>
|
<span className="text-[#78FF7F]">
|
||||||
<span className="mx-design-4 text-[#D5F7FF] md:mx-design-8">
|
{t('game.jackpotBroadcast.streak', {
|
||||||
{t('game.jackpotBroadcast.winAction')}
|
count: activeBroadcast.currentStreak,
|
||||||
</span>
|
})}
|
||||||
<span className="text-[#FFB72B]">
|
</span>
|
||||||
{formatWinAmount(activeBroadcast.totalWin)}
|
<span className="mx-design-4 text-[#D5F7FF] md:mx-design-8">
|
||||||
</span>
|
{t('game.jackpotBroadcast.winAction')}
|
||||||
</motion.div>
|
</span>
|
||||||
) : null}
|
<span className="text-[#FFB72B]">
|
||||||
</AnimatePresence>
|
{formatWinAmount(activeBroadcast.totalWin)}
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
) : null}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{showJackpotPool ? (
|
||||||
|
<JackpotPoolTicker
|
||||||
|
variant="inline"
|
||||||
|
className="!w-fit max-w-[54%] shrink-0 md:max-w-[calc(var(--design-unit)*320)]"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import diamondIcon from '@/assets/system/diamond.webp'
|
|||||||
import { SmartImage } from '@/components/smart-image'
|
import { SmartImage } from '@/components/smart-image'
|
||||||
import { DesktopAnimalOverlay } from '@/features/game/components/desktop/desktop-animal-overlay.tsx'
|
import { DesktopAnimalOverlay } from '@/features/game/components/desktop/desktop-animal-overlay.tsx'
|
||||||
import { AnimalSpriteImage } from '@/features/game/components/shared/animal-sprite-image'
|
import { AnimalSpriteImage } from '@/features/game/components/shared/animal-sprite-image'
|
||||||
|
import { LongPressProgress } from '@/features/game/components/shared/long-press-progress'
|
||||||
import { RoundBettingStartAlert } from '@/features/game/components/shared/round-betting-start-alert.tsx'
|
import { RoundBettingStartAlert } from '@/features/game/components/shared/round-betting-start-alert.tsx'
|
||||||
import { FLOWER_IMAGE_LIST } from '@/features/game/shared'
|
import { FLOWER_IMAGE_LIST } from '@/features/game/shared'
|
||||||
import { useAnimalVm } from '@/hooks/use-animal-vm'
|
import { useAnimalVm } from '@/hooks/use-animal-vm'
|
||||||
@@ -83,10 +84,17 @@ export function DesktopAnimal({
|
|||||||
)
|
)
|
||||||
const {
|
const {
|
||||||
cellWarning,
|
cellWarning,
|
||||||
|
handleClearCell,
|
||||||
|
handleLongPressCancel,
|
||||||
|
handleLongPressEnd,
|
||||||
|
handleLongPressMove,
|
||||||
|
handleLongPressStart,
|
||||||
handleSelect,
|
handleSelect,
|
||||||
handleStart,
|
handleStart,
|
||||||
isRealtimeConnecting,
|
isRealtimeConnecting,
|
||||||
lockInteraction,
|
lockInteraction,
|
||||||
|
longPressCellId,
|
||||||
|
longPressDurationMs,
|
||||||
marqueeId,
|
marqueeId,
|
||||||
selectionByCell,
|
selectionByCell,
|
||||||
showStandbyState,
|
showStandbyState,
|
||||||
@@ -212,6 +220,17 @@ export function DesktopAnimal({
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={lockInteraction || showStopOverlay}
|
disabled={lockInteraction || showStopOverlay}
|
||||||
onClick={() => handleSelect(item.id)}
|
onClick={() => handleSelect(item.id)}
|
||||||
|
onContextMenu={(event) => event.preventDefault()}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Delete' || event.key === 'Backspace') {
|
||||||
|
event.preventDefault()
|
||||||
|
handleClearCell(item.id)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onPointerCancel={handleLongPressCancel}
|
||||||
|
onPointerDown={(event) => handleLongPressStart(item.id, event)}
|
||||||
|
onPointerMove={handleLongPressMove}
|
||||||
|
onPointerUp={handleLongPressEnd}
|
||||||
animate={
|
animate={
|
||||||
showCellWarning
|
showCellWarning
|
||||||
? {
|
? {
|
||||||
@@ -372,6 +391,12 @@ export function DesktopAnimal({
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
{longPressCellId === item.id ? (
|
||||||
|
<LongPressProgress
|
||||||
|
durationMs={longPressDurationMs}
|
||||||
|
label={t('gameDesktop.animal.cancelBet')}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { motion } from 'motion/react'
|
import { motion } from 'motion/react'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import reduce from '@/assets/game/add.webp'
|
|
||||||
import arrow from '@/assets/game/arrow.webp'
|
import arrow from '@/assets/game/arrow.webp'
|
||||||
import chipBg from '@/assets/game/chip-bg.webp'
|
import chipBg from '@/assets/game/chip-bg.webp'
|
||||||
import chipLineBg from '@/assets/game/chip-line-bg.webp'
|
import chipLineBg from '@/assets/game/chip-line-bg.webp'
|
||||||
@@ -10,7 +9,6 @@ import confirmBg from '@/assets/game/confirm-bg.webp'
|
|||||||
import confirmRedBg from '@/assets/game/confirm-red-bg.png'
|
import confirmRedBg from '@/assets/game/confirm-red-bg.png'
|
||||||
import controlBg from '@/assets/game/control-bg.png'
|
import controlBg from '@/assets/game/control-bg.png'
|
||||||
import leftBottomBg from '@/assets/game/left-bg.webp'
|
import leftBottomBg from '@/assets/game/left-bg.webp'
|
||||||
import add from '@/assets/game/reduce.webp'
|
|
||||||
import totalBg from '@/assets/game/total-bg.webp'
|
import totalBg from '@/assets/game/total-bg.webp'
|
||||||
import diamond from '@/assets/system/diamond.webp'
|
import diamond from '@/assets/system/diamond.webp'
|
||||||
import { SmartBackground } from '@/components/smart-background.tsx'
|
import { SmartBackground } from '@/components/smart-background.tsx'
|
||||||
@@ -27,8 +25,6 @@ export function DesktopControl() {
|
|||||||
acceptingBets,
|
acceptingBets,
|
||||||
actionsEnabled,
|
actionsEnabled,
|
||||||
canClear,
|
canClear,
|
||||||
canDecreaseBetQuantity,
|
|
||||||
canIncreaseBetQuantity,
|
|
||||||
chips,
|
chips,
|
||||||
confirmLabel,
|
confirmLabel,
|
||||||
confirmState,
|
confirmState,
|
||||||
@@ -37,11 +33,8 @@ export function DesktopControl() {
|
|||||||
onChipSelect,
|
onChipSelect,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onClearSelections,
|
onClearSelections,
|
||||||
onDecreaseBetQuantity,
|
|
||||||
onIncreaseBetQuantity,
|
|
||||||
onOpenAutoSetting,
|
onOpenAutoSetting,
|
||||||
onRepeatSelections,
|
onRepeatSelections,
|
||||||
selectedBetQuantityLabel,
|
|
||||||
selectedChipId,
|
selectedChipId,
|
||||||
selectedCountLabel,
|
selectedCountLabel,
|
||||||
totalBetAmountLabel,
|
totalBetAmountLabel,
|
||||||
@@ -181,7 +174,7 @@ export function DesktopControl() {
|
|||||||
src={chipLineBg}
|
src={chipLineBg}
|
||||||
size="100% 100%"
|
size="100% 100%"
|
||||||
className={
|
className={
|
||||||
'flex min-w-0 flex-1 items-center gap-design-10 overflow-visible'
|
'flex min-w-0 flex-1 items-center justify-between overflow-visible'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{chips.map((chip) => {
|
{chips.map((chip) => {
|
||||||
@@ -338,52 +331,6 @@ export function DesktopControl() {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</SmartBackground>
|
</SmartBackground>
|
||||||
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'flex h-design-50 shrink-0 items-center rounded-md bg-[#091118] box-border px-design-2 py-design-3'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={!acceptingBets || !canDecreaseBetQuantity}
|
|
||||||
onClick={onDecreaseBetQuantity}
|
|
||||||
className={cn(
|
|
||||||
'flex h-design-40 w-design-40 shrink-0 items-center justify-center',
|
|
||||||
acceptingBets && canDecreaseBetQuantity
|
|
||||||
? 'cursor-pointer'
|
|
||||||
: 'cursor-not-allowed opacity-50',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<SmartImage
|
|
||||||
src={add}
|
|
||||||
alt={`add`}
|
|
||||||
className={'w-design-40 h-design-40'}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
className={'w-design-80 h-full flex items-center justify-center'}
|
|
||||||
>
|
|
||||||
{selectedBetQuantityLabel}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={!acceptingBets || !canIncreaseBetQuantity}
|
|
||||||
onClick={onIncreaseBetQuantity}
|
|
||||||
className={cn(
|
|
||||||
'flex h-design-40 w-design-40 shrink-0 items-center justify-center',
|
|
||||||
acceptingBets && canIncreaseBetQuantity
|
|
||||||
? 'cursor-pointer'
|
|
||||||
: 'cursor-not-allowed opacity-50',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<SmartImage
|
|
||||||
src={reduce}
|
|
||||||
alt={`reduce`}
|
|
||||||
className={'w-design-40 h-design-40'}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</SmartBackground>
|
</SmartBackground>
|
||||||
<SmartBackground
|
<SmartBackground
|
||||||
src={totalBg}
|
src={totalBg}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
Mail,
|
Mail,
|
||||||
Maximize,
|
Maximize,
|
||||||
Minimize,
|
Minimize,
|
||||||
|
RefreshCw,
|
||||||
UserKey,
|
UserKey,
|
||||||
UserRoundPlus,
|
UserRoundPlus,
|
||||||
Volume2,
|
Volume2,
|
||||||
@@ -17,6 +18,8 @@ import diamond from '@/assets/system/diamond.webp'
|
|||||||
import logo from '@/assets/system/logo.webp'
|
import logo from '@/assets/system/logo.webp'
|
||||||
import { SmartImage } from '@/components/smart-image.tsx'
|
import { SmartImage } from '@/components/smart-image.tsx'
|
||||||
import { useHeaderClockLabel, useHeaderVm } from '@/hooks/use-header-vm'
|
import { useHeaderClockLabel, useHeaderVm } from '@/hooks/use-header-vm'
|
||||||
|
import { useWalletBalanceRefresh } from '@/hooks/use-wallet-balance-refresh'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
import { useModalStore } from '@/store'
|
import { useModalStore } from '@/store'
|
||||||
|
|
||||||
function HeaderClock() {
|
function HeaderClock() {
|
||||||
@@ -60,6 +63,8 @@ function SignalBars({
|
|||||||
export function DesktopHeader() {
|
export function DesktopHeader() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
||||||
|
const { isRefreshingBalance, refreshWalletBalance } =
|
||||||
|
useWalletBalanceRefresh()
|
||||||
const {
|
const {
|
||||||
authStatus,
|
authStatus,
|
||||||
currentLanguageLabel,
|
currentLanguageLabel,
|
||||||
@@ -217,10 +222,7 @@ export function DesktopHeader() {
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<div className="group relative flex items-center justify-center">
|
||||||
type="button"
|
|
||||||
className="group relative flex items-center justify-center transition-transform duration-150"
|
|
||||||
>
|
|
||||||
<SmartImage
|
<SmartImage
|
||||||
src={diamond}
|
src={diamond}
|
||||||
alt="diamond"
|
alt="diamond"
|
||||||
@@ -234,14 +236,32 @@ export function DesktopHeader() {
|
|||||||
>
|
>
|
||||||
<div className="truncate">{currentUser?.coin || '--'}</div>
|
<div className="truncate">{currentUser?.coin || '--'}</div>
|
||||||
|
|
||||||
<SmartImage
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={t('commonUi.actions.refreshBalance')}
|
||||||
|
title={t('commonUi.actions.refreshBalance')}
|
||||||
|
disabled={isRefreshingBalance}
|
||||||
|
onClick={() => void refreshWalletBalance()}
|
||||||
|
className="flex h-design-24 w-design-24 shrink-0 cursor-pointer items-center justify-center rounded-[3px] text-[#8DE4EA] transition-colors hover:bg-white/10 disabled:cursor-wait disabled:opacity-65"
|
||||||
|
>
|
||||||
|
<RefreshCw
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn(
|
||||||
|
'h-design-16 w-design-16',
|
||||||
|
isRefreshingBalance && 'animate-spin',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
onClick={onOpenProcedures}
|
onClick={onOpenProcedures}
|
||||||
className={'w-design-24 h-design-24 cursor-pointer'}
|
className="h-design-24 w-design-24 shrink-0 cursor-pointer"
|
||||||
alt={'add'}
|
>
|
||||||
src={add}
|
<SmartImage className="h-full w-full" alt="add" src={add} />
|
||||||
/>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import fire from '@/assets/system/fire.webp'
|
|||||||
import lock from '@/assets/system/lock.webp'
|
import lock from '@/assets/system/lock.webp'
|
||||||
import statusCenter from '@/assets/system/status-center.webp'
|
import statusCenter from '@/assets/system/status-center.webp'
|
||||||
import statusLine from '@/assets/system/status-line.webp'
|
import statusLine from '@/assets/system/status-line.webp'
|
||||||
|
import { JackpotPoolTicker } from '@/components/jackpot-pool-ticker.tsx'
|
||||||
import { LottiePlayer } from '@/components/lottie-player.tsx'
|
import { LottiePlayer } from '@/components/lottie-player.tsx'
|
||||||
import { MessageBroadcast } from '@/components/message-broadcast.tsx'
|
import { MessageBroadcast } from '@/components/message-broadcast.tsx'
|
||||||
import { SmartBackground } from '@/components/smart-background.tsx'
|
import { SmartBackground } from '@/components/smart-background.tsx'
|
||||||
@@ -16,6 +17,8 @@ import { DesktopCountdown } from '@/features/game/components/desktop/desktop-cou
|
|||||||
import { useGameStatusVm } from '@/hooks/use-game-status-vm.ts'
|
import { useGameStatusVm } from '@/hooks/use-game-status-vm.ts'
|
||||||
import { cn } from '@/lib/utils.ts'
|
import { cn } from '@/lib/utils.ts'
|
||||||
|
|
||||||
|
const FORCE_DESKTOP_STREAK_EFFECT_PREVIEW = false
|
||||||
|
|
||||||
export function DesktopStatusLine() {
|
export function DesktopStatusLine() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const prefersReducedMotion = useReducedMotion()
|
const prefersReducedMotion = useReducedMotion()
|
||||||
@@ -31,7 +34,9 @@ export function DesktopStatusLine() {
|
|||||||
} = useGameStatusVm()
|
} = useGameStatusVm()
|
||||||
const [remainingMs, setRemainingMs] = useState(countdownMs)
|
const [remainingMs, setRemainingMs] = useState(countdownMs)
|
||||||
const showWarningCountdown = remainingMs <= 5000 && remainingMs > 0
|
const showWarningCountdown = remainingMs <= 5000 && remainingMs > 0
|
||||||
const showStreakLimitOnly = typeof streakValue === 'number' && streakValue > 1
|
const showStreakLimitOnly =
|
||||||
|
FORCE_DESKTOP_STREAK_EFFECT_PREVIEW ||
|
||||||
|
(typeof streakValue === 'number' && streakValue > 1)
|
||||||
const countdownClassName = useMemo(
|
const countdownClassName = useMemo(
|
||||||
() =>
|
() =>
|
||||||
showWarningCountdown
|
showWarningCountdown
|
||||||
@@ -41,182 +46,196 @@ export function DesktopStatusLine() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'relative w-full flex flex-col text-design-22'}>
|
<div className="relative w-full text-design-22">
|
||||||
<div className={'w-full px-design-16 mb-design-10'}>
|
<div className="flex w-full items-start gap-design-14 px-design-16">
|
||||||
<MessageBroadcast />
|
<div className="flex min-w-0 flex-1 flex-col">
|
||||||
</div>
|
<div className="mb-design-10 w-full">
|
||||||
<SmartBackground
|
<MessageBroadcast showJackpotPool={false} />
|
||||||
src={statusLine}
|
</div>
|
||||||
size="100% 100%"
|
<SmartBackground
|
||||||
className="w-full h-design-75 bg-no-repeat bg-center flex items-center justify-center"
|
src={statusLine}
|
||||||
>
|
size="100% 100%"
|
||||||
{/* 状态栏左侧 */}
|
className="flex h-design-75 w-full items-center justify-center bg-center bg-no-repeat"
|
||||||
<div
|
>
|
||||||
className={
|
{/* 状态栏左侧 */}
|
||||||
'relative h-full flex-1 flex items-center justify-center gap-design-50 overflow-visible'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{showStreakLimitOnly && (
|
|
||||||
<div
|
<div
|
||||||
className={'pointer-events-none absolute bg-center bg-no-repeat'}
|
className={
|
||||||
style={{
|
'relative flex h-full w-design-760 shrink-0 items-center justify-center gap-design-64 overflow-visible'
|
||||||
top: 'calc(var(--design-unit)*-14)',
|
}
|
||||||
right: 'calc(var(--design-unit)*-190)',
|
>
|
||||||
bottom: 'calc(var(--design-unit)*-1)',
|
{showStreakLimitOnly && (
|
||||||
left: 0,
|
<div
|
||||||
backgroundImage: `url(${streakBg})`,
|
className={
|
||||||
backgroundPosition: 'center',
|
'pointer-events-none absolute bg-center bg-no-repeat'
|
||||||
backgroundSize: '113% 150%',
|
}
|
||||||
}}
|
style={{
|
||||||
/>
|
top: 'calc(var(--design-unit)*-14)',
|
||||||
)}
|
right: 'calc(var(--design-unit)*-190)',
|
||||||
|
bottom: 'calc(var(--design-unit)*-1)',
|
||||||
|
left: 0,
|
||||||
|
backgroundImage: `url(${streakBg})`,
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
backgroundSize: '113% 150%',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!showStreakLimitOnly && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'shrink-0 whitespace-nowrap font-bold text-[#CBD3D5]'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('gameDesktop.status.odds')}:{' '}
|
||||||
|
<span className={'text-[#E3D171]'}>{oddsLabel}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'flex shrink-0 items-center gap-design-5 whitespace-nowrap font-bold text-[#CBD3D5]'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SmartImage
|
||||||
|
className={'w-design-37 h-design-47'}
|
||||||
|
alt={'fire'}
|
||||||
|
src={fire}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
{t('gameDesktop.status.streak')}:{' '}
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
'bg-gradient-to-b from-[#EBA661] to-[#FCC785] bg-clip-text text-transparent'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{streakLabel}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{!showStreakLimitOnly && (
|
|
||||||
<>
|
|
||||||
<div className={'text-[#CBD3D5] font-bold'}>
|
|
||||||
{t('gameDesktop.status.odds')}:{' '}
|
|
||||||
<span className={'text-[#E3D171]'}>{oddsLabel}</span>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'flex items-center gap-design-5 text-[#CBD3D5] font-bold'
|
'relative z-20 flex shrink-0 items-center gap-design-5 whitespace-nowrap font-bold text-[#CBD3D5]'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SmartImage
|
<SmartImage
|
||||||
className={'w-design-37 h-design-47'}
|
className={'w-design-25 h-design-33'}
|
||||||
alt={'fire'}
|
alt={'lock'}
|
||||||
src={fire}
|
src={lock}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div className={'flex items-center gap-design-10'}>
|
||||||
{t('gameDesktop.status.streak')}:{' '}
|
<div>{t('gameDesktop.status.limit')}:</div>
|
||||||
<span
|
<div className={'flex items-center gap-design-5'}>
|
||||||
className={
|
<SmartImage
|
||||||
'bg-gradient-to-b from-[#EBA661] to-[#FCC785] bg-clip-text text-transparent'
|
className={'w-design-35 h-design-35'}
|
||||||
}
|
alt={'diamond'}
|
||||||
>
|
src={diamond}
|
||||||
{streakLabel}
|
/>
|
||||||
</span>
|
<div>{limitLabel}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
<div className="relative z-20 flex h-[105px] w-design-360 items-center justify-center">
|
||||||
className={
|
<SmartBackground
|
||||||
'relative z-20 flex items-center gap-design-5 text-[#CBD3D5] font-bold'
|
src={statusCenter}
|
||||||
}
|
className="pointer-events-none absolute inset-0 bg-no-repeat bg-center bg-contain transition-opacity duration-500 ease-out"
|
||||||
>
|
size="contain"
|
||||||
<SmartImage
|
style={{
|
||||||
className={'w-design-25 h-design-33'}
|
opacity: showWarningCountdown ? 0.18 : 1,
|
||||||
alt={'lock'}
|
transform: showWarningCountdown ? 'scale(0.985)' : 'scale(1)',
|
||||||
src={lock}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className={'flex items-center gap-design-10'}>
|
<div
|
||||||
<div>{t('gameDesktop.status.limit')}:</div>
|
className="pointer-events-none absolute inset-0 overflow-visible transition-all duration-500 ease-out"
|
||||||
<div className={'flex items-center gap-design-5'}>
|
style={{
|
||||||
<SmartImage
|
opacity: showWarningCountdown ? 1 : 0,
|
||||||
className={'w-design-35 h-design-35'}
|
transform: showWarningCountdown
|
||||||
alt={'diamond'}
|
? 'scale(1.1) translateY(calc(var(--design-unit)*1))'
|
||||||
src={diamond}
|
: 'scale(1.02) translateY(0)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LottiePlayer
|
||||||
|
animationData={down5Animation}
|
||||||
|
className="h-full w-full"
|
||||||
|
loop
|
||||||
|
autoplay
|
||||||
/>
|
/>
|
||||||
<div>{limitLabel}</div>
|
</div>
|
||||||
|
<DesktopCountdown
|
||||||
|
initialMs={countdownMs}
|
||||||
|
onRemainingMsChange={setRemainingMs}
|
||||||
|
className={countdownClassName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid min-w-0 w-design-374 shrink-0 grid-cols-[minmax(0,0.78fr)_minmax(0,1.22fr)] items-center justify-items-stretch pl-design-6 pr-design-16">
|
||||||
|
<div className="flex min-w-0 items-center justify-center gap-design-6">
|
||||||
|
<span className="relative flex h-design-24 w-design-24 items-center justify-center">
|
||||||
|
<motion.span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute h-design-24 w-design-24 rounded-full bg-[rgba(120,255,127,0.22)] blur-[calc(var(--design-unit)*3)]"
|
||||||
|
animate={
|
||||||
|
prefersReducedMotion
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
opacity: [0.36, 0.88, 0.42],
|
||||||
|
scale: [0.9, 1.28, 0.98],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transition={
|
||||||
|
prefersReducedMotion
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
duration: 1.25,
|
||||||
|
ease: 'easeInOut',
|
||||||
|
repeat: Number.POSITIVE_INFINITY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<motion.span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="relative h-design-14 w-design-14 rounded-full bg-[#78FF7F] shadow-[0_0_calc(var(--design-unit)*8)_rgba(120,255,127,0.78),0_0_calc(var(--design-unit)*16)_rgba(120,255,127,0.34)]"
|
||||||
|
animate={
|
||||||
|
prefersReducedMotion
|
||||||
|
? undefined
|
||||||
|
: { opacity: [0.78, 1, 0.86], scale: [0.96, 1.12, 1] }
|
||||||
|
}
|
||||||
|
transition={
|
||||||
|
prefersReducedMotion
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
duration: 1.25,
|
||||||
|
ease: 'easeInOut',
|
||||||
|
repeat: Number.POSITIVE_INFINITY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
phaseToneClassName,
|
||||||
|
'font-black tracking-[0.08em] [text-shadow:0_0_calc(var(--design-unit)*10)_rgba(120,255,127,0.44)]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{phaseLabel}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex min-w-0 items-baseline justify-center gap-design-5 font-bold leading-none">
|
||||||
|
<span className="text-design-20 tracking-[0.06em] text-[#9FBAC0]">
|
||||||
|
{t('gameDesktop.status.roundId')}:
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0 truncate text-design-24 font-black tracking-[0.06em] text-[#E7FFFF] [text-shadow:0_0_calc(var(--design-unit)*9)_rgba(75,255,254,0.38)]">
|
||||||
|
{roundId}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SmartBackground>
|
||||||
</div>
|
</div>
|
||||||
|
<JackpotPoolTicker className="mt-design-0" variant="hud" />
|
||||||
<div className="relative z-20 flex h-[105px] w-design-360 items-center justify-center">
|
</div>
|
||||||
<SmartBackground
|
|
||||||
src={statusCenter}
|
|
||||||
className="pointer-events-none absolute inset-0 bg-no-repeat bg-center bg-contain transition-opacity duration-500 ease-out"
|
|
||||||
size="contain"
|
|
||||||
style={{
|
|
||||||
opacity: showWarningCountdown ? 0.18 : 1,
|
|
||||||
transform: showWarningCountdown ? 'scale(0.985)' : 'scale(1)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="pointer-events-none absolute inset-0 overflow-visible transition-all duration-500 ease-out"
|
|
||||||
style={{
|
|
||||||
opacity: showWarningCountdown ? 1 : 0,
|
|
||||||
transform: showWarningCountdown
|
|
||||||
? 'scale(1.1) translateY(calc(var(--design-unit)*1))'
|
|
||||||
: 'scale(1.02) translateY(0)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LottiePlayer
|
|
||||||
animationData={down5Animation}
|
|
||||||
className="h-full w-full"
|
|
||||||
loop
|
|
||||||
autoplay
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<DesktopCountdown
|
|
||||||
initialMs={countdownMs}
|
|
||||||
onRemainingMsChange={setRemainingMs}
|
|
||||||
className={countdownClassName}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-1 items-center justify-center gap-design-88">
|
|
||||||
<div className="flex items-center gap-design-9">
|
|
||||||
<span className="relative flex h-design-24 w-design-24 items-center justify-center">
|
|
||||||
<motion.span
|
|
||||||
aria-hidden="true"
|
|
||||||
className="absolute h-design-24 w-design-24 rounded-full bg-[rgba(120,255,127,0.22)] blur-[calc(var(--design-unit)*3)]"
|
|
||||||
animate={
|
|
||||||
prefersReducedMotion
|
|
||||||
? undefined
|
|
||||||
: { opacity: [0.36, 0.88, 0.42], scale: [0.9, 1.28, 0.98] }
|
|
||||||
}
|
|
||||||
transition={
|
|
||||||
prefersReducedMotion
|
|
||||||
? undefined
|
|
||||||
: {
|
|
||||||
duration: 1.25,
|
|
||||||
ease: 'easeInOut',
|
|
||||||
repeat: Number.POSITIVE_INFINITY,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<motion.span
|
|
||||||
aria-hidden="true"
|
|
||||||
className="relative h-design-14 w-design-14 rounded-full bg-[#78FF7F] shadow-[0_0_calc(var(--design-unit)*8)_rgba(120,255,127,0.78),0_0_calc(var(--design-unit)*16)_rgba(120,255,127,0.34)]"
|
|
||||||
animate={
|
|
||||||
prefersReducedMotion
|
|
||||||
? undefined
|
|
||||||
: { opacity: [0.78, 1, 0.86], scale: [0.96, 1.12, 1] }
|
|
||||||
}
|
|
||||||
transition={
|
|
||||||
prefersReducedMotion
|
|
||||||
? undefined
|
|
||||||
: {
|
|
||||||
duration: 1.25,
|
|
||||||
ease: 'easeInOut',
|
|
||||||
repeat: Number.POSITIVE_INFINITY,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
phaseToneClassName,
|
|
||||||
'font-black tracking-[0.08em] [text-shadow:0_0_calc(var(--design-unit)*10)_rgba(120,255,127,0.44)]',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{phaseLabel}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-baseline gap-design-7 font-bold leading-none">
|
|
||||||
<span className="text-design-20 tracking-[0.06em] text-[#9FBAC0]">
|
|
||||||
{t('gameDesktop.status.roundId')}:
|
|
||||||
</span>
|
|
||||||
<span className="text-design-24 font-black tracking-[0.06em] text-[#E7FFFF] [text-shadow:0_0_calc(var(--design-unit)*9)_rgba(75,255,254,0.38)]">
|
|
||||||
{roundId}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SmartBackground>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ function WithdrawField({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-design-14">
|
<div className="flex gap-design-14">
|
||||||
<div className="flex w-design-132 shrink-0 items-center justify-end whitespace-nowrap text-right text-design-16 font-medium uppercase leading-[1.15] tracking-[0.04em] text-[#6FD4DA]">
|
<div className="flex w-design-160 shrink-0 items-center justify-end text-right text-design-15 font-medium uppercase leading-[1.15] tracking-[0.04em] text-[#6FD4DA]">
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
<span className="pl-design-4">:</span>
|
<span className="pl-design-4">:</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,13 +132,15 @@ function AmountShell({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function QuickAmountCard({
|
function QuickAmountCard({
|
||||||
amount,
|
amountLabel,
|
||||||
preview,
|
diamondLabel,
|
||||||
|
feeLabel,
|
||||||
active,
|
active,
|
||||||
onClick,
|
onClick,
|
||||||
}: {
|
}: {
|
||||||
amount: number
|
amountLabel: string
|
||||||
preview: string
|
diamondLabel: string
|
||||||
|
feeLabel: string
|
||||||
active: boolean
|
active: boolean
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
}) {
|
}) {
|
||||||
@@ -147,7 +149,7 @@ function QuickAmountCard({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group relative flex h-design-76 min-w-0 w-full cursor-pointer flex-col items-start justify-center overflow-hidden rounded-[calc(var(--design-unit)*8)] border px-design-12 text-left transition-[transform,border-color,background-color,box-shadow] duration-150',
|
'group relative flex h-design-92 min-w-0 w-full cursor-pointer flex-col gap-design-5 items-start justify-center overflow-hidden rounded-[calc(var(--design-unit)*8)] border px-design-12 text-left transition-[transform,border-color,background-color,box-shadow] duration-150',
|
||||||
active
|
active
|
||||||
? 'border-[#D18A43] bg-[linear-gradient(180deg,rgba(88,54,28,0.96),rgba(56,33,18,0.92))] shadow-[0_0_calc(var(--design-unit)*14)_rgba(209,138,67,0.22),inset_0_0_calc(var(--design-unit)*12)_rgba(255,217,120,0.08)]'
|
? 'border-[#D18A43] bg-[linear-gradient(180deg,rgba(88,54,28,0.96),rgba(56,33,18,0.92))] shadow-[0_0_calc(var(--design-unit)*14)_rgba(209,138,67,0.22),inset_0_0_calc(var(--design-unit)*12)_rgba(255,217,120,0.08)]'
|
||||||
: 'border-[rgba(103,227,239,0.26)] bg-[linear-gradient(180deg,rgba(11,48,63,0.9),rgba(5,24,35,0.94))] hover:-translate-y-[1px] hover:border-[rgba(170,247,255,0.62)] hover:shadow-[0_0_calc(var(--design-unit)*12)_rgba(88,225,238,0.12)]',
|
: 'border-[rgba(103,227,239,0.26)] bg-[linear-gradient(180deg,rgba(11,48,63,0.9),rgba(5,24,35,0.94))] hover:-translate-y-[1px] hover:border-[rgba(170,247,255,0.62)] hover:shadow-[0_0_calc(var(--design-unit)*12)_rgba(88,225,238,0.12)]',
|
||||||
@@ -161,16 +163,19 @@ function QuickAmountCard({
|
|||||||
: 'bg-[rgba(122,220,230,0.26)]',
|
: 'bg-[rgba(122,220,230,0.26)]',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="text-design-24 font-semibold leading-none text-[#FFE229]">
|
<div className="max-w-full truncate pr-design-6 text-design-14 font-semibold tabular-nums leading-none text-[#FFE229]">
|
||||||
{amount}
|
{amountLabel}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'pt-design-6 text-design-12 leading-none tracking-[0.04em]',
|
'max-w-full truncate pt-design-7 text-[length:calc(var(--design-unit)*12)] leading-none',
|
||||||
active ? 'text-[#FFDFA4]' : 'text-[#63AEB6]',
|
active ? 'text-[#FFDFA4]' : 'text-[#63AEB6]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{preview}
|
{diamondLabel}
|
||||||
|
</div>
|
||||||
|
<div className="max-w-full truncate pt-design-5 text-design-10 leading-none text-[#F0B44A]">
|
||||||
|
{feeLabel}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
@@ -290,12 +295,12 @@ function PreviewRow({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex border-b border-[rgba(89,209,223,0.2)] last:border-b-0">
|
<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]">
|
<div className="flex w-[50%] min-w-0 shrink-0 items-center break-words border-r border-[rgba(89,209,223,0.2)] px-design-14 py-design-18 text-design-15 font-medium uppercase leading-[1.2] text-[#7CE3E8]">
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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]',
|
'flex min-w-0 flex-1 items-center justify-end break-words px-design-14 py-design-18 text-right text-design-16 text-[#E6FFFF]',
|
||||||
highlight && 'text-design-18 font-semibold text-[#6DFF83]',
|
highlight && 'text-design-18 font-semibold text-[#6DFF83]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -390,62 +395,6 @@ function DesktopWithdraw() {
|
|||||||
>
|
>
|
||||||
<div className="flex min-h-full min-w-0 flex-[1.7] flex-col px-design-16 py-design-14">
|
<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">
|
<div className="flex flex-col gap-design-12">
|
||||||
<WithdrawField
|
|
||||||
label={t('gameDesktop.withdraw.fields.diamondAmount')}
|
|
||||||
>
|
|
||||||
<AmountShell
|
|
||||||
amount={vm.amount}
|
|
||||||
availableBalanceText={t(
|
|
||||||
'gameDesktop.withdraw.availableBalance',
|
|
||||||
{ amount: formatNumber(vm.availableBalance) },
|
|
||||||
)}
|
|
||||||
onAmountChange={handleAmountChange}
|
|
||||||
onMinus={() => handleAmountChange(vm.amount - 1)}
|
|
||||||
onPlus={() => handleAmountChange(vm.amount + 1)}
|
|
||||||
/>
|
|
||||||
{hasSubmitted && vm.amountRequiredError ? (
|
|
||||||
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
|
|
||||||
{t('gameDesktop.withdraw.errors.amountRequired')}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{hasSubmitted && vm.amountExceedsBalance ? (
|
|
||||||
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
|
|
||||||
{t('gameDesktop.withdraw.errors.amountExceedsBalance')}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{hasSubmitted && vm.amountBelowPaymentMinimum ? (
|
|
||||||
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
|
|
||||||
{t('gameDesktop.withdraw.errors.amountBelowPaymentMinimum', {
|
|
||||||
amount: vm.paymentMinimumAmountLabel,
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{hasSubmitted && vm.amountAbovePaymentMaximum ? (
|
|
||||||
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
|
|
||||||
{t('gameDesktop.withdraw.errors.amountAbovePaymentMaximum', {
|
|
||||||
amount: vm.paymentMaximumAmountLabel,
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</WithdrawField>
|
|
||||||
|
|
||||||
<div className="flex items-start gap-design-14">
|
|
||||||
<div className="w-design-132 shrink-0" />
|
|
||||||
<div className="grid min-w-0 flex-1 grid-cols-3 gap-design-10">
|
|
||||||
{vm.quickAmounts.map((option) => (
|
|
||||||
<QuickAmountCard
|
|
||||||
key={option.id}
|
|
||||||
amount={option.diamonds}
|
|
||||||
preview={option.preview}
|
|
||||||
active={option.id === activeQuickAmountId}
|
|
||||||
onClick={() =>
|
|
||||||
handleQuickAmountSelect(option.id, option.diamonds)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<WithdrawField
|
<WithdrawField
|
||||||
label={t('gameDesktop.withdraw.fields.paymentChannel')}
|
label={t('gameDesktop.withdraw.fields.paymentChannel')}
|
||||||
>
|
>
|
||||||
@@ -516,6 +465,63 @@ function DesktopWithdraw() {
|
|||||||
</div>
|
</div>
|
||||||
</WithdrawField>
|
</WithdrawField>
|
||||||
|
|
||||||
|
<WithdrawField
|
||||||
|
label={t('gameDesktop.withdraw.fields.diamondAmount')}
|
||||||
|
>
|
||||||
|
<AmountShell
|
||||||
|
amount={vm.amount}
|
||||||
|
availableBalanceText={t(
|
||||||
|
'gameDesktop.withdraw.availableBalance',
|
||||||
|
{ amount: formatNumber(vm.availableBalance) },
|
||||||
|
)}
|
||||||
|
onAmountChange={handleAmountChange}
|
||||||
|
onMinus={() => handleAmountChange(vm.amount - 1)}
|
||||||
|
onPlus={() => handleAmountChange(vm.amount + 1)}
|
||||||
|
/>
|
||||||
|
{hasSubmitted && vm.amountRequiredError ? (
|
||||||
|
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
|
||||||
|
{t('gameDesktop.withdraw.errors.amountRequired')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{hasSubmitted && vm.amountExceedsBalance ? (
|
||||||
|
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
|
||||||
|
{t('gameDesktop.withdraw.errors.amountExceedsBalance')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{hasSubmitted && vm.amountBelowPaymentMinimum ? (
|
||||||
|
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
|
||||||
|
{t('gameDesktop.withdraw.errors.amountBelowPaymentMinimum', {
|
||||||
|
amount: vm.paymentMinimumAmountLabel,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{hasSubmitted && vm.amountAbovePaymentMaximum ? (
|
||||||
|
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
|
||||||
|
{t('gameDesktop.withdraw.errors.amountAbovePaymentMaximum', {
|
||||||
|
amount: vm.paymentMaximumAmountLabel,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</WithdrawField>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-design-14">
|
||||||
|
<div className="w-design-160 shrink-0" />
|
||||||
|
<div className="grid min-w-0 flex-1 grid-cols-3 gap-design-10">
|
||||||
|
{vm.quickAmounts.map((option) => (
|
||||||
|
<QuickAmountCard
|
||||||
|
key={option.id}
|
||||||
|
amountLabel={option.amountLabel}
|
||||||
|
diamondLabel={option.diamondLabel}
|
||||||
|
feeLabel={option.feeLabel}
|
||||||
|
active={option.id === activeQuickAmountId}
|
||||||
|
onClick={() =>
|
||||||
|
handleQuickAmountSelect(option.id, option.diamonds)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<WithdrawField
|
<WithdrawField
|
||||||
label={t('gameDesktop.withdraw.fields.currencyType')}
|
label={t('gameDesktop.withdraw.fields.currencyType')}
|
||||||
alignStart={false}
|
alignStart={false}
|
||||||
@@ -664,7 +670,7 @@ function DesktopWithdraw() {
|
|||||||
|
|
||||||
<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="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 min-h-full min-w-0 w-design-640 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]">
|
<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]">
|
||||||
{t('gameDesktop.withdraw.preview.title')}
|
{t('gameDesktop.withdraw.preview.title')}
|
||||||
</div>
|
</div>
|
||||||
@@ -684,6 +690,10 @@ function DesktopWithdraw() {
|
|||||||
value={vm.selectedCurrencyPreview.convertibleValue}
|
value={vm.selectedCurrencyPreview.convertibleValue}
|
||||||
highlight={true}
|
highlight={true}
|
||||||
/>
|
/>
|
||||||
|
<PreviewRow
|
||||||
|
label={vm.selectedCurrencyPreview.handlingFeeLabel}
|
||||||
|
value={vm.selectedCurrencyPreview.handlingFeeValue}
|
||||||
|
/>
|
||||||
<PreviewRow
|
<PreviewRow
|
||||||
label={t(
|
label={t(
|
||||||
'gameDesktop.withdraw.preview.fixedExchangeDiamondAmount',
|
'gameDesktop.withdraw.preview.fixedExchangeDiamondAmount',
|
||||||
@@ -735,7 +745,7 @@ function DesktopWithdraw() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{withdrawSubmitMutation.isPending
|
{withdrawSubmitMutation.isPending
|
||||||
? t('commonUi.action.submitting')
|
? t('commonUi.actions.submitting')
|
||||||
: `${t('gameDesktop.withdraw.confirm')}${t('gameDesktop.withdraw.withdrawal')}`}
|
: `${t('gameDesktop.withdraw.confirm')}${t('gameDesktop.withdraw.withdrawal')}`}
|
||||||
</SmartBackground>
|
</SmartBackground>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { SmartImage } from '@/components/smart-image'
|
|||||||
import { MobileAnimalOverlay } from '@/features/game/components/mobile/mobile-animal-overlay.tsx'
|
import { MobileAnimalOverlay } from '@/features/game/components/mobile/mobile-animal-overlay.tsx'
|
||||||
import { MobileStatusLine } from '@/features/game/components/mobile/mobile-status.tsx'
|
import { MobileStatusLine } from '@/features/game/components/mobile/mobile-status.tsx'
|
||||||
import { AnimalSpriteImage } from '@/features/game/components/shared/animal-sprite-image'
|
import { AnimalSpriteImage } from '@/features/game/components/shared/animal-sprite-image'
|
||||||
|
import { LongPressProgress } from '@/features/game/components/shared/long-press-progress'
|
||||||
import { FLOWER_IMAGE_LIST } from '@/features/game/shared'
|
import { FLOWER_IMAGE_LIST } from '@/features/game/shared'
|
||||||
import { useAnimalVm } from '@/hooks/use-animal-vm'
|
import { useAnimalVm } from '@/hooks/use-animal-vm'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
@@ -83,10 +84,17 @@ export function MobileAnimal({
|
|||||||
)
|
)
|
||||||
const {
|
const {
|
||||||
cellWarning,
|
cellWarning,
|
||||||
|
handleClearCell,
|
||||||
|
handleLongPressCancel,
|
||||||
|
handleLongPressEnd,
|
||||||
|
handleLongPressMove,
|
||||||
|
handleLongPressStart,
|
||||||
handleSelect,
|
handleSelect,
|
||||||
handleStart,
|
handleStart,
|
||||||
isRealtimeConnecting,
|
isRealtimeConnecting,
|
||||||
lockInteraction,
|
lockInteraction,
|
||||||
|
longPressCellId,
|
||||||
|
longPressDurationMs,
|
||||||
marqueeId,
|
marqueeId,
|
||||||
selectionByCell,
|
selectionByCell,
|
||||||
showStandbyState,
|
showStandbyState,
|
||||||
@@ -216,6 +224,18 @@ export function MobileAnimal({
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={lockInteraction || showStopOverlay}
|
disabled={lockInteraction || showStopOverlay}
|
||||||
onClick={() => handleSelect(item.id)}
|
onClick={() => handleSelect(item.id)}
|
||||||
|
onContextMenu={(event) => event.preventDefault()}
|
||||||
|
onDragStart={(event) => event.preventDefault()}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Delete' || event.key === 'Backspace') {
|
||||||
|
event.preventDefault()
|
||||||
|
handleClearCell(item.id)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onPointerCancel={handleLongPressCancel}
|
||||||
|
onPointerDown={(event) => handleLongPressStart(item.id, event)}
|
||||||
|
onPointerMove={handleLongPressMove}
|
||||||
|
onPointerUp={handleLongPressEnd}
|
||||||
animate={
|
animate={
|
||||||
showCellWarning
|
showCellWarning
|
||||||
? {
|
? {
|
||||||
@@ -238,7 +258,7 @@ export function MobileAnimal({
|
|||||||
: { duration: 0.16, ease: 'easeOut' }
|
: { duration: 0.16, ease: 'easeOut' }
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex h-design-36 flex-col items-center justify-center rounded-[calc(var(--design-unit)*6)] border border-transparent transition-[transform,border-color,box-shadow,opacity] duration-150',
|
'mobile-animal-touch-target relative flex h-design-36 flex-col items-center justify-center rounded-[calc(var(--design-unit)*6)] border border-transparent transition-[transform,border-color,box-shadow,opacity] duration-150',
|
||||||
!isRevealActive && 'overflow-hidden',
|
!isRevealActive && 'overflow-hidden',
|
||||||
lockInteraction
|
lockInteraction
|
||||||
? 'cursor-not-allowed opacity-90'
|
? 'cursor-not-allowed opacity-90'
|
||||||
@@ -376,6 +396,13 @@ export function MobileAnimal({
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
{longPressCellId === item.id ? (
|
||||||
|
<LongPressProgress
|
||||||
|
compact
|
||||||
|
durationMs={longPressDurationMs}
|
||||||
|
label={t('gameDesktop.animal.cancelBet')}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import { motion } from 'motion/react'
|
import { motion } from 'motion/react'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import reduce from '@/assets/game/add.webp'
|
|
||||||
import chipBg from '@/assets/game/chip-bg.webp'
|
import chipBg from '@/assets/game/chip-bg.webp'
|
||||||
import chipLineBg from '@/assets/game/chip-line-bg.webp'
|
import chipLineBg from '@/assets/game/chip-line-bg.webp'
|
||||||
import chipLock from '@/assets/game/chip-lock.webp'
|
import chipLock from '@/assets/game/chip-lock.webp'
|
||||||
import confirmRedBg from '@/assets/game/confirm-red-bg.png'
|
import confirmRedBg from '@/assets/game/confirm-red-bg.png'
|
||||||
import controlBg from '@/assets/game/control-bg.png'
|
import controlBg from '@/assets/game/control-bg.png'
|
||||||
import leftBottomBg from '@/assets/game/left-bg.webp'
|
import leftBottomBg from '@/assets/game/left-bg.webp'
|
||||||
import mobileAddReduceBg from '@/assets/game/mobile-add-reduce-bg.webp'
|
|
||||||
import mobileConfirmBg from '@/assets/game/mobile-contro-comfirm.webp'
|
import mobileConfirmBg from '@/assets/game/mobile-contro-comfirm.webp'
|
||||||
import mobileTotalBg from '@/assets/game/mobile-control-number.webp'
|
import mobileTotalBg from '@/assets/game/mobile-control-number.webp'
|
||||||
import add from '@/assets/game/reduce.webp'
|
|
||||||
import diamond from '@/assets/system/diamond.webp'
|
import diamond from '@/assets/system/diamond.webp'
|
||||||
import { SmartBackground } from '@/components/smart-background.tsx'
|
import { SmartBackground } from '@/components/smart-background.tsx'
|
||||||
import { SmartImage } from '@/components/smart-image.tsx'
|
import { SmartImage } from '@/components/smart-image.tsx'
|
||||||
@@ -27,8 +24,6 @@ export function MobileControl() {
|
|||||||
acceptingBets,
|
acceptingBets,
|
||||||
actionsEnabled,
|
actionsEnabled,
|
||||||
canClear,
|
canClear,
|
||||||
canDecreaseBetQuantity,
|
|
||||||
canIncreaseBetQuantity,
|
|
||||||
chips,
|
chips,
|
||||||
confirmLabel,
|
confirmLabel,
|
||||||
confirmState,
|
confirmState,
|
||||||
@@ -37,11 +32,8 @@ export function MobileControl() {
|
|||||||
onChipSelect,
|
onChipSelect,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onClearSelections,
|
onClearSelections,
|
||||||
onDecreaseBetQuantity,
|
|
||||||
onIncreaseBetQuantity,
|
|
||||||
onOpenAutoSetting,
|
onOpenAutoSetting,
|
||||||
onRepeatSelections,
|
onRepeatSelections,
|
||||||
selectedBetQuantityLabel,
|
|
||||||
selectedChipId,
|
selectedChipId,
|
||||||
selectedCountLabel,
|
selectedCountLabel,
|
||||||
totalBetAmountLabel,
|
totalBetAmountLabel,
|
||||||
@@ -154,7 +146,7 @@ export function MobileControl() {
|
|||||||
<SmartBackground
|
<SmartBackground
|
||||||
src={chipLineBg}
|
src={chipLineBg}
|
||||||
size="100% 100%"
|
size="100% 100%"
|
||||||
className="flex h-design-32 min-w-0 items-center justify-between gap-design-7 overflow-visible"
|
className="flex h-design-32 min-w-0 flex-1 items-center justify-around overflow-visible"
|
||||||
>
|
>
|
||||||
{chips.map((chip) => {
|
{chips.map((chip) => {
|
||||||
const isSelected = chip.id === selectedChipId
|
const isSelected = chip.id === selectedChipId
|
||||||
@@ -287,50 +279,6 @@ export function MobileControl() {
|
|||||||
})}
|
})}
|
||||||
</SmartBackground>
|
</SmartBackground>
|
||||||
</SmartBackground>
|
</SmartBackground>
|
||||||
|
|
||||||
<SmartBackground
|
|
||||||
src={mobileAddReduceBg}
|
|
||||||
size="100% 100%"
|
|
||||||
className="relative z-20 !ml-[calc(var(--design-unit)*-8)] pl-design-15 flex h-full w-design-103 shrink-0 items-center justify-center bg-center bg-no-repeat px-design-7 text-design-13 font-bold"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={!acceptingBets || !canDecreaseBetQuantity}
|
|
||||||
onClick={onDecreaseBetQuantity}
|
|
||||||
className={cn(
|
|
||||||
'flex h-design-24 w-design-24 shrink-0 items-center justify-center',
|
|
||||||
acceptingBets && canDecreaseBetQuantity
|
|
||||||
? 'cursor-pointer'
|
|
||||||
: 'cursor-not-allowed opacity-50',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<SmartImage
|
|
||||||
src={add}
|
|
||||||
alt="decrease"
|
|
||||||
className="h-design-16 w-design-16"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<div className="flex h-design-24 w-design-20 items-center justify-center rounded-sm">
|
|
||||||
{selectedBetQuantityLabel}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={!acceptingBets || !canIncreaseBetQuantity}
|
|
||||||
onClick={onIncreaseBetQuantity}
|
|
||||||
className={cn(
|
|
||||||
'flex h-design-24 w-design-24 shrink-0 items-center justify-center',
|
|
||||||
acceptingBets && canIncreaseBetQuantity
|
|
||||||
? 'cursor-pointer'
|
|
||||||
: 'cursor-not-allowed opacity-50',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<SmartImage
|
|
||||||
src={reduce}
|
|
||||||
alt="increase"
|
|
||||||
className="h-design-16 w-design-16"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</SmartBackground>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex h-design-40 w-full items-center gap-design-3">
|
<div className="flex h-design-40 w-full items-center gap-design-3">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Info,
|
Info,
|
||||||
Mail,
|
Mail,
|
||||||
|
RefreshCw,
|
||||||
UserKey,
|
UserKey,
|
||||||
UserRoundPlus,
|
UserRoundPlus,
|
||||||
Volume2,
|
Volume2,
|
||||||
@@ -15,6 +16,7 @@ import diamond from '@/assets/system/diamond.webp'
|
|||||||
import logo from '@/assets/system/logo.webp'
|
import logo from '@/assets/system/logo.webp'
|
||||||
import { SmartImage } from '@/components/smart-image.tsx'
|
import { SmartImage } from '@/components/smart-image.tsx'
|
||||||
import { useHeaderClockLabel, useHeaderVm } from '@/hooks/use-header-vm'
|
import { useHeaderClockLabel, useHeaderVm } from '@/hooks/use-header-vm'
|
||||||
|
import { useWalletBalanceRefresh } from '@/hooks/use-wallet-balance-refresh'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useModalStore } from '@/store'
|
import { useModalStore } from '@/store'
|
||||||
|
|
||||||
@@ -68,6 +70,8 @@ function SignalBars({
|
|||||||
export function MobileHeader() {
|
export function MobileHeader() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
||||||
|
const { isRefreshingBalance, refreshWalletBalance } =
|
||||||
|
useWalletBalanceRefresh()
|
||||||
const {
|
const {
|
||||||
authStatus,
|
authStatus,
|
||||||
currentLanguageLabel,
|
currentLanguageLabel,
|
||||||
@@ -133,11 +137,7 @@ export function MobileHeader() {
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<div className="group relative flex min-w-0 items-center justify-center">
|
||||||
type="button"
|
|
||||||
onClick={onOpenProcedures}
|
|
||||||
className="group relative flex min-w-0 items-center justify-center transition-transform duration-150 hover:-translate-y-[1px] active:translate-y-[1px]"
|
|
||||||
>
|
|
||||||
<SmartImage
|
<SmartImage
|
||||||
src={diamond}
|
src={diamond}
|
||||||
alt="diamond"
|
alt="diamond"
|
||||||
@@ -150,14 +150,36 @@ export function MobileHeader() {
|
|||||||
<span className="min-w-0 truncate">
|
<span className="min-w-0 truncate">
|
||||||
{currentUser?.coin || '--'}
|
{currentUser?.coin || '--'}
|
||||||
</span>
|
</span>
|
||||||
<SmartImage
|
<button
|
||||||
src={add}
|
type="button"
|
||||||
alt="add"
|
aria-label={t('commonUi.actions.refreshBalance')}
|
||||||
priority
|
title={t('commonUi.actions.refreshBalance')}
|
||||||
|
disabled={isRefreshingBalance}
|
||||||
|
onClick={() => void refreshWalletBalance()}
|
||||||
|
className="flex h-design-13 w-design-13 shrink-0 cursor-pointer items-center justify-center text-[#8DE4EA] disabled:cursor-wait disabled:opacity-65"
|
||||||
|
>
|
||||||
|
<RefreshCw
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn(
|
||||||
|
'h-design-9 w-design-9',
|
||||||
|
isRefreshingBalance && 'animate-spin',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onOpenProcedures}
|
||||||
className="h-design-13 w-design-13 shrink-0 cursor-pointer"
|
className="h-design-13 w-design-13 shrink-0 cursor-pointer"
|
||||||
/>
|
>
|
||||||
|
<SmartImage
|
||||||
|
src={add}
|
||||||
|
alt="add"
|
||||||
|
priority
|
||||||
|
className="h-full w-full"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full shrink-0 items-center justify-end gap-design-8 px-design-9">
|
<div className="flex h-full shrink-0 items-center justify-end gap-design-8 px-design-9">
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import { DesktopCountdown } from '@/features/game/components/desktop/desktop-cou
|
|||||||
import { useGameStatusVm } from '@/hooks/use-game-status-vm.ts'
|
import { useGameStatusVm } from '@/hooks/use-game-status-vm.ts'
|
||||||
import { cn } from '@/lib/utils.ts'
|
import { cn } from '@/lib/utils.ts'
|
||||||
|
|
||||||
|
const FORCE_MOBILE_STREAK_EFFECT_PREVIEW = false
|
||||||
|
|
||||||
function isIosDevice() {
|
function isIosDevice() {
|
||||||
if (typeof navigator === 'undefined') {
|
if (typeof navigator === 'undefined') {
|
||||||
return false
|
return false
|
||||||
@@ -160,34 +162,41 @@ export function MobileStatusLine() {
|
|||||||
export function MobileStatusMetrics() {
|
export function MobileStatusMetrics() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { limitLabel, oddsLabel, streakLabel, streakValue } = useGameStatusVm()
|
const { limitLabel, oddsLabel, streakLabel, streakValue } = useGameStatusVm()
|
||||||
const showStreakLimitOnly = typeof streakValue === 'number' && streakValue > 1
|
const showStreakLimitOnly =
|
||||||
|
FORCE_MOBILE_STREAK_EFFECT_PREVIEW ||
|
||||||
|
(typeof streakValue === 'number' && streakValue > 1)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SmartBackground
|
<SmartBackground
|
||||||
src={statusLine}
|
src={showStreakLimitOnly ? streakBg : statusLine}
|
||||||
size="100% 100%"
|
size={showStreakLimitOnly ? '106% 126%' : '100% 100%'}
|
||||||
className="relative flex h-design-26 w-full items-center overflow-visible bg-center bg-no-repeat px-design-10 font-bold leading-none text-[#CBD3D5]"
|
className={cn(
|
||||||
style={{ fontSize: 'calc(var(--design-unit) * 7)' }}
|
'relative grid h-design-26 w-full items-center overflow-visible bg-center bg-no-repeat px-design-3 font-bold leading-none text-[#CBD3D5]',
|
||||||
|
showStreakLimitOnly
|
||||||
|
? 'grid-cols-2 text-design-7'
|
||||||
|
: 'grid-cols-3 text-[calc(var(--design-unit)*5.5)]',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{showStreakLimitOnly ? (
|
{showStreakLimitOnly ? (
|
||||||
<div
|
<div className="relative z-10 flex min-w-0 items-center justify-center gap-design-2 whitespace-nowrap [text-shadow:0_0_calc(var(--design-unit)*3)_#000,0_calc(var(--design-unit)*1)_calc(var(--design-unit)*1)_#000]">
|
||||||
className="pointer-events-none absolute left-0 right-0 z-0 bg-center bg-no-repeat opacity-95"
|
<SmartImage
|
||||||
style={{
|
className="h-design-10 w-design-8"
|
||||||
backgroundImage: `url(${streakBg})`,
|
alt="fire"
|
||||||
backgroundSize: '120% 150%',
|
src={fire}
|
||||||
bottom: 'calc(var(--design-unit)*-5)',
|
/>
|
||||||
top: 'calc(var(--design-unit)*-6)',
|
<span>{t('gameDesktop.status.streak')}:</span>
|
||||||
}}
|
{/*<span className="bg-gradient-to-b from-[#FF9C36] to-[#FF9C36] bg-clip-text text-transparent">*/}
|
||||||
/>
|
<span className="text-[#FF9C36]">{streakLabel}</span>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="relative z-10 flex min-w-0 flex-1 items-center justify-center">
|
<div className="relative z-10 flex min-w-0 items-center justify-center whitespace-nowrap">
|
||||||
{t('gameDesktop.status.odds')}:
|
{t('gameDesktop.status.odds')}:
|
||||||
<span className="ml-design-3 text-[#E3D171]">{oddsLabel}</span>
|
<span className="ml-design-1 text-[#E3D171]">{oddsLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative z-10 flex min-w-0 flex-1 items-center justify-center gap-design-2">
|
<div className="relative z-10 flex min-w-0 items-center justify-center gap-design-1 whitespace-nowrap">
|
||||||
<SmartImage
|
<SmartImage
|
||||||
className="h-design-11 w-design-9"
|
className="h-design-8 w-design-7"
|
||||||
alt="fire"
|
alt="fire"
|
||||||
src={fire}
|
src={fire}
|
||||||
/>
|
/>
|
||||||
@@ -198,11 +207,18 @@ export function MobileStatusMetrics() {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="relative z-10 flex min-w-0 flex-1 items-center justify-center gap-design-2">
|
<div
|
||||||
<SmartImage className="h-design-10 w-design-8" alt="lock" src={lock} />
|
className={cn(
|
||||||
|
'relative z-10 flex min-w-0 items-center justify-center whitespace-nowrap',
|
||||||
|
showStreakLimitOnly
|
||||||
|
? 'col-start-2 gap-design-2 [text-shadow:0_0_calc(var(--design-unit)*3)_#000,0_calc(var(--design-unit)*1)_calc(var(--design-unit)*1)_#000]'
|
||||||
|
: 'col-start-3 gap-design-1',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SmartImage className="h-design-8 w-design-6" alt="lock" src={lock} />
|
||||||
<span>{t('gameDesktop.status.limit')}:</span>
|
<span>{t('gameDesktop.status.limit')}:</span>
|
||||||
<SmartImage
|
<SmartImage
|
||||||
className="h-design-10 w-design-10"
|
className="h-design-8 w-design-8"
|
||||||
alt="diamond"
|
alt="diamond"
|
||||||
src={diamond}
|
src={diamond}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -122,13 +122,15 @@ function AmountShell({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function QuickAmountCard({
|
function QuickAmountCard({
|
||||||
amount,
|
amountLabel,
|
||||||
preview,
|
diamondLabel,
|
||||||
|
feeLabel,
|
||||||
active,
|
active,
|
||||||
onClick,
|
onClick,
|
||||||
}: {
|
}: {
|
||||||
amount: number
|
amountLabel: string
|
||||||
preview: string
|
diamondLabel: string
|
||||||
|
feeLabel: string
|
||||||
active: boolean
|
active: boolean
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
}) {
|
}) {
|
||||||
@@ -137,7 +139,7 @@ function QuickAmountCard({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group relative flex h-design-42 min-w-0 w-full cursor-pointer flex-col items-start justify-center overflow-hidden rounded-[calc(var(--design-unit)*6)] border px-design-6 text-left transition-[border-color,background-color,box-shadow,filter] duration-150',
|
'group relative flex h-design-58 min-w-0 w-full cursor-pointer flex-col items-start justify-center overflow-hidden rounded-[calc(var(--design-unit)*6)] border px-design-6 text-left transition-[border-color,background-color,box-shadow,filter] duration-150',
|
||||||
active
|
active
|
||||||
? 'border-[#D18A43] bg-[linear-gradient(180deg,rgba(88,54,28,0.96),rgba(56,33,18,0.92))] shadow-[0_0_calc(var(--design-unit)*10)_rgba(209,138,67,0.2),inset_0_0_calc(var(--design-unit)*10)_rgba(255,217,120,0.08)]'
|
? 'border-[#D18A43] bg-[linear-gradient(180deg,rgba(88,54,28,0.96),rgba(56,33,18,0.92))] shadow-[0_0_calc(var(--design-unit)*10)_rgba(209,138,67,0.2),inset_0_0_calc(var(--design-unit)*10)_rgba(255,217,120,0.08)]'
|
||||||
: 'border-[rgba(103,227,239,0.26)] bg-[linear-gradient(180deg,rgba(11,48,63,0.9),rgba(5,24,35,0.94))] hover:border-[rgba(170,247,255,0.62)] hover:brightness-110',
|
: 'border-[rgba(103,227,239,0.26)] bg-[linear-gradient(180deg,rgba(11,48,63,0.9),rgba(5,24,35,0.94))] hover:border-[rgba(170,247,255,0.62)] hover:brightness-110',
|
||||||
@@ -151,16 +153,19 @@ function QuickAmountCard({
|
|||||||
: 'bg-[rgba(122,220,230,0.26)]',
|
: 'bg-[rgba(122,220,230,0.26)]',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="max-w-full truncate text-design-13 font-semibold leading-none text-[#FFE229]">
|
<div className="max-w-full truncate text-design-12 font-semibold leading-none text-[#FFE229]">
|
||||||
{amount}
|
{amountLabel}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'max-w-full truncate pt-design-3 text-design-10 leading-none',
|
'max-w-full truncate pt-design-3 text-[length:calc(var(--design-unit)*10)] leading-none',
|
||||||
active ? 'text-[#FFDFA4]' : 'text-[#63AEB6]',
|
active ? 'text-[#FFDFA4]' : 'text-[#63AEB6]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{preview}
|
{diamondLabel}
|
||||||
|
</div>
|
||||||
|
<div className="max-w-full truncate pt-design-2 text-design-10 leading-none text-[#F0B44A]">
|
||||||
|
{feeLabel}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
@@ -380,59 +385,6 @@ function MobileWithdraw() {
|
|||||||
>
|
>
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto px-design-8 py-design-7">
|
<div className="min-h-0 flex-1 overflow-y-auto px-design-8 py-design-7">
|
||||||
<div className="flex flex-col !gap-design-10">
|
<div className="flex flex-col !gap-design-10">
|
||||||
<WithdrawField
|
|
||||||
label={t('gameDesktop.withdraw.fields.diamondAmount')}
|
|
||||||
>
|
|
||||||
<AmountShell
|
|
||||||
amount={vm.amount}
|
|
||||||
availableBalanceText={t(
|
|
||||||
'gameDesktop.withdraw.availableBalance',
|
|
||||||
{ amount: formatNumber(vm.availableBalance) },
|
|
||||||
)}
|
|
||||||
onAmountChange={handleAmountChange}
|
|
||||||
onMinus={() => handleAmountChange(vm.amount - 1)}
|
|
||||||
onPlus={() => handleAmountChange(vm.amount + 1)}
|
|
||||||
/>
|
|
||||||
{hasSubmitted && vm.amountRequiredError ? (
|
|
||||||
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
|
|
||||||
{t('gameDesktop.withdraw.errors.amountRequired')}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{hasSubmitted && vm.amountExceedsBalance ? (
|
|
||||||
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
|
|
||||||
{t('gameDesktop.withdraw.errors.amountExceedsBalance')}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{hasSubmitted && vm.amountBelowPaymentMinimum ? (
|
|
||||||
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
|
|
||||||
{t('gameDesktop.withdraw.errors.amountBelowPaymentMinimum', {
|
|
||||||
amount: vm.paymentMinimumAmountLabel,
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{hasSubmitted && vm.amountAbovePaymentMaximum ? (
|
|
||||||
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
|
|
||||||
{t('gameDesktop.withdraw.errors.amountAbovePaymentMaximum', {
|
|
||||||
amount: vm.paymentMaximumAmountLabel,
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</WithdrawField>
|
|
||||||
|
|
||||||
<div className="grid min-w-0 grid-cols-3 gap-design-5">
|
|
||||||
{vm.quickAmounts.map((option) => (
|
|
||||||
<QuickAmountCard
|
|
||||||
key={option.id}
|
|
||||||
amount={option.diamonds}
|
|
||||||
preview={option.preview}
|
|
||||||
active={option.id === activeQuickAmountId}
|
|
||||||
onClick={() =>
|
|
||||||
handleQuickAmountSelect(option.id, option.diamonds)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<WithdrawField
|
<WithdrawField
|
||||||
label={t('gameDesktop.withdraw.fields.paymentChannel')}
|
label={t('gameDesktop.withdraw.fields.paymentChannel')}
|
||||||
>
|
>
|
||||||
@@ -500,6 +452,60 @@ function MobileWithdraw() {
|
|||||||
</div>
|
</div>
|
||||||
</WithdrawField>
|
</WithdrawField>
|
||||||
|
|
||||||
|
<WithdrawField
|
||||||
|
label={t('gameDesktop.withdraw.fields.diamondAmount')}
|
||||||
|
>
|
||||||
|
<AmountShell
|
||||||
|
amount={vm.amount}
|
||||||
|
availableBalanceText={t(
|
||||||
|
'gameDesktop.withdraw.availableBalance',
|
||||||
|
{ amount: formatNumber(vm.availableBalance) },
|
||||||
|
)}
|
||||||
|
onAmountChange={handleAmountChange}
|
||||||
|
onMinus={() => handleAmountChange(vm.amount - 1)}
|
||||||
|
onPlus={() => handleAmountChange(vm.amount + 1)}
|
||||||
|
/>
|
||||||
|
{hasSubmitted && vm.amountRequiredError ? (
|
||||||
|
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
|
||||||
|
{t('gameDesktop.withdraw.errors.amountRequired')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{hasSubmitted && vm.amountExceedsBalance ? (
|
||||||
|
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
|
||||||
|
{t('gameDesktop.withdraw.errors.amountExceedsBalance')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{hasSubmitted && vm.amountBelowPaymentMinimum ? (
|
||||||
|
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
|
||||||
|
{t('gameDesktop.withdraw.errors.amountBelowPaymentMinimum', {
|
||||||
|
amount: vm.paymentMinimumAmountLabel,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{hasSubmitted && vm.amountAbovePaymentMaximum ? (
|
||||||
|
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
|
||||||
|
{t('gameDesktop.withdraw.errors.amountAbovePaymentMaximum', {
|
||||||
|
amount: vm.paymentMaximumAmountLabel,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</WithdrawField>
|
||||||
|
|
||||||
|
<div className="grid min-w-0 grid-cols-3 gap-design-5">
|
||||||
|
{vm.quickAmounts.map((option) => (
|
||||||
|
<QuickAmountCard
|
||||||
|
key={option.id}
|
||||||
|
amountLabel={option.amountLabel}
|
||||||
|
diamondLabel={option.diamondLabel}
|
||||||
|
feeLabel={option.feeLabel}
|
||||||
|
active={option.id === activeQuickAmountId}
|
||||||
|
onClick={() =>
|
||||||
|
handleQuickAmountSelect(option.id, option.diamonds)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<WithdrawField
|
<WithdrawField
|
||||||
label={t('gameDesktop.withdraw.fields.currencyType')}
|
label={t('gameDesktop.withdraw.fields.currencyType')}
|
||||||
>
|
>
|
||||||
@@ -654,6 +660,10 @@ function MobileWithdraw() {
|
|||||||
value={vm.selectedCurrencyPreview.convertibleValue}
|
value={vm.selectedCurrencyPreview.convertibleValue}
|
||||||
highlight={true}
|
highlight={true}
|
||||||
/>
|
/>
|
||||||
|
<PreviewRow
|
||||||
|
label={vm.selectedCurrencyPreview.handlingFeeLabel}
|
||||||
|
value={vm.selectedCurrencyPreview.handlingFeeValue}
|
||||||
|
/>
|
||||||
<PreviewRow
|
<PreviewRow
|
||||||
label={t(
|
label={t(
|
||||||
'gameDesktop.withdraw.preview.fixedExchangeDiamondAmount',
|
'gameDesktop.withdraw.preview.fixedExchangeDiamondAmount',
|
||||||
@@ -707,7 +717,7 @@ function MobileWithdraw() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{withdrawSubmitMutation.isPending
|
{withdrawSubmitMutation.isPending
|
||||||
? t('commonUi.action.submitting')
|
? t('commonUi.actions.submitting')
|
||||||
: `${t('gameDesktop.withdraw.confirm')}`}
|
: `${t('gameDesktop.withdraw.confirm')}`}
|
||||||
</SmartBackground>
|
</SmartBackground>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
77
src/features/game/components/shared/long-press-progress.tsx
Normal file
77
src/features/game/components/shared/long-press-progress.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { motion } from 'motion/react'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface LongPressProgressProps {
|
||||||
|
compact?: boolean
|
||||||
|
durationMs: number
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LongPressProgress({
|
||||||
|
compact = false,
|
||||||
|
durationMs,
|
||||||
|
label,
|
||||||
|
}: LongPressProgressProps) {
|
||||||
|
return (
|
||||||
|
<motion.span
|
||||||
|
aria-hidden="true"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
className={cn(
|
||||||
|
'pointer-events-none absolute inset-0 z-50 flex items-center justify-center bg-[rgba(20,3,6,0.42)]',
|
||||||
|
compact
|
||||||
|
? 'rounded-[calc(var(--design-unit)*6)]'
|
||||||
|
: 'rounded-[calc(var(--design-unit)*18)]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<motion.span
|
||||||
|
initial={{ opacity: 0.25, scale: 0.82 }}
|
||||||
|
animate={{ opacity: 0.9, scale: 1 }}
|
||||||
|
transition={{ duration: durationMs / 1000, ease: 'linear' }}
|
||||||
|
className={cn(
|
||||||
|
'absolute rounded-full bg-[radial-gradient(circle,rgba(255,104,104,0.28)_0%,rgba(255,48,64,0.12)_52%,transparent_72%)]',
|
||||||
|
compact ? 'h-design-25 w-design-25' : 'h-design-76 w-design-76',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="0 0 36 36"
|
||||||
|
className={cn(
|
||||||
|
'relative -rotate-90 drop-shadow-[0_0_calc(var(--design-unit)*6)_rgba(255,74,86,0.72)]',
|
||||||
|
compact ? 'h-design-22 w-design-22' : 'h-design-64 w-design-64',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="18"
|
||||||
|
cy="18"
|
||||||
|
r="15.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="rgba(255,255,255,0.2)"
|
||||||
|
strokeWidth="3"
|
||||||
|
/>
|
||||||
|
<motion.circle
|
||||||
|
cx="18"
|
||||||
|
cy="18"
|
||||||
|
r="15.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="#FF5968"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeWidth="3"
|
||||||
|
initial={{ pathLength: 0 }}
|
||||||
|
animate={{ pathLength: 1 }}
|
||||||
|
transition={{ duration: durationMs / 1000, ease: 'linear' }}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'absolute z-10 text-center font-bold uppercase leading-[1.05] text-[#FFE5E8] [text-shadow:0_0_calc(var(--design-unit)*4)_rgba(255,40,58,0.95)]',
|
||||||
|
compact
|
||||||
|
? 'max-w-design-18 text-[calc(var(--design-unit)*3)]'
|
||||||
|
: 'max-w-design-52 text-design-9',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
</motion.span>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -48,6 +48,7 @@ function createEmptyDashboardState(nowIso: string): DashboardState {
|
|||||||
return {
|
return {
|
||||||
countdownMs: 0,
|
countdownMs: 0,
|
||||||
featuredCellId: null,
|
featuredCellId: null,
|
||||||
|
jackpotPool: null,
|
||||||
onlinePlayers: 0,
|
onlinePlayers: 0,
|
||||||
tableLimitMax: 0,
|
tableLimitMax: 0,
|
||||||
tableLimitMin: 0,
|
tableLimitMin: 0,
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react'
|
import {
|
||||||
|
type PointerEvent as ReactPointerEvent,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { notify } from '@/lib/notify'
|
import { notify } from '@/lib/notify'
|
||||||
import { useAudioStore, useAuthStore, useModalStore } from '@/store'
|
import { useAudioStore, useAuthStore, useModalStore } from '@/store'
|
||||||
import {
|
import { useGameRoundStore, useGameSessionStore } from '@/store/game'
|
||||||
selectSelectionTotal,
|
|
||||||
useGameRoundStore,
|
|
||||||
useGameSessionStore,
|
|
||||||
} from '@/store/game'
|
|
||||||
import type { DesktopAnimalWarningType } from '@/type'
|
import type { DesktopAnimalWarningType } from '@/type'
|
||||||
|
|
||||||
|
const LONG_PRESS_DURATION_MS = 500
|
||||||
|
const LONG_PRESS_CLICK_SUPPRESSION_MS = 1_000
|
||||||
|
const LONG_PRESS_MOVE_TOLERANCE_PX = 8
|
||||||
|
|
||||||
function parseBalance(value: string | number | null | undefined) {
|
function parseBalance(value: string | number | null | undefined) {
|
||||||
if (typeof value === 'number') {
|
if (typeof value === 'number') {
|
||||||
return Number.isFinite(value) ? value : 0
|
return Number.isFinite(value) ? value : 0
|
||||||
@@ -57,9 +64,6 @@ export function useAnimalVm(
|
|||||||
)
|
)
|
||||||
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
||||||
const activeChipId = useGameRoundStore((state) => state.activeChipId)
|
const activeChipId = useGameRoundStore((state) => state.activeChipId)
|
||||||
const activeBetQuantity = useGameRoundStore(
|
|
||||||
(state) => state.activeBetQuantity,
|
|
||||||
)
|
|
||||||
const chips = useGameRoundStore((state) => state.chips)
|
const chips = useGameRoundStore((state) => state.chips)
|
||||||
const clearSelections = useGameRoundStore((state) => state.clearSelections)
|
const clearSelections = useGameRoundStore((state) => state.clearSelections)
|
||||||
const roundId = useGameRoundStore((state) => state.round.id)
|
const roundId = useGameRoundStore((state) => state.round.id)
|
||||||
@@ -72,7 +76,6 @@ export function useAnimalVm(
|
|||||||
(state) => state.removeSelectionsForCell,
|
(state) => state.removeSelectionsForCell,
|
||||||
)
|
)
|
||||||
const selections = useGameRoundStore((state) => state.selections)
|
const selections = useGameRoundStore((state) => state.selections)
|
||||||
const totalBetAmount = useGameRoundStore(selectSelectionTotal)
|
|
||||||
const connection = useGameSessionStore((state) => state.connection)
|
const connection = useGameSessionStore((state) => state.connection)
|
||||||
const tableLimitMax = useGameSessionStore(
|
const tableLimitMax = useGameSessionStore(
|
||||||
(state) => state.dashboard.tableLimitMax,
|
(state) => state.dashboard.tableLimitMax,
|
||||||
@@ -90,6 +93,17 @@ export function useAnimalVm(
|
|||||||
cellId: number
|
cellId: number
|
||||||
type: DesktopAnimalWarningType
|
type: DesktopAnimalWarningType
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
const [longPressCellId, setLongPressCellId] = useState<number | null>(null)
|
||||||
|
const longPressTimerRef = useRef<number | null>(null)
|
||||||
|
const longPressOriginRef = useRef<{
|
||||||
|
pointerId: number
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
} | null>(null)
|
||||||
|
const suppressedClickRef = useRef<{
|
||||||
|
cellId: number
|
||||||
|
expiresAt: number
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
const activeChip = useMemo(
|
const activeChip = useMemo(
|
||||||
() => chips.find((chip) => chip.id === activeChipId) ?? chips[0] ?? null,
|
() => chips.find((chip) => chip.id === activeChipId) ?? chips[0] ?? null,
|
||||||
@@ -125,6 +139,13 @@ export function useAnimalVm(
|
|||||||
showStandbyState || hasSubmittedCurrentRound || roundPhase !== 'betting'
|
showStandbyState || hasSubmittedCurrentRound || roundPhase !== 'betting'
|
||||||
const selectedCellCount = Object.keys(selectionByCell).length
|
const selectedCellCount = Object.keys(selectionByCell).length
|
||||||
|
|
||||||
|
const clearLongPressTimer = useCallback(() => {
|
||||||
|
if (longPressTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(longPressTimerRef.current)
|
||||||
|
longPressTimerRef.current = null
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cellWarning === null) {
|
if (cellWarning === null) {
|
||||||
return
|
return
|
||||||
@@ -166,6 +187,12 @@ export function useAnimalVm(
|
|||||||
}
|
}
|
||||||
}, [animalIds, shouldAnimateStandby])
|
}, [animalIds, shouldAnimateStandby])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
clearLongPressTimer()
|
||||||
|
}
|
||||||
|
}, [clearLongPressTimer])
|
||||||
|
|
||||||
const handleStart = () => {
|
const handleStart = () => {
|
||||||
if (authStatus !== 'authenticated') {
|
if (authStatus !== 'authenticated') {
|
||||||
notify.warning(t('commonUi.toast.loginRequired'))
|
notify.warning(t('commonUi.toast.loginRequired'))
|
||||||
@@ -179,6 +206,19 @@ export function useAnimalVm(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSelect = (animalId: number) => {
|
const handleSelect = (animalId: number) => {
|
||||||
|
const suppressedClick = suppressedClickRef.current
|
||||||
|
|
||||||
|
if (suppressedClick !== null) {
|
||||||
|
suppressedClickRef.current = null
|
||||||
|
|
||||||
|
if (
|
||||||
|
suppressedClick.cellId === animalId &&
|
||||||
|
Date.now() <= suppressedClick.expiresAt
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (roundPhase !== 'betting' || lockInteraction) {
|
if (roundPhase !== 'betting' || lockInteraction) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -188,12 +228,9 @@ export function useAnimalVm(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectionByCell[animalId]) {
|
const hasExistingSelection = Boolean(selectionByCell[animalId])
|
||||||
removeSelectionsForCell(animalId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedCellCount >= maxSelectionCount) {
|
if (!hasExistingSelection && selectedCellCount >= maxSelectionCount) {
|
||||||
setCellWarning({
|
setCellWarning({
|
||||||
cellId: animalId,
|
cellId: animalId,
|
||||||
type: 'limit',
|
type: 'limit',
|
||||||
@@ -201,9 +238,16 @@ export function useAnimalVm(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextBetAmount = (activeChip?.amount ?? 0) * activeBetQuantity
|
const currentSingleBetAmount = selections[0]?.amount ?? 0
|
||||||
|
const nextSingleBetAmount = hasExistingSelection
|
||||||
|
? currentSingleBetAmount + (activeChip?.amount ?? 0)
|
||||||
|
: (activeChip?.amount ?? 0)
|
||||||
|
const nextSelectedCellCount = hasExistingSelection
|
||||||
|
? selectedCellCount
|
||||||
|
: selectedCellCount + 1
|
||||||
|
const nextTotalBetAmount = nextSingleBetAmount * nextSelectedCellCount
|
||||||
|
|
||||||
if (tableLimitMax > 0 && totalBetAmount + nextBetAmount > tableLimitMax) {
|
if (tableLimitMax > 0 && nextSingleBetAmount > tableLimitMax) {
|
||||||
setCellWarning({
|
setCellWarning({
|
||||||
cellId: animalId,
|
cellId: animalId,
|
||||||
type: 'betLimit',
|
type: 'betLimit',
|
||||||
@@ -211,7 +255,7 @@ export function useAnimalVm(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalBetAmount + nextBetAmount > balance) {
|
if (nextTotalBetAmount > balance) {
|
||||||
setCellWarning({
|
setCellWarning({
|
||||||
cellId: animalId,
|
cellId: animalId,
|
||||||
type: 'balance',
|
type: 'balance',
|
||||||
@@ -222,12 +266,94 @@ export function useAnimalVm(
|
|||||||
placeBet(animalId)
|
placeBet(animalId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleClearCell = (animalId: number) => {
|
||||||
|
if (lockInteraction || !selectionByCell[animalId]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
removeSelectionsForCell(animalId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLongPressStart = (
|
||||||
|
animalId: number,
|
||||||
|
event: ReactPointerEvent<HTMLButtonElement>,
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
lockInteraction ||
|
||||||
|
!selectionByCell[animalId] ||
|
||||||
|
(event.pointerType === 'mouse' && event.button !== 0)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clearLongPressTimer()
|
||||||
|
suppressedClickRef.current = null
|
||||||
|
setLongPressCellId(animalId)
|
||||||
|
longPressOriginRef.current = {
|
||||||
|
pointerId: event.pointerId,
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY,
|
||||||
|
}
|
||||||
|
event.currentTarget.setPointerCapture(event.pointerId)
|
||||||
|
longPressTimerRef.current = window.setTimeout(() => {
|
||||||
|
suppressedClickRef.current = {
|
||||||
|
cellId: animalId,
|
||||||
|
expiresAt: Date.now() + LONG_PRESS_CLICK_SUPPRESSION_MS,
|
||||||
|
}
|
||||||
|
longPressTimerRef.current = null
|
||||||
|
longPressOriginRef.current = null
|
||||||
|
setLongPressCellId(null)
|
||||||
|
handleClearCell(animalId)
|
||||||
|
|
||||||
|
if (typeof navigator.vibrate === 'function') {
|
||||||
|
navigator.vibrate(30)
|
||||||
|
}
|
||||||
|
}, LONG_PRESS_DURATION_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLongPressMove = (event: ReactPointerEvent<HTMLButtonElement>) => {
|
||||||
|
const origin = longPressOriginRef.current
|
||||||
|
|
||||||
|
if (!origin || origin.pointerId !== event.pointerId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
Math.abs(event.clientX - origin.x) > LONG_PRESS_MOVE_TOLERANCE_PX ||
|
||||||
|
Math.abs(event.clientY - origin.y) > LONG_PRESS_MOVE_TOLERANCE_PX
|
||||||
|
) {
|
||||||
|
clearLongPressTimer()
|
||||||
|
longPressOriginRef.current = null
|
||||||
|
setLongPressCellId(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLongPressEnd = () => {
|
||||||
|
clearLongPressTimer()
|
||||||
|
longPressOriginRef.current = null
|
||||||
|
setLongPressCellId(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLongPressCancel = () => {
|
||||||
|
clearLongPressTimer()
|
||||||
|
longPressOriginRef.current = null
|
||||||
|
suppressedClickRef.current = null
|
||||||
|
setLongPressCellId(null)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cellWarning,
|
cellWarning,
|
||||||
|
handleClearCell,
|
||||||
|
handleLongPressCancel,
|
||||||
|
handleLongPressEnd,
|
||||||
|
handleLongPressMove,
|
||||||
|
handleLongPressStart,
|
||||||
handleSelect,
|
handleSelect,
|
||||||
handleStart,
|
handleStart,
|
||||||
isRealtimeConnecting,
|
isRealtimeConnecting,
|
||||||
lockInteraction,
|
lockInteraction,
|
||||||
|
longPressCellId,
|
||||||
|
longPressDurationMs: LONG_PRESS_DURATION_MS,
|
||||||
marqueeId: shouldAnimateStandby ? marqueeId : null,
|
marqueeId: shouldAnimateStandby ? marqueeId : null,
|
||||||
selectionByCell,
|
selectionByCell,
|
||||||
showStandbyState,
|
showStandbyState,
|
||||||
|
|||||||
@@ -56,32 +56,21 @@ function formatBetAmount(amount: number) {
|
|||||||
return amount.toFixed(2).replace(/\.?0+$/, '')
|
return amount.toFixed(2).replace(/\.?0+$/, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupSelections(selections: BetSelection[]) {
|
function resolveUniformBet(selections: BetSelection[]) {
|
||||||
return selections.reduce<
|
const latestSelection = selections.at(-1)
|
||||||
Map<string, { amount: number; betId: number; numbers: number[] }>
|
const betId = toBetId(latestSelection?.chipId ?? '')
|
||||||
>((accumulator, selection) => {
|
|
||||||
const betId = toBetId(selection.chipId)
|
|
||||||
|
|
||||||
if (betId === null) {
|
if (!latestSelection || betId === null || latestSelection.amount <= 0) {
|
||||||
return accumulator
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupKey = `${betId}:${selection.amount}`
|
return {
|
||||||
const current = accumulator.get(groupKey)
|
amount: latestSelection.amount,
|
||||||
|
betId,
|
||||||
if (current) {
|
numbers: [...new Set(selections.map((selection) => selection.cellId))].sort(
|
||||||
current.numbers.push(selection.cellId)
|
(left, right) => left - right,
|
||||||
return accumulator
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
accumulator.set(groupKey, {
|
|
||||||
amount: selection.amount,
|
|
||||||
betId,
|
|
||||||
numbers: [selection.cellId],
|
|
||||||
})
|
|
||||||
|
|
||||||
return accumulator
|
|
||||||
}, new Map())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAutoHostingRunner() {
|
export function useAutoHostingRunner() {
|
||||||
@@ -167,9 +156,9 @@ export function useAutoHostingRunner() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupedSelections = groupSelections(selections)
|
const uniformBet = resolveUniformBet(selections)
|
||||||
|
|
||||||
if (groupedSelections.size === 0) {
|
if (!uniformBet || uniformBet.numbers.length === 0) {
|
||||||
stopHosting()
|
stopHosting()
|
||||||
notify.warning(t('commonUi.toast.autoHostingStopped'))
|
notify.warning(t('commonUi.toast.autoHostingStopped'))
|
||||||
return
|
return
|
||||||
@@ -181,7 +170,7 @@ export function useAutoHostingRunner() {
|
|||||||
)
|
)
|
||||||
const balance = parseBalance(currentUser.coin)
|
const balance = parseBalance(currentUser.coin)
|
||||||
|
|
||||||
if (tableLimitMax > 0 && totalBetAmount > tableLimitMax) {
|
if (tableLimitMax > 0 && uniformBet.amount > tableLimitMax) {
|
||||||
stopHosting()
|
stopHosting()
|
||||||
notify.warning(t('commonUi.toast.autoHostingStoppedBetLimit'))
|
notify.warning(t('commonUi.toast.autoHostingStoppedBetLimit'))
|
||||||
return
|
return
|
||||||
@@ -199,38 +188,22 @@ export function useAutoHostingRunner() {
|
|||||||
const submitAutoBet = async () => {
|
const submitAutoBet = async () => {
|
||||||
try {
|
try {
|
||||||
let latestBalance = currentUser.coin ?? '0'
|
let latestBalance = currentUser.coin ?? '0'
|
||||||
let remainingBalance = parseBalance(latestBalance)
|
const formattedSingleBetAmount = formatBetAmount(uniformBet.amount)
|
||||||
|
const result = await placeGameBet({
|
||||||
|
bet_amount: formattedSingleBetAmount,
|
||||||
|
bet_id: uniformBet.betId,
|
||||||
|
idempotency_key: createIdempotencyKey(),
|
||||||
|
numbers: uniformBet.numbers.join(','),
|
||||||
|
period_no: round.id,
|
||||||
|
single_bet_amount: formattedSingleBetAmount,
|
||||||
|
})
|
||||||
|
|
||||||
for (const group of groupedSelections.values()) {
|
if (result.status !== 'accepted') {
|
||||||
const uniqueNumbers = [...new Set(group.numbers)].sort(
|
throw new Error(t('commonUi.toast.betRejected'))
|
||||||
(left, right) => left - right,
|
|
||||||
)
|
|
||||||
const groupCost = group.amount * uniqueNumbers.length
|
|
||||||
|
|
||||||
if (groupCost > remainingBalance) {
|
|
||||||
stopHosting()
|
|
||||||
notify.warning(t('commonUi.toast.autoHostingStoppedBalance'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const formattedSingleBetAmount = formatBetAmount(group.amount)
|
|
||||||
const result = await placeGameBet({
|
|
||||||
bet_amount: formattedSingleBetAmount,
|
|
||||||
bet_id: group.betId,
|
|
||||||
idempotency_key: createIdempotencyKey(),
|
|
||||||
numbers: uniqueNumbers.join(','),
|
|
||||||
period_no: round.id,
|
|
||||||
single_bet_amount: formattedSingleBetAmount,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.status !== 'accepted') {
|
|
||||||
throw new Error(t('commonUi.toast.betRejected'))
|
|
||||||
}
|
|
||||||
|
|
||||||
latestBalance = result.balance_after
|
|
||||||
remainingBalance = parseBalance(latestBalance)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
latestBalance = result.balance_after
|
||||||
|
|
||||||
const latestHostingState = useGameAutoHostingStore.getState()
|
const latestHostingState = useGameAutoHostingStore.getState()
|
||||||
const latestUser = useAuthStore.getState().currentUser
|
const latestUser = useAuthStore.getState().currentUser
|
||||||
|
|
||||||
|
|||||||
@@ -61,16 +61,10 @@ export function useGameControlVm() {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const chips = useGameRoundStore((state) => state.chips)
|
const chips = useGameRoundStore((state) => state.chips)
|
||||||
const activeChipId = useGameRoundStore((state) => state.activeChipId)
|
const activeChipId = useGameRoundStore((state) => state.activeChipId)
|
||||||
const activeBetQuantity = useGameRoundStore(
|
|
||||||
(state) => state.activeBetQuantity,
|
|
||||||
)
|
|
||||||
const round = useGameRoundStore((state) => state.round)
|
const round = useGameRoundStore((state) => state.round)
|
||||||
const maxSelectionCount = useGameRoundStore(
|
const maxSelectionCount = useGameRoundStore(
|
||||||
(state) => state.maxSelectionCount,
|
(state) => state.maxSelectionCount,
|
||||||
)
|
)
|
||||||
const adjustBetQuantity = useGameRoundStore(
|
|
||||||
(state) => state.adjustBetQuantity,
|
|
||||||
)
|
|
||||||
const selections = useGameRoundStore((state) => state.selections)
|
const selections = useGameRoundStore((state) => state.selections)
|
||||||
const clearSelections = useGameRoundStore((state) => state.clearSelections)
|
const clearSelections = useGameRoundStore((state) => state.clearSelections)
|
||||||
const restoreRecentSuccessfulSelections = useGameRoundStore(
|
const restoreRecentSuccessfulSelections = useGameRoundStore(
|
||||||
@@ -126,31 +120,12 @@ export function useGameControlVm() {
|
|||||||
Boolean(round.id) && currentUser?.lastBetPeriodNo === round.id
|
Boolean(round.id) && currentUser?.lastBetPeriodNo === round.id
|
||||||
const hasInsufficientBalance = hasSelections && totalBetAmount > balance
|
const hasInsufficientBalance = hasSelections && totalBetAmount > balance
|
||||||
const hasExceededBetLimit =
|
const hasExceededBetLimit =
|
||||||
hasSelections && tableLimitMax > 0 && totalBetAmount > tableLimitMax
|
hasSelections &&
|
||||||
const canIncreaseBetQuantity = useMemo(() => {
|
tableLimitMax > 0 &&
|
||||||
if (!hasSelections) {
|
selections.some((selection) => selection.amount > tableLimitMax)
|
||||||
return true
|
const selectedCellCount = new Set(
|
||||||
}
|
selections.map((selection) => selection.cellId),
|
||||||
|
).size
|
||||||
const activeChip = chips.find((chip) => chip.id === activeChipId)
|
|
||||||
|
|
||||||
if (!activeChip) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextQuantity = activeBetQuantity + 1
|
|
||||||
const nextTotalPerSelection = activeChip.amount * nextQuantity
|
|
||||||
const nextTotal = nextTotalPerSelection * selections.length
|
|
||||||
|
|
||||||
return tableLimitMax <= 0 || nextTotal <= tableLimitMax
|
|
||||||
}, [
|
|
||||||
hasSelections,
|
|
||||||
chips,
|
|
||||||
activeChipId,
|
|
||||||
activeBetQuantity,
|
|
||||||
selections.length,
|
|
||||||
tableLimitMax,
|
|
||||||
])
|
|
||||||
const confirmState: ConfirmState =
|
const confirmState: ConfirmState =
|
||||||
isSubmitting || isAutoHosting
|
isSubmitting || isAutoHosting
|
||||||
? 'submitting'
|
? 'submitting'
|
||||||
@@ -202,8 +177,9 @@ export function useGameControlVm() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const betId = toBetId(selections[0]?.chipId ?? activeChipId)
|
const latestSelection = selections.at(-1)
|
||||||
const singleBetAmount = selections[0]?.amount ?? selectedChip?.amount ?? 0
|
const betId = toBetId(latestSelection?.chipId ?? activeChipId)
|
||||||
|
const singleBetAmount = latestSelection?.amount ?? selectedChip?.amount ?? 0
|
||||||
|
|
||||||
if (betId === null || singleBetAmount <= 0) {
|
if (betId === null || singleBetAmount <= 0) {
|
||||||
notify.warning(t('commonUi.toast.betUnavailable'))
|
notify.warning(t('commonUi.toast.betUnavailable'))
|
||||||
@@ -316,8 +292,6 @@ export function useGameControlVm() {
|
|||||||
round.phase === 'betting' &&
|
round.phase === 'betting' &&
|
||||||
!hasSubmittedCurrentRound &&
|
!hasSubmittedCurrentRound &&
|
||||||
!isAutoHosting,
|
!isAutoHosting,
|
||||||
canDecreaseBetQuantity: activeBetQuantity > 1,
|
|
||||||
canIncreaseBetQuantity,
|
|
||||||
confirmLabel:
|
confirmLabel:
|
||||||
confirmState === 'idle'
|
confirmState === 'idle'
|
||||||
? t('gameDesktop.control.selectNumbers')
|
? t('gameDesktop.control.selectNumbers')
|
||||||
@@ -331,23 +305,13 @@ export function useGameControlVm() {
|
|||||||
confirmState,
|
confirmState,
|
||||||
isConfirmClickable: confirmState === 'ready' && !isAutoHosting,
|
isConfirmClickable: confirmState === 'ready' && !isAutoHosting,
|
||||||
onChipSelect: selectChip,
|
onChipSelect: selectChip,
|
||||||
onDecreaseBetQuantity: () => adjustBetQuantity(-1),
|
|
||||||
onIncreaseBetQuantity: () => {
|
|
||||||
if (!canIncreaseBetQuantity) {
|
|
||||||
notify.warning(t('commonUi.toast.betLimitExceeded'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
adjustBetQuantity(1)
|
|
||||||
},
|
|
||||||
onConfirm: handleConfirm,
|
onConfirm: handleConfirm,
|
||||||
onClearSelections: clearSelections,
|
onClearSelections: clearSelections,
|
||||||
onOpenAutoSetting: handleOpenAutoSetting,
|
onOpenAutoSetting: handleOpenAutoSetting,
|
||||||
onRepeatSelections: handleRepeatSelections,
|
onRepeatSelections: handleRepeatSelections,
|
||||||
maxSelectionCountLabel: maxSelectionCount,
|
maxSelectionCountLabel: maxSelectionCount,
|
||||||
selectedBetQuantityLabel: activeBetQuantity,
|
|
||||||
selectedChipId: activeChipId,
|
selectedChipId: activeChipId,
|
||||||
selectedCountLabel: selections.length,
|
selectedCountLabel: selectedCellCount,
|
||||||
totalBetAmountLabel: formatChipDisplayValue(totalBetAmount),
|
totalBetAmountLabel: formatChipDisplayValue(totalBetAmount),
|
||||||
chips: chipItems,
|
chips: chipItems,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ function applyLobbySync(result: Awaited<ReturnType<typeof getGameLobbyInit>>) {
|
|||||||
},
|
},
|
||||||
dashboard: {
|
dashboard: {
|
||||||
...currentSessionState.dashboard,
|
...currentSessionState.dashboard,
|
||||||
|
jackpotPool: result.snapshot.dashboard.jackpotPool,
|
||||||
tableLimitMax: result.snapshot.dashboard.tableLimitMax,
|
tableLimitMax: result.snapshot.dashboard.tableLimitMax,
|
||||||
tableLimitMin: result.snapshot.dashboard.tableLimitMin,
|
tableLimitMin: result.snapshot.dashboard.tableLimitMin,
|
||||||
},
|
},
|
||||||
@@ -220,6 +221,55 @@ function applyPeriodPayoutMessage(
|
|||||||
useGameRoundStore.getState().playPreparedRevealAnimation(roundId || null)
|
useGameRoundStore.getState().playPreparedRevealAnimation(roundId || null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let jackpotPoolRefreshTimerId: number | null = null
|
||||||
|
|
||||||
|
const JACKPOT_STREAK_TRIGGER_COUNT = 10
|
||||||
|
const JACKPOT_POOL_REFRESH_DELAY_MS = 2_000
|
||||||
|
|
||||||
|
async function refreshJackpotPoolFromLobbyInit() {
|
||||||
|
const authState = useAuthStore.getState()
|
||||||
|
|
||||||
|
if (authState.status !== 'authenticated' || !authState.accessToken) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await getGameLobbyInit()
|
||||||
|
|
||||||
|
useGameSessionStore.getState().syncDashboard({
|
||||||
|
jackpotPool: result.snapshot.dashboard.jackpotPool,
|
||||||
|
updatedAt: result.snapshot.dashboard.updatedAt,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
'Failed to refresh jackpot pool after streak milestone',
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleJackpotPoolRefreshForStreak(
|
||||||
|
previousStreak: number,
|
||||||
|
nextStreak: number | undefined,
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
nextStreak !== JACKPOT_STREAK_TRIGGER_COUNT ||
|
||||||
|
previousStreak >= JACKPOT_STREAK_TRIGGER_COUNT ||
|
||||||
|
typeof window === 'undefined'
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jackpotPoolRefreshTimerId !== null) {
|
||||||
|
window.clearTimeout(jackpotPoolRefreshTimerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
jackpotPoolRefreshTimerId = window.setTimeout(() => {
|
||||||
|
jackpotPoolRefreshTimerId = null
|
||||||
|
void refreshJackpotPoolFromLobbyInit()
|
||||||
|
}, JACKPOT_POOL_REFRESH_DELAY_MS)
|
||||||
|
}
|
||||||
|
|
||||||
function applyUserStreakMessage(message: GameSocketMessage) {
|
function applyUserStreakMessage(message: GameSocketMessage) {
|
||||||
const streakData = extractUserStreakMessageData(message)
|
const streakData = extractUserStreakMessageData(message)
|
||||||
const currentUser = useAuthStore.getState().currentUser
|
const currentUser = useAuthStore.getState().currentUser
|
||||||
@@ -228,6 +278,11 @@ function applyUserStreakMessage(message: GameSocketMessage) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scheduleJackpotPoolRefreshForStreak(
|
||||||
|
currentUser.currentStreak ?? 0,
|
||||||
|
streakData.currentStreak,
|
||||||
|
)
|
||||||
|
|
||||||
useAuthStore.getState().setCurrentUser({
|
useAuthStore.getState().setCurrentUser({
|
||||||
...currentUser,
|
...currentUser,
|
||||||
currentStreak: streakData.currentStreak,
|
currentStreak: streakData.currentStreak,
|
||||||
@@ -298,6 +353,11 @@ function applyBetWinMessage(message: GameSocketMessage) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scheduleJackpotPoolRefreshForStreak(
|
||||||
|
currentUser.currentStreak ?? 0,
|
||||||
|
betWinData.current_streak,
|
||||||
|
)
|
||||||
|
|
||||||
useAuthStore.getState().setCurrentUser({
|
useAuthStore.getState().setCurrentUser({
|
||||||
...currentUser,
|
...currentUser,
|
||||||
coin: betWinData.balance_after ?? currentUser.coin,
|
coin: betWinData.balance_after ?? currentUser.coin,
|
||||||
|
|||||||
45
src/hooks/use-wallet-balance-refresh.ts
Normal file
45
src/hooks/use-wallet-balance-refresh.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { useCallback, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { getCurrentUserProfile } from '@/api'
|
||||||
|
import { notify } from '@/lib/notify'
|
||||||
|
import { useAuthStore } from '@/store'
|
||||||
|
|
||||||
|
export function useWalletBalanceRefresh() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const setCurrentUser = useAuthStore((state) => state.setCurrentUser)
|
||||||
|
const [isRefreshingBalance, setIsRefreshingBalance] = useState(false)
|
||||||
|
const refreshPromiseRef = useRef<Promise<void> | null>(null)
|
||||||
|
|
||||||
|
const refreshWalletBalance = useCallback(() => {
|
||||||
|
if (refreshPromiseRef.current) {
|
||||||
|
return refreshPromiseRef.current
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRefreshingBalance(true)
|
||||||
|
|
||||||
|
const refreshPromise = getCurrentUserProfile()
|
||||||
|
.then((currentUser) => {
|
||||||
|
setCurrentUser(currentUser)
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
notify.error(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: t('commonUi.toast.requestFailed'),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
refreshPromiseRef.current = null
|
||||||
|
setIsRefreshingBalance(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
refreshPromiseRef.current = refreshPromise
|
||||||
|
|
||||||
|
return refreshPromise
|
||||||
|
}, [setCurrentUser, t])
|
||||||
|
|
||||||
|
return {
|
||||||
|
isRefreshingBalance,
|
||||||
|
refreshWalletBalance,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +1,150 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
DEFAULT_WITHDRAW_CONFIG,
|
DEFAULT_WITHDRAW_CONFIG,
|
||||||
QUICK_FIAT_AMOUNTS,
|
|
||||||
WITHDRAW_EMAIL_PATTERN,
|
WITHDRAW_EMAIL_PATTERN,
|
||||||
WITHDRAW_PHONE_PATTERN,
|
WITHDRAW_PHONE_PATTERN,
|
||||||
} from '@/constants'
|
} from '@/constants'
|
||||||
import { useDepositWithdrawConfig } from '@/hooks/use-deposit-withdraw-config'
|
import { useDepositWithdrawConfig } from '@/hooks/use-deposit-withdraw-config'
|
||||||
import { useAuthStore } from '@/store'
|
import { useAuthStore } from '@/store'
|
||||||
import type { DepositWithdrawConfig } from '@/type'
|
import type { AuthWithdrawAccount, DepositWithdrawConfig } from '@/type'
|
||||||
|
|
||||||
|
const QUICK_WITHDRAW_OPTION_COUNT = 6
|
||||||
|
|
||||||
|
interface WithdrawDiamondBounds {
|
||||||
|
maximumDiamonds: number
|
||||||
|
minimumDiamonds: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WithdrawAccountDefaults {
|
||||||
|
bankAccount: string
|
||||||
|
holderName: string
|
||||||
|
receiverEmail: string
|
||||||
|
receiverPhone: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWithdrawAccountDefaults(
|
||||||
|
withdraw: AuthWithdrawAccount | null | undefined,
|
||||||
|
): WithdrawAccountDefaults {
|
||||||
|
return {
|
||||||
|
bankAccount: withdraw?.receiveAccount ?? '',
|
||||||
|
holderName: withdraw?.receiverName ?? '',
|
||||||
|
receiverEmail: withdraw?.receiverEmail ?? '',
|
||||||
|
receiverPhone: withdraw?.receiverMobile ?? '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formatNumber(locale: string, value: number) {
|
function formatNumber(locale: string, value: number) {
|
||||||
return new Intl.NumberFormat(locale).format(value)
|
return new Intl.NumberFormat(locale).format(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInitialWithdrawAmount(
|
function formatConfiguredAmount(locale: string, value: string) {
|
||||||
selectedExchangeRate: number,
|
const normalizedValue = value.trim()
|
||||||
maxWithdrawAmount: number,
|
const numericValue = Number(normalizedValue)
|
||||||
) {
|
|
||||||
if (maxWithdrawAmount <= 0) {
|
if (!Number.isFinite(numericValue)) {
|
||||||
return 0
|
return normalizedValue || '0'
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.min(
|
const fractionDigits = Math.min(
|
||||||
maxWithdrawAmount,
|
6,
|
||||||
Math.max(1, Math.round(QUICK_FIAT_AMOUNTS[0] / selectedExchangeRate)),
|
normalizedValue.includes('.')
|
||||||
|
? (normalizedValue.split('.')[1]?.length ?? 0)
|
||||||
|
: 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return new Intl.NumberFormat(locale, {
|
||||||
|
maximumFractionDigits: fractionDigits,
|
||||||
|
minimumFractionDigits: fractionDigits,
|
||||||
|
}).format(numericValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateNetWithdrawalAmount(
|
||||||
|
grossAmount: number,
|
||||||
|
handlingFee: string,
|
||||||
|
) {
|
||||||
|
const parsedHandlingFee = Number(handlingFee)
|
||||||
|
|
||||||
|
if (!Number.isFinite(parsedHandlingFee)) {
|
||||||
|
return grossAmount
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlingFeeRate = Math.min(100, Math.max(0, parsedHandlingFee)) / 100
|
||||||
|
|
||||||
|
return grossAmount * (1 - handlingFeeRate)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitialWithdrawAmount(bounds: WithdrawDiamondBounds | null) {
|
||||||
|
return bounds?.minimumDiamonds ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWithdrawDiamondBounds(
|
||||||
|
selectedExchangeRate: number,
|
||||||
|
maxWithdrawAmount: number,
|
||||||
|
minimumFiatAmount: number | null,
|
||||||
|
maximumFiatAmount: number | null,
|
||||||
|
): WithdrawDiamondBounds | null {
|
||||||
|
if (selectedExchangeRate <= 0 || maxWithdrawAmount <= 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const minimumDiamonds = Math.max(
|
||||||
|
1,
|
||||||
|
minimumFiatAmount === null
|
||||||
|
? 1
|
||||||
|
: Math.ceil(minimumFiatAmount / selectedExchangeRate),
|
||||||
|
)
|
||||||
|
const methodMaximumDiamonds =
|
||||||
|
maximumFiatAmount === null
|
||||||
|
? maxWithdrawAmount
|
||||||
|
: Math.floor(maximumFiatAmount / selectedExchangeRate)
|
||||||
|
const maximumDiamonds = Math.min(maxWithdrawAmount, methodMaximumDiamonds)
|
||||||
|
|
||||||
|
if (maximumDiamonds < minimumDiamonds) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
maximumDiamonds,
|
||||||
|
minimumDiamonds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getQuickWithdrawAmounts(bounds: WithdrawDiamondBounds) {
|
||||||
|
const { maximumDiamonds, minimumDiamonds } = bounds
|
||||||
|
const roundingUnit =
|
||||||
|
10 ** Math.max(0, Math.floor(Math.log10(maximumDiamonds)) - 1)
|
||||||
|
const roundedMinimumDiamonds =
|
||||||
|
Math.ceil(minimumDiamonds / roundingUnit) * roundingUnit
|
||||||
|
const roundedMaximumDiamonds =
|
||||||
|
Math.floor(maximumDiamonds / roundingUnit) * roundingUnit
|
||||||
|
|
||||||
|
if (roundedMinimumDiamonds > roundedMaximumDiamonds) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableOptionCount =
|
||||||
|
(roundedMaximumDiamonds - roundedMinimumDiamonds) / roundingUnit + 1
|
||||||
|
|
||||||
|
if (availableOptionCount <= QUICK_WITHDRAW_OPTION_COUNT) {
|
||||||
|
return Array.from(
|
||||||
|
{ length: availableOptionCount },
|
||||||
|
(_, index) => roundedMinimumDiamonds + index * roundingUnit,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastOptionIndex = availableOptionCount - 1
|
||||||
|
const optionIndexes = new Set<number>([0, lastOptionIndex])
|
||||||
|
|
||||||
|
for (let index = 1; index < QUICK_WITHDRAW_OPTION_COUNT - 1; index += 1) {
|
||||||
|
optionIndexes.add(
|
||||||
|
Math.round((lastOptionIndex * index) / (QUICK_WITHDRAW_OPTION_COUNT - 1)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...optionIndexes]
|
||||||
|
.sort((left, right) => left - right)
|
||||||
|
.map((index) => roundedMinimumDiamonds + index * roundingUnit)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrencyExchangeRate(
|
function getCurrencyExchangeRate(
|
||||||
@@ -99,6 +218,11 @@ export function useWithdrawVm() {
|
|||||||
const { i18n, t } = useTranslation()
|
const { i18n, t } = useTranslation()
|
||||||
const currentUser = useAuthStore((state) => state.currentUser)
|
const currentUser = useAuthStore((state) => state.currentUser)
|
||||||
const withdrawConfigQuery = useDepositWithdrawConfig()
|
const withdrawConfigQuery = useDepositWithdrawConfig()
|
||||||
|
const withdrawAccountDefaults = useMemo(
|
||||||
|
() => getWithdrawAccountDefaults(currentUser?.withdraw),
|
||||||
|
[currentUser?.withdraw],
|
||||||
|
)
|
||||||
|
const hasEditedWithdrawAccountRef = useRef(false)
|
||||||
const config = useMemo(() => {
|
const config = useMemo(() => {
|
||||||
const baseConfig = getNormalizedConfig(
|
const baseConfig = getNormalizedConfig(
|
||||||
withdrawConfigQuery.data,
|
withdrawConfigQuery.data,
|
||||||
@@ -127,10 +251,18 @@ export function useWithdrawVm() {
|
|||||||
const [paymentChannelCode, setPaymentChannelCode] = useState('')
|
const [paymentChannelCode, setPaymentChannelCode] = useState('')
|
||||||
const [paymentType, setPaymentType] = useState('')
|
const [paymentType, setPaymentType] = useState('')
|
||||||
const [bankCode, setBankCode] = useState('')
|
const [bankCode, setBankCode] = useState('')
|
||||||
const [holderName, setHolderName] = useState('')
|
const [holderName, setHolderNameState] = useState(
|
||||||
const [bankAccount, setBankAccount] = useState('')
|
() => withdrawAccountDefaults.holderName,
|
||||||
const [receiverEmail, setReceiverEmail] = useState('')
|
)
|
||||||
const [receiverPhone, setReceiverPhone] = useState('')
|
const [bankAccount, setBankAccountState] = useState(
|
||||||
|
() => withdrawAccountDefaults.bankAccount,
|
||||||
|
)
|
||||||
|
const [receiverEmail, setReceiverEmailState] = useState(
|
||||||
|
() => withdrawAccountDefaults.receiverEmail,
|
||||||
|
)
|
||||||
|
const [receiverPhone, setReceiverPhoneState] = useState(
|
||||||
|
() => withdrawAccountDefaults.receiverPhone,
|
||||||
|
)
|
||||||
|
|
||||||
const sortedPayChannels = useMemo(
|
const sortedPayChannels = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -144,6 +276,7 @@ export function useWithdrawVm() {
|
|||||||
const selectedPaymentChannel =
|
const selectedPaymentChannel =
|
||||||
sortedPayChannels.find((channel) => channel.code === paymentChannelCode) ??
|
sortedPayChannels.find((channel) => channel.code === paymentChannelCode) ??
|
||||||
null
|
null
|
||||||
|
const selectedHandlingFee = selectedPaymentChannel?.handlingFee ?? '0.00'
|
||||||
const availablePaymentMethods = useMemo(
|
const availablePaymentMethods = useMemo(
|
||||||
() => selectedPaymentChannel?.methods ?? [],
|
() => selectedPaymentChannel?.methods ?? [],
|
||||||
[selectedPaymentChannel],
|
[selectedPaymentChannel],
|
||||||
@@ -158,6 +291,9 @@ export function useWithdrawVm() {
|
|||||||
const selectedCurrency =
|
const selectedCurrency =
|
||||||
availableCurrencies.find((item) => item.code === currencyCode) ?? null
|
availableCurrencies.find((item) => item.code === currencyCode) ?? null
|
||||||
const selectedCurrencyCode = selectedCurrency?.code ?? ''
|
const selectedCurrencyCode = selectedCurrency?.code ?? ''
|
||||||
|
const selectedHandlingFeeValue = selectedPaymentChannel
|
||||||
|
? `${formatConfiguredAmount(locale, selectedHandlingFee)}%`
|
||||||
|
: '--'
|
||||||
const selectedRate = selectedCurrency?.withdrawCoinsPerFiatValue ?? 0
|
const selectedRate = selectedCurrency?.withdrawCoinsPerFiatValue ?? 0
|
||||||
const selectedExchangeRate = selectedCurrency
|
const selectedExchangeRate = selectedCurrency
|
||||||
? getCurrencyExchangeRate(config, selectedCurrency.code, selectedRate || 1)
|
? getCurrencyExchangeRate(config, selectedCurrency.code, selectedRate || 1)
|
||||||
@@ -170,6 +306,21 @@ export function useWithdrawVm() {
|
|||||||
const selectedPaymentMaximumAmount = selectedPaymentMethod
|
const selectedPaymentMaximumAmount = selectedPaymentMethod
|
||||||
? toOptionalNumber(selectedPaymentMethod.maximumAmount)
|
? toOptionalNumber(selectedPaymentMethod.maximumAmount)
|
||||||
: null
|
: null
|
||||||
|
const withdrawDiamondBounds = useMemo(
|
||||||
|
() =>
|
||||||
|
getWithdrawDiamondBounds(
|
||||||
|
selectedExchangeRate,
|
||||||
|
maxWithdrawAmount,
|
||||||
|
selectedPaymentMinimumAmount,
|
||||||
|
selectedPaymentMaximumAmount,
|
||||||
|
),
|
||||||
|
[
|
||||||
|
maxWithdrawAmount,
|
||||||
|
selectedExchangeRate,
|
||||||
|
selectedPaymentMaximumAmount,
|
||||||
|
selectedPaymentMinimumAmount,
|
||||||
|
],
|
||||||
|
)
|
||||||
const amountBelowPaymentMinimum =
|
const amountBelowPaymentMinimum =
|
||||||
amount > 0 &&
|
amount > 0 &&
|
||||||
selectedPaymentMinimumAmount !== null &&
|
selectedPaymentMinimumAmount !== null &&
|
||||||
@@ -198,6 +349,28 @@ export function useWithdrawVm() {
|
|||||||
},
|
},
|
||||||
[maxWithdrawAmount],
|
[maxWithdrawAmount],
|
||||||
)
|
)
|
||||||
|
const applyWithdrawAccountDefaults = useCallback(() => {
|
||||||
|
setHolderNameState(withdrawAccountDefaults.holderName)
|
||||||
|
setBankAccountState(withdrawAccountDefaults.bankAccount)
|
||||||
|
setReceiverEmailState(withdrawAccountDefaults.receiverEmail)
|
||||||
|
setReceiverPhoneState(withdrawAccountDefaults.receiverPhone)
|
||||||
|
}, [withdrawAccountDefaults])
|
||||||
|
const setHolderName = useCallback((nextHolderName: string) => {
|
||||||
|
hasEditedWithdrawAccountRef.current = true
|
||||||
|
setHolderNameState(nextHolderName)
|
||||||
|
}, [])
|
||||||
|
const setBankAccount = useCallback((nextBankAccount: string) => {
|
||||||
|
hasEditedWithdrawAccountRef.current = true
|
||||||
|
setBankAccountState(nextBankAccount)
|
||||||
|
}, [])
|
||||||
|
const setReceiverEmail = useCallback((nextReceiverEmail: string) => {
|
||||||
|
hasEditedWithdrawAccountRef.current = true
|
||||||
|
setReceiverEmailState(nextReceiverEmail)
|
||||||
|
}, [])
|
||||||
|
const setReceiverPhone = useCallback((nextReceiverPhone: string) => {
|
||||||
|
hasEditedWithdrawAccountRef.current = true
|
||||||
|
setReceiverPhoneState(nextReceiverPhone)
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const firstAvailablePayChannel = sortedPayChannels[0]
|
const firstAvailablePayChannel = sortedPayChannels[0]
|
||||||
@@ -274,13 +447,11 @@ export function useWithdrawVm() {
|
|||||||
}, [bankCode, sortedBanks])
|
}, [bankCode, sortedBanks])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasInitializedAmount && selectedExchangeRate > 0) {
|
if (!hasInitializedAmount && withdrawDiamondBounds) {
|
||||||
setAmount(
|
setAmount(getInitialWithdrawAmount(withdrawDiamondBounds))
|
||||||
getInitialWithdrawAmount(selectedExchangeRate, maxWithdrawAmount),
|
|
||||||
)
|
|
||||||
setHasInitializedAmount(true)
|
setHasInitializedAmount(true)
|
||||||
}
|
}
|
||||||
}, [hasInitializedAmount, maxWithdrawAmount, selectedExchangeRate, setAmount])
|
}, [hasInitializedAmount, setAmount, withdrawDiamondBounds])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (amount > maxWithdrawAmount) {
|
if (amount > maxWithdrawAmount) {
|
||||||
@@ -288,20 +459,47 @@ export function useWithdrawVm() {
|
|||||||
}
|
}
|
||||||
}, [amount, maxWithdrawAmount, setAmount])
|
}, [amount, maxWithdrawAmount, setAmount])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasEditedWithdrawAccountRef.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
applyWithdrawAccountDefaults()
|
||||||
|
}, [applyWithdrawAccountDefaults])
|
||||||
|
|
||||||
const quickAmounts = useMemo(() => {
|
const quickAmounts = useMemo(() => {
|
||||||
if (!selectedCurrency || selectedExchangeRate <= 0) {
|
if (!selectedCurrency || !withdrawDiamondBounds) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
return QUICK_FIAT_AMOUNTS.map((fiatAmount) => ({
|
return getQuickWithdrawAmounts(withdrawDiamondBounds).map((diamonds) => ({
|
||||||
diamonds: Math.min(
|
amountLabel: `${selectedCurrency.code} ${formatNumber(
|
||||||
maxWithdrawAmount,
|
locale,
|
||||||
Math.max(1, Math.round(fiatAmount / selectedExchangeRate)),
|
calculateNetWithdrawalAmount(
|
||||||
),
|
diamonds * selectedExchangeRate,
|
||||||
id: `quick-${selectedCurrency.code}-${fiatAmount}`,
|
selectedHandlingFee,
|
||||||
preview: `${selectedCurrency.code} ${formatNumber(locale, fiatAmount)}`,
|
),
|
||||||
|
)}`,
|
||||||
|
diamondLabel: t('gameDesktop.withdraw.preview.quickDiamondAmount', {
|
||||||
|
amount: formatNumber(locale, diamonds),
|
||||||
|
}),
|
||||||
|
diamonds,
|
||||||
|
feeLabel: t('gameDesktop.withdraw.preview.quickHandlingFee', {
|
||||||
|
fee: selectedHandlingFeeValue,
|
||||||
|
}),
|
||||||
|
id: `quick-${paymentChannelCode}-${paymentType}-${selectedCurrency.code}-${diamonds}`,
|
||||||
}))
|
}))
|
||||||
}, [locale, maxWithdrawAmount, selectedCurrency, selectedExchangeRate])
|
}, [
|
||||||
|
locale,
|
||||||
|
paymentChannelCode,
|
||||||
|
paymentType,
|
||||||
|
selectedCurrency,
|
||||||
|
selectedExchangeRate,
|
||||||
|
selectedHandlingFee,
|
||||||
|
selectedHandlingFeeValue,
|
||||||
|
t,
|
||||||
|
withdrawDiamondBounds,
|
||||||
|
])
|
||||||
|
|
||||||
const resetForm = useCallback(() => {
|
const resetForm = useCallback(() => {
|
||||||
const nextPaymentChannelCode = sortedPayChannels[0]?.code ?? ''
|
const nextPaymentChannelCode = sortedPayChannels[0]?.code ?? ''
|
||||||
@@ -311,6 +509,7 @@ export function useWithdrawVm() {
|
|||||||
) ?? null
|
) ?? null
|
||||||
const nextCurrency =
|
const nextCurrency =
|
||||||
getChannelCurrencies(config, nextPaymentChannel)[0] ?? null
|
getChannelCurrencies(config, nextPaymentChannel)[0] ?? null
|
||||||
|
const nextPaymentMethod = nextPaymentChannel?.methods[0] ?? null
|
||||||
const nextExchangeRate = nextCurrency
|
const nextExchangeRate = nextCurrency
|
||||||
? getCurrencyExchangeRate(
|
? getCurrencyExchangeRate(
|
||||||
config,
|
config,
|
||||||
@@ -318,22 +517,32 @@ export function useWithdrawVm() {
|
|||||||
nextCurrency.withdrawCoinsPerFiatValue || 1,
|
nextCurrency.withdrawCoinsPerFiatValue || 1,
|
||||||
)
|
)
|
||||||
: 0
|
: 0
|
||||||
|
const nextWithdrawDiamondBounds = getWithdrawDiamondBounds(
|
||||||
setAmountState(
|
nextExchangeRate,
|
||||||
nextExchangeRate > 0
|
maxWithdrawAmount,
|
||||||
? getInitialWithdrawAmount(nextExchangeRate, maxWithdrawAmount)
|
nextPaymentMethod
|
||||||
: 0,
|
? toOptionalNumber(nextPaymentMethod.minimumAmount)
|
||||||
|
: null,
|
||||||
|
nextPaymentMethod
|
||||||
|
? toOptionalNumber(nextPaymentMethod.maximumAmount)
|
||||||
|
: null,
|
||||||
)
|
)
|
||||||
setHasInitializedAmount(true)
|
const nextAmount = getInitialWithdrawAmount(nextWithdrawDiamondBounds)
|
||||||
|
|
||||||
|
setAmountState(nextAmount)
|
||||||
|
setHasInitializedAmount(nextAmount > 0)
|
||||||
setCurrencyCode(nextCurrency?.code ?? '')
|
setCurrencyCode(nextCurrency?.code ?? '')
|
||||||
setPaymentChannelCode(nextPaymentChannelCode)
|
setPaymentChannelCode(nextPaymentChannelCode)
|
||||||
setPaymentType(nextPaymentChannel?.methods[0]?.code ?? '')
|
setPaymentType(nextPaymentMethod?.code ?? '')
|
||||||
setBankCode('')
|
setBankCode('')
|
||||||
setHolderName('')
|
hasEditedWithdrawAccountRef.current = false
|
||||||
setBankAccount('')
|
applyWithdrawAccountDefaults()
|
||||||
setReceiverEmail('')
|
}, [
|
||||||
setReceiverPhone('')
|
applyWithdrawAccountDefaults,
|
||||||
}, [config, maxWithdrawAmount, sortedPayChannels])
|
config,
|
||||||
|
maxWithdrawAmount,
|
||||||
|
sortedPayChannels,
|
||||||
|
])
|
||||||
|
|
||||||
const selectedCurrencyPreview = useMemo(() => {
|
const selectedCurrencyPreview = useMemo(() => {
|
||||||
const previewCurrencyCode = selectedCurrency?.code ?? '--'
|
const previewCurrencyCode = selectedCurrency?.code ?? '--'
|
||||||
@@ -355,11 +564,24 @@ export function useWithdrawVm() {
|
|||||||
selectedCurrency && selectedExchangeRate > 0
|
selectedCurrency && selectedExchangeRate > 0
|
||||||
? `${formatNumber(
|
? `${formatNumber(
|
||||||
locale,
|
locale,
|
||||||
amount * selectedExchangeRate,
|
calculateNetWithdrawalAmount(
|
||||||
|
amount * selectedExchangeRate,
|
||||||
|
selectedHandlingFee,
|
||||||
|
),
|
||||||
)} ${selectedCurrency.code}`
|
)} ${selectedCurrency.code}`
|
||||||
: '--',
|
: '--',
|
||||||
|
handlingFeeLabel: t('gameDesktop.withdraw.preview.handlingFee'),
|
||||||
|
handlingFeeValue: selectedHandlingFeeValue,
|
||||||
}
|
}
|
||||||
}, [amount, locale, selectedCurrency, selectedExchangeRate, t])
|
}, [
|
||||||
|
amount,
|
||||||
|
locale,
|
||||||
|
selectedCurrency,
|
||||||
|
selectedExchangeRate,
|
||||||
|
selectedHandlingFee,
|
||||||
|
selectedHandlingFeeValue,
|
||||||
|
t,
|
||||||
|
])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
amount,
|
amount,
|
||||||
|
|||||||
@@ -24,9 +24,11 @@ export function normalizeAuthUserProfile(dto: AuthUserProfileDto): AuthUser {
|
|||||||
return {
|
return {
|
||||||
channelId: dto.channel_id,
|
channelId: dto.channel_id,
|
||||||
coin: dto.coin,
|
coin: dto.coin,
|
||||||
|
coinBalance: dto.coin_balance,
|
||||||
createTime: dto.create_time,
|
createTime: dto.create_time,
|
||||||
currentStreak: dto.current_streak,
|
currentStreak: dto.current_streak,
|
||||||
email: dto.email,
|
email: dto.email,
|
||||||
|
frozenBalance: dto.frozen_balance,
|
||||||
headImage: dto.head_image,
|
headImage: dto.head_image,
|
||||||
id: dto.uuid,
|
id: dto.uuid,
|
||||||
lastBetPeriodNo: dto.last_bet_period_no,
|
lastBetPeriodNo: dto.last_bet_period_no,
|
||||||
@@ -34,8 +36,18 @@ export function normalizeAuthUserProfile(dto: AuthUserProfileDto): AuthUser {
|
|||||||
phone: dto.phone,
|
phone: dto.phone,
|
||||||
registerInviteCode: dto.register_invite_code,
|
registerInviteCode: dto.register_invite_code,
|
||||||
riskFlags: dto.risk_flags,
|
riskFlags: dto.risk_flags,
|
||||||
|
totalDepositCoin: dto.total_deposit_coin,
|
||||||
|
totalWithdrawCoin: dto.total_withdraw_coin,
|
||||||
username: dto.username,
|
username: dto.username,
|
||||||
uuid: dto.uuid,
|
uuid: dto.uuid,
|
||||||
|
withdraw: dto.withdraw
|
||||||
|
? {
|
||||||
|
receiveAccount: dto.withdraw.receive_account,
|
||||||
|
receiverEmail: dto.withdraw.receiver_email,
|
||||||
|
receiverMobile: dto.withdraw.receiver_mobile,
|
||||||
|
receiverName: dto.withdraw.receiver_name,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -178,6 +178,13 @@ export default {
|
|||||||
streak: '{{count}}-win streak',
|
streak: '{{count}}-win streak',
|
||||||
winAction: 'won',
|
winAction: 'won',
|
||||||
},
|
},
|
||||||
|
jackpotPool: {
|
||||||
|
ariaLabel: 'Current jackpot pool {{amount}}',
|
||||||
|
iconAlt: 'Jackpot pool',
|
||||||
|
label: 'Jackpot',
|
||||||
|
promoLine1: 'Streak Win 10 Rounds',
|
||||||
|
promoLine2: 'And Hit The Super',
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
unifiedBetHint: 'Unified bet',
|
unifiedBetHint: 'Unified bet',
|
||||||
totalBet: 'Total bet',
|
totalBet: 'Total bet',
|
||||||
@@ -230,6 +237,7 @@ export default {
|
|||||||
'1. **Betting Stage**',
|
'1. **Betting Stage**',
|
||||||
' Players choose the numbers they want to bet on from 36 numbers, select chips, confirm the bet, and the system deducts the balance and creates the bet order.',
|
' Players choose the numbers they want to bet on from 36 numbers, select chips, confirm the bet, and the system deducts the balance and creates the bet order.',
|
||||||
' The maximum number of selections per round depends on the current system configuration.',
|
' The maximum number of selections per round depends on the current system configuration.',
|
||||||
|
' (Long press the number to cancel the bet)',
|
||||||
'',
|
'',
|
||||||
'2. **Bet Closing Stage**',
|
'2. **Bet Closing Stage**',
|
||||||
' Once the round enters the closing stage, the system stops accepting new bets.',
|
' Once the round enters the closing stage, the system stops accepting new bets.',
|
||||||
@@ -246,7 +254,8 @@ export default {
|
|||||||
'',
|
'',
|
||||||
'1. Only **1 winning number** is drawn in each round.',
|
'1. Only **1 winning number** is drawn in each round.',
|
||||||
'2. If the player’s selected numbers **include the winning number**, the bet is considered a win.',
|
'2. If the player’s selected numbers **include the winning number**, the bet is considered a win.',
|
||||||
'3. If a player selects multiple numbers and the bet amount is the same for each number, then:',
|
'3. Each round, a player may bet on **1–5 numbers only**, and must not select more than **5**.',
|
||||||
|
'4. If a player selects multiple numbers and the bet amount is the same for each number, then:',
|
||||||
'',
|
'',
|
||||||
'```text',
|
'```text',
|
||||||
'Winning Number Stake = Total Bet Amount ÷ Number of Selected Numbers',
|
'Winning Number Stake = Total Bet Amount ÷ Number of Selected Numbers',
|
||||||
@@ -470,6 +479,10 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
commonUi: {
|
commonUi: {
|
||||||
|
actions: {
|
||||||
|
refreshBalance: 'Refresh balance',
|
||||||
|
submitting: TEXT_SUBMITTING,
|
||||||
|
},
|
||||||
dialog: {
|
dialog: {
|
||||||
close: 'Close alert',
|
close: 'Close alert',
|
||||||
confirm: 'OK',
|
confirm: 'OK',
|
||||||
@@ -689,6 +702,7 @@ export default {
|
|||||||
announcement: 'Announcement',
|
announcement: 'Announcement',
|
||||||
},
|
},
|
||||||
animal: {
|
animal: {
|
||||||
|
cancelBet: 'Cancel Bet',
|
||||||
insufficientBalanceRecharge: 'Insufficient balance, please top up',
|
insufficientBalanceRecharge: 'Insufficient balance, please top up',
|
||||||
betLimitExceeded: TEXT_BET_LIMIT_EXCEEDED,
|
betLimitExceeded: TEXT_BET_LIMIT_EXCEEDED,
|
||||||
loading: 'Loading',
|
loading: 'Loading',
|
||||||
@@ -839,6 +853,9 @@ export default {
|
|||||||
exchangeRate: 'Exchange Rate ({{currency}})',
|
exchangeRate: 'Exchange Rate ({{currency}})',
|
||||||
exchangeRateValue: '{{coins}} {{platformCoinLabel}} = 1 {{currency}}',
|
exchangeRateValue: '{{coins}} {{platformCoinLabel}} = 1 {{currency}}',
|
||||||
convertible: 'Convertible {{currency}}',
|
convertible: 'Convertible {{currency}}',
|
||||||
|
handlingFee: 'Handling Fee',
|
||||||
|
quickDiamondAmount: '{{amount}} diamonds',
|
||||||
|
quickHandlingFee: 'Fee deducted: {{fee}}',
|
||||||
fixedExchangeDiamondAmount: 'Fixed Exchange Diamond Amount',
|
fixedExchangeDiamondAmount: 'Fixed Exchange Diamond Amount',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -177,6 +177,13 @@ export default {
|
|||||||
streak: 'streak {{count}} menang',
|
streak: 'streak {{count}} menang',
|
||||||
winAction: 'memenangkan',
|
winAction: 'memenangkan',
|
||||||
},
|
},
|
||||||
|
jackpotPool: {
|
||||||
|
ariaLabel: 'Pool jackpot saat ini {{amount}}',
|
||||||
|
iconAlt: 'Pool jackpot',
|
||||||
|
label: 'Jackpot',
|
||||||
|
promoLine1: 'Menang 10 Ronde Beruntun',
|
||||||
|
promoLine2: 'Dan Raih Super Jackpot',
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
unifiedBetHint: 'Bet seragam',
|
unifiedBetHint: 'Bet seragam',
|
||||||
totalBet: 'Total bet',
|
totalBet: 'Total bet',
|
||||||
@@ -229,6 +236,7 @@ export default {
|
|||||||
'1. **Tahap Taruhan**',
|
'1. **Tahap Taruhan**',
|
||||||
' Pemain memilih nomor yang ingin dipertaruhkan dari 36 nomor, memilih chip, mengonfirmasi taruhan, dan sistem akan memotong saldo serta membuat catatan taruhan.',
|
' Pemain memilih nomor yang ingin dipertaruhkan dari 36 nomor, memilih chip, mengonfirmasi taruhan, dan sistem akan memotong saldo serta membuat catatan taruhan.',
|
||||||
' Jumlah maksimum nomor yang dapat dipilih per putaran bergantung pada konfigurasi sistem saat ini.',
|
' Jumlah maksimum nomor yang dapat dipilih per putaran bergantung pada konfigurasi sistem saat ini.',
|
||||||
|
' (Tekan lama nomor untuk batalkan taruhan)',
|
||||||
'',
|
'',
|
||||||
'2. **Tahap Penutupan Taruhan**',
|
'2. **Tahap Penutupan Taruhan**',
|
||||||
' Setelah putaran memasuki tahap penutupan, sistem berhenti menerima taruhan baru.',
|
' Setelah putaran memasuki tahap penutupan, sistem berhenti menerima taruhan baru.',
|
||||||
@@ -245,7 +253,8 @@ export default {
|
|||||||
'',
|
'',
|
||||||
'1. Hanya **1 nomor pemenang** yang akan dikeluarkan di setiap putaran.',
|
'1. Hanya **1 nomor pemenang** yang akan dikeluarkan di setiap putaran.',
|
||||||
'2. Jika nomor yang dipilih pemain **mencakup nomor pemenang**, maka taruhan tersebut dianggap menang.',
|
'2. Jika nomor yang dipilih pemain **mencakup nomor pemenang**, maka taruhan tersebut dianggap menang.',
|
||||||
'3. Jika pemain memilih beberapa nomor dan jumlah taruhan untuk setiap nomor sama, maka:',
|
'3. Dalam setiap putaran, pemain hanya boleh bertaruh pada **1–5 nomor**, dan tidak boleh melebihi **5 nomor**.',
|
||||||
|
'4. Jika pemain memilih beberapa nomor dan jumlah taruhan untuk setiap nomor sama, maka:',
|
||||||
'',
|
'',
|
||||||
'```text',
|
'```text',
|
||||||
'Nilai Taruhan Nomor Menang = Total Taruhan ÷ Jumlah Nomor yang Dipilih',
|
'Nilai Taruhan Nomor Menang = Total Taruhan ÷ Jumlah Nomor yang Dipilih',
|
||||||
@@ -469,6 +478,10 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
commonUi: {
|
commonUi: {
|
||||||
|
actions: {
|
||||||
|
refreshBalance: 'Segarkan saldo',
|
||||||
|
submitting: TEXT_SUBMITTING,
|
||||||
|
},
|
||||||
dialog: {
|
dialog: {
|
||||||
close: 'Tutup notifikasi',
|
close: 'Tutup notifikasi',
|
||||||
confirm: 'OK',
|
confirm: 'OK',
|
||||||
@@ -689,6 +702,7 @@ export default {
|
|||||||
announcement: 'Pengumuman',
|
announcement: 'Pengumuman',
|
||||||
},
|
},
|
||||||
animal: {
|
animal: {
|
||||||
|
cancelBet: 'Batalkan Taruhan',
|
||||||
insufficientBalanceRecharge: 'Saldo tidak cukup, silakan isi ulang',
|
insufficientBalanceRecharge: 'Saldo tidak cukup, silakan isi ulang',
|
||||||
betLimitExceeded: TEXT_BET_LIMIT_EXCEEDED,
|
betLimitExceeded: TEXT_BET_LIMIT_EXCEEDED,
|
||||||
loading: 'Memuat',
|
loading: 'Memuat',
|
||||||
@@ -841,6 +855,9 @@ export default {
|
|||||||
exchangeRate: 'Rasio Penukaran ({{currency}})',
|
exchangeRate: 'Rasio Penukaran ({{currency}})',
|
||||||
exchangeRateValue: '{{coins}} {{platformCoinLabel}} = 1 {{currency}}',
|
exchangeRateValue: '{{coins}} {{platformCoinLabel}} = 1 {{currency}}',
|
||||||
convertible: 'Bisa Ditukar {{currency}}',
|
convertible: 'Bisa Ditukar {{currency}}',
|
||||||
|
handlingFee: 'Biaya Penanganan',
|
||||||
|
quickDiamondAmount: '{{amount}} diamond',
|
||||||
|
quickHandlingFee: 'Biaya dipotong: {{fee}}',
|
||||||
fixedExchangeDiamondAmount: 'Jumlah Berlian Tukar Tetap',
|
fixedExchangeDiamondAmount: 'Jumlah Berlian Tukar Tetap',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -180,6 +180,13 @@ export default {
|
|||||||
streak: 'streak {{count}} menang',
|
streak: 'streak {{count}} menang',
|
||||||
winAction: 'memenangi',
|
winAction: 'memenangi',
|
||||||
},
|
},
|
||||||
|
jackpotPool: {
|
||||||
|
ariaLabel: 'Dana jackpot semasa {{amount}}',
|
||||||
|
iconAlt: 'Dana jackpot',
|
||||||
|
label: 'Jackpot',
|
||||||
|
promoLine1: '10 Kemenangan Berturut',
|
||||||
|
promoLine2: 'Dan Raih Super Jackpot',
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
unifiedBetHint: 'Taruhan seragam',
|
unifiedBetHint: 'Taruhan seragam',
|
||||||
totalBet: 'Jumlah taruhan',
|
totalBet: 'Jumlah taruhan',
|
||||||
@@ -232,6 +239,7 @@ export default {
|
|||||||
'1. **Peringkat Pertaruhan**',
|
'1. **Peringkat Pertaruhan**',
|
||||||
' Pemain memilih nombor yang ingin dipertaruhkan daripada 36 nombor, memilih cip, mengesahkan pertaruhan, dan sistem akan menolak baki serta menjana rekod pertaruhan.',
|
' Pemain memilih nombor yang ingin dipertaruhkan daripada 36 nombor, memilih cip, mengesahkan pertaruhan, dan sistem akan menolak baki serta menjana rekod pertaruhan.',
|
||||||
' Bilangan maksimum nombor yang boleh dipilih bagi setiap pusingan bergantung pada konfigurasi semasa sistem.',
|
' Bilangan maksimum nombor yang boleh dipilih bagi setiap pusingan bergantung pada konfigurasi semasa sistem.',
|
||||||
|
' (Tekan lama nombor untuk batalkan pertaruhan)',
|
||||||
'',
|
'',
|
||||||
'2. **Peringkat Penutupan Pertaruhan**',
|
'2. **Peringkat Penutupan Pertaruhan**',
|
||||||
' Apabila pusingan memasuki peringkat penutupan, sistem akan berhenti menerima pertaruhan baharu.',
|
' Apabila pusingan memasuki peringkat penutupan, sistem akan berhenti menerima pertaruhan baharu.',
|
||||||
@@ -248,7 +256,8 @@ export default {
|
|||||||
'',
|
'',
|
||||||
'1. Hanya **1 nombor kemenangan** akan dikeluarkan dalam setiap pusingan.',
|
'1. Hanya **1 nombor kemenangan** akan dikeluarkan dalam setiap pusingan.',
|
||||||
'2. Jika nombor yang dipilih oleh pemain **mengandungi nombor kemenangan**, pertaruhan tersebut dianggap menang.',
|
'2. Jika nombor yang dipilih oleh pemain **mengandungi nombor kemenangan**, pertaruhan tersebut dianggap menang.',
|
||||||
'3. Jika pemain memilih beberapa nombor dan jumlah pertaruhan bagi setiap nombor adalah sama, maka:',
|
'3. Dalam setiap pusingan, pemain hanya boleh bertaruh pada **1–5 nombor**, dan tidak boleh melebihi **5 nombor**.',
|
||||||
|
'4. Jika pemain memilih beberapa nombor dan jumlah pertaruhan bagi setiap nombor adalah sama, maka:',
|
||||||
'',
|
'',
|
||||||
'```text',
|
'```text',
|
||||||
'Nilai Pertaruhan Nombor Menang = Jumlah Pertaruhan ÷ Bilangan Nombor yang Dipilih',
|
'Nilai Pertaruhan Nombor Menang = Jumlah Pertaruhan ÷ Bilangan Nombor yang Dipilih',
|
||||||
@@ -472,6 +481,10 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
commonUi: {
|
commonUi: {
|
||||||
|
actions: {
|
||||||
|
refreshBalance: 'Segarkan baki',
|
||||||
|
submitting: TEXT_SUBMITTING,
|
||||||
|
},
|
||||||
dialog: {
|
dialog: {
|
||||||
close: 'Tutup notifikasi',
|
close: 'Tutup notifikasi',
|
||||||
confirm: 'OK',
|
confirm: 'OK',
|
||||||
@@ -694,6 +707,7 @@ export default {
|
|||||||
announcement: 'Pengumuman',
|
announcement: 'Pengumuman',
|
||||||
},
|
},
|
||||||
animal: {
|
animal: {
|
||||||
|
cancelBet: 'Batalkan Pertaruhan',
|
||||||
insufficientBalanceRecharge: 'Baki tidak mencukupi, sila tambah nilai',
|
insufficientBalanceRecharge: 'Baki tidak mencukupi, sila tambah nilai',
|
||||||
betLimitExceeded: TEXT_BET_LIMIT_EXCEEDED,
|
betLimitExceeded: TEXT_BET_LIMIT_EXCEEDED,
|
||||||
loading: 'Memuatkan',
|
loading: 'Memuatkan',
|
||||||
@@ -845,6 +859,9 @@ export default {
|
|||||||
exchangeRate: 'Kadar Pertukaran ({{currency}})',
|
exchangeRate: 'Kadar Pertukaran ({{currency}})',
|
||||||
exchangeRateValue: '{{coins}} {{platformCoinLabel}} = 1 {{currency}}',
|
exchangeRateValue: '{{coins}} {{platformCoinLabel}} = 1 {{currency}}',
|
||||||
convertible: 'Boleh Tukar {{currency}}',
|
convertible: 'Boleh Tukar {{currency}}',
|
||||||
|
handlingFee: 'Yuran Pengendalian',
|
||||||
|
quickDiamondAmount: '{{amount}} berlian',
|
||||||
|
quickHandlingFee: 'Yuran ditolak: {{fee}}',
|
||||||
fixedExchangeDiamondAmount: 'Jumlah Berlian Tukaran Tetap',
|
fixedExchangeDiamondAmount: 'Jumlah Berlian Tukaran Tetap',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -175,6 +175,13 @@ export default {
|
|||||||
streak: '{{count}} 连胜',
|
streak: '{{count}} 连胜',
|
||||||
winAction: '赢得',
|
winAction: '赢得',
|
||||||
},
|
},
|
||||||
|
jackpotPool: {
|
||||||
|
ariaLabel: '彩金池当前金额 {{amount}}',
|
||||||
|
iconAlt: '彩金池',
|
||||||
|
label: '彩金池',
|
||||||
|
promoLine1: '连胜 10 局',
|
||||||
|
promoLine2: '并触发超级大奖',
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
unifiedBetHint: '统一下注额',
|
unifiedBetHint: '统一下注额',
|
||||||
totalBet: '总下注',
|
totalBet: '总下注',
|
||||||
@@ -224,6 +231,7 @@ export default {
|
|||||||
'1. **下注阶段**',
|
'1. **下注阶段**',
|
||||||
' 玩家在 36 个号码中选择想要押注的号码,选择筹码后确认下注,系统扣款并生成注单。',
|
' 玩家在 36 个号码中选择想要押注的号码,选择筹码后确认下注,系统扣款并生成注单。',
|
||||||
' 单期最多可选号码数,以系统当前配置为准。',
|
' 单期最多可选号码数,以系统当前配置为准。',
|
||||||
|
' (长按号码取消下注)',
|
||||||
'',
|
'',
|
||||||
'2. **封盘阶段**',
|
'2. **封盘阶段**',
|
||||||
' 进入封盘后,系统停止接受新注单。',
|
' 进入封盘后,系统停止接受新注单。',
|
||||||
@@ -238,10 +246,11 @@ export default {
|
|||||||
'',
|
'',
|
||||||
'1. 每期只开出 **1 个中奖号码**。',
|
'1. 每期只开出 **1 个中奖号码**。',
|
||||||
'2. 如果玩家押注的号码中 **包含开奖号码**,则视为中奖。',
|
'2. 如果玩家押注的号码中 **包含开奖号码**,则视为中奖。',
|
||||||
'3. 如果玩家选择了多个号码,且每个号码押注金额相同,则:',
|
'3. 每场玩家最多只能下注 **1~5 个号码**,不能超过 **5 个**。',
|
||||||
|
'4. 如果玩家选择了多个号码,且每个号码押注金额相同,则:',
|
||||||
'',
|
'',
|
||||||
'```text',
|
'```text',
|
||||||
'压中号码面额 = 本笔压注总额 ÷ 所选号码个数',
|
'压中号码面额 = 本笔投注总额 ÷ 所选号码个数',
|
||||||
'```',
|
'```',
|
||||||
'',
|
'',
|
||||||
'**示例:**',
|
'**示例:**',
|
||||||
@@ -453,6 +462,10 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
commonUi: {
|
commonUi: {
|
||||||
|
actions: {
|
||||||
|
refreshBalance: '刷新余额',
|
||||||
|
submitting: TEXT_SUBMITTING,
|
||||||
|
},
|
||||||
dialog: {
|
dialog: {
|
||||||
close: '关闭提示',
|
close: '关闭提示',
|
||||||
confirm: '知道了',
|
confirm: '知道了',
|
||||||
@@ -664,6 +677,7 @@ export default {
|
|||||||
announcement: '公告栏',
|
announcement: '公告栏',
|
||||||
},
|
},
|
||||||
animal: {
|
animal: {
|
||||||
|
cancelBet: '取消下注',
|
||||||
insufficientBalanceRecharge: '余额不足,请充值',
|
insufficientBalanceRecharge: '余额不足,请充值',
|
||||||
betLimitExceeded: TEXT_BET_LIMIT_EXCEEDED,
|
betLimitExceeded: TEXT_BET_LIMIT_EXCEEDED,
|
||||||
loading: '加载中',
|
loading: '加载中',
|
||||||
@@ -809,6 +823,9 @@ export default {
|
|||||||
exchangeRate: '兑换比例 ({{currency}})',
|
exchangeRate: '兑换比例 ({{currency}})',
|
||||||
exchangeRateValue: '{{coins}} {{platformCoinLabel}} = 1 {{currency}}',
|
exchangeRateValue: '{{coins}} {{platformCoinLabel}} = 1 {{currency}}',
|
||||||
convertible: '可兑换{{currency}}',
|
convertible: '可兑换{{currency}}',
|
||||||
|
handlingFee: '手续费',
|
||||||
|
quickDiamondAmount: '{{amount}} 钻石',
|
||||||
|
quickHandlingFee: '扣除手续费 {{fee}}',
|
||||||
fixedExchangeDiamondAmount: '固定兑换钻石金额',
|
fixedExchangeDiamondAmount: '固定兑换钻石金额',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { JackpotPoolTicker } from '@/components/jackpot-pool-ticker.tsx'
|
||||||
import { MessageBroadcast } from '@/components/message-broadcast.tsx'
|
import { MessageBroadcast } from '@/components/message-broadcast.tsx'
|
||||||
import { MobileAnimal } from '@/features/game/components/mobile/mobile-animal.tsx'
|
import { MobileAnimal } from '@/features/game/components/mobile/mobile-animal.tsx'
|
||||||
import { MobileBettingStartAlert } from '@/features/game/components/mobile/mobile-betting-start-alert.tsx'
|
import { MobileBettingStartAlert } from '@/features/game/components/mobile/mobile-betting-start-alert.tsx'
|
||||||
@@ -12,11 +13,12 @@ export function MobileEntry() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MobileHeader />
|
<MobileHeader />
|
||||||
<div className="w-full px-design-10 mb-design-5">
|
<div className="mx-auto mb-design-5 flex h-design-55 w-[calc(100%-20*var(--design-unit))] items-stretch gap-design-5">
|
||||||
<MessageBroadcast />
|
<div className="flex min-w-0 flex-1 flex-col gap-design-5">
|
||||||
</div>
|
<MessageBroadcast showJackpotPool={false} />
|
||||||
<div className="mx-auto mb-design-5 w-[calc(100%-20*var(--design-unit))]">
|
<MobileStatusMetrics />
|
||||||
<MobileStatusMetrics />
|
</div>
|
||||||
|
<JackpotPoolTicker variant="hud" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main className="mx-auto flex w-[calc(100%-20*var(--design-unit))] flex-col gap-design-5 pb-design-8">
|
<main className="mx-auto flex w-[calc(100%-20*var(--design-unit))] flex-col gap-design-5 pb-design-8">
|
||||||
|
|||||||
@@ -76,7 +76,10 @@ function DesktopAutoSettingModal() {
|
|||||||
|
|
||||||
const balance = parseBalance(currentUser?.coin)
|
const balance = parseBalance(currentUser?.coin)
|
||||||
|
|
||||||
if (tableLimitMax > 0 && totalBetAmount > tableLimitMax) {
|
if (
|
||||||
|
tableLimitMax > 0 &&
|
||||||
|
selections.some((selection) => selection.amount > tableLimitMax)
|
||||||
|
) {
|
||||||
notify.warning(t('commonUi.toast.betLimitExceeded'))
|
notify.warning(t('commonUi.toast.betLimitExceeded'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function DesktopWithdrawTopupModal() {
|
|||||||
}
|
}
|
||||||
isNormalBg={true}
|
isNormalBg={true}
|
||||||
titleAlign="left"
|
titleAlign="left"
|
||||||
className={'w-design-1200 h-design-700'}
|
className={'h-design-700 w-design-1320'}
|
||||||
>
|
>
|
||||||
<div className={'w-full h-[96%]'}>
|
<div className={'w-full h-[96%]'}>
|
||||||
{type === 'withdraw' ? <DesktopWithdraw /> : <DesktopTopup />}
|
{type === 'withdraw' ? <DesktopWithdraw /> : <DesktopTopup />}
|
||||||
|
|||||||
@@ -76,7 +76,10 @@ function MobileAutoSettingModal() {
|
|||||||
|
|
||||||
const balance = parseBalance(currentUser?.coin)
|
const balance = parseBalance(currentUser?.coin)
|
||||||
|
|
||||||
if (tableLimitMax > 0 && totalBetAmount > tableLimitMax) {
|
if (
|
||||||
|
tableLimitMax > 0 &&
|
||||||
|
selections.some((selection) => selection.amount > tableLimitMax)
|
||||||
|
) {
|
||||||
notify.warning(t('commonUi.toast.betLimitExceeded'))
|
notify.warning(t('commonUi.toast.betLimitExceeded'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export const registerFormSchema = z
|
|||||||
inviteCode: z
|
inviteCode: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(1, 'auth.validation.inviteCode.required')
|
|
||||||
.max(INVITE_CODE_MAX_LENGTH, 'auth.validation.inviteCode.max'),
|
.max(INVITE_CODE_MAX_LENGTH, 'auth.validation.inviteCode.max'),
|
||||||
password: passwordSchema,
|
password: passwordSchema,
|
||||||
mobile: usernameSchema,
|
mobile: usernameSchema,
|
||||||
|
|||||||
@@ -86,10 +86,6 @@ function normalizeBetQuantity(quantity: number) {
|
|||||||
return Math.max(MIN_BET_QUANTITY, Math.floor(quantity))
|
return Math.max(MIN_BET_QUANTITY, Math.floor(quantity))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSelectionBetAmount(chip: Chip, quantity: number) {
|
|
||||||
return chip.amount * normalizeBetQuantity(quantity)
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncSelectionsBetAmount(
|
function syncSelectionsBetAmount(
|
||||||
selections: BetSelection[],
|
selections: BetSelection[],
|
||||||
chipId: string,
|
chipId: string,
|
||||||
@@ -102,21 +98,6 @@ function syncSelectionsBetAmount(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveSelectionQuantity(
|
|
||||||
selections: BetSelection[],
|
|
||||||
chips: Chip[],
|
|
||||||
activeChipId: string,
|
|
||||||
) {
|
|
||||||
const chip = getChipById(chips, activeChipId)
|
|
||||||
const firstSelection = selections[0]
|
|
||||||
|
|
||||||
if (!chip || !firstSelection || chip.amount <= 0) {
|
|
||||||
return MIN_BET_QUANTITY
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalizeBetQuantity(firstSelection.amount / chip.amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
function createInitialRoundState(): GameRoundSlice & {
|
function createInitialRoundState(): GameRoundSlice & {
|
||||||
activeChipId: string
|
activeChipId: string
|
||||||
activeBetQuantity: number
|
activeBetQuantity: number
|
||||||
@@ -158,12 +139,18 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set, get) => ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentBetAmount = state.selections[0]?.amount ?? activeChip.amount
|
||||||
|
const nextBetAmount = Math.max(
|
||||||
|
activeChip.amount,
|
||||||
|
currentBetAmount + activeChip.amount * delta,
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeBetQuantity: nextQuantity,
|
activeBetQuantity: nextQuantity,
|
||||||
selections: syncSelectionsBetAmount(
|
selections: syncSelectionsBetAmount(
|
||||||
state.selections,
|
state.selections,
|
||||||
activeChip.id,
|
activeChip.id,
|
||||||
getSelectionBetAmount(activeChip, nextQuantity),
|
nextBetAmount,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -276,17 +263,34 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set, get) => ({
|
|||||||
if (
|
if (
|
||||||
!activeChip ||
|
!activeChip ||
|
||||||
state.round.phase !== 'betting' ||
|
state.round.phase !== 'betting' ||
|
||||||
hasExistingSelection ||
|
(!hasExistingSelection && selectedCellCount >= state.maxSelectionCount)
|
||||||
selectedCellCount >= state.maxSelectionCount
|
|
||||||
) {
|
) {
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentBetAmount = state.selections[0]?.amount ?? 0
|
||||||
|
const nextBetAmount = hasExistingSelection
|
||||||
|
? currentBetAmount + activeChip.amount
|
||||||
|
: activeChip.amount
|
||||||
|
const syncedSelections = syncSelectionsBetAmount(
|
||||||
|
state.selections,
|
||||||
|
activeChip.id,
|
||||||
|
nextBetAmount,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (hasExistingSelection) {
|
||||||
|
return {
|
||||||
|
activeBetQuantity: MIN_BET_QUANTITY,
|
||||||
|
selections: syncedSelections,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
activeBetQuantity: MIN_BET_QUANTITY,
|
||||||
selections: [
|
selections: [
|
||||||
...state.selections,
|
...syncedSelections,
|
||||||
{
|
{
|
||||||
amount: getSelectionBetAmount(activeChip, state.activeBetQuantity),
|
amount: nextBetAmount,
|
||||||
cellId,
|
cellId,
|
||||||
chipId: activeChip.id,
|
chipId: activeChip.id,
|
||||||
id: `bet-${cellId}-${state.selections.length + 1}-${Date.now()}`,
|
id: `bet-${cellId}-${state.selections.length + 1}-${Date.now()}`,
|
||||||
@@ -372,23 +376,11 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set, get) => ({
|
|||||||
nextSelections,
|
nextSelections,
|
||||||
state.activeChipId,
|
state.activeChipId,
|
||||||
)
|
)
|
||||||
const nextActiveChip = getChipById(state.chips, nextActiveChipId)
|
|
||||||
const nextBetQuantity = resolveSelectionQuantity(
|
|
||||||
nextSelections,
|
|
||||||
state.chips,
|
|
||||||
nextActiveChipId,
|
|
||||||
)
|
|
||||||
|
|
||||||
set({
|
set({
|
||||||
activeBetQuantity: nextBetQuantity,
|
activeBetQuantity: MIN_BET_QUANTITY,
|
||||||
activeChipId: nextActiveChipId,
|
activeChipId: nextActiveChipId,
|
||||||
selections: nextActiveChip
|
selections: nextSelections,
|
||||||
? syncSelectionsBetAmount(
|
|
||||||
nextSelections,
|
|
||||||
nextActiveChipId,
|
|
||||||
getSelectionBetAmount(nextActiveChip, nextBetQuantity),
|
|
||||||
)
|
|
||||||
: nextSelections,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -411,11 +403,6 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set, get) => ({
|
|||||||
return {
|
return {
|
||||||
activeBetQuantity: MIN_BET_QUANTITY,
|
activeBetQuantity: MIN_BET_QUANTITY,
|
||||||
activeChipId: chipId,
|
activeChipId: chipId,
|
||||||
selections: syncSelectionsBetAmount(
|
|
||||||
state.selections,
|
|
||||||
chipId,
|
|
||||||
getSelectionBetAmount(nextChip, MIN_BET_QUANTITY),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
@import "shadcn/tailwind.css";
|
@import "shadcn/tailwind.css";
|
||||||
@import "@fontsource-variable/geist";
|
@import "@fontsource-variable/geist";
|
||||||
|
@import "@fontsource-variable/oxanium";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@@ -12,6 +13,8 @@
|
|||||||
--font-mono:
|
--font-mono:
|
||||||
"JetBrains Mono", "SFMono-Regular", "SF Mono", Consolas, monospace;
|
"JetBrains Mono", "SFMono-Regular", "SF Mono", Consolas, monospace;
|
||||||
--font-countdown: "Countdown", "Inter", "SF Pro Display", sans-serif;
|
--font-countdown: "Countdown", "Inter", "SF Pro Display", sans-serif;
|
||||||
|
--font-jackpot-amount:
|
||||||
|
"Oxanium Variable", "Countdown", "SF Pro Display", sans-serif;
|
||||||
--color-game-bg: #07111f;
|
--color-game-bg: #07111f;
|
||||||
--color-game-surface: #0d1b2d;
|
--color-game-surface: #0d1b2d;
|
||||||
--color-game-surface-strong: #12253d;
|
--color-game-surface-strong: #12253d;
|
||||||
@@ -194,6 +197,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
.mobile-animal-touch-target,
|
||||||
|
.mobile-animal-touch-target * {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-animal-touch-target img {
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
|
||||||
.auth-password-input::-ms-clear,
|
.auth-password-input::-ms-clear,
|
||||||
.auth-password-input::-ms-reveal {
|
.auth-password-input::-ms-reveal {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export interface FinancePayChannelDto {
|
|||||||
currency_codes?: string[]
|
currency_codes?: string[]
|
||||||
deposit_banks?: FinanceWithdrawBankDto[]
|
deposit_banks?: FinanceWithdrawBankDto[]
|
||||||
fee_note?: string
|
fee_note?: string
|
||||||
|
handling_fee?: string
|
||||||
methods?: FinanceDepositMethodDto[]
|
methods?: FinanceDepositMethodDto[]
|
||||||
min_bank?: string
|
min_bank?: string
|
||||||
min_ewallet?: string
|
min_ewallet?: string
|
||||||
@@ -171,6 +172,7 @@ export interface FinancePayChannel {
|
|||||||
currencyCodes: string[]
|
currencyCodes: string[]
|
||||||
depositBanks: FinanceWithdrawBank[]
|
depositBanks: FinanceWithdrawBank[]
|
||||||
feeNote: string
|
feeNote: string
|
||||||
|
handlingFee: string
|
||||||
methods: FinanceDepositMethod[]
|
methods: FinanceDepositMethod[]
|
||||||
minBank: string
|
minBank: string
|
||||||
minEwallet: string
|
minEwallet: string
|
||||||
|
|||||||
@@ -4,12 +4,23 @@ export type AuthStatus = 'anonymous' | 'authenticated' | 'restoring'
|
|||||||
|
|
||||||
export type AuthSubmitContext = 'login' | 'register'
|
export type AuthSubmitContext = 'login' | 'register'
|
||||||
|
|
||||||
|
export type AuthCoinAmount = number | string
|
||||||
|
|
||||||
|
export interface AuthWithdrawAccount {
|
||||||
|
receiveAccount?: string
|
||||||
|
receiverEmail?: string
|
||||||
|
receiverMobile?: string
|
||||||
|
receiverName?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface AuthUser {
|
export interface AuthUser {
|
||||||
createTime?: number
|
createTime?: number
|
||||||
channelId?: number
|
channelId?: number
|
||||||
coin?: string
|
coin?: AuthCoinAmount
|
||||||
|
coinBalance?: number
|
||||||
currentStreak?: number
|
currentStreak?: number
|
||||||
email?: string
|
email?: string
|
||||||
|
frozenBalance?: number
|
||||||
headImage?: string
|
headImage?: string
|
||||||
id: string
|
id: string
|
||||||
isJackpot?: boolean
|
isJackpot?: boolean
|
||||||
@@ -21,8 +32,11 @@ export interface AuthUser {
|
|||||||
riskFlags?: number
|
riskFlags?: number
|
||||||
roles?: string[]
|
roles?: string[]
|
||||||
streakLevel?: number
|
streakLevel?: number
|
||||||
|
totalDepositCoin?: number
|
||||||
|
totalWithdrawCoin?: number
|
||||||
username?: string
|
username?: string
|
||||||
uuid?: string
|
uuid?: string
|
||||||
|
withdraw?: AuthWithdrawAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthSessionInput {
|
export interface AuthSessionInput {
|
||||||
@@ -47,7 +61,7 @@ export interface AuthTokenDto {
|
|||||||
|
|
||||||
export interface AuthUserDto {
|
export interface AuthUserDto {
|
||||||
channel_id: number
|
channel_id: number
|
||||||
coin: string
|
coin: AuthCoinAmount
|
||||||
phone?: string
|
phone?: string
|
||||||
risk_flags: number
|
risk_flags: number
|
||||||
username: string
|
username: string
|
||||||
@@ -69,17 +83,27 @@ export interface RefreshTokenDto {
|
|||||||
|
|
||||||
export interface AuthUserProfileDto {
|
export interface AuthUserProfileDto {
|
||||||
channel_id: number
|
channel_id: number
|
||||||
coin: string
|
coin: number
|
||||||
|
coin_balance: number
|
||||||
create_time: number
|
create_time: number
|
||||||
current_streak: number
|
current_streak: number
|
||||||
email: string
|
email: string
|
||||||
|
frozen_balance: number
|
||||||
head_image: string
|
head_image: string
|
||||||
last_bet_period_no: string
|
last_bet_period_no: string
|
||||||
phone: string
|
phone: string
|
||||||
register_invite_code: string
|
register_invite_code: string
|
||||||
risk_flags: number
|
risk_flags: number
|
||||||
|
total_deposit_coin: number
|
||||||
|
total_withdraw_coin: number
|
||||||
username: string
|
username: string
|
||||||
uuid: string
|
uuid: string
|
||||||
|
withdraw?: {
|
||||||
|
receive_account: string
|
||||||
|
receiver_email: string
|
||||||
|
receiver_mobile: string
|
||||||
|
receiver_name: string
|
||||||
|
} | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginRequestDto {
|
export interface LoginRequestDto {
|
||||||
@@ -96,7 +120,7 @@ export interface LogoutRequestDto {
|
|||||||
export interface RegisterRequestDto {
|
export interface RegisterRequestDto {
|
||||||
captcha: string
|
captcha: string
|
||||||
device_id?: string
|
device_id?: string
|
||||||
invite_code: string
|
invite_code?: string
|
||||||
password: string
|
password: string
|
||||||
username: string
|
username: string
|
||||||
}
|
}
|
||||||
@@ -124,7 +148,7 @@ export type LogoutPayload = LoginPayload
|
|||||||
|
|
||||||
export interface RegisterPayload {
|
export interface RegisterPayload {
|
||||||
captcha: string
|
captcha: string
|
||||||
inviteCode: string
|
inviteCode?: string
|
||||||
mobile: string
|
mobile: string
|
||||||
password: string
|
password: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,9 +109,16 @@ export interface AnnouncementState {
|
|||||||
lastUpdatedAt: string | null
|
lastUpdatedAt: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface JackpotPoolState {
|
||||||
|
current: number
|
||||||
|
speedPerSecond: number
|
||||||
|
updatedAt: string | null
|
||||||
|
}
|
||||||
|
|
||||||
export interface DashboardState {
|
export interface DashboardState {
|
||||||
countdownMs: number
|
countdownMs: number
|
||||||
featuredCellId: number | null
|
featuredCellId: number | null
|
||||||
|
jackpotPool: JackpotPoolState | null
|
||||||
onlinePlayers: number
|
onlinePlayers: number
|
||||||
tableLimitMax: number
|
tableLimitMax: number
|
||||||
tableLimitMin: number
|
tableLimitMin: number
|
||||||
@@ -332,6 +339,7 @@ export interface AnnouncementStateDto {
|
|||||||
export interface DashboardStateDto {
|
export interface DashboardStateDto {
|
||||||
countdown_ms: number
|
countdown_ms: number
|
||||||
featured_cell_id: number | null
|
featured_cell_id: number | null
|
||||||
|
jackpot_pool?: GameJackpotPoolDto | null
|
||||||
online_players: number
|
online_players: number
|
||||||
table_limit_max: number
|
table_limit_max: number
|
||||||
table_limit_min: number
|
table_limit_min: number
|
||||||
@@ -339,6 +347,11 @@ export interface DashboardStateDto {
|
|||||||
updated_at: string | null
|
updated_at: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GameJackpotPoolDto {
|
||||||
|
current: string
|
||||||
|
speed: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface ConnectionStateDto {
|
export interface ConnectionStateDto {
|
||||||
connected_at: string | null
|
connected_at: string | null
|
||||||
last_error: string | null
|
last_error: string | null
|
||||||
@@ -451,6 +464,7 @@ export interface GameLobbyStreakWinRewardDto {
|
|||||||
export interface GameLobbyInitDto {
|
export interface GameLobbyInitDto {
|
||||||
bet_config: GameLobbyBetConfigDto
|
bet_config: GameLobbyBetConfigDto
|
||||||
dictionary: GameLobbyDictionaryItemDto[]
|
dictionary: GameLobbyDictionaryItemDto[]
|
||||||
|
jackpot_pool?: GameJackpotPoolDto | null
|
||||||
period?: GameLobbyPeriodDto | null
|
period?: GameLobbyPeriodDto | null
|
||||||
runtime_enabled: boolean
|
runtime_enabled: boolean
|
||||||
server_time: number
|
server_time: number
|
||||||
|
|||||||
Reference in New Issue
Block a user