feat: 精简玩家端页面结构并支持局域网开发热更新
Some checks failed
lotteryfront CI / build (push) Has been cancelled

- 合并钱包、订单、开奖、通知等独立 screen 到页面层,减少重复壳组件
- 优化订单列表/详情、钱包划转与待对账展示、开奖核对与结果详情交互
- 改进入口 gate、移动端视口与下拉刷新在窄屏下的表现
- dev 绑定 0.0.0.0 并默认放行 192.168/10 网段,修复局域网 HMR WebSocket
This commit is contained in:
2026-06-26 14:10:47 +08:00
parent dd1155978a
commit e5622c58cb
51 changed files with 1184 additions and 1335 deletions

View File

@@ -1,57 +0,0 @@
"use client";
import { useRouter } from "next/navigation";
import { useCallback, useEffect } from "react";
import { useSWRConfig } from "swr";
import { getWalletBalance } from "@/api/wallet";
import { TransferInPage } from "@/features/wallet/wallet-transfer-forms";
import { WalletTransferCreditGuard } from "@/features/wallet/wallet-transfer-credit-guard";
import { WalletTransferLoadingPanel } from "@/features/wallet/wallet-transfer-loading-panel";
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
import { useApiQuery } from "@/hooks/use-api-query";
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
const BALANCE_KEY = (currency: string) => ["wallet/balance", currency];
/** 独立路由 `/wallet/transfer-in` */
export function TransferInScreen() {
const router = useRouter();
const { activeCurrency: currency } = useActivePlayerCurrency();
const { mutate } = useSWRConfig();
const { data: balance, isLoading: loading } = useApiQuery(
BALANCE_KEY(currency),
() => getWalletBalance({ currency }),
);
useEffect(() => {
const onRefresh = () => void mutate(BALANCE_KEY(currency));
window.addEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onRefresh);
return () => window.removeEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onRefresh);
}, [mutate, currency]);
const onSuccess = useCallback(async () => {
await mutate(BALANCE_KEY(currency));
router.push("/wallet");
}, [mutate, currency, router]);
if (loading && !balance) {
return <WalletTransferLoadingPanel titleKey="wallet.transferInTitle" />;
}
return (
<WalletTransferCreditGuard balance={balance} loading={loading}>
<TransferInPage
currency={currency}
lotteryMinor={Number(balance?.balance ?? 0)}
mainMinor={
balance?.main_balance === null || balance?.main_balance === undefined
? null
: Number(balance.main_balance)
}
onSuccess={onSuccess}
/>
</WalletTransferCreditGuard>
);
}

View File

@@ -1,52 +0,0 @@
"use client";
import { useRouter } from "next/navigation";
import { useCallback, useEffect } from "react";
import { useSWRConfig } from "swr";
import { getWalletBalance } from "@/api/wallet";
import { TransferOutPage } from "@/features/wallet/wallet-transfer-forms";
import { WalletTransferCreditGuard } from "@/features/wallet/wallet-transfer-credit-guard";
import { WalletTransferLoadingPanel } from "@/features/wallet/wallet-transfer-loading-panel";
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
import { useApiQuery } from "@/hooks/use-api-query";
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
const BALANCE_KEY = (currency: string) => ["wallet/balance", currency];
/** 独立路由 `/wallet/transfer-out` */
export function TransferOutScreen() {
const router = useRouter();
const { activeCurrency: currency } = useActivePlayerCurrency();
const { mutate } = useSWRConfig();
const { data: balance, isLoading: loading } = useApiQuery(
BALANCE_KEY(currency),
() => getWalletBalance({ currency }),
);
useEffect(() => {
const onRefresh = () => void mutate(BALANCE_KEY(currency));
window.addEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onRefresh);
return () => window.removeEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onRefresh);
}, [mutate, currency]);
const onSuccess = useCallback(async () => {
await mutate(BALANCE_KEY(currency));
router.push("/wallet");
}, [mutate, currency, router]);
if (loading && !balance) {
return <WalletTransferLoadingPanel titleKey="wallet.transferOutTitle" />;
}
return (
<WalletTransferCreditGuard balance={balance} loading={loading}>
<TransferOutPage
currency={currency}
availableMinor={Number(balance?.available_balance ?? 0)}
onSuccess={onSuccess}
/>
</WalletTransferCreditGuard>
);
}

View File

@@ -241,24 +241,14 @@ export function LogRow({
</div>
<div className="mt-3 flex items-center justify-between rounded-xl bg-[#f8fbff] px-3 py-2 text-xs">
{creditMode && item.affects_available_credit === false ? (
<span className="font-medium text-slate-500">
{t("wallet.creditPaymentRecordOnly", {
defaultValue: "账期收付记账,不计入可用信用",
})}
</span>
) : (
<>
<span className="font-semibold text-slate-500">
{creditMode ? t("wallet.creditAvailableAfter") : t("wallet.balanceAfter")}
</span>
<span className="font-mono font-black tabular-nums text-[#32518d]">
{item.balance_after != null
? formatMinorAsCurrency(item.balance_after, ccy)
: "—"}
</span>
</>
)}
<span className="font-semibold text-slate-500">
{creditMode ? t("wallet.creditAvailableAfter") : t("wallet.balanceAfter")}
</span>
<span className="font-mono font-black tabular-nums text-[#32518d]">
{item.balance_after != null
? formatMinorAsCurrency(item.balance_after, ccy)
: "—"}
</span>
</div>
</li>
);

View File

@@ -1,151 +0,0 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { getWalletBalance, getWalletLogs } from "@/api/wallet";
import { Button } from "@/components/ui/button";
import { PlayerPanel } from "@/components/layout/player-panel";
import { WalletLogsBlock } from "@/features/wallet/wallet-logs-block";
import { dispatchWalletLogsRefresh } from "@/hooks/use-pending-wallet-reconcile";
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
import { formatWalletClientError } from "@/lib/wallet-api-error";
import { usePlayerSessionStore } from "@/stores/player-session-store";
import { getWalletLogsLastPage, type WalletLogsData } from "@/types/api/wallet-logs";
const WALLET_LOGS_PAGE_SIZE = 10;
export function WalletLogsScreen() {
const { activeCurrency: currency } = useActivePlayerCurrency();
const { t } = useTranslation("player");
const [logs, setLogs] = useState<WalletLogsData | null>(null);
const [filter, setFilter] = useState("");
const [loading, setLoading] = useState(true);
const [logsLoading, setLogsLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [error, setError] = useState<string | null>(null);
const profile = usePlayerSessionStore((s) => s.profile);
const [creditMode, setCreditMode] = useState(() => isCreditFundingPlayer(profile));
const loadMoreRef = useRef<HTMLDivElement | null>(null);
const fetchPassRef = useRef(true);
const load = useCallback(async (targetPage = 1, append = false) => {
setError(null);
if (append) {
setLoadingMore(true);
} else if (fetchPassRef.current) {
setLoading(true);
fetchPassRef.current = false;
} else {
setLogsLoading(true);
}
try {
const balance = await getWalletBalance({ currency });
setCreditMode(isCreditFundingPlayer(balance));
const nextLogs = await getWalletLogs({
page: targetPage,
size: WALLET_LOGS_PAGE_SIZE,
type: filter || undefined,
currency,
});
setLogs((current) =>
append && current
? { ...nextLogs, items: [...current.items, ...nextLogs.items] }
: nextLogs,
);
dispatchWalletLogsRefresh(nextLogs.pending_reconcile ?? []);
} catch (e) {
setError(formatWalletClientError(e, t));
if (!append) {
setLogs(null);
}
} finally {
setLoading(false);
setLogsLoading(false);
setLoadingMore(false);
}
}, [currency, filter, t]);
useEffect(() => {
queueMicrotask(() => {
void load(1, false);
});
}, [currency, load]);
useEffect(() => {
const onCurrencyChange = () => void load(1, false);
window.addEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onCurrencyChange);
return () => window.removeEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onCurrencyChange);
}, [load]);
const hasMore = logs ? logs.page < getWalletLogsLastPage(logs) : false;
const loadMore = useCallback(() => {
if (!logs || !hasMore || loadingMore) return;
void load(logs.page + 1, true);
}, [hasMore, load, loadingMore, logs]);
useEffect(() => {
const target = loadMoreRef.current;
if (!target || loading || logsLoading || loadingMore || !hasMore) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry?.isIntersecting) {
loadMore();
}
},
{ rootMargin: "160px" },
);
observer.observe(target);
return () => observer.disconnect();
}, [hasMore, loadMore, loading, loadingMore, logsLoading]);
return (
<PlayerPanel
title={
creditMode
? t("wallet.creditLogsTitle", { defaultValue: "信用流水" })
: t("wallet.logsTitle")
}
backHref="/wallet"
backLabel={
creditMode
? t("wallet.creditTitle", { defaultValue: "信用" })
: t("wallet.title")
}
>
<div className="space-y-3">
{error ? (
<div className="rounded-xl border border-red-200 bg-red-50 px-3 py-3 text-sm text-red-700">
<p>{error}</p>
<Button
type="button"
className="mt-3 bg-[#e5002c] text-white hover:bg-[#d10028]"
onClick={() => void load()}
>
{t("actions.retry")}
</Button>
</div>
) : null}
<WalletLogsBlock
logs={logs}
logsLoading={loading || logsLoading}
loadingMore={loadingMore}
hasMore={hasMore}
onLoadMore={loadMore}
loadMoreRef={loadMoreRef}
filter={filter}
onFilterChange={setFilter}
currency={currency}
creditMode={creditMode}
/>
</div>
</PlayerPanel>
);
}

View File

@@ -1,45 +0,0 @@
"use client";
import Link from "next/link";
import { AlertTriangle } from "lucide-react";
import { useTranslation } from "react-i18next";
import { usePendingWalletReconcile } from "@/hooks/use-pending-wallet-reconcile";
/** 钱包盘:顶栏铃铛隐藏后,在钱包页展示待对账提醒入口 */
export function WalletPendingReconcileBanner() {
const { t } = useTranslation("player");
const { pending, unreadCount, hasPending } = usePendingWalletReconcile();
if (!hasPending) {
return null;
}
return (
<div className="rounded-xl border border-amber-200 bg-amber-50 px-3 py-3 text-sm text-amber-950">
<div className="flex items-start gap-2">
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-amber-600" aria-hidden />
<div className="min-w-0 flex-1">
<p className="font-bold text-amber-900">{t("wallet.pendingTitle")}</p>
<p className="mt-1 text-xs leading-relaxed text-amber-950/85">
{t("wallet.pendingDescription")}
</p>
{unreadCount > 0 ? (
<p className="mt-1.5 text-xs font-semibold text-amber-800">
{t("notifications.unreadCount", { count: unreadCount })}
</p>
) : null}
<Link
href="/notifications"
className="mt-2 inline-flex text-xs font-bold text-[#0b56b7] underline-offset-2 hover:underline"
>
{t("wallet.viewPendingReconcile", {
defaultValue: "查看待对账详情({{count}}",
count: pending.length,
})}
</Link>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,153 @@
"use client";
import { BellRing, CheckCheck } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { usePendingWalletReconcile } from "@/hooks/use-pending-wallet-reconcile";
import { formatPlayerInstant } from "@/lib/player-datetime";
import { formatMinorAsCurrency } from "@/lib/money";
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
import {
pendingReconcileDescriptionKey,
pendingReconcileTitleKey,
} from "@/lib/pending-reconcile-notification";
import { usePlayerSessionStore } from "@/stores/player-session-store";
import { cn } from "@/lib/utils";
type WalletPendingReconcileSectionProps = {
onViewLogs?: () => void;
};
/** 钱包页内嵌:待对账提醒(原独立 /notifications 页) */
export function WalletPendingReconcileSection({
onViewLogs,
}: WalletPendingReconcileSectionProps) {
const { t } = useTranslation("player");
const creditMode = isCreditFundingPlayer(usePlayerSessionStore((state) => state.profile));
const { pending, unreadPending, unreadCount, loading, markAsRead, markAllAsRead } =
usePendingWalletReconcile();
const unreadSet = new Set(unreadPending.map((item) => item.transfer_no));
if (creditMode) {
return null;
}
return (
<section id="wallet-pending" className="scroll-mt-3 space-y-3">
<div className="flex items-center justify-between rounded-xl border border-[#dce7f7] bg-[#f8fbff] px-3 py-2.5">
<div className="flex items-center gap-2">
<BellRing className="size-4 text-[#0b56b7]" aria-hidden />
<p className="text-sm font-semibold text-[#0b3f96]">
{t("notifications.title")}
{unreadCount > 0 ? (
<span className="ml-1.5 text-xs font-bold text-amber-700">
({t("notifications.unreadCount", { count: unreadCount })})
</span>
) : null}
</p>
</div>
<Button
type="button"
size="sm"
variant="ghost"
className="h-8 px-2 text-xs font-bold text-[#0b56b7] hover:bg-[#ebf2ff]"
onClick={markAllAsRead}
disabled={pending.length === 0 || unreadCount === 0}
>
<CheckCheck className="mr-1 size-3.5" aria-hidden />
{t("notifications.markAllRead")}
</Button>
</div>
{loading && pending.length === 0 ? (
<div className="rounded-xl border border-[#dce7f7] bg-white px-4 py-6 text-center text-sm text-slate-500">
{t("actions.loading")}
</div>
) : null}
{!loading && pending.length === 0 ? (
<div className="rounded-xl border border-dashed border-[#dce7f7] bg-white px-4 py-8 text-center">
<p className="text-sm text-slate-500">{t("notifications.empty")}</p>
</div>
) : null}
{pending.length > 0 ? (
<ul className="space-y-2">
{pending.map((item) => {
const cardRead = !unreadSet.has(item.transfer_no);
return (
<li
key={item.transfer_no}
className={cn(
"rounded-xl border px-3 py-3 transition-colors",
cardRead
? "border-[#e4eaf4] bg-white"
: "border-amber-200 bg-amber-50/80",
)}
>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<p className="text-sm font-bold text-amber-900">
{t(pendingReconcileTitleKey(item.type))}
</p>
<p className="mt-0.5 text-xs text-slate-500">
{formatPlayerInstant(item.created_at)}
</p>
</div>
<div className="flex shrink-0 flex-col items-end gap-1">
<span className="rounded-full bg-amber-100 px-2 py-0.5 text-[11px] font-bold text-amber-800">
{t("notifications.pendingBadge")}
</span>
<span
className={cn(
"text-[11px] font-semibold",
cardRead ? "text-slate-400" : "text-amber-700",
)}
>
{cardRead ? t("notifications.read") : t("notifications.unread")}
</span>
</div>
</div>
<p className="mt-2 text-xs leading-relaxed text-amber-950/85">
{t(pendingReconcileDescriptionKey(item.type))}
</p>
<p className="mt-2 text-sm text-slate-700">
<span className="text-xs font-medium text-slate-500">
{t("notifications.amountLabel")}{" "}
</span>
{formatMinorAsCurrency(item.amount, item.currency_code)}
</p>
<div className="mt-3 flex items-center gap-2">
<Button
type="button"
size="sm"
variant="outline"
className="h-8 rounded-full border-[#dce7f7] px-3 text-xs font-semibold text-[#0b56b7]"
onClick={() => markAsRead(item.transfer_no)}
>
{t("notifications.markRead")}
</Button>
<Button
type="button"
size="sm"
className="h-8 rounded-full bg-[#07459f] px-3 text-xs font-semibold text-white hover:bg-[#063b88]"
onClick={() => {
markAsRead(item.transfer_no);
onViewLogs?.();
}}
>
{t("notifications.viewLogs")}
</Button>
</div>
</li>
);
})}
</ul>
) : null}
</section>
);
}

View File

@@ -2,6 +2,7 @@
import { Wallet } from "lucide-react";
import Image from "next/image";
import { useRouter, useSearchParams } from "next/navigation";
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -13,7 +14,7 @@ import {
TransferInDialog,
TransferOutDialog,
} from "@/features/wallet/wallet-transfer-dialogs";
import { WalletPendingReconcileBanner } from "@/features/wallet/wallet-pending-reconcile-banner";
import { WalletPendingReconcileSection } from "@/features/wallet/wallet-pending-reconcile-section";
import { PlayerMoneyDisplay } from "@/components/player-money-display";
import { WalletLogsBlock } from "@/features/wallet/wallet-logs-block";
import { dispatchWalletLogsRefresh } from "@/hooks/use-pending-wallet-reconcile";
@@ -28,9 +29,18 @@ import { getWalletLogsLastPage, type WalletLogsData } from "@/types/api/wallet-l
const WALLET_LOGS_PAGE_SIZE = 10;
function scrollToWalletSection(id: string): void {
if (typeof document === "undefined") return;
document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" });
}
export function WalletScreen() {
const router = useRouter();
const searchParams = useSearchParams();
const { activeCurrency: currency } = useActivePlayerCurrency();
const { t } = useTranslation("player");
const [transferInOpen, setTransferInOpen] = useState(false);
const [transferOutOpen, setTransferOutOpen] = useState(false);
const [balance, setBalance] = useState<WalletBalanceData | null>(null);
const [logs, setLogs] = useState<WalletLogsData | null>(null);
const [filter, setFilter] = useState("");
@@ -39,8 +49,47 @@ export function WalletScreen() {
const [loadingMore, setLoadingMore] = useState(false);
const [error, setError] = useState<string | null>(null);
const loadMoreRef = useRef<HTMLDivElement | null>(null);
const filterInitializedRef = useRef(false);
const prevFilterRef = useRef("");
const actionDeepLinkHandledRef = useRef(false);
const sectionDeepLinkHandledRef = useRef(false);
const profile = usePlayerSessionStore((s) => s.profile);
const isCreditPlayer = isCreditFundingPlayer(balance) || isCreditFundingPlayer(profile);
useEffect(() => {
if (actionDeepLinkHandledRef.current || loading) return;
const action = searchParams.get("action");
if (action !== "transfer-in" && action !== "transfer-out") return;
actionDeepLinkHandledRef.current = true;
if (!isCreditPlayer) {
if (action === "transfer-in") {
setTransferInOpen(true);
} else {
setTransferOutOpen(true);
}
}
router.replace("/wallet", { scroll: false });
}, [isCreditPlayer, loading, router, searchParams]);
useEffect(() => {
if (sectionDeepLinkHandledRef.current || loading) return;
const section = searchParams.get("section");
if (section !== "logs" && section !== "pending") return;
sectionDeepLinkHandledRef.current = true;
const targetId =
section === "logs" || (section === "pending" && isCreditPlayer)
? "wallet-logs"
: "wallet-pending";
const timer = window.setTimeout(() => {
scrollToWalletSection(targetId);
router.replace("/wallet", { scroll: false });
}, 120);
return () => window.clearTimeout(timer);
}, [isCreditPlayer, loading, router, searchParams]);
const loadLogs = useCallback(async (targetPage = 1, append = false) => {
const nextLogs = await getWalletLogs({
@@ -164,8 +213,6 @@ export function WalletScreen() {
const hasMore = logs ? logs.page < getWalletLogsLastPage(logs) : false;
const profile = usePlayerSessionStore((s) => s.profile);
const isCreditPlayer = isCreditFundingPlayer(balance) || isCreditFundingPlayer(profile);
const displayMinor = isCreditPlayer
? Number(balance?.available_balance ?? 0)
: Number(balance?.balance ?? 0);
@@ -261,17 +308,8 @@ export function WalletScreen() {
</div>
</section>
{isCreditPlayer ? (
<p className="rounded-xl border border-[#d6e4ff] bg-[#f5f9ff] px-3 py-3 text-sm text-[#0b3f96]/85">
{t("wallet.creditNoTransferHint", {
defaultValue:
"由代理授信,无需主站转入转出;中奖不即时派彩,盈亏在账期统一结算,额度调整请联系代理。",
})}
</p>
) : (
<>
<WalletPendingReconcileBanner />
<div className="grid grid-cols-2 gap-3">
{!isCreditPlayer ? (
<div className="grid grid-cols-2 gap-3">
<TransferInDialog
idPrefix="wallet-"
currency={currency}
@@ -285,6 +323,8 @@ export function WalletScreen() {
triggerVariant="hall"
triggerLabel={t("wallet.transferIn", { defaultValue: "Transfer In" })}
triggerClassName="h-14 rounded-2xl text-base font-black"
open={transferInOpen}
onOpenChange={setTransferInOpen}
/>
<TransferOutDialog
idPrefix="wallet-"
@@ -294,11 +334,17 @@ export function WalletScreen() {
triggerVariant="hall"
triggerLabel={t("wallet.transferOut", { defaultValue: "Transfer Out" })}
triggerClassName="h-14 rounded-2xl text-base font-black"
open={transferOutOpen}
onOpenChange={setTransferOutOpen}
/>
</div>
</>
)}
) : null}
<WalletPendingReconcileSection
onViewLogs={() => scrollToWalletSection("wallet-logs")}
/>
<div id="wallet-logs" className="scroll-mt-3">
<WalletLogsBlock
creditMode={isCreditPlayer}
logs={logs}
@@ -311,6 +357,7 @@ export function WalletScreen() {
onFilterChange={setFilter}
currency={currency}
/>
</div>
</div>
</PlayerPanel>
);

View File

@@ -2,9 +2,6 @@
import { useRouter } from "next/navigation";
import { useEffect, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { PlayerPanel } from "@/components/layout/player-panel";
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
import type { WalletBalanceData } from "@/types/api/wallet-balance";
@@ -21,7 +18,6 @@ export function WalletTransferCreditGuard({
children,
}: WalletTransferCreditGuardProps) {
const router = useRouter();
const { t } = useTranslation("player");
useEffect(() => {
if (loading || !balance) {
@@ -37,20 +33,7 @@ export function WalletTransferCreditGuard({
}
if (isCreditFundingPlayer(balance)) {
return (
<PlayerPanel
title={t("wallet.creditTitle", { defaultValue: "信用" })}
backHref="/wallet"
backLabel={t("wallet.creditTitle", { defaultValue: "信用" })}
>
<p className="rounded-xl border border-[#d6e4ff] bg-[#f5f9ff] px-3 py-3 text-sm text-[#0b3f96]/85">
{t("wallet.creditNoTransferHint", {
defaultValue:
"由代理授信,无需主站转入转出;中奖不即时派彩,盈亏在账期统一结算,额度调整请联系代理。",
})}
</p>
</PlayerPanel>
);
return null;
}
return <>{children}</>;

View File

@@ -1,7 +1,7 @@
"use client";
import { ArrowDownLeft, ArrowUpRight } from "lucide-react";
import { useState } from "react";
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
@@ -23,6 +23,8 @@ type BaseProps = {
onSuccess: () => Promise<void>;
/** 避免同页多实例 input id 冲突 */
idPrefix?: string;
open?: boolean;
onOpenChange?: (open: boolean) => void;
};
export function TransferInDialog({
@@ -34,6 +36,8 @@ export function TransferInDialog({
triggerClassName,
triggerVariant = "wallet",
triggerLabel,
open: controlledOpen,
onOpenChange: controlledOnOpenChange,
}: BaseProps & {
lotteryMinor: number;
mainMinor?: number | null;
@@ -41,7 +45,19 @@ export function TransferInDialog({
triggerVariant?: "wallet" | "hall";
triggerLabel?: string;
}) {
const [open, setOpen] = useState(false);
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
const isControlled = controlledOpen !== undefined;
const open = isControlled ? controlledOpen : uncontrolledOpen;
const setOpen = useCallback(
(next: boolean) => {
if (isControlled) {
controlledOnOpenChange?.(next);
} else {
setUncontrolledOpen(next);
}
},
[controlledOnOpenChange, isControlled],
);
const { t } = useTranslation("player");
const resolvedTriggerLabel = triggerLabel ?? t("wallet.transferIn");
@@ -94,13 +110,27 @@ export function TransferOutDialog({
triggerClassName,
triggerVariant = "wallet",
triggerLabel,
open: controlledOpen,
onOpenChange: controlledOnOpenChange,
}: BaseProps & {
availableMinor: number;
triggerClassName?: string;
triggerVariant?: "wallet" | "hall";
triggerLabel?: string;
}) {
const [open, setOpen] = useState(false);
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
const isControlled = controlledOpen !== undefined;
const open = isControlled ? controlledOpen : uncontrolledOpen;
const setOpen = useCallback(
(next: boolean) => {
if (isControlled) {
controlledOnOpenChange?.(next);
} else {
setUncontrolledOpen(next);
}
},
[controlledOnOpenChange, isControlled],
);
const { t } = useTranslation("player");
const resolvedTriggerLabel = triggerLabel ?? t("wallet.transferOut");