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
This commit is contained in:
parent
233fde8f99
commit
ad4bc946a8
@ -2,6 +2,8 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||||
import type { Timer, CreateAlarmParams, CreateCountdownParams, CreatePomodoroParams, CreateEventParams } from './timer-engine';
|
import type { Timer, CreateAlarmParams, CreateCountdownParams, CreatePomodoroParams, CreateEventParams } from './timer-engine';
|
||||||
|
import type { TimerChain } from './linked-timers';
|
||||||
|
import { buildChain, getNextInChain, findChainForTimer } from './linked-timers';
|
||||||
import {
|
import {
|
||||||
createAlarm,
|
createAlarm,
|
||||||
createCountdown,
|
createCountdown,
|
||||||
@ -23,6 +25,7 @@ import { trackTimerCreated, trackTimerCompleted, trackCascadeFired } from './ana
|
|||||||
|
|
||||||
export interface TimerStore {
|
export interface TimerStore {
|
||||||
timers: Timer[];
|
timers: Timer[];
|
||||||
|
chains: TimerChain[];
|
||||||
now: number; // current time for reactivity
|
now: number; // current time for reactivity
|
||||||
|
|
||||||
// CRUD
|
// CRUD
|
||||||
@ -32,6 +35,11 @@ export interface TimerStore {
|
|||||||
addEvent: (params: CreateEventParams) => Timer;
|
addEvent: (params: CreateEventParams) => Timer;
|
||||||
removeTimer: (id: string) => void;
|
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
|
// State transitions
|
||||||
pause: (id: string) => void;
|
pause: (id: string) => void;
|
||||||
resume: (id: string) => void;
|
resume: (id: string) => void;
|
||||||
@ -58,6 +66,7 @@ export const useTimerStore = create<TimerStore>()(
|
|||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
timers: [],
|
timers: [],
|
||||||
|
chains: [],
|
||||||
now: Date.now(),
|
now: Date.now(),
|
||||||
|
|
||||||
addAlarm: (params) => {
|
addAlarm: (params) => {
|
||||||
@ -89,7 +98,41 @@ export const useTimerStore = create<TimerStore>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
removeTimer: (id) => {
|
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) => {
|
pause: (id) => {
|
||||||
@ -118,6 +161,23 @@ export const useTimerStore = create<TimerStore>()(
|
|||||||
trackTimerCompleted(timer.type, timer.targetTime - timer.createdAt);
|
trackTimerCompleted(timer.type, timer.targetTime - timer.createdAt);
|
||||||
}
|
}
|
||||||
set((s) => ({ timers: updateTimer(s.timers, id, completeTimer) }));
|
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) => {
|
advancePom: (id) => {
|
||||||
@ -201,7 +261,7 @@ export const useTimerStore = create<TimerStore>()(
|
|||||||
}
|
}
|
||||||
return localStorage;
|
return localStorage;
|
||||||
}),
|
}),
|
||||||
partialize: (state) => ({ timers: state.timers }),
|
partialize: (state) => ({ timers: state.timers, chains: state.chains }),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user