import { Group, Loader, Text } from "@mantine/core"; import { Clock } from "lucide-react"; import { useEffect, useState } from "react"; export interface CountdownTimerProps { /** ISO timestamp the countdown targets. */ deadline: string | null | undefined; /** Optional label shown before the time (e.g. "Window closes in"). */ label?: string; /** Text shown once the deadline has passed. */ expiredText?: string; /** * Optional grace stage: once `deadline` passes, count down to this later * ISO timestamp instead (e.g. the payment drain tail). `expiredText` then * only shows after the grace deadline has also passed. */ graceDeadline?: string | null; /** Label shown while counting down the grace stage (e.g. "Payment processing — closes in"). */ graceLabel?: string; /** Visual size of the time text. */ size?: "xs" | "sm" | "md" | "lg"; /** Colour once under this many seconds remain (urgency). Default 300 (5 min). */ urgentUnderSeconds?: number; } function pad(n: number): string { return String(n).padStart(2, "0"); } /** Break a remaining-milliseconds figure into a human string. */ function formatRemaining(ms: number): string { const total = Math.floor(ms / 1000); const days = Math.floor(total / 86400); const hours = Math.floor((total % 86400) / 3600); const minutes = Math.floor((total % 3600) / 60); const seconds = total % 60; if (days > 0) return `${days}d ${pad(hours)}h ${pad(minutes)}m`; if (hours > 0) return `${hours}h ${pad(minutes)}m ${pad(seconds)}s`; return `${pad(minutes)}m ${pad(seconds)}s`; } /** * Live countdown to an ISO deadline. Ticks once a second, shows the remaining * time (d/h/m/s), turns red when under `urgentUnderSeconds`, and shows * `expiredText` once the deadline is in the past. When `graceDeadline` is set, * a lapsed main deadline rolls into a calm second-stage countdown (spinner, * no urgency colours) toward it before `expiredText` takes over. Display only * — enforcement lives server-side. */ export function CountdownTimer({ deadline, label, expiredText = "Expired", graceDeadline, graceLabel, size = "sm", urgentUnderSeconds = 300, }: CountdownTimerProps) { const [now, setNow] = useState(() => Date.now()); useEffect(() => { if (!deadline) return; setNow(Date.now()); const id = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(id); }, [deadline, graceDeadline]); if (!deadline) return null; const target = new Date(deadline).getTime(); if (Number.isNaN(target)) return null; const remaining = target - now; const expired = remaining <= 0; const graceTarget = graceDeadline ? new Date(graceDeadline).getTime() : NaN; const graceRemaining = Number.isNaN(graceTarget) ? 0 : graceTarget - now; const inGrace = expired && graceRemaining > 0; if (inGrace) { return ( {graceLabel && ( {graceLabel} )} {formatRemaining(graceRemaining)} ); } const urgent = !expired && remaining <= urgentUnderSeconds * 1000; const color = expired ? "red.7" : urgent ? "orange.7" : "dimmed"; return ( {label && ( {label} )} {expired ? expiredText : formatRemaining(remaining)} ); } export default CountdownTimer;