Fix homepage opportunity and focus panel layout
This commit is contained in:
@@ -1146,7 +1146,6 @@
|
||||
}
|
||||
|
||||
.root :global(.home-pro-card) {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -1665,12 +1664,15 @@
|
||||
}
|
||||
|
||||
.root :global(.home-card-section.intraday) {
|
||||
border-color: rgba(34, 211, 238, 0.18);
|
||||
border-color: rgba(34, 211, 238, 0.2);
|
||||
background:
|
||||
radial-gradient(circle at 0% 35%, rgba(34, 211, 238, 0.12), transparent 44%),
|
||||
linear-gradient(180deg, rgba(7, 14, 26, 0.94), rgba(9, 17, 30, 0.82));
|
||||
}
|
||||
|
||||
.root :global(.home-intraday-chart) {
|
||||
position: relative;
|
||||
padding: 10px 0 0;
|
||||
padding: 6px 0 0;
|
||||
}
|
||||
|
||||
.root :global(.home-intraday-chart svg) {
|
||||
@@ -1742,6 +1744,37 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.root :global(.home-intraday-reports) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 7px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.root :global(.home-intraday-reports span) {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
padding: 7px 8px;
|
||||
border: 1px solid rgba(34, 211, 238, 0.12);
|
||||
border-radius: 10px;
|
||||
background: rgba(2, 8, 18, 0.55);
|
||||
}
|
||||
|
||||
.root :global(.home-intraday-reports b) {
|
||||
color: rgba(148, 163, 184, 0.9);
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.root :global(.home-intraday-reports strong) {
|
||||
color: #e0f2fe;
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.root :global(.home-card-section.forecast) {
|
||||
border-color: rgba(99, 102, 241, 0.24);
|
||||
}
|
||||
@@ -1806,33 +1839,40 @@
|
||||
}
|
||||
|
||||
.root :global(.home-card-section.probability) {
|
||||
border-color: rgba(115, 137, 161, 0.16);
|
||||
border-color: rgba(251, 113, 133, 0.18);
|
||||
background:
|
||||
radial-gradient(circle at 100% 0%, rgba(251, 113, 133, 0.08), transparent 42%),
|
||||
linear-gradient(180deg, rgba(7, 14, 26, 0.94), rgba(9, 17, 30, 0.82));
|
||||
}
|
||||
|
||||
.root :global(.home-probability-list) {
|
||||
.root :global(.home-probability-list),
|
||||
.root :global(.home-probability-ladder) {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.root :global(.home-probability-row) {
|
||||
.root :global(.home-probability-row),
|
||||
.root :global(.home-probability-ladder-row) {
|
||||
display: grid;
|
||||
grid-template-columns: 58px 1fr;
|
||||
grid-template-columns: 58px minmax(0, 1fr) 42px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 9px;
|
||||
color: rgba(226, 232, 240, 0.95);
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.root :global(.home-probability-row div) {
|
||||
.root :global(.home-probability-row div),
|
||||
.root :global(.home-probability-track) {
|
||||
position: relative;
|
||||
height: 18px;
|
||||
height: 14px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: rgba(115, 137, 161, 0.12);
|
||||
}
|
||||
|
||||
.root :global(.home-probability-row i) {
|
||||
.root :global(.home-probability-row i),
|
||||
.root :global(.home-probability-track i) {
|
||||
position: absolute;
|
||||
inset: 0 auto 0 0;
|
||||
border-radius: inherit;
|
||||
@@ -1840,22 +1880,31 @@
|
||||
box-shadow: 0 0 10px rgba(34, 211, 238, 0.14);
|
||||
}
|
||||
|
||||
.root :global(.home-probability-row:nth-child(3) i) {
|
||||
.root :global(.home-probability-row:nth-child(3) i),
|
||||
.root :global(.home-probability-ladder-row:nth-child(3) i) {
|
||||
background: linear-gradient(90deg, #f59e0b, #fbbf24);
|
||||
}
|
||||
|
||||
.root :global(.home-probability-row:nth-child(n + 4) i) {
|
||||
.root :global(.home-probability-row:nth-child(n + 4) i),
|
||||
.root :global(.home-probability-ladder-row:nth-child(n + 4) i) {
|
||||
background: linear-gradient(90deg, #ef4444, #fb7185);
|
||||
}
|
||||
|
||||
.root :global(.home-probability-row strong) {
|
||||
.root :global(.home-probability-row strong),
|
||||
.root :global(.home-probability-ladder-row strong) {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
padding-left: 12px;
|
||||
color: #f8fafc;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
line-height: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.root :global(.home-probability-threshold) {
|
||||
color: rgba(226, 232, 240, 0.95);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.root :global(.home-card-empty) {
|
||||
@@ -2033,7 +2082,7 @@
|
||||
}
|
||||
|
||||
.root :global(.home-summary-card) {
|
||||
min-height: 118px;
|
||||
min-height: 106px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
@@ -2145,8 +2194,8 @@
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-height: 122px;
|
||||
gap: 9px;
|
||||
min-height: 132px;
|
||||
padding: 14px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.14);
|
||||
border-radius: 16px;
|
||||
@@ -2247,6 +2296,7 @@
|
||||
font-size: 11px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
.root :global(.opportunity-yes) {
|
||||
@@ -2267,6 +2317,21 @@
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.root :global(.opportunity-sparkline) {
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.root :global(.opportunity-sparkline polyline) {
|
||||
fill: none;
|
||||
stroke: #18e6d4;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
filter: drop-shadow(0 0 8px rgba(34, 211, 238, 0.22));
|
||||
}
|
||||
|
||||
/* Market-closed opportunity card — dimmed state */
|
||||
.root :global(.opportunity-card.market-closed) {
|
||||
opacity: 0.6;
|
||||
|
||||
@@ -28,7 +28,6 @@ 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(
|
||||
@@ -227,6 +226,11 @@ type HomeTrendChart = {
|
||||
forecastPath: string;
|
||||
legendText: string;
|
||||
observationDots: Array<{ cx: number; cy: number; key: string }>;
|
||||
hourlyReports: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
temperatureText: string;
|
||||
}>;
|
||||
hoverPoints: Array<{
|
||||
cx: number;
|
||||
cy: number;
|
||||
@@ -399,32 +403,6 @@ function HomeMapToolbar() {
|
||||
</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>
|
||||
<div className="home-map-legend" aria-hidden="true">
|
||||
<span>{locale === "en-US" ? "Temperature (°F)" : "温度 (°F)"}</span>
|
||||
<div className="home-map-legend-bar" />
|
||||
@@ -565,11 +543,23 @@ function buildHomeTrendChart(
|
||||
temperatureText: formatTemperature(point.y, detail.temp_symbol || "°C"),
|
||||
};
|
||||
});
|
||||
const hourlyReports = hoverSource
|
||||
.filter((point) => {
|
||||
const label = String(point.labelTime || "");
|
||||
return /(^|\D)\d{1,2}:00($|\D)/.test(label) || hoverSource.length <= 8;
|
||||
})
|
||||
.slice(-6)
|
||||
.map((point, index) => ({
|
||||
key: `hourly-${point.labelTime}-${index}`,
|
||||
label: point.labelTime,
|
||||
temperatureText: formatTemperature(point.y, detail.temp_symbol || "°C"),
|
||||
}));
|
||||
|
||||
return {
|
||||
forecastPath,
|
||||
legendText: chartData.legendText,
|
||||
observationDots,
|
||||
hourlyReports,
|
||||
hoverPoints,
|
||||
};
|
||||
}
|
||||
@@ -747,7 +737,7 @@ function HomeIntelligencePanel({ snapshots }: { snapshots: CitySnapshot[] }) {
|
||||
? detail?.multi_model_daily?.[detail.local_date]?.probabilities
|
||||
: undefined) ||
|
||||
[];
|
||||
const displayedProbabilities = probabilityBuckets.slice(0, 5);
|
||||
const displayedProbabilities = probabilityBuckets.slice(0, 4);
|
||||
const modelLabels = getModelLabels(detail);
|
||||
const marketScan = detail?.market_scan;
|
||||
const marketBucket =
|
||||
@@ -910,6 +900,29 @@ function HomeIntelligencePanel({ snapshots }: { snapshots: CitySnapshot[] }) {
|
||||
keySignals,
|
||||
} = spotlightView;
|
||||
const subtitle = `${cityCode} · ${localizedAirportName}`;
|
||||
const proCard = (
|
||||
<div className={clsx("home-pro-card", isPro && "active")}>
|
||||
<div>
|
||||
<span>{proLabel}</span>
|
||||
<strong>
|
||||
{isPro
|
||||
? locale === "en-US"
|
||||
? "Today intraday analysis is the primary paid workflow."
|
||||
: "今日日内分析是当前主要付费工作流。"
|
||||
: locale === "en-US"
|
||||
? "History review and future dates stay paid."
|
||||
: "历史复盘和未来日期保持付费。"}
|
||||
</strong>
|
||||
</div>
|
||||
{isPro ? (
|
||||
<button type="button" onClick={() => void store.openTodayModal()}>
|
||||
{locale === "en-US" ? "Today intraday" : "今日日内分析"}
|
||||
</button>
|
||||
) : (
|
||||
<Link href="/account">{locale === "en-US" ? "Upgrade" : "升级"}</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<aside
|
||||
@@ -928,12 +941,9 @@ function HomeIntelligencePanel({ snapshots }: { snapshots: CitySnapshot[] }) {
|
||||
: "最佳机会"
|
||||
: locale === "en-US"
|
||||
? "FOCUS CITY"
|
||||
: "焦点城市"}
|
||||
: "焦点城市"}
|
||||
</span>
|
||||
</div>
|
||||
<button type="button" className="home-why-link">
|
||||
{locale === "en-US" ? "Why this city?" : "为什么是它?"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="home-card-titlebar">
|
||||
@@ -962,6 +972,8 @@ function HomeIntelligencePanel({ snapshots }: { snapshots: CitySnapshot[] }) {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{proCard}
|
||||
|
||||
<div className="home-weather-hero">
|
||||
<div>
|
||||
<strong>{formatTemperature(currentTemp, symbol)}</strong>
|
||||
@@ -1007,8 +1019,8 @@ function HomeIntelligencePanel({ snapshots }: { snapshots: CitySnapshot[] }) {
|
||||
{trendChart ? (
|
||||
<div className="home-card-section intraday">
|
||||
<h3>
|
||||
{locale === "en-US" ? "Intraday trend" : "今日日内走势"}{" "}
|
||||
<small>{locale === "en-US" ? "compact" : "简版"}</small>
|
||||
{locale === "en-US" ? "Hourly reports" : "小时准点报"}{" "}
|
||||
<small>{locale === "en-US" ? "1h cadence" : "1h 级别"}</small>
|
||||
</h3>
|
||||
<div className="home-intraday-chart">
|
||||
<svg viewBox="0 0 296 78" aria-hidden="true">
|
||||
@@ -1056,6 +1068,16 @@ function HomeIntelligencePanel({ snapshots }: { snapshots: CitySnapshot[] }) {
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{trendChart.hourlyReports.length ? (
|
||||
<div className="home-intraday-reports">
|
||||
{trendChart.hourlyReports.map((report) => (
|
||||
<span key={report.key}>
|
||||
<b>{report.label}</b>
|
||||
<strong>{report.temperatureText}</strong>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -1087,23 +1109,25 @@ function HomeIntelligencePanel({ snapshots }: { snapshots: CitySnapshot[] }) {
|
||||
|
||||
<div className="home-card-section probability">
|
||||
<h3>
|
||||
{probabilityTitle} <small>({dayMaxLabel})</small>
|
||||
{probabilityTitle} <small>{dayMaxLabel}</small>
|
||||
</h3>
|
||||
{displayedProbabilities.length ? (
|
||||
<div className="home-probability-list">
|
||||
<div className="home-probability-ladder">
|
||||
{displayedProbabilities.map((bucket, index) => {
|
||||
const probability = Number(bucket.probability ?? 0);
|
||||
const width = Math.max(6, Math.min(100, probability * 100));
|
||||
return (
|
||||
<div
|
||||
key={`${getProbabilityLabel(bucket, symbol)}-${index}`}
|
||||
className="home-probability-row"
|
||||
className="home-probability-ladder-row"
|
||||
>
|
||||
<span>{getProbabilityLabel(bucket, symbol)}</span>
|
||||
<div>
|
||||
<span className="home-probability-threshold">
|
||||
{getProbabilityLabel(bucket, symbol)}
|
||||
</span>
|
||||
<div className="home-probability-track">
|
||||
<i style={{ width: `${width}%` }} />
|
||||
<strong>{formatProbability(probability)}</strong>
|
||||
</div>
|
||||
<strong>{formatProbability(probability)}</strong>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -1182,27 +1206,6 @@ function HomeIntelligencePanel({ snapshots }: { snapshots: CitySnapshot[] }) {
|
||||
<p>{observationSource}</p>
|
||||
</div>
|
||||
|
||||
<div className={clsx("home-pro-card", isPro && "active")}>
|
||||
<div>
|
||||
<span>{proLabel}</span>
|
||||
<strong>
|
||||
{isPro
|
||||
? locale === "en-US"
|
||||
? "Open today intraday analysis first. History review and future-day workflow stay available after that."
|
||||
: "先打开今日日内分析,历史复盘和未来日工作流仍可继续查看。"
|
||||
: locale === "en-US"
|
||||
? "History review and future dates stay paid."
|
||||
: "历史复盘和未来日期保持付费。"}
|
||||
</strong>
|
||||
</div>
|
||||
{isPro ? (
|
||||
<button type="button" onClick={() => void store.openTodayModal()}>
|
||||
{locale === "en-US" ? "Today intraday" : "今日日内分析"}
|
||||
</button>
|
||||
) : (
|
||||
<Link href="/account">{locale === "en-US" ? "Upgrade" : "升级"}</Link>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -1265,8 +1268,8 @@ function OpportunityStrip({ snapshots }: { snapshots: CitySnapshot[] }) {
|
||||
</span>
|
||||
<strong>
|
||||
{locale === "en-US"
|
||||
? "Sorted by score and risk"
|
||||
: "按综合评分排序"}
|
||||
? "Market question · YES/NO · Edge · Trend"
|
||||
: "市场问题 · YES/NO 价格 · Edge · 小趋势线"}
|
||||
</strong>
|
||||
</div>
|
||||
<Link href="/docs/intraday-signal" className="opportunity-view-all">
|
||||
@@ -1291,6 +1294,23 @@ function OpportunityStrip({ snapshots }: { snapshots: CitySnapshot[] }) {
|
||||
summary?.risk?.level ||
|
||||
detail?.risk?.level;
|
||||
const marketClosed = !tradableOpportunity;
|
||||
const marketBucket = detail?.market_scan?.temperature_bucket;
|
||||
const marketQuestion =
|
||||
detail?.market_scan?.primary_market?.question ||
|
||||
`${getProbabilityLabel(marketBucket || {}, symbol)} ${
|
||||
detail?.market_scan?.selected_date ||
|
||||
detail?.local_date ||
|
||||
""
|
||||
}`;
|
||||
const opportunitySparkline = buildSparklinePoints(
|
||||
detail?.market_scan?.sparkline?.length
|
||||
? detail.market_scan.sparkline
|
||||
: [
|
||||
Number(summary?.current?.temp ?? detail?.current?.temp ?? 0),
|
||||
Number(debPrediction ?? 0),
|
||||
Number(detail?.forecast?.today_high ?? debPrediction ?? 0),
|
||||
],
|
||||
);
|
||||
return (
|
||||
<button
|
||||
key={city.name}
|
||||
@@ -1316,8 +1336,7 @@ function OpportunityStrip({ snapshots }: { snapshots: CitySnapshot[] }) {
|
||||
</span>
|
||||
</div>
|
||||
<span className="opportunity-meta">
|
||||
> {formatTemperature(debPrediction, symbol)}{" "}
|
||||
{locale === "en-US" ? "target" : "目标"}
|
||||
{marketQuestion}
|
||||
{marketClosed && (
|
||||
<span className="opportunity-market-status">
|
||||
{" · "}
|
||||
@@ -1338,6 +1357,11 @@ function OpportunityStrip({ snapshots }: { snapshots: CitySnapshot[] }) {
|
||||
{formatEdge(getMarketEdgeValue(detail))}
|
||||
</span>
|
||||
</div>
|
||||
<svg className="opportunity-sparkline" viewBox="0 0 92 28" aria-hidden="true">
|
||||
{opportunitySparkline ? (
|
||||
<polyline points={opportunitySparkline} />
|
||||
) : null}
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user