Add Polymarket market link and refresh side panel docs

This commit is contained in:
2569718930@qq.com
2026-03-30 00:09:35 +08:00
parent 6c7b6de8f9
commit ee114fb5bf
5 changed files with 199 additions and 31 deletions
@@ -11,6 +11,7 @@ import { useI18n } from "@/hooks/useI18n";
import { getOfficialSourceLinks } from "@/lib/dashboard-official-sources";
import { getCityScenery } from "@/lib/dashboard-scenery";
import { CityDetail } from "@/lib/dashboard-types";
import { getTodayPolymarketUrl } from "@/lib/polymarket-market-links";
import {
getCityProfileStats,
getRiskBadgeLabel,
@@ -167,6 +168,10 @@ export function DetailPanel() {
() => (detail ? getOfficialSourceLinks(detail) : []),
[detail],
);
const marketUrl = useMemo(
() => getTodayPolymarketUrl(detail, locale),
[detail, locale],
);
const scenery = getCityScenery(detail?.name);
const blurActiveElement = () => {
if (typeof document === "undefined") return;
@@ -280,6 +285,21 @@ export function DetailPanel() {
{getRiskBadgeLabel(panelRiskLevel, locale)}
</span>
<div className="relative group">
{marketUrl ? (
<a
className="history-btn"
href={marketUrl}
target="_blank"
rel="noreferrer"
title={
locale === "en-US"
? "Open today's Polymarket market"
: "打开今日 Polymarket 题目页"
}
>
{locale === "en-US" ? "Open Market" : "打开 Polymarket"}
</a>
) : null}
<button
type="button"
className={clsx("history-btn", !isPro && "pro-locked")}
+40 -8
View File
@@ -351,7 +351,7 @@ export const DOCS_PAGES: DocsPage[] = [
content: {
"zh-CN": {
title: "浏览器插件",
description: "侧边栏插件是主站的轻量入口,负责监控、基础判断和导流,不承载完整分析链路。",
description: "PolyWeather Side Panel 是一个面向天气交易场景的浏览器侧边栏工具,负责自动识别城市、展示简版走势与城市档案,并把用户导回完整分析页面。",
sections: [
{
id: "extension-install",
@@ -372,9 +372,25 @@ export const DOCS_PAGES: DocsPage[] = [
{
type: "bullets",
items: [
"自动识别当前市场 URL,对应切换城市。",
"展示风险徽章、城市档案、今日日内走势简版和多日预报。",
"提供今日日内分析、历史对账和返回主站的快捷入口。",
"自动识别当前 Polymarket 页面中的城市,也支持手动切换。",
"展示城市档案:结算站点、站点距离、观测更新时间、周边站点数量。",
"展示今日日内走势(简版):DEB 走势与官方观测(METAR / HKO / CWA)对照,可悬停查看时间与温度。",
"展示多日最高温预报(简版),并提供一键刷新与跳转主站入口。",
],
},
],
},
{
id: "extension-permission",
title: "权限与隐私",
blocks: [
{
type: "bullets",
items: [
"`tabs`:用于识别当前活动标签页 URL 并自动匹配城市。",
"`storage`:用于保存插件配置与本地缓存,仅存储在本地浏览器。",
"`sidePanel`:用于在浏览器侧边栏展示界面。",
"插件不要求用户登录,不收集个人身份信息,不上传浏览历史,仅在必要时请求天气接口数据。",
],
},
],
@@ -409,7 +425,7 @@ export const DOCS_PAGES: DocsPage[] = [
},
"en-US": {
title: "Browser Extension",
description: "The side-panel extension is a lightweight lead-in to the main site. It focuses on monitoring, basic bias, and traffic flow back to the full dashboard.",
description: "PolyWeather Side Panel is a browser side-panel tool for weather trading workflows. It auto-detects cities, shows compact intraday and city-profile context, and routes users back to the full dashboard.",
sections: [
{
id: "extension-install",
@@ -430,9 +446,25 @@ export const DOCS_PAGES: DocsPage[] = [
{
type: "bullets",
items: [
"Auto-detects the current market URL and switches the side panel to the matching city.",
"Shows risk badges, city profile, a compact intraday chart, and multi-day forecast.",
"Provides quick links into the main intraday analysis and history reconciliation views.",
"Auto-detects the current Polymarket page city, with manual switching also available.",
"Shows a city profile with settlement station, station distance, observation timestamp, and nearby station count.",
"Shows a compact intraday chart with DEB versus official observations (METAR / HKO / CWA), including hoverable time and temperature.",
"Shows a compact multi-day daily-high forecast, plus refresh and jump-to-site actions.",
],
},
],
},
{
id: "extension-permission",
title: "Permissions and privacy",
blocks: [
{
type: "bullets",
items: [
"`tabs`: used to inspect the active tab URL and match the current city.",
"`storage`: used for local configuration and local cache only.",
"`sidePanel`: used to render the browser side panel UI.",
"The extension does not require login, does not collect personally identifiable information, and does not upload browsing history. It only requests weather endpoints when needed to render the panel.",
],
},
],
+112
View File
@@ -0,0 +1,112 @@
import type { CityDetail } from "@/lib/dashboard-types";
import type { Locale } from "@/lib/i18n";
const CITY_TO_MARKET_SLUG: Record<string, string> = {
ankara: "ankara",
atlanta: "atlanta",
austin: "austin",
beijing: "beijing",
"buenos aires": "buenos-aires",
chengdu: "chengdu",
chicago: "chicago",
chongqing: "chongqing",
dallas: "dallas",
houston: "houston",
"hong kong": "hong-kong",
istanbul: "istanbul",
london: "london",
"los angeles": "los-angeles",
lucknow: "lucknow",
madrid: "madrid",
mexico: "mexico-city",
"mexico city": "mexico-city",
miami: "miami",
milan: "milan",
munich: "munich",
"new york": "nyc",
paris: "paris",
"san francisco": "san-francisco",
"sao paulo": "sao-paulo",
seattle: "seattle",
seoul: "seoul",
shanghai: "shanghai",
shenzhen: "shenzhen",
singapore: "singapore",
taipei: "taipei",
"tel aviv": "tel-aviv",
tokyo: "tokyo",
toronto: "toronto",
warsaw: "warsaw",
wellington: "wellington",
wuhan: "wuhan",
};
const MONTHS = [
"january",
"february",
"march",
"april",
"may",
"june",
"july",
"august",
"september",
"october",
"november",
"december",
];
function normalizeCityKey(detail?: CityDetail | null) {
return String(detail?.name || detail?.display_name || "")
.trim()
.toLowerCase();
}
function slugifyCityName(cityKey: string) {
return cityKey
.trim()
.toLowerCase()
.replace(/['.]/g, "")
.replace(/&/g, " and ")
.replace(/\s+/g, "-");
}
function normalizeDateParts(localDate?: string | null) {
const value = String(localDate || "").trim();
const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) return null;
const year = Number(match[1]);
const monthIndex = Number(match[2]) - 1;
const day = Number(match[3]);
if (
!Number.isFinite(year) ||
!Number.isFinite(monthIndex) ||
!Number.isFinite(day) ||
monthIndex < 0 ||
monthIndex > 11
) {
return null;
}
return {
year,
month: MONTHS[monthIndex],
day,
};
}
export function getTodayPolymarketUrl(
detail?: CityDetail | null,
locale: Locale = "en-US",
) {
const cityKey = normalizeCityKey(detail);
const citySlug = CITY_TO_MARKET_SLUG[cityKey] || slugifyCityName(cityKey);
const dateParts = normalizeDateParts(detail?.local_date);
if (!citySlug || !dateParts) return null;
const prefix =
locale === "zh-CN"
? "https://polymarket.com/zh/event/"
: "https://polymarket.com/event/";
return `${prefix}highest-temperature-in-${citySlug}-on-${dateParts.month}-${dateParts.day}-${dateParts.year}`;
}