167 lines
5.8 KiB
TypeScript
167 lines
5.8 KiB
TypeScript
"use client";
|
|
|
|
import { Hourglass, Landmark, TimerReset } from "lucide-react";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { drawStatusHud, 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({
|
|
hud,
|
|
payload,
|
|
}: {
|
|
hud: ReturnType<typeof drawStatusHud>;
|
|
payload: DrawCurrentPayload;
|
|
}) {
|
|
const { t } = useTranslation("player");
|
|
const sealedCountdown = isHallSealedCountdownUi(payload.status);
|
|
let seconds = 0;
|
|
let label = t("draw.closesIn");
|
|
|
|
if (hud.countdownKind === "close") {
|
|
seconds = Math.max(0, payload.seconds_to_close);
|
|
} else if (hud.countdownKind === "draw") {
|
|
seconds = Math.max(0, payload.seconds_to_draw);
|
|
label = sealedCountdown ? t("draw.drawsIn") : t("draw.closesIn");
|
|
} else if (hud.countdownKind === "cooldown") {
|
|
seconds = Math.max(0, payload.seconds_remaining_in_cooldown ?? 0);
|
|
label = t("draw.coolDown");
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<span className="text-lg font-black tabular-nums text-[#ff143d]">
|
|
{formatSecondsClock(seconds)}
|
|
</span>
|
|
<span className="mt-1 text-[11px] text-slate-500">{label}</span>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export function HallDrawPanel({ drawLive }: { drawLive: HallDrawLiveSnapshot }) {
|
|
const { raw, display, error, reload } = drawLive;
|
|
const { t } = useTranslation("player");
|
|
|
|
if (error) {
|
|
return (
|
|
<section className="mb-4 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-4 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-4 rounded-xl border border-[#e3ebf6] bg-white px-3 py-4 text-center text-sm text-slate-500 shadow-sm">
|
|
{t("draw.noIssue")}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
const hud = drawStatusHud(display.status);
|
|
const sealedUi = isHallSealedCountdownUi(display.status);
|
|
|
|
return (
|
|
<section
|
|
className={cn(
|
|
"mb-4 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 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>
|
|
{sealedUi ? (
|
|
<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" aria-hidden />
|
|
{t("draw.sealedNotice")}
|
|
</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>
|
|
);
|
|
}
|