Catalog
Code
"use client";
import * as React from "react";
import { Check, Copy, X } from "lucide-react";
import { cn } from "@/lib/utils";
export interface CountdownBannerProps {
/** ISO date or timestamp when the promo ends. */
endsAt: string | number | Date;
title?: string;
code?: string;
onDismiss?: () => void;
className?: string;
}
function parts(ms: number) {
const s = Math.max(0, Math.floor(ms / 1000));
return { d: Math.floor(s / 86400), h: Math.floor((s % 86400) / 3600), m: Math.floor((s % 3600) / 60), s: s % 60 };
}
export function CountdownBanner({ endsAt, title = "Flash sale — 20% off everything", code, onDismiss, className }: CountdownBannerProps) {
const end = React.useMemo(() => new Date(endsAt).getTime(), [endsAt]);
const [now, setNow] = React.useState<number | null>(null); // null on the server → no hydration mismatch
const [copied, setCopied] = React.useState(false);
const [open, setOpen] = React.useState(true);
React.useEffect(() => {
// Start the clock only after hydration so server and client HTML match.
// eslint-disable-next-line react-hooks/set-state-in-effect
setNow(Date.now());
const id = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(id);
}, []);
if (!open) return null;
const t = parts(now === null ? 0 : end - now);
const cell = (v: number, l: string) => (
<div className="flex flex-col items-center">
<span className="min-w-10 rounded-md bg-white/15 px-1.5 py-1 text-center font-mono text-lg font-semibold tabular-nums">
{now === null ? "--" : String(v).padStart(2, "0")}
</span>
<span className="mt-0.5 text-[10px] uppercase tracking-wider opacity-70">{l}</span>
</div>
);
return (
<div className={cn("relative flex w-full flex-wrap items-center justify-center gap-x-6 gap-y-3 rounded-2xl bg-gradient-to-r from-violet-600 via-fuchsia-600 to-rose-500 px-6 py-4 text-white shadow-lg", className)}>
<p className="font-semibold">{title}</p>
<div className="flex gap-2">
{cell(t.d, "days")}
{cell(t.h, "hrs")}
{cell(t.m, "min")}
{cell(t.s, "sec")}
</div>
{code && (
<button
onClick={() => {
navigator.clipboard?.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}}
className="flex items-center gap-2 rounded-lg border border-dashed border-white/60 px-3 py-1.5 font-mono text-sm transition hover:bg-white/10"
>
{code}
{copied ? <Check className="size-4" /> : <Copy className="size-4" />}
</button>
)}
<button
aria-label="Dismiss"
onClick={() => (setOpen(false), onDismiss?.())}
className="absolute right-2 top-2 rounded-full p-1 opacity-70 transition hover:bg-white/15 hover:opacity-100"
>
<X className="size-4" />
</button>
</div>
);
}