Skip to content

Commit 6dfd2b2

Browse files
authored
feat: runner dashboard save data in local storage (#271)
1 parent c1acaa2 commit 6dfd2b2

4 files changed

Lines changed: 60 additions & 25 deletions

File tree

src/hooks/useLocalStorage.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useEffect, useRef, useState } from 'preact/hooks'
2+
3+
export function useLocalStorage<T>(key: string, defaultValue: T) {
4+
const [value, setValue] = useState<T>(defaultValue)
5+
const isFirstRender = useRef(true)
6+
7+
// Read from localStorage after hydration
8+
useEffect(() => {
9+
try {
10+
const stored = window.localStorage.getItem(key)
11+
if (stored !== null) {
12+
setValue(JSON.parse(stored) as T)
13+
}
14+
} catch {
15+
// localStorage unavailable
16+
}
17+
isFirstRender.current = false
18+
}, [key])
19+
20+
// Persist to localStorage on subsequent changes (skip initial render)
21+
useEffect(() => {
22+
if (isFirstRender.current) return
23+
try {
24+
window.localStorage.setItem(key, JSON.stringify(value))
25+
} catch {
26+
/* storage full or unavailable */
27+
}
28+
}, [key, value])
29+
30+
return [value, setValue] as const
31+
}

src/pages/apps/GlucidCalculatorComponent.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { JSX } from 'preact'
22
// biome-ignore lint/correctness/noUnusedImports: Needed for JSX types
33
import * as React from 'preact/compat'
4-
import { useState } from 'preact/hooks'
4+
import { useLocalStorage } from '../../hooks/useLocalStorage'
55

66
type NutritionZone = {
77
min: number
@@ -51,12 +51,12 @@ const PRESET_DISTANCES = [
5151
]
5252

5353
export default function GlucidCalculator(): JSX.Element {
54-
const [weight, setWeight] = useState<number>(70)
55-
const [distance, setDistance] = useState<number>(42.195)
56-
const [estimatedHours, setEstimatedHours] = useState<number>(3)
57-
const [estimatedMinutes, setEstimatedMinutes] = useState<number>(50)
58-
const [glucidPerGel, setGlucidPerGel] = useState<number>(25)
59-
const [numberOfGels, setNumberOfGels] = useState<number>(9)
54+
const [weight, setWeight] = useLocalStorage<number>('runner-dashboard:nutrition.weight', 70)
55+
const [distance, setDistance] = useLocalStorage<number>('runner-dashboard:nutrition.distance', 42.195)
56+
const [estimatedHours, setEstimatedHours] = useLocalStorage<number>('runner-dashboard:nutrition.estimatedHours', 3)
57+
const [estimatedMinutes, setEstimatedMinutes] = useLocalStorage<number>('runner-dashboard:nutrition.estimatedMinutes', 50)
58+
const [glucidPerGel, setGlucidPerGel] = useLocalStorage<number>('runner-dashboard:nutrition.glucidPerGel', 25)
59+
const [numberOfGels, setNumberOfGels] = useLocalStorage<number>('runner-dashboard:nutrition.numberOfGels', 9)
6060

6161
// Calculations
6262
const totalTimeHours = estimatedHours + estimatedMinutes / 60

src/pages/apps/RunnerDashboardTabs.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { JSX } from 'preact'
22
// biome-ignore lint/correctness/noUnusedImports: Needed for JSX types
33
import * as React from 'preact/compat'
4-
import { useState } from 'preact/hooks'
4+
import { useLocalStorage } from '../../hooks/useLocalStorage'
55
import GlucidCalculatorComponent from './GlucidCalculatorComponent'
66
import PaceCalculatorComponent from './PaceCalculatorComponent'
77
import SplitsCalculatorComponent from './SplitsCalculatorComponent'
@@ -15,7 +15,7 @@ const TABS: { id: Tab; label: string; icon: string }[] = [
1515
]
1616

1717
export default function RunnerDashboardTabs(): JSX.Element {
18-
const [activeTab, setActiveTab] = useState<Tab>('pace')
18+
const [activeTab, setActiveTab] = useLocalStorage<Tab>('runner-dashboard:activeTab', 'pace')
1919

2020
return (
2121
<div class="space-y-6">

src/pages/apps/SplitsCalculatorComponent.tsx

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { JSX } from 'preact'
22
// biome-ignore lint/correctness/noUnusedImports: Needed for JSX types
33
import * as React from 'preact/compat'
44
import type { Dispatch, StateUpdater } from 'preact/hooks'
5-
import { useState } from 'preact/hooks'
5+
import { useLocalStorage } from '../../hooks/useLocalStorage'
66

77
type Distance = {
88
name: string
@@ -56,20 +56,24 @@ function calculateSpeed(totalSeconds: number, distanceKm: number): number {
5656
}
5757

5858
export default function SplitsCalculator(): JSX.Element {
59-
const [selectedDistance, setSelectedDistance] = useState<Distance>(
60-
DISTANCES[6],
61-
) // Marathon default
62-
const [time1, setTime1] = useState<TimeInput>({
63-
hours: 3,
64-
minutes: 50,
65-
seconds: 0,
66-
})
67-
const [showSecondTime, setShowSecondTime] = useState(false)
68-
const [time2, setTime2] = useState<TimeInput>({
69-
hours: 4,
70-
minutes: 15,
71-
seconds: 0,
72-
})
59+
const [distanceName, setDistanceName] = useLocalStorage<string>(
60+
'runner-dashboard:splits.distanceName',
61+
'Marathon',
62+
)
63+
const selectedDistance =
64+
DISTANCES.find(d => d.name === distanceName) ?? DISTANCES[6]
65+
const [time1, setTime1] = useLocalStorage<TimeInput>(
66+
'runner-dashboard:splits.time1',
67+
{ hours: 3, minutes: 50, seconds: 0 },
68+
)
69+
const [showSecondTime, setShowSecondTime] = useLocalStorage<boolean>(
70+
'runner-dashboard:splits.showSecondTime',
71+
false,
72+
)
73+
const [time2, setTime2] = useLocalStorage<TimeInput>(
74+
'runner-dashboard:splits.time2',
75+
{ hours: 4, minutes: 15, seconds: 0 },
76+
)
7377

7478
const totalSeconds1 = timeToSeconds(time1)
7579
const pacePerKm1 = calculatePacePerKm(totalSeconds1, selectedDistance.km)
@@ -139,7 +143,7 @@ export default function SplitsCalculator(): JSX.Element {
139143
<button
140144
key={dist.name}
141145
type="button"
142-
onClick={() => setSelectedDistance(dist)}
146+
onClick={() => setDistanceName(dist.name)}
143147
class={`rounded-lg px-4 py-2 text-sm font-medium transition-all cursor-pointer ${
144148
selectedDistance.name === dist.name
145149
? 'bg-blue-500 text-white shadow-md'

0 commit comments

Comments
 (0)