// ============================================================================ // Stylescape | Countdown Timer // ============================================================================ // Displays a countdown to a target date/time. // Supports data-ss-countdown attributes for declarative configuration. // ============================================================================ /** * Configuration options for CountdownTimer */ export interface CountdownTimerOptions { /** Target end time */ endTime?: Date | string | number; /** Format string (default: "HH:MM:SS") */ format?: "HH:MM:SS" | "DD:HH:MM:SS" | "full" | "compact"; /** Text to show when countdown ends */ endText?: string; /** Update interval in ms (default: 1000) */ interval?: number; /** Callback when countdown ends */ onComplete?: () => void; /** Callback on each tick */ onTick?: (remaining: CountdownTime) => void; /** Whether to show leading zeros */ leadingZeros?: boolean; } /** * Time breakdown for countdown */ export interface CountdownTime { days: number; hours: number; minutes: number; seconds: number; total: number; } /** * Countdown timer that updates a display element. * * @example JavaScript * ```typescript * const countdown = new CountdownTimer("#countdown", { * endTime: "2025-12-31T00:00:00", * onComplete: () => console.log("Happy New Year!") * }) * ``` * * @example HTML with data-ss * ```html *
*
* ``` */ export class CountdownTimer { private element: HTMLElement | null; private options: Required; private intervalId: number | null = null; private endTime: number; constructor( selectorOrElement: string | HTMLElement, options: CountdownTimerOptions = {}, ) { this.element = typeof selectorOrElement === "string" ? document.querySelector(selectorOrElement) : selectorOrElement; this.options = { endTime: options.endTime ?? new Date(Date.now() + 3600000), // Default: 1 hour format: options.format ?? "HH:MM:SS", endText: options.endText ?? "Time's up!", interval: options.interval ?? 1000, onComplete: options.onComplete ?? (() => {}), onTick: options.onTick ?? (() => {}), leadingZeros: options.leadingZeros !== false, }; // Parse end time if (typeof this.options.endTime === "string") { this.endTime = new Date(this.options.endTime).getTime(); } else if (typeof this.options.endTime === "number") { this.endTime = this.options.endTime; } else { this.endTime = this.options.endTime.getTime(); } if (!this.element) { console.warn("[Stylescape] CountdownTimer element not found"); return; } this.start(); } // ======================================================================== // Public Methods // ======================================================================== /** * Start the countdown */ public start(): void { if (this.intervalId) return; this.tick(); // Initial tick this.intervalId = window.setInterval( () => this.tick(), this.options.interval, ); } /** * Stop the countdown */ public stop(): void { if (this.intervalId) { clearInterval(this.intervalId); this.intervalId = null; } } /** * Reset with new end time */ public reset(endTime: Date | string | number): void { this.stop(); if (typeof endTime === "string") { this.endTime = new Date(endTime).getTime(); } else if (typeof endTime === "number") { this.endTime = endTime; } else { this.endTime = endTime.getTime(); } this.start(); } /** * Get remaining time breakdown */ public getRemaining(): CountdownTime { const total = Math.max(0, this.endTime - Date.now()); return { total, days: Math.floor(total / (1000 * 60 * 60 * 24)), hours: Math.floor((total / (1000 * 60 * 60)) % 24), minutes: Math.floor((total / (1000 * 60)) % 60), seconds: Math.floor((total / 1000) % 60), }; } /** * Destroy the countdown */ public destroy(): void { this.stop(); this.element = null; } // ======================================================================== // Private Methods // ======================================================================== private tick(): void { const remaining = this.getRemaining(); this.options.onTick(remaining); if (remaining.total <= 0) { this.stop(); this.updateDisplay(this.options.endText); this.options.onComplete(); return; } this.updateDisplay(this.formatTime(remaining)); } private formatTime(time: CountdownTime): string { const pad = (n: number) => this.options.leadingZeros ? n.toString().padStart(2, "0") : n.toString(); switch (this.options.format) { case "DD:HH:MM:SS": return `${pad(time.days)}:${pad(time.hours)}:${pad(time.minutes)}:${pad(time.seconds)}`; case "full": return `${time.days}d ${time.hours}h ${time.minutes}m ${time.seconds}s`; case "compact": if (time.days > 0) return `${time.days}d ${time.hours}h`; if (time.hours > 0) return `${time.hours}h ${time.minutes}m`; return `${time.minutes}m ${time.seconds}s`; case "HH:MM:SS": default: { const totalHours = time.days * 24 + time.hours; return `${pad(totalHours)}:${pad(time.minutes)}:${pad(time.seconds)}`; } } } private updateDisplay(text: string): void { if (this.element) { this.element.textContent = text; } } } export default CountdownTimer;