Fazekit
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>
  );
}

More in E-commerce