From cf5502b6d620fb3fb4a17d13405db0f22669d856 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Tue, 28 Apr 2026 09:06:55 +0800 Subject: [PATCH] Reduce dashboard orchestration coupling ScanTerminalDashboard had accumulated terminal fetching, theme persistence, local clock updates, and AI pinned-city hydration in one component. Moving those responsibilities into focused hooks keeps the screen component as the composition layer while preserving the existing UI and data flow. Constraint: Refactor must not change the current decision-card or scan-terminal behavior. Rejected: Split visual card components in the same commit | too much surface area for one safe refactor pass. Confidence: high Scope-risk: moderate Reversibility: clean Tested: npm run build Not-tested: Browser manual regression across map, calendar, and pinned-card interactions. --- .../dashboard/ScanTerminalDashboard.tsx | 302 ++---------------- .../use-ai-pinned-city-workspace.ts | 150 +++++++++ .../scan-terminal/use-scan-terminal-query.ts | 138 ++++++++ .../use-scan-terminal-ui-state.ts | 49 +++ 4 files changed, 365 insertions(+), 274 deletions(-) create mode 100644 frontend/components/dashboard/scan-terminal/use-ai-pinned-city-workspace.ts create mode 100644 frontend/components/dashboard/scan-terminal/use-scan-terminal-query.ts create mode 100644 frontend/components/dashboard/scan-terminal/use-scan-terminal-ui-state.ts diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index be2036ac..9a3f2472 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -31,37 +31,27 @@ import { I18nProvider, useI18n } from "@/hooks/useI18n"; import type { CityDetail, ScanOpportunityRow, - ScanTerminalResponse, } from "@/lib/dashboard-types"; -import { - buildBrowserBackendHeaders, - fetchBackendApi, -} from "@/lib/backend-api"; -import { getLocalizedCityName } from "@/lib/dashboard-home-copy"; import { AiPinnedForecastView } from "@/components/dashboard/scan-terminal/AiPinnedForecastView"; import { CalendarView } from "@/components/dashboard/scan-terminal/CalendarView"; import { AiForecastKPIBar } from "@/components/dashboard/scan-terminal/AiForecastKPIBar"; import { LoadingSignal } from "@/components/dashboard/scan-terminal/LoadingSignal"; import { OpportunityOverview } from "@/components/dashboard/scan-terminal/OpportunityOverview"; -import { - findDetailForCity, - isFullEnoughForDeepAnalysis, - waitForDeepAnalysisQueue, -} from "@/components/dashboard/scan-terminal/city-detail-utils"; -import type { AiPinnedCity } from "@/components/dashboard/scan-terminal/types"; +import { findDetailForCity } from "@/components/dashboard/scan-terminal/city-detail-utils"; import { findRowForCity, - formatUserLocalTime, normalizeCityKey, - prettifyCityName, rowMatchesCity, sortRowsByUserTime, } from "@/components/dashboard/scan-terminal/decision-utils"; +import { useAiPinnedCityWorkspace } from "@/components/dashboard/scan-terminal/use-ai-pinned-city-workspace"; +import { useScanTerminalQuery } from "@/components/dashboard/scan-terminal/use-scan-terminal-query"; +import { + useScanTerminalTheme, + useUserLocalClock, +} from "@/components/dashboard/scan-terminal/use-scan-terminal-ui-state"; type ContentView = "opportunities" | "analysis" | "map" | "calendar"; -type ThemeMode = "dark" | "light"; -const SCAN_TERMINAL_AUTO_REFRESH_MS = 10 * 60_000; -const SCAN_TERMINAL_MANUAL_REFRESH_COOLDOWN_MS = 2 * 60_000; function ScanTerminalScreen() { const store = useDashboardStore(); @@ -71,40 +61,37 @@ function ScanTerminalScreen() { const accountHref = store.proAccess.authenticated ? "/account" : "/auth/login?next=%2Faccount"; - const [terminalData, setTerminalData] = useState(null); - const [scanLoading, setScanLoading] = useState(false); - const [scanError, setScanError] = useState(null); + const { + refreshScanTerminalManually, + scanError, + scanLoading, + terminalData, + } = useScanTerminalQuery({ + isPro, + proAccessLoading: store.proAccess.loading, + }); const [selectedRowId, setSelectedRowId] = useState(null); const [activeView, setActiveView] = useState("opportunities"); const [mapSelectedCityName, setMapSelectedCityName] = useState(null); - const [aiPinnedCities, setAiPinnedCities] = useState([]); const [showScanPaywall, setShowScanPaywall] = useState(false); - const [userLocalTime, setUserLocalTime] = useState("--"); - const [themeMode, setThemeMode] = useState("dark"); + const userLocalTime = useUserLocalClock(); + const { setThemeMode, themeMode } = useScanTerminalTheme(); const lastMapSelectedCityRef = useRef(""); - const aiFullHydrationRef = useRef>(new Set()); - const aiHydrationQueueRef = useRef([]); - const aiHydrationRunningRef = useRef(false); - const scanRequestSeqRef = useRef(0); - const scanLoadingRef = useRef(false); - const lastForcedScanRefreshAtRef = useRef(0); - - useEffect(() => { - const root = document.documentElement; - const hadLight = root.classList.contains("light"); - const hadDark = root.classList.contains("dark"); - root.classList.toggle("light", themeMode === "light"); - root.classList.toggle("dark", themeMode === "dark"); - return () => { - root.classList.toggle("light", hadLight); - root.classList.toggle("dark", hadDark); - }; - }, [themeMode]); const timeSortedRows = useMemo( () => sortRowsByUserTime(terminalData?.rows || []), [terminalData?.rows], ); + const { + addAiPinnedCity, + aiPinnedCities, + refreshAiPinnedCityDetail, + removeAiPinnedCity, + } = useAiPinnedCityWorkspace({ + locale, + store, + timeSortedRows, + }); const selectedRow = useMemo(() => { if (!timeSortedRows.length) return null; return timeSortedRows.find((row) => row.id === selectedRowId) || timeSortedRows[0] || null; @@ -229,130 +216,6 @@ function ScanTerminalScreen() { } }, [activeView, isPro, store.proAccess.loading]); - const fetchScanTerminal = useCallback( - async ({ - forceRefresh = false, - showLoading = false, - }: { - forceRefresh?: boolean; - showLoading?: boolean; - } = {}) => { - if (store.proAccess.loading || !isPro) return; - const requestSeq = ++scanRequestSeqRef.current; - const controller = new AbortController(); - if (forceRefresh) { - lastForcedScanRefreshAtRef.current = Date.now(); - } - if (showLoading) { - scanLoadingRef.current = true; - setScanLoading(true); - } - setScanError(null); - const params = new URLSearchParams({ - scan_mode: "tradable", - min_price: "0.05", - max_price: "0.95", - min_edge_pct: "2", - min_liquidity: "500", - market_type: "maxtemp", - time_range: "today", - limit: "36", - force_refresh: String(forceRefresh), - }); - if (forceRefresh) { - params.set("_ts", String(Date.now())); - } - try { - const headers = await buildBrowserBackendHeaders({ - Accept: "application/json", - }); - const response = await fetchBackendApi(`/api/scan/terminal?${params.toString()}`, { - cache: "no-store", - headers, - signal: controller.signal, - }); - if (!response.ok) { - let message = `HTTP ${response.status}`; - try { - const payload = await response.json(); - message = String(payload?.error || payload?.detail || message); - } catch { - // Keep HTTP status message. - } - throw new Error(message); - } - const payload = (await response.json()) as ScanTerminalResponse; - if (requestSeq !== scanRequestSeqRef.current) return; - setTerminalData(payload); - setScanError(null); - } catch (error) { - if (controller.signal.aborted || requestSeq !== scanRequestSeqRef.current) return; - setScanError(error instanceof Error ? error.message : String(error)); - } finally { - if (showLoading) { - scanLoadingRef.current = false; - setScanLoading(false); - } - } - }, - [isPro, store.proAccess.loading], - ); - - useEffect(() => { - if (store.proAccess.loading) return; - if (!isPro) { - scanLoadingRef.current = false; - setScanLoading(false); - setScanError(null); - setTerminalData(null); - return; - } - void fetchScanTerminal({ forceRefresh: false, showLoading: true }); - }, [fetchScanTerminal, isPro, store.proAccess.loading]); - - const refreshScanTerminalManually = useCallback(() => { - const now = Date.now(); - const lastForced = lastForcedScanRefreshAtRef.current; - const withinCooldown = - lastForced > 0 && - now - lastForced < SCAN_TERMINAL_MANUAL_REFRESH_COOLDOWN_MS && - terminalData; - if (withinCooldown) { - setScanError(null); - return; - } - void fetchScanTerminal({ forceRefresh: true, showLoading: true }); - }, [fetchScanTerminal, terminalData]); - - useEffect(() => { - if (store.proAccess.loading || !isPro) return; - const intervalId = window.setInterval(() => { - if (document.hidden) return; - if (scanLoadingRef.current) return; - void fetchScanTerminal({ forceRefresh: true, showLoading: false }); - }, SCAN_TERMINAL_AUTO_REFRESH_MS); - return () => window.clearInterval(intervalId); - }, [fetchScanTerminal, isPro, store.proAccess.loading]); - - useEffect(() => { - setUserLocalTime(formatUserLocalTime()); - const intervalId = window.setInterval(() => { - setUserLocalTime(formatUserLocalTime()); - }, 10_000); - return () => window.clearInterval(intervalId); - }, []); - - useEffect(() => { - const stored = window.localStorage.getItem("polyweather_scan_theme"); - if (stored === "light") { - setThemeMode("light"); - } - }, []); - - useEffect(() => { - window.localStorage.setItem("polyweather_scan_theme", themeMode); - }, [themeMode]); - const resolvedView: ContentView = activeView; const mapFocusedCity = mapSelectedCityName || store.selectedCity; const activeDetailRow = @@ -369,115 +232,6 @@ function ScanTerminalScreen() { } }, [activeDetailRow, store.cityDetailsByName, store.ensureCityDetail]); - const runAiHydrationQueue = useCallback(async () => { - if (aiHydrationRunningRef.current) return; - aiHydrationRunningRef.current = true; - try { - while (aiHydrationQueueRef.current.length > 0) { - const nextCity = aiHydrationQueueRef.current.shift(); - const key = normalizeCityKey(nextCity || ""); - if (!nextCity || !key) continue; - const existingDetail = findDetailForCity(store.cityDetailsByName, nextCity); - try { - const detail = await store.ensureCityDetail( - nextCity, - Boolean(existingDetail) && !isFullEnoughForDeepAnalysis(existingDetail), - "full", - ); - if (!isFullEnoughForDeepAnalysis(detail)) { - aiFullHydrationRef.current.delete(key); - } - } catch { - aiFullHydrationRef.current.delete(key); - } - await waitForDeepAnalysisQueue(1200); - } - } finally { - aiHydrationRunningRef.current = false; - if (aiHydrationQueueRef.current.length > 0) { - void runAiHydrationQueue(); - } - } - }, [store.cityDetailsByName, store.ensureCityDetail]); - - const queueAiFullHydration = useCallback( - (cityName: string) => { - const key = normalizeCityKey(cityName); - if (!key || aiFullHydrationRef.current.has(key)) return; - aiFullHydrationRef.current.add(key); - aiHydrationQueueRef.current.push(cityName); - void runAiHydrationQueue(); - }, - [runAiHydrationQueue], - ); - - const addAiPinnedCity = useCallback((cityName: string) => { - const cleanName = String(cityName || "").trim(); - const key = normalizeCityKey(cleanName); - if (!key) return; - const matchedRow = findRowForCity(timeSortedRows, cleanName); - const prettyName = prettifyCityName(cleanName); - const displayName = - matchedRow?.city_display_name || - matchedRow?.display_name || - getLocalizedCityName(cleanName, prettyName || cleanName, locale) || - prettyName || - cleanName; - setAiPinnedCities((current) => { - const existing = current.findIndex( - (item) => normalizeCityKey(item.cityName) === key, - ); - const nextItem = { - cityName: matchedRow?.city || cleanName, - displayName, - addedAt: Date.now(), - }; - if (existing >= 0) { - const next = [...current]; - next[existing] = { ...next[existing], ...nextItem }; - return [ - next[existing], - ...next.filter((_, index) => index !== existing), - ]; - } - return [nextItem, ...current].slice(0, 8); - }); - queueAiFullHydration(matchedRow?.city || cleanName); - }, [locale, queueAiFullHydration, timeSortedRows]); - - const removeAiPinnedCity = useCallback((cityName: string) => { - const key = normalizeCityKey(cityName); - aiFullHydrationRef.current.delete(key); - aiHydrationQueueRef.current = aiHydrationQueueRef.current.filter( - (queuedCity) => normalizeCityKey(queuedCity) !== key, - ); - setAiPinnedCities((current) => - current.filter((item) => normalizeCityKey(item.cityName) !== key), - ); - }, []); - - const refreshAiPinnedCityDetail = useCallback( - async (cityName: string) => { - const key = normalizeCityKey(cityName); - if (key) { - aiFullHydrationRef.current.delete(key); - } - await store.ensureCityDetail(cityName, true, "full"); - }, - [store.ensureCityDetail], - ); - - useEffect(() => { - aiPinnedCities.forEach((item) => { - const key = normalizeCityKey(item.cityName); - if (!key || aiFullHydrationRef.current.has(key)) return; - const detail = findDetailForCity(store.cityDetailsByName, item.cityName); - const needsFullHydration = !isFullEnoughForDeepAnalysis(detail); - if (!needsFullHydration) return; - queueAiFullHydration(item.cityName); - }); - }, [aiPinnedCities, queueAiFullHydration, store.cityDetailsByName]); - const handleMapCitySelect = useCallback((cityName: string) => { setMapSelectedCityName(cityName); lastMapSelectedCityRef.current = normalizeCityKey(cityName); diff --git a/frontend/components/dashboard/scan-terminal/use-ai-pinned-city-workspace.ts b/frontend/components/dashboard/scan-terminal/use-ai-pinned-city-workspace.ts new file mode 100644 index 00000000..8cbd9c07 --- /dev/null +++ b/frontend/components/dashboard/scan-terminal/use-ai-pinned-city-workspace.ts @@ -0,0 +1,150 @@ +"use client"; + +import { useCallback, useEffect, useRef, useState } from "react"; +import { getLocalizedCityName } from "@/lib/dashboard-home-copy"; +import type { ScanOpportunityRow } from "@/lib/dashboard-types"; +import type { useDashboardStore } from "@/hooks/useDashboardStore"; +import { + findDetailForCity, + isFullEnoughForDeepAnalysis, + waitForDeepAnalysisQueue, +} from "@/components/dashboard/scan-terminal/city-detail-utils"; +import { + findRowForCity, + normalizeCityKey, + prettifyCityName, +} from "@/components/dashboard/scan-terminal/decision-utils"; +import type { AiPinnedCity } from "@/components/dashboard/scan-terminal/types"; + +type DashboardStore = ReturnType; + +export function useAiPinnedCityWorkspace({ + locale, + store, + timeSortedRows, +}: { + locale: string; + store: DashboardStore; + timeSortedRows: ScanOpportunityRow[]; +}) { + const [aiPinnedCities, setAiPinnedCities] = useState([]); + const aiFullHydrationRef = useRef>(new Set()); + const aiHydrationQueueRef = useRef([]); + const aiHydrationRunningRef = useRef(false); + + const runAiHydrationQueue = useCallback(async () => { + if (aiHydrationRunningRef.current) return; + aiHydrationRunningRef.current = true; + try { + while (aiHydrationQueueRef.current.length > 0) { + const nextCity = aiHydrationQueueRef.current.shift(); + const key = normalizeCityKey(nextCity || ""); + if (!nextCity || !key) continue; + const existingDetail = findDetailForCity(store.cityDetailsByName, nextCity); + try { + const detail = await store.ensureCityDetail( + nextCity, + Boolean(existingDetail) && !isFullEnoughForDeepAnalysis(existingDetail), + "full", + ); + if (!isFullEnoughForDeepAnalysis(detail)) { + aiFullHydrationRef.current.delete(key); + } + } catch { + aiFullHydrationRef.current.delete(key); + } + await waitForDeepAnalysisQueue(1200); + } + } finally { + aiHydrationRunningRef.current = false; + if (aiHydrationQueueRef.current.length > 0) { + void runAiHydrationQueue(); + } + } + }, [store.cityDetailsByName, store.ensureCityDetail]); + + const queueAiFullHydration = useCallback( + (cityName: string) => { + const key = normalizeCityKey(cityName); + if (!key || aiFullHydrationRef.current.has(key)) return; + aiFullHydrationRef.current.add(key); + aiHydrationQueueRef.current.push(cityName); + void runAiHydrationQueue(); + }, + [runAiHydrationQueue], + ); + + const addAiPinnedCity = useCallback((cityName: string) => { + const cleanName = String(cityName || "").trim(); + const key = normalizeCityKey(cleanName); + if (!key) return; + const matchedRow = findRowForCity(timeSortedRows, cleanName); + const prettyName = prettifyCityName(cleanName); + const displayName = + matchedRow?.city_display_name || + matchedRow?.display_name || + getLocalizedCityName(cleanName, prettyName || cleanName, locale) || + prettyName || + cleanName; + setAiPinnedCities((current) => { + const existing = current.findIndex( + (item) => normalizeCityKey(item.cityName) === key, + ); + const nextItem = { + cityName: matchedRow?.city || cleanName, + displayName, + addedAt: Date.now(), + }; + if (existing >= 0) { + const next = [...current]; + next[existing] = { ...next[existing], ...nextItem }; + return [ + next[existing], + ...next.filter((_, index) => index !== existing), + ]; + } + return [nextItem, ...current].slice(0, 8); + }); + queueAiFullHydration(matchedRow?.city || cleanName); + }, [locale, queueAiFullHydration, timeSortedRows]); + + const removeAiPinnedCity = useCallback((cityName: string) => { + const key = normalizeCityKey(cityName); + aiFullHydrationRef.current.delete(key); + aiHydrationQueueRef.current = aiHydrationQueueRef.current.filter( + (queuedCity) => normalizeCityKey(queuedCity) !== key, + ); + setAiPinnedCities((current) => + current.filter((item) => normalizeCityKey(item.cityName) !== key), + ); + }, []); + + const refreshAiPinnedCityDetail = useCallback( + async (cityName: string) => { + const key = normalizeCityKey(cityName); + if (key) { + aiFullHydrationRef.current.delete(key); + } + await store.ensureCityDetail(cityName, true, "full"); + }, + [store.ensureCityDetail], + ); + + useEffect(() => { + aiPinnedCities.forEach((item) => { + const key = normalizeCityKey(item.cityName); + if (!key || aiFullHydrationRef.current.has(key)) return; + const detail = findDetailForCity(store.cityDetailsByName, item.cityName); + const needsFullHydration = !isFullEnoughForDeepAnalysis(detail); + if (!needsFullHydration) return; + queueAiFullHydration(item.cityName); + }); + }, [aiPinnedCities, queueAiFullHydration, store.cityDetailsByName]); + + return { + addAiPinnedCity, + aiPinnedCities, + refreshAiPinnedCityDetail, + removeAiPinnedCity, + }; +} diff --git a/frontend/components/dashboard/scan-terminal/use-scan-terminal-query.ts b/frontend/components/dashboard/scan-terminal/use-scan-terminal-query.ts new file mode 100644 index 00000000..087e0200 --- /dev/null +++ b/frontend/components/dashboard/scan-terminal/use-scan-terminal-query.ts @@ -0,0 +1,138 @@ +"use client"; + +import { useCallback, useEffect, useRef, useState } from "react"; +import { + buildBrowserBackendHeaders, + fetchBackendApi, +} from "@/lib/backend-api"; +import type { ScanTerminalResponse } from "@/lib/dashboard-types"; + +const SCAN_TERMINAL_AUTO_REFRESH_MS = 10 * 60_000; +const SCAN_TERMINAL_MANUAL_REFRESH_COOLDOWN_MS = 2 * 60_000; + +export function useScanTerminalQuery({ + isPro, + proAccessLoading, +}: { + isPro: boolean; + proAccessLoading: boolean; +}) { + const [terminalData, setTerminalData] = useState(null); + const [scanLoading, setScanLoading] = useState(false); + const [scanError, setScanError] = useState(null); + const scanRequestSeqRef = useRef(0); + const scanLoadingRef = useRef(false); + const lastForcedScanRefreshAtRef = useRef(0); + + const fetchScanTerminal = useCallback( + async ({ + forceRefresh = false, + showLoading = false, + }: { + forceRefresh?: boolean; + showLoading?: boolean; + } = {}) => { + if (proAccessLoading || !isPro) return; + const requestSeq = ++scanRequestSeqRef.current; + const controller = new AbortController(); + if (forceRefresh) { + lastForcedScanRefreshAtRef.current = Date.now(); + } + if (showLoading) { + scanLoadingRef.current = true; + setScanLoading(true); + } + setScanError(null); + const params = new URLSearchParams({ + scan_mode: "tradable", + min_price: "0.05", + max_price: "0.95", + min_edge_pct: "2", + min_liquidity: "500", + market_type: "maxtemp", + time_range: "today", + limit: "36", + force_refresh: String(forceRefresh), + }); + if (forceRefresh) { + params.set("_ts", String(Date.now())); + } + try { + const headers = await buildBrowserBackendHeaders({ + Accept: "application/json", + }); + const response = await fetchBackendApi(`/api/scan/terminal?${params.toString()}`, { + cache: "no-store", + headers, + signal: controller.signal, + }); + if (!response.ok) { + let message = `HTTP ${response.status}`; + try { + const payload = await response.json(); + message = String(payload?.error || payload?.detail || message); + } catch { + // Keep HTTP status message. + } + throw new Error(message); + } + const payload = (await response.json()) as ScanTerminalResponse; + if (requestSeq !== scanRequestSeqRef.current) return; + setTerminalData(payload); + setScanError(null); + } catch (error) { + if (controller.signal.aborted || requestSeq !== scanRequestSeqRef.current) return; + setScanError(error instanceof Error ? error.message : String(error)); + } finally { + if (showLoading) { + scanLoadingRef.current = false; + setScanLoading(false); + } + } + }, + [isPro, proAccessLoading], + ); + + useEffect(() => { + if (proAccessLoading) return; + if (!isPro) { + scanLoadingRef.current = false; + setScanLoading(false); + setScanError(null); + setTerminalData(null); + return; + } + void fetchScanTerminal({ forceRefresh: false, showLoading: true }); + }, [fetchScanTerminal, isPro, proAccessLoading]); + + const refreshScanTerminalManually = useCallback(() => { + const now = Date.now(); + const lastForced = lastForcedScanRefreshAtRef.current; + const withinCooldown = + lastForced > 0 && + now - lastForced < SCAN_TERMINAL_MANUAL_REFRESH_COOLDOWN_MS && + terminalData; + if (withinCooldown) { + setScanError(null); + return; + } + void fetchScanTerminal({ forceRefresh: true, showLoading: true }); + }, [fetchScanTerminal, terminalData]); + + useEffect(() => { + if (proAccessLoading || !isPro) return; + const intervalId = window.setInterval(() => { + if (document.hidden) return; + if (scanLoadingRef.current) return; + void fetchScanTerminal({ forceRefresh: true, showLoading: false }); + }, SCAN_TERMINAL_AUTO_REFRESH_MS); + return () => window.clearInterval(intervalId); + }, [fetchScanTerminal, isPro, proAccessLoading]); + + return { + refreshScanTerminalManually, + scanError, + scanLoading, + terminalData, + }; +} diff --git a/frontend/components/dashboard/scan-terminal/use-scan-terminal-ui-state.ts b/frontend/components/dashboard/scan-terminal/use-scan-terminal-ui-state.ts new file mode 100644 index 00000000..a41b0b8a --- /dev/null +++ b/frontend/components/dashboard/scan-terminal/use-scan-terminal-ui-state.ts @@ -0,0 +1,49 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { formatUserLocalTime } from "@/components/dashboard/scan-terminal/decision-utils"; + +export type ThemeMode = "dark" | "light"; + +export function useUserLocalClock() { + const [userLocalTime, setUserLocalTime] = useState("--"); + + useEffect(() => { + setUserLocalTime(formatUserLocalTime()); + const intervalId = window.setInterval(() => { + setUserLocalTime(formatUserLocalTime()); + }, 10_000); + return () => window.clearInterval(intervalId); + }, []); + + return userLocalTime; +} + +export function useScanTerminalTheme() { + const [themeMode, setThemeMode] = useState("dark"); + + useEffect(() => { + const root = document.documentElement; + const hadLight = root.classList.contains("light"); + const hadDark = root.classList.contains("dark"); + root.classList.toggle("light", themeMode === "light"); + root.classList.toggle("dark", themeMode === "dark"); + return () => { + root.classList.toggle("light", hadLight); + root.classList.toggle("dark", hadDark); + }; + }, [themeMode]); + + useEffect(() => { + const stored = window.localStorage.getItem("polyweather_scan_theme"); + if (stored === "light") { + setThemeMode("light"); + } + }, []); + + useEffect(() => { + window.localStorage.setItem("polyweather_scan_theme", themeMode); + }, [themeMode]); + + return { setThemeMode, themeMode }; +}