refactor(game): 重构项目结构,优化链路, 移动端适配
- 移除 useGameBoardVm 数据层实施说明文档 - 移除核心玩法与前端规则摘要文档 - 移除游戏模块数据与界面分层第一阶段实施稿文档 - 清理与数据层重构相关的技术方案说明 - 删除关于 PC 和 Mobile 界面分离的设计规划 - 移除 view-model hooks 架构设计相关内容
This commit is contained in:
@@ -17,7 +17,6 @@ import { SmartBackground } from '@/components/smart-background.tsx'
|
||||
import { SmartImage } from '@/components/smart-image'
|
||||
import { REWARD_OVERLAY_DURATION_MS } from '@/constants'
|
||||
import {
|
||||
type BetSelection,
|
||||
FLOWER_IMAGE_BY_ID,
|
||||
groupSelectionsByCell,
|
||||
} from '@/features/game/shared'
|
||||
@@ -28,6 +27,7 @@ import {
|
||||
useGameAutoHostingStore,
|
||||
useGameRoundStore,
|
||||
} from '@/store/game'
|
||||
import type { BetSelection } from '@/type'
|
||||
|
||||
const REWARD_OVERLAY_FADE_OUT_MS = 300
|
||||
const REWARD_CHILDREN_FADE_IN_MS = 2_000
|
||||
|
||||
@@ -7,8 +7,8 @@ import diamondIcon from '@/assets/system/diamond.webp'
|
||||
import { SmartImage } from '@/components/smart-image'
|
||||
import { DesktopAnimalOverlay } from '@/features/game/components/desktop/desktop-animal-overlay.tsx'
|
||||
import { RoundBettingStartAlert } from '@/features/game/components/shared/round-betting-start-alert.tsx'
|
||||
import { useAnimalVm } from '@/features/game/hooks/use-animal-vm'
|
||||
import { FLOWER_IMAGE_LIST } from '@/features/game/shared'
|
||||
import { useAnimalVm } from '@/hooks/use-animal-vm'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
import { useGameRoundStore } from '@/store/game'
|
||||
|
||||
@@ -16,7 +16,7 @@ import diamond from '@/assets/system/diamond.webp'
|
||||
import { SmartBackground } from '@/components/smart-background.tsx'
|
||||
import { SmartImage } from '@/components/smart-image.tsx'
|
||||
import { ACTION_OPTIONS } from '@/constants'
|
||||
import { useGameControlVm } from '@/features/game/hooks/use-game-control-vm.ts'
|
||||
import { useGameControlVm } from '@/hooks/use-game-control-vm.ts'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useModalStore } from '@/store'
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import historyBg from '@/assets/system/history-bg.png'
|
||||
import { SmartBackground } from '@/components/smart-background.tsx'
|
||||
import { SmartImage } from '@/components/smart-image'
|
||||
import { DataLoadingIndicator } from '@/components/ui/data-loading-indicator'
|
||||
import { useGameHistoryVm } from '@/features/game/hooks/use-game-history-vm.ts'
|
||||
import { FLOWER_IMAGE_BY_ID } from '@/features/game/shared'
|
||||
import { useGameHistoryVm } from '@/hooks/use-game-history-vm.ts'
|
||||
|
||||
function HistoryRewardNumber({
|
||||
className,
|
||||
|
||||
@@ -16,10 +16,7 @@ import chatImage from '@/assets/system/chat.webp'
|
||||
import diamond from '@/assets/system/diamond.webp'
|
||||
import logo from '@/assets/system/logo.webp'
|
||||
import { SmartImage } from '@/components/smart-image.tsx'
|
||||
import {
|
||||
useHeaderClockLabel,
|
||||
useHeaderVm,
|
||||
} from '@/features/game/hooks/use-header-vm'
|
||||
import { useHeaderClockLabel, useHeaderVm } from '@/hooks/use-header-vm'
|
||||
import { useModalStore } from '@/store'
|
||||
|
||||
function HeaderClock() {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { SmartBackground } from '@/components/smart-background.tsx'
|
||||
import { SmartImage } from '@/components/smart-image.tsx'
|
||||
import { DesktopCountdown } from '@/features/game/components/desktop/desktop-countdown.tsx'
|
||||
import { DesktopTitle } from '@/features/game/components/desktop/desktop-title.tsx'
|
||||
import { useGameStatusVm } from '@/features/game/hooks/use-game-status-vm.ts'
|
||||
import { useGameStatusVm } from '@/hooks/use-game-status-vm.ts'
|
||||
import { cn } from '@/lib/utils.ts'
|
||||
|
||||
export function DesktopStatusLine() {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { createDeposit, type DepositTierItem } from '@/api'
|
||||
import { DataLoadingIndicator } from '@/components/ui/data-loading-indicator'
|
||||
import { createDeposit, type DepositTierItem } from '@/features/game/api'
|
||||
import { useDepositTierList } from '@/features/game/hooks/use-deposit-tier-list'
|
||||
import { useDepositTierList } from '@/hooks/use-deposit-tier-list'
|
||||
import { notify } from '@/lib/notify'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select.tsx'
|
||||
import { useWithdrawSubmit } from '@/features/game/hooks/use-withdraw-submit'
|
||||
import { useWithdrawVm } from '@/features/game/hooks/use-withdraw-vm'
|
||||
import { useWithdrawSubmit } from '@/hooks/use-withdraw-submit'
|
||||
import { useWithdrawVm } from '@/hooks/use-withdraw-vm'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useModalStore } from '@/store'
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export { FullscreenLottieOverlay } from '@/components/fullscreen-lottie-overlay.tsx'
|
||||
export type { FullscreenLottieSource } from '@/components/fullscreen-lottie-overlay.types.ts'
|
||||
export { DesktopHeader } from '@/features/game/components/desktop/desktop-header'
|
||||
export { EntryNoticeGateModal } from '@/features/game/components/shared/entry-notice-gate-modal'
|
||||
@@ -7,15 +7,15 @@ import {
|
||||
VolumeX,
|
||||
Wifi,
|
||||
} from 'lucide-react'
|
||||
import { motion } from 'motion/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import avatar from '@/assets/system/avatar.webp'
|
||||
import chatImage from '@/assets/system/chat.webp'
|
||||
import diamond from '@/assets/system/diamond.webp'
|
||||
import logo from '@/assets/system/logo.webp'
|
||||
import { SmartImage } from '@/components/smart-image.tsx'
|
||||
import {
|
||||
useHeaderClockLabel,
|
||||
useHeaderVm,
|
||||
} from '@/features/game/hooks/use-header-vm'
|
||||
import { useHeaderClockLabel, useHeaderVm } from '@/hooks/use-header-vm'
|
||||
import { useModalStore } from '@/store'
|
||||
|
||||
function MobileHeaderClock() {
|
||||
const systemTimeLabel = useHeaderClockLabel()
|
||||
@@ -29,6 +29,7 @@ function MobileHeaderClock() {
|
||||
|
||||
export function MobileHeader() {
|
||||
const { t } = useTranslation()
|
||||
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
||||
const {
|
||||
authStatus,
|
||||
currentLanguageLabel,
|
||||
@@ -53,7 +54,7 @@ export function MobileHeader() {
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-30 h-design-62">
|
||||
<div className="border-b-2 border-[#787553] bg-[#020B14]] bg-[#020B14] flex h-design-33 w-full items-center">
|
||||
<div className="border-b-2 border-[#787553] bg-[#020B14] flex h-design-33 w-full items-center">
|
||||
<div className="flex h-design-23 w-design-130 shrink-0 items-center justify-center border-r border-[rgba(128,223,231,0.45)] px-design-10">
|
||||
<SmartImage
|
||||
src={logo}
|
||||
@@ -64,6 +65,20 @@ export function MobileHeader() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenLanguage}
|
||||
className={`${actionButtonClassName} !px-design-10 justify-between`}
|
||||
>
|
||||
<SmartImage
|
||||
src={currentLanguageOption.icon}
|
||||
alt={currentLanguageLabel}
|
||||
className="h-design-14 w-design-14 shrink-0 rounded-full"
|
||||
imgClassName="object-cover"
|
||||
/>
|
||||
<div className="min-w-0 truncate">{currentLanguageLabel}</div>
|
||||
</button>
|
||||
|
||||
{authStatus === 'authenticated' ? (
|
||||
<div className="flex h-full min-w-0 flex-1 items-center justify-end gap-design-7 px-design-9">
|
||||
<button
|
||||
@@ -213,19 +228,20 @@ export function MobileHeader() {
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
<motion.button
|
||||
type="button"
|
||||
onClick={onOpenLanguage}
|
||||
className={`${actionButtonClassName} !px-design-10 justify-between`}
|
||||
onClick={() => setModalOpen('desktopSupport', true)}
|
||||
whileTap={{
|
||||
scale: 0.95,
|
||||
}}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
>
|
||||
<SmartImage
|
||||
src={currentLanguageOption.icon}
|
||||
alt={currentLanguageLabel}
|
||||
className="h-design-14 w-design-14 shrink-0 rounded-full"
|
||||
imgClassName="object-cover"
|
||||
className={'h-design-20 w-design-20 cursor-pointer'}
|
||||
alt={'chatImage'}
|
||||
src={chatImage}
|
||||
/>
|
||||
<div className="min-w-0 truncate">{currentLanguageLabel}</div>
|
||||
</button>
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
186
src/features/game/components/mobile/mobile-topup.tsx
Normal file
186
src/features/game/components/mobile/mobile-topup.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { createDeposit, type DepositTierItem } from '@/api'
|
||||
import { DataLoadingIndicator } from '@/components/ui/data-loading-indicator'
|
||||
import { useDepositTierList } from '@/hooks/use-deposit-tier-list'
|
||||
import { notify } from '@/lib/notify'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const PANEL_CLASS =
|
||||
'rounded-md border border-[rgba(110,229,243,0.24)] bg-[linear-gradient(180deg,rgba(7,30,43,0.9),rgba(3,15,26,0.94))] shadow-[inset_0_0_calc(var(--design-unit)*12)_rgba(88,225,238,0.08)]'
|
||||
|
||||
function formatNumber(value: number) {
|
||||
return new Intl.NumberFormat('en-US').format(value)
|
||||
}
|
||||
|
||||
function MobileTopup() {
|
||||
const { t } = useTranslation()
|
||||
const tierListQuery = useDepositTierList()
|
||||
const tiers = tierListQuery.data ?? []
|
||||
const createDepositInFlightRef = useRef(false)
|
||||
const pendingPayWindowRef = useRef<Window | null>(null)
|
||||
const createDepositMutation = useMutation({
|
||||
mutationFn: ({
|
||||
channelCode,
|
||||
tierId,
|
||||
}: {
|
||||
channelCode: string
|
||||
tierId: string
|
||||
}) =>
|
||||
createDeposit({
|
||||
channel_code: channelCode,
|
||||
idempotency_key: String(Date.now()),
|
||||
tier_id: tierId,
|
||||
}),
|
||||
})
|
||||
|
||||
const handleCreateDeposit = async (tier: DepositTierItem) => {
|
||||
if (createDepositInFlightRef.current || createDepositMutation.isPending) {
|
||||
return
|
||||
}
|
||||
|
||||
const channelCode = tier.payChannelCode ?? tier.channels[0]?.code ?? ''
|
||||
|
||||
if (!channelCode) {
|
||||
notify.error(t('commonUi.toast.requestFailed'))
|
||||
return
|
||||
}
|
||||
|
||||
createDepositInFlightRef.current = true
|
||||
const payWindow = window.open('', '_blank')
|
||||
|
||||
if (!payWindow) {
|
||||
createDepositInFlightRef.current = false
|
||||
notify.error(t('gameDesktop.topup.tier.openPayUrlFailed'))
|
||||
return
|
||||
}
|
||||
|
||||
payWindow.opener = null
|
||||
pendingPayWindowRef.current = payWindow
|
||||
|
||||
try {
|
||||
const result = await createDepositMutation.mutateAsync({
|
||||
channelCode,
|
||||
tierId: tier.id,
|
||||
})
|
||||
const payUrl = result.pay_url.trim()
|
||||
|
||||
if (!payUrl) {
|
||||
payWindow.close()
|
||||
notify.error(t('gameDesktop.topup.tier.missingPayUrl'))
|
||||
return
|
||||
}
|
||||
|
||||
payWindow.location.replace(payUrl)
|
||||
notify.success(t('gameDesktop.topup.tier.createSuccess'))
|
||||
} catch (error) {
|
||||
payWindow.close()
|
||||
notify.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t('commonUi.toast.requestFailed'),
|
||||
)
|
||||
} finally {
|
||||
createDepositInFlightRef.current = false
|
||||
|
||||
if (pendingPayWindowRef.current === payWindow) {
|
||||
pendingPayWindowRef.current = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 w-full px-design-8 pb-design-8 text-[#D9FFFF]">
|
||||
<div
|
||||
className={cn(
|
||||
PANEL_CLASS,
|
||||
'flex h-full min-h-0 w-full min-w-0 flex-col overflow-hidden px-design-10 py-design-10',
|
||||
)}
|
||||
>
|
||||
<div className="mb-design-8 flex items-center border-b border-[rgba(89,209,223,0.2)] pb-design-8">
|
||||
<div className="text-design-16 font-semibold text-[#9AF5FB]">
|
||||
{t('gameDesktop.topup.tier.title')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tierListQuery.isLoading ? (
|
||||
<DataLoadingIndicator
|
||||
label={t('gameDesktop.topup.tier.loading')}
|
||||
className="h-full min-h-0 rounded-[calc(var(--design-unit)*6)] border border-[rgba(103,227,239,0.18)] bg-[rgba(6,24,35,0.52)]"
|
||||
/>
|
||||
) : tierListQuery.isError ? (
|
||||
<div className="flex h-full min-h-0 items-center justify-center rounded-[calc(var(--design-unit)*6)] border border-[rgba(185,63,68,0.28)] bg-[rgba(34,13,16,0.42)] px-design-12 text-center text-design-14 text-[#F4A9AE]">
|
||||
{t('gameDesktop.topup.tier.failed')}
|
||||
</div>
|
||||
) : tiers.length === 0 ? (
|
||||
<div className="flex h-full min-h-0 items-center justify-center rounded-[calc(var(--design-unit)*6)] border border-[rgba(103,227,239,0.18)] bg-[rgba(6,24,35,0.52)] px-design-12 text-center text-design-14 text-[#8FDDE6]">
|
||||
{t('gameDesktop.topup.tier.empty')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid min-h-0 min-w-0 grid-cols-2 gap-design-8 overflow-y-auto pr-design-1">
|
||||
{tiers.map((tier) => (
|
||||
<button
|
||||
key={tier.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void handleCreateDeposit(tier)
|
||||
}}
|
||||
className={cn(
|
||||
'relative min-h-design-126 overflow-hidden rounded-[calc(var(--design-unit)*7)] border border-[rgba(103,227,239,0.24)] bg-[linear-gradient(180deg,rgba(11,48,63,0.9),rgba(5,24,35,0.94))] px-design-8 py-design-8 text-left shadow-[0_0_calc(var(--design-unit)*8)_rgba(88,225,238,0.08)] transition-[border-color,box-shadow,filter] duration-150',
|
||||
createDepositMutation.isPending
|
||||
? 'cursor-wait opacity-80'
|
||||
: 'cursor-pointer hover:border-[rgba(170,247,255,0.62)] hover:brightness-110 active:brightness-90',
|
||||
)}
|
||||
>
|
||||
<div className="absolute right-design-6 top-design-6 max-w-design-48 truncate rounded-full border border-[rgba(121,219,229,0.28)] bg-[rgba(10,39,52,0.7)] px-design-5 py-[2px] text-design-8 leading-none text-[#7CDDE7]">
|
||||
{tier.currency ?? 'FIAT'}
|
||||
</div>
|
||||
|
||||
<div className="pr-design-46 text-design-10 uppercase leading-tight tracking-[0.04em] text-[#63AEB6]">
|
||||
{tier.title}
|
||||
</div>
|
||||
|
||||
<div className="pt-design-5 text-design-18 font-semibold leading-none text-[#FFE229]">
|
||||
{formatNumber(tier.payAmount)}
|
||||
</div>
|
||||
<div className="pt-design-3 text-design-10 text-[#9FDCE3]">
|
||||
{t('gameDesktop.topup.tier.coins')}:{' '}
|
||||
{formatNumber(tier.totalAmount)}
|
||||
</div>
|
||||
|
||||
<div className="mt-design-7 rounded-[calc(var(--design-unit)*5)] border border-[rgba(89,209,223,0.18)] bg-[rgba(4,19,28,0.58)] px-design-6 py-design-5">
|
||||
<div className="flex items-center justify-between gap-design-6 text-design-10">
|
||||
<span className="text-[#7CE3E8]">
|
||||
{t('gameDesktop.topup.tier.bonus')}
|
||||
</span>
|
||||
<span className="text-[#FFF1C9]">
|
||||
{formatNumber(tier.bonusAmount)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-design-3 flex items-center justify-between gap-design-6 text-design-10">
|
||||
<span className="text-[#7CE3E8]">Channels</span>
|
||||
<span className="line-clamp-1 text-right text-[#6DFF83]">
|
||||
{tier.channels.length > 0
|
||||
? tier.channels
|
||||
.map((channel) => channel.name)
|
||||
.join(', ')
|
||||
: '--'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{tier.desc ? (
|
||||
<div className="mt-design-5 line-clamp-2 text-design-9 leading-[1.25] text-[#6DAAB0]">
|
||||
{tier.desc}
|
||||
</div>
|
||||
) : null}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MobileTopup
|
||||
640
src/features/game/components/mobile/mobile-withdraw.tsx
Normal file
640
src/features/game/components/mobile/mobile-withdraw.tsx
Normal file
@@ -0,0 +1,640 @@
|
||||
import { Minus, Plus } from 'lucide-react'
|
||||
import { type ReactNode, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import lengthBlueBtn from '@/assets/system/length-blue-btn.webp'
|
||||
import lengthGreenBtn from '@/assets/system/length-green-btn.webp'
|
||||
import { SmartBackground } from '@/components/smart-background.tsx'
|
||||
import { Input } from '@/components/ui/input.tsx'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select.tsx'
|
||||
import { useWithdrawSubmit } from '@/hooks/use-withdraw-submit'
|
||||
import { useWithdrawVm } from '@/hooks/use-withdraw-vm'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useModalStore } from '@/store'
|
||||
|
||||
const PANEL_CLASS =
|
||||
'rounded-md border border-[rgba(110,229,243,0.24)] bg-[linear-gradient(180deg,rgba(7,30,43,0.9),rgba(3,15,26,0.94))] shadow-[inset_0_0_calc(var(--design-unit)*10)_rgba(88,225,238,0.08)]'
|
||||
|
||||
function formatNumber(value: number) {
|
||||
return new Intl.NumberFormat('en-US').format(value)
|
||||
}
|
||||
|
||||
function getPaymentGlyph(code: string, name: string) {
|
||||
if (code.toLowerCase().includes('alipay')) {
|
||||
return '支'
|
||||
}
|
||||
|
||||
return name.trim().slice(0, 1).toUpperCase() || code.slice(0, 1).toUpperCase()
|
||||
}
|
||||
|
||||
function WithdrawField({
|
||||
label,
|
||||
children,
|
||||
}: {
|
||||
label: string
|
||||
children: ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-design-3">
|
||||
<div className="text-design-10 font-medium uppercase leading-none text-[#6FD4DA]">
|
||||
{label}
|
||||
</div>
|
||||
<div className="min-w-0">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AmountShell({
|
||||
amount,
|
||||
availableBalanceText,
|
||||
onAmountChange,
|
||||
onMinus,
|
||||
onPlus,
|
||||
}: {
|
||||
amount: number
|
||||
availableBalanceText: string
|
||||
onAmountChange: (value: number) => void
|
||||
onMinus: () => void
|
||||
onPlus: () => void
|
||||
}) {
|
||||
function handleInputChange(value: string) {
|
||||
const nextValue = Number(value.replace(/[^\d]/g, ''))
|
||||
|
||||
onAmountChange(Number.isFinite(nextValue) ? nextValue : 0)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-design-2">
|
||||
<div className="flex h-design-34 items-center gap-design-6 rounded-[calc(var(--design-unit)*5)] border border-[rgba(103,227,239,0.32)] bg-[linear-gradient(180deg,rgba(14,64,74,0.82),rgba(8,36,47,0.78))] px-design-6 shadow-[inset_0_0_calc(var(--design-unit)*10)_rgba(93,239,255,0.08)]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onMinus}
|
||||
className="flex h-design-24 w-design-24 shrink-0 items-center justify-center rounded-[calc(var(--design-unit)*4)] border border-[rgba(109,232,244,0.44)] bg-[rgba(37,115,123,0.32)] text-[#E1FEFF] transition hover:border-[rgba(170,247,255,0.82)] hover:bg-[rgba(66,146,151,0.35)]"
|
||||
>
|
||||
<Minus className="h-design-12 w-design-12" />
|
||||
</button>
|
||||
|
||||
<input
|
||||
value={amount === 0 ? '' : String(amount)}
|
||||
onChange={(event) => handleInputChange(event.target.value)}
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
className="h-full min-w-0 flex-1 bg-transparent text-center text-design-16 font-medium text-[#A1EBF3] outline-none placeholder:text-[rgba(109,170,176,0.55)]"
|
||||
placeholder="0"
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onPlus}
|
||||
className="flex h-design-24 w-design-24 shrink-0 items-center justify-center rounded-[calc(var(--design-unit)*4)] border border-[rgba(109,232,244,0.44)] bg-[rgba(37,115,123,0.32)] text-[#E1FEFF] transition hover:border-[rgba(170,247,255,0.82)] hover:bg-[rgba(66,146,151,0.35)]"
|
||||
>
|
||||
<Plus className="h-design-12 w-design-12" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="pl-design-2 text-design-10 text-[#6DAAB0]">
|
||||
{availableBalanceText}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function QuickAmountCard({
|
||||
amount,
|
||||
preview,
|
||||
active,
|
||||
onClick,
|
||||
}: {
|
||||
amount: number
|
||||
preview: string
|
||||
active: boolean
|
||||
onClick: () => void
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
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',
|
||||
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-[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',
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'absolute right-design-6 top-design-6 h-design-5 w-design-5 rounded-full transition',
|
||||
active
|
||||
? 'bg-[#FFD15E] shadow-[0_0_8px_rgba(255,209,94,0.8)]'
|
||||
: 'bg-[rgba(122,220,230,0.26)]',
|
||||
)}
|
||||
/>
|
||||
<div className="max-w-full truncate text-design-13 font-semibold leading-none text-[#FFE229]">
|
||||
{amount}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'max-w-full truncate pt-design-3 text-design-10 leading-none',
|
||||
active ? 'text-[#FFDFA4]' : 'text-[#63AEB6]',
|
||||
)}
|
||||
>
|
||||
{preview}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function PaymentCard({
|
||||
active,
|
||||
label,
|
||||
glyph,
|
||||
onClick,
|
||||
}: {
|
||||
active: boolean
|
||||
label: string
|
||||
glyph: string
|
||||
onClick: () => void
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'group relative flex h-design-42 min-w-0 cursor-pointer items-center gap-design-6 rounded-[calc(var(--design-unit)*6)] border px-design-6 text-left transition-[border-color,background-color,box-shadow,filter] duration-150',
|
||||
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.18),inset_0_0_calc(var(--design-unit)*10)_rgba(255,217,120,0.08)]'
|
||||
: 'border-[rgba(103,227,239,0.24)] 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',
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'absolute right-design-6 top-design-6 h-design-5 w-design-5 rounded-full transition',
|
||||
active
|
||||
? 'bg-[#FFD15E] shadow-[0_0_8px_rgba(255,209,94,0.8)]'
|
||||
: 'bg-[rgba(122,220,230,0.26)]',
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-design-26 w-design-26 shrink-0 items-center justify-center rounded-[calc(var(--design-unit)*5)] border text-design-12 font-semibold leading-none transition-colors',
|
||||
active
|
||||
? 'border-[rgba(255,218,132,0.45)] bg-[rgba(255,211,113,0.16)] text-[#FFD97A]'
|
||||
: 'border-[rgba(121,219,229,0.28)] bg-[rgba(10,39,52,0.7)] text-[#8DE4EA]',
|
||||
)}
|
||||
>
|
||||
{glyph}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 pr-design-7">
|
||||
<div
|
||||
className={cn(
|
||||
'truncate !text-design-12 font-medium leading-none',
|
||||
active ? 'text-[#FFF1C9]' : 'text-[#D7FBFF]',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'pt-design-2 !text-design-10 uppercase leading-none tracking-[0.03em]',
|
||||
active ? 'text-[#FFDFA4]' : 'text-[#63AEB6]',
|
||||
)}
|
||||
>
|
||||
Channel
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function InputShell({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
error,
|
||||
errorMessage,
|
||||
uppercase = false,
|
||||
type = 'text',
|
||||
}: {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
placeholder: string
|
||||
error?: boolean
|
||||
errorMessage?: string
|
||||
uppercase?: boolean
|
||||
type?: 'text' | 'email' | 'tel'
|
||||
}) {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-design-3">
|
||||
<Input
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
placeholder={placeholder}
|
||||
className={cn(
|
||||
'h-design-30 rounded-[calc(var(--design-unit)*5)] border px-design-8 text-design-12',
|
||||
uppercase && 'uppercase',
|
||||
error
|
||||
? 'border-[#B93F44] bg-[rgba(34,13,16,0.78)] text-[#FCEEEE]'
|
||||
: 'border-[rgba(103,227,239,0.24)] bg-[linear-gradient(180deg,rgba(10,47,57,0.84),rgba(5,23,32,0.92))] text-[#ACF1F6]',
|
||||
)}
|
||||
/>
|
||||
{error && errorMessage ? (
|
||||
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
|
||||
{errorMessage}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PreviewRow({
|
||||
label,
|
||||
value,
|
||||
highlight = false,
|
||||
}: {
|
||||
label: string
|
||||
value: ReactNode
|
||||
highlight?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div className="flex border-b border-[rgba(89,209,223,0.2)] last:border-b-0">
|
||||
<div className="flex w-[46%] shrink-0 items-center border-r border-[rgba(89,209,223,0.2)] px-design-6 py-design-7 text-design-10 font-medium uppercase leading-[1.15] text-[#7CE3E8]">
|
||||
{label}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'flex min-w-0 flex-1 items-center justify-end px-design-6 py-design-7 text-right text-design-10 text-[#E6FFFF]',
|
||||
highlight && 'text-design-11 font-semibold text-[#6DFF83]',
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MobileWithdraw() {
|
||||
const { t } = useTranslation()
|
||||
const vm = useWithdrawVm()
|
||||
const withdrawSubmitMutation = useWithdrawSubmit()
|
||||
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
||||
const [hasSubmitted, setHasSubmitted] = useState(false)
|
||||
const [activeQuickAmountId, setActiveQuickAmountId] = useState<string | null>(
|
||||
null,
|
||||
)
|
||||
|
||||
function handleAmountChange(nextAmount: number) {
|
||||
vm.setAmount(Math.max(0, nextAmount))
|
||||
setActiveQuickAmountId(null)
|
||||
}
|
||||
|
||||
function handleQuickAmountSelect(optionId: string, amount: number) {
|
||||
vm.setAmount(Math.max(0, amount))
|
||||
setActiveQuickAmountId(optionId)
|
||||
}
|
||||
|
||||
function resetWithdrawFormState() {
|
||||
setHasSubmitted(false)
|
||||
setActiveQuickAmountId(null)
|
||||
vm.resetForm()
|
||||
}
|
||||
|
||||
function handleCloseWithdraw() {
|
||||
resetWithdrawFormState()
|
||||
setModalOpen('desktopWithdrawTopup', false)
|
||||
}
|
||||
|
||||
function handleConfirmWithdraw() {
|
||||
if (withdrawSubmitMutation.isPending) {
|
||||
return
|
||||
}
|
||||
|
||||
setHasSubmitted(true)
|
||||
|
||||
if (
|
||||
vm.amountRequiredError ||
|
||||
vm.amountExceedsBalance ||
|
||||
vm.holderNameError ||
|
||||
vm.bankAccountError ||
|
||||
vm.paymentChannelCodeError ||
|
||||
vm.bankCodeError ||
|
||||
vm.receiverEmailError ||
|
||||
vm.receiverPhoneError
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
withdrawSubmitMutation.mutate(
|
||||
{
|
||||
bank_code: vm.bankCode,
|
||||
channel_code: vm.paymentChannelCode,
|
||||
idempotency_key: String(Date.now()),
|
||||
receive_account: vm.bankAccount.trim(),
|
||||
receiver_email: vm.receiverEmail.trim(),
|
||||
receiver_mobile: vm.receiverPhone.trim(),
|
||||
receiver_name: vm.holderName.trim(),
|
||||
receive_type: 'bank',
|
||||
withdraw_coin: vm.amount,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleCloseWithdraw()
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 w-full px-design-6 pb-design-6 text-[#D9FFFF]">
|
||||
<div
|
||||
className={cn(
|
||||
PANEL_CLASS,
|
||||
'flex h-full min-h-0 w-full min-w-0 flex-col overflow-hidden',
|
||||
)}
|
||||
>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-design-8 py-design-7">
|
||||
<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}
|
||||
</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
|
||||
label={t('gameDesktop.withdraw.fields.currencyType')}
|
||||
>
|
||||
<Select
|
||||
value={vm.currencyCode}
|
||||
onValueChange={vm.setCurrencyCode}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="h-design-30 w-full rounded-[calc(var(--design-unit)*5)] border-[rgba(103,227,239,0.3)] bg-[linear-gradient(180deg,rgba(12,61,72,0.82),rgba(6,28,39,0.9))] px-design-8 text-left !text-design-12 text-[#A5EDF4] shadow-[inset_0_0_calc(var(--design-unit)*10)_rgba(94,237,255,0.08)] data-[size=default]:h-design-30 data-[placeholder]:text-[rgba(109,170,176,0.55)] [&_svg]:h-design-14 [&_svg]:w-design-14 [&_svg]:text-[#79DFEA]"
|
||||
aria-label={t('gameDesktop.withdraw.currencySelection')}
|
||||
>
|
||||
<SelectValue
|
||||
placeholder={t('gameDesktop.withdraw.selectCurrency')}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{vm.config.currencies.map((option) => (
|
||||
<SelectItem key={option.code} value={option.code}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</WithdrawField>
|
||||
|
||||
<WithdrawField
|
||||
label={t('gameDesktop.withdraw.fields.paymentChannel')}
|
||||
>
|
||||
<div className="flex w-full flex-col gap-design-3">
|
||||
{vm.sortedPayChannels.length > 0 ? (
|
||||
<div className="grid grid-cols-2 gap-design-5">
|
||||
{vm.sortedPayChannels.map((channel) => (
|
||||
<PaymentCard
|
||||
key={channel.code}
|
||||
active={channel.code === vm.paymentChannelCode}
|
||||
label={channel.name}
|
||||
glyph={getPaymentGlyph(channel.code, channel.name)}
|
||||
onClick={() => vm.setPaymentChannelCode(channel.code)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-design-38 items-center rounded-[calc(var(--design-unit)*6)] border border-[rgba(185,63,68,0.45)] bg-[rgba(34,13,16,0.6)] px-design-8 text-design-10 text-[#F4B1B1]">
|
||||
{t('gameDesktop.withdraw.errors.paymentChannelUnavailable')}
|
||||
</div>
|
||||
)}
|
||||
{hasSubmitted && vm.paymentChannelCodeError ? (
|
||||
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
|
||||
{t('gameDesktop.withdraw.errors.paymentChannelRequired')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</WithdrawField>
|
||||
|
||||
<WithdrawField label={t('gameDesktop.withdraw.fields.bankCode')}>
|
||||
<div className="flex w-full flex-col gap-design-3">
|
||||
<Select
|
||||
value={vm.bankCode}
|
||||
onValueChange={vm.setBankCode}
|
||||
disabled={vm.sortedBanks.length === 0}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
'h-design-30 w-full rounded-[calc(var(--design-unit)*5)] bg-[linear-gradient(180deg,rgba(12,61,72,0.82),rgba(6,28,39,0.9))] px-design-8 text-left !text-design-12 text-[#A5EDF4] shadow-[inset_0_0_calc(var(--design-unit)*10)_rgba(94,237,255,0.08)] data-[size=default]:h-design-30 data-[placeholder]:text-[rgba(109,170,176,0.55)] [&_svg]:h-design-14 [&_svg]:w-design-14 [&_svg]:text-[#79DFEA]',
|
||||
hasSubmitted && vm.bankCodeError
|
||||
? 'border-[#B93F44]'
|
||||
: 'border-[rgba(103,227,239,0.3)]',
|
||||
)}
|
||||
aria-label={t('gameDesktop.withdraw.fields.bankCode')}
|
||||
>
|
||||
<SelectValue
|
||||
placeholder={t(
|
||||
'gameDesktop.withdraw.placeholders.bankCode',
|
||||
)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{vm.sortedBanks.map((bank) => (
|
||||
<SelectItem key={bank.code} value={bank.code}>
|
||||
{bank.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{vm.sortedBanks.length === 0 ? (
|
||||
<div className="pl-design-2 text-design-10 text-[#F4B1B1]">
|
||||
{t('gameDesktop.withdraw.errors.bankCodeUnavailable')}
|
||||
</div>
|
||||
) : null}
|
||||
{hasSubmitted && vm.bankCodeError ? (
|
||||
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
|
||||
{t('gameDesktop.withdraw.errors.bankCodeRequired')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</WithdrawField>
|
||||
|
||||
<WithdrawField
|
||||
label={t('gameDesktop.withdraw.fields.cardHolderName')}
|
||||
>
|
||||
<InputShell
|
||||
value={vm.holderName}
|
||||
onChange={vm.setHolderName}
|
||||
placeholder={t(
|
||||
'gameDesktop.withdraw.placeholders.cardHolderName',
|
||||
)}
|
||||
error={hasSubmitted && vm.holderNameError}
|
||||
errorMessage={t(
|
||||
'gameDesktop.withdraw.errors.cardHolderNameRequired',
|
||||
)}
|
||||
/>
|
||||
</WithdrawField>
|
||||
|
||||
<WithdrawField
|
||||
label={t('gameDesktop.withdraw.fields.bankAccountNumber')}
|
||||
>
|
||||
<InputShell
|
||||
value={vm.bankAccount}
|
||||
onChange={vm.setBankAccount}
|
||||
placeholder={t(
|
||||
'gameDesktop.withdraw.placeholders.bankAccountNumber',
|
||||
)}
|
||||
error={hasSubmitted && vm.bankAccountError}
|
||||
errorMessage={t(
|
||||
'gameDesktop.withdraw.errors.bankAccountRequired',
|
||||
)}
|
||||
/>
|
||||
</WithdrawField>
|
||||
|
||||
<WithdrawField
|
||||
label={t('gameDesktop.withdraw.fields.receiverEmail')}
|
||||
>
|
||||
<InputShell
|
||||
value={vm.receiverEmail}
|
||||
onChange={vm.setReceiverEmail}
|
||||
placeholder={t(
|
||||
'gameDesktop.withdraw.placeholders.receiverEmail',
|
||||
)}
|
||||
type="email"
|
||||
error={hasSubmitted && vm.receiverEmailError}
|
||||
errorMessage={t(
|
||||
'gameDesktop.withdraw.errors.receiverEmailInvalid',
|
||||
)}
|
||||
/>
|
||||
</WithdrawField>
|
||||
|
||||
<WithdrawField
|
||||
label={t('gameDesktop.withdraw.fields.receiverPhone')}
|
||||
>
|
||||
<InputShell
|
||||
value={vm.receiverPhone}
|
||||
onChange={vm.setReceiverPhone}
|
||||
placeholder={t(
|
||||
'gameDesktop.withdraw.placeholders.receiverPhone',
|
||||
)}
|
||||
type="tel"
|
||||
error={hasSubmitted && vm.receiverPhoneError}
|
||||
errorMessage={t(
|
||||
'gameDesktop.withdraw.errors.receiverPhoneInvalid',
|
||||
)}
|
||||
/>
|
||||
</WithdrawField>
|
||||
|
||||
<div className="overflow-hidden rounded-[calc(var(--design-unit)*4)] border border-[rgba(89,209,223,0.22)] bg-[rgba(4,19,28,0.58)]">
|
||||
<PreviewRow
|
||||
label={t('gameDesktop.withdraw.preview.diamondAmount')}
|
||||
value={formatNumber(vm.amount)}
|
||||
/>
|
||||
<PreviewRow
|
||||
label={vm.selectedCurrencyPreview.exchangeRateLabel}
|
||||
value={vm.selectedCurrencyPreview.exchangeRateValue}
|
||||
/>
|
||||
<PreviewRow
|
||||
label={vm.selectedCurrencyPreview.convertibleLabel}
|
||||
value={vm.selectedCurrencyPreview.convertibleValue}
|
||||
highlight={true}
|
||||
/>
|
||||
<PreviewRow
|
||||
label={t(
|
||||
'gameDesktop.withdraw.preview.fixedExchangeDiamondAmount',
|
||||
)}
|
||||
value="0-0-0 0:0:0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[calc(var(--design-unit)*4)] border border-[rgba(240,175,66,0.2)] bg-[rgba(110,77,26,0.24)] px-design-8 py-design-6 text-design-10 leading-[1.3] text-[#F0B44A]">
|
||||
{vm.withdrawCopy.rateHint}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-design-3 px-design-1 text-design-10 uppercase leading-[1.3] text-[#7AD8E0]">
|
||||
<div>
|
||||
{vm.withdrawCopy.processingLabel}:{' '}
|
||||
<span className="text-[#77FF76]">
|
||||
{vm.withdrawCopy.processingValue}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
{vm.withdrawCopy.noticeLabel}:{' '}
|
||||
<span className="text-red-700">{vm.withdrawCopy.feeNote}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center justify-between gap-design-6 border-t border-[rgba(89,209,223,0.22)] bg-[rgba(3,15,24,0.86)] px-design-8 py-design-5">
|
||||
<SmartBackground
|
||||
as="button"
|
||||
type="button"
|
||||
src={lengthGreenBtn}
|
||||
size="100% 100%"
|
||||
onClick={handleCloseWithdraw}
|
||||
className="flex h-design-38 flex-1 cursor-pointer items-center justify-center pb-design-2 text-center !text-design-12 font-bold uppercase text-[#F0FFFF] transition hover:brightness-110 active:scale-[0.98]"
|
||||
>
|
||||
{t('gameDesktop.withdraw.cancel')}
|
||||
</SmartBackground>
|
||||
<SmartBackground
|
||||
as="button"
|
||||
type="button"
|
||||
src={lengthBlueBtn}
|
||||
size="100% 100%"
|
||||
onClick={handleConfirmWithdraw}
|
||||
disabled={withdrawSubmitMutation.isPending}
|
||||
className={cn(
|
||||
'flex h-design-38 flex-1 items-center justify-center whitespace-nowrap pb-design-2 text-center !text-design-12 font-bold uppercase leading-[1.05] text-[#F0FFFF] transition',
|
||||
withdrawSubmitMutation.isPending
|
||||
? 'cursor-not-allowed opacity-70'
|
||||
: 'cursor-pointer hover:brightness-110 active:scale-[0.98]',
|
||||
)}
|
||||
>
|
||||
{withdrawSubmitMutation.isPending
|
||||
? t('commonUi.action.submitting')
|
||||
: `${t('gameDesktop.withdraw.confirm')}`}
|
||||
</SmartBackground>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MobileWithdraw
|
||||
@@ -3,9 +3,11 @@ import dayjs from 'dayjs'
|
||||
import { RotateCw } from 'lucide-react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getNoticeList } from '@/api'
|
||||
import lengthGreenBtn from '@/assets/system/length-green-btn.webp'
|
||||
import checkIcon from '@/assets/system/right.webp'
|
||||
import { CenterModal } from '@/components/center-modal.tsx'
|
||||
import { MobileCenterModal } from '@/components/mobile-center-modal.tsx'
|
||||
import { SmartBackground } from '@/components/smart-background.tsx'
|
||||
import { SmartImage } from '@/components/smart-image.tsx'
|
||||
import { DataLoadingIndicator } from '@/components/ui/data-loading-indicator.tsx'
|
||||
@@ -13,7 +15,6 @@ import {
|
||||
ENTRY_NOTICE_CONFIRM_INTERVAL_MS,
|
||||
ENTRY_NOTICE_LAST_CONFIRMED_AT_KEY,
|
||||
} from '@/constants'
|
||||
import { getNoticeList } from '@/features/game/api'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
import { useModalStore } from '@/store/modal'
|
||||
@@ -36,8 +37,15 @@ function setLastConfirmedAt(storageKey: string, timestamp: number) {
|
||||
localStorage.setItem(storageKey, String(timestamp))
|
||||
}
|
||||
|
||||
export function EntryNoticeGateModal() {
|
||||
interface EntryNoticeGateModalProps {
|
||||
variant?: 'desktop' | 'mobile'
|
||||
}
|
||||
|
||||
export function EntryNoticeGateModal({
|
||||
variant = 'desktop',
|
||||
}: EntryNoticeGateModalProps = {}) {
|
||||
const { t } = useTranslation()
|
||||
const isMobile = variant === 'mobile'
|
||||
const authStatus = useAuthStore((state) => state.status)
|
||||
const authIsHydrated = useAuthStore((state) => state.isHydrated)
|
||||
const accessToken = useAuthStore((state) => state.accessToken)
|
||||
@@ -118,29 +126,64 @@ export function EntryNoticeGateModal() {
|
||||
return null
|
||||
}
|
||||
|
||||
const Modal = isMobile ? MobileCenterModal : CenterModal
|
||||
|
||||
return (
|
||||
<CenterModal
|
||||
<Modal
|
||||
open={shouldShowModal}
|
||||
isShowClose={false}
|
||||
isNormalBg={true}
|
||||
title={
|
||||
<div className="modal-title-glow text-design-26">
|
||||
<div
|
||||
className={cn(
|
||||
'modal-title-glow',
|
||||
isMobile ? 'text-design-16' : 'text-design-26',
|
||||
)}
|
||||
>
|
||||
{t('game.modals.entryNotice.title')}
|
||||
</div>
|
||||
}
|
||||
titleAlign="left"
|
||||
className="h-design-700 w-design-1000 max-h-[92vh] max-w-[92vw]"
|
||||
className={
|
||||
isMobile
|
||||
? 'h-design-400'
|
||||
: 'h-design-700 w-design-1000 max-h-[92vh] max-w-[92vw]'
|
||||
}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col gap-design-20 px-design-14 pb-design-30 pt-design-8">
|
||||
<div className="min-h-0 flex-1 overflow-y-auto rounded-md border border-[#2B8CA3]/45 bg-[#001B24]/70 p-design-18 shadow-[inset_0_0_calc(var(--design-unit)*18)_rgba(39,175,205,0.1)]">
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-full w-full flex-col',
|
||||
isMobile
|
||||
? 'gap-design-10 px-design-8 pb-design-8 pt-design-4'
|
||||
: 'gap-design-20 px-design-14 pb-design-30 pt-design-8',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'min-h-0 flex-1 overflow-y-auto rounded-md border border-[#2B8CA3]/45 bg-[#001B24]/70 shadow-[inset_0_0_calc(var(--design-unit)*18)_rgba(39,175,205,0.1)]',
|
||||
isMobile ? 'p-design-8' : 'p-design-18',
|
||||
)}
|
||||
>
|
||||
{noticeListQuery.isPending ? (
|
||||
<DataLoadingIndicator
|
||||
label={t('game.modals.entryNotice.loading')}
|
||||
className="h-full min-h-[calc(var(--design-unit)*320)]"
|
||||
className={cn(
|
||||
'h-full',
|
||||
isMobile
|
||||
? 'min-h-[calc(var(--design-unit)*220)] text-design-12'
|
||||
: 'min-h-[calc(var(--design-unit)*320)]',
|
||||
)}
|
||||
/>
|
||||
) : noticeListQuery.isError ? (
|
||||
<div className="flex h-full min-h-[calc(var(--design-unit)*320)] flex-col items-center justify-center gap-design-18 text-center text-[#9CE8F2]">
|
||||
<div className="text-design-22">
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-full flex-col items-center justify-center text-center text-[#9CE8F2]',
|
||||
isMobile
|
||||
? 'min-h-[calc(var(--design-unit)*220)] gap-design-10'
|
||||
: 'min-h-[calc(var(--design-unit)*320)] gap-design-18',
|
||||
)}
|
||||
>
|
||||
<div className={isMobile ? 'text-design-13' : 'text-design-22'}>
|
||||
{t('game.modals.entryNotice.loadFailed')}
|
||||
</div>
|
||||
<button
|
||||
@@ -148,30 +191,75 @@ export function EntryNoticeGateModal() {
|
||||
onClick={() => {
|
||||
void noticeListQuery.refetch()
|
||||
}}
|
||||
className="inline-flex items-center gap-design-8 rounded-md border border-[#4AC6DE]/45 bg-[#0B4454] px-design-18 py-design-10 text-design-18 text-[#D7FFFF] transition hover:bg-[#0E576D]"
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-md border border-[#4AC6DE]/45 bg-[#0B4454] text-[#D7FFFF] transition hover:bg-[#0E576D]',
|
||||
isMobile
|
||||
? 'gap-design-5 px-design-10 py-design-6 text-design-12'
|
||||
: 'gap-design-8 px-design-18 py-design-10 text-design-18',
|
||||
)}
|
||||
>
|
||||
<RotateCw className="h-design-18 w-design-18" />
|
||||
<RotateCw
|
||||
className={
|
||||
isMobile
|
||||
? 'h-design-13 w-design-13'
|
||||
: 'h-design-18 w-design-18'
|
||||
}
|
||||
/>
|
||||
{t('game.modals.entryNotice.retry')}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-design-16">
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col',
|
||||
isMobile ? 'gap-design-8' : 'gap-design-16',
|
||||
)}
|
||||
>
|
||||
{popoutNotices.map((notice, index) => (
|
||||
<article
|
||||
key={notice.notice_id}
|
||||
className="rounded-md border border-[#2B8CA3]/45 bg-[linear-gradient(180deg,rgba(9,63,78,0.96)_0%,rgba(6,42,53,0.98)_100%)] p-design-20"
|
||||
className={cn(
|
||||
'rounded-md border border-[#2B8CA3]/45 bg-[linear-gradient(180deg,rgba(9,63,78,0.96)_0%,rgba(6,42,53,0.98)_100%)]',
|
||||
isMobile ? 'p-design-9' : 'p-design-20',
|
||||
)}
|
||||
>
|
||||
<div className="mb-design-12 flex flex-wrap items-center justify-between gap-design-12">
|
||||
<div className="min-w-0 flex-1 text-design-24 font-semibold leading-tight text-white">
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-wrap items-center justify-between',
|
||||
isMobile
|
||||
? 'mb-design-6 gap-design-6'
|
||||
: 'mb-design-12 gap-design-12',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'min-w-0 flex-1 font-semibold leading-tight text-white',
|
||||
isMobile ? 'text-design-13' : 'text-design-24',
|
||||
)}
|
||||
>
|
||||
{index + 1}. {notice.title}
|
||||
</div>
|
||||
<div className="rounded-full border border-[#51BCD1]/35 bg-[#0A4252]/80 px-design-12 py-design-5 text-design-15 text-[#9CE8F2]">
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-full border border-[#51BCD1]/35 bg-[#0A4252]/80 text-[#9CE8F2]',
|
||||
isMobile
|
||||
? 'px-design-7 py-design-3 text-design-9'
|
||||
: 'px-design-12 py-design-5 text-design-15',
|
||||
)}
|
||||
>
|
||||
{dayjs(notice.publish_time * 1000).format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap text-design-18 leading-[1.8] text-[#C4F2F7]">
|
||||
<div
|
||||
className={cn(
|
||||
'whitespace-pre-wrap text-[#C4F2F7]',
|
||||
isMobile
|
||||
? 'text-design-11 leading-[1.55]'
|
||||
: 'text-design-18 leading-[1.8]',
|
||||
)}
|
||||
>
|
||||
{notice.content ?? ''}
|
||||
</div>
|
||||
</article>
|
||||
@@ -180,8 +268,20 @@ export function EntryNoticeGateModal() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 flex-col items-center justify-center gap-design-20">
|
||||
<label className="inline-flex cursor-pointer items-center justify-center gap-design-12 text-design-20 text-[#C4F2F7]">
|
||||
<div
|
||||
className={cn(
|
||||
'flex shrink-0 flex-col items-center justify-center',
|
||||
isMobile ? 'gap-design-8' : 'gap-design-20',
|
||||
)}
|
||||
>
|
||||
<label
|
||||
className={cn(
|
||||
'inline-flex cursor-pointer items-center justify-center text-[#C4F2F7]',
|
||||
isMobile
|
||||
? 'gap-design-6 text-design-11'
|
||||
: 'gap-design-12 text-design-20',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={hasAgreed}
|
||||
@@ -192,7 +292,10 @@ export function EntryNoticeGateModal() {
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
'flex h-design-32 w-design-32 shrink-0 items-center justify-center rounded-[calc(var(--design-unit)*5)] border transition',
|
||||
'flex shrink-0 items-center justify-center border transition',
|
||||
isMobile
|
||||
? 'h-design-20 w-design-20 rounded-[calc(var(--design-unit)*4)]'
|
||||
: 'h-design-32 w-design-32 rounded-[calc(var(--design-unit)*5)]',
|
||||
hasAgreed
|
||||
? 'border-[#4AFF49]/80 bg-[#071F11]'
|
||||
: 'border-[#6CCDCF]/70 bg-[#031D25]',
|
||||
@@ -204,7 +307,12 @@ export function EntryNoticeGateModal() {
|
||||
alt=""
|
||||
priority={true}
|
||||
showSkeleton={false}
|
||||
className="h-design-34 w-design-38 overflow-visible"
|
||||
className={cn(
|
||||
'overflow-visible',
|
||||
isMobile
|
||||
? 'h-design-22 w-design-24'
|
||||
: 'h-design-34 w-design-38',
|
||||
)}
|
||||
imgClassName="object-contain"
|
||||
/>
|
||||
) : null}
|
||||
@@ -230,7 +338,10 @@ export function EntryNoticeGateModal() {
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'flex h-design-72 w-design-270 items-center justify-center rounded-md pb-design-5 text-design-22 font-bold transition',
|
||||
'flex items-center justify-center rounded-md font-bold transition',
|
||||
isMobile
|
||||
? 'h-design-42 w-design-156 pb-design-3 text-design-14'
|
||||
: 'h-design-72 w-design-270 pb-design-5 text-design-22',
|
||||
canEnter
|
||||
? 'modal-title-glow cursor-pointer text-white hover:brightness-110 active:brightness-95'
|
||||
: 'cursor-not-allowed text-white opacity-80 grayscale',
|
||||
@@ -248,6 +359,10 @@ export function EntryNoticeGateModal() {
|
||||
</SmartBackground>
|
||||
</div>
|
||||
</div>
|
||||
</CenterModal>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export function MobileEntryNoticeGateModal() {
|
||||
return <EntryNoticeGateModal variant="mobile" />
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DataLoadingIndicator } from '@/components/ui/data-loading-indicator'
|
||||
import type { PeriodHistoryDisplayItem } from '@/features/game/hooks/use-period-history-vm'
|
||||
import type { PeriodHistoryDisplayItem } from '@/hooks/use-period-history-vm'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface PeriodHistoryListLabels {
|
||||
|
||||
Reference in New Issue
Block a user