11import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
22import { fetchRuns , fetchSensors , fetchScannerStatus , triggerScan , updateNote , fetchSeasons } from "./api" ;
3+ import { Moon , Sun } from "lucide-react" ;
34import { RunRecord , RunsResponse , ScannerStatus , SensorsResponse , Season } from "./types" ;
45import { RunTable } from "./components/RunTable" ;
56import { DataDownload } from "./components/data-download" ;
67
78type ScanState = "idle" | "running" | "success" | "error" ;
9+ type Theme = "light" | "dark" ;
10+
11+ function getInitialTheme ( ) : Theme {
12+ if ( typeof window === "undefined" ) return "light" ;
13+ const saved = window . localStorage . getItem ( "theme" ) ;
14+ if ( saved === "light" || saved === "dark" ) return saved ;
15+ return window . matchMedia ( "(prefers-color-scheme: dark)" ) . matches ? "dark" : "light" ;
16+ }
817
918interface DownloaderSelection {
1019 runKey ?: string ;
@@ -27,6 +36,7 @@ export default function App() {
2736 const [ scanSeason , setScanSeason ] = useState < string > ( "" ) ;
2837 const [ downloaderSelection , setDownloaderSelection ] = useState < DownloaderSelection | null > ( null ) ;
2938 const [ scannerStatus , setScannerStatus ] = useState < ScannerStatus | null > ( null ) ;
39+ const [ theme , setTheme ] = useState < Theme > ( getInitialTheme ) ;
3040 const sensorsSectionRef = useRef < HTMLElement | null > ( null ) ;
3141 const downloaderSectionRef = useRef < HTMLElement | null > ( null ) ;
3242 const statusFinishedRef = useRef < string | null > ( null ) ;
@@ -89,6 +99,11 @@ export default function App() {
8999 [ loadData ]
90100 ) ;
91101
102+ useEffect ( ( ) => {
103+ document . documentElement . setAttribute ( "data-theme" , theme ) ;
104+ window . localStorage . setItem ( "theme" , theme ) ;
105+ } , [ theme ] ) ;
106+
92107 useEffect ( ( ) => {
93108 void loadData ( ) ;
94109 void loadStatus ( false ) ;
@@ -230,36 +245,46 @@ export default function App() {
230245 </ p >
231246 </ div >
232247
233- { seasons . length > 0 && (
234- < div style = { { textAlign : "right" } } >
235- < p style = { { fontSize : "0.7rem" , color : "#6b7280" , margin : "0 0 0.25rem 0" , textTransform : "uppercase" , letterSpacing : "0.05em" } } > Active Season</ p >
236- < div style = { { display : "flex" , flexWrap : "wrap" , gap : "0" , border : "1px solid #e5e7eb" , borderRadius : "6px" , overflow : "hidden" , maxWidth : "320px" } } >
237- { seasons . map ( s => {
238- const active = s . name === selectedSeason ;
239- const sc = seasonColor ( s . name ) ;
240- return (
241- < button
242- key = { s . name }
243- onClick = { ( ) => setSelectedSeason ( s . name ) }
244- style = { {
245- padding : "0.35rem 0.75rem" ,
246- border : "none" ,
247- borderRight : "1px solid #e5e7eb" ,
248- background : active ? sc : "transparent" ,
249- color : active ? "#fff" : sc ,
250- fontWeight : active ? "bold" : "normal" ,
251- fontSize : "0.85rem" ,
252- cursor : "pointer" ,
253- transition : "background 0.15s" ,
254- } }
255- >
256- { s . name }
257- </ button >
258- ) ;
259- } ) }
248+ < div style = { { display : "flex" , gap : "0.75rem" , alignItems : "flex-start" } } >
249+ { seasons . length > 0 && (
250+ < div style = { { textAlign : "right" } } >
251+ < p style = { { fontSize : "0.7rem" , color : "var(--text-muted)" , margin : "0 0 0.25rem 0" , textTransform : "uppercase" , letterSpacing : "0.05em" } } > Active Season</ p >
252+ < div style = { { display : "flex" , flexWrap : "wrap" , gap : "0" , border : "1px solid var(--border)" , borderRadius : "6px" , overflow : "hidden" , maxWidth : "320px" } } >
253+ { seasons . map ( s => {
254+ const active = s . name === selectedSeason ;
255+ const sc = seasonColor ( s . name ) ;
256+ return (
257+ < button
258+ key = { s . name }
259+ onClick = { ( ) => setSelectedSeason ( s . name ) }
260+ style = { {
261+ padding : "0.35rem 0.75rem" ,
262+ border : "none" ,
263+ borderRight : "1px solid var(--border)" ,
264+ background : active ? sc : "transparent" ,
265+ color : active ? "#fff" : sc ,
266+ fontWeight : active ? "bold" : "normal" ,
267+ fontSize : "0.85rem" ,
268+ cursor : "pointer" ,
269+ transition : "background 0.15s" ,
270+ } }
271+ >
272+ { s . name }
273+ </ button >
274+ ) ;
275+ } ) }
276+ </ div >
260277 </ div >
261- </ div >
262- ) }
278+ ) }
279+ < button
280+ className = "theme-toggle"
281+ onClick = { ( ) => setTheme ( ( t ) => ( t === "dark" ? "light" : "dark" ) ) }
282+ aria-label = { `Switch to ${ theme === "dark" ? "light" : "dark" } mode` }
283+ title = { `Switch to ${ theme === "dark" ? "light" : "dark" } mode` }
284+ >
285+ { theme === "dark" ? < Sun size = { 18 } /> : < Moon size = { 18 } /> }
286+ </ button >
287+ </ div >
263288 </ div >
264289 </ header >
265290
@@ -275,7 +300,7 @@ export default function App() {
275300 value = { scanSeason }
276301 onChange = { ( e ) => setScanSeason ( e . target . value ) }
277302 disabled = { scanButtonDisabled }
278- style = { { padding : "0.5rem" , borderRadius : "4px" , border : "1px solid #ccc " , fontSize : "0.9rem" } }
303+ style = { { padding : "0.5rem" , borderRadius : "4px" , border : "1px solid var(--border-strong) " , fontSize : "0.9rem" , background : "var(--surface)" , color : "var(--text) " } }
279304 >
280305 { seasons . map ( s => (
281306 < option key = { s . name } value = { s . name } > { s . name } </ option >
@@ -288,7 +313,7 @@ export default function App() {
288313 < button className = "button secondary" onClick = { ( ) => void handleRefreshClick ( ) } disabled = { loading } >
289314 { loading ? "Refreshing..." : "Refresh Data" }
290315 </ button >
291- < p style = { { fontSize : "0.75rem" , color : "#9ca3af " , margin : "0" } } >
316+ < p style = { { fontSize : "0.75rem" , color : "var(--text-subtle) " , margin : "0" } } >
292317 Use the top right season selector to switch the active season.
293318 </ p >
294319 { scanState !== "idle" && (
@@ -309,7 +334,7 @@ export default function App() {
309334 </ div >
310335
311336 { error && (
312- < div className = "card" style = { { border : "1px solid #fecaca " , background : "#fef2f2 " } } >
337+ < div className = "card" style = { { border : "1px solid var(--error-card-border) " , background : "var(--error-card-bg)" , color : "var(--error-text) " } } >
313338 < strong > Heads up:</ strong > { error }
314339 </ div >
315340 ) }
@@ -361,6 +386,7 @@ export default function App() {
361386 sensors = { sensorsPreview }
362387 season = { selectedSeason }
363388 externalSelection = { downloaderSelection ?? undefined }
389+ theme = { theme }
364390 />
365391 </ section >
366392 </ div >
0 commit comments