From 30f1f8d67d967530e6bede0900fba519f6146793 Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Wed, 9 Sep 2026 21:53:31 +0330 Subject: [PATCH] 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 --- app/components/flip-countdown.tsx | 145 +++++++++++++++++++++++ app/globals.css | 185 ++++++++++++++++++++++++++++++ app/page.tsx | 14 ++- 3 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 app/components/flip-countdown.tsx diff --git a/app/components/flip-countdown.tsx b/app/components/flip-countdown.tsx new file mode 100644 index 0000000..c278c84 --- /dev/null +++ b/app/components/flip-countdown.tsx @@ -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(null); + const [flipTo, setFlipTo] = useState(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 ( +