106 lines
3.0 KiB
TypeScript
106 lines
3.0 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { cn } from '@/lib/utils'
|
|
import { Clock, AlertTriangle } from 'lucide-react'
|
|
|
|
interface CountdownTimerProps {
|
|
deadline: Date
|
|
label?: string
|
|
className?: string
|
|
}
|
|
|
|
interface TimeRemaining {
|
|
days: number
|
|
hours: number
|
|
minutes: number
|
|
seconds: number
|
|
totalMs: number
|
|
}
|
|
|
|
function getTimeRemaining(deadline: Date): TimeRemaining {
|
|
const totalMs = deadline.getTime() - Date.now()
|
|
if (totalMs <= 0) {
|
|
return { days: 0, hours: 0, minutes: 0, seconds: 0, totalMs: 0 }
|
|
}
|
|
|
|
const seconds = Math.floor((totalMs / 1000) % 60)
|
|
const minutes = Math.floor((totalMs / 1000 / 60) % 60)
|
|
const hours = Math.floor((totalMs / (1000 * 60 * 60)) % 24)
|
|
const days = Math.floor(totalMs / (1000 * 60 * 60 * 24))
|
|
|
|
return { days, hours, minutes, seconds, totalMs }
|
|
}
|
|
|
|
function formatCountdown(time: TimeRemaining): string {
|
|
if (time.totalMs <= 0) return 'Deadline passed'
|
|
|
|
const { days, hours, minutes, seconds } = time
|
|
|
|
// Less than 1 hour: show minutes and seconds
|
|
if (days === 0 && hours === 0) {
|
|
return `${minutes}m ${seconds}s`
|
|
}
|
|
|
|
// Less than 24 hours: show hours and minutes
|
|
if (days === 0) {
|
|
return `${hours}h ${minutes}m ${seconds}s`
|
|
}
|
|
|
|
// More than 24 hours: show days, hours, minutes
|
|
return `${days}d ${hours}h ${minutes}m`
|
|
}
|
|
|
|
type Urgency = 'expired' | 'critical' | 'warning' | 'normal'
|
|
|
|
function getUrgency(totalMs: number): Urgency {
|
|
if (totalMs <= 0) return 'expired'
|
|
if (totalMs < 60 * 60 * 1000) return 'critical' // < 1 hour
|
|
if (totalMs < 24 * 60 * 60 * 1000) return 'warning' // < 24 hours
|
|
return 'normal'
|
|
}
|
|
|
|
const urgencyStyles: Record<Urgency, string> = {
|
|
expired: 'text-muted-foreground bg-muted',
|
|
critical: 'text-red-700 bg-red-50 border-red-200 dark:text-red-400 dark:bg-red-950/50 dark:border-red-900',
|
|
warning: 'text-amber-700 bg-amber-50 border-amber-200 dark:text-amber-400 dark:bg-amber-950/50 dark:border-amber-900',
|
|
normal: 'text-green-700 bg-green-50 border-green-200 dark:text-green-400 dark:bg-green-950/50 dark:border-green-900',
|
|
}
|
|
|
|
export function CountdownTimer({ deadline, label, className }: CountdownTimerProps) {
|
|
const [time, setTime] = useState<TimeRemaining>(() => getTimeRemaining(deadline))
|
|
|
|
useEffect(() => {
|
|
const timer = setInterval(() => {
|
|
const remaining = getTimeRemaining(deadline)
|
|
setTime(remaining)
|
|
if (remaining.totalMs <= 0) {
|
|
clearInterval(timer)
|
|
}
|
|
}, 1000)
|
|
|
|
return () => clearInterval(timer)
|
|
}, [deadline])
|
|
|
|
const urgency = getUrgency(time.totalMs)
|
|
const displayText = formatCountdown(time)
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-medium',
|
|
urgencyStyles[urgency],
|
|
className
|
|
)}
|
|
>
|
|
{urgency === 'critical' ? (
|
|
<AlertTriangle className="h-3 w-3 shrink-0" />
|
|
) : (
|
|
<Clock className="h-3 w-3 shrink-0" />
|
|
)}
|
|
{label && <span className="hidden sm:inline">{label}</span>}
|
|
<span className="tabular-nums">{displayText}</span>
|
|
</div>
|
|
)
|
|
}
|