清理最后一批 Polymarket 注释+死代码 WeatherDecisionBand
This commit is contained in:
@@ -4,7 +4,7 @@ import type { CityDetail } from "@/lib/dashboard-types";
|
||||
import { getDisplayAirportPrimary } from "@/lib/airport-observation-display";
|
||||
import { formatTemperatureValue } from "@/lib/temperature-utils";
|
||||
|
||||
// Settlement runway mapping — matches Polymarket settlement anchors
|
||||
// Settlement runway mapping — matches settlement anchors
|
||||
const SETTLEMENT_RUNWAY_PAIRS: Record<string, Array<[string, string]>> = {
|
||||
shanghai: [["17L", "35R"]],
|
||||
beijing: [["01", "19"]],
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import type { ScanOpportunityRow } from "@/lib/dashboard-types";
|
||||
import { REGIONS, getCityRegion } from "./continent-grouping";
|
||||
import { rowName, temp } from "./utils";
|
||||
|
||||
interface CitySelectorDropdownProps {
|
||||
isEn: boolean;
|
||||
rows: ScanOpportunityRow[];
|
||||
onSelectCity: (city: string) => void;
|
||||
onClose: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CitySelectorDropdown({
|
||||
isEn,
|
||||
rows,
|
||||
onSelectCity,
|
||||
onClose,
|
||||
className,
|
||||
}: CitySelectorDropdownProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [activeTab, setActiveTab] = useState<string>("all");
|
||||
|
||||
// Auto-focus input on mount
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
// Handle click outside and Escape key
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
// Tab definitions
|
||||
const tabs = useMemo(() => {
|
||||
return [
|
||||
{ key: "all", labelEn: "ALL", labelZh: "全部" },
|
||||
...REGIONS.map((r) => ({
|
||||
key: r.key,
|
||||
labelEn: r.labelEn.replace("Asia", "").replace("America", "").trim() || r.labelEn,
|
||||
labelZh: r.labelZh,
|
||||
})),
|
||||
];
|
||||
}, []);
|
||||
|
||||
// Filter rows
|
||||
const filteredRows = useMemo(() => {
|
||||
const q = searchQuery.toLowerCase().trim();
|
||||
return rows.filter((row) => {
|
||||
// 1. Region filter
|
||||
if (activeTab !== "all") {
|
||||
const region = getCityRegion(row);
|
||||
if (region !== activeTab) return false;
|
||||
}
|
||||
|
||||
// 2. Query filter
|
||||
if (!q) return true;
|
||||
const haystack = [
|
||||
row.city,
|
||||
row.city_display_name,
|
||||
row.display_name,
|
||||
row.airport,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.map((s) => s!.toLowerCase());
|
||||
return haystack.some((s) => s.includes(q));
|
||||
});
|
||||
}, [rows, searchQuery, activeTab]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx(
|
||||
"flex flex-col bg-white border border-slate-200 rounded-md shadow-2xl overflow-hidden text-xs text-[#202833] animate-in fade-in-50 zoom-in-95 duration-100",
|
||||
className
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()} // Prevent card activation
|
||||
>
|
||||
{/* Search Input Area */}
|
||||
<div className="flex items-center gap-2 p-2 border-b border-slate-100 bg-[#f8fafc]">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder={isEn ? "Search city or airport..." : "搜索城市、机场..."}
|
||||
className="w-full px-2.5 py-1.5 border border-slate-300 rounded bg-white text-xs outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="text-slate-400 hover:text-slate-600 px-1 font-mono"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Region filter tabs */}
|
||||
<div className="flex items-center gap-1.5 px-2.5 py-1.5 border-b border-slate-100 bg-slate-50/50 overflow-x-auto scrollbar-none">
|
||||
{tabs.map((tab) => {
|
||||
const isActive = activeTab === tab.key;
|
||||
return (
|
||||
<button
|
||||
key={tab.key}
|
||||
type="button"
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={clsx(
|
||||
"px-2 py-0.5 text-[10px] font-bold rounded whitespace-nowrap transition-all",
|
||||
isActive
|
||||
? "bg-blue-600 text-white shadow-sm"
|
||||
: "bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-700"
|
||||
)}
|
||||
>
|
||||
{isEn ? tab.labelEn : tab.labelZh}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Scrollable list */}
|
||||
<div className="flex-1 overflow-y-auto max-h-[220px] divide-y divide-slate-50">
|
||||
{filteredRows.length === 0 ? (
|
||||
<div className="p-4 text-center text-slate-400 font-medium">
|
||||
{isEn ? "No matching cities" : "无匹配城市"}
|
||||
</div>
|
||||
) : (
|
||||
filteredRows.map((row) => {
|
||||
const cityName = rowName(row);
|
||||
const obsTemp = row.current_temp ?? row.current_max_so_far;
|
||||
const debPrediction = row.deb_prediction;
|
||||
const symbol = row.temp_symbol || "°C";
|
||||
|
||||
return (
|
||||
<button
|
||||
key={row.id}
|
||||
type="button"
|
||||
onClick={() => onSelectCity(String(row.city || "").toLowerCase())}
|
||||
className="flex w-full items-center justify-between px-3 py-2 text-left hover:bg-blue-50/50 transition-colors group"
|
||||
>
|
||||
<div className="min-w-0 flex-1 pr-3">
|
||||
<div className="font-bold text-slate-800 group-hover:text-blue-600 truncate">
|
||||
{cityName}
|
||||
</div>
|
||||
{row.airport && (
|
||||
<div className="text-[10px] text-slate-400 font-mono truncate">
|
||||
{row.airport}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Weather info */}
|
||||
<div className="flex items-center gap-3 font-mono text-[11px] shrink-0 text-slate-500">
|
||||
{obsTemp !== undefined && obsTemp !== null && (
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="text-[8px] text-slate-400 font-sans scale-90 uppercase">Obs</span>
|
||||
<strong className="text-slate-700 font-bold">
|
||||
{obsTemp}
|
||||
<span className="text-[9px] font-normal font-sans">{symbol}</span>
|
||||
</strong>
|
||||
</div>
|
||||
)}
|
||||
{debPrediction !== undefined && debPrediction !== null && (
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="text-[8px] text-slate-400 font-sans scale-90 uppercase">DEB</span>
|
||||
<strong className="text-orange-600 font-bold">
|
||||
{debPrediction}
|
||||
<span className="text-[9px] font-normal font-sans">{symbol}</span>
|
||||
</strong>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -832,11 +832,13 @@ export function LiveTemperatureThresholdChart({
|
||||
row,
|
||||
allRows = [],
|
||||
compact = false,
|
||||
onSearchClick,
|
||||
}: {
|
||||
isEn: boolean;
|
||||
row: ScanOpportunityRow | null;
|
||||
allRows?: ScanOpportunityRow[];
|
||||
compact?: boolean;
|
||||
onSearchClick?: () => void;
|
||||
}) {
|
||||
const [hourly, setHourly] = useState<HourlyForecast>(null);
|
||||
const city = String(row?.city || "").toLowerCase().trim();
|
||||
@@ -1009,19 +1011,37 @@ export function LiveTemperatureThresholdChart({
|
||||
[series, data],
|
||||
);
|
||||
|
||||
const panelTitle = row
|
||||
? `${rowName(row)} · ${
|
||||
isEn
|
||||
? timeframe === "1D"
|
||||
? "Live & Forecast"
|
||||
: `${timeframe} Forecast`
|
||||
: timeframe === "1D"
|
||||
? "实测与预测"
|
||||
: `${timeframe}预报`
|
||||
}`
|
||||
: isEn
|
||||
? "Temperature Chart"
|
||||
: "气温图表";
|
||||
const subtitle = row
|
||||
? isEn
|
||||
? timeframe === "1D"
|
||||
? "Live & Forecast"
|
||||
: `${timeframe} Forecast`
|
||||
: timeframe === "1D"
|
||||
? "实测与预测"
|
||||
: `${timeframe}预报`
|
||||
: "";
|
||||
|
||||
const panelTitle = row ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSearchClick}
|
||||
className={clsx(
|
||||
"flex items-center gap-1.5 px-1.5 py-0.5 rounded text-left transition-colors font-bold text-slate-800 outline-none select-none",
|
||||
onSearchClick ? "hover:bg-slate-200/80 cursor-pointer" : ""
|
||||
)}
|
||||
>
|
||||
<span>{rowName(row)}</span>
|
||||
{onSearchClick && <span className="text-[8px] text-slate-400">▼</span>}
|
||||
</button>
|
||||
<span className="text-slate-400 font-normal">·</span>
|
||||
<span className="text-slate-500 font-normal">{subtitle}</span>
|
||||
</div>
|
||||
) : isEn ? (
|
||||
"Temperature Chart"
|
||||
) : (
|
||||
"气温图表"
|
||||
);
|
||||
|
||||
const timeframeActions = (
|
||||
<div className="flex items-center gap-1 rounded bg-[#eef2f6] p-0.5 border border-slate-200">
|
||||
|
||||
@@ -10,7 +10,7 @@ export function Panel({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
title: string;
|
||||
title: React.ReactNode;
|
||||
actions?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import clsx from "clsx";
|
||||
import type { WeatherDecisionView } from "@/components/dashboard/scan-terminal/city-card-decision-utils";
|
||||
|
||||
export function WeatherDecisionBand({
|
||||
decisionView,
|
||||
decisionWhyText,
|
||||
isEn,
|
||||
paceDeltaText,
|
||||
}: {
|
||||
decisionView: WeatherDecisionView;
|
||||
decisionWhyText: string;
|
||||
isEn: boolean;
|
||||
paceDeltaText: string;
|
||||
}) {
|
||||
return (
|
||||
<section className={clsx("scan-ai-decision-band", decisionView.tone)}>
|
||||
<div className="scan-ai-decision-main">
|
||||
<span>{decisionView.kicker}</span>
|
||||
<strong>{decisionView.action}</strong>
|
||||
<p className="scan-ai-decision-why">{decisionWhyText}</p>
|
||||
</div>
|
||||
<div className="scan-ai-decision-metrics">
|
||||
<span>
|
||||
{isEn ? "Weather range" : "天气区间"}
|
||||
<b>{decisionView.targetRange}</b>
|
||||
</span>
|
||||
<span>
|
||||
{isEn ? "Path delta" : "路径偏差"} <b>{paceDeltaText}</b>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user