From f390852705baff29256eb6d00eed36ce1edebab2 Mon Sep 17 00:00:00 2001 From: Ju Date: Mon, 27 Jul 2026 16:34:52 +0000 Subject: [PATCH 1/2] feat: enhance TimerCard with label editing and improve TimerPanel to manage timer labels --- src/components/panels/TimerCard.tsx | 57 +++++++++++++++++++++++++--- src/components/panels/TimerPanel.tsx | 22 +++++++++-- src/hooks/useCountdown.ts | 22 ++++++++++- 3 files changed, 89 insertions(+), 12 deletions(-) diff --git a/src/components/panels/TimerCard.tsx b/src/components/panels/TimerCard.tsx index f0cea50..a5a2ef6 100644 --- a/src/components/panels/TimerCard.tsx +++ b/src/components/panels/TimerCard.tsx @@ -26,17 +26,20 @@ function parseMmSs(text: string): number | null { interface TimerCardProps { label: string; + onLabelChange?: (label: string) => void; onRemove?: () => void; } -const TimerCard: React.FC = ({ label, onRemove }) => { - const { status, remainingMs, start, reset } = useCountdown(); +const TimerCard: React.FC = ({ label, onLabelChange, onRemove }) => { + const { status, remainingMs, start, pause, resume, reset } = useCountdown(); const [presetMs, setPresetMs] = useState(DEFAULT_PRESET_MS); const [inputText, setInputText] = useState(formatMmSs(DEFAULT_PRESET_MS)); const [inputInvalid, setInputInvalid] = useState(false); const isIdle = status === 'idle'; + const isRunning = status === 'running'; + const isPaused = status === 'paused'; const isFinished = status === 'finished'; const countdownColor = isFinished @@ -68,7 +71,13 @@ const TimerCard: React.FC = ({ label, onRemove }) => { return (
-

{label}

+ onLabelChange?.(e.target.value)} + /> {onRemove && ( - ) : ( + )} + {isRunning && ( + + )} + {isPaused && ( + + )} + {!isIdle && ( @@ -142,11 +162,28 @@ const TimerCard: React.FC = ({ label, onRemove }) => { align-items: center; } - h4 { + .label-input { margin: 0; + padding: 0.15rem 0.3rem; font-size: 0.95rem; font-weight: 600; color: #eaeaea; + background: transparent; + border: 1px solid transparent; + border-radius: 4px; + width: 100%; + min-width: 0; + font-family: inherit; + } + + .label-input:hover { + border-color: #444; + } + + .label-input:focus { + outline: none; + background: #111; + border-color: #0070f3; } .remove-btn { @@ -229,6 +266,14 @@ const TimerCard: React.FC = ({ label, onRemove }) => { button.reset:hover { background: #666; } + + button.pause { + background: #d9a441; + } + + button.pause:hover { + background: #c4923a; + } `}
); diff --git a/src/components/panels/TimerPanel.tsx b/src/components/panels/TimerPanel.tsx index 6151929..8bba273 100644 --- a/src/components/panels/TimerPanel.tsx +++ b/src/components/panels/TimerPanel.tsx @@ -5,7 +5,7 @@ import TimerCard from './TimerCard'; const DEFAULT_TIMER_COUNT = 5; const MIN_TIMERS = 1; -const MAX_TIMERS = 8; +const MAX_TIMERS = 20; function smallestFreeId(ids: number[]): number { const used = new Set(ids); @@ -14,21 +14,34 @@ function smallestFreeId(ids: number[]): number { return id; } +const initialIds = Array.from({ length: DEFAULT_TIMER_COUNT }, (_, i) => i + 1); + const TimerPanel: React.FC = () => { - const [timerIds, setTimerIds] = useState( - Array.from({ length: DEFAULT_TIMER_COUNT }, (_, i) => i + 1), + const [timerIds, setTimerIds] = useState(initialIds); + const [labels, setLabels] = useState>(() => + Object.fromEntries(initialIds.map((id) => [id, `Timer ${id}`])), ); const addTimer = () => { setTimerIds((prev) => { if (prev.length >= MAX_TIMERS) return prev; const id = smallestFreeId(prev); + setLabels((prevLabels) => ({ ...prevLabels, [id]: `Timer ${id}` })); return [...prev, id].sort((a, b) => a - b); }); }; const removeTimer = (id: number) => { setTimerIds((prev) => (prev.length > MIN_TIMERS ? prev.filter((t) => t !== id) : prev)); + setLabels((prev) => { + const next = { ...prev }; + delete next[id]; + return next; + }); + }; + + const renameTimer = (id: number, label: string) => { + setLabels((prev) => ({ ...prev, [id]: label })); }; return ( @@ -49,7 +62,8 @@ const TimerPanel: React.FC = () => { {timerIds.map((id) => ( renameTimer(id, label)} onRemove={timerIds.length > MIN_TIMERS ? () => removeTimer(id) : undefined} /> ))} diff --git a/src/hooks/useCountdown.ts b/src/hooks/useCountdown.ts index 225f323..c64bf63 100644 --- a/src/hooks/useCountdown.ts +++ b/src/hooks/useCountdown.ts @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'; -export type CountdownStatus = 'idle' | 'running' | 'finished'; +export type CountdownStatus = 'idle' | 'running' | 'paused' | 'finished'; const TICK_INTERVAL_MS = 200; @@ -10,6 +10,8 @@ export interface UseCountdownResult { status: CountdownStatus; remainingMs: number; start: (durationMs: number) => void; + pause: () => void; + resume: () => void; reset: () => void; } @@ -43,11 +45,27 @@ export function useCountdown(): UseCountdownResult { setStatus('running'); }; + const pause = () => { + setStatus((prev) => { + if (prev !== 'running') return prev; + endTimeRef.current = null; + return 'paused'; + }); + }; + + const resume = () => { + setStatus((prev) => { + if (prev !== 'paused') return prev; + endTimeRef.current = Date.now() + remainingMs; + return 'running'; + }); + }; + const reset = () => { endTimeRef.current = null; setRemainingMs(0); setStatus('idle'); }; - return { status, remainingMs, start, reset }; + return { status, remainingMs, start, pause, resume, reset }; } From 4071461f4fb664b29c6a3866162b46aeea711240 Mon Sep 17 00:00:00 2001 From: Ju Date: Tue, 28 Jul 2026 18:07:10 +0000 Subject: [PATCH 2/2] feat: refactor TimerPanel to support multiple timer groups and improve timer management --- src/components/panels/TimerPanel.tsx | 120 ++++++++++++++++++++------- 1 file changed, 92 insertions(+), 28 deletions(-) diff --git a/src/components/panels/TimerPanel.tsx b/src/components/panels/TimerPanel.tsx index 8bba273..48e2be3 100644 --- a/src/components/panels/TimerPanel.tsx +++ b/src/components/panels/TimerPanel.tsx @@ -3,9 +3,17 @@ import React, { useState } from 'react'; import TimerCard from './TimerCard'; -const DEFAULT_TIMER_COUNT = 5; -const MIN_TIMERS = 1; -const MAX_TIMERS = 20; +type TimerGroup = 'orders' | 'other'; + +const DEFAULT_ORDER_COUNT = 3; +const DEFAULT_OTHER_COUNT = 2; +const MIN_TIMERS_PER_GROUP = 1; +const MAX_TIMERS_PER_GROUP = 20; + +const GROUPS: { key: TimerGroup; title: string; addLabel: string }[] = [ + { key: 'orders', title: 'Orders', addLabel: '+ Add Order Timer' }, + { key: 'other', title: 'Cooking', addLabel: '+ Add Timer' }, +]; function smallestFreeId(ids: number[]): number { const used = new Set(ids); @@ -14,30 +22,50 @@ function smallestFreeId(ids: number[]): number { return id; } -const initialIds = Array.from({ length: DEFAULT_TIMER_COUNT }, (_, i) => i + 1); +const initialOrderIds = Array.from({ length: DEFAULT_ORDER_COUNT }, (_, i) => i + 1); +const initialOtherIds = Array.from( + { length: DEFAULT_OTHER_COUNT }, + (_, i) => DEFAULT_ORDER_COUNT + i + 1, +); +const initialIds = [...initialOrderIds, ...initialOtherIds]; const TimerPanel: React.FC = () => { const [timerIds, setTimerIds] = useState(initialIds); + const [groups, setGroups] = useState>(() => ({ + ...Object.fromEntries(initialOrderIds.map((id) => [id, 'orders' as TimerGroup])), + ...Object.fromEntries(initialOtherIds.map((id) => [id, 'other' as TimerGroup])), + })); const [labels, setLabels] = useState>(() => Object.fromEntries(initialIds.map((id) => [id, `Timer ${id}`])), ); - const addTimer = () => { + const addTimer = (group: TimerGroup) => { setTimerIds((prev) => { - if (prev.length >= MAX_TIMERS) return prev; + const countInGroup = prev.filter((id) => groups[id] === group).length; + if (countInGroup >= MAX_TIMERS_PER_GROUP) return prev; const id = smallestFreeId(prev); setLabels((prevLabels) => ({ ...prevLabels, [id]: `Timer ${id}` })); + setGroups((prevGroups) => ({ ...prevGroups, [id]: group })); return [...prev, id].sort((a, b) => a - b); }); }; const removeTimer = (id: number) => { - setTimerIds((prev) => (prev.length > MIN_TIMERS ? prev.filter((t) => t !== id) : prev)); + const group = groups[id]; + const countInGroup = timerIds.filter((t) => groups[t] === group).length; + if (countInGroup <= MIN_TIMERS_PER_GROUP) return; + + setTimerIds((prev) => prev.filter((t) => t !== id)); setLabels((prev) => { const next = { ...prev }; delete next[id]; return next; }); + setGroups((prev) => { + const next = { ...prev }; + delete next[id]; + return next; + }); }; const renameTimer = (id: number, label: string) => { @@ -48,26 +76,37 @@ const TimerPanel: React.FC = () => {

Multi-Timer

-
-
- {timerIds.map((id) => ( - renameTimer(id, label)} - onRemove={timerIds.length > MIN_TIMERS ? () => removeTimer(id) : undefined} - /> - ))} -
+ {GROUPS.map(({ key, title, addLabel }) => { + const idsInGroup = timerIds.filter((id) => groups[id] === key); + return ( +
+
+

{title}

+ +
+ +
+ {idsInGroup.map((id) => ( + renameTimer(id, label)} + onRemove={idsInGroup.length > MIN_TIMERS_PER_GROUP ? () => removeTimer(id) : undefined} + /> + ))} +
+
+ ); + })}