o2o-triple-pick/components/Countdown.tsx

54 lines
1.6 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
// "투표 종료까지" 카운트다운 (D7). 마감 = 킥오프 정각(lockAt).
// 서버/클라 hydration mismatch 방지: 시간 계산은 마운트 후 useEffect에서만.
export default function Countdown({
to,
label = "투표 종료까지",
}: {
to: string;
label?: string;
}) {
const [left, setLeft] = useState<number | null>(null);
useEffect(() => {
const target = new Date(to).getTime();
const tick = () => setLeft(Math.max(0, target - Date.now()));
tick();
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, [to]);
const ended = left !== null && left <= 0;
return (
<div className="flex items-center justify-center gap-2 rounded-xl border border-[var(--line-d)] bg-[var(--bg2)] px-3 py-2.5">
<span className="text-[12px] font-semibold text-[var(--ink-muted)]">
{ended ? "투표가 마감되었습니다" : label}
</span>
{!ended && (
<span
className="font-mono text-[18px] font-extrabold tabular-nums text-[var(--green)]"
style={{ textShadow: "0 0 12px rgba(74,255,160,0.45)" }}
suppressHydrationWarning
>
{left === null ? "--:--:--" : fmt(left)}
</span>
)}
</div>
);
}
function fmt(ms: number): string {
const s = Math.floor(ms / 1000);
const d = Math.floor(s / 86400);
const h = Math.floor((s % 86400) / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = s % 60;
const pad = (n: number) => String(n).padStart(2, "0");
if (d > 0) return `${d}${pad(h)}:${pad(m)}:${pad(sec)}`;
return `${pad(h)}:${pad(m)}:${pad(sec)}`;
}