-
-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathCountdownTimer.tsx
More file actions
120 lines (102 loc) · 3.5 KB
/
CountdownTimer.tsx
File metadata and controls
120 lines (102 loc) · 3.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
'use client';
import { useEffect, useState } from 'react';
import { useTranslations } from 'next-intl';
import { cn } from '@/lib/utils';
import { AlertTriangle } from 'lucide-react';
interface CountdownTimerProps {
timeLimitSeconds: number;
onTimeUp: () => void;
isActive: boolean;
startedAt: Date;
}
export function CountdownTimer({
timeLimitSeconds,
onTimeUp,
isActive,
startedAt,
}: CountdownTimerProps) {
const t = useTranslations('quiz.timer');
const endTime = startedAt.getTime() + timeLimitSeconds * 1000;
const [remainingSeconds, setRemainingSeconds] = useState(() =>
Math.max(0, Math.floor((endTime - Date.now()) / 1000))
);
useEffect(() => {
if (!isActive) return;
const interval = setInterval(() => {
const now = Date.now();
const remaining = Math.max(0, Math.floor((endTime - now) / 1000));
setRemainingSeconds(remaining);
if (remaining === 0) {
clearInterval(interval);
queueMicrotask(onTimeUp);
}
}, 100);
return () => clearInterval(interval);
}, [isActive, onTimeUp, endTime]);
// Force update when tab becomes visible again
useEffect(() => {
if (!isActive) return;
const handleVisibilityChange = () => {
if (!document.hidden) {
const remaining = Math.max(0, Math.floor((endTime - Date.now()) / 1000));
setRemainingSeconds(remaining);
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
}, [isActive, endTime]);
const minutes = Math.floor(remainingSeconds / 60);
const seconds = remainingSeconds % 60;
const percentage = (remainingSeconds / timeLimitSeconds) * 100;
const getColorClasses = () => {
if (percentage <= 10) {
return 'text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950/30 border-red-200 dark:border-red-800';
}
if (percentage <= 30) {
return 'text-yellow-600 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-950/30 border-yellow-200 dark:border-yellow-800';
}
return 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-950/30 border-blue-200 dark:border-blue-800';
};
const getProgressBarColor = () => {
if (percentage <= 10) return 'bg-red-600';
if (percentage <= 30) return 'bg-yellow-600';
return 'bg-blue-600';
};
if (!isActive) return null;
return (
<div className={cn(
'rounded-lg border-2 p-4 transition-all',
getColorClasses(),
percentage <= 10 && 'animate-pulse'
)}>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">{t('label')}</span>
<span className="text-2xl font-bold font-mono">
{String(minutes).padStart(2, '0')}:{String(seconds).padStart(2, '0')}
</span>
</div>
<div className="h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
<div
className={cn(
'h-full transition-all duration-1000 ease-linear',
getProgressBarColor()
)}
style={{ width: `${percentage}%` }}
/>
</div>
{percentage <= 30 && (
<p className="text-xs mt-2 font-medium">
{percentage <= 10 ? (
<>
<AlertTriangle className="w-4 h-4 inline text-amber-500" /> {t('almostDone')}
</>
) : (
<>
<span aria-hidden="true">⏰</span> {t('hurryUp')}
</>
)}
</p>
)}
</div>
);
}