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

@@ -0,0 +1,29 @@
"use client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { getDrawResults } from "@/api/draw";
/** 旧 `/results/check` 路由:跳转到最新期详情并展开查奖面板 */
export function CheckWinningRedirect() {
const router = useRouter();
useEffect(() => {
void (async () => {
try {
const res = await getDrawResults({ page: 1, size: 1 });
const drawNo = res.items[0]?.draw_no;
if (drawNo) {
router.replace(`/results/${encodeURIComponent(drawNo)}?check=1`);
return;
}
} catch {
/* 回落到列表 */
}
router.replace("/results");
})();
}, [router]);
return null;
}

View File

@@ -1,310 +1,31 @@
"use client";
import Link from "next/link";
import { BriefcaseBusiness, CheckCircle2, Clock3, RefreshCw, XIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { getDrawResults } from "@/api/draw";
import { getTicketDrawMyMatch, getTicketItems } from "@/api/ticket-items";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { PlayerPanel } from "@/components/layout/player-panel";
import { useCurrencyCatalog } from "@/hooks/use-currency-catalog";
import { formatMinorAsCurrency } from "@/lib/money";
import { formatPlayerInstant } from "@/lib/player-datetime";
import { playLabel } from "@/lib/play-labels";
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
import type { DrawResultListItem } from "@/types/api/draw-results";
import type { TicketDrawMyMatchPayload, TicketItemListRow } from "@/types/api/ticket-items";
type WinningCheckResult = {
draw: DrawResultListItem;
match: TicketDrawMyMatchPayload;
tickets: TicketItemListRow[];
};
import { Skeleton } from "@/components/ui/skeleton";
import { DrawWinningCheckPanel } from "@/features/results/draw-winning-check-panel";
/** 保留组件供内嵌使用;独立路由已 redirect 至期号详情 */
export function CheckWinningScreen() {
const { t } = useTranslation("player");
useCurrencyCatalog();
const [ticketNo, setTicketNo] = useState("");
const [latestDraw, setLatestDraw] = useState<DrawResultListItem | null>(null);
const [recent, setRecent] = useState<string[]>([]);
const [result, setResult] = useState<WinningCheckResult | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [drawNo, setDrawNo] = useState<string | null>(null);
useEffect(() => {
queueMicrotask(() => {
void (async () => {
try {
const res = await getDrawResults({ page: 1, size: 1 });
setLatestDraw(res.items[0] ?? null);
} catch {
setLatestDraw(null);
}
})();
});
void getDrawResults({ page: 1, size: 1 })
.then((res) => setDrawNo(res.items[0]?.draw_no ?? null))
.catch(() => setDrawNo(null));
}, []);
const normalizedTicketNo = ticketNo.trim();
const runCheck = useCallback(async () => {
if (!latestDraw || normalizedTicketNo === "") {
return;
}
setLoading(true);
setError(null);
try {
const [match, tickets] = await Promise.all([
getTicketDrawMyMatch(latestDraw.draw_no),
getTicketItems({
draw_no: latestDraw.draw_no,
number: normalizedTicketNo,
per_page: 10,
page: 1,
}),
]);
const next = {
draw: latestDraw,
match,
tickets: tickets.items,
};
setResult(next);
setRecent((current) => [normalizedTicketNo, ...current.filter((x) => x !== normalizedTicketNo)].slice(0, 5));
} catch {
setError(t("results.check.loadFailed"));
} finally {
setLoading(false);
}
}, [latestDraw, normalizedTicketNo, t]);
return (
<PlayerPanel title={t("results.check.title")} backHref="/results" backLabel={t("results.title")}>
<div className="space-y-3">
<section className="overflow-hidden rounded-2xl border border-red-100 bg-white shadow-[0_12px_32px_rgba(15,23,42,0.06)]">
<div className="bg-gradient-to-b from-red-50 to-white px-5 pb-5 pt-8 text-center">
<div className="mx-auto flex size-24 items-center justify-center rounded-full bg-white text-[#e5002c] shadow-[0_18px_40px_rgba(229,0,44,0.14)]">
<BriefcaseBusiness className="size-12" />
</div>
<h2 className="mt-5 text-lg font-black text-slate-950">
{t("results.check.enterTicket")}
</h2>
<p className="mx-auto mt-2 max-w-xs text-sm leading-relaxed text-slate-500">
{t("results.check.description")}
</p>
</div>
<div className="space-y-2.5 px-3 pb-3">
<label className="block space-y-1.5">
<span className="text-xs font-black text-slate-700">
{t("results.check.ticketNumber")}
</span>
<Input
value={ticketNo}
placeholder={t("results.check.placeholder")}
onChange={(e) => setTicketNo(e.target.value)}
className="h-12 rounded-xl border-[#dce7f7] bg-white font-mono text-base font-bold"
/>
</label>
{latestDraw ? (
<p className="text-xs text-slate-500">
{t("results.check.latestDraw", { drawNo: latestDraw.draw_no })}
</p>
) : null}
{error ? <p className="text-sm font-semibold text-[#e5002c]">{error}</p> : null}
<Button
type="button"
disabled={!latestDraw || normalizedTicketNo === "" || loading}
onClick={() => void runCheck()}
className="h-12 w-full rounded-xl bg-[#e5002c] text-base font-black text-white hover:bg-[#d10028]"
>
{loading ? t("results.check.loading") : t("results.check.submit")}
</Button>
</div>
</section>
<section className="rounded-2xl border border-[#dfe8f6] bg-white p-4 shadow-[0_10px_26px_rgba(15,23,42,0.05)]">
<div className="flex items-center justify-between">
<h3 className="font-black text-slate-950">
{t("results.check.recent")}
</h3>
{recent.length > 0 ? (
<button type="button" className="text-sm font-bold text-[#0b56b7]" onClick={() => setRecent([])}>
{t("actions.clear")}
</button>
) : null}
</div>
<div className="mt-3 divide-y divide-[#edf2f9]">
{recent.length === 0 ? (
<p className="py-4 text-sm text-slate-500">
{t("results.check.noRecent")}
</p>
) : (
recent.map((row) => (
<button
key={row}
type="button"
className="flex w-full items-center justify-between py-3 text-left"
onClick={() => setTicketNo(row)}
>
<span className="flex items-center gap-2 font-mono text-sm font-black text-slate-800">
<Clock3 className="size-4 text-slate-400" />
{row}
</span>
<span className="text-xs text-slate-400">
{latestDraw?.business_date ?? "—"}
</span>
</button>
))
)}
</div>
</section>
</div>
<WinningResultDialog
open={result !== null}
data={result}
query={normalizedTicketNo}
onOpenChange={(open) => {
if (!open) setResult(null);
}}
onCheckAnother={() => {
setResult(null);
setTicketNo("");
}}
/>
{drawNo ? (
<DrawWinningCheckPanel drawNo={drawNo} collapsible={false} />
) : (
<Skeleton className="h-56 rounded-2xl" />
)}
</PlayerPanel>
);
}
function WinningResultDialog({
open,
data,
query,
onOpenChange,
onCheckAnother,
}: {
open: boolean;
data: WinningCheckResult | null;
query: string;
onOpenChange: (open: boolean) => void;
onCheckAnother: () => void;
}) {
const { t } = useTranslation("player");
const totalWin = (data?.match.total_win_minor ?? 0) + (data?.match.total_jackpot_win_minor ?? 0);
const isWon = totalWin > 0 || (data?.match.winning_ticket_count ?? 0) > 0;
const firstTicket = useMemo(() => data?.tickets[0] ?? null, [data]);
const { activeCurrency } = useActivePlayerCurrency();
const currency = firstTicket?.currency_code ?? activeCurrency;
if (!data) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
showCloseButton={false}
className="flex max-h-[calc(100dvh-24px)] flex-col gap-0 overflow-hidden rounded-2xl border-[#e4ebf6] bg-white p-0 shadow-[0_24px_70px_rgba(15,23,42,0.28)] sm:max-w-md"
>
<div className="relative shrink-0 px-5 pt-4">
<button
type="button"
onClick={() => onOpenChange(false)}
className="absolute right-3 top-3 z-10 inline-flex size-9 items-center justify-center rounded-full text-slate-500 hover:bg-slate-100"
aria-label={t("actions.close")}
>
<XIcon className="size-5" />
</button>
<DialogHeader className="items-center text-center">
<div className="flex size-16 items-center justify-center rounded-full border-4 border-emerald-100 bg-white text-emerald-600">
<CheckCircle2 className="size-11" />
</div>
<DialogTitle className="mt-3 text-xl font-black text-slate-950">
{isWon
? t("results.check.winTitle")
: t("results.check.noWinTitle")}
</DialogTitle>
<DialogDescription className="text-sm text-slate-500">
{t("results.check.ticketNumber")}
</DialogDescription>
</DialogHeader>
</div>
<div className="min-h-0 flex-1 overflow-y-auto px-5 pb-5">
<div className="mx-auto mt-3 w-fit rounded-xl bg-emerald-50 px-8 py-2 font-mono text-lg font-black text-[#0a8f3e]">
{query}
</div>
<div className="mt-5 grid grid-cols-2 overflow-hidden rounded-xl border border-emerald-100 bg-emerald-50 text-center">
<div className="border-r border-emerald-100 px-3 py-4">
<p className="text-xs font-medium text-slate-500">
{t("results.check.match")}
</p>
<p className="mt-2 text-lg font-black text-[#0a8f3e]">
{firstTicket ? playLabel(firstTicket.play_code, t) : isWon ? t("orders.hit") : "—"}
</p>
</div>
<div className="px-3 py-4">
<p className="text-xs font-medium text-slate-500">
{t("results.check.amount")}
</p>
<p className="mt-2 font-mono text-lg font-black text-[#0a8f3e]">
{formatMinorAsCurrency(totalWin, currency)}
</p>
</div>
</div>
<div className="mt-5 rounded-xl border border-[#e8eef7] bg-white p-4 text-sm">
<p className="font-black text-slate-950">
{t("results.check.drawInfo")}
</p>
<div className="mt-2.5 grid grid-cols-2 gap-3 text-slate-500">
<div>
<p className="text-xs">{t("results.check.issueNo")}</p>
<p className="mt-1 font-mono font-black text-slate-900">{data.draw.draw_no}</p>
</div>
<div>
<p className="text-xs">{t("results.businessDate")}</p>
<p className="mt-1 font-semibold text-slate-900">{data.draw.business_date}</p>
</div>
<div className="col-span-2">
<p className="text-xs">{t("results.drawTime", { time: "" }).replace(":", "").trim()}</p>
<p className="mt-1 font-semibold text-slate-900">
{formatPlayerInstant(data.draw.draw_time_iso ?? data.draw.draw_time ?? null)}
</p>
</div>
</div>
</div>
<div className="mt-5 grid gap-3">
<Button
type="button"
nativeButton={false}
className="h-12 rounded-xl bg-[#07459f] text-base font-black text-white hover:bg-[#063b88]"
render={<Link href={`/orders?draw_no=${encodeURIComponent(data.draw.draw_no)}&number=${encodeURIComponent(query)}`} />}
>
{t("results.check.viewBetDetails")}
</Button>
<Button
type="button"
variant="outline"
className="h-12 rounded-xl border-[#ff3650] text-base font-black text-[#e5002c] hover:bg-[#fff5f6]"
onClick={onCheckAnother}
>
<RefreshCw className="size-5" />
{t("results.check.checkAnother")}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}
}

View File

@@ -1,11 +1,12 @@
"use client";
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { useSearchParams } from "next/navigation";
import { useTranslation } from "react-i18next";
import { getDrawResultByNo } from "@/api/draw";
import { getTicketDrawMyMatch } from "@/api/ticket-items";
import Link from "next/link";
import { Button, buttonVariants } from "@/components/ui/button";
import { PlayerPanel } from "@/components/layout/player-panel";
import {
@@ -16,6 +17,7 @@ import {
CardTitle,
} from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { DrawWinningCheckPanel } from "@/features/results/draw-winning-check-panel";
import { JackpotResultsStrip } from "@/features/results/jackpot-results-strip";
import { TwentyThreeResultsGrid } from "@/features/results/twenty-three-results-grid";
import { useCurrencyCatalog } from "@/hooks/use-currency-catalog";
@@ -37,6 +39,8 @@ type DrawResultDetailScreenProps = {
/** §4.6 开奖结果详情23 分区 + [< >] 切换 + 本人命中高亮 + Jackpot */
export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps) {
const { t } = useTranslation("player");
const searchParams = useSearchParams();
const checkOpen = searchParams.get("check") === "1";
const { activeCurrency } = useActivePlayerCurrency();
const creditMode = isCreditFundingPlayer(usePlayerSessionStore((state) => state.profile));
useCurrencyCatalog();
@@ -268,20 +272,14 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
</div>
) : null}
<div className="rounded-xl border border-[#e8eef7] bg-[#f8fbff] px-3 py-3">
<p className="text-xs leading-relaxed text-slate-500">
{t("results.hitHint")}
</p>
<Link
href="/results/check"
className={cn(
buttonVariants({ variant: "default", size: "sm" }),
"mt-3 h-10 w-full rounded-xl bg-[#e5002c] text-white hover:bg-[#d10028]",
)}
>
{t("results.viewMyWinning")}
</Link>
</div>
<DrawWinningCheckPanel
key={checkOpen ? `${data.draw_no}:check` : data.draw_no}
drawNo={data.draw_no}
businessDate={data.business_date}
drawTimeIso={data.draw_time_iso}
drawTime={data.draw_time}
defaultOpen={checkOpen}
/>
</CardContent>
</Card>
</div>

View File

@@ -19,7 +19,7 @@ import {
} from "@/components/ui/select";
import { PlayerPanel } from "@/components/layout/player-panel";
import { JackpotResultsStrip } from "@/features/results/jackpot-results-strip";
import { TwentyThreeResultsGrid } from "@/features/results/twenty-three-results-grid";
import { useCurrencyCatalog } from "@/hooks/use-currency-catalog";
import { formatPlayerInstant } from "@/lib/player-datetime";
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
@@ -259,14 +259,17 @@ export function DrawResultsListScreen() {
{t("results.openDetail", { defaultValue: "查看详情" })}
</Link>
</div>
<div className="pt-4">
<TwentyThreeResultsGrid numbers={featured.results} />
<Link
href="/results/check"
className="mt-4 inline-flex h-10 w-full items-center justify-center rounded-xl bg-[#e5002c] px-4 text-sm font-bold text-white transition-colors hover:bg-[#d10028]"
>
{t("results.viewMyWinning")}
</Link>
<div className="mt-3 grid grid-cols-3 gap-2 text-center">
{RESULTS_TOP_PRIZE_KEYS.map((tier) => (
<div key={tier} className="rounded-lg border border-[#edf2f8] bg-[#f8fbff] py-2">
<p className="text-[11px] font-bold text-[#7890b8]">
{t(resultsPrizeLabelKey(tier))}
</p>
<p className="mt-1 font-mono text-lg font-black tabular-nums text-[#e5002c]">
{featured.results[tier]}
</p>
</div>
))}
</div>
</div>
) : null}

View File

@@ -0,0 +1,354 @@
"use client";
import Link from "next/link";
import { BriefcaseBusiness, CheckCircle2, ChevronDown, Clock3, RefreshCw, XIcon } from "lucide-react";
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { getTicketDrawMyMatch, getTicketItems } from "@/api/ticket-items";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { useCurrencyCatalog } from "@/hooks/use-currency-catalog";
import { formatMinorAsCurrency } from "@/lib/money";
import { formatPlayerInstant } from "@/lib/player-datetime";
import { playLabel } from "@/lib/play-labels";
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
import { cn } from "@/lib/utils";
import type { DrawResultListItem } from "@/types/api/draw-results";
import type { TicketDrawMyMatchPayload, TicketItemListRow } from "@/types/api/ticket-items";
type WinningCheckResult = {
draw: DrawResultListItem;
match: TicketDrawMyMatchPayload;
tickets: TicketItemListRow[];
};
export type DrawWinningCheckPanelProps = {
drawNo: string;
businessDate?: string | null;
drawTimeIso?: string | null;
drawTime?: string | null;
defaultOpen?: boolean;
collapsible?: boolean;
};
export function DrawWinningCheckPanel({
drawNo,
businessDate,
drawTimeIso,
drawTime,
defaultOpen = false,
collapsible = true,
}: DrawWinningCheckPanelProps) {
const { t } = useTranslation("player");
useCurrencyCatalog();
const [open, setOpen] = useState(defaultOpen);
const [ticketNo, setTicketNo] = useState("");
const [recent, setRecent] = useState<string[]>([]);
const [result, setResult] = useState<WinningCheckResult | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const normalizedTicketNo = ticketNo.trim();
const runCheck = useCallback(async () => {
if (normalizedTicketNo === "") {
return;
}
setLoading(true);
setError(null);
try {
const [match, tickets] = await Promise.all([
getTicketDrawMyMatch(drawNo),
getTicketItems({
draw_no: drawNo,
number: normalizedTicketNo,
per_page: 10,
page: 1,
}),
]);
const next = {
draw: {
draw_id: "",
draw_no: drawNo,
business_date: businessDate ?? "",
draw_time: drawTime ?? null,
draw_time_iso: drawTimeIso ?? null,
result_version: 0,
result_source: null,
results: { "1st": "", "2nd": "", "3rd": "", starter: [], consolation: [] },
result_items: [],
} satisfies DrawResultListItem,
match,
tickets: tickets.items,
};
setResult(next);
setRecent((current) =>
[normalizedTicketNo, ...current.filter((x) => x !== normalizedTicketNo)].slice(0, 5),
);
} catch {
setError(t("results.check.loadFailed"));
} finally {
setLoading(false);
}
}, [businessDate, drawNo, drawTime, drawTimeIso, normalizedTicketNo, t]);
const panelBody = (
<div className="space-y-3">
<div className="space-y-2.5">
<label className="block space-y-1.5">
<span className="text-xs font-black text-slate-700">
{t("results.check.ticketNumber")}
</span>
<Input
value={ticketNo}
placeholder={t("results.check.placeholder")}
onChange={(e) => setTicketNo(e.target.value)}
className="h-12 rounded-xl border-[#dce7f7] bg-white font-mono text-base font-bold"
/>
</label>
<p className="text-xs text-slate-500">
{t("results.check.forDraw", { drawNo })}
</p>
{error ? <p className="text-sm font-semibold text-[#e5002c]">{error}</p> : null}
<Button
type="button"
disabled={normalizedTicketNo === "" || loading}
onClick={() => void runCheck()}
className="h-12 w-full rounded-xl bg-[#e5002c] text-base font-black text-white hover:bg-[#d10028]"
>
{loading ? t("results.check.loading") : t("results.check.submit")}
</Button>
</div>
<div className="rounded-xl border border-[#dfe8f6] bg-white p-4">
<div className="flex items-center justify-between">
<h3 className="font-black text-slate-950">{t("results.check.recent")}</h3>
{recent.length > 0 ? (
<button
type="button"
className="text-sm font-bold text-[#0b56b7]"
onClick={() => setRecent([])}
>
{t("actions.clear")}
</button>
) : null}
</div>
<div className="mt-3 divide-y divide-[#edf2f9]">
{recent.length === 0 ? (
<p className="py-4 text-sm text-slate-500">{t("results.check.noRecent")}</p>
) : (
recent.map((row) => (
<button
key={row}
type="button"
className="flex w-full items-center justify-between py-3 text-left"
onClick={() => setTicketNo(row)}
>
<span className="flex items-center gap-2 font-mono text-sm font-black text-slate-800">
<Clock3 className="size-4 text-slate-400" />
{row}
</span>
<span className="text-xs text-slate-400">{businessDate ?? "—"}</span>
</button>
))
)}
</div>
</div>
</div>
);
return (
<>
<section className="overflow-hidden rounded-2xl border border-red-100 bg-white shadow-[0_12px_32px_rgba(15,23,42,0.06)]">
{collapsible ? (
<>
<button
type="button"
className="flex w-full items-center justify-between gap-3 bg-gradient-to-b from-red-50 to-white px-4 py-4 text-left"
onClick={() => setOpen((value) => !value)}
aria-expanded={open}
>
<div className="flex min-w-0 items-center gap-3">
<div className="flex size-12 shrink-0 items-center justify-center rounded-full bg-white text-[#e5002c] shadow-[0_12px_28px_rgba(229,0,44,0.12)]">
<BriefcaseBusiness className="size-6" />
</div>
<div className="min-w-0">
<h2 className="text-base font-black text-slate-950">
{t("results.check.title")}
</h2>
<p className="mt-0.5 truncate text-xs text-slate-500">
{t("results.check.forDraw", { drawNo })}
</p>
</div>
</div>
<ChevronDown
className={cn(
"size-5 shrink-0 text-[#7890b8] transition-transform",
open && "rotate-180",
)}
aria-hidden
/>
</button>
{open ? <div className="border-t border-red-100 px-3 pb-3 pt-2">{panelBody}</div> : null}
</>
) : (
<>
<div className="bg-gradient-to-b from-red-50 to-white px-5 pb-5 pt-8 text-center">
<div className="mx-auto flex size-24 items-center justify-center rounded-full bg-white text-[#e5002c] shadow-[0_18px_40px_rgba(229,0,44,0.14)]">
<BriefcaseBusiness className="size-12" />
</div>
<h2 className="mt-5 text-lg font-black text-slate-950">
{t("results.check.enterTicket")}
</h2>
<p className="mx-auto mt-2 max-w-xs text-sm leading-relaxed text-slate-500">
{t("results.check.forDraw", { drawNo })}
</p>
</div>
<div className="px-3 pb-3">{panelBody}</div>
</>
)}
</section>
<WinningResultDialog
open={result !== null}
data={result}
query={normalizedTicketNo}
onOpenChange={(nextOpen) => {
if (!nextOpen) setResult(null);
}}
onCheckAnother={() => {
setResult(null);
setTicketNo("");
}}
/>
</>
);
}
function WinningResultDialog({
open,
data,
query,
onOpenChange,
onCheckAnother,
}: {
open: boolean;
data: WinningCheckResult | null;
query: string;
onOpenChange: (open: boolean) => void;
onCheckAnother: () => void;
}) {
const { t } = useTranslation("player");
const totalWin = (data?.match.total_win_minor ?? 0) + (data?.match.total_jackpot_win_minor ?? 0);
const isWon = totalWin > 0 || (data?.match.winning_ticket_count ?? 0) > 0;
const firstTicket = data?.tickets[0] ?? null;
const { activeCurrency } = useActivePlayerCurrency();
const currency = firstTicket?.currency_code ?? activeCurrency;
if (!data) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
showCloseButton={false}
className="flex max-h-[calc(100dvh-24px)] flex-col gap-0 overflow-hidden rounded-2xl border-[#e4ebf6] bg-white p-0 shadow-[0_24px_70px_rgba(15,23,42,0.28)] sm:max-w-md"
>
<div className="relative shrink-0 px-5 pt-4">
<button
type="button"
onClick={() => onOpenChange(false)}
className="absolute right-3 top-3 z-10 inline-flex size-9 items-center justify-center rounded-full text-slate-500 hover:bg-slate-100"
aria-label={t("actions.close")}
>
<XIcon className="size-5" />
</button>
<DialogHeader className="items-center text-center">
<div className="flex size-16 items-center justify-center rounded-full border-4 border-emerald-100 bg-white text-emerald-600">
<CheckCircle2 className="size-11" />
</div>
<DialogTitle className="mt-3 text-xl font-black text-slate-950">
{isWon ? t("results.check.winTitle") : t("results.check.noWinTitle")}
</DialogTitle>
<DialogDescription className="text-sm text-slate-500">
{t("results.check.ticketNumber")}
</DialogDescription>
</DialogHeader>
</div>
<div className="min-h-0 flex-1 overflow-y-auto px-5 pb-5">
<div className="mx-auto mt-3 w-fit rounded-xl bg-emerald-50 px-8 py-2 font-mono text-lg font-black text-[#0a8f3e]">
{query}
</div>
<div className="mt-5 grid grid-cols-2 overflow-hidden rounded-xl border border-emerald-100 bg-emerald-50 text-center">
<div className="border-r border-emerald-100 px-3 py-4">
<p className="text-xs font-medium text-slate-500">{t("results.check.match")}</p>
<p className="mt-2 text-lg font-black text-[#0a8f3e]">
{firstTicket ? playLabel(firstTicket.play_code, t) : isWon ? t("orders.hit") : "—"}
</p>
</div>
<div className="px-3 py-4">
<p className="text-xs font-medium text-slate-500">{t("results.check.amount")}</p>
<p className="mt-2 font-mono text-lg font-black text-[#0a8f3e]">
{formatMinorAsCurrency(totalWin, currency)}
</p>
</div>
</div>
<div className="mt-5 rounded-xl border border-[#e8eef7] bg-white p-4 text-sm">
<p className="font-black text-slate-950">{t("results.check.drawInfo")}</p>
<div className="mt-2.5 grid grid-cols-2 gap-3 text-slate-500">
<div>
<p className="text-xs">{t("results.check.issueNo")}</p>
<p className="mt-1 font-mono font-black text-slate-900">{data.draw.draw_no}</p>
</div>
<div>
<p className="text-xs">{t("results.businessDate")}</p>
<p className="mt-1 font-semibold text-slate-900">{data.draw.business_date}</p>
</div>
<div className="col-span-2">
<p className="text-xs">{t("results.drawTime", { time: "" }).replace(":", "").trim()}</p>
<p className="mt-1 font-semibold text-slate-900">
{formatPlayerInstant(data.draw.draw_time_iso ?? data.draw.draw_time ?? null)}
</p>
</div>
</div>
</div>
<div className="mt-5 grid gap-3">
<Button
type="button"
nativeButton={false}
className="h-12 rounded-xl bg-[#07459f] text-base font-black text-white hover:bg-[#063b88]"
render={
<Link
href={`/orders?draw_no=${encodeURIComponent(data.draw.draw_no)}&number=${encodeURIComponent(query)}`}
/>
}
>
{t("results.check.viewBetDetails")}
</Button>
<Button
type="button"
variant="outline"
className="h-12 rounded-xl border-[#ff3650] text-base font-black text-[#e5002c] hover:bg-[#fff5f6]"
onClick={onCheckAnother}
>
<RefreshCw className="size-5" />
{t("results.check.checkAnother")}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}