"use client"; import { startTransition, useEffect, useMemo, useRef, useState } from "react"; import clsx from "clsx"; import { Clock } from "lucide-react"; import { useDashboardStore } from "@/hooks/useDashboardStore"; import { useI18n } from "@/hooks/useI18n"; import { CityListItem, DeviationMonitor } from "@/lib/dashboard-types"; type RiskGroupKey = "high" | "medium" | "low" | "other"; const GROUP_STATE_STORAGE_KEY = "polyWeather_sidebar_groups_v1"; const DEFAULT_EXPANDED_GROUPS: Record = { high: true, medium: true, low: false, other: false, }; function toRiskGroup(level?: string): RiskGroupKey { if (level === "high" || level === "medium" || level === "low") return level; return "other"; } function toPerformanceGroup(city: CityListItem): RiskGroupKey { return toRiskGroup(city.deb_recent_tier); } function normalizeExpandedGroups( value: unknown, ): Record { if (!value || typeof value !== "object") { return DEFAULT_EXPANDED_GROUPS; } const candidate = value as Partial>; return { high: typeof candidate.high === "boolean" ? candidate.high : DEFAULT_EXPANDED_GROUPS.high, medium: typeof candidate.medium === "boolean" ? candidate.medium : DEFAULT_EXPANDED_GROUPS.medium, low: typeof candidate.low === "boolean" ? candidate.low : DEFAULT_EXPANDED_GROUPS.low, other: typeof candidate.other === "boolean" ? candidate.other : DEFAULT_EXPANDED_GROUPS.other, }; } export function CitySidebar() { const store = useDashboardStore(); const { locale, t } = useI18n(); const selectedCity = store.selectedCity; const riskOrder = { high: 0, medium: 1, low: 2, other: 3 }; const [expandedGroups, setExpandedGroups] = useState< Record >(DEFAULT_EXPANDED_GROUPS); const cityItemRefs = useRef>({}); const sortedCities = useMemo( () => [...store.cities].sort((a, b) => { const aGroup = toPerformanceGroup(a); const bGroup = toPerformanceGroup(b); const aHitRate = Number(a.deb_recent_hit_rate ?? -1); const bHitRate = Number(b.deb_recent_hit_rate ?? -1); const aSamples = Number(a.deb_recent_sample_count ?? 0); const bSamples = Number(b.deb_recent_sample_count ?? 0); return ( (riskOrder[aGroup] ?? 3) - (riskOrder[bGroup] ?? 3) || bHitRate - aHitRate || bSamples - aSamples || a.display_name.localeCompare(b.display_name) ); }), [store.cities], ); const groupedCities = useMemo(() => { const groups: Record = { high: [], medium: [], low: [], other: [], }; sortedCities.forEach((city) => { groups[toPerformanceGroup(city)].push(city); }); return groups; }, [sortedCities]); useEffect(() => { if (!selectedCity) return; const selected = store.cities.find((city) => city.name === selectedCity); if (!selected) return; const groupKey = toPerformanceGroup(selected); setExpandedGroups((current) => current[groupKey] ? current : { ...current, [groupKey]: true }, ); }, [selectedCity, store.cities]); useEffect(() => { if (!selectedCity) return; const selected = store.cities.find((city) => city.name === selectedCity); if (!selected) return; const groupKey = toPerformanceGroup(selected); if (!expandedGroups[groupKey]) return; const frameId = window.requestAnimationFrame(() => { cityItemRefs.current[selectedCity]?.scrollIntoView({ block: "center", inline: "nearest", behavior: "smooth", }); }); return () => window.cancelAnimationFrame(frameId); }, [expandedGroups, selectedCity, store.cities]); useEffect(() => { if (typeof window === "undefined") return; const raw = window.localStorage.getItem(GROUP_STATE_STORAGE_KEY); if (!raw) return; try { const parsed = JSON.parse(raw); setExpandedGroups(normalizeExpandedGroups(parsed)); } catch {} }, []); useEffect(() => { if (typeof window === "undefined") return; try { window.localStorage.setItem( GROUP_STATE_STORAGE_KEY, JSON.stringify(expandedGroups), ); } catch {} }, [expandedGroups]); const formatDeviationText = (monitor?: DeviationMonitor | null) => { if (!monitor?.available) return ""; const label = locale === "en-US" ? monitor.label_en : monitor.label_zh; const trendLabel = locale === "en-US" ? monitor.trend_label_en : monitor.trend_label_zh; if (!label) return ""; return trendLabel ? `${label} · ${trendLabel}` : label; }; const groupMeta: Array<{ key: RiskGroupKey; label: string }> = [ { key: "high", label: t("sidebar.group.high") }, { key: "medium", label: t("sidebar.group.medium") }, { key: "low", label: t("sidebar.group.low") }, { key: "other", label: t("sidebar.group.other") }, ]; return ( ); }