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>
This commit is contained in:
co-authored by
Cursor
parent
1d52d78513
commit
30f1f8d67d
@@ -0,0 +1,145 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user