diff --git a/apps/portal/src/app/[locale]/tools/rubrics/hooks/use-judging-timer.ts b/apps/portal/src/app/[locale]/tools/rubrics/hooks/use-judging-timer.ts index dc2b72de7..72122ab9f 100644 --- a/apps/portal/src/app/[locale]/tools/rubrics/hooks/use-judging-timer.ts +++ b/apps/portal/src/app/[locale]/tools/rubrics/hooks/use-judging-timer.ts @@ -48,7 +48,8 @@ type TimerAction = | 'TICK' | 'FORWARD' | 'BACK' - | { type: 'STAGE_COMPLETE'; nextStage: number }; + | { type: 'STAGE_COMPLETE'; nextStage: number } + | { type: 'TICK_BY'; count: number }; const createTimerReducer = (stages: ReturnType) => { return (state: TimerState, action: TimerAction): TimerState => { @@ -60,6 +61,27 @@ const createTimerReducer = (stages: ReturnType) => { }; } + if (typeof action === 'object' && action.type === 'TICK_BY') { + const { count } = action; + let { currentStage, stageTimeRemaining } = state; + let remaining = count; + + while (remaining > 0) { + if (stageTimeRemaining > remaining) { + stageTimeRemaining -= remaining; + remaining = 0; + } else if (currentStage < stages.length - 1) { + remaining -= stageTimeRemaining; + currentStage++; + stageTimeRemaining = stages[currentStage].duration; + } else { + return { currentStage, stageTimeRemaining: 0, isRunning: false }; + } + } + + return { ...state, currentStage, stageTimeRemaining }; + } + switch (action) { case 'START': return { ...state, isRunning: true }; @@ -143,6 +165,7 @@ export const useJudgingTimer = (): [JudgingTimerState, JudgingTimerControls] => const intervalRef = useRef(null); const previousStageRef = useRef(state.currentStage); + const lastTickAtRef = useRef(null); const start = useCallback(() => { dispatch('START'); @@ -174,11 +197,20 @@ export const useJudgingTimer = (): [JudgingTimerState, JudgingTimerControls] => }, []); useEffect(() => { - if (state.isRunning && state.stageTimeRemaining > 0) { + if (state.isRunning) { + lastTickAtRef.current = Date.now(); intervalRef.current = setInterval(() => { - dispatch('TICK'); - }, 1000); + if (lastTickAtRef.current === null) return; + const now = Date.now(); + const elapsedMs = now - lastTickAtRef.current; + const elapsedTicks = Math.floor(elapsedMs / 1000); + if (elapsedTicks > 0) { + lastTickAtRef.current += elapsedTicks * 1000; + dispatch({ type: 'TICK_BY', count: elapsedTicks }); + } + }, 500); } else { + lastTickAtRef.current = null; if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; @@ -190,7 +222,7 @@ export const useJudgingTimer = (): [JudgingTimerState, JudgingTimerControls] => clearInterval(intervalRef.current); } }; - }, [state.isRunning, state.stageTimeRemaining]); + }, [state.isRunning]); // Handle stage progression sound useEffect(() => { diff --git a/apps/portal/src/app/[locale]/tools/scorer/hooks/use-field-timer.ts b/apps/portal/src/app/[locale]/tools/scorer/hooks/use-field-timer.ts index 0bb853fad..12c6c047d 100644 --- a/apps/portal/src/app/[locale]/tools/scorer/hooks/use-field-timer.ts +++ b/apps/portal/src/app/[locale]/tools/scorer/hooks/use-field-timer.ts @@ -24,10 +24,10 @@ type TimerState = { isRunning: boolean; }; -type TimerAction = 'START' | 'PAUSE' | 'RESUME' | 'STOP' | 'RESET' | 'TICK'; +type TimerAction = 'START' | 'PAUSE' | 'RESUME' | 'STOP' | 'RESET' | 'TICK' | { type: 'TICK_BY'; count: number }; const timerReducer = (state: TimerState, action: TimerAction): TimerState => { - switch (action) { + switch (typeof action === 'object' ? action.type : action) { case 'START': return { ...state, isRunning: true }; case 'PAUSE': @@ -46,6 +46,14 @@ const timerReducer = (state: TimerState, action: TimerAction): TimerState => { } return { ...state, timeRemaining: newTime }; } + case 'TICK_BY': { + const count = (action as { type: 'TICK_BY'; count: number }).count; + const newTime = Math.max(0, state.timeRemaining - count); + if (newTime <= 0) { + return { ...state, isRunning: false, timeRemaining: 0 }; + } + return { ...state, timeRemaining: newTime }; + } default: return state; } @@ -60,6 +68,8 @@ export const useFieldTimer = (): [FieldTimerState, FieldTimerControls] => { }); const intervalRef = useRef(null); + const lastTickAtRef = useRef(null); + const prevTimeRemainingRef = useRef(MATCH_TIME); const start = useCallback(() => { dispatch('START'); @@ -84,11 +94,20 @@ export const useFieldTimer = (): [FieldTimerState, FieldTimerControls] => { }, []); useEffect(() => { - if (state.isRunning && state.timeRemaining > 0) { + if (state.isRunning) { + lastTickAtRef.current = Date.now(); intervalRef.current = setInterval(() => { - dispatch('TICK'); - }, 1000); + if (lastTickAtRef.current === null) return; + const now = Date.now(); + const elapsedMs = now - lastTickAtRef.current; + const elapsedTicks = Math.floor(elapsedMs / 1000); + if (elapsedTicks > 0) { + lastTickAtRef.current += elapsedTicks * 1000; + dispatch({ type: 'TICK_BY', count: elapsedTicks }); + } + }, 500); } else { + lastTickAtRef.current = null; if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; @@ -100,13 +119,17 @@ export const useFieldTimer = (): [FieldTimerState, FieldTimerControls] => { clearInterval(intervalRef.current); } }; - }, [state.isRunning, state.timeRemaining]); + }, [state.isRunning]); useEffect(() => { - if (state.timeRemaining === 30) { + const prev = prevTimeRemainingRef.current; + const curr = state.timeRemaining; + prevTimeRemainingRef.current = curr; + + if (curr <= 30 && curr > 0 && prev > 30) { playSound('endgame'); } - if (state.timeRemaining === 0) { + if (curr === 0) { playSound('end'); } }, [state.timeRemaining, playSound]); diff --git a/package-lock.json b/package-lock.json index 0192a6cf5..ba4a0c6f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,7 +56,7 @@ "morgan": "^1.10.1", "motion": "^12.34.5", "nanoid": "^5.1.6", - "next": "^16.1.6", + "next": "^16.2.3", "next-intl": "^4.9.1", "pg": "^8.17.1", "pino": "^10.3.0", @@ -87,7 +87,7 @@ "@babel/preset-react": "^7.28.5", "@eslint/js": "^9.39.2", "@firstisrael/prettier-config": "^1.1.0", - "@next/eslint-plugin-next": "^16.1.1", + "@next/eslint-plugin-next": "^16.2.3", "@nx/devkit": "22.5.3", "@nx/eslint": "22.5.3", "@nx/eslint-plugin": "22.5.3", @@ -123,7 +123,7 @@ "@types/ws": "^8.18.1", "babel-plugin-react-compiler": "^1.0.0", "eslint": "^9.39.2", - "eslint-config-next": "^16.1.6", + "eslint-config-next": "^16.2.3", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "6.10.2", @@ -7546,15 +7546,15 @@ } }, "node_modules/@next/env": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", - "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.4.tgz", + "integrity": "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.6.tgz", - "integrity": "sha512-/Qq3PTagA6+nYVfryAtQ7/9FEr/6YVyvOtl6rZnGsbReGLf0jZU6gkpr1FuChAQpvV46a78p4cmHOVP8mbfSMQ==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.4.tgz", + "integrity": "sha512-tOX826JJ96gYK/go18sPUgMq9FK1tqxBFfUCEufJb5XIkWFFmpgU7mahJANKGkHs7F41ir3tReJ3Lv5La0RvhA==", "dev": true, "license": "MIT", "dependencies": { @@ -7562,9 +7562,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", - "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.4.tgz", + "integrity": "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==", "cpu": [ "arm64" ], @@ -7578,9 +7578,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", - "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.4.tgz", + "integrity": "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==", "cpu": [ "x64" ], @@ -7594,9 +7594,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", - "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.4.tgz", + "integrity": "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==", "cpu": [ "arm64" ], @@ -7610,9 +7610,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", - "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.4.tgz", + "integrity": "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==", "cpu": [ "arm64" ], @@ -7626,9 +7626,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", - "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.4.tgz", + "integrity": "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==", "cpu": [ "x64" ], @@ -7642,9 +7642,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", - "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.4.tgz", + "integrity": "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==", "cpu": [ "x64" ], @@ -7658,9 +7658,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", - "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.4.tgz", + "integrity": "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==", "cpu": [ "arm64" ], @@ -7674,9 +7674,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", - "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.4.tgz", + "integrity": "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==", "cpu": [ "x64" ], @@ -19826,13 +19826,13 @@ } }, "node_modules/eslint-config-next": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.6.tgz", - "integrity": "sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.4.tgz", + "integrity": "sha512-A6ekXYFj/YQxBPMl45g3e+U8zJo+X2+ZQwcz34pPKjpc/3S4roBA2Rd9xWB4FKuSxhofo1/95WjzmUY+wHrOhg==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "16.1.6", + "@next/eslint-plugin-next": "16.2.4", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", @@ -26700,14 +26700,14 @@ } }, "node_modules/next": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", - "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.4.tgz", + "integrity": "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==", "license": "MIT", "dependencies": { - "@next/env": "16.1.6", + "@next/env": "16.2.4", "@swc/helpers": "0.5.15", - "baseline-browser-mapping": "^2.8.3", + "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -26719,15 +26719,15 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.1.6", - "@next/swc-darwin-x64": "16.1.6", - "@next/swc-linux-arm64-gnu": "16.1.6", - "@next/swc-linux-arm64-musl": "16.1.6", - "@next/swc-linux-x64-gnu": "16.1.6", - "@next/swc-linux-x64-musl": "16.1.6", - "@next/swc-win32-arm64-msvc": "16.1.6", - "@next/swc-win32-x64-msvc": "16.1.6", - "sharp": "^0.34.4" + "@next/swc-darwin-arm64": "16.2.4", + "@next/swc-darwin-x64": "16.2.4", + "@next/swc-linux-arm64-gnu": "16.2.4", + "@next/swc-linux-arm64-musl": "16.2.4", + "@next/swc-linux-x64-gnu": "16.2.4", + "@next/swc-linux-x64-musl": "16.2.4", + "@next/swc-win32-arm64-msvc": "16.2.4", + "@next/swc-win32-x64-msvc": "16.2.4", + "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", diff --git a/package.json b/package.json index be4dced23..b2f1a053a 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "morgan": "^1.10.1", "motion": "^12.34.5", "nanoid": "^5.1.6", - "next": "^16.1.6", + "next": "^16.2.3", "next-intl": "^4.9.1", "pg": "^8.17.1", "pino": "^10.3.0", @@ -93,7 +93,7 @@ "@babel/preset-react": "^7.28.5", "@eslint/js": "^9.39.2", "@firstisrael/prettier-config": "^1.1.0", - "@next/eslint-plugin-next": "^16.1.1", + "@next/eslint-plugin-next": "^16.2.3", "@nx/devkit": "22.5.3", "@nx/eslint": "22.5.3", "@nx/eslint-plugin": "22.5.3", @@ -129,7 +129,7 @@ "@types/ws": "^8.18.1", "babel-plugin-react-compiler": "^1.0.0", "eslint": "^9.39.2", - "eslint-config-next": "^16.1.6", + "eslint-config-next": "^16.2.3", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "6.10.2",