Files
hoomat-store/app/components/flip-countdown.tsx
T
Alireza HassaniandCursor 30f1f8d67d Add special-sale title with daily flip countdown.
Show a theme-colored (#c96) hour/minute/second flip clock on the left of the sale carousel that resets each Tehran day.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 21:53:31 +03:30

146 lines
4.1 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
type TimeParts = {
hours: number;
minutes: number;
seconds: number;
};
const LABELS = {
hours: "ساعت",
minutes: "دقیقه",
seconds: "ثانیه",
} as const;
const FLIP_HALF_MS = 300;
const FLIP_TOTAL_MS = FLIP_HALF_MS * 2;
function endOfTehranDay(): number {
const parts = new Intl.DateTimeFormat("en-CA", {
timeZone: "Asia/Tehran",
year: "numeric",
month: "2-digit",
day: "2-digit",
}).formatToParts(new Date());
const year = parts.find((part) => part.type === "year")?.value;
const month = parts.find((part) => part.type === "month")?.value;
const day = parts.find((part) => part.type === "day")?.value;
return new Date(`${year}-${month}-${day}T23:59:59+03:30`).getTime();
}
function getRemaining(): TimeParts {
const total = Math.max(0, Math.floor((endOfTehranDay() - Date.now()) / 1000));
return {
hours: Math.floor(total / 3600),
minutes: Math.floor((total % 3600) / 60),
seconds: total % 60,
};
}
function FlipDigit({ value }: { value: string }) {
const [top, setTop] = useState(value);
const [bottom, setBottom] = useState(value);
const [flipFrom, setFlipFrom] = useState<string | null>(null);
const [flipTo, setFlipTo] = useState<string | null>(null);
const settledRef = useRef(value);
useEffect(() => {
if (value === settledRef.current) return;
const from = settledRef.current;
setFlipFrom(from);
setFlipTo(value);
setTop(value);
const revealBottom = window.setTimeout(() => {
setBottom(value);
}, FLIP_HALF_MS);
const clearFlip = window.setTimeout(() => {
settledRef.current = value;
setFlipFrom(null);
setFlipTo(null);
}, FLIP_TOTAL_MS);
return () => {
window.clearTimeout(revealBottom);
window.clearTimeout(clearFlip);
settledRef.current = value;
setBottom(value);
setFlipFrom(null);
setFlipTo(null);
};
}, [value]);
return (
<span className={`flip-digit${flipFrom ? " is-flipping" : ""}`} aria-hidden="true">
<span className="flip-half flip-half-top">
<span className="flip-num">{top}</span>
</span>
<span className="flip-half flip-half-bottom">
<span className="flip-num">{bottom}</span>
</span>
<span className="flip-digit-hinge" />
{flipFrom != null && flipTo != null ? (
<>
<span className="flip-half flip-anim flip-anim-top" key={`top-${flipFrom}-${flipTo}`}>
<span className="flip-num">{flipFrom}</span>
</span>
<span className="flip-half flip-anim flip-anim-bottom" key={`bot-${flipFrom}-${flipTo}`}>
<span className="flip-num">{flipTo}</span>
</span>
</>
) : null}
</span>
);
}
function FlipUnit({ value, label }: { value: number; label: string }) {
const digits = String(value).padStart(2, "0");
return (
<div className="flip-unit">
<div className="flip-unit-digits">
<FlipDigit value={digits[0]} />
<FlipDigit value={digits[1]} />
</div>
<span className="flip-unit-label">{label}</span>
</div>
);
}
export function FlipCountdown({ className = "" }: { className?: string }) {
const [parts, setParts] = useState<TimeParts>(() => getRemaining());
useEffect(() => {
const tick = () => setParts(getRemaining());
tick();
const timer = window.setInterval(tick, 1000);
return () => window.clearInterval(timer);
}, []);
const readable = `${parts.hours} ساعت و ${parts.minutes} دقیقه`;
return (
<div
className={`flip-countdown ${className}`.trim()}
role="timer"
aria-live="polite"
aria-label={`زمان باقی‌مانده فروش ویژه امروز: ${readable}`}
>
<FlipUnit value={parts.hours} label={LABELS.hours} />
<span className="flip-separator" aria-hidden="true">
:
</span>
<FlipUnit value={parts.minutes} label={LABELS.minutes} />
<span className="flip-separator" aria-hidden="true">
:
</span>
<FlipUnit value={parts.seconds} label={LABELS.seconds} />
</div>
);
}