From d12182717c28be4eb28e3b83d9554774879f536a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 01:44:45 +0000 Subject: [PATCH 1/6] Initial plan From 0ebff8b4c2e8a160a51f26f3117f783a4ba0d81d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 01:53:22 +0000 Subject: [PATCH 2/6] Add Update section to Settings page with useAppUpdateAvailable refactor Co-authored-by: vanister <7736967+vanister@users.noreply.github.com> --- src/hooks/useAppUpdateAvailable.ts | 2 ++ src/pages/settings/Settings.tsx | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/hooks/useAppUpdateAvailable.ts b/src/hooks/useAppUpdateAvailable.ts index 2f0c814..648b6c8 100644 --- a/src/hooks/useAppUpdateAvailable.ts +++ b/src/hooks/useAppUpdateAvailable.ts @@ -28,4 +28,6 @@ export function useAppUpdateAvailable() { action: { label: 'Reload', onClick: applyUpdate } }); }, [needsUpdate, applyUpdate, showToast]); + + return { needsUpdate, applyUpdate }; } diff --git a/src/pages/settings/Settings.tsx b/src/pages/settings/Settings.tsx index 3167d2a..87c207d 100644 --- a/src/pages/settings/Settings.tsx +++ b/src/pages/settings/Settings.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import type { Location } from '../../data/data-types'; +import { useAppUpdateAvailable } from '../../hooks/useAppUpdateAvailable'; import { useLocations } from '../../hooks/useLocations'; import { usePageTitle } from '../../hooks/usePageTitle'; import { useImmerState } from '../../hooks/useImmerState'; @@ -26,6 +27,7 @@ export function Settings() { const navigate = useNavigate(); const { getLocationList, deleteLocation } = useLocations(); + const { needsUpdate, applyUpdate } = useAppUpdateAvailable(); const [locations, setLocations] = useState([]); useEffect(() => { @@ -131,6 +133,30 @@ export function Settings() { + {needsUpdate && ( +
+ +
+
+
+
+ +
+

A new version is available

+
+ +
+
+
+ )} +
From 27140e692ba187170130dab192db8465785c30db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 02:15:38 +0000 Subject: [PATCH 3/6] Extract SettingsSection wrapper component to consolidate repeating section styles Co-authored-by: vanister <7736967+vanister@users.noreply.github.com> --- package-lock.json | 8 +- package.json | 2 +- src/pages/settings/Settings.tsx | 106 ++++++++++++------------- src/pages/settings/SettingsSection.tsx | 20 +++++ 4 files changed, 74 insertions(+), 62 deletions(-) create mode 100644 src/pages/settings/SettingsSection.tsx diff --git a/package-lock.json b/package-lock.json index dd167a9..a7aac99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@cloudflare/vite-plugin": "^1.25.6", "@eslint/js": "^9.39.1", "@tailwindcss/vite": "^4.1.18", - "@types/node": "^24.10.1", + "@types/node": "^24.11.0", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", @@ -3951,9 +3951,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", - "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", + "version": "24.11.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz", + "integrity": "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 541983f..74ea20c 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@cloudflare/vite-plugin": "^1.25.6", "@eslint/js": "^9.39.1", "@tailwindcss/vite": "^4.1.18", - "@types/node": "^24.10.1", + "@types/node": "^24.11.0", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", diff --git a/src/pages/settings/Settings.tsx b/src/pages/settings/Settings.tsx index 87c207d..dfa326a 100644 --- a/src/pages/settings/Settings.tsx +++ b/src/pages/settings/Settings.tsx @@ -10,6 +10,7 @@ import { Icon } from '../../components/Icon'; import { SectionHeader } from '../../components/SectionHeader'; import { EmptyState } from '../../components/EmptyState'; import { LocationItem } from './LocationItem'; +import { SettingsSection } from './SettingsSection'; import { formatBytes } from '../../utilities/formatUtils'; type SettingsState = { @@ -107,73 +108,64 @@ export function Settings() { )}
-
- -
- {state.storageUsed !== null && state.storageQuota !== null ? ( - <> -
- Used - - {formatBytes(state.storageUsed)} of {formatBytes(state.storageQuota)} - + + {state.storageUsed !== null && state.storageQuota !== null ? ( + <> +
+ Used + + {formatBytes(state.storageUsed)} of {formatBytes(state.storageQuota)} + +
+ {storagePercent !== null && ( +
+
- {storagePercent !== null && ( -
-
-
- )} - - ) : ( -

Storage information unavailable

- )} -
-
+ )} + + ) : ( +

Storage information unavailable

+ )} + {needsUpdate && ( -
- -
-
-
-
- -
-

A new version is available

+ +
+
+
+
- +

A new version is available

+
-
+ )} -
- -
-
-
- -
-
-

EV Charge Tracker

-

Version 1.0.0

-
+ +
+
+ +
+
+

EV Charge Tracker

+

Version 1.0.0

-

- Track your electric vehicle charging sessions, costs, and usage across all your locations. -

-
+

+ Track your electric vehicle charging sessions, costs, and usage across all your locations. +

+ ); diff --git a/src/pages/settings/SettingsSection.tsx b/src/pages/settings/SettingsSection.tsx new file mode 100644 index 0000000..c050821 --- /dev/null +++ b/src/pages/settings/SettingsSection.tsx @@ -0,0 +1,20 @@ +import { clsx } from 'clsx'; +import type { ReactNode } from 'react'; +import { SectionHeader } from '../../components/SectionHeader'; + +type SettingsSectionProps = { + title: string; + children: ReactNode; + cardClassName?: string; +}; + +export function SettingsSection({ title, children, cardClassName }: SettingsSectionProps) { + return ( +
+ +
+ {children} +
+
+ ); +} From 5b5a77be9f65a08820b0045ae3183f4026766e71 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 05:12:24 +0000 Subject: [PATCH 4/6] Extract section body components; fix storage null checks with != null guard clause Co-authored-by: vanister <7736967+vanister@users.noreply.github.com> --- src/pages/settings/AboutSectionBody.tsx | 20 +++ src/pages/settings/LocationsSection.tsx | 72 ++++++++++ src/pages/settings/Settings.tsx | 153 ++-------------------- src/pages/settings/StorageSectionBody.tsx | 57 ++++++++ src/pages/settings/UpdateSectionBody.tsx | 26 ++++ 5 files changed, 183 insertions(+), 145 deletions(-) create mode 100644 src/pages/settings/AboutSectionBody.tsx create mode 100644 src/pages/settings/LocationsSection.tsx create mode 100644 src/pages/settings/StorageSectionBody.tsx create mode 100644 src/pages/settings/UpdateSectionBody.tsx diff --git a/src/pages/settings/AboutSectionBody.tsx b/src/pages/settings/AboutSectionBody.tsx new file mode 100644 index 0000000..9a0be74 --- /dev/null +++ b/src/pages/settings/AboutSectionBody.tsx @@ -0,0 +1,20 @@ +import { Icon } from '../../components/Icon'; + +export function AboutSectionBody() { + return ( + <> +
+
+ +
+
+

EV Charge Tracker

+

Version 1.0.0

+
+
+

+ Track your electric vehicle charging sessions, costs, and usage across all your locations. +

+ + ); +} diff --git a/src/pages/settings/LocationsSection.tsx b/src/pages/settings/LocationsSection.tsx new file mode 100644 index 0000000..716686a --- /dev/null +++ b/src/pages/settings/LocationsSection.tsx @@ -0,0 +1,72 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import type { Location } from '../../data/data-types'; +import { useLocations } from '../../hooks/useLocations'; +import { ItemListButton } from '../../components/ItemListButton'; +import { SectionHeader } from '../../components/SectionHeader'; +import { EmptyState } from '../../components/EmptyState'; +import { LocationItem } from './LocationItem'; + +export function LocationsSection() { + const navigate = useNavigate(); + const { getLocationList, deleteLocation } = useLocations(); + const [locations, setLocations] = useState([]); + + useEffect(() => { + const loadLocations = async () => { + const result = await getLocationList(); + + if (result.success) { + setLocations(result.data); + } + }; + + loadLocations(); + }, [getLocationList]); + + const handleDelete = async (id: string) => { + const confirmed = window.confirm('Are you sure you want to delete this location?'); + + if (!confirmed) { + return; + } + + const result = await deleteLocation(id); + + if (!result.success) { + alert(`Failed to delete location: ${result.error}`); + return; + } + + setLocations((prev) => prev.filter((l) => l.id !== id)); + }; + + return ( +
+ + + {locations.length === 0 ? ( + navigate('/settings/locations/add')} + /> + ) : ( +
+ navigate('/settings/locations/add')} + className="mb-3" + /> +
+ {locations.map((location) => ( + + ))} +
+
+ )} +
+ ); +} diff --git a/src/pages/settings/Settings.tsx b/src/pages/settings/Settings.tsx index dfa326a..6f9ce55 100644 --- a/src/pages/settings/Settings.tsx +++ b/src/pages/settings/Settings.tsx @@ -1,170 +1,33 @@ -import { useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import type { Location } from '../../data/data-types'; import { useAppUpdateAvailable } from '../../hooks/useAppUpdateAvailable'; -import { useLocations } from '../../hooks/useLocations'; import { usePageTitle } from '../../hooks/usePageTitle'; -import { useImmerState } from '../../hooks/useImmerState'; -import { ItemListButton } from '../../components/ItemListButton'; -import { Icon } from '../../components/Icon'; -import { SectionHeader } from '../../components/SectionHeader'; -import { EmptyState } from '../../components/EmptyState'; -import { LocationItem } from './LocationItem'; import { SettingsSection } from './SettingsSection'; -import { formatBytes } from '../../utilities/formatUtils'; - -type SettingsState = { - storageUsed: number | null; - storageQuota: number | null; -}; - -const DEFAULT_STATE: SettingsState = { - storageUsed: null, - storageQuota: null -}; +import { LocationsSection } from './LocationsSection'; +import { StorageSectionBody } from './StorageSectionBody'; +import { UpdateSectionBody } from './UpdateSectionBody'; +import { AboutSectionBody } from './AboutSectionBody'; export function Settings() { usePageTitle('Settings'); - const navigate = useNavigate(); - const { getLocationList, deleteLocation } = useLocations(); const { needsUpdate, applyUpdate } = useAppUpdateAvailable(); - const [locations, setLocations] = useState([]); - - useEffect(() => { - const loadLocations = async () => { - const result = await getLocationList(); - - if (result.success) { - setLocations(result.data); - } - }; - - loadLocations(); - }, [getLocationList]); - const [state, setState] = useImmerState(DEFAULT_STATE); - - useEffect(() => { - const loadStorageEstimate = async () => { - const estimate = await navigator.storage?.estimate?.(); - setState((draft) => { - draft.storageUsed = estimate?.usage ?? null; - draft.storageQuota = estimate?.quota ?? null; - }); - }; - - loadStorageEstimate(); - }, [setState]); - - const handleDelete = async (id: string) => { - const confirmed = window.confirm('Are you sure you want to delete this location?'); - - if (!confirmed) { - return; - } - - const result = await deleteLocation(id); - - if (!result.success) { - alert(`Failed to delete location: ${result.error}`); - return; - } - - setLocations((prev) => prev.filter((l) => l.id !== id)); - }; - - const storagePercent = - state.storageUsed !== null && state.storageQuota !== null && state.storageQuota > 0 - ? Math.min(100, (state.storageUsed / state.storageQuota) * 100) - : null; return (
-
- - - {locations.length === 0 ? ( - navigate('/settings/locations/add')} - /> - ) : ( -
- navigate('/settings/locations/add')} - className="mb-3" - /> -
- {locations.map((location) => ( - - ))} -
-
- )} -
+ - {state.storageUsed !== null && state.storageQuota !== null ? ( - <> -
- Used - - {formatBytes(state.storageUsed)} of {formatBytes(state.storageQuota)} - -
- {storagePercent !== null && ( -
-
-
- )} - - ) : ( -

Storage information unavailable

- )} + {needsUpdate && ( -
-
-
- -
-

A new version is available

-
- -
+
)} -
-
- -
-
-

EV Charge Tracker

-

Version 1.0.0

-
-
-

- Track your electric vehicle charging sessions, costs, and usage across all your locations. -

+
diff --git a/src/pages/settings/StorageSectionBody.tsx b/src/pages/settings/StorageSectionBody.tsx new file mode 100644 index 0000000..b6f7844 --- /dev/null +++ b/src/pages/settings/StorageSectionBody.tsx @@ -0,0 +1,57 @@ +import { useEffect } from 'react'; +import { useImmerState } from '../../hooks/useImmerState'; +import { formatBytes } from '../../utilities/formatUtils'; + +type StorageState = { + storageUsed: number | null; + storageQuota: number | null; +}; + +const DEFAULT_STATE: StorageState = { + storageUsed: null, + storageQuota: null +}; + +export function StorageSectionBody() { + const [state, setState] = useImmerState(DEFAULT_STATE); + + useEffect(() => { + const loadStorageEstimate = async () => { + const estimate = await navigator.storage?.estimate?.(); + setState((draft) => { + draft.storageUsed = estimate?.usage ?? null; + draft.storageQuota = estimate?.quota ?? null; + }); + }; + + loadStorageEstimate(); + }, [setState]); + + if (state.storageUsed == null || state.storageQuota == null) { + return

Storage information unavailable

; + } + + const storagePercent = + state.storageQuota > 0 + ? Math.min(100, (state.storageUsed / state.storageQuota) * 100) + : null; + + return ( + <> +
+ Used + + {formatBytes(state.storageUsed)} of {formatBytes(state.storageQuota)} + +
+ {storagePercent != null && ( +
+
+
+ )} + + ); +} diff --git a/src/pages/settings/UpdateSectionBody.tsx b/src/pages/settings/UpdateSectionBody.tsx new file mode 100644 index 0000000..a7028b5 --- /dev/null +++ b/src/pages/settings/UpdateSectionBody.tsx @@ -0,0 +1,26 @@ +import { Icon } from '../../components/Icon'; + +type UpdateSectionBodyProps = { + onApply: () => void; +}; + +export function UpdateSectionBody({ onApply }: UpdateSectionBodyProps) { + return ( +
+
+
+ +
+

A new version is available

+
+ +
+ ); +} From 2e220be72d62cf76f222e442e7f2e0634f186a02 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 10:54:58 +0000 Subject: [PATCH 5/6] Hook dontToast param; self-contained UpdateSectionBody; always-visible Update section; LocationsSection as SettingsSection child Co-authored-by: vanister <7736967+vanister@users.noreply.github.com> --- src/hooks/useAppUpdateAvailable.ts | 6 +-- src/pages/settings/LocationsSection.tsx | 51 ++++++++++-------------- src/pages/settings/Settings.tsx | 17 ++++---- src/pages/settings/UpdateSectionBody.tsx | 21 +++++++--- 4 files changed, 48 insertions(+), 47 deletions(-) diff --git a/src/hooks/useAppUpdateAvailable.ts b/src/hooks/useAppUpdateAvailable.ts index 648b6c8..f755c4e 100644 --- a/src/hooks/useAppUpdateAvailable.ts +++ b/src/hooks/useAppUpdateAvailable.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef } from 'react'; import { useRegisterSW } from 'virtual:pwa-register/react'; import { useToast } from './useToast'; -export function useAppUpdateAvailable() { +export function useAppUpdateAvailable(dontToast = false) { const { needRefresh: [needsUpdate], updateServiceWorker @@ -16,7 +16,7 @@ export function useAppUpdateAvailable() { }, [updateServiceWorker]); useEffect(() => { - if (!needsUpdate || toastShownRef.current) { + if (!needsUpdate || toastShownRef.current || dontToast) { return; } @@ -27,7 +27,7 @@ export function useAppUpdateAvailable() { persistent: true, action: { label: 'Reload', onClick: applyUpdate } }); - }, [needsUpdate, applyUpdate, showToast]); + }, [needsUpdate, applyUpdate, showToast, dontToast]); return { needsUpdate, applyUpdate }; } diff --git a/src/pages/settings/LocationsSection.tsx b/src/pages/settings/LocationsSection.tsx index 716686a..d9f5805 100644 --- a/src/pages/settings/LocationsSection.tsx +++ b/src/pages/settings/LocationsSection.tsx @@ -3,11 +3,10 @@ import { useNavigate } from 'react-router-dom'; import type { Location } from '../../data/data-types'; import { useLocations } from '../../hooks/useLocations'; import { ItemListButton } from '../../components/ItemListButton'; -import { SectionHeader } from '../../components/SectionHeader'; import { EmptyState } from '../../components/EmptyState'; import { LocationItem } from './LocationItem'; -export function LocationsSection() { +export function LocationsSectionBody() { const navigate = useNavigate(); const { getLocationList, deleteLocation } = useLocations(); const [locations, setLocations] = useState([]); @@ -41,32 +40,26 @@ export function LocationsSection() { setLocations((prev) => prev.filter((l) => l.id !== id)); }; - return ( -
- - - {locations.length === 0 ? ( - navigate('/settings/locations/add')} - /> - ) : ( -
- navigate('/settings/locations/add')} - className="mb-3" - /> -
- {locations.map((location) => ( - - ))} -
-
- )} -
+ return locations.length === 0 ? ( + navigate('/settings/locations/add')} + /> + ) : ( +
+ navigate('/settings/locations/add')} + className="mb-3" + /> +
+ {locations.map((location) => ( + + ))} +
+
); } diff --git a/src/pages/settings/Settings.tsx b/src/pages/settings/Settings.tsx index 6f9ce55..58bbc8b 100644 --- a/src/pages/settings/Settings.tsx +++ b/src/pages/settings/Settings.tsx @@ -1,7 +1,6 @@ -import { useAppUpdateAvailable } from '../../hooks/useAppUpdateAvailable'; import { usePageTitle } from '../../hooks/usePageTitle'; import { SettingsSection } from './SettingsSection'; -import { LocationsSection } from './LocationsSection'; +import { LocationsSectionBody } from './LocationsSection'; import { StorageSectionBody } from './StorageSectionBody'; import { UpdateSectionBody } from './UpdateSectionBody'; import { AboutSectionBody } from './AboutSectionBody'; @@ -9,22 +8,20 @@ import { AboutSectionBody } from './AboutSectionBody'; export function Settings() { usePageTitle('Settings'); - const { needsUpdate, applyUpdate } = useAppUpdateAvailable(); - return (
- + + + - {needsUpdate && ( - - - - )} + + + diff --git a/src/pages/settings/UpdateSectionBody.tsx b/src/pages/settings/UpdateSectionBody.tsx index a7028b5..77a65a4 100644 --- a/src/pages/settings/UpdateSectionBody.tsx +++ b/src/pages/settings/UpdateSectionBody.tsx @@ -1,10 +1,21 @@ +import { useAppUpdateAvailable } from '../../hooks/useAppUpdateAvailable'; import { Icon } from '../../components/Icon'; -type UpdateSectionBodyProps = { - onApply: () => void; -}; +export function UpdateSectionBody() { + // dontToast=true prevents the toast from showing while the user is already on this page + const { needsUpdate, applyUpdate } = useAppUpdateAvailable(true); + + if (!needsUpdate) { + return ( +
+
+ +
+

App is up to date

+
+ ); + } -export function UpdateSectionBody({ onApply }: UpdateSectionBodyProps) { return (
@@ -15,7 +26,7 @@ export function UpdateSectionBody({ onApply }: UpdateSectionBodyProps) {

A new version is available

- +
); }