From dfd8e9f2e7ddee02d8cda6a4fb593c741e1227d3 Mon Sep 17 00:00:00 2001 From: John Stewartsson J R Date: Sun, 21 Jun 2026 15:06:12 +0530 Subject: [PATCH 1/5] feat(dashboard): implement integrated Pomodoro Focus Timer widget with circular progress and configs (#2622) --- src/components/dashboard/PomodoroWidget.tsx | 214 ++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 src/components/dashboard/PomodoroWidget.tsx diff --git a/src/components/dashboard/PomodoroWidget.tsx b/src/components/dashboard/PomodoroWidget.tsx new file mode 100644 index 000000000..14be4b09e --- /dev/null +++ b/src/components/dashboard/PomodoroWidget.tsx @@ -0,0 +1,214 @@ +import React, { useState, useEffect, useRef } from "react"; + +type TimerMode = "WORK" | "BREAK"; + +export default function PomodoroWidget() { + const [mode, setMode] = useState("WORK"); + const [secondsLeft, setSecondsLeft] = useState(25 * 60); + const [isActive, setIsActive] = useState(false); + const [customWorkMin, setCustomWorkMin] = useState(25); + const [customBreakMin, setCustomBreakMin] = useState(5); + const [showSettings, setShowSettings] = useState(false); + + const totalSeconds = mode === "WORK" ? customWorkMin * 60 : customBreakMin * 60; + const timerRef = useRef(null); + + // Synchronize timer duration changes cleanly when values or modes alter + useEffect(() => { + if (!isActive) { + setSecondsLeft(mode === "WORK" ? customWorkMin * 60 : customBreakMin * 60); + } + }, [customWorkMin, customBreakMin, mode]); + + // Unified core ticker countdown loops mechanism + useEffect(() => { + if (isActive) { + timerRef.current = setInterval(() => { + setSecondsLeft((prev) => { + if (prev <= 1) { + clearInterval(timerRef.current!); + setIsActive(false); + triggerCompletionNotification(); + // Automatically switch modes upon completion state transition + setMode((oldMode) => (oldMode === "WORK" ? "BREAK" : "WORK")); + return 0; + } + return prev - 1; + }); + }, 1000); + } else if (timerRef.current) { + clearInterval(timerRef.current); + } + + return () => { + if (timerRef.current) clearInterval(timerRef.current); + }; + }, [isActive]); + + const toggleTimer = () => setIsActive(!isActive); + + const resetTimer = () => { + setIsActive(false); + setSecondsLeft(mode === "WORK" ? customWorkMin * 60 : customBreakMin * 60); + }; + + const switchMode = (newMode: TimerMode) => { + setIsActive(false); + setMode(newMode); + setSecondsLeft(newMode === "WORK" ? customWorkMin * 60 : customBreakMin * 60); + }; + + const triggerCompletionNotification = () => { + // Subtle non-intrusive HTML5 Web Audio API synthesization mapping context + try { + const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)(); + const oscillator = audioCtx.createOscillator(); + const gainNode = audioCtx.createGain(); + oscillator.connect(gainNode); + gainNode.connect(audioCtx.destination); + oscillator.type = "sine"; + oscillator.frequency.setValueAtTime(587.33, audioCtx.currentTime); // D5 high tone hint note + gainNode.gain.setValueAtTime(0.1, audioCtx.currentTime); + oscillator.start(); + oscillator.stop(audioCtx.currentTime + 0.3); + } catch (e) { + console.warn("Web Audio alert synthesized hint skipped block:", e); + } + + if (Notification.permission === "granted") { + new Notification(`${mode === "WORK" ? "Work Session" : "Break Session"} Concluded!`, { + body: mode === "WORK" ? "Time to take a well-deserved short break!" : "Ready to jump back into hyper-focus work session?", + }); + } + }; + + useEffect(() => { + if (typeof window !== "undefined" && Notification.permission === "default") { + Notification.requestPermission(); + } + }, []); + + // Structural SVG circle stroke computation mapping properties + const radius = 80; + const circumference = 2 * Math.PI * radius; + const strokeDashoffset = totalSeconds > 0 ? circumference - (secondsLeft / totalSeconds) * circumference : circumference; + + const formatTime = (totalSecs: number): string => { + const mins = Math.floor(totalSecs / 60); + const secs = totalSecs % 60; + return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`; + }; + + return ( +
+
+

Focus Session

+ +
+ + {showSettings ? ( +
+
+ Work Duration (m): + setCustomWorkMin(Math.max(1, parseInt(e.target.value) || 1))} + className="w-16 px-2 py-1 border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 rounded text-center focus:outline-none" + /> +
+
+ Break Duration (m): + setCustomBreakMin(Math.max(1, parseInt(e.target.value) || 1))} + className="w-16 px-2 py-1 border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 rounded text-center focus:outline-none" + /> +
+
+ ) : ( +
+ + +
+ )} + + {/* Real-time Circular SVG Progression Indicator Rings */} +
+ + + + +
+ + {formatTime(secondsLeft)} + + + {mode === "WORK" ? "Focusing" : "Resting"} + +
+
+ +
+ + +
+
+ ); +} + From 2a52cc43fdccf140212d79e2d87b5e386b0d9c0e Mon Sep 17 00:00:00 2001 From: John Stewartsson J R Date: Sun, 21 Jun 2026 15:13:35 +0530 Subject: [PATCH 2/5] fix(dashboard): eliminate forbidden explicit any cast from audio context instantiation (#222) --- src/components/dashboard/PomodoroWidget.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/dashboard/PomodoroWidget.tsx b/src/components/dashboard/PomodoroWidget.tsx index 14be4b09e..c884cd2e1 100644 --- a/src/components/dashboard/PomodoroWidget.tsx +++ b/src/components/dashboard/PomodoroWidget.tsx @@ -59,9 +59,10 @@ export default function PomodoroWidget() { }; const triggerCompletionNotification = () => { - // Subtle non-intrusive HTML5 Web Audio API synthesization mapping context + // Subtle non-intrusive HTML5 Web Audio API synthesization mapping context without forbidden 'any' try { - const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)(); + const AudioContextClass = window.AudioContext || (window as unknown as Record).webkitAudioContext; + const audioCtx = new AudioContextClass(); const oscillator = audioCtx.createOscillator(); const gainNode = audioCtx.createGain(); oscillator.connect(gainNode); @@ -211,4 +212,3 @@ export default function PomodoroWidget() { ); } - From 27623647857938db76a800f1b26ce4a8ec0ff65c Mon Sep 17 00:00:00 2001 From: John Stewartsson J R Date: Mon, 22 Jun 2026 15:38:51 +0530 Subject: [PATCH 3/5] fix(dashboard): eliminate unused next-auth User import to satisfy strict ESLint rules (#2622) From 0791c0ce86a78170dda3e4ccf39a166c85fd6970 Mon Sep 17 00:00:00 2001 From: John Stewartsson J R Date: Mon, 22 Jun 2026 15:46:18 +0530 Subject: [PATCH 4/5] fix(dashboard): purge unused React import token from Pomodoro layout to pass ESLint checks (#2622) --- src/components/dashboard/PomodoroWidget.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard/PomodoroWidget.tsx b/src/components/dashboard/PomodoroWidget.tsx index c884cd2e1..c53b41fb0 100644 --- a/src/components/dashboard/PomodoroWidget.tsx +++ b/src/components/dashboard/PomodoroWidget.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef } from "react"; type TimerMode = "WORK" | "BREAK"; From e54fe381172c58f324991fbf452e72f24926a761 Mon Sep 17 00:00:00 2001 From: John Stewartsson J R Date: Mon, 22 Jun 2026 15:56:52 +0530 Subject: [PATCH 5/5] fix(dashboard): purge unused React import token from Pomodoro layout to pass ESLint checks (#2622) --- src/components/dashboard/PomodoroWidget.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/dashboard/PomodoroWidget.tsx b/src/components/dashboard/PomodoroWidget.tsx index c53b41fb0..3c9e4ac3b 100644 --- a/src/components/dashboard/PomodoroWidget.tsx +++ b/src/components/dashboard/PomodoroWidget.tsx @@ -212,3 +212,4 @@ export default function PomodoroWidget() { ); } +