import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import type { ClockStyle } from "@workspace/api-client-react"; import { formatTime, formatDate, formatHijri, formatWeekday, } from "@/lib/i18n-format"; export const CLOCK_STYLES: ClockStyle[] = [ "full", "digital", "digital-no-seconds", "analog", "minimal", ]; export const DEFAULT_CLOCK_STYLE: ClockStyle = "full"; export function resolveClockStyle(value: ClockStyle | null | undefined): ClockStyle { if (!value) return DEFAULT_CLOCK_STYLE; return CLOCK_STYLES.includes(value) ? value : DEFAULT_CLOCK_STYLE; } export function useNow(intervalMs = 1000): Date { const [now, setNow] = useState(() => new Date()); useEffect(() => { const t = setInterval(() => setNow(new Date()), intervalMs); return () => clearInterval(t); }, [intervalMs]); return now; } type ClockProps = { style: ClockStyle | null | undefined; time: Date; lang: string; hour12?: boolean | null; size?: "sm" | "md"; }; export const DEFAULT_CLOCK_HOUR12 = false; export function resolveClockHour12(value: boolean | null | undefined): boolean { return value ?? DEFAULT_CLOCK_HOUR12; } function AnalogClock({ time, size = "md" }: { time: Date; size?: "sm" | "md" }) { const hours = time.getHours() % 12; const minutes = time.getMinutes(); const seconds = time.getSeconds(); const hourAngle = (hours + minutes / 60) * 30; const minuteAngle = (minutes + seconds / 60) * 6; const secondAngle = seconds * 6; const px = size === "sm" ? 38 : 52; const center = px / 2; const ticks = Array.from({ length: 12 }, (_, i) => { const angle = (i * 30 * Math.PI) / 180; const outer = center - 2; const inner = center - (i % 3 === 0 ? 6 : 4); const x1 = center + Math.sin(angle) * inner; const y1 = center - Math.cos(angle) * inner; const x2 = center + Math.sin(angle) * outer; const y2 = center - Math.cos(angle) * outer; return ( ); }); const hand = (angle: number, length: number, width: number, color: string, opacity = 1) => { const rad = (angle * Math.PI) / 180; const x = center + Math.sin(rad) * length; const y = center - Math.cos(rad) * length; return ( ); }; return ( {ticks} {hand(hourAngle, center - 14, 2.2, "currentColor", 0.95)} {hand(minuteAngle, center - 8, 1.6, "currentColor", 0.85)} {hand(secondAngle, center - 6, 1, "hsl(var(--primary))", 0.95)} ); } /** * Classic analog clock widget for the home screen. Latin digits 1-12, * 60 minute ticks, double-ring frame, sweeping seconds hand. Uses * currentColor so it inherits the foreground color and works in any * theme. The `size` prop controls overall pixel diameter. */ export function AnalogClockWidget({ time, lang, hour12, className, size = 240, showLabels = true, }: { time: Date; lang: string; hour12?: boolean | null; className?: string; size?: number; showLabels?: boolean; }) { const use12 = resolveClockHour12(hour12); const hours = time.getHours() % 12; const minutes = time.getMinutes(); const seconds = time.getSeconds(); const hourAngle = (hours + minutes / 60) * 30; const minuteAngle = (minutes + seconds / 60) * 6; const secondAngle = seconds * 6; const cx = size / 2; const cy = size / 2; const r = cx - 4; const scale = size / 240; const DIGITS = [ "12", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", ]; const numeralOffset = Math.max(10, 18 * scale); const numeralFont = Math.max(8, 14 * scale); const numerals = DIGITS.map((digit, i) => { const angle = ((i * 30 - 90) * Math.PI) / 180; const numR = r - numeralOffset; const x = cx + Math.cos(angle) * numR; const y = cy + Math.sin(angle) * numR; return ( {digit} ); }); const ticks = Array.from({ length: 60 }, (_, i) => { const angle = (i * 6 * Math.PI) / 180; const isHour = i % 5 === 0; const outer = r - 2 * scale; const inner = isHour ? r - 8 * scale : r - 5 * scale; const x1 = cx + Math.sin(angle) * inner; const y1 = cy - Math.cos(angle) * inner; const x2 = cx + Math.sin(angle) * outer; const y2 = cy - Math.cos(angle) * outer; return ( ); }); const hand = ( angle: number, length: number, width: number, color: string, opacity = 1, tailLength = 0, ) => { const rad = (angle * Math.PI) / 180; const x = cx + Math.sin(rad) * length; const y = cy - Math.cos(rad) * length; const tx = cx - Math.sin(rad) * tailLength; const ty = cy + Math.cos(rad) * tailLength; return ( ); }; return (
{/* Outer bezel ring */} {/* Face */} {/* Inner ring */} {ticks} {numerals} {/* Hour hand */} {hand(hourAngle, r - 50 * scale, 4.5 * scale, "currentColor", 0.95, 8 * scale)} {/* Minute hand */} {hand(minuteAngle, r - 22 * scale, 3 * scale, "currentColor", 0.85, 10 * scale)} {/* Second hand */} {hand(secondAngle, r - 14 * scale, 1.2 * scale, "hsl(var(--primary))", 1, 18 * scale)} {/* Center pin */}
{showLabels && (
{formatTime(time, lang, { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: use12, })}
{formatWeekday(time, lang)} · {formatDate(time, lang)}
)}
); } const HOME_CLOCK_STORAGE_KEY = "tx:home-clock-visible"; const HOME_CLOCK_EVENT = "tx:home-clock-changed"; function readHomeClockVisible(): boolean { if (typeof window === "undefined") return true; try { const v = window.localStorage.getItem(HOME_CLOCK_STORAGE_KEY); return v === null ? true : v === "1"; } catch { return true; } } /** * Small hook to read & persist whether the big home-screen analog * clock widget is visible. Persists in localStorage and broadcasts * a custom event so multiple consumers stay in sync within the same * tab (storage events only fire across tabs). */ export function useHomeClockVisibility(): [boolean, (next: boolean) => void] { const [visible, setVisibleState] = useState(readHomeClockVisible); useEffect(() => { const onChange = () => setVisibleState(readHomeClockVisible()); window.addEventListener(HOME_CLOCK_EVENT, onChange); window.addEventListener("storage", onChange); return () => { window.removeEventListener(HOME_CLOCK_EVENT, onChange); window.removeEventListener("storage", onChange); }; }, []); const setVisible = (next: boolean) => { try { window.localStorage.setItem(HOME_CLOCK_STORAGE_KEY, next ? "1" : "0"); } catch { /* ignore */ } setVisibleState(next); window.dispatchEvent(new Event(HOME_CLOCK_EVENT)); }; return [visible, setVisible]; } const TOPBAR_CLOCK_STORAGE_KEY = "tx:topbar-clock-visible"; const TOPBAR_CLOCK_EVENT = "tx:topbar-clock-changed"; function readTopbarClockVisible(): boolean { if (typeof window === "undefined") return true; try { const v = window.localStorage.getItem(TOPBAR_CLOCK_STORAGE_KEY); return v === null ? true : v === "1"; } catch { return true; } } /** * Visibility for the small clock displayed in the home page top bar. * Independent of useHomeClockVisibility, which controls the large * grid widget on the home screen. */ export function useTopbarClockVisibility(): [boolean, (next: boolean) => void] { const [visible, setVisibleState] = useState(readTopbarClockVisible); useEffect(() => { const onChange = () => setVisibleState(readTopbarClockVisible()); window.addEventListener(TOPBAR_CLOCK_EVENT, onChange); window.addEventListener("storage", onChange); return () => { window.removeEventListener(TOPBAR_CLOCK_EVENT, onChange); window.removeEventListener("storage", onChange); }; }, []); const setVisible = (next: boolean) => { try { window.localStorage.setItem(TOPBAR_CLOCK_STORAGE_KEY, next ? "1" : "0"); } catch { /* ignore */ } setVisibleState(next); window.dispatchEvent(new Event(TOPBAR_CLOCK_EVENT)); }; return [visible, setVisible]; } const HOME_CLOCK_SIZE_EVENT = "tx:home-clock-size-changed"; export type HomeClockSize = "compact" | "large"; function homeClockSizeKey(userId: number | string | null | undefined): string { return userId == null ? "tx:home-clock-size" : `tx:home-clock-size:${userId}`; } function readHomeClockSize(userId: number | string | null | undefined): HomeClockSize { if (typeof window === "undefined") return "compact"; try { const v = window.localStorage.getItem(homeClockSizeKey(userId)); return v === "large" ? "large" : "compact"; } catch { return "compact"; } } export function useHomeClockSize( userId: number | string | null | undefined, ): [HomeClockSize, (next: HomeClockSize) => void] { const [size, setSizeState] = useState(() => readHomeClockSize(userId)); useEffect(() => { setSizeState(readHomeClockSize(userId)); const onChange = () => setSizeState(readHomeClockSize(userId)); window.addEventListener(HOME_CLOCK_SIZE_EVENT, onChange); window.addEventListener("storage", onChange); return () => { window.removeEventListener(HOME_CLOCK_SIZE_EVENT, onChange); window.removeEventListener("storage", onChange); }; }, [userId]); const setSize = (next: HomeClockSize) => { try { window.localStorage.setItem(homeClockSizeKey(userId), next); } catch { /* ignore */ } setSizeState(next); window.dispatchEvent(new Event(HOME_CLOCK_SIZE_EVENT)); }; return [size, setSize]; } const HOME_CLOCK_POS_EVENT = "tx:home-clock-pos-changed"; function homeClockPosKey(userId: number | string | null | undefined): string { return userId == null ? "tx:home-clock-position" : `tx:home-clock-position:${userId}`; } function readHomeClockPosition(userId: number | string | null | undefined): number { if (typeof window === "undefined") return -1; try { const v = window.localStorage.getItem(homeClockPosKey(userId)); if (v === null) return -1; const n = Number(v); return Number.isFinite(n) && n >= 0 ? Math.floor(n) : -1; } catch { return -1; } } export function useHomeClockPosition( userId: number | string | null | undefined, ): [number, (next: number) => void] { const [pos, setPosState] = useState(() => readHomeClockPosition(userId)); useEffect(() => { setPosState(readHomeClockPosition(userId)); const onChange = () => setPosState(readHomeClockPosition(userId)); window.addEventListener(HOME_CLOCK_POS_EVENT, onChange); window.addEventListener("storage", onChange); return () => { window.removeEventListener(HOME_CLOCK_POS_EVENT, onChange); window.removeEventListener("storage", onChange); }; }, [userId]); const setPos = (next: number) => { try { window.localStorage.setItem(homeClockPosKey(userId), String(next)); } catch { /* ignore */ } setPosState(next); window.dispatchEvent(new Event(HOME_CLOCK_POS_EVENT)); }; return [pos, setPos]; } export function Clock({ style, time, lang, hour12, size = "md" }: ClockProps) { const resolved = resolveClockStyle(style); const use12 = resolveClockHour12(hour12); const dayName = formatWeekday(time, lang); const hijriDate = formatHijri(time, lang); const gregorianDate = formatDate(time, lang); const timeNoSec = formatTime(time, lang, { hour: "2-digit", minute: "2-digit", hour12: use12, }); const timeWithSec = formatTime(time, lang, { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: use12, }); const timeFontClass = size === "sm" ? "font-mono text-sm font-bold tabular-nums tracking-tight" : "font-mono text-base sm:text-lg font-bold tabular-nums tracking-tight"; if (resolved === "analog") { return (
{timeNoSec}
{dayName}
); } if (resolved === "minimal") { return (
{timeNoSec}
); } if (resolved === "digital-no-seconds") { return (
{timeNoSec}
{gregorianDate}
); } if (resolved === "digital") { return (
{timeWithSec}
{gregorianDate}
); } // full (default) return (
{timeNoSec}
{dayName} {hijriDate}
{gregorianDate}
); }