From ad4bc946a8241c7cec6474b71bb2e279a97405fe Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Sat, 28 Feb 2026 13:52:20 -0800 Subject: [PATCH] feat(web): wire TimerChain[] into Zustand store with auto-start - chains[] persisted alongside timers in localStorage - addChain/removeChain with linkedTimerId bookkeeping - removeTimer auto-cleans chain references - complete() auto-starts next timer in chain via getNextInChain - 373/373 tests pass --- web/src/lib/store.ts | 64 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/web/src/lib/store.ts b/web/src/lib/store.ts index 1a259b0..19ff539 100644 --- a/web/src/lib/store.ts +++ b/web/src/lib/store.ts @@ -2,6 +2,8 @@ import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; import type { Timer, CreateAlarmParams, CreateCountdownParams, CreatePomodoroParams, CreateEventParams } from './timer-engine'; +import type { TimerChain } from './linked-timers'; +import { buildChain, getNextInChain, findChainForTimer } from './linked-timers'; import { createAlarm, createCountdown, @@ -23,6 +25,7 @@ import { trackTimerCreated, trackTimerCompleted, trackCascadeFired } from './ana export interface TimerStore { timers: Timer[]; + chains: TimerChain[]; now: number; // current time for reactivity // CRUD @@ -32,6 +35,11 @@ export interface TimerStore { addEvent: (params: CreateEventParams) => Timer; removeTimer: (id: string) => void; + // Chain operations + addChain: (name: string, timerIds: string[], delayMs?: number) => TimerChain; + removeChain: (chainId: string) => void; + getChainForTimer: (timerId: string) => TimerChain | null; + // State transitions pause: (id: string) => void; resume: (id: string) => void; @@ -58,6 +66,7 @@ export const useTimerStore = create()( persist( (set, get) => ({ timers: [], + chains: [], now: Date.now(), addAlarm: (params) => { @@ -89,7 +98,41 @@ export const useTimerStore = create()( }, removeTimer: (id) => { - set((s) => ({ timers: s.timers.filter((t) => t.id !== id) })); + set((s) => ({ + timers: s.timers.filter((t) => t.id !== id), + chains: s.chains + .map((c) => ({ + ...c, + timerIds: c.timerIds.filter((tid) => tid !== id), + links: c.links.filter((l) => l.fromId !== id && l.toId !== id), + })) + .filter((c) => c.timerIds.length > 1), + })); + }, + + addChain: (name, timerIds, delayMs = 0) => { + const chain = buildChain(name, timerIds, delayMs); + set((s) => ({ + chains: [...s.chains, chain], + timers: s.timers.map((t) => + timerIds.includes(t.id) ? { ...t, linkedTimerId: chain.id } : t + ), + })); + return chain; + }, + + removeChain: (chainId) => { + const chain = get().chains.find((c) => c.id === chainId); + set((s) => ({ + chains: s.chains.filter((c) => c.id !== chainId), + timers: s.timers.map((t) => + chain && chain.timerIds.includes(t.id) ? { ...t, linkedTimerId: null } : t + ), + })); + }, + + getChainForTimer: (timerId) => { + return findChainForTimer(get().chains, timerId); }, pause: (id) => { @@ -118,6 +161,23 @@ export const useTimerStore = create()( trackTimerCompleted(timer.type, timer.targetTime - timer.createdAt); } set((s) => ({ timers: updateTimer(s.timers, id, completeTimer) })); + // Auto-start next timer in chain + const { chains, timers } = get(); + const link = getNextInChain(chains, id); + if (link) { + const nextTimer = timers.find((t) => t.id === link.toId); + if (nextTimer && nextTimer.state === 'idle') { + const now = Date.now(); + const targetTime = now + (nextTimer.duration ?? 0) + link.delay; + set((s) => ({ + timers: s.timers.map((t) => + t.id === link.toId + ? { ...t, state: 'active' as const, startedAt: now + link.delay, targetTime } + : t + ), + })); + } + } }, advancePom: (id) => { @@ -201,7 +261,7 @@ export const useTimerStore = create()( } return localStorage; }), - partialize: (state) => ({ timers: state.timers }), + partialize: (state) => ({ timers: state.timers, chains: state.chains }), } ) );