统一网站页面标题横幅并完成相关页面的设计适配

This commit is contained in:
2026-09-24 18:56:38 +08:00
parent 19efd87d0b
commit 8968251b94
20 changed files with 309 additions and 110 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

+15
View File
@@ -0,0 +1,15 @@
import { notFound } from "next/navigation";
import { AboutPage } from "@/components/about/about-page";
import { getLocaleMessages, isLocale } from "@/lib/i18n";
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
if (!isLocale(locale)) notFound();
return { title: getLocaleMessages(locale).footer.columns[1].links[0] };
}
export default async function AboutRoute({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
if (!isLocale(locale)) notFound();
return <AboutPage locale={locale} />;
}
+15
View File
@@ -0,0 +1,15 @@
import { notFound } from "next/navigation";
import { SimpleTitlePage } from "@/components/pages/simple-title-page";
import { getLocaleMessages, isLocale } from "@/lib/i18n";
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
if (!isLocale(locale)) notFound();
return { title: getLocaleMessages(locale).header.nav[3] };
}
export default async function CityPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
if (!isLocale(locale)) notFound();
return <SimpleTitlePage locale={locale} title={getLocaleMessages(locale).header.nav[3]} image="/city/city-hero.png" imagePosition="47% 24%" />;
}
+16
View File
@@ -0,0 +1,16 @@
import { notFound } from "next/navigation";
import { SimpleTitlePage } from "@/components/pages/simple-title-page";
import { getLocaleMessages, isLocale } from "@/lib/i18n";
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
if (!isLocale(locale)) notFound();
return { title: getLocaleMessages(locale).footer.columns[1].links[2] };
}
export default async function JoinUsPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
if (!isLocale(locale)) notFound();
const messages = getLocaleMessages(locale);
return <SimpleTitlePage locale={locale} title={messages.footer.columns[1].links[2]} image="/pages/join-hero.png" imagePosition="center" />;
}
+1 -1
View File
@@ -22,5 +22,5 @@ export default async function PolicyPage({ params }: { params: Promise<{ slug: s
if (!isLocale(locale)) notFound();
const catalog = await getPolicyCatalog(slug);
if (!catalog["zh-Hans"]) notFound();
return <main className="policy-page"><div className="policy-page-inner"><PolicyPageContent catalog={catalog} /></div></main>;
return <PolicyPageContent catalog={catalog} />;
}
+15
View File
@@ -0,0 +1,15 @@
import { notFound } from "next/navigation";
import { SimpleTitlePage } from "@/components/pages/simple-title-page";
import { getLocaleMessages, isLocale } from "@/lib/i18n";
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
if (!isLocale(locale)) notFound();
return { title: getLocaleMessages(locale).header.nav[2] };
}
export default async function RailwayPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
if (!isLocale(locale)) notFound();
return <SimpleTitlePage locale={locale} title={getLocaleMessages(locale).header.nav[2]} image="/pages/railway-hero.png" imagePosition="47% 24%" />;
}
+58 -38
View File
@@ -74,19 +74,29 @@ a { color: inherit; text-decoration: none; }
.home-collage-small { top: 376px; left: 27.28%; width: 44.3%; height: 272px; background-image: url("/home/collage-3.png"); }
.home-collage-bottom { top: 376px; left: 0; width: 25.83%; height: 272px; background-image: url("/home/collage-4.png"); }
.home-collage-tall { top: 0; right: 0; width: 27.1%; height: 648px; background-image: url("/home/collage-5.png"); }
.home-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 50px; margin-top: 100px; padding-bottom: 121px; }
.home-action-card { display: block; min-width: 0; color: var(--ink); }
.home-action-image { height: 323px; border-radius: 13px; background: center / cover no-repeat; transition: transform .2s ease, box-shadow .2s ease; }
.home-action-image--about { background-image: url("/home/about-card.png"); }
.home-action-image--join { background-image: url("/home/join-card.png"); }
.home-action-card > span { display: flex; align-items: center; gap: 20px; margin-top: 42px; font-size: 42px; font-weight: 700; line-height: 1.2; letter-spacing: -.04em; }
.home-action-card b { color: var(--blue); font-family: Arial, sans-serif; font-size: 36px; font-weight: 400; line-height: 1; }
.home-action-card:hover .home-action-image, .home-action-card:focus-visible .home-action-image { box-shadow: 0 10px 25px rgba(15, 23, 42, .16); transform: translateY(-3px); }
.page-action-cards { display: grid; width: min(1200px, calc(100% - 32px)); grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 50px; margin-right: auto; margin-left: auto; padding-bottom: 121px; }
.home-actions { margin-top: 100px; }
.page-action-card { display: block; min-width: 0; color: var(--ink); }
.page-action-image { height: 323px; border-radius: 13px; background-position: center; background-size: cover; background-repeat: no-repeat; transition: transform .2s ease, box-shadow .2s ease; }
.page-action-card > span { display: flex; align-items: center; gap: 20px; margin-top: 42px; font-size: 42px; font-weight: 700; line-height: 1.2; letter-spacing: -.04em; }
.page-action-card b { color: var(--blue); font-family: Arial, sans-serif; font-size: 36px; font-weight: 400; line-height: 1; }
.page-action-card:hover .page-action-image, .page-action-card:focus-visible .page-action-image { box-shadow: 0 10px 25px rgba(15, 23, 42, .16); transform: translateY(-3px); }
.title-page { padding-bottom: 48px; }
.page-title-hero { position: relative; display: flex; width: min(1200px, calc(100% - 32px)); height: 414px; align-items: flex-start; margin: 0 auto; padding-top: 156px; }
.page-title-hero-media { position: absolute; inset: 0; overflow: hidden; border-radius: 10px; background-image: var(--page-title-hero-image); background-position: var(--page-title-hero-position); background-size: cover; background-repeat: no-repeat; }
.page-title-hero-label { position: relative; z-index: 1; margin-left: -44px; padding: 10px 46px; background: #8f56a2; color: #fff; }
.page-title-hero-label h1 { margin: 0; font-size: 62px; font-weight: 700; line-height: 80.4px; letter-spacing: .25px; }
.page-location { display: flex; width: min(1200px, calc(100% - 40px)); height: 88.8px; align-items: center; margin: 0 auto; border-top: .8px solid #d9d9d9; }
.about-introduction { display: grid; width: min(1200px, calc(100% - 32px)); min-height: 512px; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 50px auto 0; overflow: hidden; border-radius: 10px; }
.about-introduction-copy { display: flex; flex-direction: column; justify-content: center; padding: 64px 80px; background: #535360; color: #fff; }
.about-introduction-copy p { align-self: flex-start; margin: 0 0 39px; padding: 4px 10px; background: #1267d8; color: #fff; font-size: 18px; font-weight: 700; line-height: 28px; }
.about-introduction-copy h2 { margin: 0; font-size: 40px; font-weight: 700; line-height: 1.28; letter-spacing: -.04em; }
.about-introduction-image { min-height: 512px; background: url("/pages/about-intro-overlay.png") center / cover no-repeat; }
.about-action-cards { margin-top: 50px; }
.news-page { padding: 30px 0 60px; }
.friendly-links-hero { background: var(--policy-hero-image) center 28.2% / 100% auto no-repeat; }
.friendly-links-page-inner { gap: 50px; }
.friendly-links-content { width: min(1214px, 100%); margin: 0 auto; }
.friendly-links-content { width: min(1200px, calc(100% - 32px)); margin: 50px auto 0; }
.friendly-links-section + .friendly-links-section { margin-top: 50px; }
.friendly-links-section h2 { margin: 0 0 20px; color: #181c20; font-size: 40px; font-weight: 700; line-height: 48px; }
.friendly-links-section h2::after { display: block; width: 28.875px; height: 5.25px; margin-top: 17.5px; background: #8f56a2; content: ""; }
@@ -99,10 +109,7 @@ a.friendly-link-card:hover, a.friendly-link-card:focus-visible { border-color: #
.friendly-link-card-logo img { display: block; width: 65.67px; height: 65.67px; object-fit: cover; }
.friendly-link-card-description { color: #8d949a; font-size: 16.42px; line-height: 21.89px; text-align: center; }
.friendly-link-card-archive { position: absolute; top: 13px; right: 5px; padding: 4px 6px; border-radius: 4px; background: #e72329; color: #fff; font-size: 10px; line-height: 12px; }
.friendly-links-page .page-bottom-nav { margin-top: 176px; }
.members-page-inner { gap: 50px; }
.members-hero { background: var(--policy-hero-image) center 47% / 100% auto no-repeat; }
.members-sections { width: min(1200px, 100%); }
.members-sections { width: min(1200px, calc(100% - 32px)); margin: 50px auto 0; }
.member-section + .member-section { margin-top: 46px; }
.member-section-heading { margin: 0 0 20px; }
.member-section-heading h2 { margin: 0; color: #202326; font-size: 30px; font-weight: 700; line-height: 1.35; letter-spacing: -.03em; }
@@ -117,7 +124,6 @@ a.member-card:hover, a.member-card:focus-visible { opacity: .82; transform: tran
.member-card-copy h3 { margin: 0; font-size: 20px; font-weight: 700; line-height: 1.4; letter-spacing: -.03em; }
.member-card-copy p { margin: 4px 0 0; color: #5f6464; font-size: 14px; line-height: 1.45; }
.member-empty { margin: 0; padding: 40px 0; color: #697684; font-size: 16px; }
.members-page .page-bottom-nav { margin-top: 80px; }
.news-page-inner { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
.news-page-header { display: flex; justify-content: center; align-items: center; margin: 0 0 30px; }
.news-page-header h1 { margin: 0; font-size: clamp(52px, 5vw, 88px); letter-spacing: -0.07em; line-height: 1; font-weight: 800; }
@@ -165,12 +171,8 @@ a.member-card:hover, a.member-card:focus-visible { opacity: .82; transform: tran
.news-detail-body li { margin: 8px 0; }
.news-detail-body a, .policy-body a { color: #1267d8; text-decoration: underline; text-underline-offset: 3px; }
.news-detail-body a:hover, .policy-body a:hover { color: #8f56a2; }
.policy-page { padding: 0 0 48px; background: #fff; }
.policy-page-inner { display: flex; flex-direction: column; gap: 50px; align-items: center; padding: 0 40px 48px; }
.policy-hero { width: min(1200px, 100%); min-height: 322px; display: flex; align-items: flex-start; padding: 156px 48px 0; border-radius: 10px; background: linear-gradient(110deg, rgba(143, 86, 162, 0.82), rgba(143, 86, 162, 0.18)), var(--policy-hero-image, url("/policy-hero.png")) center / cover; overflow: hidden; }
.policy-hero-inner { margin-left: -48px; padding: 10px 46px; background: #8f56a2; }
.policy-hero h1 { margin: 0; color: #fff; font-size: clamp(38px, 4vw, 62px); line-height: 1.3; letter-spacing: 0.25px; }
.policy-card { width: min(1200px, 100%); padding: 28px; border-radius: 26px; background: #fefdfa; box-shadow: 0 10px 15px rgba(15, 23, 42, 0.04); }
.policy-page { background: var(--paper); }
.policy-card { width: min(1200px, calc(100% - 32px)); margin: 50px auto 0; padding: 28px; border-radius: 26px; background: #fefdfa; box-shadow: 0 10px 15px rgba(15, 23, 42, 0.04); }
.policy-ai-notice { display: inline-flex; margin: 0 0 26px; padding: 6px 12px; border-radius: 999px; background: rgba(255, 179, 15, 0.1); color: #ff8f0f; font-size: 13px; line-height: 1.5; }
.policy-body { color: #1d2732; font-size: 17px; line-height: 1.9; }
.policy-body h1, .policy-body h2, .policy-body h3 { color: #8f56a2; font-weight: 400; }
@@ -263,6 +265,8 @@ html[data-theme="dark"] .news-detail-body, html[data-theme="dark"] .policy-body
html[data-theme="dark"] .policy-page { background: var(--paper); }
html[data-theme="dark"] .policy-card { background: #19222d; box-shadow: 0 10px 15px rgba(0, 0, 0, 0.2); }
html[data-theme="dark"] .page-bottom-nav { border-color: #394553; }
html[data-theme="dark"] .page-location { border-color: #394553; }
html[data-theme="dark"] .about-introduction-copy { background: #1a222d; }
html[data-theme="dark"] .page-breadcrumb, html[data-theme="dark"] .page-breadcrumb-separator, html[data-theme="dark"] .page-breadcrumb a { color: #cbd5df; }
html[data-theme="dark"] .news-share { border-color: rgba(255, 255, 255, 0.14); }
html[data-theme="dark"] .status-code, html[data-theme="dark"] .status-content h1 { color: var(--ink); }
@@ -307,6 +311,8 @@ html[data-theme="dark"] .member-card-copy p, html[data-theme="dark"] .member-emp
html:not([data-theme]) .policy-page { background: var(--paper); }
html:not([data-theme]) .policy-card { background: #19222d; box-shadow: 0 10px 15px rgba(0, 0, 0, 0.2); }
html:not([data-theme]) .page-bottom-nav { border-color: #394553; }
html:not([data-theme]) .page-location { border-color: #394553; }
html:not([data-theme]) .about-introduction-copy { background: #1a222d; }
html:not([data-theme]) .page-breadcrumb, html:not([data-theme]) .page-breadcrumb-separator, html:not([data-theme]) .page-breadcrumb a { color: #cbd5df; }
html:not([data-theme]) .news-share { border-color: rgba(255, 255, 255, 0.14); }
html:not([data-theme]) .status-code, html:not([data-theme]) .status-content h1 { color: var(--ink); }
@@ -364,18 +370,25 @@ html[data-theme="dark"] .member-card-copy p, html[data-theme="dark"] .member-emp
.home-collage-small { top: 202px; left: 39%; width: 61%; height: 158px; }
.home-collage-tall { display: none; }
.home-news { margin-top: 48px; }
.home-actions { width: calc(100% - 32px); grid-template-columns: 1fr; gap: 42px; margin-top: 48px; padding-bottom: 48px; }
.home-action-image { height: auto; aspect-ratio: 575 / 323; }
.home-action-card > span { margin-top: 24px; font-size: clamp(32px, 7vw, 42px); }
.page-action-cards { width: calc(100% - 32px); grid-template-columns: 1fr; gap: 42px; padding-bottom: 48px; }
.home-actions, .about-action-cards { margin-top: 48px; }
.page-action-image { height: auto; aspect-ratio: 575 / 323; }
.page-action-card > span { margin-top: 24px; font-size: clamp(32px, 7vw, 42px); }
.title-page { padding-bottom: 32px; }
.page-title-hero { width: calc(100% - 32px); height: clamp(300px, 46vw, 414px); padding-top: clamp(104px, 17.3vw, 156px); }
.page-title-hero-label { margin-left: -20px; padding: 8px 32px; }
.page-title-hero-label h1 { font-size: clamp(40px, 6.88vw, 62px); line-height: 1.3; }
.page-location { width: calc(100% - 32px); min-height: 80px; height: auto; padding: 24px 0; }
.about-introduction { width: calc(100% - 32px); min-height: 420px; margin-top: 48px; }
.about-introduction-copy { padding: 48px; }
.about-introduction-copy h2 { font-size: clamp(32px, 4.45vw, 40px); }
.about-introduction-image { min-height: 420px; }
.news-page-inner { padding: 0 16px; }
.friendly-links-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.news-page-header h1 { font-size: clamp(46px, 12vw, 72px); }
.news-toolbar { align-items: flex-start; flex-wrap: wrap; gap: 16px; }
.news-toolbar-right { margin-left: auto; }
.policy-page-inner { gap: 28px; padding: 0 16px 32px; }
.policy-hero { min-height: 220px; padding: 90px 28px 0; }
.policy-hero-inner { margin-left: -28px; padding: 8px 28px; }
.policy-card { padding: 20px; border-radius: 20px; }
.policy-card { width: calc(100% - 32px); margin-top: 48px; padding: 20px; border-radius: 20px; }
.policy-body { font-size: 16px; }
.policy-body h1 { font-size: 38px; }
.policy-body h2 { margin-top: 32px; font-size: 36px; }
@@ -433,12 +446,23 @@ html[data-theme="dark"] .member-card-copy p, html[data-theme="dark"] .member-emp
.home-collage { width: calc(100% - 24px); height: 280px; margin-top: 36px; }
.home-collage-wide, .home-collage-medium { height: 148px; }
.home-collage-bottom, .home-collage-small { top: 160px; height: 120px; }
.home-actions { width: calc(100% - 24px); gap: 32px; margin-top: 36px; padding-bottom: 24px; }
.home-action-card > span { gap: 14px; margin-top: 18px; font-size: 30px; }
.home-action-card b { font-size: 30px; }
.page-action-cards { width: calc(100% - 24px); gap: 32px; padding-bottom: 24px; }
.home-actions, .about-action-cards { margin-top: 36px; }
.page-action-card > span { gap: 14px; margin-top: 18px; font-size: 30px; }
.page-action-card b { font-size: 30px; }
.title-page { padding-bottom: 24px; }
.page-title-hero { width: calc(100% - 24px); height: 290px; padding-top: 96px; }
.page-title-hero-label { margin-left: -12px; padding: 8px 26px; }
.page-title-hero-label h1 { font-size: clamp(34px, 10vw, 42px); line-height: 1.3; }
.page-location { width: calc(100% - 24px); min-height: 72px; padding: 20px 0; }
.about-introduction { width: calc(100% - 24px); grid-template-columns: 1fr; min-height: 0; margin-top: 36px; }
.about-introduction-copy { min-height: 280px; padding: 42px 28px; }
.about-introduction-copy p { margin-bottom: 24px; }
.about-introduction-copy h2 { font-size: clamp(30px, 8.5vw, 36px); }
.about-introduction-image { min-height: 280px; }
.news-page { padding-top: 20px; }
.friendly-links-content { width: 100%; }
.friendly-links-content, .members-sections { width: calc(100% - 24px); margin-top: 36px; }
.friendly-links-section + .friendly-links-section { margin-top: 36px; }
.friendly-links-section h2 { margin-bottom: 16px; font-size: 30px; line-height: 1.35; }
.friendly-links-grid { grid-template-columns: 1fr; gap: 12px; }
@@ -474,11 +498,7 @@ html[data-theme="dark"] .member-card-copy p, html[data-theme="dark"] .member-emp
.status-content h1 { overflow-wrap: anywhere; }
.status-back-button { font-size: 24px; }
.policy-page-inner { gap: 20px; padding: 0 12px 24px; }
.policy-hero { min-height: 170px; padding: 64px 20px 0; border-radius: 8px; }
.policy-hero-inner { margin-left: -20px; padding: 7px 20px; max-width: 100%; }
.policy-hero h1 { font-size: 34px; overflow-wrap: anywhere; }
.policy-card { padding: 16px; border-radius: 18px; }
.policy-card { width: calc(100% - 24px); margin-top: 36px; padding: 16px; border-radius: 18px; }
.policy-body { font-size: 16px; line-height: 1.8; overflow-wrap: anywhere; }
.policy-body h1 { font-size: 34px; }
.policy-body h2 { margin: 30px 0 18px; font-size: 34px; }
+47
View File
@@ -0,0 +1,47 @@
import { PageActionCards } from "@/components/layout/page-action-cards";
import { PageLocation } from "@/components/layout/page-location";
import { PageTitleHero } from "@/components/layout/page-title-hero";
import { getLocaleMessages, type Locale } from "@/lib/i18n";
const introduction: Record<Locale, { label: string; title: [string, string, string] }> = {
"zh-Hans": { label: "小简介", title: ["铁道+建筑+生存", "一个以高兴就好的", "小服务器"] },
"zh-Hant": { label: "小簡介", title: ["鐵道+建築+生存", "一個以開心為主的", "小伺服器"] },
en: { label: "A short introduction", title: ["Railways, building", "and survival — a small", "server built for joy."] },
jp: { label: "紹介", title: ["鉄道・建築・サバイバル", "楽しさを大切にする", "小さなサーバーです"] },
};
export function AboutPage({ locale }: { locale: Locale }) {
const messages = getLocaleMessages(locale);
const title = messages.footer.columns[1].links[0];
const copy = introduction[locale];
return (
<>
<main className="title-page about-page">
<PageTitleHero title={title} image="/pages/about-hero.png" imagePosition="center 52%" />
<section className="about-introduction" aria-labelledby="about-introduction-title">
<div className="about-introduction-copy">
<p>{copy.label}</p>
<h2 id="about-introduction-title">{copy.title[0]}<br />{copy.title[1]}<br />{copy.title[2]}</h2>
</div>
<div className="about-introduction-image" aria-hidden="true" />
</section>
<PageActionCards
locale={locale}
label={messages.footer.columns[1].title}
className="about-action-cards"
items={[
{ href: "/members", image: "/home/about-card.png", label: messages.footer.columns[1].links[1] },
{ href: "/join-us", image: "/home/join-card.png", label: messages.footer.columns[1].links[2] },
]}
/>
</main>
<PageLocation
ariaLabel={messages.common.pagePosition}
items={[{ label: messages.common.home, href: "/" }, { label: messages.footer.columns[1].title }, { label: title }]}
/>
</>
);
}
+12 -22
View File
@@ -1,6 +1,7 @@
"use client";
import { useRouteLocale } from "@/components/layout/locale-context";
import { PageActionCards } from "@/components/layout/page-action-cards";
import Link from "next/link";
import { useEffect, useState } from "react";
@@ -10,32 +11,22 @@ import { applyLocale, getInitialLocale, withLocale, getLocaleMessages, type Loca
const homeFeatureCopy: Record<Locale, {
railway: { title: [string, string]; description: [string, string] };
city: { title: [string, string]; description: [string, string] };
about: string;
join: string;
}> = {
"zh-Hans": {
railway: { title: ["从点", "到网"], description: ["从一个车站扩展到线网", "铁路承载了根基"] },
city: { title: ["从地面", "到天空"], description: ["城建蓬勃发展", "自由盛放"] },
about: "关于我们",
join: "加入我们",
city: { title: ["从地面", "到天空"], description: ["城市蓬勃发展", "自由盛放"] },
},
"zh-Hant": {
railway: { title: ["從點", "到網"], description: ["從一個車站擴展至路網", "鐵路承載了根基"] },
city: { title: ["從地面", "到天空"], description: ["城市建設蓬勃發展", "自由綻放"] },
about: "關於我們",
join: "加入我們",
city: { title: ["從地面", "到天空"], description: ["城市蓬勃發展", "自由綻放"] },
},
en: {
railway: { title: ["From a stop", "to a network"], description: ["From one station to a network.", "Railways carry the foundation."] },
city: { title: ["From ground", "to sky"], description: ["Cities thrive and grow.", "Ideas bloom freely."] },
about: "About us",
join: "Join us",
},
jp: {
railway: { title: ["一点から", "ネットワークへ"], description: ["ひとつの駅から路線網へ。", "鉄道がその基盤を支えます。"] },
city: { title: ["地上から", "空へ"], description: ["都市は大きく発展し、", "自由に花開きます。"] },
about: "私たちについて",
join: "参加する",
},
};
@@ -137,16 +128,15 @@ export function HomePageContent({ newsByLocale }: { newsByLocale: Record<Locale,
</div>
</section>
<section className="home-actions" aria-label={t.common.siteTitle}>
<Link href={withLocale("/about", locale)} className="home-action-card">
<div className="home-action-image home-action-image--about" aria-hidden="true" />
<span>{features.about} <b aria-hidden="true">→</b></span>
</Link>
<Link href={withLocale("/join-us", locale)} className="home-action-card">
<div className="home-action-image home-action-image--join" aria-hidden="true" />
<span>{features.join} <b aria-hidden="true">→</b></span>
</Link>
</section>
<PageActionCards
locale={locale}
label={t.common.siteTitle}
className="home-actions"
items={[
{ href: "/about", image: "/home/about-card.png", label: t.footer.columns[1].title },
{ href: "/join-us", image: "/home/join-card.png", label: t.footer.columns[1].links[2] },
]}
/>
</main>
);
}
@@ -0,0 +1,31 @@
import Link from "next/link";
import { withLocale, type Locale } from "@/lib/i18n";
export type PageActionCard = {
href: string;
image: string;
label: string;
};
export function PageActionCards({
locale,
label,
items,
className = "",
}: {
locale: Locale;
label: string;
items: PageActionCard[];
className?: string;
}) {
return (
<section className={`page-action-cards ${className}`.trim()} aria-label={label}>
{items.map((item) => (
<Link key={item.href} href={withLocale(item.href, locale)} className="page-action-card">
<div className="page-action-image" style={{ backgroundImage: `url("${item.image}")` }} aria-hidden="true" />
<span>{item.label} <b aria-hidden="true">→</b></span>
</Link>
))}
</section>
);
}
+11
View File
@@ -0,0 +1,11 @@
import { PageBreadcrumb } from "@/components/layout/page-breadcrumb";
export function PageLocation({
items,
ariaLabel,
}: {
items: Array<{ label: string; href?: string }>;
ariaLabel: string;
}) {
return <div className="page-location"><PageBreadcrumb items={items} ariaLabel={ariaLabel} /></div>;
}
+23
View File
@@ -0,0 +1,23 @@
import type { CSSProperties } from "react";
export function PageTitleHero({
title,
image,
imagePosition = "center",
}: {
title: string;
image: string;
imagePosition?: string;
}) {
const mediaStyle = {
"--page-title-hero-image": `url("${image}")`,
"--page-title-hero-position": imagePosition,
} as CSSProperties;
return (
<section className="page-title-hero" aria-labelledby="page-title">
<div className="page-title-hero-media" style={mediaStyle} aria-hidden="true" />
<div className="page-title-hero-label"><h1 id="page-title">{title}</h1></div>
</section>
);
}
+8 -11
View File
@@ -1,6 +1,6 @@
import links from "@/data/links.json";
import type { CSSProperties } from "react";
import { PageBreadcrumb } from "@/components/layout/page-breadcrumb";
import { PageLocation } from "@/components/layout/page-location";
import { PageTitleHero } from "@/components/layout/page-title-hero";
import { getLocaleMessages, type Locale } from "@/lib/i18n";
type LocalizedText = Partial<Record<Locale, string>>;
@@ -48,15 +48,12 @@ function FriendlyLinkCard({ link, locale, visitLabel }: { link: LinkEntry; local
export function FriendlyLinksPage({ locale }: { locale: Locale }) {
const messages = getLocaleMessages(locale);
const heroStyle = { "--policy-hero-image": 'url("/links/friendly-links-hero.png")' } as CSSProperties;
const visitLabels: Record<Locale, string> = { "zh-Hans": "访问", "zh-Hant": "造訪", en: "Visit", jp: "訪問" };
return (
<main className="policy-page friendly-links-page">
<div className="policy-page-inner friendly-links-page-inner">
<section className="policy-hero friendly-links-hero" aria-labelledby="friendly-links-title" style={heroStyle}>
<div className="policy-hero-inner"><h1 id="friendly-links-title">{messages.footer.legal[2]}</h1></div>
</section>
<>
<main className="title-page friendly-links-page">
<PageTitleHero title={messages.footer.legal[2]} image="/links/friendly-links-hero.png" imagePosition="center 28.2%" />
<div className="friendly-links-content">
{linkSections.map((section) => (
<section className="friendly-links-section" key={section.id} aria-labelledby={`friendly-links-${section.id}`}>
@@ -67,8 +64,8 @@ export function FriendlyLinksPage({ locale }: { locale: Locale }) {
</section>
))}
</div>
</div>
<div className="page-bottom-nav"><div className="page-bottom-nav-inner"><PageBreadcrumb items={[{ label: messages.common.home, href: "/" }, { label: messages.footer.legal[2] }]} /></div></div>
</main>
</main>
<PageLocation ariaLabel={messages.common.pagePosition} items={[{ label: messages.common.home, href: "/" }, { label: messages.footer.legal[2] }]} />
</>
);
}
+11 -11
View File
@@ -1,6 +1,6 @@
import members from "@/data/members.json";
import type { CSSProperties } from "react";
import { PageBreadcrumb } from "@/components/layout/page-breadcrumb";
import { PageLocation } from "@/components/layout/page-location";
import { PageTitleHero } from "@/components/layout/page-title-hero";
import { getLocaleMessages, type Locale } from "@/lib/i18n";
type LocalizedText = Record<Locale, string>;
@@ -43,14 +43,11 @@ function MemberCard({ member, locale }: { member: MemberEntry; locale: Locale })
export function MembersPage({ locale }: { locale: Locale }) {
const messages = getLocaleMessages(locale);
const pageTitle = memberData.title[locale];
const heroStyle = { "--policy-hero-image": 'url("/members/members-hero.png")' } as CSSProperties;
return (
<main className="policy-page members-page">
<div className="policy-page-inner members-page-inner">
<section className="policy-hero members-hero" aria-labelledby="members-page-title" style={heroStyle}>
<div className="policy-hero-inner"><h1 id="members-page-title">{pageTitle}</h1></div>
</section>
<>
<main className="title-page members-page">
<PageTitleHero title={pageTitle} image="/members/members-hero.png" imagePosition="center 47%" />
<div className="members-sections">
{memberData.sections.map((section) => (
<section className="member-section" key={section.id} aria-labelledby={`members-${section.id}`}>
@@ -63,8 +60,11 @@ export function MembersPage({ locale }: { locale: Locale }) {
</section>
))}
</div>
</div>
<div className="page-bottom-nav"><div className="page-bottom-nav-inner"><PageBreadcrumb items={[{ label: messages.common.home, href: "/" }, { label: messages.footer.columns[1].title }, { label: pageTitle }]} /></div></div>
</main>
</main>
<PageLocation
ariaLabel={messages.common.pagePosition}
items={[{ label: messages.common.home, href: "/" }, { label: messages.footer.columns[1].title }, { label: pageTitle }]}
/>
</>
);
}
@@ -0,0 +1,24 @@
import { PageLocation } from "@/components/layout/page-location";
import { PageTitleHero } from "@/components/layout/page-title-hero";
import { getLocaleMessages, type Locale } from "@/lib/i18n";
export function SimpleTitlePage({
locale,
title,
image,
imagePosition,
}: {
locale: Locale;
title: string;
image: string;
imagePosition?: string;
}) {
const messages = getLocaleMessages(locale);
return (
<>
<main className="title-page"><PageTitleHero title={title} image={image} imagePosition={imagePosition} /></main>
<PageLocation ariaLabel={messages.common.pagePosition} items={[{ label: messages.common.home, href: "/" }, { label: title }]} />
</>
);
}
+22 -27
View File
@@ -5,7 +5,8 @@ import { useRouteLocale } from "@/components/layout/locale-context";
import { useEffect, useState } from "react";
import { getAiTranslationNotice, getInitialLocale, getLocaleMessages, type Locale } from "@/lib/i18n";
import type { PolicyDocument } from "@/data/policy";
import { PageBreadcrumb } from "@/components/layout/page-breadcrumb";
import { PageLocation } from "@/components/layout/page-location";
import { PageTitleHero } from "@/components/layout/page-title-hero";
type PolicyCatalog = Partial<Record<Locale, PolicyDocument>>;
@@ -30,31 +31,25 @@ export function PolicyPageContent({ catalog }: { catalog: PolicyCatalog }) {
return (
<>
<section className="policy-hero" aria-labelledby="policy-title">
<div className="policy-hero-inner">
<h1 id="policy-title">{document.title}</h1>
</div>
</section>
<article className="policy-card">
{isTranslated && (
<div className="news-detail-ai-banner" aria-label="AI translated content">
{getAiTranslationNotice(locale)}
</div>
)}
<div className="policy-body" dangerouslySetInnerHTML={{ __html: document.html }} />
</article>
<div className="page-bottom-nav">
<div className="page-bottom-nav-inner">
<PageBreadcrumb
ariaLabel={messages.common.pagePosition}
items={[
{ label: messages.common.home, href: "/" },
{ label: messages.footer.columns[0].title },
{ label: document.title },
]}
/>
</div>
</div>
<main className="title-page policy-page">
<PageTitleHero title={document.title} image="/policy-hero.png" />
<article className="policy-card">
{isTranslated && (
<div className="news-detail-ai-banner" aria-label="AI translated content">
{getAiTranslationNotice(locale)}
</div>
)}
<div className="policy-body" dangerouslySetInnerHTML={{ __html: document.html }} />
</article>
</main>
<PageLocation
ariaLabel={messages.common.pagePosition}
items={[
{ label: messages.common.home, href: "/" },
{ label: messages.footer.columns[0].title },
{ label: document.title },
]}
/>
</>
);
}
}