diff --git a/src/components/CountdownTimer.jsx b/src/components/CountdownTimer.jsx index 433eb8e..7cfcc46 100644 --- a/src/components/CountdownTimer.jsx +++ b/src/components/CountdownTimer.jsx @@ -1,5 +1,16 @@ import { useState, useEffect, useRef } from "react"; +const playSound = (audioRef) => { + if (audioRef.current) { + audioRef.current.currentTime = 0; + audioRef.current.play().catch(e => console.error("Error playing sound:", e)); + } +}; + +const STROKE_WIDTH = 10; +const RADIUS = 70; +const CIRCUMFERENCE = 2 * Math.PI * RADIUS; + const CountdownTimer = ({ duration = 30, onTimeUp, @@ -10,24 +21,63 @@ const CountdownTimer = ({ className = "", initialTimeRemaining = null, onTimeUpdate = null, + startSoundSrc = null, + warningSoundSrc = null, + timeUpSoundSrc = null, + finalCountdownSoundSrc = null, + isMuted = false, }) => { const [timeRemaining, setTimeRemaining] = useState(initialTimeRemaining ?? duration); const intervalRef = useRef(null); const hasWarningFired = useRef(false); const hasTimeUpFired = useRef(false); + const hasStartedFired = useRef(false); + const lastCountdownSoundTime = useRef(0); + + const startAudioRef = useRef(null); + const warningAudioRef = useRef(null); + const timeUpAudioRef = useRef(null); + const finalCountdownAudioRef = useRef(null); + + useEffect(() => { + if (!isMuted) { + if (startSoundSrc) startAudioRef.current = new Audio(startSoundSrc); + if (warningSoundSrc) warningAudioRef.current = new Audio(warningSoundSrc); + if (timeUpSoundSrc) timeUpAudioRef.current = new Audio(timeUpSoundSrc); + if (finalCountdownSoundSrc) finalCountdownAudioRef.current = new Audio(finalCountdownSoundSrc); + } + + return () => { + [startAudioRef, warningAudioRef, timeUpAudioRef, finalCountdownAudioRef].forEach(ref => { + if (ref.current) { + ref.current.pause(); + ref.current = null; + } + }); + }; + }, [isMuted, startSoundSrc, warningSoundSrc, timeUpSoundSrc, finalCountdownSoundSrc]); - // Reset when duration or initial value changes (e.g., new question) useEffect(() => { setTimeRemaining(initialTimeRemaining ?? duration); hasWarningFired.current = false; hasTimeUpFired.current = false; + hasStartedFired.current = false; + lastCountdownSoundTime.current = 0; if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }, [duration, initialTimeRemaining]); - // Start/stop the interval based on pause/active flags + useEffect(() => { + if (isActive && !isPaused && !hasStartedFired.current) { + if (initialTimeRemaining === null || initialTimeRemaining !== duration) { + if (!isMuted) playSound(startAudioRef); + hasStartedFired.current = true; + } + } + }, [isActive, isPaused, duration, initialTimeRemaining, isMuted]); + useEffect(() => { if (!isActive || isPaused || timeRemaining <= 0) { if (intervalRef.current) { @@ -41,28 +91,30 @@ const CountdownTimer = ({ setTimeRemaining((prev) => { const next = Math.max(prev - 1, 0); - // onTimeUpdate callback, invoked in a microtask to avoid potential react state warnings if (onTimeUpdate) { Promise.resolve().then(() => onTimeUpdate(next)); } - // Warning callback + if (next > 0 && next <= 5 && next !== lastCountdownSoundTime.current) { + if (!isMuted) playSound(finalCountdownAudioRef); + lastCountdownSoundTime.current = next; + } + if (next === showWarningAt && !hasWarningFired.current && onWarning) { hasWarningFired.current = true; + if (!isMuted) playSound(warningAudioRef); onWarning(); } - // Time up logic if (next === 0) { if (!hasTimeUpFired.current) { hasTimeUpFired.current = true; + if (!isMuted) playSound(timeUpAudioRef); - // Global handler for QuizQuestion, if it exists if (typeof window !== "undefined" && window.quizQuestionHandleTimeOut) { window.quizQuestionHandleTimeOut(); } - - // onTimeUp callback + if (onTimeUp) { onTimeUp(); } @@ -79,36 +131,36 @@ const CountdownTimer = ({ intervalRef.current = null; } }; - }, [isActive, isPaused, timeRemaining, onTimeUp, onTimeUpdate, onWarning, showWarningAt]); + }, [isActive, isPaused, timeRemaining, onTimeUp, onTimeUpdate, onWarning, showWarningAt, isMuted]); const progressPercentage = Math.max(0, Math.min(100, (timeRemaining / duration) * 100)); - - const getTimerColor = () => { - const percentage = (timeRemaining / duration) * 100; - if (percentage <= 20) return "text-red-500"; - if (percentage <= 40) return "text-yellow-500"; - return "text-green-500"; + const isWarning = timeRemaining > 0 && timeRemaining <= showWarningAt && !isPaused; + const isDanger = timeRemaining > 0 && timeRemaining <= 5 && !isPaused; + const isTimeUp = timeRemaining === 0; + + const progressOffset = CIRCUMFERENCE - (progressPercentage / 100) * CIRCUMFERENCE; + const center = RADIUS + STROKE_WIDTH; + const size = center * 2; + + const getRingColorClass = () => { + if (isTimeUp || progressPercentage <= (showWarningAt / duration) * 100) return "stroke-red-500"; + if (progressPercentage <= 40) return "stroke-yellow-500"; + return "stroke-green-500"; }; - const getProgressBarColor = () => { - const percentage = (timeRemaining / duration) * 100; - if (percentage <= 20) return "bg-red-500"; - if (percentage <= 40) return "bg-yellow-500"; - return "bg-green-500"; - }; - - const getProgressBarGlow = () => { - const percentage = (timeRemaining / duration) * 100; - if (percentage <= 20) return "shadow-red-500/50"; - if (percentage <= 40) return "shadow-yellow-500/50"; - return "shadow-green-500/50"; + const getTimerTextColor = () => { + if (isTimeUp || progressPercentage <= (showWarningAt / duration) * 100) return "text-red-500"; + if (progressPercentage <= 40) return "text-yellow-500"; + return "text-green-500"; }; const formatTime = (seconds) => { - if (seconds >= 60) { - const minutes = Math.floor(seconds / 60); - const remainingSeconds = seconds % 60; - return `${minutes}m ${remainingSeconds}s`; + const totalMinutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + const formattedSeconds = String(remainingSeconds).padStart(2, '0'); + + if (totalMinutes > 0) { + return `${totalMinutes}m ${formattedSeconds}s`; } return `${seconds}s`; }; @@ -116,54 +168,77 @@ const CountdownTimer = ({ if (!isActive) return null; return ( -