From 24fa8277ce093a0eb918a466ebb02760c7f2691b Mon Sep 17 00:00:00 2001 From: Vladyslav Soloviov Date: Sun, 17 Aug 2025 20:39:28 +0300 Subject: [PATCH 01/30] markup and basic logic --- src/App.tsx | 89 +++++++++++++++++++++++++++++++++++- src/Settings.tsx | 40 ++++++++++++++++ src/Timers.tsx | 116 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 src/Settings.tsx create mode 100644 src/Timers.tsx diff --git a/src/App.tsx b/src/App.tsx index 29b4758..1d2af8b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1 +1,88 @@ -export const App = () =>
+import * as React from "react" +import { + FluentProvider, + webLightTheme, + webDarkTheme, + Tab, + TabList, + makeStaticStyles, +} from "@fluentui/react-components" +import { + WrenchSettingsFilled, + WrenchSettingsRegular, + TimerFilled, + TimerRegular, + bundleIcon, +} from "@fluentui/react-icons" +import type { + SelectTabData, + SelectTabEvent, + TabValue, + Theme, +} from "@fluentui/react-components" +import { Timers } from "./Timers" +import { Settings } from "./Settings" + +const TimerIcon = bundleIcon(TimerFilled, TimerRegular) +const SettingsIcon = bundleIcon(WrenchSettingsFilled, WrenchSettingsRegular) + +const useGlobalStyles = makeStaticStyles([ + "html, body, #root, .App { margin: 0; padding: 0; height: 100vh; }", +]) + +const THEMES: { [key: string]: Theme } = { + dark: webDarkTheme, + light: webLightTheme, +} + +export const App = () => { + useGlobalStyles() + + const [theme, setTheme] = React.useState("light") + + const [selectedValue, setSelectedValue] = React.useState("timers") + + const [tabsView, setTabsView] = React.useState(true) + + const onTabSelect = (event: SelectTabEvent, data: SelectTabData) => { + setSelectedValue(data.value) + } + + return ( + +
+ {tabsView ? ( + <> + + } value="timers"> + Timers + + } value="settings"> + Settings + + +
+ {selectedValue === "timers" && } + {selectedValue === "settings" && ( + + )} +
+ + ) : ( +
+ + +
+ )} +
+
+ ) +} diff --git a/src/Settings.tsx b/src/Settings.tsx new file mode 100644 index 0000000..42a9196 --- /dev/null +++ b/src/Settings.tsx @@ -0,0 +1,40 @@ +import * as React from "react" +import { + Field, + Radio, + RadioGroup, + Checkbox, + Button, +} from "@fluentui/react-components" +import type { CheckboxProps } from "@fluentui/react-components" + +export const Settings = (props: any) => { + const [checked, setChecked] = React.useState(true) + + return ( + <> + + props.setTheme(data.value)} + > + + + + + setChecked(data.checked)} + label="Preserve local timer between sessions" + /> +
+ + + ) +} diff --git a/src/Timers.tsx b/src/Timers.tsx new file mode 100644 index 0000000..ad6e247 --- /dev/null +++ b/src/Timers.tsx @@ -0,0 +1,116 @@ +import { + TableBody, + TableCell, + TableRow, + Table, + TableHeader, + TableHeaderCell, + Input, + Label, + Button, + makeStyles, + useId, +} from "@fluentui/react-components" + +const items = [ + { + name: "name", + value: "value", + action: "action", + }, + // { + // file: { label: "Meeting notes", icon: }, + // author: { label: "Max Mustermann", status: "available" }, + // lastUpdated: { label: "7h ago", timestamp: 1 }, + // lastUpdate: { + // label: "You edited this", + // icon: , + // }, + // }, + // { + // file: { label: "Thursday presentation", icon: }, + // author: { label: "Erika Mustermann", status: "busy" }, + // lastUpdated: { label: "Yesterday at 1:45 PM", timestamp: 2 }, + // lastUpdate: { + // label: "You recently opened this", + // icon: , + // }, + // }, + // { + // file: { label: "Training recording", icon: }, + // author: { label: "John Doe", status: "away" }, + // lastUpdated: { label: "Yesterday at 1:45 PM", timestamp: 2 }, + // lastUpdate: { + // label: "You recently opened this", + // icon: , + // }, + // }, + // { + // file: { label: "Purchase order", icon: }, + // author: { label: "Jane Doe", status: "offline" }, + // lastUpdated: { label: "Tue at 9:30 AM", timestamp: 3 }, + // lastUpdate: { + // label: "You shared this in a Teams chat", + // icon: , + // }, + // }, +] + +const columns = [ + { columnKey: "name", label: "Name" }, + { columnKey: "value", label: "Value" }, + { columnKey: "action", label: "Action" }, +] + +const useStyles = makeStyles({ + form: { + display: "flex", + flexDirection: "row", + }, + input: { + // Stack the label above the field + display: "flex", + flexDirection: "column", + // Use 2px gap below the label (per the design system) + gap: "2px", + // Prevent the example from taking the full width of the page (optional) + maxWidth: "400px", + }, +}) + +export const Timers = () => { + const inputId = useId("input") + const styles = useStyles() + + return ( + <> + + + + {columns.map(column => ( + + {column.label} + + ))} + + + + {items.map(item => ( + + {item.name} + {item.value} + {item.action} + + ))} + +
+
+ +
+ + +
+
+ + ) +} From 8c5b3750bd0a34a61edd901fbf7a0adffca17182 Mon Sep 17 00:00:00 2001 From: Vladyslav Soloviov Date: Sun, 24 Aug 2025 19:00:57 +0300 Subject: [PATCH 02/30] added redux, implemented single timer, rewrited structure --- package-lock.json | 2 +- package.json | 2 +- src/App.tsx | 88 ---------------------- src/Settings.tsx | 40 ---------- src/Timers.tsx | 116 ----------------------------- src/app/App.tsx | 65 ++++++++++++++++ src/app/hooks.ts | 6 ++ src/app/store.ts | 19 +++++ src/app/withTypes.ts | 8 ++ src/components/TabsView.tsx | 41 ++++++++++ src/features/settings/Settings.tsx | 43 +++++++++++ src/features/settings/slice.ts | 32 ++++++++ src/features/timer/ServerTimer.tsx | 21 ++++++ src/features/timer/Timer.tsx | 24 ++++++ src/features/timer/slice.ts | 18 +++++ src/features/timers/Timers.tsx | 113 ++++++++++++++++++++++++++++ src/features/timers/api.ts | 24 ++++++ src/features/timers/slice.ts | 62 +++++++++++++++ src/features/timers/thunks.ts | 59 +++++++++++++++ src/features/timers/types.ts | 18 +++++ src/hooks/useTicker.ts | 14 ++++ src/main.tsx | 8 +- tsconfig.app.json | 6 +- vite.config.ts | 6 ++ 24 files changed, 586 insertions(+), 249 deletions(-) delete mode 100644 src/App.tsx delete mode 100644 src/Settings.tsx delete mode 100644 src/Timers.tsx create mode 100644 src/app/App.tsx create mode 100644 src/app/hooks.ts create mode 100644 src/app/store.ts create mode 100644 src/app/withTypes.ts create mode 100644 src/components/TabsView.tsx create mode 100644 src/features/settings/Settings.tsx create mode 100644 src/features/settings/slice.ts create mode 100644 src/features/timer/ServerTimer.tsx create mode 100644 src/features/timer/Timer.tsx create mode 100644 src/features/timer/slice.ts create mode 100644 src/features/timers/Timers.tsx create mode 100644 src/features/timers/api.ts create mode 100644 src/features/timers/slice.ts create mode 100644 src/features/timers/thunks.ts create mode 100644 src/features/timers/types.ts create mode 100644 src/hooks/useTicker.ts diff --git a/package-lock.json b/package-lock.json index 64fe1bc..4fe7364 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "dependencies": { "@fluentui/react-components": "^9.68.2", - "@reduxjs/toolkit": "^2.6.1", + "@reduxjs/toolkit": "^2.8.2", "react": "^18.3.1", "react-dom": "^18.3.1", "react-redux": "^9.2.0" diff --git a/package.json b/package.json index 590876b..52effbd 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ }, "dependencies": { "@fluentui/react-components": "^9.68.2", - "@reduxjs/toolkit": "^2.6.1", + "@reduxjs/toolkit": "^2.8.2", "react": "^18.3.1", "react-dom": "^18.3.1", "react-redux": "^9.2.0" diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index 1d2af8b..0000000 --- a/src/App.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import * as React from "react" -import { - FluentProvider, - webLightTheme, - webDarkTheme, - Tab, - TabList, - makeStaticStyles, -} from "@fluentui/react-components" -import { - WrenchSettingsFilled, - WrenchSettingsRegular, - TimerFilled, - TimerRegular, - bundleIcon, -} from "@fluentui/react-icons" -import type { - SelectTabData, - SelectTabEvent, - TabValue, - Theme, -} from "@fluentui/react-components" -import { Timers } from "./Timers" -import { Settings } from "./Settings" - -const TimerIcon = bundleIcon(TimerFilled, TimerRegular) -const SettingsIcon = bundleIcon(WrenchSettingsFilled, WrenchSettingsRegular) - -const useGlobalStyles = makeStaticStyles([ - "html, body, #root, .App { margin: 0; padding: 0; height: 100vh; }", -]) - -const THEMES: { [key: string]: Theme } = { - dark: webDarkTheme, - light: webLightTheme, -} - -export const App = () => { - useGlobalStyles() - - const [theme, setTheme] = React.useState("light") - - const [selectedValue, setSelectedValue] = React.useState("timers") - - const [tabsView, setTabsView] = React.useState(true) - - const onTabSelect = (event: SelectTabEvent, data: SelectTabData) => { - setSelectedValue(data.value) - } - - return ( - -
- {tabsView ? ( - <> - - } value="timers"> - Timers - - } value="settings"> - Settings - - -
- {selectedValue === "timers" && } - {selectedValue === "settings" && ( - - )} -
- - ) : ( -
- - -
- )} -
-
- ) -} diff --git a/src/Settings.tsx b/src/Settings.tsx deleted file mode 100644 index 42a9196..0000000 --- a/src/Settings.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from "react" -import { - Field, - Radio, - RadioGroup, - Checkbox, - Button, -} from "@fluentui/react-components" -import type { CheckboxProps } from "@fluentui/react-components" - -export const Settings = (props: any) => { - const [checked, setChecked] = React.useState(true) - - return ( - <> - - props.setTheme(data.value)} - > - - - - - setChecked(data.checked)} - label="Preserve local timer between sessions" - /> -
- - - ) -} diff --git a/src/Timers.tsx b/src/Timers.tsx deleted file mode 100644 index ad6e247..0000000 --- a/src/Timers.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { - TableBody, - TableCell, - TableRow, - Table, - TableHeader, - TableHeaderCell, - Input, - Label, - Button, - makeStyles, - useId, -} from "@fluentui/react-components" - -const items = [ - { - name: "name", - value: "value", - action: "action", - }, - // { - // file: { label: "Meeting notes", icon: }, - // author: { label: "Max Mustermann", status: "available" }, - // lastUpdated: { label: "7h ago", timestamp: 1 }, - // lastUpdate: { - // label: "You edited this", - // icon: , - // }, - // }, - // { - // file: { label: "Thursday presentation", icon: }, - // author: { label: "Erika Mustermann", status: "busy" }, - // lastUpdated: { label: "Yesterday at 1:45 PM", timestamp: 2 }, - // lastUpdate: { - // label: "You recently opened this", - // icon: , - // }, - // }, - // { - // file: { label: "Training recording", icon: }, - // author: { label: "John Doe", status: "away" }, - // lastUpdated: { label: "Yesterday at 1:45 PM", timestamp: 2 }, - // lastUpdate: { - // label: "You recently opened this", - // icon: , - // }, - // }, - // { - // file: { label: "Purchase order", icon: }, - // author: { label: "Jane Doe", status: "offline" }, - // lastUpdated: { label: "Tue at 9:30 AM", timestamp: 3 }, - // lastUpdate: { - // label: "You shared this in a Teams chat", - // icon: , - // }, - // }, -] - -const columns = [ - { columnKey: "name", label: "Name" }, - { columnKey: "value", label: "Value" }, - { columnKey: "action", label: "Action" }, -] - -const useStyles = makeStyles({ - form: { - display: "flex", - flexDirection: "row", - }, - input: { - // Stack the label above the field - display: "flex", - flexDirection: "column", - // Use 2px gap below the label (per the design system) - gap: "2px", - // Prevent the example from taking the full width of the page (optional) - maxWidth: "400px", - }, -}) - -export const Timers = () => { - const inputId = useId("input") - const styles = useStyles() - - return ( - <> - - - - {columns.map(column => ( - - {column.label} - - ))} - - - - {items.map(item => ( - - {item.name} - {item.value} - {item.action} - - ))} - -
-
- -
- - -
-
- - ) -} diff --git a/src/app/App.tsx b/src/app/App.tsx new file mode 100644 index 0000000..080a213 --- /dev/null +++ b/src/app/App.tsx @@ -0,0 +1,65 @@ +import { + FluentProvider, + webLightTheme, + webDarkTheme, + makeStaticStyles, +} from "@fluentui/react-components" +import { + WrenchSettingsFilled, + WrenchSettingsRegular, + TimerFilled, + TimerRegular, + bundleIcon, +} from "@fluentui/react-icons" +import type { Theme } from "@fluentui/react-components" +import { useAppSelector } from "@/app/hooks" +import { Timers } from "@/features/timers/Timers" +import { Settings } from "@/features/settings/Settings" +import { selectTabsView, selectTheme } from "@/features/settings/slice" +import { TabsView } from "@/components/TabsView" +import { useTicker } from "@/hooks/useTicker" + +const TimerIcon = bundleIcon(TimerFilled, TimerRegular) +const SettingsIcon = bundleIcon(WrenchSettingsFilled, WrenchSettingsRegular) + +const useGlobalStyles = makeStaticStyles([ + "html, body, #root, .App { margin: 0; padding: 0; height: 100vh; }", +]) + +const THEMES: { [key: string]: Theme } = { + dark: webDarkTheme, + light: webLightTheme, +} + +export const App = () => { + useGlobalStyles() + useTicker(250) + + const tabsView = useAppSelector(selectTabsView) + const theme: string = useAppSelector(selectTheme) + + const tabs = [ + { id: "timers", label: "Timers", icon: , content: }, + { + id: "settings", + label: "Settings", + icon: , + content: , + }, + ] + + return ( + +
+ {tabsView ? ( + + ) : ( +
+ + +
+ )} +
+
+ ) +} diff --git a/src/app/hooks.ts b/src/app/hooks.ts new file mode 100644 index 0000000..8b116d0 --- /dev/null +++ b/src/app/hooks.ts @@ -0,0 +1,6 @@ +import { useDispatch, useSelector, useStore } from "react-redux" +import type { AppDispatch, AppStore, RootState } from "./store" + +export const useAppDispatch = useDispatch.withTypes() +export const useAppSelector = useSelector.withTypes() +export const useAppStore = useStore.withTypes() diff --git a/src/app/store.ts b/src/app/store.ts new file mode 100644 index 0000000..1bc386b --- /dev/null +++ b/src/app/store.ts @@ -0,0 +1,19 @@ +import { configureStore } from "@reduxjs/toolkit" +import settingsReducer from "@/features/settings/slice" +import timersReducer from "@/features/timers/slice" +import timerReducer from "@/features/timer/slice" + +export const store = configureStore({ + reducer: { + settings: settingsReducer, + timers: timersReducer, + timer: timerReducer, + }, +}) + +// Get the type of our store variable +export type AppStore = typeof store +// Infer the `RootState` and `AppDispatch` types from the store itself +export type RootState = ReturnType +// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} +export type AppDispatch = AppStore["dispatch"] diff --git a/src/app/withTypes.ts b/src/app/withTypes.ts new file mode 100644 index 0000000..1160a56 --- /dev/null +++ b/src/app/withTypes.ts @@ -0,0 +1,8 @@ +import { createAsyncThunk } from "@reduxjs/toolkit" + +import type { RootState, AppDispatch } from "./store" + +export const createAppAsyncThunk = createAsyncThunk.withTypes<{ + state: RootState + dispatch: AppDispatch +}>() diff --git a/src/components/TabsView.tsx b/src/components/TabsView.tsx new file mode 100644 index 0000000..c9acf2d --- /dev/null +++ b/src/components/TabsView.tsx @@ -0,0 +1,41 @@ +import { ReactNode, useState } from "react" +import { TabList, Tab } from "@fluentui/react-components" +import type { + TabSlots, + SelectTabData, + SelectTabEvent, +} from "@fluentui/react-components" + +type TabConfig = { + id: string + label: string + icon?: TabSlots["icon"] + content: ReactNode +} + +interface TabsViewProps { + tabs: TabConfig[] + selected: string +} + +export function TabsView({ tabs, selected }: TabsViewProps) { + const [selectedTab, setSelectedTab] = useState(selected) + + const onTabSelect = (event: SelectTabEvent, data: SelectTabData) => { + setSelectedTab(data.value as string) + } + + return ( + <> + + {tabs.map(({ id, icon, label }) => ( + + {label} + + ))} + + +
{tabs.find(tab => tab.id === selectedTab)?.content}
+ + ) +} diff --git a/src/features/settings/Settings.tsx b/src/features/settings/Settings.tsx new file mode 100644 index 0000000..dbd86ef --- /dev/null +++ b/src/features/settings/Settings.tsx @@ -0,0 +1,43 @@ +import { + Field, + Radio, + RadioGroup, + Checkbox, + Button, +} from "@fluentui/react-components" +import { useAppDispatch, useAppSelector } from "@/app/hooks" +import { + toggleTabs, + setTheme, + togglePreserveLocalTimer, +} from "@/features/settings/slice" +import { selectTabsView, selectTheme } from "@/features/settings/slice" + +export const Settings = () => { + const dispatch = useAppDispatch() + const tabsView = useAppSelector(selectTabsView) + const theme = useAppSelector(selectTheme) + + return ( + <> + + dispatch(setTheme(data.value))} + > + + + + + dispatch(togglePreserveLocalTimer())} + label="Preserve local timer between sessions" + /> +
+ + + ) +} diff --git a/src/features/settings/slice.ts b/src/features/settings/slice.ts new file mode 100644 index 0000000..45a4996 --- /dev/null +++ b/src/features/settings/slice.ts @@ -0,0 +1,32 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit" +import { RootState } from "@/app/store" + +export const settings = createSlice({ + name: "tabsView", + initialState: { + tabsView: true, + theme: "light", + preserveLocalTimer: false, + }, + reducers: { + toggleTabs: state => { + state.tabsView = !state.tabsView + }, + setTheme: (state, action: PayloadAction) => { + state.theme = action.payload + }, + togglePreserveLocalTimer: state => { + state.preserveLocalTimer = !state.preserveLocalTimer + }, + }, +}) + +export const { toggleTabs, setTheme, togglePreserveLocalTimer } = + settings.actions + +export const selectTabsView = (state: RootState) => state.settings.tabsView +export const selectTheme = (state: RootState) => state.settings.theme +export const selectPreserveLocalTimer = (state: RootState) => + state.settings.preserveLocalTimer + +export default settings.reducer diff --git a/src/features/timer/ServerTimer.tsx b/src/features/timer/ServerTimer.tsx new file mode 100644 index 0000000..94dc1c3 --- /dev/null +++ b/src/features/timer/ServerTimer.tsx @@ -0,0 +1,21 @@ +import { useEffect, useState } from "react" +import { useAppSelector } from "@/app/hooks" +import { selectLocalTimer } from "./slice" +import { Timer } from "@/features/timers/types" + +function formatElapsed(seconds: number) { + const hrs = Math.floor(seconds / 3600) + const mins = Math.floor((seconds % 3600) / 60) + const secs = Math.floor(seconds % 60) + + return `${hrs.toString().padStart(2, "0")}:${mins + .toString() + .padStart(2, "0")}:${secs.toString().padStart(2, "0")}` +} + +export default function ServerTimer({ elapsed, receivedAt }: Timer) { + useAppSelector(selectLocalTimer) + const now = Date.now() / 1000 + + return <>{formatElapsed(elapsed + (now - receivedAt))} +} diff --git a/src/features/timer/Timer.tsx b/src/features/timer/Timer.tsx new file mode 100644 index 0000000..5da2ab6 --- /dev/null +++ b/src/features/timer/Timer.tsx @@ -0,0 +1,24 @@ +import { useEffect, useState } from "react" +import { useAppSelector } from "@/app/hooks" +import { selectLocalTimer } from "./slice" + +function formatTime(totalSeconds: number): string { + const hours = String(Math.floor(totalSeconds / 3600)).padStart(2, "0") + const minutes = String(Math.floor((totalSeconds % 3600) / 60)).padStart( + 2, + "0", + ) + const seconds = String(totalSeconds % 60).padStart(2, "0") + return `${hours}:${minutes}:${seconds}` +} + +export default function Timer({ inputSeconds }: { inputSeconds?: number }) { + const localTimer: number = useAppSelector(selectLocalTimer) + const [seconds, setSeconds] = useState(inputSeconds || 0) + + useEffect(() => { + setSeconds(prev => prev + 1) + }, [localTimer]) + + return <>{formatTime(seconds)} +} diff --git a/src/features/timer/slice.ts b/src/features/timer/slice.ts new file mode 100644 index 0000000..81d91f5 --- /dev/null +++ b/src/features/timer/slice.ts @@ -0,0 +1,18 @@ +import { createSlice } from "@reduxjs/toolkit" +import type { RootState } from "@/app/store" + +export const timer = createSlice({ + name: "timer", + initialState: { localTimer: 0 }, + reducers: { + tick: state => { + state.localTimer += 1 + }, + }, +}) + +export const { tick } = timer.actions + +export default timer.reducer + +export const selectLocalTimer = (state: RootState) => state.timer.localTimer diff --git a/src/features/timers/Timers.tsx b/src/features/timers/Timers.tsx new file mode 100644 index 0000000..55d90f8 --- /dev/null +++ b/src/features/timers/Timers.tsx @@ -0,0 +1,113 @@ +import { useState, useEffect, FormEvent } from "react" +import { + TableBody, + TableCell, + TableRow, + Table, + TableHeader, + TableHeaderCell, + Input, + Label, + Button, + makeStyles, + useId, +} from "@fluentui/react-components" +import { useAppSelector, useAppDispatch } from "@/app/hooks" +import { selectAllTimers, selectTimersStatus } from "@/features/timers/slice" +import { fetchTimers, createTimer } from "@/features/timers/thunks" +import Timer from "@/features/timer/Timer" +import ServerTimer from "../timer/ServerTimer" + +const columns = [ + { columnKey: "name", label: "Name" }, + { columnKey: "value", label: "Value" }, + { columnKey: "action", label: "Action" }, +] + +const useStyles = makeStyles({ + form: { + display: "flex", + flexDirection: "row", + }, + input: { + display: "flex", + flexDirection: "column", + gap: "2px", + maxWidth: "400px", + }, +}) + +export const Timers = () => { + const inputId = useId("input") + const styles = useStyles() + const dispatch = useAppDispatch() + const timers = useAppSelector(selectAllTimers) + const timersStatus = useAppSelector(selectTimersStatus) + const [newTimerName, setNewTimerName] = useState("") + + useEffect(() => { + if (timersStatus === "idle") { + dispatch(fetchTimers()) + } + + const interval = setInterval(() => { + dispatch(fetchTimers()) + }, 30000) + return () => clearInterval(interval) + }, []) + + const onSubmit = (e: FormEvent) => { + e.preventDefault() + + dispatch(createTimer(newTimerName)).then(() => dispatch(fetchTimers())) + } + + return ( + <> + + + + {columns.map(column => ( + + {column.label} + + ))} + + + + + local + + + + + + + + {timers.map(([id, timer]) => ( + + {id} + + + + + + + + ))} + +
+
+ +
+ setNewTimerName(e.target.value)} + /> + +
+
+ + ) +} diff --git a/src/features/timers/api.ts b/src/features/timers/api.ts new file mode 100644 index 0000000..1aebfbf --- /dev/null +++ b/src/features/timers/api.ts @@ -0,0 +1,24 @@ +import { Timer, TimerResponse } from "./types" + +export async function getTimer(id: string): Promise { + const response = await fetch(`http://localhost:8080/${id}`) + const data: Timer = await response.json() + + return data +} + +export async function getListOfTimers(): Promise { + const response = await fetch("http://localhost:8080/list") + const data: string[] = await response.json() + + return data +} + +export async function postTimer(id: string): Promise { + const response = await fetch(`http://localhost:8080/reset/${id}`, { + method: "POST", + }) + const data: Timer = await response.json() + + return data +} diff --git a/src/features/timers/slice.ts b/src/features/timers/slice.ts new file mode 100644 index 0000000..546e821 --- /dev/null +++ b/src/features/timers/slice.ts @@ -0,0 +1,62 @@ +import { createSlice, createSelector } from "@reduxjs/toolkit" +import type { RootState } from "@/app/store" +import { Timer, TimersState } from "./types" +import { fetchTimers, createTimer } from "./thunks" + +const initialState: TimersState = { + map: {}, + status: "idle", + error: null, +} + +export const timers = createSlice({ + name: "timers", + initialState, + reducers: {}, + extraReducers: builder => { + builder.addCase(fetchTimers.pending, (state, action) => { + state.status = "pending" + }) + builder.addCase(fetchTimers.fulfilled, (state, action) => { + state.status = "succeeded" + state.map = action.payload.reduce( + (acc, { id, start, elapsed, receivedAt }) => { + acc[id] = { start, elapsed, receivedAt } + return acc + }, + {} as Record, + ) + }) + builder.addCase(fetchTimers.rejected, (state, action) => { + state.status = "failed" + state.error = action.error.message ?? "Unknown Error" + }) + builder.addCase(createTimer.pending, (state, action) => { + state.status = "pending" + }) + builder.addCase(createTimer.fulfilled, (state, action) => { + // TODO + // state.status = "succeeded" + // state.timers = action.payload + // action.payload.forEach(id => { + // dispatch(fetchTimerById(id)) + // }) + }) + builder.addCase(createTimer.rejected, (state, action: any) => { + state.status = "failed" + state.error = action.error.message ?? "Unknown Error" + }) + }, +}) + +export const {} = timers.actions + +export default timers.reducer + +const selectTimerMap = (state: RootState) => state.timers.map +export const selectAllTimers = createSelector( + selectTimerMap, + (map: Record) => Object.entries(map), +) +export const selectTimersStatus = (state: RootState) => state.timers.status +export const selectTimersError = (state: RootState) => state.timers.error diff --git a/src/features/timers/thunks.ts b/src/features/timers/thunks.ts new file mode 100644 index 0000000..154affb --- /dev/null +++ b/src/features/timers/thunks.ts @@ -0,0 +1,59 @@ +import { createAppAsyncThunk } from "@/app/withTypes" +import { TimerResponse, TimerWithId } from "./types" +import { getTimer, getListOfTimers, postTimer } from "./api" + +export const fetchTimerById = createAppAsyncThunk( + "timer/fetchTimerById", + async (id: string, { rejectWithValue }) => { + try { + const response: TimerResponse = await getTimer(id) + + return response + } catch (err: unknown) { + if (err instanceof Error) { + return rejectWithValue(err.message) + } + return rejectWithValue("Unknown error") + } + }, +) + +export const fetchTimers = createAppAsyncThunk< + TimerWithId[], + void, + { rejectValue: string } +>("timer/fetchTimers", async (_, { rejectWithValue }) => { + try { + const ids: string[] = await getListOfTimers() + + return Promise.all( + ids.map(async id => { + const response: TimerResponse = await getTimer(id) + const receivedAt = Date.now() / 1000 + + return { id, ...response, receivedAt } + }), + ) + } catch (err: unknown) { + if (err instanceof Error) { + return rejectWithValue(err.message) + } + return rejectWithValue("Unknown error") + } +}) + +export const createTimer = createAppAsyncThunk( + "timer/createTimer", + async (id: string, { rejectWithValue }) => { + try { + const response: TimerResponse = await postTimer(id) + + return response + } catch (err: unknown) { + if (err instanceof Error) { + return rejectWithValue(err.message) + } + return rejectWithValue("Unknown error") + } + }, +) diff --git a/src/features/timers/types.ts b/src/features/timers/types.ts new file mode 100644 index 0000000..df9ba46 --- /dev/null +++ b/src/features/timers/types.ts @@ -0,0 +1,18 @@ +type WithId = T & { id: string } + +export interface TimerResponse { + start: number + elapsed: number +} + +export interface Timer extends TimerResponse { + receivedAt: number +} + +export type TimerWithId = WithId + +export interface TimersState { + map: Record + status: "idle" | "pending" | "succeeded" | "failed" + error: string | null +} diff --git a/src/hooks/useTicker.ts b/src/hooks/useTicker.ts new file mode 100644 index 0000000..97a2115 --- /dev/null +++ b/src/hooks/useTicker.ts @@ -0,0 +1,14 @@ +import { useEffect } from "react" +import { tick } from "@/features/timer/slice" +import { useAppDispatch } from "@/app/hooks" + +export function useTicker(intervalMs: number = 1000) { + const dispatch = useAppDispatch() + + useEffect(() => { + const intervalId = setInterval(() => { + dispatch(tick()) + }, intervalMs) + return () => clearInterval(intervalId) + }, []) +} diff --git a/src/main.tsx b/src/main.tsx index bf27bf0..d23c174 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,8 @@ import { StrictMode } from "react" import { createRoot } from "react-dom/client" -import { App } from "./App" +import { App } from "./app/App" +import { store } from "./app/store" +import { Provider } from "react-redux" const container = document.getElementById("root") @@ -9,7 +11,9 @@ if (container) { root.render( - + + + , ) } else { diff --git a/tsconfig.app.json b/tsconfig.app.json index ea6a38d..67f76ff 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -22,7 +22,11 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true, - "types": ["vitest/globals"] + "types": ["vitest/globals"], + "baseUrl": "src", + "paths": { + "@/*": ["*"] + } }, "include": ["src"] } diff --git a/vite.config.ts b/vite.config.ts index 0fd6681..93125a9 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,6 +7,12 @@ import packageJson from "./package.json" with { type: "json" } export default defineConfig({ plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + server: { open: true, }, From fc127e63c6ed2019f08ff6eeb4f72abf762b7b4f Mon Sep 17 00:00:00 2001 From: Vladyslav Soloviov Date: Mon, 25 Aug 2025 00:41:42 +0300 Subject: [PATCH 03/30] fix local timer --- src/features/timer/{Timer.tsx => LocalTimer.tsx} | 12 +++--------- src/features/timer/slice.ts | 6 +++--- src/features/timers/Timers.tsx | 4 ++-- src/hooks/useTicker.ts | 2 +- 4 files changed, 9 insertions(+), 15 deletions(-) rename src/features/timer/{Timer.tsx => LocalTimer.tsx} (53%) diff --git a/src/features/timer/Timer.tsx b/src/features/timer/LocalTimer.tsx similarity index 53% rename from src/features/timer/Timer.tsx rename to src/features/timer/LocalTimer.tsx index 5da2ab6..5b81637 100644 --- a/src/features/timer/Timer.tsx +++ b/src/features/timer/LocalTimer.tsx @@ -1,4 +1,3 @@ -import { useEffect, useState } from "react" import { useAppSelector } from "@/app/hooks" import { selectLocalTimer } from "./slice" @@ -8,17 +7,12 @@ function formatTime(totalSeconds: number): string { 2, "0", ) - const seconds = String(totalSeconds % 60).padStart(2, "0") + const seconds = String(Math.trunc(totalSeconds % 60)).padStart(2, "0") return `${hours}:${minutes}:${seconds}` } -export default function Timer({ inputSeconds }: { inputSeconds?: number }) { +export default function LocalTimer() { const localTimer: number = useAppSelector(selectLocalTimer) - const [seconds, setSeconds] = useState(inputSeconds || 0) - useEffect(() => { - setSeconds(prev => prev + 1) - }, [localTimer]) - - return <>{formatTime(seconds)} + return <>{formatTime(localTimer / 1000)} } diff --git a/src/features/timer/slice.ts b/src/features/timer/slice.ts index 81d91f5..421627f 100644 --- a/src/features/timer/slice.ts +++ b/src/features/timer/slice.ts @@ -1,12 +1,12 @@ -import { createSlice } from "@reduxjs/toolkit" +import { createSlice, PayloadAction } from "@reduxjs/toolkit" import type { RootState } from "@/app/store" export const timer = createSlice({ name: "timer", initialState: { localTimer: 0 }, reducers: { - tick: state => { - state.localTimer += 1 + tick: (state, action: PayloadAction) => { + state.localTimer += action.payload }, }, }) diff --git a/src/features/timers/Timers.tsx b/src/features/timers/Timers.tsx index 55d90f8..b2e1cdb 100644 --- a/src/features/timers/Timers.tsx +++ b/src/features/timers/Timers.tsx @@ -15,7 +15,7 @@ import { import { useAppSelector, useAppDispatch } from "@/app/hooks" import { selectAllTimers, selectTimersStatus } from "@/features/timers/slice" import { fetchTimers, createTimer } from "@/features/timers/thunks" -import Timer from "@/features/timer/Timer" +import LocalTimer from "@/features/timer/LocalTimer" import ServerTimer from "../timer/ServerTimer" const columns = [ @@ -78,7 +78,7 @@ export const Timers = () => { local - + diff --git a/src/hooks/useTicker.ts b/src/hooks/useTicker.ts index 97a2115..30e9c76 100644 --- a/src/hooks/useTicker.ts +++ b/src/hooks/useTicker.ts @@ -7,7 +7,7 @@ export function useTicker(intervalMs: number = 1000) { useEffect(() => { const intervalId = setInterval(() => { - dispatch(tick()) + dispatch(tick(intervalMs)) }, intervalMs) return () => clearInterval(intervalId) }, []) From 550b4ffe1fccae3dc6a5dbef830e01da32a1f1fa Mon Sep 17 00:00:00 2001 From: Vladyslav Soloviov Date: Mon, 25 Aug 2025 00:48:34 +0300 Subject: [PATCH 04/30] returned tabs view state on the app level --- src/app/App.tsx | 14 +++++++++++++- src/components/TabsView.tsx | 15 +++++---------- src/features/timer/ServerTimer.tsx | 1 - 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index 080a213..cc2112c 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,3 +1,4 @@ +import { useState } from "react" import { FluentProvider, webLightTheme, @@ -13,6 +14,7 @@ import { } from "@fluentui/react-icons" import type { Theme } from "@fluentui/react-components" import { useAppSelector } from "@/app/hooks" +import type { SelectTabData, SelectTabEvent } from "@fluentui/react-components" import { Timers } from "@/features/timers/Timers" import { Settings } from "@/features/settings/Settings" import { selectTabsView, selectTheme } from "@/features/settings/slice" @@ -38,6 +40,12 @@ export const App = () => { const tabsView = useAppSelector(selectTabsView) const theme: string = useAppSelector(selectTheme) + const [selectedTab, setSelectedTab] = useState("timers") + + const onTabSelect = (event: SelectTabEvent, data: SelectTabData) => { + setSelectedTab(data.value as string) + } + const tabs = [ { id: "timers", label: "Timers", icon: , content: }, { @@ -52,7 +60,11 @@ export const App = () => {
{tabsView ? ( - + ) : (
diff --git a/src/components/TabsView.tsx b/src/components/TabsView.tsx index c9acf2d..9752fdb 100644 --- a/src/components/TabsView.tsx +++ b/src/components/TabsView.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useState } from "react" +import type { ReactNode } from "react" import { TabList, Tab } from "@fluentui/react-components" import type { TabSlots, @@ -16,18 +16,13 @@ type TabConfig = { interface TabsViewProps { tabs: TabConfig[] selected: string + onTabSelect: (event: SelectTabEvent, data: SelectTabData) => void } -export function TabsView({ tabs, selected }: TabsViewProps) { - const [selectedTab, setSelectedTab] = useState(selected) - - const onTabSelect = (event: SelectTabEvent, data: SelectTabData) => { - setSelectedTab(data.value as string) - } - +export function TabsView({ tabs, selected, onTabSelect }: TabsViewProps) { return ( <> - + {tabs.map(({ id, icon, label }) => ( {label} @@ -35,7 +30,7 @@ export function TabsView({ tabs, selected }: TabsViewProps) { ))} -
{tabs.find(tab => tab.id === selectedTab)?.content}
+
{tabs.find(tab => tab.id === selected)?.content}
) } diff --git a/src/features/timer/ServerTimer.tsx b/src/features/timer/ServerTimer.tsx index 94dc1c3..26af939 100644 --- a/src/features/timer/ServerTimer.tsx +++ b/src/features/timer/ServerTimer.tsx @@ -1,4 +1,3 @@ -import { useEffect, useState } from "react" import { useAppSelector } from "@/app/hooks" import { selectLocalTimer } from "./slice" import { Timer } from "@/features/timers/types" From bba8dfe6ad3e43d72a24fd9af2dbcd9f59e656b1 Mon Sep 17 00:00:00 2001 From: Vladyslav Soloviov Date: Mon, 25 Aug 2025 03:50:02 +0300 Subject: [PATCH 05/30] added tabsViewMiddleware --- src/app/App.tsx | 2 +- src/app/store.ts | 3 +++ src/features/settings/slice.ts | 7 ++++++- src/features/settings/tabsViewMiddleware.ts | 18 ++++++++++++++++++ 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 src/features/settings/tabsViewMiddleware.ts diff --git a/src/app/App.tsx b/src/app/App.tsx index cc2112c..3ce681a 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -37,7 +37,7 @@ export const App = () => { useGlobalStyles() useTicker(250) - const tabsView = useAppSelector(selectTabsView) + const tabsView: boolean = useAppSelector(selectTabsView) const theme: string = useAppSelector(selectTheme) const [selectedTab, setSelectedTab] = useState("timers") diff --git a/src/app/store.ts b/src/app/store.ts index 1bc386b..6567eda 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -2,6 +2,7 @@ import { configureStore } from "@reduxjs/toolkit" import settingsReducer from "@/features/settings/slice" import timersReducer from "@/features/timers/slice" import timerReducer from "@/features/timer/slice" +import { tabsViewMiddleware } from "@/features/settings/tabsViewMiddleware" export const store = configureStore({ reducer: { @@ -9,6 +10,8 @@ export const store = configureStore({ timers: timersReducer, timer: timerReducer, }, + middleware: getDefaultMiddleware => + getDefaultMiddleware().concat(tabsViewMiddleware), }) // Get the type of our store variable diff --git a/src/features/settings/slice.ts b/src/features/settings/slice.ts index 45a4996..c761195 100644 --- a/src/features/settings/slice.ts +++ b/src/features/settings/slice.ts @@ -1,10 +1,15 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit" import { RootState } from "@/app/store" +const queryString = window.location.search +const urlParams = new URLSearchParams(queryString) +const tabsViewParam = urlParams.get("tabsView") +const tabsView = !(tabsViewParam === "false") + export const settings = createSlice({ name: "tabsView", initialState: { - tabsView: true, + tabsView, theme: "light", preserveLocalTimer: false, }, diff --git a/src/features/settings/tabsViewMiddleware.ts b/src/features/settings/tabsViewMiddleware.ts new file mode 100644 index 0000000..943fd4b --- /dev/null +++ b/src/features/settings/tabsViewMiddleware.ts @@ -0,0 +1,18 @@ +import { Middleware } from "@reduxjs/toolkit" +import { toggleTabs } from "@/features/settings/slice" + +export const tabsViewMiddleware: Middleware = store => next => action => { + const result = next(action) + + if (toggleTabs.match(action)) { + const state = store.getState() + const view = state.settings.tabsView + + const searchParams = new URLSearchParams(window.location.search) + searchParams.set("tabsView", view) + const newUrl = `${window.location.pathname}?${searchParams.toString()}` + window.history.replaceState({}, "", newUrl) + } + + return result +} From 25cf7b3f41367a210e69b996799f8f69bfbaca61 Mon Sep 17 00:00:00 2001 From: Vladyslav Soloviov Date: Mon, 25 Aug 2025 14:21:33 +0300 Subject: [PATCH 06/30] fix warning label attribute --- src/app/App.tsx | 2 +- src/features/settings/Settings.tsx | 26 ++++++++++---------------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index 3ce681a..6f66bb9 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -42,7 +42,7 @@ export const App = () => { const [selectedTab, setSelectedTab] = useState("timers") - const onTabSelect = (event: SelectTabEvent, data: SelectTabData) => { + const onTabSelect = (_event: SelectTabEvent, data: SelectTabData) => { setSelectedTab(data.value as string) } diff --git a/src/features/settings/Settings.tsx b/src/features/settings/Settings.tsx index dbd86ef..347465a 100644 --- a/src/features/settings/Settings.tsx +++ b/src/features/settings/Settings.tsx @@ -1,10 +1,4 @@ -import { - Field, - Radio, - RadioGroup, - Checkbox, - Button, -} from "@fluentui/react-components" +import { Radio, RadioGroup, Checkbox, Button } from "@fluentui/react-components" import { useAppDispatch, useAppSelector } from "@/app/hooks" import { toggleTabs, @@ -20,15 +14,15 @@ export const Settings = () => { return ( <> - - dispatch(setTheme(data.value))} - > - - - - + + dispatch(setTheme(data.value))} + aria-label="theme" + > + + + dispatch(togglePreserveLocalTimer())} From 64c54feeea3e7916b38f4a151fb4c7d93b4d6c5d Mon Sep 17 00:00:00 2001 From: Vladyslav Soloviov Date: Mon, 25 Aug 2025 15:06:58 +0300 Subject: [PATCH 07/30] format time --- src/features/timer/LocalTimer.tsx | 11 +---------- src/features/timer/ServerTimer.tsx | 13 ++----------- src/helpers.ts/formatTime.ts | 9 +++++++++ 3 files changed, 12 insertions(+), 21 deletions(-) create mode 100644 src/helpers.ts/formatTime.ts diff --git a/src/features/timer/LocalTimer.tsx b/src/features/timer/LocalTimer.tsx index 5b81637..50bce13 100644 --- a/src/features/timer/LocalTimer.tsx +++ b/src/features/timer/LocalTimer.tsx @@ -1,15 +1,6 @@ import { useAppSelector } from "@/app/hooks" import { selectLocalTimer } from "./slice" - -function formatTime(totalSeconds: number): string { - const hours = String(Math.floor(totalSeconds / 3600)).padStart(2, "0") - const minutes = String(Math.floor((totalSeconds % 3600) / 60)).padStart( - 2, - "0", - ) - const seconds = String(Math.trunc(totalSeconds % 60)).padStart(2, "0") - return `${hours}:${minutes}:${seconds}` -} +import { formatTime } from "@/helpers.ts/formatTime" export default function LocalTimer() { const localTimer: number = useAppSelector(selectLocalTimer) diff --git a/src/features/timer/ServerTimer.tsx b/src/features/timer/ServerTimer.tsx index 26af939..467d0a2 100644 --- a/src/features/timer/ServerTimer.tsx +++ b/src/features/timer/ServerTimer.tsx @@ -1,20 +1,11 @@ import { useAppSelector } from "@/app/hooks" import { selectLocalTimer } from "./slice" import { Timer } from "@/features/timers/types" - -function formatElapsed(seconds: number) { - const hrs = Math.floor(seconds / 3600) - const mins = Math.floor((seconds % 3600) / 60) - const secs = Math.floor(seconds % 60) - - return `${hrs.toString().padStart(2, "0")}:${mins - .toString() - .padStart(2, "0")}:${secs.toString().padStart(2, "0")}` -} +import { formatTime } from "@/helpers.ts/formatTime" export default function ServerTimer({ elapsed, receivedAt }: Timer) { useAppSelector(selectLocalTimer) const now = Date.now() / 1000 - return <>{formatElapsed(elapsed + (now - receivedAt))} + return <>{formatTime(elapsed + (now - receivedAt))} } diff --git a/src/helpers.ts/formatTime.ts b/src/helpers.ts/formatTime.ts new file mode 100644 index 0000000..2f2b564 --- /dev/null +++ b/src/helpers.ts/formatTime.ts @@ -0,0 +1,9 @@ +export function formatTime(totalSeconds: number): string { + const hours = String(Math.floor(totalSeconds / 3600)).padStart(2, "0") + const minutes = String(Math.floor((totalSeconds % 3600) / 60)).padStart( + 2, + "0", + ) + const seconds = String(Math.trunc(totalSeconds % 60)).padStart(2, "0") + return `${hours}:${minutes}:${seconds}` +} From 00e022cc25763dfb004b596f31e2b9559ce85d9e Mon Sep 17 00:00:00 2001 From: Vladyslav Soloviov Date: Tue, 26 Aug 2025 03:00:48 +0300 Subject: [PATCH 08/30] added preservation of a local timer --- src/app/App.tsx | 4 +- src/app/store.ts | 5 ++- src/features/settings/Settings.tsx | 4 +- src/features/settings/middleware.ts | 29 ++++++++++++++ src/features/settings/slice.ts | 2 +- src/features/settings/tabsViewMiddleware.ts | 18 --------- src/features/timer/LocalTimer.tsx | 8 ++-- src/features/timer/middelware.ts | 44 +++++++++++++++++++++ src/features/timer/slice.ts | 24 +++++++++-- src/hooks/useTicker.ts | 13 +++--- 10 files changed, 115 insertions(+), 36 deletions(-) create mode 100644 src/features/settings/middleware.ts delete mode 100644 src/features/settings/tabsViewMiddleware.ts create mode 100644 src/features/timer/middelware.ts diff --git a/src/app/App.tsx b/src/app/App.tsx index 6f66bb9..6a5cc95 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -19,7 +19,7 @@ import { Timers } from "@/features/timers/Timers" import { Settings } from "@/features/settings/Settings" import { selectTabsView, selectTheme } from "@/features/settings/slice" import { TabsView } from "@/components/TabsView" -import { useTicker } from "@/hooks/useTicker" +import { useTimer } from "@/hooks/useTicker" const TimerIcon = bundleIcon(TimerFilled, TimerRegular) const SettingsIcon = bundleIcon(WrenchSettingsFilled, WrenchSettingsRegular) @@ -35,7 +35,7 @@ const THEMES: { [key: string]: Theme } = { export const App = () => { useGlobalStyles() - useTicker(250) + useTimer(250) const tabsView: boolean = useAppSelector(selectTabsView) const theme: string = useAppSelector(selectTheme) diff --git a/src/app/store.ts b/src/app/store.ts index 6567eda..4bb016b 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -2,7 +2,8 @@ import { configureStore } from "@reduxjs/toolkit" import settingsReducer from "@/features/settings/slice" import timersReducer from "@/features/timers/slice" import timerReducer from "@/features/timer/slice" -import { tabsViewMiddleware } from "@/features/settings/tabsViewMiddleware" +import { tabsViewMiddleware } from "@/features/settings/middleware" +import { timerMiddelware } from "@/features/timer/middelware" export const store = configureStore({ reducer: { @@ -11,7 +12,7 @@ export const store = configureStore({ timer: timerReducer, }, middleware: getDefaultMiddleware => - getDefaultMiddleware().concat(tabsViewMiddleware), + getDefaultMiddleware().concat(tabsViewMiddleware, timerMiddelware), }) // Get the type of our store variable diff --git a/src/features/settings/Settings.tsx b/src/features/settings/Settings.tsx index 347465a..20f7f0d 100644 --- a/src/features/settings/Settings.tsx +++ b/src/features/settings/Settings.tsx @@ -4,6 +4,7 @@ import { toggleTabs, setTheme, togglePreserveLocalTimer, + selectPreserveLocalTimer, } from "@/features/settings/slice" import { selectTabsView, selectTheme } from "@/features/settings/slice" @@ -11,6 +12,7 @@ export const Settings = () => { const dispatch = useAppDispatch() const tabsView = useAppSelector(selectTabsView) const theme = useAppSelector(selectTheme) + const preserve = useAppSelector(selectPreserveLocalTimer) return ( <> @@ -24,7 +26,7 @@ export const Settings = () => { dispatch(togglePreserveLocalTimer())} label="Preserve local timer between sessions" /> diff --git a/src/features/settings/middleware.ts b/src/features/settings/middleware.ts new file mode 100644 index 0000000..1045ea3 --- /dev/null +++ b/src/features/settings/middleware.ts @@ -0,0 +1,29 @@ +import { Middleware } from "@reduxjs/toolkit" +import { toggleTabs, togglePreserveLocalTimer } from "@/features/settings/slice" +import { saveLocalTimerToStorage } from "../timer/slice" + +export const tabsViewMiddleware: Middleware = store => next => action => { + const result = next(action) + const state = store.getState() + + if (toggleTabs.match(action)) { + const { tabsView } = state.settings + + const searchParams = new URLSearchParams(window.location.search) + searchParams.set("tabsView", tabsView) + const newUrl = `${window.location.pathname}?${searchParams.toString()}` + window.history.replaceState({}, "", newUrl) + } + + if (togglePreserveLocalTimer.match(action)) { + const { preserveLocalTimer } = state.settings + + if (preserveLocalTimer) { + store.dispatch(saveLocalTimerToStorage()) + } else { + localStorage.removeItem("localTimer") + } + } + + return result +} diff --git a/src/features/settings/slice.ts b/src/features/settings/slice.ts index c761195..306672f 100644 --- a/src/features/settings/slice.ts +++ b/src/features/settings/slice.ts @@ -11,7 +11,7 @@ export const settings = createSlice({ initialState: { tabsView, theme: "light", - preserveLocalTimer: false, + preserveLocalTimer: true, }, reducers: { toggleTabs: state => { diff --git a/src/features/settings/tabsViewMiddleware.ts b/src/features/settings/tabsViewMiddleware.ts deleted file mode 100644 index 943fd4b..0000000 --- a/src/features/settings/tabsViewMiddleware.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Middleware } from "@reduxjs/toolkit" -import { toggleTabs } from "@/features/settings/slice" - -export const tabsViewMiddleware: Middleware = store => next => action => { - const result = next(action) - - if (toggleTabs.match(action)) { - const state = store.getState() - const view = state.settings.tabsView - - const searchParams = new URLSearchParams(window.location.search) - searchParams.set("tabsView", view) - const newUrl = `${window.location.pathname}?${searchParams.toString()}` - window.history.replaceState({}, "", newUrl) - } - - return result -} diff --git a/src/features/timer/LocalTimer.tsx b/src/features/timer/LocalTimer.tsx index 50bce13..592afc7 100644 --- a/src/features/timer/LocalTimer.tsx +++ b/src/features/timer/LocalTimer.tsx @@ -1,9 +1,11 @@ import { useAppSelector } from "@/app/hooks" -import { selectLocalTimer } from "./slice" +import { selectLocalTimer, selectStart } from "./slice" import { formatTime } from "@/helpers.ts/formatTime" export default function LocalTimer() { - const localTimer: number = useAppSelector(selectLocalTimer) + useAppSelector(selectLocalTimer) + const start = useAppSelector(selectStart) + const now = Date.now() / 1000 - return <>{formatTime(localTimer / 1000)} + return <>{formatTime(now - start)} } diff --git a/src/features/timer/middelware.ts b/src/features/timer/middelware.ts new file mode 100644 index 0000000..e7e75ef --- /dev/null +++ b/src/features/timer/middelware.ts @@ -0,0 +1,44 @@ +import { Middleware } from "@reduxjs/toolkit" +import { + startTimer, + restoreTimer, + saveLocalTimerToStorage, +} from "@/features/timer/slice" +import { tick } from "@/features/timer/slice" + +export const timerMiddelware: Middleware = store => { + let intervalId: ReturnType | null = null + + return next => action => { + const result = next(action) + const state = store.getState() + const { preserveLocalTimer } = state.settings + + if (startTimer.match(action) && intervalId === null) { + if (preserveLocalTimer) { + const localTimer: string | null = localStorage.getItem("localTimer") + + localTimer !== null && store.dispatch(restoreTimer(+localTimer)) + } + + const frequencyMs = action.payload + + intervalId = setInterval(() => { + store.dispatch(tick(frequencyMs)) + }, frequencyMs) + } + + if (saveLocalTimerToStorage.match(action)) { + const { start } = state.timer + + if (preserveLocalTimer) { + localStorage.setItem("localTimer", start) + } + + intervalId !== null && clearInterval(intervalId) + intervalId = null + } + + return result + } +} diff --git a/src/features/timer/slice.ts b/src/features/timer/slice.ts index 421627f..ba59dab 100644 --- a/src/features/timer/slice.ts +++ b/src/features/timer/slice.ts @@ -1,18 +1,36 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit" +import { createSlice, PayloadAction, createAction } from "@reduxjs/toolkit" import type { RootState } from "@/app/store" +interface LocalTimerState { + start: number + localTimer: number +} + +const initialState: LocalTimerState = { + start: 0, + localTimer: 0, +} + export const timer = createSlice({ name: "timer", - initialState: { localTimer: 0 }, + initialState: initialState, reducers: { tick: (state, action: PayloadAction) => { state.localTimer += action.payload }, + startTimer: (state, _action: PayloadAction) => { + state.start = Date.now() / 1000 + }, + restoreTimer: (state, action: { payload: number }) => { + state.start = action.payload + }, }, }) -export const { tick } = timer.actions +export const { tick, startTimer, restoreTimer } = timer.actions +export const saveLocalTimerToStorage = createAction("timer/saveLocalToStorage") export default timer.reducer export const selectLocalTimer = (state: RootState) => state.timer.localTimer +export const selectStart = (state: RootState) => state.timer.start diff --git a/src/hooks/useTicker.ts b/src/hooks/useTicker.ts index 30e9c76..38e9f13 100644 --- a/src/hooks/useTicker.ts +++ b/src/hooks/useTicker.ts @@ -1,14 +1,15 @@ import { useEffect } from "react" -import { tick } from "@/features/timer/slice" import { useAppDispatch } from "@/app/hooks" +import { startTimer, saveLocalTimerToStorage } from "@/features/timer/slice" -export function useTicker(intervalMs: number = 1000) { +export function useTimer(frequencyMs: number) { const dispatch = useAppDispatch() useEffect(() => { - const intervalId = setInterval(() => { - dispatch(tick(intervalMs)) - }, intervalMs) - return () => clearInterval(intervalId) + dispatch(startTimer(frequencyMs)) + + return () => { + dispatch(saveLocalTimerToStorage()) + } }, []) } From c2b8ab34ecba0bd8b6aa1f89b3357c7303c3b62c Mon Sep 17 00:00:00 2001 From: Vladyslav Soloviov Date: Tue, 26 Aug 2025 04:46:02 +0300 Subject: [PATCH 09/30] fix preservation --- src/features/settings/slice.ts | 3 ++- src/features/timer/middelware.ts | 5 +++++ src/features/timer/slice.ts | 1 + src/hooks/useTicker.ts | 4 ++-- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/features/settings/slice.ts b/src/features/settings/slice.ts index 306672f..18f2dba 100644 --- a/src/features/settings/slice.ts +++ b/src/features/settings/slice.ts @@ -5,13 +5,14 @@ const queryString = window.location.search const urlParams = new URLSearchParams(queryString) const tabsViewParam = urlParams.get("tabsView") const tabsView = !(tabsViewParam === "false") +const localTimer: string | null = localStorage.getItem("localTimer") export const settings = createSlice({ name: "tabsView", initialState: { tabsView, theme: "light", - preserveLocalTimer: true, + preserveLocalTimer: !!localTimer, }, reducers: { toggleTabs: state => { diff --git a/src/features/timer/middelware.ts b/src/features/timer/middelware.ts index e7e75ef..175ab03 100644 --- a/src/features/timer/middelware.ts +++ b/src/features/timer/middelware.ts @@ -3,6 +3,7 @@ import { startTimer, restoreTimer, saveLocalTimerToStorage, + stopTimer, } from "@/features/timer/slice" import { tick } from "@/features/timer/slice" @@ -34,9 +35,13 @@ export const timerMiddelware: Middleware = store => { if (preserveLocalTimer) { localStorage.setItem("localTimer", start) } + } + if (stopTimer.match(action)) { intervalId !== null && clearInterval(intervalId) intervalId = null + + store.dispatch(saveLocalTimerToStorage()) } return result diff --git a/src/features/timer/slice.ts b/src/features/timer/slice.ts index ba59dab..96e3805 100644 --- a/src/features/timer/slice.ts +++ b/src/features/timer/slice.ts @@ -29,6 +29,7 @@ export const timer = createSlice({ export const { tick, startTimer, restoreTimer } = timer.actions export const saveLocalTimerToStorage = createAction("timer/saveLocalToStorage") +export const stopTimer = createAction("timer/stopTimer") export default timer.reducer diff --git a/src/hooks/useTicker.ts b/src/hooks/useTicker.ts index 38e9f13..48cc5bb 100644 --- a/src/hooks/useTicker.ts +++ b/src/hooks/useTicker.ts @@ -1,6 +1,6 @@ import { useEffect } from "react" import { useAppDispatch } from "@/app/hooks" -import { startTimer, saveLocalTimerToStorage } from "@/features/timer/slice" +import { startTimer, stopTimer } from "@/features/timer/slice" export function useTimer(frequencyMs: number) { const dispatch = useAppDispatch() @@ -9,7 +9,7 @@ export function useTimer(frequencyMs: number) { dispatch(startTimer(frequencyMs)) return () => { - dispatch(saveLocalTimerToStorage()) + dispatch(stopTimer()) } }, []) } From 26b4eb80749953ef5487c27e6f7021bfb63f6d43 Mon Sep 17 00:00:00 2001 From: Vladyslav Soloviov Date: Wed, 27 Aug 2025 18:48:58 +0300 Subject: [PATCH 10/30] reset local timer --- src/app/App.tsx | 3 ++- src/app/constants.ts | 1 + src/features/timer/LocalTimer.tsx | 19 +++++++++++++++++-- src/features/timer/middelware.ts | 16 ++++++++++------ src/features/timer/slice.ts | 8 ++++++-- src/features/timers/Timers.tsx | 10 +--------- 6 files changed, 37 insertions(+), 20 deletions(-) create mode 100644 src/app/constants.ts diff --git a/src/app/App.tsx b/src/app/App.tsx index 6a5cc95..5f9433d 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -20,6 +20,7 @@ import { Settings } from "@/features/settings/Settings" import { selectTabsView, selectTheme } from "@/features/settings/slice" import { TabsView } from "@/components/TabsView" import { useTimer } from "@/hooks/useTicker" +import { FREQUENCY_MS } from "./constants" const TimerIcon = bundleIcon(TimerFilled, TimerRegular) const SettingsIcon = bundleIcon(WrenchSettingsFilled, WrenchSettingsRegular) @@ -35,7 +36,7 @@ const THEMES: { [key: string]: Theme } = { export const App = () => { useGlobalStyles() - useTimer(250) + useTimer(FREQUENCY_MS) const tabsView: boolean = useAppSelector(selectTabsView) const theme: string = useAppSelector(selectTheme) diff --git a/src/app/constants.ts b/src/app/constants.ts new file mode 100644 index 0000000..557a85f --- /dev/null +++ b/src/app/constants.ts @@ -0,0 +1 @@ +export const FREQUENCY_MS = 250 diff --git a/src/features/timer/LocalTimer.tsx b/src/features/timer/LocalTimer.tsx index 592afc7..3f04013 100644 --- a/src/features/timer/LocalTimer.tsx +++ b/src/features/timer/LocalTimer.tsx @@ -1,11 +1,26 @@ import { useAppSelector } from "@/app/hooks" -import { selectLocalTimer, selectStart } from "./slice" +import { + selectLocalTimer, + selectStart, + resetTimer, +} from "@/features/timer/slice" import { formatTime } from "@/helpers.ts/formatTime" +import { TableRow, TableCell, Button } from "@fluentui/react-components" +import { useAppDispatch } from "@/app/hooks" export default function LocalTimer() { useAppSelector(selectLocalTimer) const start = useAppSelector(selectStart) const now = Date.now() / 1000 + const dispatch = useAppDispatch() - return <>{formatTime(now - start)} + return ( + + local + {formatTime(now - start)} + + + + + ) } diff --git a/src/features/timer/middelware.ts b/src/features/timer/middelware.ts index 175ab03..ac28d66 100644 --- a/src/features/timer/middelware.ts +++ b/src/features/timer/middelware.ts @@ -4,8 +4,10 @@ import { restoreTimer, saveLocalTimerToStorage, stopTimer, + resetTimer, + tick, } from "@/features/timer/slice" -import { tick } from "@/features/timer/slice" +import { FREQUENCY_MS } from "@/app/constants" export const timerMiddelware: Middleware = store => { let intervalId: ReturnType | null = null @@ -15,7 +17,7 @@ export const timerMiddelware: Middleware = store => { const state = store.getState() const { preserveLocalTimer } = state.settings - if (startTimer.match(action) && intervalId === null) { + if (startTimer.match(action)) { if (preserveLocalTimer) { const localTimer: string | null = localStorage.getItem("localTimer") @@ -24,12 +26,14 @@ export const timerMiddelware: Middleware = store => { const frequencyMs = action.payload - intervalId = setInterval(() => { - store.dispatch(tick(frequencyMs)) - }, frequencyMs) + if (intervalId === null) { + intervalId = setInterval(() => { + store.dispatch(tick(frequencyMs || FREQUENCY_MS)) + }, frequencyMs) + } } - if (saveLocalTimerToStorage.match(action)) { + if (saveLocalTimerToStorage.match(action) || resetTimer.match(action)) { const { start } = state.timer if (preserveLocalTimer) { diff --git a/src/features/timer/slice.ts b/src/features/timer/slice.ts index 96e3805..7d11951 100644 --- a/src/features/timer/slice.ts +++ b/src/features/timer/slice.ts @@ -18,18 +18,22 @@ export const timer = createSlice({ tick: (state, action: PayloadAction) => { state.localTimer += action.payload }, - startTimer: (state, _action: PayloadAction) => { + startTimer: (state, _action: PayloadAction) => { state.start = Date.now() / 1000 }, restoreTimer: (state, action: { payload: number }) => { state.start = action.payload }, + resetTimer: (state, _action: PayloadAction) => { + state.start = Date.now() / 1000 + }, }, }) -export const { tick, startTimer, restoreTimer } = timer.actions +export const { tick, startTimer, restoreTimer, resetTimer } = timer.actions export const saveLocalTimerToStorage = createAction("timer/saveLocalToStorage") export const stopTimer = createAction("timer/stopTimer") +// export const resetTimer = createAction("timer/resetTimer") export default timer.reducer diff --git a/src/features/timers/Timers.tsx b/src/features/timers/Timers.tsx index b2e1cdb..08b279c 100644 --- a/src/features/timers/Timers.tsx +++ b/src/features/timers/Timers.tsx @@ -75,15 +75,7 @@ export const Timers = () => { - - local - - - - - - - + {timers.map(([id, timer]) => ( {id} From 57261790d9f05450135535ea8ff4393cc3f8b4e0 Mon Sep 17 00:00:00 2001 From: Vladyslav Soloviov Date: Thu, 28 Aug 2025 00:07:49 +0300 Subject: [PATCH 11/30] reset of the server timer --- src/features/timer/ServerTimer.tsx | 24 +++++++++++++++++++++--- src/features/timers/Timers.tsx | 13 ++----------- src/features/timers/slice.ts | 14 ++++++-------- src/features/timers/thunks.ts | 28 +++++++++++++++------------- 4 files changed, 44 insertions(+), 35 deletions(-) diff --git a/src/features/timer/ServerTimer.tsx b/src/features/timer/ServerTimer.tsx index 467d0a2..d9ee502 100644 --- a/src/features/timer/ServerTimer.tsx +++ b/src/features/timer/ServerTimer.tsx @@ -1,11 +1,29 @@ import { useAppSelector } from "@/app/hooks" +import { TableCell, TableRow, Button } from "@fluentui/react-components" import { selectLocalTimer } from "./slice" -import { Timer } from "@/features/timers/types" +import { TimerWithId } from "@/features/timers/types" import { formatTime } from "@/helpers.ts/formatTime" +import { useAppDispatch } from "@/app/hooks" +import { createTimer } from "@/features/timers/thunks" -export default function ServerTimer({ elapsed, receivedAt }: Timer) { +export default function ServerTimer({ id, elapsed, receivedAt }: TimerWithId) { useAppSelector(selectLocalTimer) const now = Date.now() / 1000 + const dispatch = useAppDispatch() - return <>{formatTime(elapsed + (now - receivedAt))} + return ( + + {id} + {formatTime(elapsed + (now - receivedAt))} + + + + + ) } diff --git a/src/features/timers/Timers.tsx b/src/features/timers/Timers.tsx index 08b279c..7e4e0cd 100644 --- a/src/features/timers/Timers.tsx +++ b/src/features/timers/Timers.tsx @@ -1,7 +1,6 @@ import { useState, useEffect, FormEvent } from "react" import { TableBody, - TableCell, TableRow, Table, TableHeader, @@ -59,7 +58,7 @@ export const Timers = () => { const onSubmit = (e: FormEvent) => { e.preventDefault() - dispatch(createTimer(newTimerName)).then(() => dispatch(fetchTimers())) + dispatch(createTimer(newTimerName)) } return ( @@ -77,15 +76,7 @@ export const Timers = () => { {timers.map(([id, timer]) => ( - - {id} - - - - - - - + ))} diff --git a/src/features/timers/slice.ts b/src/features/timers/slice.ts index 546e821..fb95133 100644 --- a/src/features/timers/slice.ts +++ b/src/features/timers/slice.ts @@ -14,7 +14,7 @@ export const timers = createSlice({ initialState, reducers: {}, extraReducers: builder => { - builder.addCase(fetchTimers.pending, (state, action) => { + builder.addCase(fetchTimers.pending, state => { state.status = "pending" }) builder.addCase(fetchTimers.fulfilled, (state, action) => { @@ -31,16 +31,14 @@ export const timers = createSlice({ state.status = "failed" state.error = action.error.message ?? "Unknown Error" }) - builder.addCase(createTimer.pending, (state, action) => { + builder.addCase(createTimer.pending, state => { state.status = "pending" }) builder.addCase(createTimer.fulfilled, (state, action) => { - // TODO - // state.status = "succeeded" - // state.timers = action.payload - // action.payload.forEach(id => { - // dispatch(fetchTimerById(id)) - // }) + const { id, elapsed, start, receivedAt } = action.payload + + state.status = "succeeded" + state.map[id] = { elapsed, start, receivedAt } }) builder.addCase(createTimer.rejected, (state, action: any) => { state.status = "failed" diff --git a/src/features/timers/thunks.ts b/src/features/timers/thunks.ts index 154affb..b84e66e 100644 --- a/src/features/timers/thunks.ts +++ b/src/features/timers/thunks.ts @@ -42,18 +42,20 @@ export const fetchTimers = createAppAsyncThunk< } }) -export const createTimer = createAppAsyncThunk( - "timer/createTimer", - async (id: string, { rejectWithValue }) => { - try { - const response: TimerResponse = await postTimer(id) +export const createTimer = createAppAsyncThunk< + TimerWithId, + string, + { rejectValue: string } +>("timer/createTimer", async (id: string, { rejectWithValue }) => { + try { + const response: TimerResponse = await postTimer(id) + const receivedAt = Date.now() / 1000 - return response - } catch (err: unknown) { - if (err instanceof Error) { - return rejectWithValue(err.message) - } - return rejectWithValue("Unknown error") + return { id, ...response, receivedAt } + } catch (err: unknown) { + if (err instanceof Error) { + return rejectWithValue(err.message) } - }, -) + return rejectWithValue("Unknown error") + } +}) From 08b3b1dcd0783725536d7619f2d20fad5080cc14 Mon Sep 17 00:00:00 2001 From: Vladyslav Soloviov Date: Thu, 28 Aug 2025 00:19:22 +0300 Subject: [PATCH 12/30] fix creation of local timer --- src/features/timer/slice.ts | 1 - src/features/timers/Timers.tsx | 9 ++++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/features/timer/slice.ts b/src/features/timer/slice.ts index 7d11951..1a69127 100644 --- a/src/features/timer/slice.ts +++ b/src/features/timer/slice.ts @@ -33,7 +33,6 @@ export const timer = createSlice({ export const { tick, startTimer, restoreTimer, resetTimer } = timer.actions export const saveLocalTimerToStorage = createAction("timer/saveLocalToStorage") export const stopTimer = createAction("timer/stopTimer") -// export const resetTimer = createAction("timer/resetTimer") export default timer.reducer diff --git a/src/features/timers/Timers.tsx b/src/features/timers/Timers.tsx index 7e4e0cd..3368758 100644 --- a/src/features/timers/Timers.tsx +++ b/src/features/timers/Timers.tsx @@ -16,6 +16,7 @@ import { selectAllTimers, selectTimersStatus } from "@/features/timers/slice" import { fetchTimers, createTimer } from "@/features/timers/thunks" import LocalTimer from "@/features/timer/LocalTimer" import ServerTimer from "../timer/ServerTimer" +import { resetTimer } from "../timer/slice" const columns = [ { columnKey: "name", label: "Name" }, @@ -58,7 +59,13 @@ export const Timers = () => { const onSubmit = (e: FormEvent) => { e.preventDefault() - dispatch(createTimer(newTimerName)) + if (newTimerName === "local") { + dispatch(resetTimer()) + } else { + dispatch(createTimer(newTimerName)) + } + + setNewTimerName("") } return ( From 4e8dd35b8ab513f15726308c66ecbeea75e569e9 Mon Sep 17 00:00:00 2001 From: Vladyslav Soloviov Date: Thu, 28 Aug 2025 00:24:03 +0300 Subject: [PATCH 13/30] added local const, title and placeholder --- src/app/constants.ts | 1 + src/features/timer/LocalTimer.tsx | 5 +++-- src/features/timers/Timers.tsx | 9 +++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/app/constants.ts b/src/app/constants.ts index 557a85f..30546c3 100644 --- a/src/app/constants.ts +++ b/src/app/constants.ts @@ -1 +1,2 @@ export const FREQUENCY_MS = 250 +export const LOCAL = "local" diff --git a/src/features/timer/LocalTimer.tsx b/src/features/timer/LocalTimer.tsx index 3f04013..6796a72 100644 --- a/src/features/timer/LocalTimer.tsx +++ b/src/features/timer/LocalTimer.tsx @@ -7,6 +7,7 @@ import { import { formatTime } from "@/helpers.ts/formatTime" import { TableRow, TableCell, Button } from "@fluentui/react-components" import { useAppDispatch } from "@/app/hooks" +import { LOCAL } from "@/app/constants" export default function LocalTimer() { useAppSelector(selectLocalTimer) @@ -15,8 +16,8 @@ export default function LocalTimer() { const dispatch = useAppDispatch() return ( - - local + + {LOCAL} {formatTime(now - start)} diff --git a/src/features/timers/Timers.tsx b/src/features/timers/Timers.tsx index 3368758..cd0a4c9 100644 --- a/src/features/timers/Timers.tsx +++ b/src/features/timers/Timers.tsx @@ -17,6 +17,7 @@ import { fetchTimers, createTimer } from "@/features/timers/thunks" import LocalTimer from "@/features/timer/LocalTimer" import ServerTimer from "../timer/ServerTimer" import { resetTimer } from "../timer/slice" +import { LOCAL } from "@/app/constants" const columns = [ { columnKey: "name", label: "Name" }, @@ -59,7 +60,7 @@ export const Timers = () => { const onSubmit = (e: FormEvent) => { e.preventDefault() - if (newTimerName === "local") { + if (newTimerName === LOCAL) { dispatch(resetTimer()) } else { dispatch(createTimer(newTimerName)) @@ -94,8 +95,12 @@ export const Timers = () => { id={inputId} value={newTimerName} onChange={e => setNewTimerName(e.target.value)} + title="Enter the name of a new timer" + placeholder="Enter the name" /> - +
From 3f4ec5ba6bdba92b4878e430ddd71f70101d3da9 Mon Sep 17 00:00:00 2001 From: Vladyslav Soloviov Date: Thu, 28 Aug 2025 02:15:32 +0300 Subject: [PATCH 14/30] fix styles a bit --- package.json | 2 +- src/features/settings/Settings.tsx | 28 +++++++++++++++++++++++----- src/features/timers/Timers.tsx | 12 ++++-------- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 52effbd..a6f1423 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "type-check": "tsc -b --noEmit" }, "dependencies": { - "@fluentui/react-components": "^9.68.2", + "@fluentui/react-components": "9.68.2", "@reduxjs/toolkit": "^2.8.2", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/src/features/settings/Settings.tsx b/src/features/settings/Settings.tsx index 20f7f0d..aca0400 100644 --- a/src/features/settings/Settings.tsx +++ b/src/features/settings/Settings.tsx @@ -1,4 +1,10 @@ -import { Radio, RadioGroup, Checkbox, Button } from "@fluentui/react-components" +import { + Radio, + RadioGroup, + Checkbox, + Button, + makeStyles, +} from "@fluentui/react-components" import { useAppDispatch, useAppSelector } from "@/app/hooks" import { toggleTabs, @@ -8,14 +14,24 @@ import { } from "@/features/settings/slice" import { selectTabsView, selectTheme } from "@/features/settings/slice" +const useStyles = makeStyles({ + container: { + padding: "20px 0 0 20px", + display: "flex", + flexDirection: "column", + gap: "10px", + }, +}) + export const Settings = () => { + const styles = useStyles() const dispatch = useAppDispatch() const tabsView = useAppSelector(selectTabsView) const theme = useAppSelector(selectTheme) const preserve = useAppSelector(selectPreserveLocalTimer) return ( - <> +
{ onChange={() => dispatch(togglePreserveLocalTimer())} label="Preserve local timer between sessions" /> -
- - +
) } diff --git a/src/features/timers/Timers.tsx b/src/features/timers/Timers.tsx index cd0a4c9..4352fb7 100644 --- a/src/features/timers/Timers.tsx +++ b/src/features/timers/Timers.tsx @@ -27,14 +27,10 @@ const columns = [ const useStyles = makeStyles({ form: { - display: "flex", - flexDirection: "row", - }, - input: { display: "flex", flexDirection: "column", - gap: "2px", - maxWidth: "400px", + gap: "5px", + padding: "20px 0 0 20px", }, }) @@ -71,7 +67,7 @@ export const Timers = () => { return ( <> - +
{columns.map(column => ( @@ -88,7 +84,7 @@ export const Timers = () => { ))}
-
+
Date: Thu, 28 Aug 2025 02:35:59 +0300 Subject: [PATCH 15/30] added .env --- .gitignore | 1 + package-lock.json | 4 ++-- package.json | 2 +- src/features/timers/api.ts | 7 ++++--- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index db2f80e..232f584 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ dist-ssr .yalc/ yalc.lock +.env diff --git a/package-lock.json b/package-lock.json index 4fe7364..19e21e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "vite-template-redux", "version": "0.0.0", "dependencies": { - "@fluentui/react-components": "^9.68.2", + "@fluentui/react-components": "9.68.2", "@reduxjs/toolkit": "^2.8.2", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -8522,4 +8522,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index a6f1423..52effbd 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "type-check": "tsc -b --noEmit" }, "dependencies": { - "@fluentui/react-components": "9.68.2", + "@fluentui/react-components": "^9.68.2", "@reduxjs/toolkit": "^2.8.2", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/src/features/timers/api.ts b/src/features/timers/api.ts index 1aebfbf..a3450ff 100644 --- a/src/features/timers/api.ts +++ b/src/features/timers/api.ts @@ -1,21 +1,22 @@ import { Timer, TimerResponse } from "./types" +const URL = import.meta.env.VITE_API_URL export async function getTimer(id: string): Promise { - const response = await fetch(`http://localhost:8080/${id}`) + const response = await fetch(`${URL}/${id}`) const data: Timer = await response.json() return data } export async function getListOfTimers(): Promise { - const response = await fetch("http://localhost:8080/list") + const response = await fetch(`${URL}/list`) const data: string[] = await response.json() return data } export async function postTimer(id: string): Promise { - const response = await fetch(`http://localhost:8080/reset/${id}`, { + const response = await fetch(`${URL}/reset/${id}`, { method: "POST", }) const data: Timer = await response.json() From 9399c22ade51bc97b8109fede8180f603c81e7e8 Mon Sep 17 00:00:00 2001 From: Vladyslav Soloviov Date: Thu, 28 Aug 2025 04:15:52 +0300 Subject: [PATCH 16/30] added spinner --- src/features/timers/Timers.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/features/timers/Timers.tsx b/src/features/timers/Timers.tsx index 4352fb7..7bcdc63 100644 --- a/src/features/timers/Timers.tsx +++ b/src/features/timers/Timers.tsx @@ -10,6 +10,7 @@ import { Button, makeStyles, useId, + Spinner, } from "@fluentui/react-components" import { useAppSelector, useAppDispatch } from "@/app/hooks" import { selectAllTimers, selectTimersStatus } from "@/features/timers/slice" @@ -26,6 +27,12 @@ const columns = [ ] const useStyles = makeStyles({ + spinnerContainer: { + display: "flex", + justifyContent: "center", + alignItems: "center", + minHeight: "42px", + }, form: { display: "flex", flexDirection: "column", @@ -84,6 +91,9 @@ export const Timers = () => { ))} +
+ {timersStatus === "idle" && } +
From 6aec3333caee2abe67f0160eed70c758fe6c88f7 Mon Sep 17 00:00:00 2001 From: Vladyslav Soloviov Date: Thu, 28 Aug 2025 04:42:23 +0300 Subject: [PATCH 17/30] spinner for timers list --- src/features/timers/Timers.tsx | 24 ++++++++++++++++++++---- src/features/timers/slice.ts | 4 ++++ src/features/timers/types.ts | 1 + 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/features/timers/Timers.tsx b/src/features/timers/Timers.tsx index 7bcdc63..f04bd86 100644 --- a/src/features/timers/Timers.tsx +++ b/src/features/timers/Timers.tsx @@ -13,7 +13,11 @@ import { Spinner, } from "@fluentui/react-components" import { useAppSelector, useAppDispatch } from "@/app/hooks" -import { selectAllTimers, selectTimersStatus } from "@/features/timers/slice" +import { + selectAllTimers, + selectIsInitialLoading, + selectTimersStatus, +} from "@/features/timers/slice" import { fetchTimers, createTimer } from "@/features/timers/thunks" import LocalTimer from "@/features/timer/LocalTimer" import ServerTimer from "../timer/ServerTimer" @@ -39,6 +43,9 @@ const useStyles = makeStyles({ gap: "5px", padding: "20px 0 0 20px", }, + input: { + marginRight: "10px", + }, }) export const Timers = () => { @@ -47,6 +54,7 @@ export const Timers = () => { const dispatch = useAppDispatch() const timers = useAppSelector(selectAllTimers) const timersStatus = useAppSelector(selectTimersStatus) + const isInitialLoading = useAppSelector(selectIsInitialLoading) const [newTimerName, setNewTimerName] = useState("") useEffect(() => { @@ -92,21 +100,29 @@ export const Timers = () => {
- {timersStatus === "idle" && } + {timersStatus === "pending" && isInitialLoading && ( + + )}
-
+
setNewTimerName(e.target.value)} title="Enter the name of a new timer" placeholder="Enter the name" /> - + {/* */}
diff --git a/src/features/timers/slice.ts b/src/features/timers/slice.ts index fb95133..ffb1186 100644 --- a/src/features/timers/slice.ts +++ b/src/features/timers/slice.ts @@ -7,6 +7,7 @@ const initialState: TimersState = { map: {}, status: "idle", error: null, + isInitialLoading: true, } export const timers = createSlice({ @@ -19,6 +20,7 @@ export const timers = createSlice({ }) builder.addCase(fetchTimers.fulfilled, (state, action) => { state.status = "succeeded" + state.isInitialLoading = false state.map = action.payload.reduce( (acc, { id, start, elapsed, receivedAt }) => { acc[id] = { start, elapsed, receivedAt } @@ -58,3 +60,5 @@ export const selectAllTimers = createSelector( ) export const selectTimersStatus = (state: RootState) => state.timers.status export const selectTimersError = (state: RootState) => state.timers.error +export const selectIsInitialLoading = (state: RootState) => + state.timers.isInitialLoading diff --git a/src/features/timers/types.ts b/src/features/timers/types.ts index df9ba46..11c2895 100644 --- a/src/features/timers/types.ts +++ b/src/features/timers/types.ts @@ -15,4 +15,5 @@ export interface TimersState { map: Record status: "idle" | "pending" | "succeeded" | "failed" error: string | null + isInitialLoading: boolean } From e361256ce191b223e210b2e86331177140f7a3f4 Mon Sep 17 00:00:00 2001 From: Vladyslav Soloviov Date: Thu, 28 Aug 2025 04:48:50 +0300 Subject: [PATCH 18/30] renamed list --- src/app/App.tsx | 11 ++++++++--- src/app/store.ts | 2 +- src/features/timer/ServerTimer.tsx | 4 ++-- .../{timers/Timers.tsx => timersList/TimersList.tsx} | 6 +++--- src/features/{timers => timersList}/api.ts | 0 src/features/{timers => timersList}/slice.ts | 0 src/features/{timers => timersList}/thunks.ts | 0 src/features/{timers => timersList}/types.ts | 0 8 files changed, 14 insertions(+), 9 deletions(-) rename src/features/{timers/Timers.tsx => timersList/TimersList.tsx} (95%) rename src/features/{timers => timersList}/api.ts (100%) rename src/features/{timers => timersList}/slice.ts (100%) rename src/features/{timers => timersList}/thunks.ts (100%) rename src/features/{timers => timersList}/types.ts (100%) diff --git a/src/app/App.tsx b/src/app/App.tsx index 5f9433d..1217eaf 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -15,7 +15,7 @@ import { import type { Theme } from "@fluentui/react-components" import { useAppSelector } from "@/app/hooks" import type { SelectTabData, SelectTabEvent } from "@fluentui/react-components" -import { Timers } from "@/features/timers/Timers" +import { TimersList } from "@/features/timersList/TimersList" import { Settings } from "@/features/settings/Settings" import { selectTabsView, selectTheme } from "@/features/settings/slice" import { TabsView } from "@/components/TabsView" @@ -48,7 +48,12 @@ export const App = () => { } const tabs = [ - { id: "timers", label: "Timers", icon: , content: }, + { + id: "timers", + label: "Timers", + icon: , + content: , + }, { id: "settings", label: "Settings", @@ -68,7 +73,7 @@ export const App = () => { /> ) : (
- +
)} diff --git a/src/app/store.ts b/src/app/store.ts index 4bb016b..5fcb339 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -1,6 +1,6 @@ import { configureStore } from "@reduxjs/toolkit" import settingsReducer from "@/features/settings/slice" -import timersReducer from "@/features/timers/slice" +import timersReducer from "@/features/timersList/slice" import timerReducer from "@/features/timer/slice" import { tabsViewMiddleware } from "@/features/settings/middleware" import { timerMiddelware } from "@/features/timer/middelware" diff --git a/src/features/timer/ServerTimer.tsx b/src/features/timer/ServerTimer.tsx index d9ee502..030d48c 100644 --- a/src/features/timer/ServerTimer.tsx +++ b/src/features/timer/ServerTimer.tsx @@ -1,10 +1,10 @@ import { useAppSelector } from "@/app/hooks" import { TableCell, TableRow, Button } from "@fluentui/react-components" import { selectLocalTimer } from "./slice" -import { TimerWithId } from "@/features/timers/types" +import { TimerWithId } from "@/features/timersList/types" import { formatTime } from "@/helpers.ts/formatTime" import { useAppDispatch } from "@/app/hooks" -import { createTimer } from "@/features/timers/thunks" +import { createTimer } from "@/features/timersList/thunks" export default function ServerTimer({ id, elapsed, receivedAt }: TimerWithId) { useAppSelector(selectLocalTimer) diff --git a/src/features/timers/Timers.tsx b/src/features/timersList/TimersList.tsx similarity index 95% rename from src/features/timers/Timers.tsx rename to src/features/timersList/TimersList.tsx index f04bd86..4910f01 100644 --- a/src/features/timers/Timers.tsx +++ b/src/features/timersList/TimersList.tsx @@ -17,8 +17,8 @@ import { selectAllTimers, selectIsInitialLoading, selectTimersStatus, -} from "@/features/timers/slice" -import { fetchTimers, createTimer } from "@/features/timers/thunks" +} from "@/features/timersList/slice" +import { fetchTimers, createTimer } from "@/features/timersList/thunks" import LocalTimer from "@/features/timer/LocalTimer" import ServerTimer from "../timer/ServerTimer" import { resetTimer } from "../timer/slice" @@ -48,7 +48,7 @@ const useStyles = makeStyles({ }, }) -export const Timers = () => { +export const TimersList = () => { const inputId = useId("input") const styles = useStyles() const dispatch = useAppDispatch() diff --git a/src/features/timers/api.ts b/src/features/timersList/api.ts similarity index 100% rename from src/features/timers/api.ts rename to src/features/timersList/api.ts diff --git a/src/features/timers/slice.ts b/src/features/timersList/slice.ts similarity index 100% rename from src/features/timers/slice.ts rename to src/features/timersList/slice.ts diff --git a/src/features/timers/thunks.ts b/src/features/timersList/thunks.ts similarity index 100% rename from src/features/timers/thunks.ts rename to src/features/timersList/thunks.ts diff --git a/src/features/timers/types.ts b/src/features/timersList/types.ts similarity index 100% rename from src/features/timers/types.ts rename to src/features/timersList/types.ts From 36caa5267f470afda7164dc3107448b343045c94 Mon Sep 17 00:00:00 2001 From: Vladyslav Soloviov Date: Thu, 28 Aug 2025 16:22:05 +0300 Subject: [PATCH 19/30] separated form and timers list --- src/app/App.tsx | 8 ++- src/app/store.ts | 2 + src/features/newTimerForm/NewTimerForm.tsx | 73 ++++++++++++++++++++++ src/features/newTimerForm/slice.ts | 42 +++++++++++++ src/features/newTimerForm/types.ts | 4 ++ src/features/timer/ServerTimer.tsx | 3 +- src/features/timersList/TimersList.tsx | 56 +---------------- src/features/timersList/slice.ts | 45 ++++++++----- src/features/timersList/thunks.ts | 6 +- src/features/timersList/types.ts | 11 +++- 10 files changed, 175 insertions(+), 75 deletions(-) create mode 100644 src/features/newTimerForm/NewTimerForm.tsx create mode 100644 src/features/newTimerForm/slice.ts create mode 100644 src/features/newTimerForm/types.ts diff --git a/src/app/App.tsx b/src/app/App.tsx index 1217eaf..bcf75b4 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -16,6 +16,7 @@ import type { Theme } from "@fluentui/react-components" import { useAppSelector } from "@/app/hooks" import type { SelectTabData, SelectTabEvent } from "@fluentui/react-components" import { TimersList } from "@/features/timersList/TimersList" +import { NewTimerForm } from "@/features/newTimerForm/NewTimerForm" import { Settings } from "@/features/settings/Settings" import { selectTabsView, selectTheme } from "@/features/settings/slice" import { TabsView } from "@/components/TabsView" @@ -52,7 +53,12 @@ export const App = () => { id: "timers", label: "Timers", icon: , - content: , + content: ( + <> + + + + ), }, { id: "settings", diff --git a/src/app/store.ts b/src/app/store.ts index 5fcb339..1b8e83a 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -2,6 +2,7 @@ import { configureStore } from "@reduxjs/toolkit" import settingsReducer from "@/features/settings/slice" import timersReducer from "@/features/timersList/slice" import timerReducer from "@/features/timer/slice" +import newTimerFormReducer from "@/features/newTimerForm/slice" import { tabsViewMiddleware } from "@/features/settings/middleware" import { timerMiddelware } from "@/features/timer/middelware" @@ -10,6 +11,7 @@ export const store = configureStore({ settings: settingsReducer, timers: timersReducer, timer: timerReducer, + form: newTimerFormReducer, }, middleware: getDefaultMiddleware => getDefaultMiddleware().concat(tabsViewMiddleware, timerMiddelware), diff --git a/src/features/newTimerForm/NewTimerForm.tsx b/src/features/newTimerForm/NewTimerForm.tsx new file mode 100644 index 0000000..af2c440 --- /dev/null +++ b/src/features/newTimerForm/NewTimerForm.tsx @@ -0,0 +1,73 @@ +import { useState, FormEvent } from "react" +import { + Input, + Label, + Button, + makeStyles, + useId, + Spinner, +} from "@fluentui/react-components" +import { useAppDispatch, useAppSelector } from "@/app/hooks" +import { createTimer } from "@/features/timersList/thunks" +import { resetTimer } from "@/features/timer/slice" +import { selectFormStatus } from "@/features/newTimerForm/slice" +import { LOCAL } from "@/app/constants" +import { name } from "@/features/newTimerForm/slice" + +const useStyles = makeStyles({ + form: { + display: "flex", + flexDirection: "column", + gap: "5px", + padding: "20px 0 0 20px", + }, + input: { + marginRight: "10px", + }, +}) + +export const NewTimerForm = () => { + const inputId = useId("input") + const styles = useStyles() + const dispatch = useAppDispatch() + const status = useAppSelector(selectFormStatus) + const [newTimerName, setNewTimerName] = useState("") + + const onSubmit = (e: FormEvent) => { + e.preventDefault() + + if (newTimerName === LOCAL) { + dispatch(resetTimer()) + } else { + dispatch(createTimer({ id: newTimerName, source: name })) + } + + setNewTimerName("") + } + + return ( +
+ +
+ setNewTimerName(e.target.value)} + title="Enter the name of a new timer" + placeholder="Enter the name" + /> + + {status === "pending" && ( + + )} +
+
+ ) +} diff --git a/src/features/newTimerForm/slice.ts b/src/features/newTimerForm/slice.ts new file mode 100644 index 0000000..edcc350 --- /dev/null +++ b/src/features/newTimerForm/slice.ts @@ -0,0 +1,42 @@ +import { createSlice } from "@reduxjs/toolkit" +import type { RootState } from "@/app/store" +import { FormState } from "@/features/newTimerForm/types" +import { createTimer } from "@/features/timersList/thunks" + +export const name = "form" + +const initialState: FormState = { + status: "idle", + error: null, +} + +export const form = createSlice({ + name, + initialState, + reducers: {}, + extraReducers: builder => { + builder.addCase(createTimer.pending, (state, action) => { + if (action.meta.arg.source === name) { + state.status = "pending" + } + }) + builder.addCase(createTimer.fulfilled, (state, action) => { + if (action.meta.arg.source === name) { + state.status = "succeeded" + } + }) + builder.addCase(createTimer.rejected, (state, action: any) => { + if (action.meta.arg.source === name) { + state.status = "failed" + state.error = action.error.message ?? "Unknown Error" + } + }) + }, +}) + +export const {} = form.actions + +export default form.reducer + +export const selectFormStatus = (state: RootState) => state.form.status +export const selectFormError = (state: RootState) => state.form.error diff --git a/src/features/newTimerForm/types.ts b/src/features/newTimerForm/types.ts new file mode 100644 index 0000000..700a0f2 --- /dev/null +++ b/src/features/newTimerForm/types.ts @@ -0,0 +1,4 @@ +export interface FormState { + status: "idle" | "pending" | "succeeded" | "failed" + error: string | null +} diff --git a/src/features/timer/ServerTimer.tsx b/src/features/timer/ServerTimer.tsx index 030d48c..1bab629 100644 --- a/src/features/timer/ServerTimer.tsx +++ b/src/features/timer/ServerTimer.tsx @@ -5,6 +5,7 @@ import { TimerWithId } from "@/features/timersList/types" import { formatTime } from "@/helpers.ts/formatTime" import { useAppDispatch } from "@/app/hooks" import { createTimer } from "@/features/timersList/thunks" +import { name } from "@/features/timersList/slice" export default function ServerTimer({ id, elapsed, receivedAt }: TimerWithId) { useAppSelector(selectLocalTimer) @@ -18,7 +19,7 @@ export default function ServerTimer({ id, elapsed, receivedAt }: TimerWithId) { - {status === "pending" && ( + {status === STATUS.pending && ( )} - {status === "failed" && ( + {status === STATUS.failed && ( {error} )}
diff --git a/src/features/timer/ServerTimer.tsx b/src/features/timer/ServerTimer.tsx index 6f0ea73..fbd85b9 100644 --- a/src/features/timer/ServerTimer.tsx +++ b/src/features/timer/ServerTimer.tsx @@ -12,6 +12,7 @@ import { selectResetError, } from "@/features/timersList/slice" import { ThemeSpinner } from "@/components/ThemeSpinner" +import { STATUS } from "@/app/constants" export default function ServerTimer({ id, elapsed, receivedAt }: TimerWithId) { useAppSelector(selectLocalTimer) @@ -20,8 +21,8 @@ export default function ServerTimer({ id, elapsed, receivedAt }: TimerWithId) { const resetStatus = useAppSelector(selectResetStatus) const resetId = useAppSelector(selectResetId) const isCurrentId = id === resetId - const isPending = resetStatus === "pending" && isCurrentId - const isFailed = resetStatus === "failed" && isCurrentId + const isPending = resetStatus === STATUS.pending && isCurrentId + const isFailed = resetStatus === STATUS.failed && isCurrentId const error = useAppSelector(selectResetError) return ( diff --git a/src/features/timersList/TimersList.tsx b/src/features/timersList/TimersList.tsx index 3c902ac..ff46b53 100644 --- a/src/features/timersList/TimersList.tsx +++ b/src/features/timersList/TimersList.tsx @@ -18,6 +18,7 @@ import { fetchTimers } from "@/features/timersList/thunks" import LocalTimer from "@/features/timer/LocalTimer" import ServerTimer from "@/features/timer/ServerTimer" import { ThemeSpinner } from "@/components/ThemeSpinner" +import { STATUS } from "@/app/constants" const columns = [ { columnKey: "name", label: "Name" }, @@ -73,10 +74,10 @@ export const TimersList = () => {
- {timersStatus === "pending" && isInitialLoading && ( + {timersStatus === STATUS.pending && isInitialLoading && ( )} - {timersStatus === "failed" && ( + {timersStatus === STATUS.failed && ( {error} )}
From 65ad8210f62b0202252654e9fb8cf1479a251767 Mon Sep 17 00:00:00 2001 From: Vladyslav Soloviov Date: Sun, 31 Aug 2025 16:30:22 +0300 Subject: [PATCH 30/30] renamed midleware --- src/app/store.ts | 4 ++-- src/features/settings/middleware.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/store.ts b/src/app/store.ts index 731c03b..ca29928 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -3,7 +3,7 @@ import settingsReducer from "@/features/settings/slice" import timersReducer from "@/features/timersList/slice" import timerReducer from "@/features/timer/slice" import newTimerFormReducer from "@/features/newTimerForm/slice" -import { tabsViewMiddleware } from "@/features/settings/middleware" +import { settingsMiddleware } from "@/features/settings/middleware" import { timerMiddelware } from "@/features/timer/middelware" export const store = configureStore({ @@ -14,7 +14,7 @@ export const store = configureStore({ form: newTimerFormReducer, }, middleware: getDefaultMiddleware => - getDefaultMiddleware().concat(tabsViewMiddleware, timerMiddelware), + getDefaultMiddleware().concat(settingsMiddleware, timerMiddelware), }) export type AppStore = typeof store diff --git a/src/features/settings/middleware.ts b/src/features/settings/middleware.ts index 1045ea3..c113820 100644 --- a/src/features/settings/middleware.ts +++ b/src/features/settings/middleware.ts @@ -2,7 +2,7 @@ import { Middleware } from "@reduxjs/toolkit" import { toggleTabs, togglePreserveLocalTimer } from "@/features/settings/slice" import { saveLocalTimerToStorage } from "../timer/slice" -export const tabsViewMiddleware: Middleware = store => next => action => { +export const settingsMiddleware: Middleware = store => next => action => { const result = next(action) const state = store.getState()