初步建设主页和新闻页面

This commit is contained in:
2026-08-22 22:23:38 +08:00
parent 4238b81205
commit de9982c9eb
75 changed files with 2913 additions and 427 deletions
+14
View File
@@ -0,0 +1,14 @@
import Link from "next/link";
export function PageBreadcrumb({ items }: { items: Array<{ label: string; href?: string }> }) {
return (
<nav className="page-breadcrumb" aria-label="页面位置">
{items.map((item, index) => (
<span className="page-breadcrumb-item" key={`${item.label}-${index}`}>
{index > 0 && <span className="page-breadcrumb-separator" aria-hidden="true">›</span>}
{item.href ? <Link href={item.href}>{item.label}</Link> : <span>{item.label}</span>}
</span>
))}
</nav>
);
}
+113
View File
@@ -0,0 +1,113 @@
"use client";
import Image from "next/image";
import { useEffect, useState } from "react";
import { applyLocale, getInitialLocale, getLocaleMessages, type Locale } from "@/lib/i18n";
const footerColumnHrefs = [
["/policy/user-agreement", "/policy/privacy-policy", "/policy/disclaimer", "/policy/resource-pack"],
["/about", "/join-us"],
["mailto:connect@kivotos.cc", "mailto:infringement@kivotos.cc"],
] as const;
const footerLegalHrefs = ["https://wiki.kivotosrailway.com", "https://url.kivotos.cc/status", "/links"] as const;
function isIcpVisibleHost(hostname: string) {
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname.endsWith(".kivotos.cc");
}
export function SiteFooter() {
const [locale, setLocale] = useState<Locale>("zh-CN");
const [showIcp, setShowIcp] = useState(false);
useEffect(() => {
const syncLocale = (nextLocale: Locale) => setLocale(nextLocale);
const handleLocaleChange = (event: Event) => {
const nextLocale = (event as CustomEvent<{ locale?: Locale }>).detail?.locale ?? getInitialLocale();
syncLocale(nextLocale);
};
const handleStorage = () => {
syncLocale(getInitialLocale());
};
const currentLocale = getInitialLocale();
syncLocale(currentLocale);
applyLocale(currentLocale);
setShowIcp(isIcpVisibleHost(window.location.hostname));
window.addEventListener("kr-locale-change", handleLocaleChange);
window.addEventListener("storage", handleStorage);
return () => {
window.removeEventListener("kr-locale-change", handleLocaleChange);
window.removeEventListener("storage", handleStorage);
};
}, []);
const messages = getLocaleMessages(locale);
return (
<footer className="site-footer">
<div className="site-footer-inner">
<div className="site-footer-top">
<div className="site-footer-socials" aria-label="socials">
<span className="site-footer-title">{messages.footer.follow}</span>
<div className="site-footer-icons">
{messages.footer.social.map((item, index) => (
<a key={`social-${index}`} href="#" aria-label={item} className="site-footer-icon" title={item}>
{[
"◌",
"◍",
"X",
"in",
"▶",
][index] ?? "•"}
</a>
))}
</div>
</div>
</div>
<div className="site-footer-main">
<div className="site-footer-columns">
{messages.footer.columns.map((group, groupIndex) => (
<div key={`column-${groupIndex}`} className="site-footer-column">
<h3>{group.title}</h3>
<ul>
{group.links.map((link, linkIndex) => (
<li key={`column-${groupIndex}-link-${linkIndex}`}><a href={footerColumnHrefs[groupIndex][linkIndex]}>{link}</a></li>
))}
</ul>
</div>
))}
</div>
<div className="site-footer-brand-wrap">
<div className="site-footer-brand" aria-label="Kivotos Railway">
<Image
src="/icon/kivotosrailway.svg"
alt="Kivotos Railway"
width={270}
height={58}
className="site-footer-brand-logo"
/>
</div>
</div>
</div>
<div className="site-footer-meta">
<div className="site-footer-legal-row">
{messages.footer.legal.map((item, itemIndex) => (
<a key={`legal-${itemIndex}`} href={footerLegalHrefs[itemIndex]}>{item}</a>
))}
</div>
<p className="site-footer-description">{messages.footer.description}</p>
<div className="site-footer-copyright-row">
<span>{messages.footer.copyright}</span>
{showIcp && <span className="site-footer-icp">{messages.footer.icp}</span>}
</div>
</div>
</div>
</footer>
);
}
+91
View File
@@ -0,0 +1,91 @@
"use client";
import Link from "next/link";
import Image from "next/image";
import { useEffect, useState } from "react";
import { applyLocale, getInitialLocale, getLocaleMessages, localeLabels, locales, localeShortLabels, type Locale } from "@/lib/i18n";
export function SiteHeader() {
const [locale, setLocale] = useState<Locale>("zh-CN");
const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => {
const syncLocale = (nextLocale: Locale) => setLocale(nextLocale);
const handleLocaleChange = (event: Event) => {
const nextLocale = (event as CustomEvent<{ locale?: Locale }>).detail?.locale ?? getInitialLocale();
syncLocale(nextLocale);
};
const handleStorage = () => {
syncLocale(getInitialLocale());
};
const currentLocale = getInitialLocale();
syncLocale(currentLocale);
applyLocale(currentLocale);
window.addEventListener("kr-locale-change", handleLocaleChange);
window.addEventListener("storage", handleStorage);
return () => {
window.removeEventListener("kr-locale-change", handleLocaleChange);
window.removeEventListener("storage", handleStorage);
};
}, []);
const messages = getLocaleMessages(locale);
return (
<header className="site-header">
<Link className="site-logo" href="/" aria-label={`Kivotos Railway ${localeLabels[locale]}`}>
<Image
className="site-logo-image"
src="/icon/kivotosrailway.svg"
alt="Kivotos Railway"
width={240}
height={64}
priority
/>
</Link>
<nav className={`site-navigation${menuOpen ? " is-open" : ""}`} aria-label={localeLabels[locale]}>
{messages.header.nav.map((label, index) => {
const href = ["/", "/news", "/railway", "/about", "/members"][index] ?? "/";
return (
<Link key={href} href={href} onClick={() => setMenuOpen(false)}>
{label}
</Link>
);
})}
</nav>
<div className="site-header-actions">
<div className="site-locale" aria-label="语言选择">
{locales.map((item) => (
<button
key={item}
type="button"
className={item === locale ? "active" : ""}
aria-pressed={item === locale}
onClick={() => {
setLocale(item);
applyLocale(item);
}}
aria-label={localeLabels[item]}
>
{localeShortLabels[item]}
</button>
))}
</div>
<button
className="site-menu-button"
type="button"
aria-label="打开导航"
aria-expanded={menuOpen}
onClick={() => setMenuOpen((current) => !current)}
>
<span /><span /><span />
</button>
</div>
</header>
);
}