Files
lotteryFront/src/features/hall/hall-draw-panel.tsx
kang ca3a1db770 feat: enhance player panel and draw status handling
- Refactored PlayerPanel layout for improved title positioning and responsiveness.
- Added new function to check if betting is blocked based on hall status.
- Updated HallDrawPanel to utilize the new betting status check and display appropriate messages.
- Enhanced i18n support with new notices for review and non-bettable states across multiple languages.
2026-05-25 15:35:50 +08:00

193 lines
6.9 KiB
TypeScript

"use client";
import { Hourglass, Landmark, TimerReset } from "lucide-react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { drawStatusHud, isHallBlockedForBetting, isHallSealedCountdownUi } from "@/features/draw/draw-status-meta";
import type { HallDrawLiveSnapshot } from "@/features/hall/use-hall-draw-live";
import { formatSecondsClock } from "@/lib/format-gmt";
import { formatLotteryInstant } from "@/lib/player-datetime";
import { cn } from "@/lib/utils";
import type { DrawCurrentPayload } from "@/types/api/draw-current";
function CurrentTime({ payload }: { payload: DrawCurrentPayload }) {
const { t } = useTranslation("player");
const source = payload.close_time ?? payload.draw_time ?? payload.start_time;
const formatted = source ? formatLotteryInstant(source) : null;
if (!formatted) {
return (
<>
<span className="text-lg font-black tabular-nums text-[#0b3f96]">--:--:--</span>
<span className="mt-1 text-[11px] text-slate-500">{t("draw.currentTime")}</span>
</>
);
}
const parts = formatted.split(" ");
const date = parts.slice(0, -1).join(" ");
const time = parts.at(-1);
return (
<>
<span className="text-lg font-black tabular-nums text-[#0b3f96]">{time}</span>
<span className="mt-1 text-[11px] text-slate-500">{date}</span>
</>
);
}
function CloseTime({
serverNowMs,
hud,
payload,
}: {
serverNowMs: number;
hud: ReturnType<typeof drawStatusHud>;
payload: DrawCurrentPayload;
}) {
const { t } = useTranslation("player");
const sealedCountdown = isHallSealedCountdownUi(payload.status);
const [elapsedSeconds, setElapsedSeconds] = useState(0);
useEffect(() => {
const intervalId = window.setInterval(() => {
setElapsedSeconds((current) => current + 1);
}, 1000);
return () => window.clearInterval(intervalId);
}, []);
const nowMs = serverNowMs + elapsedSeconds * 1000;
let seconds = 0;
let label = t("draw.closesIn");
if (hud.countdownKind === "none") {
label = t(hud.labelKey, { defaultValue: hud.labelKey });
} else if (hud.countdownKind === "close") {
seconds = Math.max(0, Math.ceil(((payload.close_time ? Date.parse(payload.close_time) : 0) - nowMs) / 1000));
} else if (hud.countdownKind === "draw") {
seconds = Math.max(0, Math.ceil(((payload.draw_time ? Date.parse(payload.draw_time) : 0) - nowMs) / 1000));
label = sealedCountdown ? t("draw.drawsIn") : t("draw.closesIn");
} else if (hud.countdownKind === "cooldown") {
seconds = Math.max(0, Math.ceil(((payload.cooling_end_time ? Date.parse(payload.cooling_end_time) : 0) - nowMs) / 1000));
label = t("draw.coolDown");
}
return (
<>
<span className="text-lg font-black tabular-nums text-[#ff143d]">
{hud.countdownKind === "none" ? "--:--" : formatSecondsClock(seconds)}
</span>
<span className="mt-1 text-[11px] text-slate-500">{label}</span>
</>
);
}
export function HallDrawPanel({ drawLive }: { drawLive: HallDrawLiveSnapshot }) {
const { raw, display, serverNowMs, error, reload } = drawLive;
const { t } = useTranslation("player");
if (error) {
return (
<section className="mb-3 rounded-xl border border-red-200 bg-red-50 px-3 py-3 text-sm text-red-700">
<p>{t(error, { defaultValue: error })}</p>
<Button
type="button"
variant="outline"
size="sm"
className="mt-2 border-red-200 bg-white text-red-700"
onClick={() => void reload()}
>
{t("actions.retry")}
</Button>
</section>
);
}
if (raw === undefined || display === undefined) {
return (
<section className="mb-3 rounded-xl border border-[#e3ebf6] bg-white p-3 shadow-sm">
<div className="grid grid-cols-3 gap-2">
<Skeleton className="h-14 rounded-lg" />
<Skeleton className="h-14 rounded-lg" />
<Skeleton className="h-14 rounded-lg" />
</div>
</section>
);
}
if (raw === null || display === null) {
return (
<section className="mb-3 rounded-xl border border-[#e3ebf6] bg-white px-3 py-3 text-center text-sm text-slate-500 shadow-sm">
{t("draw.noIssue")}
</section>
);
}
const hud = drawStatusHud(display.status);
const sealedUi = isHallSealedCountdownUi(display.status);
const blockedUi = isHallBlockedForBetting(display.status);
return (
<section
className={cn(
"mb-3 overflow-hidden rounded-xl border border-[#e1e9f5] bg-white shadow-[0_6px_20px_rgba(15,23,42,0.06)]",
sealedUi && "border-red-200 bg-red-50/30",
)}
aria-label={t("draw.currentIssue")}
>
<div className="grid grid-cols-[1fr_1.05fr_1fr] divide-x divide-[#e7edf6]">
<div className="flex min-w-0 flex-col items-center justify-center px-2 py-3 text-center">
<div className="min-w-0 max-w-full overflow-x-auto">
<p className="text-[11px] font-semibold text-slate-500">{t("draw.issueNo")}</p>
<p className="whitespace-nowrap text-xs font-black tabular-nums text-[#ff143d] sm:text-sm">
{display.draw_no}
</p>
</div>
</div>
<div className="flex min-w-0 flex-col items-center justify-center px-2 py-3 text-center">
<CurrentTime payload={display} />
</div>
<div className="relative flex min-w-0 flex-col items-center justify-center px-2 py-3 text-center">
<CloseTime
key={`${display.draw_no}-${display.status}-${serverNowMs}`}
serverNowMs={serverNowMs}
hud={hud}
payload={display}
/>
<Hourglass
className={cn(
"absolute right-2 top-1/2 size-5 -translate-y-1/2",
sealedUi ? "text-[#ff143d]" : "text-red-300",
)}
aria-hidden
/>
</div>
</div>
{blockedUi ? (
<div className="flex items-center gap-2 border-t border-red-100 bg-red-50 px-3 py-2 text-xs font-medium text-red-600">
<TimerReset className="size-4 shrink-0" aria-hidden />
{display.status === "review"
? t("draw.reviewNotice")
: sealedUi
? t("draw.sealedNotice")
: t("draw.notBettableNotice")}
</div>
) : (
<div className="flex items-center justify-between border-t border-[#eef3f9] bg-[#fbfdff] px-3 py-1.5 text-[11px] text-slate-500">
<span className="inline-flex items-center gap-1.5">
<span className={cn("size-2 rounded-full", hud.dotClass)} />
{t(hud.labelKey, { defaultValue: hud.labelKey })}
</span>
<span className="inline-flex items-center gap-1">
<Landmark className="size-3.5" aria-hidden />
{t("draw.hall")}
</span>
</div>
)}
</section>
);
}