Refine homepage to match dashboard reference

This commit is contained in:
2569718930@qq.com
2026-04-23 03:16:30 +08:00
parent 81f7e62ac5
commit 558a0031be
4 changed files with 510 additions and 179 deletions
@@ -186,6 +186,16 @@ export function CitySidebar() {
{ key: "low", label: t("sidebar.group.low") },
{ key: "other", label: t("sidebar.group.other") },
];
const syncTime = useMemo(
() =>
new Intl.DateTimeFormat(locale === "en-US" ? "en-US" : "zh-CN", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
}).format(new Date()),
[locale],
);
return (
<nav className="city-list">
@@ -352,6 +362,17 @@ export function CitySidebar() {
);
})}
</div>
<div className="sidebar-footer">
<div>
{locale === "en-US" ? "Data sync" : "数据更新"} {syncTime}
</div>
<div>
{locale === "en-US"
? "Sources: METAR · Open-Meteo · DEB · Market"
: "数据来源:METAR · Open-Meteo · DEB · 市场"}
</div>
</div>
</nav>
);
}
@@ -97,6 +97,33 @@
gap: 10px;
}
.root :global(.home-map-header) {
position: fixed;
top: calc(var(--header-height) + 28px);
left: calc(var(--sidebar-width) + 42px);
z-index: 930;
display: flex;
align-items: center;
}
.root :global(.home-map-title) {
display: flex;
flex-direction: column;
gap: 4px;
}
.root :global(.home-map-title strong) {
color: #f8fafc;
font-size: 15px;
font-weight: 750;
}
.root :global(.home-map-title span) {
color: rgba(148, 163, 184, 0.76);
font-size: 11px;
font-weight: 600;
}
.root :global(.home-map-modes) {
display: inline-flex;
align-items: center;
@@ -152,6 +179,46 @@
justify-content: center;
}
.root :global(.home-map-legend) {
position: fixed;
left: 50%;
bottom: 196px;
transform: translateX(-50%);
z-index: 930;
display: flex;
align-items: center;
gap: 12px;
padding: 8px 14px;
border-radius: 14px;
border: 1px solid rgba(115, 137, 161, 0.18);
background: rgba(6, 12, 22, 0.86);
backdrop-filter: blur(16px);
}
.root :global(.home-map-legend span) {
color: rgba(203, 213, 225, 0.82);
font-size: 10px;
font-weight: 700;
}
.root :global(.home-map-legend-bar) {
width: 178px;
height: 8px;
border-radius: 999px;
background: linear-gradient(90deg, #2563eb, #06b6d4, #22c55e, #eab308, #ef4444);
}
.root :global(.home-map-legend-values) {
display: flex;
align-items: center;
gap: 8px;
}
.root :global(.home-map-legend-values span) {
color: rgba(148, 163, 184, 0.72);
font-size: 9px;
}
.weatherAura {
position: absolute;
inset: 0;
@@ -262,10 +329,46 @@
.root :global(.brand) {
display: flex;
align-items: center;
gap: 14px;
gap: 12px;
min-width: 0;
}
.root :global(.brand-mark) {
width: 28px;
height: 28px;
border-radius: 9px;
border: 1px solid rgba(34, 211, 238, 0.28);
background:
radial-gradient(circle at 30% 30%, rgba(34, 211, 238, 0.24), transparent 52%),
linear-gradient(180deg, rgba(11, 22, 40, 0.96), rgba(6, 13, 24, 0.96));
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.08);
}
.root :global(.brand-mark i) {
width: 13px;
height: 13px;
border-radius: 999px;
border: 2px solid #22d3ee;
position: relative;
}
.root :global(.brand-mark i::before),
.root :global(.brand-mark i::after) {
content: "";
position: absolute;
inset: -4px;
border-radius: 999px;
border: 1px solid rgba(34, 211, 238, 0.34);
}
.root :global(.brand-mark i::after) {
inset: -8px;
border-color: rgba(59, 130, 246, 0.18);
}
.root :global(.brand h1) {
font-size: 18px;
font-weight: 800;
@@ -297,13 +400,14 @@
display: inline-flex;
align-items: center;
gap: 7px;
padding: 10px 12px;
padding: 12px 18px;
border-radius: 10px;
color: rgba(203, 213, 225, 0.82);
text-decoration: none;
font-size: 13px;
font-weight: 650;
font-size: 14px;
font-weight: 550;
transition: var(--transition);
position: relative;
}
.root :global(.header-nav-link:hover) {
@@ -313,8 +417,19 @@
.root :global(.header-nav-link.active) {
color: #f8fafc;
background: rgba(8, 145, 178, 0.12);
box-shadow: inset 0 -2px 0 var(--accent-cyan);
background: transparent;
box-shadow: none;
}
.root :global(.header-nav-link.active::after) {
content: "";
position: absolute;
left: 18px;
right: 18px;
bottom: 4px;
height: 2px;
border-radius: 999px;
background: var(--accent-cyan);
}
.root :global(.header-right) {
@@ -414,6 +529,40 @@
.root :global(.refresh-btn.spinning) {
animation: spin 1s linear infinite;
}
.root :global(.header-utility-btn) {
min-width: 32px;
height: 32px;
padding: 0 12px;
border-radius: 8px;
border: 1px solid rgba(115, 137, 161, 0.18);
background: rgba(8, 15, 27, 0.82);
color: rgba(226, 232, 240, 0.8);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
text-decoration: none;
font-size: 12px;
font-weight: 650;
transition: var(--transition);
}
.root :global(.header-utility-btn:hover) {
color: #f8fafc;
border-color: rgba(34, 211, 238, 0.34);
background: rgba(11, 22, 40, 0.94);
}
.root :global(.header-utility-btn.active) {
color: #f8fafc;
border-color: rgba(59, 130, 246, 0.34);
}
.root :global(.header-utility-btn.more) {
padding: 0;
width: 32px;
}
@keyframes spin {
from {
transform: rotate(0deg);
@@ -506,6 +655,14 @@
border-radius: 2px;
}
.root :global(.sidebar-footer) {
border-top: 1px solid var(--border-subtle);
padding: 10px 12px;
color: rgba(148, 163, 184, 0.72);
font-size: 10px;
line-height: 1.55;
}
.root :global(.city-group) {
border: 1px solid rgba(115, 137, 161, 0.12);
border-radius: 14px;
@@ -811,16 +968,16 @@
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 5px;
min-height: 24px;
gap: 6px;
min-height: 26px;
border-radius: 999px;
padding: 4px 9px 4px 7px;
border: 1px solid rgba(148, 163, 184, 0.16);
background: rgba(9, 18, 36, 0.38);
padding: 5px 10px 5px 8px;
border: 1px solid rgba(148, 163, 184, 0.2);
background: rgba(9, 18, 36, 0.58);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
color: rgba(226, 232, 240, 0.82);
font-size: 11px;
font-weight: 800;
color: rgba(226, 232, 240, 0.88);
font-size: 10px;
font-weight: 900;
letter-spacing: 0.01em;
white-space: nowrap;
}
@@ -836,21 +993,21 @@
}
.root :global(.home-risk-badge.high) {
border-color: rgba(248, 113, 113, 0.2);
border-color: rgba(248, 113, 113, 0.34);
color: #fda4af;
background: rgba(69, 10, 10, 0.18);
background: rgba(69, 10, 10, 0.32);
}
.root :global(.home-risk-badge.medium) {
border-color: rgba(251, 191, 36, 0.18);
color: #fbbf24;
background: rgba(67, 36, 6, 0.16);
border-color: rgba(251, 191, 36, 0.3);
color: #fcd34d;
background: rgba(67, 36, 6, 0.28);
}
.root :global(.home-risk-badge.low) {
border-color: rgba(52, 211, 153, 0.18);
color: #34d399;
background: rgba(7, 47, 35, 0.16);
border-color: rgba(52, 211, 153, 0.28);
color: #86efac;
background: rgba(7, 47, 35, 0.28);
}
.root :global(.home-panel-subtitle) {
@@ -1044,6 +1201,29 @@
background: rgba(148, 163, 184, 0.1);
}
.root :global(.home-panel-header) {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.root :global(.home-focus-title) {
color: #f8fafc;
font-size: 12px;
font-weight: 700;
}
.root :global(.home-why-link) {
border: none;
background: transparent;
color: #60a5fa;
cursor: pointer;
font: inherit;
font-size: 11px;
font-weight: 650;
}
.root :global(.home-top-opportunity-label) {
display: inline-flex;
align-self: flex-start;
@@ -1774,7 +1954,7 @@
}
.root :global(.home-summary-card) {
min-height: 92px;
min-height: 118px;
display: flex;
flex-direction: column;
justify-content: space-between;
@@ -1784,23 +1964,57 @@
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-head) {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.root :global(.home-summary-card strong) {
.root :global(.home-summary-card-head strong) {
color: #f8fafc;
font-size: 14px;
font-weight: 760;
line-height: 1.2;
}
.root :global(.home-summary-card-head span) {
color: #60a5fa;
font-size: 10px;
font-weight: 700;
}
.root :global(.home-summary-card-body) {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
align-items: end;
}
.root :global(.home-summary-stat) {
display: flex;
flex-direction: column;
gap: 4px;
}
.root :global(.home-summary-stat b) {
color: #f8fafc;
font-size: 28px;
font-weight: 850;
line-height: 1;
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(.home-summary-stat span) {
color: rgba(148, 163, 184, 0.82);
font-size: 10px;
font-weight: 650;
}
.root :global(.home-summary-stat b.accent-red) { color: #fb7185; }
.root :global(.home-summary-stat b.accent-amber) { color: #fbbf24; }
.root :global(.home-summary-stat b.accent-green) { color: #4ade80; }
.root :global(.home-summary-stat b.accent-cyan) { color: #22d3ee; }
.root :global(.opportunity-strip-heading) {
display: flex;
@@ -1875,6 +2089,12 @@
gap: 8px;
}
.root :global(.opportunity-price-pair) {
display: inline-flex;
align-items: center;
gap: 10px;
}
.root :global(.opportunity-rank) {
width: 24px;
height: 24px;
@@ -1937,6 +2157,12 @@
font-weight: 800;
}
.root :global(.opportunity-no) {
color: #fda4af;
font-size: 12px;
font-weight: 800;
}
.root :global(.opportunity-edge) {
color: rgba(34, 211, 238, 0.9);
font-size: 18px;
+57 -97
View File
@@ -8,11 +8,7 @@ import {
UserRound,
RotateCw,
BookOpen,
Sparkles,
House,
Bell,
BarChart3,
CandlestickChart,
MoreHorizontal,
} from "lucide-react";
import { useDashboardStore } from "@/hooks/useDashboardStore";
import { useI18n } from "@/hooks/useI18n";
@@ -39,7 +35,7 @@ export function HeaderBar({
refreshSpinning?: boolean;
}) {
const store = useDashboardStore();
const { locale, setLocale, t } = useI18n();
const { locale, t } = useI18n();
const pathname = usePathname();
const isAuthenticated = store.proAccess.authenticated;
const docsHref = "/docs/intro";
@@ -49,37 +45,28 @@ export function HeaderBar({
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" : "提醒",
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"
: "新用户可免费体验 3 天 Pro";
const isRefreshing = refreshSpinning ?? store.loadingState.refresh;
const handleRefresh = () => {
if (refreshAction) {
@@ -91,9 +78,6 @@ export function HeaderBar({
const accountHref = isAuthenticated
? "/account"
: "/auth/login?next=%2Faccount";
const accountLabel = isAuthenticated
? t("header.account")
: t("header.signIn");
const accountAria = isAuthenticated
? t("header.accountAria")
: t("header.signInAria");
@@ -138,6 +122,9 @@ export function HeaderBar({
return (
<header className="header">
<div className="brand">
<span className="brand-mark" aria-hidden="true">
<i />
</span>
<h1>PolyWeather</h1>
<span className="subtitle">{t("header.subtitle")}</span>
</div>
@@ -146,87 +133,18 @@ export function HeaderBar({
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>
);
})}
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
className={clsx("header-nav-link", item.active && "active")}
>
<span>{item.label}</span>
</Link>
))}
</nav>
<div className="header-right">
<div
className="lang-switch"
role="group"
aria-label={t("header.langAria")}
>
<button
type="button"
className={clsx("lang-btn", locale === "zh-CN" && "active")}
onClick={() => setLocale("zh-CN")}
>
{t("header.langZh")}
</button>
<button
type="button"
className={clsx("lang-btn", locale === "en-US" && "active")}
onClick={() => setLocale("en-US")}
>
{t("header.langEn")}
</button>
</div>
<Link
href={docsHref}
className={clsx("info-btn", docsActive && "active")}
title={t("header.docsAria")}
aria-label={t("header.docsAria")}
>
<BookOpen size={14} strokeWidth={2} />
{t("header.docs")}
</Link>
<Link
href="/account"
className="trial-promo-badge"
title={trialPromoLabel}
aria-label={trialPromoLabel}
>
<Sparkles size={12} strokeWidth={2} />
<span>{trialPromoLabel}</span>
</Link>
<Link
href={accountHref}
className="account-btn"
title={accountAria}
aria-label={accountAria}
>
{isAuthenticated ? <UserRound size={14} /> : <LogIn size={14} />}
<span>{accountLabel}</span>
</Link>
{showRenewReminder ? (
<Link
href="/account"
className={clsx(
"account-renew-badge",
!store.proAccess.subscriptionActive && "expired",
)}
title={renewReminderLabel}
aria-label={renewReminderLabel}
>
<span>{renewReminderLabel}</span>
</Link>
) : null}
<div className="live-badge" id="liveBadge">
<span className="pulse-dot" />
<span>{t("header.live")}</span>
@@ -241,6 +159,48 @@ export function HeaderBar({
>
<RotateCw size={16} strokeWidth={2} />
</button>
<Link
href={docsHref}
className={clsx("header-utility-btn", docsActive && "active")}
title={t("header.docsAria")}
aria-label={t("header.docsAria")}
>
<BookOpen size={14} strokeWidth={2} />
<span>{t("header.docs")}</span>
</Link>
<Link
href={accountHref}
className="header-utility-btn"
title={accountAria}
aria-label={accountAria}
>
{isAuthenticated ? <UserRound size={14} /> : <LogIn size={14} />}
</Link>
<button
type="button"
className="header-utility-btn more"
aria-label={locale === "en-US" ? "More actions" : "更多操作"}
title={locale === "en-US" ? "More actions" : "更多操作"}
>
<MoreHorizontal size={16} strokeWidth={2} />
</button>
{showRenewReminder ? (
<Link
href="/account"
className={clsx(
"account-renew-badge",
!store.proAccess.subscriptionActive && "expired",
)}
title={renewReminderLabel}
aria-label={renewReminderLabel}
>
<span>{renewReminderLabel}</span>
</Link>
) : null}
</div>
</header>
);
@@ -238,21 +238,36 @@ type HomeForecastDay = {
maxTemp: number;
};
type HomeSummaryMetric = {
type HomeSummaryCard = {
key: string;
label: string;
value: string;
accent?: "green" | "amber" | "red" | "cyan";
title: string;
linkLabel?: string;
items: Array<{
label: string;
value: string;
accent?: "green" | "amber" | "red" | "cyan";
}>;
};
function buildDashboardSummaryMetrics(
function buildDashboardSummaryCards(
snapshots: CitySnapshot[],
locale: string,
): HomeSummaryMetric[] {
): HomeSummaryCard[] {
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 strongAgreement = snapshots.filter(
(snapshot) => Number(snapshot.city.deb_recent_hit_rate ?? 0) >= 0.66,
).length;
const mediumAgreement = snapshots.filter((snapshot) => {
const rate = Number(snapshot.city.deb_recent_hit_rate ?? 0);
return rate >= 0.4 && rate < 0.66;
}).length;
const weakAgreement = Math.max(
snapshots.length - strongAgreement - mediumAgreement,
0,
);
const avgEdge = topTradable.length
? topTradable.reduce((sum, snapshot) => {
const edgeValue = Number(getMarketEdgeValue(snapshot.detail));
@@ -266,28 +281,92 @@ function buildDashboardSummaryMetrics(
return [
{
key: "high",
label: locale === "en-US" ? "High risk" : "高风险",
value: String(highCount),
accent: "red",
key: "opportunities",
title: locale === "en-US" ? "Today's Opportunities" : "今日机会分布",
items: [
{
label: locale === "en-US" ? "High risk" : "高风险",
value: String(highCount),
accent: "red",
},
{
label: locale === "en-US" ? "Medium risk" : "中风险",
value: String(mediumCount),
accent: "amber",
},
{
label: locale === "en-US" ? "Low risk" : "低风险",
value: String(lowCount),
accent: "green",
},
],
},
{
key: "medium",
label: locale === "en-US" ? "Medium risk" : "中风险",
value: String(mediumCount),
accent: "amber",
key: "market-summary",
title: locale === "en-US" ? "Market Summary" : "市场概览",
items: [
{
label: locale === "en-US" ? "Total markets" : "总市场数",
value: String(snapshots.length),
},
{
label: locale === "en-US" ? "Active" : "活跃市场",
value: String(topTradable.length),
accent: "cyan",
},
{
label: locale === "en-US" ? "Avg. edge" : "平均优势",
value: formatEdge(avgEdge),
accent: "green",
},
],
},
{
key: "low",
label: locale === "en-US" ? "Low risk" : "低风险",
value: String(lowCount),
accent: "green",
key: "model-agreement",
title: locale === "en-US" ? "Model Agreement" : "模型一致性",
items: [
{
label: locale === "en-US" ? "High" : "高",
value: `${strongAgreement}`,
accent: "green",
},
{
label: locale === "en-US" ? "Medium" : "中",
value: `${mediumAgreement}`,
accent: "amber",
},
{
label: locale === "en-US" ? "Low" : "低",
value: `${weakAgreement}`,
accent: "red",
},
],
},
{
key: "edge",
label: locale === "en-US" ? "Avg. edge" : "平均优势",
value: formatEdge(avgEdge),
accent: "cyan",
key: "impact-window",
title: locale === "en-US" ? "High Impact Window" : "高影响窗口",
items: [
{
label: locale === "en-US" ? "Next 6 hours" : "未来 6 小时",
value:
locale === "en-US"
? `${Math.min(topTradable.length, 12)} markets`
: `${Math.min(topTradable.length, 12)} 个市场`,
},
{
label: locale === "en-US" ? "Focus city" : "焦点城市",
value: topTradable[0]?.city.display_name || "--",
accent: "cyan",
},
{
label: locale === "en-US" ? "Best edge" : "最佳优势",
value:
topTradable.length > 0
? formatEdge(getMarketEdgeValue(topTradable[0]?.detail))
: "--",
accent: "green",
},
],
},
];
}
@@ -295,29 +374,50 @@ function buildDashboardSummaryMetrics(
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>
<>
<div className="home-map-header">
<div className="home-map-title">
<strong>{locale === "en-US" ? "Global weather regime" : "全球天气态势"}</strong>
<span>{locale === "en-US" ? "Live monitored weather derivatives board" : "天气衍生品实时监控面板"}</span>
</div>
</div>
<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>
<button
type="button"
className="home-map-expand"
aria-label={locale === "en-US" ? "Expand map" : "放大地图"}
>
<Expand size={15} strokeWidth={2} />
</button>
</div>
<div className="home-map-legend" aria-hidden="true">
<span>{locale === "en-US" ? "Temperature (°F)" : "温度 (°F)"}</span>
<div className="home-map-legend-bar" />
<div className="home-map-legend-values">
<span>-4</span>
<span>14</span>
<span>32</span>
<span>50</span>
<span>68</span>
<span>86</span>
<span>104</span>
</div>
</div>
</>
);
}
@@ -771,6 +871,15 @@ function HomeIntelligencePanel({ snapshots }: { snapshots: CitySnapshot[] }) {
×
</button>
<div className="home-panel-header">
<span className="home-focus-title">
{locale === "en-US" ? "Focus city" : "焦点城市"}
</span>
<button type="button" className="home-why-link">
{locale === "en-US" ? "Why this city?" : "为什么是它?"}
</button>
</div>
<div className="home-top-opportunity-label">
{showOpportunityLabel
? locale === "en-US"
@@ -1052,8 +1161,8 @@ function OpportunityStrip({
}) {
const { locale } = useI18n();
const store = useDashboardStore();
const summaryMetrics = useMemo(
() => buildDashboardSummaryMetrics(snapshots, locale),
const summaryCards = useMemo(
() => buildDashboardSummaryCards(snapshots, locale),
[locale, snapshots],
);
const items = useMemo(
@@ -1069,10 +1178,22 @@ function OpportunityStrip({
aria-label={locale === "en-US" ? "Opportunity strip" : "机会条"}
>
<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>
{summaryCards.map((card) => (
<div key={card.key} className="home-summary-card">
<div className="home-summary-card-head">
<strong>{card.title}</strong>
<span>{locale === "en-US" ? "View all" : "查看全部"}</span>
</div>
<div className="home-summary-card-body">
{card.items.map((item) => (
<div key={`${card.key}-${item.label}`} className="home-summary-stat">
<b className={item.accent ? `accent-${item.accent}` : undefined}>
{item.value}
</b>
<span>{item.label}</span>
</div>
))}
</div>
</div>
))}
</div>
@@ -1118,7 +1239,10 @@ function OpportunityStrip({
{locale === "en-US" ? "target" : "目标"}
</span>
<div className="opportunity-card-footer">
<span className="opportunity-yes">YES {formatCents(detail?.market_scan?.yes_buy)}</span>
<div className="opportunity-price-pair">
<span className="opportunity-yes">YES {formatCents(detail?.market_scan?.yes_buy)}</span>
<span className="opportunity-no">NO {formatCents(detail?.market_scan?.no_buy)}</span>
</div>
<span className="opportunity-edge">{formatEdge(getMarketEdgeValue(detail))}</span>
</div>
</button>