feat(layout): add Navigation, Footer and Logo components with root layout shell
This commit is contained in:
parent
ba5287add8
commit
c441ed7026
@ -1,5 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Megrim, Audiowide, Cinzel, Inter } from "next/font/google";
|
||||
import Navigation from "@/components/Navigation";
|
||||
import Footer from "@/components/Footer";
|
||||
import "./globals.css";
|
||||
|
||||
const megrim = Megrim({
|
||||
@ -51,7 +53,11 @@ export default function RootLayout({
|
||||
lang="zh-CN"
|
||||
className={`${megrim.variable} ${audiowide.variable} ${cinzel.variable} ${inter.variable} h-full antialiased`}
|
||||
>
|
||||
<body className="min-h-full">{children}</body>
|
||||
<body className="min-h-full flex flex-col">
|
||||
<Navigation />
|
||||
<main className="flex-1">{children}</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,23 +1,24 @@
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="min-h-screen flex flex-col items-center justify-center px-6 py-16">
|
||||
<section className="px-6 py-20 sm:py-28">
|
||||
<div className="max-w-5xl mx-auto flex flex-col items-center">
|
||||
{/* Logo */}
|
||||
<h1
|
||||
className="font-logo text-7xl sm:text-8xl tracking-[0.5em] uppercase glow-text-purple text-center mb-4"
|
||||
style={{ paddingLeft: "0.5em" }}
|
||||
className="font-logo text-6xl sm:text-8xl tracking-[0.4em] uppercase glow-text-purple text-center mb-4"
|
||||
style={{ paddingLeft: "0.4em" }}
|
||||
>
|
||||
Cyber
|
||||
<span className="text-purple-300 mx-3 text-5xl sm:text-6xl align-middle">
|
||||
<span className="text-purple-300 mx-2 sm:mx-3 text-4xl sm:text-6xl align-middle">
|
||||
✦
|
||||
</span>
|
||||
Star
|
||||
</h1>
|
||||
|
||||
{/* Subtitle */}
|
||||
<p className="font-label text-sm tracking-[0.4em] uppercase text-purple-300 mb-2">
|
||||
<p className="font-label text-xs sm:text-sm tracking-[0.4em] uppercase text-purple-300 mb-2">
|
||||
Virtual Idol Debut Project
|
||||
</p>
|
||||
<p className="text-white/60 text-sm mb-12">
|
||||
<p className="text-white/60 text-sm mb-10">
|
||||
虚拟偶像 Top12 出道企划
|
||||
</p>
|
||||
|
||||
@ -25,7 +26,7 @@ export default function Home() {
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-purple-500/10 border border-purple-500/30 mb-12">
|
||||
<span className="w-2 h-2 rounded-full bg-purple-400 animate-pulse-glow"></span>
|
||||
<span className="font-label text-xs tracking-widest text-purple-300 uppercase">
|
||||
Phase 2 · Theme Ready
|
||||
Phase 3 · Layout Ready
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -79,8 +80,9 @@ export default function Home() {
|
||||
</div>
|
||||
|
||||
<p className="mt-16 text-white/40 text-xs font-mono">
|
||||
Phase 3 → Layout + Navigation 即将开始
|
||||
Phase 4 → Core Components 即将开始
|
||||
</p>
|
||||
</main>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
57
src/components/Footer.tsx
Normal file
57
src/components/Footer.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import Link from "next/link";
|
||||
import Logo from "./Logo";
|
||||
|
||||
const FOOTER_LINKS = [
|
||||
{ label: "活动规则", href: "/rules" },
|
||||
{ label: "隐私协议", href: "/privacy" },
|
||||
{ label: "用户协议", href: "/terms" },
|
||||
{ label: "联系客服", href: "/support" },
|
||||
] as const;
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="border-t border-white/[0.06] bg-[rgba(8,5,26,0.6)] backdrop-blur-sm mt-20">
|
||||
<div className="max-w-7xl mx-auto px-6 sm:px-8 py-10 grid gap-8 md:grid-cols-3 items-start">
|
||||
{/* Brand */}
|
||||
<div>
|
||||
<Logo size="sm" href={null} />
|
||||
<p className="mt-3 text-xs text-white/45 leading-relaxed">
|
||||
虚拟偶像 Top12 出道企划
|
||||
<br />
|
||||
Cyber Star · Virtual Idol Debut Project
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Links */}
|
||||
<nav className="md:col-span-1">
|
||||
<p className="font-label text-[10px] tracking-[0.3em] uppercase text-purple-300 mb-3">
|
||||
Information
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm">
|
||||
{FOOTER_LINKS.map((link) => (
|
||||
<li key={link.href}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-white/55 hover:text-purple-300 transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{/* Caption */}
|
||||
<div className="text-xs text-white/40 md:text-right">
|
||||
<p className="font-label tracking-[0.25em] uppercase text-purple-300/70 mb-2">
|
||||
© 2026 Cyber Star
|
||||
</p>
|
||||
<p>All rights reserved.</p>
|
||||
<p className="mt-1 font-mono text-white/30">
|
||||
airlabs.art · powered by Next.js
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
41
src/components/Logo.tsx
Normal file
41
src/components/Logo.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import Link from "next/link";
|
||||
|
||||
type LogoSize = "sm" | "md" | "lg" | "xl";
|
||||
|
||||
interface LogoProps {
|
||||
size?: LogoSize;
|
||||
href?: string | null;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SIZE_MAP: Record<LogoSize, { text: string; star: string; gap: string }> = {
|
||||
sm: { text: "text-base", star: "text-xs", gap: "mx-1" },
|
||||
md: { text: "text-xl sm:text-2xl", star: "text-sm", gap: "mx-1.5" },
|
||||
lg: { text: "text-4xl sm:text-5xl", star: "text-2xl", gap: "mx-2" },
|
||||
xl: { text: "text-6xl sm:text-7xl", star: "text-4xl", gap: "mx-3" },
|
||||
};
|
||||
|
||||
export default function Logo({
|
||||
size = "md",
|
||||
href = "/",
|
||||
className = "",
|
||||
}: LogoProps) {
|
||||
const { text, star, gap } = SIZE_MAP[size];
|
||||
|
||||
const inner = (
|
||||
<span
|
||||
className={`font-logo tracking-[0.25em] uppercase glow-text-purple inline-flex items-baseline ${text} ${className}`}
|
||||
>
|
||||
Cyber
|
||||
<span className={`text-purple-300 ${gap} ${star}`}>✦</span>
|
||||
Star
|
||||
</span>
|
||||
);
|
||||
|
||||
if (!href) return inner;
|
||||
return (
|
||||
<Link href={href} className="inline-block hover:opacity-90 transition-opacity">
|
||||
{inner}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
110
src/components/Navigation.tsx
Normal file
110
src/components/Navigation.tsx
Normal file
@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import Logo from "./Logo";
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ label: "HOME", href: "/" },
|
||||
{ label: "VOTE", href: "/vote" },
|
||||
{ label: "RANKING", href: "/ranking" },
|
||||
{ label: "NEWS", href: "/news" },
|
||||
{ label: "ABOUT", href: "/about" },
|
||||
] as const;
|
||||
|
||||
export default function Navigation() {
|
||||
const pathname = usePathname();
|
||||
|
||||
const isActive = (href: string) => {
|
||||
if (href === "/") return pathname === "/";
|
||||
return pathname.startsWith(href);
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 backdrop-blur-xl bg-[rgba(13,10,36,0.85)] border-b border-white/[0.08]">
|
||||
<nav className="max-w-7xl mx-auto h-16 px-6 sm:px-8 flex items-center gap-8">
|
||||
<Logo size="md" />
|
||||
|
||||
{/* Nav links */}
|
||||
<ul className="hidden md:flex items-center gap-8 flex-1 font-display text-xs tracking-[0.25em]">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const active = isActive(item.href);
|
||||
return (
|
||||
<li key={item.href}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={`relative py-1 transition-colors uppercase ${
|
||||
active
|
||||
? "text-purple-300 glow-text-purple"
|
||||
: "text-white/65 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
{active && (
|
||||
<span
|
||||
className="absolute -bottom-0.5 left-0 right-0 h-px bg-purple-400"
|
||||
style={{ boxShadow: "0 0 8px #a78bfa" }}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
{/* Right side */}
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
{/* Search button (icon-only on mobile) */}
|
||||
<button
|
||||
type="button"
|
||||
aria-label="搜索"
|
||||
className="w-9 h-9 rounded-md flex items-center justify-center text-white/65 hover:text-white hover:bg-white/[0.06] transition-colors"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.3-4.3" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Login button */}
|
||||
<Link
|
||||
href="/login"
|
||||
className="font-display text-[10px] sm:text-xs tracking-[0.2em] uppercase px-4 sm:px-5 h-9 inline-flex items-center justify-center rounded border border-[var(--border-purple)] text-purple-300 hover:bg-purple-500/10 hover:shadow-[0_0_20px_rgba(139,92,246,0.3)] transition-all"
|
||||
>
|
||||
Login / Sign Up
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Mobile nav links (horizontal scroll) */}
|
||||
<div className="md:hidden border-t border-white/[0.05] overflow-x-auto">
|
||||
<ul className="flex items-center gap-6 px-6 py-2.5 font-display text-[11px] tracking-[0.25em] whitespace-nowrap">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const active = isActive(item.href);
|
||||
return (
|
||||
<li key={item.href}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={`uppercase transition-colors ${
|
||||
active ? "text-purple-300" : "text-white/55"
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user