diff --git a/frontend/components/landing/InstitutionalLandingPage.tsx b/frontend/components/landing/InstitutionalLandingPage.tsx index 94a3576d..64cedcf3 100644 --- a/frontend/components/landing/InstitutionalLandingPage.tsx +++ b/frontend/components/landing/InstitutionalLandingPage.tsx @@ -10,6 +10,8 @@ import { pickLandingLocale, type LandingLocale, } from "@/components/landing/landingLocale"; +import type { CityListItem } from "@/lib/dashboard-types"; +import { STATIC_CITY_LIST } from "@/lib/static-cities"; const COVERAGE_EN = [ "AMOS 60s runway sensors", @@ -47,6 +49,52 @@ const PRO_FEATURES_ZH = [ "订阅与准入问题优先支持", ]; +const SUPPORTED_CITY_GROUPS: Array<{ + descriptionEn: string; + descriptionZh: string; + include: (city: CityListItem) => boolean; + labelEn: string; + labelZh: string; +}> = [ + { + labelEn: "Asia-Pacific", + labelZh: "亚太", + descriptionEn: "China, East Asia, Southeast Asia, South Asia, and Oceania markets.", + descriptionZh: "中国、东亚、东南亚、南亚与大洋洲市场。", + include: (city) => city.lon >= 60 || city.lon <= -170, + }, + { + labelEn: "Europe / Middle East / Africa", + labelZh: "欧洲 / 中东 / 非洲", + descriptionEn: "Airport and official-station markets across EMEA.", + descriptionZh: "覆盖欧洲、中东和非洲的机场与官方站市场。", + include: (city) => city.lon > -30 && city.lon < 60, + }, + { + labelEn: "Americas", + labelZh: "美洲", + descriptionEn: "North and South American temperature markets.", + descriptionZh: "北美与南美温度市场。", + include: (city) => city.lon >= -170 && city.lon <= -30, + }, +]; + +const supportedCityGroups = SUPPORTED_CITY_GROUPS.map((group) => ({ + ...group, + cities: STATIC_CITY_LIST.filter(group.include).sort((left, right) => + cityDisplayName(left).localeCompare(cityDisplayName(right), "en"), + ), +})).filter((group) => group.cities.length > 0); + +const supportedCityCount = supportedCityGroups.reduce( + (total, group) => total + group.cities.length, + 0, +); + +function cityDisplayName(city: CityListItem) { + return city.display_name || city.name; +} + type IconName = | "radar" | "gauge" @@ -266,6 +314,9 @@ function InstitutionalLandingScreen({ locale }: { locale: LandingLocale }) { {isEn ? "Screens" : "截图"} + + {isEn ? "Cities" : "城市"} + {isEn ? "Guide" : "读图"} @@ -664,6 +715,70 @@ function InstitutionalLandingScreen({ locale }: { locale: LandingLocale }) { + +
+
+
+
+

+ {isEn ? "Supported cities" : "当前支持城市"} +

+

+ {isEn + ? `${supportedCityCount} supported temperature markets` + : `目前支持 ${supportedCityCount} 个温度市场城市`} +

+

+ {isEn + ? "Coverage is generated from the same city list used by the terminal. If your market is not listed, contact us to evaluate the settlement station, source cadence, and alert workflow." + : "这里复用终端同一份城市列表生成。未列出的市场可以联系评估结算站、数据源频率和提醒工作流。"} +

+ + {isEn ? "Open terminal" : "进入终端"} + + +
+ +
+ {supportedCityGroups.map((group) => ( +
+
+
+

+ {isEn ? group.labelEn : group.labelZh} +

+

+ {isEn ? group.descriptionEn : group.descriptionZh} +

+
+ + {group.cities.length} + +
+
    + {group.cities.map((city) => ( +
  • + + {cityDisplayName(city)} + + + {city.icao} + +
  • + ))} +
+
+ ))} +
+
+
+
); diff --git a/frontend/components/landing/__tests__/landingPricingReferral.test.ts b/frontend/components/landing/__tests__/landingPricingReferral.test.ts index 9e93722e..a0947d2f 100644 --- a/frontend/components/landing/__tests__/landingPricingReferral.test.ts +++ b/frontend/components/landing/__tests__/landingPricingReferral.test.ts @@ -23,6 +23,10 @@ export function runTests() { path.join(root, "components", "landing", "LandingLocaleToggle.tsx"), "utf8", ); + const staticCitiesSource = fs.readFileSync( + path.join(root, "lib", "static-cities.ts"), + "utf8", + ); const appPageSource = fs.readFileSync(path.join(root, "app", "page.tsx"), "utf8"); const publicPngPath = path.join(root, "public", "static", "web.png"); const fixturePngPath = path.join(root, "components", "landing", "__tests__", "fixtures", "web.png"); @@ -65,6 +69,24 @@ export function runTests() { assert(source.includes("/static/web.webp"), "landing page must load the lighter WebP product preview image"); assert(source.includes("/static/tel.png"), "landing page must include the Telegram alert screenshot"); assert(source.includes("#screenshots"), "landing navigation must expose the product screenshot section"); + assert(source.includes("#supported-cities"), "landing navigation must expose the supported cities section"); + assert(source.includes('id="supported-cities"'), "landing page must include a supported cities section"); + assert(source.includes("当前支持城市"), "landing page must tell Chinese users which cities are currently supported"); + assert(source.includes("Supported cities"), "landing page must tell English users which cities are currently supported"); + assert(source.includes('from "@/lib/static-cities"'), "landing supported cities must reuse the static city list instead of hand-writing coverage"); + assert(source.includes("SUPPORTED_CITY_GROUPS"), "landing supported cities must group coverage by region for scanning"); + assert( + staticCitiesSource.includes('"display_name": "Hong Kong"') && + staticCitiesSource.includes('"display_name": "New York"') && + staticCitiesSource.includes('"display_name": "Shanghai"'), + "static city fallback must include representative supported markets", + ); + assert( + source.includes("group.cities.map") && + source.includes("cityDisplayName(city)") && + source.includes("{city.icao}"), + "landing supported cities must render names and station codes from the generated city groups", + ); assert(source.includes("结算源优先") && source.includes("差异化卖点"), "landing page must explain the differentiated settlement-source positioning"); assert(!source.includes('src="/static/web.png"'), "landing hero must not use the heavy PNG as its primary LCP image"); assert(