Rework homepage toward reference dashboard

This commit is contained in:
2569718930@qq.com
2026-04-23 02:17:31 +08:00
parent 040313175f
commit 2589e11415
4 changed files with 487 additions and 150 deletions
+41 -2
View File
@@ -2,7 +2,7 @@
import { startTransition, useEffect, useMemo, useRef, useState } from "react";
import clsx from "clsx";
import { Clock } from "lucide-react";
import { Clock, Search } from "lucide-react";
import { useDashboardStore } from "@/hooks/useDashboardStore";
import { useI18n } from "@/hooks/useI18n";
import { getLocalizedCityName } from "@/lib/dashboard-home-copy";
@@ -62,7 +62,9 @@ export function CitySidebar() {
const [expandedGroups, setExpandedGroups] = useState<
Record<RiskGroupKey, boolean>
>(DEFAULT_EXPANDED_GROUPS);
const [searchQuery, setSearchQuery] = useState("");
const cityItemRefs = useRef<Record<string, HTMLButtonElement | null>>({});
const normalizedQuery = searchQuery.trim().toLowerCase();
const sortedCities = useMemo(
() =>
@@ -91,10 +93,36 @@ export function CitySidebar() {
other: [],
};
sortedCities.forEach((city) => {
const summary = store.citySummariesByName[city.name];
const detail = store.cityDetailsByName[city.name];
const localizedName = getLocalizedCityName(
city.name,
summary?.display_name || detail?.display_name || city.display_name,
locale,
);
if (normalizedQuery) {
const searchCorpus = [
city.name,
city.display_name,
city.airport,
city.icao,
localizedName,
]
.filter(Boolean)
.join(" ")
.toLowerCase();
if (!searchCorpus.includes(normalizedQuery)) return;
}
groups[toPerformanceGroup(city)].push(city);
});
return groups;
}, [sortedCities]);
}, [
locale,
normalizedQuery,
sortedCities,
store.cityDetailsByName,
store.citySummariesByName,
]);
useEffect(() => {
if (!selectedCity) return;
@@ -166,6 +194,17 @@ export function CitySidebar() {
<span className="city-count">{store.cities.length}</span>
</div>
<div className="city-search">
<Search size={14} strokeWidth={2} aria-hidden="true" />
<input
type="search"
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
placeholder={locale === "en-US" ? "Search city" : "搜索城市"}
aria-label={locale === "en-US" ? "Search city" : "搜索城市"}
/>
</div>
<div className="city-list-items">
{groupMeta.map((group) => {
const citiesInGroup = groupedCities[group.key];
@@ -32,8 +32,8 @@
/* Spacing — aligned with global layout tokens */
--panel-width: 560px;
--header-height: 52px;
--sidebar-width: 260px;
--header-height: 72px;
--sidebar-width: 240px;
/* Effects — using global elevation tokens */
--glass-blur: 20px;
@@ -69,9 +69,9 @@
.root :global(.map) {
position: absolute;
top: calc(var(--header-height) + 16px);
right: 418px;
bottom: 128px;
left: calc(var(--sidebar-width) + 30px);
right: 392px;
bottom: 164px;
left: calc(var(--sidebar-width) + 22px);
z-index: 1;
overflow: hidden;
border: 1px solid rgba(34, 211, 238, 0.12);
@@ -82,6 +82,71 @@
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.root :global(.home-map-toolbar) {
position: fixed;
top: calc(var(--header-height) + 30px);
right: 412px;
z-index: 930;
display: flex;
align-items: center;
gap: 10px;
}
.root :global(.home-map-modes) {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px;
border-radius: 14px;
border: 1px solid rgba(148, 163, 184, 0.16);
background: rgba(9, 16, 28, 0.84);
backdrop-filter: blur(16px);
}
.root :global(.home-map-modes button),
.root :global(.home-map-expand) {
border: none;
color: rgba(203, 213, 225, 0.88);
background: transparent;
cursor: pointer;
font: inherit;
transition: var(--transition);
}
.root :global(.home-map-modes button) {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 10px 14px;
border-radius: 10px;
font-size: 12px;
font-weight: 700;
}
.root :global(.home-map-modes button.active) {
background: linear-gradient(180deg, rgba(28, 100, 242, 0.76), rgba(31, 72, 184, 0.76));
color: #f8fafc;
box-shadow: 0 10px 24px rgba(37, 99, 235, 0.24);
}
.root :global(.home-map-modes button:hover),
.root :global(.home-map-expand:hover) {
color: #f8fafc;
background: rgba(30, 41, 59, 0.72);
}
.root :global(.home-map-expand) {
width: 38px;
height: 38px;
border-radius: 12px;
border: 1px solid rgba(148, 163, 184, 0.16);
background: rgba(9, 16, 28, 0.84);
backdrop-filter: blur(16px);
display: inline-flex;
align-items: center;
justify-content: center;
}
.weatherAura {
position: absolute;
inset: 0;
@@ -181,12 +246,9 @@
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
background: linear-gradient(
180deg,
rgba(8, 12, 20, 0.96) 0%,
rgba(8, 12, 20, 0.82) 100%
);
gap: 24px;
padding: 0 16px;
background: rgba(7, 11, 18, 0.94);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid var(--border-glass);
@@ -194,8 +256,9 @@
.root :global(.brand) {
display: flex;
align-items: baseline;
gap: 10px;
align-items: center;
gap: 14px;
min-width: 0;
}
.root :global(.brand h1) {
@@ -212,15 +275,48 @@
font-size: 11px;
font-weight: 500;
color: var(--text-muted);
letter-spacing: 0.6px;
text-transform: uppercase;
letter-spacing: 0.04em;
opacity: 0.8;
}
.root :global(.header-nav) {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
flex: 1 1 auto;
min-width: 0;
}
.root :global(.header-nav-link) {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 10px 12px;
border-radius: 10px;
color: rgba(203, 213, 225, 0.82);
text-decoration: none;
font-size: 13px;
font-weight: 650;
transition: var(--transition);
}
.root :global(.header-nav-link:hover) {
color: #f8fafc;
background: rgba(30, 41, 59, 0.44);
}
.root :global(.header-nav-link.active) {
color: #f8fafc;
background: rgba(8, 145, 178, 0.12);
box-shadow: inset 0 -2px 0 var(--accent-cyan);
}
.root :global(.header-right) {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.root :global(.lang-switch) {
@@ -344,7 +440,7 @@
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
padding: 14px 16px 10px;
border-bottom: 1px solid var(--border-subtle);
font-size: 13px;
font-weight: 700;
@@ -352,6 +448,32 @@
letter-spacing: -0.01em;
}
.root :global(.city-search) {
display: flex;
align-items: center;
gap: 8px;
margin: 0 14px 12px;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid rgba(148, 163, 184, 0.12);
background: rgba(8, 15, 28, 0.72);
color: rgba(148, 163, 184, 0.86);
}
.root :global(.city-search input) {
width: 100%;
border: none;
outline: none;
background: transparent;
color: #f8fafc;
font: inherit;
font-size: 13px;
}
.root :global(.city-search input::placeholder) {
color: rgba(148, 163, 184, 0.72);
}
.root :global(.city-count) {
background: rgba(99, 102, 241, 0.2);
color: var(--accent-blue);
@@ -366,7 +488,7 @@
.root :global(.city-list-items) {
overflow-y: auto;
flex: 1;
padding: 6px 6px 8px;
padding: 0 6px 8px;
}
.root :global(.city-list-items::-webkit-scrollbar) {
width: 4px;
@@ -599,10 +721,10 @@
/* ── Homepage Intelligence Layer ── */
.root :global(.home-intelligence-panel) {
position: fixed;
top: calc(var(--header-height) + 18px);
top: calc(var(--header-height) + 16px);
right: 18px;
bottom: 132px;
width: min(382px, calc(100vw - var(--sidebar-width) - 64px));
bottom: 20px;
width: min(360px, calc(100vw - var(--sidebar-width) - 56px));
z-index: 890;
display: flex;
flex-direction: column;
@@ -875,7 +997,7 @@
.root :global(.home-intelligence-panel.full) {
bottom: 18px;
width: min(372px, calc(100vw - var(--sidebar-width) - 64px));
width: min(360px, calc(100vw - var(--sidebar-width) - 56px));
gap: 12px;
overflow-y: auto;
border-color: rgba(34, 211, 238, 0.34);
@@ -948,10 +1070,10 @@
.root :global(.home-card-titlebar h2) {
color: #f8fafc;
font-size: 28px;
font-size: 22px;
font-weight: 900;
line-height: 1;
letter-spacing: -0.055em;
letter-spacing: -0.045em;
}
.root :global(.home-card-titlebar p),
@@ -1615,14 +1737,13 @@
.root :global(.home-opportunity-strip) {
position: fixed;
left: calc(var(--sidebar-width) + 30px);
right: 418px;
left: calc(var(--sidebar-width) + 22px);
right: 392px;
bottom: 18px;
z-index: 880;
display: grid;
grid-template-columns: 170px 1fr;
gap: 14px;
align-items: stretch;
display: flex;
flex-direction: column;
gap: 12px;
padding: 12px;
border: 1px solid rgba(148, 163, 184, 0.14);
border-radius: 22px;
@@ -1634,12 +1755,53 @@
animation: opportunity-strip-enter 580ms cubic-bezier(0.16, 1, 0.3, 1) 120ms both;
}
.root :global(.opportunity-strip-heading) {
.root :global(.home-summary-grid) {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
.root :global(.home-summary-card) {
min-height: 92px;
display: flex;
flex-direction: column;
justify-content: center;
gap: 7px;
padding: 4px 8px;
justify-content: space-between;
padding: 14px 16px;
border-radius: 16px;
border: 1px solid rgba(148, 163, 184, 0.14);
background: rgba(8, 15, 28, 0.7);
}
.root :global(.home-summary-card span) {
color: rgba(148, 163, 184, 0.88);
font-size: 12px;
font-weight: 650;
}
.root :global(.home-summary-card strong) {
color: #f8fafc;
font-size: 28px;
font-weight: 850;
letter-spacing: -0.04em;
}
.root :global(.home-summary-card.accent-red strong) { color: #fb7185; }
.root :global(.home-summary-card.accent-amber strong) { color: #fbbf24; }
.root :global(.home-summary-card.accent-green strong) { color: #4ade80; }
.root :global(.home-summary-card.accent-cyan strong) { color: #22d3ee; }
.root :global(.opportunity-strip-heading) {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 2px 4px;
}
.root :global(.opportunity-strip-heading > div) {
display: flex;
flex-direction: column;
gap: 5px;
}
.root :global(.opportunity-strip-heading strong) {
@@ -1648,28 +1810,36 @@
line-height: 1.25;
}
.root :global(.opportunity-view-all) {
color: #60a5fa;
text-decoration: none;
font-size: 12px;
font-weight: 700;
}
.root :global(.opportunity-card-grid) {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
.root :global(.opportunity-card-grid.top-opportunities) {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.root :global(.opportunity-card) {
position: relative;
min-width: 0;
display: grid;
grid-template-columns: 10px 1fr;
grid-template-areas:
"dot city"
"dot meta"
"dot hit";
gap: 3px 9px;
align-content: center;
min-height: 76px;
padding: 12px;
border: 1px solid rgba(148, 163, 184, 0.12);
display: flex;
flex-direction: column;
gap: 12px;
min-height: 122px;
padding: 14px;
border: 1px solid rgba(148, 163, 184, 0.14);
border-radius: 16px;
background: rgba(15, 23, 42, 0.5);
background:
radial-gradient(circle at 100% 0%, rgba(30, 64, 175, 0.12), transparent 32%),
rgba(10, 16, 28, 0.84);
color: inherit;
cursor: pointer;
font: inherit;
@@ -1680,32 +1850,59 @@
.root :global(.opportunity-card:hover) {
transform: translateY(-2px);
border-color: rgba(34, 211, 238, 0.32);
background: rgba(15, 23, 42, 0.72);
background:
radial-gradient(circle at 100% 0%, rgba(30, 64, 175, 0.18), transparent 32%),
rgba(15, 23, 42, 0.92);
}
.root :global(.opportunity-risk-dot) {
grid-area: dot;
align-self: center;
width: 8px;
height: 42px;
.root :global(.opportunity-card-header),
.root :global(.opportunity-card-footer) {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.root :global(.opportunity-rank) {
width: 24px;
height: 24px;
border-radius: 999px;
background: rgba(148, 163, 184, 0.55);
border: 1px solid rgba(148, 163, 184, 0.24);
display: inline-flex;
align-items: center;
justify-content: center;
color: #f8fafc;
font-size: 12px;
font-weight: 800;
flex-shrink: 0;
}
.root :global(.opportunity-risk-dot.high) {
background: linear-gradient(180deg, #fb7185, #f97316);
.root :global(.opportunity-pill) {
margin-left: auto;
padding: 4px 8px;
border-radius: 999px;
font-size: 10px;
font-weight: 800;
border: 1px solid rgba(148, 163, 184, 0.18);
color: rgba(226, 232, 240, 0.86);
}
.root :global(.opportunity-risk-dot.medium) {
background: linear-gradient(180deg, #facc15, #f97316);
.root :global(.opportunity-pill.high) {
color: #fda4af;
border-color: rgba(251, 113, 133, 0.26);
}
.root :global(.opportunity-risk-dot.low) {
background: linear-gradient(180deg, #34d399, #22d3ee);
.root :global(.opportunity-pill.medium) {
color: #fbbf24;
border-color: rgba(251, 191, 36, 0.24);
}
.root :global(.opportunity-pill.low) {
color: #4ade80;
border-color: rgba(74, 222, 128, 0.24);
}
.root :global(.opportunity-city) {
grid-area: city;
overflow: hidden;
color: #f8fafc;
font-size: 13px;
@@ -1715,7 +1912,6 @@
}
.root :global(.opportunity-meta) {
grid-area: meta;
overflow: hidden;
color: rgba(203, 213, 225, 0.88);
font-size: 11px;
@@ -1723,10 +1919,15 @@
white-space: nowrap;
}
.root :global(.opportunity-hit) {
grid-area: hit;
.root :global(.opportunity-yes) {
color: #22d3ee;
font-size: 12px;
font-weight: 800;
}
.root :global(.opportunity-edge) {
color: rgba(34, 211, 238, 0.9);
font-size: 11px;
font-size: 18px;
font-weight: 800;
}
+54 -12
View File
@@ -10,6 +10,9 @@ import {
BookOpen,
Sparkles,
House,
Bell,
BarChart3,
CandlestickChart,
} from "lucide-react";
import { useDashboardStore } from "@/hooks/useDashboardStore";
import { useI18n } from "@/hooks/useI18n";
@@ -41,8 +44,38 @@ export function HeaderBar({
const isAuthenticated = store.proAccess.authenticated;
const docsHref = "/docs/intro";
const docsActive = pathname?.startsWith("/docs");
const homeHref = "/";
const homeActive = pathname === "/";
const navItems = [
{
href: "/",
label: locale === "en-US" ? "Dashboard" : "总览",
active: pathname === "/",
icon: House,
},
{
href: "/docs/intraday-signal",
label: locale === "en-US" ? "Markets" : "市场",
active: pathname?.startsWith("/docs/intraday-signal"),
icon: CandlestickChart,
},
{
href: "/docs/model-stack-deb",
label: locale === "en-US" ? "Analytics" : "分析",
active: pathname?.startsWith("/docs/model-stack-deb"),
icon: BarChart3,
},
{
href: "/docs/history-review",
label: locale === "en-US" ? "History" : "历史",
active: pathname?.startsWith("/docs/history-review"),
icon: BookOpen,
},
{
href: "/docs/alert-playbook",
label: locale === "en-US" ? "Alerts" : "提醒",
active: pathname?.startsWith("/docs/alert-playbook"),
icon: Bell,
},
];
const trialPromoLabel =
locale === "en-US"
? "New users get 3-day Pro trial"
@@ -109,6 +142,25 @@ export function HeaderBar({
<span className="subtitle">{t("header.subtitle")}</span>
</div>
<nav
className="header-nav"
aria-label={locale === "en-US" ? "Primary" : "主导航"}
>
{navItems.map((item) => {
const Icon = item.icon;
return (
<Link
key={item.href}
href={item.href}
className={clsx("header-nav-link", item.active && "active")}
>
<Icon size={14} strokeWidth={2} />
<span>{item.label}</span>
</Link>
);
})}
</nav>
<div className="header-right">
<div
className="lang-switch"
@@ -131,16 +183,6 @@ export function HeaderBar({
</button>
</div>
<Link
href={homeHref}
className={clsx("info-btn", homeActive && "active")}
title={t("header.homeAria")}
aria-label={t("header.homeAria")}
>
<House size={14} strokeWidth={2} />
{t("header.home")}
</Link>
<Link
href={docsHref}
className={clsx("info-btn", docsActive && "active")}
@@ -28,6 +28,7 @@ import {
getWeatherSummary,
} from "@/lib/dashboard-utils";
import { normalizeObservationSourceLabel } from "@/lib/source-labels";
import { Expand, Thermometer, CloudRain, Wind } from "lucide-react";
const loadHistoryModal = () =>
import("@/components/dashboard/HistoryModal").then(
@@ -237,7 +238,88 @@ type HomeForecastDay = {
maxTemp: number;
};
const RECENT_OPENED_CITIES_STORAGE_KEY = "polyWeather_recent_opened_cities_v1";
type HomeSummaryMetric = {
key: string;
label: string;
value: string;
accent?: "green" | "amber" | "red" | "cyan";
};
function buildDashboardSummaryMetrics(
snapshots: CitySnapshot[],
locale: string,
): HomeSummaryMetric[] {
const topTradable = snapshots.filter((snapshot) => snapshot.tradableOpportunity);
const highCount = snapshots.filter((snapshot) => snapshot.city.deb_recent_tier === "high").length;
const mediumCount = snapshots.filter((snapshot) => snapshot.city.deb_recent_tier === "medium").length;
const lowCount = snapshots.filter((snapshot) => snapshot.city.deb_recent_tier === "low").length;
const avgEdge = topTradable.length
? topTradable.reduce((sum, snapshot) => {
const edgeValue = Number(getMarketEdgeValue(snapshot.detail));
const normalized =
Number.isFinite(edgeValue) && Math.abs(edgeValue) <= 1
? edgeValue * 100
: edgeValue;
return sum + (Number.isFinite(normalized) ? normalized : 0);
}, 0) / topTradable.length
: 0;
return [
{
key: "high",
label: locale === "en-US" ? "High risk" : "高风险",
value: String(highCount),
accent: "red",
},
{
key: "medium",
label: locale === "en-US" ? "Medium risk" : "中风险",
value: String(mediumCount),
accent: "amber",
},
{
key: "low",
label: locale === "en-US" ? "Low risk" : "低风险",
value: String(lowCount),
accent: "green",
},
{
key: "edge",
label: locale === "en-US" ? "Avg. edge" : "平均优势",
value: formatEdge(avgEdge),
accent: "cyan",
},
];
}
function HomeMapToolbar() {
const { locale } = useI18n();
return (
<div className="home-map-toolbar" aria-label={locale === "en-US" ? "Map layer controls" : "地图图层控件"}>
<div className="home-map-modes">
<button type="button" className="active">
<Thermometer size={14} strokeWidth={2} />
<span>{locale === "en-US" ? "Temperature" : "温度"}</span>
</button>
<button type="button">
<CloudRain size={14} strokeWidth={2} />
<span>{locale === "en-US" ? "Precipitation" : "降水"}</span>
</button>
<button type="button">
<Wind size={14} strokeWidth={2} />
<span>{locale === "en-US" ? "Wind" : "风场"}</span>
</button>
</div>
<button
type="button"
className="home-map-expand"
aria-label={locale === "en-US" ? "Expand map" : "放大地图"}
>
<Expand size={15} strokeWidth={2} />
</button>
</div>
);
}
function projectHomeTrendPoint(
x: number,
@@ -965,24 +1047,18 @@ function HomeIntelligencePanel({ snapshots }: { snapshots: CitySnapshot[] }) {
function OpportunityStrip({
snapshots,
recentCityNames,
}: {
snapshots: CitySnapshot[];
recentCityNames: string[];
}) {
const store = useDashboardStore();
const { locale } = useI18n();
const snapshotByName = useMemo(
() => new Map(snapshots.map((snapshot) => [snapshot.city.name, snapshot])),
[snapshots],
const store = useDashboardStore();
const summaryMetrics = useMemo(
() => buildDashboardSummaryMetrics(snapshots, locale),
[locale, snapshots],
);
const items = useMemo(
() =>
recentCityNames
.map((cityName) => snapshotByName.get(cityName) || null)
.filter((snapshot): snapshot is CitySnapshot => snapshot != null)
.slice(0, 4),
[recentCityNames, snapshotByName],
() => snapshots.filter((snapshot) => snapshot.tradableOpportunity).slice(0, 5),
[snapshots],
);
if (!items.length) return null;
@@ -992,14 +1068,28 @@ function OpportunityStrip({
className="home-opportunity-strip"
aria-label={locale === "en-US" ? "Opportunity strip" : "机会条"}
>
<div className="opportunity-strip-heading">
<span>{locale === "en-US" ? "Recently opened" : "最近打开"}</span>
<strong>
{locale === "en-US" ? "Recently opened cities" : "最近打开的城市"}
</strong>
<div className="home-summary-grid">
{summaryMetrics.map((metric) => (
<div key={metric.key} className={clsx("home-summary-card", metric.accent && `accent-${metric.accent}`)}>
<span>{metric.label}</span>
<strong>{metric.value}</strong>
</div>
))}
</div>
<div className="opportunity-card-grid">
{items.map(({ city, detail, summary }) => {
<div className="opportunity-strip-heading">
<div>
<span>{locale === "en-US" ? "Today's Top Opportunities" : "今日最佳机会"}</span>
<strong>
{locale === "en-US" ? "Sorted by edge and tradability" : "按优势与可交易性排序"}
</strong>
</div>
<Link href="/docs/intraday-signal" className="opportunity-view-all">
{locale === "en-US" ? "View all" : "查看全部"}
</Link>
</div>
<div className="opportunity-card-grid top-opportunities">
{items.map(({ city, detail, summary }, index) => {
const symbol = getTempSymbol(city, summary, detail);
const currentTemp = summary?.current?.temp ?? detail?.current?.temp;
const debPrediction = summary?.deb?.prediction ?? detail?.deb?.prediction;
@@ -1016,16 +1106,21 @@ function OpportunityStrip({
className="opportunity-card"
onClick={() => void store.focusCity(city.name)}
>
<span className={clsx("opportunity-risk-dot", String(tier || "other"))} />
<span className="opportunity-city">{localizedCityName}</span>
<div className="opportunity-card-header">
<span className="opportunity-rank">{index + 1}</span>
<span className="opportunity-city">{localizedCityName}</span>
<span className={clsx("opportunity-pill", String(tier || "other"))}>
{getRiskCopy(tier as RiskLevel | undefined, locale)}
</span>
</div>
<span className="opportunity-meta">
{formatTemperature(currentTemp, symbol)} / DEB{" "}
{formatTemperature(debPrediction, symbol)}
</span>
<span className="opportunity-hit">
{locale === "en-US" ? "Hit" : "命中"}{" "}
{formatPercent(city.deb_recent_hit_rate)}
&gt; {formatTemperature(debPrediction, symbol)}{" "}
{locale === "en-US" ? "target" : "目标"}
</span>
<div className="opportunity-card-footer">
<span className="opportunity-yes">YES {formatCents(detail?.market_scan?.yes_buy)}</span>
<span className="opportunity-edge">{formatEdge(getMarketEdgeValue(detail))}</span>
</div>
</button>
);
})}
@@ -1039,7 +1134,6 @@ function DashboardScreen() {
const { t } = useI18n();
const didAutoFocusRef = useRef(false);
const preloadedOpportunityRef = useRef<Set<string>>(new Set());
const [recentCityNames, setRecentCityNames] = useState<string[]>([]);
const activeSummary = store.selectedCity
? store.citySummariesByName[store.selectedCity] || null
: null;
@@ -1098,26 +1192,9 @@ function DashboardScreen() {
),
[store.cities, store.cityDetailsByName, store.citySummariesByName],
);
const fallbackRecentCityNames = useMemo(
() => homepageSnapshots.slice(0, 4).map((snapshot) => snapshot.city.name),
[homepageSnapshots],
);
const showHomepageChrome =
!store.historyState.isOpen && !store.futureModalDate;
useEffect(() => {
if (typeof window === "undefined") return;
try {
const raw = window.localStorage.getItem(RECENT_OPENED_CITIES_STORAGE_KEY);
if (!raw) return;
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) return;
setRecentCityNames(
parsed.map((item) => String(item || "").trim()).filter(Boolean).slice(0, 8),
);
} catch {}
}, []);
useEffect(() => {
if (didAutoFocusRef.current) return;
if (!showHomepageChrome) return;
@@ -1141,44 +1218,22 @@ function DashboardScreen() {
});
}, [homepageSnapshots, showHomepageChrome, store]);
useEffect(() => {
const selectedCity = store.selectedCity;
if (!selectedCity) return;
setRecentCityNames((current) => {
const next = [
selectedCity,
...current.filter((name) => name !== selectedCity),
].slice(0, 8);
if (typeof window !== "undefined") {
try {
window.localStorage.setItem(
RECENT_OPENED_CITIES_STORAGE_KEY,
JSON.stringify(next),
);
} catch {}
}
return next;
});
}, [store.selectedCity]);
return (
<div
className={clsx(
styles.root,
<div
className={clsx(
styles.root,
detailChromeStyles.root,
modalChromeStyles.root,
)}
>
<MapCanvas />
<HomeMapToolbar />
<HeaderBar />
<CitySidebar />
{showHomepageChrome ? (
<>
<HomeIntelligencePanel snapshots={homepageSnapshots} />
<OpportunityStrip
snapshots={homepageSnapshots}
recentCityNames={recentCityNames.length ? recentCityNames : fallbackRecentCityNames}
/>
<OpportunityStrip snapshots={homepageSnapshots} />
</>
) : null}
{showCitySyncToast ? (