Add landing supported cities section
This commit is contained in:
@@ -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 }) {
|
||||
<a href="#screenshots" className="hover:text-slate-950">
|
||||
{isEn ? "Screens" : "截图"}
|
||||
</a>
|
||||
<a href="#supported-cities" className="hover:text-slate-950">
|
||||
{isEn ? "Cities" : "城市"}
|
||||
</a>
|
||||
<Link href="/docs/chart-guide" className="hover:text-slate-950">
|
||||
{isEn ? "Guide" : "读图"}
|
||||
</Link>
|
||||
@@ -664,6 +715,70 @@ function InstitutionalLandingScreen({ locale }: { locale: LandingLocale }) {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="supported-cities" className="scroll-mt-16 border-t border-slate-200 bg-[#fbfbfa] px-4 py-20 sm:px-6">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="grid gap-8 lg:grid-cols-[0.72fr_1.28fr] lg:items-start">
|
||||
<div>
|
||||
<p className="text-xs font-bold uppercase tracking-[0.18em] text-slate-400">
|
||||
{isEn ? "Supported cities" : "当前支持城市"}
|
||||
</p>
|
||||
<h2 className="mt-3 text-3xl font-black tracking-tight text-slate-950 sm:text-4xl">
|
||||
{isEn
|
||||
? `${supportedCityCount} supported temperature markets`
|
||||
: `目前支持 ${supportedCityCount} 个温度市场城市`}
|
||||
</h2>
|
||||
<p className="mt-4 text-sm leading-7 text-slate-600">
|
||||
{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."
|
||||
: "这里复用终端同一份城市列表生成。未列出的市场可以联系评估结算站、数据源频率和提醒工作流。"}
|
||||
</p>
|
||||
<Link
|
||||
href="/terminal"
|
||||
className="mt-6 inline-flex h-10 items-center justify-center gap-2 rounded-md border border-slate-200 bg-white px-3 text-sm font-bold text-slate-700 shadow-sm hover:border-slate-300 hover:text-slate-950"
|
||||
>
|
||||
{isEn ? "Open terminal" : "进入终端"}
|
||||
<LandingIcon name="arrow" size={15} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-3">
|
||||
{supportedCityGroups.map((group) => (
|
||||
<article key={group.labelEn} className="rounded-lg border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-base font-black text-slate-950">
|
||||
{isEn ? group.labelEn : group.labelZh}
|
||||
</h3>
|
||||
<p className="mt-2 text-xs leading-5 text-slate-500">
|
||||
{isEn ? group.descriptionEn : group.descriptionZh}
|
||||
</p>
|
||||
</div>
|
||||
<span className="shrink-0 rounded-md border border-slate-200 bg-[#fbfbfa] px-2 py-1 font-mono text-xs font-black text-slate-700">
|
||||
{group.cities.length}
|
||||
</span>
|
||||
</div>
|
||||
<ul className="mt-5 space-y-2">
|
||||
{group.cities.map((city) => (
|
||||
<li
|
||||
key={city.name}
|
||||
className="flex min-h-9 items-center justify-between gap-3 border-b border-slate-100 pb-2 last:border-b-0 last:pb-0"
|
||||
>
|
||||
<span className="min-w-0 truncate text-sm font-semibold text-slate-800">
|
||||
{cityDisplayName(city)}
|
||||
</span>
|
||||
<span className="shrink-0 font-mono text-[11px] font-bold text-slate-400">
|
||||
{city.icao}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user