Skip to content

Commit c8bbda9

Browse files
authored
Merge pull request #418 from timbornemann/codex/evaluate-and-migrate-store-structure
refactor: centralize state management with Zustand
2 parents 50ff7e2 + 2b589e5 commit c8bbda9

14 files changed

Lines changed: 205 additions & 202 deletions

docs/frontend.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ The frontend lives in the `src` directory and is built with React 18, TypeScript
1313
- [`src/pages`](../src/pages) – route components; each page represents a top-level feature like tasks, notes or the pomodoro timer.
1414
- [`src/hooks`](../src/hooks) – custom React hooks for shared logic.
1515
- [`src/providers`](../src/providers) – context providers for state like themes or query clients.
16+
- [`src/stores`](../src/stores) – Zustand stores for global state.
1617
- [`src/locales`](../src/locales) – translation files in German and English consumed by `react-i18next`.
1718
- [`src/utils`](../src/utils) and [`src/lib`](../src/lib) – helper functions and abstractions.
18-
- [`src/shared`](../src/shared)Zustand stores and shared types.
19+
- [`src/shared`](../src/shared)shared utilities and types.
1920

2021
## Routing
2122

@@ -25,9 +26,9 @@ defined in [`src/components/Navbar.tsx`](../src/components/Navbar.tsx).
2526

2627
## State Management
2728

28-
Local state is handled with Zustand stores and React hooks. For example,
29-
[`useTimers`](../src/hooks/useTimers.tsx) persists timer state to local storage
30-
and syncs it to the server.
29+
Local state is handled with Zustand stores. For example,
30+
the [timers store](../src/stores/timers.ts) persists timer state to local storage
31+
and syncs it to the server via the [TimersProvider](../src/providers/TimersProvider.tsx).
3132
Server state and caching are provided by `@tanstack/react-query` via context
3233
providers in [`src/providers`](../src/providers).
3334

docs/state-management.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# State Management
2+
3+
This project evaluated different libraries for global state handling. Redux Toolkit offers powerful tooling but introduces boilerplate. Zustand provides a minimal API, shallow learning curve and works well with React's hooks.
4+
5+
We standardize on **Zustand** for client-side state:
6+
7+
- Stores live in [`src/stores`](../src/stores).
8+
- Context providers that persist or sync data reside in [`src/providers`](../src/providers).
9+
- Components and hooks import stores directly, e.g. `import { useTimers } from "@/stores/timers"`.
10+
11+
Existing local stores were migrated into this structure. New global state should follow the same pattern.

src/components/Dashboard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import {
5353
} from "@dnd-kit/sortable";
5454
import { CSS } from "@dnd-kit/utilities";
5555
import Navbar from "./Navbar";
56-
import { usePomodoroStore } from "./PomodoroTimer";
56+
import { usePomodoroStore } from "@/stores/pomodoro";
5757

5858
interface SortableCategoryProps {
5959
category: Category;

src/components/PomodoroTicker.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect, useRef } from "react";
2-
import { usePomodoroStore } from "./PomodoroTimer";
2+
import { usePomodoroStore } from "@/stores/pomodoro";
33
import { usePomodoroHistory } from "@/hooks/usePomodoroHistory.tsx";
44

55
const PomodoroTicker = () => {

src/components/PomodoroTimer.tsx

Lines changed: 1 addition & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import React, { useEffect, useRef, useState } from "react";
22
import { useTranslation } from "react-i18next";
33
import ReactDOM from "react-dom/client";
44
import { Button } from "@/components/ui/button";
5-
import { create } from "zustand";
6-
import { persist } from "zustand/middleware";
5+
import { usePomodoroStore } from "@/stores/pomodoro";
76
import { useSettings, SettingsProvider } from "@/hooks/useSettings";
87
import { hslToHex, hexToHsl, complementaryColor } from "@/utils/color";
98
import { playSound } from "@/utils/sounds";
@@ -12,118 +11,6 @@ import {
1211
PomodoroHistoryProvider,
1312
} from "@/hooks/usePomodoroHistory.tsx";
1413

15-
interface PomodoroState {
16-
isRunning: boolean;
17-
isPaused: boolean;
18-
remainingTime: number;
19-
mode: "work" | "break";
20-
currentTaskId?: string;
21-
workDuration: number;
22-
breakDuration: number;
23-
startTime?: number;
24-
lastTick?: number;
25-
pauseStart?: number;
26-
start: (taskId?: string) => void;
27-
pause: () => void;
28-
resume: () => void;
29-
reset: () => void;
30-
startBreak: () => void;
31-
skipBreak: () => void;
32-
tick: () => void;
33-
setStartTime: (time?: number) => void;
34-
setLastTick: (time: number) => void;
35-
setDurations: (work: number, brk: number) => void;
36-
}
37-
38-
const WORK_DURATION = 25 * 60; // 25 Minuten
39-
const BREAK_DURATION = 5 * 60; // 5 Minuten
40-
41-
export const usePomodoroStore = create<PomodoroState>()(
42-
persist(
43-
(set) => ({
44-
isRunning: false,
45-
isPaused: false,
46-
pauseStart: undefined,
47-
remainingTime: WORK_DURATION,
48-
mode: "work",
49-
currentTaskId: undefined,
50-
workDuration: WORK_DURATION,
51-
breakDuration: BREAK_DURATION,
52-
startTime: undefined,
53-
lastTick: undefined,
54-
start: (taskId?: string) =>
55-
set((state) => ({
56-
isRunning: true,
57-
isPaused: false,
58-
pauseStart: undefined,
59-
remainingTime: state.workDuration,
60-
mode: "work",
61-
currentTaskId: taskId,
62-
startTime: Date.now(),
63-
lastTick: Date.now(),
64-
})),
65-
pause: () => set({ isPaused: true, pauseStart: Date.now() }),
66-
resume: () =>
67-
set({ isPaused: false, lastTick: Date.now(), pauseStart: undefined }),
68-
reset: () =>
69-
set((state) => ({
70-
isRunning: false,
71-
isPaused: false,
72-
remainingTime: state.workDuration,
73-
mode: "work",
74-
currentTaskId: undefined,
75-
startTime: undefined,
76-
lastTick: undefined,
77-
pauseStart: undefined,
78-
})),
79-
startBreak: () =>
80-
set((state) => ({
81-
isRunning: true,
82-
isPaused: false,
83-
pauseStart: undefined,
84-
mode: "break",
85-
remainingTime: state.breakDuration,
86-
lastTick: Date.now(),
87-
})),
88-
skipBreak: () =>
89-
set((state) => ({
90-
isRunning: true,
91-
isPaused: false,
92-
pauseStart: undefined,
93-
mode: "work",
94-
remainingTime: state.workDuration,
95-
lastTick: Date.now(),
96-
})),
97-
tick: () =>
98-
set((state) => {
99-
if (!state.isRunning || state.isPaused) return state;
100-
if (state.remainingTime > 0) {
101-
return {
102-
remainingTime: state.remainingTime - 1,
103-
lastTick: Date.now(),
104-
};
105-
}
106-
const nextMode = state.mode === "work" ? "break" : "work";
107-
return {
108-
mode: nextMode,
109-
remainingTime:
110-
nextMode === "work" ? state.workDuration : state.breakDuration,
111-
lastTick: Date.now(),
112-
} as PomodoroState;
113-
}),
114-
setStartTime: (time) => set({ startTime: time }),
115-
setLastTick: (time) => set({ lastTick: time }),
116-
setDurations: (work, brk) =>
117-
set((state) => ({
118-
workDuration: work,
119-
breakDuration: brk,
120-
remainingTime: !state.isRunning ? work : state.remainingTime,
121-
})),
122-
}),
123-
{ name: "pomodoro" },
124-
),
125-
);
126-
12714
const formatTime = (sec: number) => {
12815
const m = Math.floor(sec / 60)
12916
.toString()

src/components/TimerCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
Settings,
1111
} from "lucide-react";
1212
import TimerCircle from "./TimerCircle";
13-
import { useTimers } from "@/hooks/useTimers.tsx";
13+
import { useTimers } from "@/stores/timers";
1414
import { useSettings } from "@/hooks/useSettings";
1515
import { isColorDark, complementarySameHue, adjustColor } from "@/utils/color";
1616
import { Button } from "@/components/ui/button";

src/components/TimerTicker.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect } from "react";
2-
import { useTimers } from "@/hooks/useTimers.tsx";
2+
import { useTimers } from "@/stores/timers";
33

44
const TimerTicker = () => {
55
const tick = useTimers((state) => state.tick);

src/pages/Kanban.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import TaskCard from "@/components/TaskCard";
44
import Navbar from "@/components/Navbar";
55
import TaskModal from "@/components/TaskModal";
66
import { useNavigate } from "react-router-dom";
7-
import { usePomodoroStore } from "@/components/PomodoroTimer";
7+
import { usePomodoroStore } from "@/stores/pomodoro";
88
import { useToast } from "@/hooks/use-toast";
99
import { Task, TaskFormData } from "@/types";
1010
import { flattenTasks, FlattenedTask } from "@/utils/taskUtils";

src/pages/TimerDetail.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
44
import Navbar from "@/components/Navbar";
55
import TimerCircle from "@/components/TimerCircle";
66
import { Button } from "@/components/ui/button";
7-
import { useTimers } from "@/hooks/useTimers.tsx";
7+
import { useTimers } from "@/stores/timers";
88
import { Play, Pause, RotateCcw, Plus, Edit } from "lucide-react";
99
import { useSettings } from "@/hooks/useSettings";
1010
import { isColorDark, complementaryColor } from "@/utils/color";

src/pages/Timers.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Navbar from "@/components/Navbar";
33
import TimerCard from "@/components/TimerCard";
44
import TimerModal from "@/components/TimerModal";
55
import { Button } from "@/components/ui/button";
6-
import { useTimers } from "@/hooks/useTimers.tsx";
6+
import { useTimers } from "@/stores/timers";
77
import { useTranslation } from "react-i18next";
88
import {
99
DndContext,

0 commit comments

Comments
 (0)