diff --git a/frontend/.env.local b/frontend/.env.local index 449d8f7f..cca51555 100644 --- a/frontend/.env.local +++ b/frontend/.env.local @@ -23,12 +23,15 @@ NEXT_PUBLIC_SITE_URL=https://polyweather-pro.vercel.app # 常用:前端鉴权开关 # true: 启用 Supabase 登录 # false: 关闭登录能力,访客模式 -POLYWEATHER_AUTH_ENABLED=false +POLYWEATHER_AUTH_ENABLED=true # 常用:是否强制登录 # true: middleware 强制登录后才能访问主页面 # false: 登录可选,访客可浏览 -POLYWEATHER_AUTH_REQUIRED=false +POLYWEATHER_AUTH_REQUIRED=true + +# 关闭本地开发鉴权绕过 +NEXT_PUBLIC_POLYWEATHER_LOCAL_FULL_ACCESS=false # 可选:分享式看板访问令牌 # 设置后,可通过 /?access_token= 打开受保护看板 diff --git a/frontend/components/dashboard/DashboardEntry.tsx b/frontend/components/dashboard/DashboardEntry.tsx deleted file mode 100644 index 74f53fda..00000000 --- a/frontend/components/dashboard/DashboardEntry.tsx +++ /dev/null @@ -1,19 +0,0 @@ -"use client"; - -import dynamic from "next/dynamic"; -import { DashboardShellSkeleton } from "@/components/dashboard/DashboardShellSkeleton"; - -const ScanTerminalDashboard = dynamic( - () => - import("@/components/dashboard/ScanTerminalDashboard").then( - (module) => module.ScanTerminalDashboard, - ), - { - ssr: false, - loading: () => , - }, -); - -export function DashboardEntry() { - return ; -} diff --git a/frontend/components/dashboard/scan-terminal/MobileCityPicker.tsx b/frontend/components/dashboard/scan-terminal/MobileCityPicker.tsx deleted file mode 100644 index 4d97095e..00000000 --- a/frontend/components/dashboard/scan-terminal/MobileCityPicker.tsx +++ /dev/null @@ -1,314 +0,0 @@ -"use client"; - -import { Search, ChevronDown } from "lucide-react"; -import { useMemo, useState } from "react"; -import type { ScanOpportunityRow } from "@/lib/dashboard-types"; - -type Locale = "zh-CN" | "en-US"; - -type CityRegionGroup = { - key: string; - label: { en: string; zh: string }; - cities: string[]; -}; - -const CITY_REGION_GROUPS: CityRegionGroup[] = [ - { - key: "eastAsia", - label: { en: "East Asia", zh: "东亚" }, - cities: [ - "tokyo", "seoul", "busan", "beijing", "shanghai", "chengdu", - "chongqing", "wuhan", "shenzhen", "guangzhou", "qingdao", - "hong kong", "lau fau shan", "taipei", - ], - }, - { - key: "southeastAsia", - label: { en: "Southeast Asia", zh: "东南亚" }, - cities: ["singapore", "kuala lumpur", "jakarta", "manila"], - }, - { - key: "southAsia", - label: { en: "South Asia", zh: "南亚" }, - cities: ["karachi", "lucknow"], - }, - { - key: "middleEast", - label: { en: "Middle East", zh: "中东" }, - cities: ["tel aviv", "jeddah", "istanbul", "ankara"], - }, - { - key: "europe", - label: { en: "Europe", zh: "欧洲" }, - cities: [ - "london", "paris", "moscow", "munich", "milan", "warsaw", - "helsinki", "amsterdam", "madrid", - ], - }, - { - key: "northAmerica", - label: { en: "North America", zh: "北美" }, - cities: [ - "new york", "los angeles", "san francisco", "denver", "austin", - "houston", "chicago", "dallas", "miami", "atlanta", "seattle", - "toronto", "mexico city", "panama city", - ], - }, - { - key: "southAmerica", - label: { en: "South America", zh: "南美" }, - cities: ["buenos aires", "são paulo"], - }, - { - key: "africa", - label: { en: "Africa", zh: "非洲" }, - cities: ["cape town"], - }, - { - key: "oceania", - label: { en: "Oceania", zh: "大洋洲" }, - cities: ["wellington"], - }, -]; - -const CITY_SEARCH_INDEX: Record = { - "tokyo": ["东京", "東京", "RJTT", "tok", "tyo", "haneda", "羽田"], - "seoul": ["首尔", "RKSI", "sel", "seo", "incheon", "仁川"], - "busan": ["釜山", "RKPK", "pus", "bus", "gimhae", "金海"], - "beijing": ["北京", "ZBAA", "pek", "bjs", "bj", "capital", "首都"], - "shanghai": ["上海", "ZSPD", "sha", "sh", "pudong", "浦东"], - "chengdu": ["成都", "ZUUU", "ctu", "cd", "shuangliu", "双流"], - "chongqing": ["重庆", "ZUCK", "ckg", "cq", "jiangbei", "江北"], - "wuhan": ["武汉", "ZHHH", "wuh", "wh", "tianhe", "天河"], - "shenzhen": ["深圳", "ZGSZ", "szx", "sz", "baoan", "宝安"], - "guangzhou": ["广州", "ZGGG", "can", "gz", "baiyun", "白云"], - "qingdao": ["青岛", "青島", "ZSQD", "tao", "qdo", "jiaodong", "胶东"], - "hong kong": ["香港", "VHHH", "hkg", "hk", "observatory", "天文台"], - "lau fau shan": ["流浮山", "LFS", "lfs"], - "taipei": ["台北", "臺北", "RCSS", "tpe", "tp", "台湾", "臺灣"], - "singapore": ["新加坡", "WSSS", "sin", "sg", "changi", "樟宜"], - "kuala lumpur": ["吉隆坡", "WMKK", "kul", "sepang", "雪邦"], - "jakarta": ["雅加达", "雅加達", "WIHH", "jkt", "halim"], - "manila": ["马尼拉", "馬尼拉", "RPLL", "mnl", "ninoy"], - "karachi": ["卡拉奇", "OPKC", "khi", "jinnah", "真纳"], - "lucknow": ["勒克瑙", "VILK", "luc"], - "tel aviv": ["特拉维夫", "LLBG", "tlv", "ben gurion"], - "jeddah": ["吉达", "吉達", "OEJN", "jed", "king abdulaziz", "阿卜杜勒阿齐兹"], - "istanbul": ["伊斯坦布尔", "LTFM", "ist", "ltfm"], - "ankara": ["安卡拉", "LTAC", "ank", "esenboğa"], - "london": ["伦敦", "EGLC", "lon", "city airport"], - "paris": ["巴黎", "LFPB", "par", "le bourget"], - "moscow": ["莫斯科", "UUWW", "mos", "mow", "vnukovo"], - "munich": ["慕尼黑", "EDDM", "mun"], - "milan": ["米兰", "米蘭", "LIMC", "mil", "mxp", "malpensa", "马尔彭萨"], - "warsaw": ["华沙", "華沙", "EPWA", "waw", "war", "chopin", "肖邦"], - "helsinki": ["赫尔辛基", "赫爾辛基", "EFHK", "hel", "vantaa"], - "amsterdam": ["阿姆斯特丹", "EHAM", "ams", "schiphol", "史基浦"], - "madrid": ["马德里", "馬德里", "LEMD", "mad", "barajas"], - "new york": ["纽约", "KLGA", "nyc", "ny", "laguardia"], - "los angeles": ["洛杉矶", "KLAX", "la", "lax"], - "san francisco": ["旧金山", "KSFO", "sf", "sfo"], - "denver": ["丹佛", "奥罗拉", "KBKF", "aur", "buckley"], - "austin": ["奥斯汀", "KAUS", "aus"], - "houston": ["休斯顿", "KHOU", "hou", "hobby"], - "chicago": ["芝加哥", "KORD", "chi", "ohare"], - "dallas": ["达拉斯", "KDAL", "dal", "love field"], - "miami": ["迈阿密", "KMIA", "mia"], - "atlanta": ["亚特兰大", "KATL", "atl", "hartsfield"], - "seattle": ["西雅图", "KSEA", "sea", "seatac"], - "toronto": ["多伦多", "CYYZ", "tor", "pearson"], - "mexico city": ["墨西哥城", "MMMX", "cdmx"], - "panama city": ["巴拿马城", "MPMG", "pty"], - "buenos aires": ["布宜诺斯艾利斯", "SAEZ", "ba", "ezeiza"], - "são paulo": ["圣保罗", "SBGR", "sp", "guarulhos"], -"cape town": ["开普敦", "開普敦", "FACT", "cpt"], - "wellington": ["惠灵顿", "NZWN", "wel"], -}; - -function normalizeCityKey(name: string): string { - return String(name || "").trim().toLowerCase(); -} - -function matchesCitySearch( - cityKey: string, - displayName: string, - query: string, -): boolean { - const q = query.trim().toLowerCase(); - if (!q) return true; - if (cityKey.includes(q)) return true; - if (displayName.toLowerCase().includes(q)) return true; - const aliases = CITY_SEARCH_INDEX[cityKey]; - if (!aliases) return false; - return aliases.some((alias) => alias.toLowerCase().includes(q)); -} - -function pickCityRow( - rows: ScanOpportunityRow[], - cityKey: string, -): ScanOpportunityRow | null { - return ( - rows.find( - (row) => normalizeCityKey(row.city) === cityKey, - ) || null - ); -} - -export function MobileCityPicker({ - isEn, - rows, - onSelectCity, -}: { - isEn: boolean; - rows: ScanOpportunityRow[]; - onSelectCity: (row: ScanOpportunityRow) => void; -}) { - const [searchQuery, setSearchQuery] = useState(""); - const [expandedRegions, setExpandedRegions] = useState>( - () => new Set(CITY_REGION_GROUPS.map((g) => g.key)), - ); - - const filteredGroups = useMemo(() => { - const q = searchQuery.trim().toLowerCase(); - return CITY_REGION_GROUPS - .map((group) => { - const matched = group.cities.filter((cityKey) => { - const row = pickCityRow(rows, cityKey); - if (!row) return false; - const display = - row.city_display_name || row.display_name || cityKey; - return matchesCitySearch(cityKey, display, q); - }); - return { ...group, matched }; - }) - .filter((group) => group.matched.length > 0); - }, [rows, searchQuery]); - - const totalMatched = useMemo( - () => filteredGroups.reduce((sum, g) => sum + g.matched.length, 0), - [filteredGroups], - ); - - const toggleRegion = (key: string) => { - setExpandedRegions((prev) => { - const next = new Set(prev); - if (next.has(key)) { - next.delete(key); - } else { - next.add(key); - } - return next; - }); - }; - - return ( -
-
- - setSearchQuery(e.target.value)} - /> - {searchQuery ? ( - - ) : null} -
- - {searchQuery ? ( -
- {isEn - ? `${totalMatched} city${totalMatched !== 1 ? "ies" : "y"} found` - : `找到 ${totalMatched} 个城市`} -
- ) : null} - - {filteredGroups.length === 0 ? ( -
- {isEn ? "No cities match your search" : "没有匹配的城市"} -
- ) : ( -
- {filteredGroups.map((group) => { - const isExpanded = expandedRegions.has(group.key) || !!searchQuery; - return ( -
- - {isExpanded ? ( -
- {group.matched.map((cityKey) => { - const row = pickCityRow(rows, cityKey); - if (!row) return null; - const display = - row.city_display_name || - row.display_name || - cityKey; - const currentTemp = - row.current_temp ?? row.current_max_so_far ?? null; - const deb = row.deb_prediction ?? null; - const tempUnit = row.temp_symbol || "°C"; - return ( - - ); - })} -
- ) : null} -
- ); - })} -
- )} -
- ); -} diff --git a/frontend/components/dashboard/scan-terminal/ScanTerminalShellParts.tsx b/frontend/components/dashboard/scan-terminal/ScanTerminalShellParts.tsx index a693c809..9b8b94d7 100644 --- a/frontend/components/dashboard/scan-terminal/ScanTerminalShellParts.tsx +++ b/frontend/components/dashboard/scan-terminal/ScanTerminalShellParts.tsx @@ -4,7 +4,6 @@ import clsx from "clsx"; import Link from "next/link"; import type { Dispatch, SetStateAction } from "react"; import { LogIn, UserRound } from "lucide-react"; -import { ProFeaturePaywall } from "@/components/dashboard/ProFeaturePaywall"; import { LoadingSignal } from "@/components/dashboard/scan-terminal/LoadingSignal"; import type { Locale } from "@/lib/i18n"; @@ -137,27 +136,3 @@ export function ScanTerminalTopBar({ ); } - -export function ScanPaywallModal({ - isEn, - onClose, -}: { - isEn: boolean; - onClose: () => void; -}) { - return ( -
{ - if (event.target === event.currentTarget) { - onClose(); - } - }} - > - -
- ); -} diff --git a/frontend/components/dashboard/scan-terminal/WelcomeOverlay.tsx b/frontend/components/dashboard/scan-terminal/WelcomeOverlay.tsx deleted file mode 100644 index 65ab8d35..00000000 --- a/frontend/components/dashboard/scan-terminal/WelcomeOverlay.tsx +++ /dev/null @@ -1,120 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { MapPin, FileText, Crown } from "lucide-react"; - -const STORAGE_KEY = "polyweather_welcome_seen"; - -type Props = { - locale: string; - onDismiss: () => void; -}; - -const steps = [ - { - icon: MapPin, - titleEn: "Pick a city on the map", - titleZh: "从地图选城市", - descEn: "Click any colored bubble to view real-time weather and forecasts.", - descZh: "点击地图上的彩色气泡,查看实时天气和预报。", - }, - { - icon: FileText, - titleEn: "Read the city briefing", - titleZh: "查看城市简报", - descEn: "The right panel shows current temperature, DEB forecast, and detailed metrics.", - descZh: "右侧面板展示当前温度、DEB 预报和各项指标。", - }, - { - icon: Crown, - titleEn: "Unlock Pro for deep analysis", - titleZh: "解锁 Pro 深入分析", - descEn: "Pro unlocks AI-powered METAR reads, model evidence, market layer, and intraday analysis.", - descZh: "Pro 解锁 AI 机场报文解读、模型证据、市场层、日内分析。", - }, -]; - -export function WelcomeOverlay({ locale, onDismiss }: Props) { - const [visible, setVisible] = useState(false); - const [step, setStep] = useState(0); - const isEn = locale === "en-US"; - - useEffect(() => { - const seen = localStorage.getItem(STORAGE_KEY); - if (!seen) { - setVisible(true); - } - }, []); - - if (!visible) return null; - - const current = steps[step]; - const isLast = step === steps.length - 1; - - const handleDismiss = () => { - localStorage.setItem(STORAGE_KEY, "1"); - setVisible(false); - onDismiss(); - }; - - const handleNext = () => { - if (isLast) { - handleDismiss(); - } else { - setStep(step + 1); - } - }; - - return ( -
-
e.stopPropagation()} - > -
- {steps.map((_, i) => ( - - ))} -
- -
- -
- -

- {isEn ? current.titleEn : current.titleZh} -

-

- {isEn ? current.descEn : current.descZh} -

- -
- - -
-
-
- ); -} diff --git a/frontend/public/android-chrome-192x192.png b/frontend/public/android-chrome-192x192.png index 2680e797..be7794d2 100644 Binary files a/frontend/public/android-chrome-192x192.png and b/frontend/public/android-chrome-192x192.png differ diff --git a/frontend/public/android-chrome-512x512.png b/frontend/public/android-chrome-512x512.png index c009d57c..ff12a0a2 100644 Binary files a/frontend/public/android-chrome-512x512.png and b/frontend/public/android-chrome-512x512.png differ diff --git a/frontend/public/apple-touch-icon.png b/frontend/public/apple-touch-icon.png index d2880cd8..98694810 100644 Binary files a/frontend/public/apple-touch-icon.png and b/frontend/public/apple-touch-icon.png differ diff --git a/frontend/public/favicon-16x16.png b/frontend/public/favicon-16x16.png index 79d399ac..3c108b49 100644 Binary files a/frontend/public/favicon-16x16.png and b/frontend/public/favicon-16x16.png differ diff --git a/frontend/public/favicon-32x32.png b/frontend/public/favicon-32x32.png index befc4488..7b05c647 100644 Binary files a/frontend/public/favicon-32x32.png and b/frontend/public/favicon-32x32.png differ diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico index 0d37a4b3..065cf824 100644 Binary files a/frontend/public/favicon.ico and b/frontend/public/favicon.ico differ diff --git a/frontend/public/site.webmanifest b/frontend/public/site.webmanifest index 8bb8939a..89d0299b 100644 --- a/frontend/public/site.webmanifest +++ b/frontend/public/site.webmanifest @@ -1 +1 @@ -{"background_color":"#ffffff","display":"standalone","icons":[{"sizes":"192x192","src":"/android-chrome-192x192.png","type":"image/png"},{"sizes":"512x512","src":"/android-chrome-512x512.png","type":"image/png"}],"name":"","short_name":"","theme_color":"#ffffff"} \ No newline at end of file +{"background_color":"#ffffff","display":"standalone","icons":[{"sizes":"192x192","src":"/android-chrome-192x192.png","type":"image/png"},{"sizes":"512x512","src":"/android-chrome-512x512.png","type":"image/png"}],"name":"PolyWeather","short_name":"PolyWeather","theme_color":"#171d24"} \ No newline at end of file