Compare commits

..

No commits in common. "main" and "v0.3.4" have entirely different histories.
main ... v0.3.4

14 changed files with 27 additions and 65 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -14,7 +14,7 @@ const Body = z.object({
/** /**
* POST /api/auth/send-otp * POST /api/auth/send-otp
* · 60s / IP 5 100 * · 60s / IP 5 5
*/ */
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
try { try {
@ -31,7 +31,7 @@ export async function POST(req: NextRequest) {
} }
const ip = await getClientIp(); const ip = await getClientIp();
if (ip) { if (ip) {
const ipRl = await rateLimit(`otp:ip:${ip}`, 300, 100); const ipRl = await rateLimit(`otp:ip:${ip}`, 300, 5);
if (!ipRl.allowed) return ERR.RATE_LIMITED(); if (!ipRl.allowed) return ERR.RATE_LIMITED();
} }

View File

@ -16,10 +16,10 @@ export async function generateMetadata({
}: ArtistPageProps): Promise<Metadata> { }: ArtistPageProps): Promise<Metadata> {
const { id } = await params; const { id } = await params;
const artist = getArtist(id); const artist = getArtist(id);
if (!artist) return { title: "艺人不存在 · 银河初星计划 C . S . G" }; if (!artist) return { title: "艺人不存在 · CYBER STAR" };
return { return {
title: `${artist.name} · ${artist.enName} · 银河初星计划 C . S . G`, title: `${artist.name} · ${artist.enName} · CYBER STAR`,
description: artist.bio.slice(0, 120), description: artist.bio.slice(0, 120),
}; };
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -33,27 +33,15 @@ const inter = Inter({
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "银河初星计划 C . S . G", title: "CYBER ✦ STAR · 虚拟偶像 Top12 出道企划",
description: description:
"36 位虚拟偶像候选人,由你投票决出最终出道 Top12。银河初星计划 C . S . G。", "36 位虚拟偶像候选人,由你投票决出最终出道 Top12。Cyber Star · Virtual Idol Debut Project.",
keywords: [ keywords: ["虚拟偶像", "出道", "投票", "Top12", "Cyber Star", "Virtual Idol"],
"银河初星计划",
"C . S . G",
"虚拟偶像",
"出道",
"投票",
"Top12",
"Virtual Idol",
],
openGraph: { openGraph: {
title: "银河初星计划 C . S . G", title: "CYBER ✦ STAR",
description: "虚拟偶像 Top12 出道企划", description: "虚拟偶像 Top12 出道企划",
type: "website", type: "website",
}, },
icons: {
icon: "/favicon.ico?v=4",
shortcut: "/favicon.ico?v=4",
},
}; };
export default function RootLayout({ export default function RootLayout({

View File

@ -93,8 +93,8 @@ export default function LoginForm() {
{/* Logo */} {/* Logo */}
<div className="flex flex-col items-center mb-8"> <div className="flex flex-col items-center mb-8">
<img <img
src="/logo-v4.png?v=4" src="/logo-v3.png"
alt="银河初星计划 C . S . G" alt="CYBER STAR"
decoding="async" decoding="async"
draggable={false} draggable={false}
className="block select-none h-20 sm:h-24 w-auto" className="block select-none h-20 sm:h-24 w-auto"

View File

@ -2,7 +2,7 @@ import { Suspense } from "react";
import LoginForm from "./LoginForm"; import LoginForm from "./LoginForm";
export const metadata = { export const metadata = {
title: "登录 · 银河初星计划 C . S . G", title: "登录 · CYBER STAR",
}; };
export default function LoginPage() { export default function LoginPage() {

View File

@ -4,7 +4,7 @@ import { auth } from "@/lib/auth";
import MeContent from "./MeContent"; import MeContent from "./MeContent";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "个人中心 · 银河初星计划 C . S . G", title: "个人中心 · CYBER STAR",
}; };
export default async function MePage() { export default async function MePage() {

View File

@ -4,7 +4,7 @@ export default function Footer() {
<footer className="border-t border-white/[0.06] bg-deep mt-16"> <footer className="border-t border-white/[0.06] bg-deep mt-16">
<div className="max-w-[1500px] mx-auto px-6 sm:px-8 h-16 flex items-center justify-center text-center"> <div className="max-w-[1500px] mx-auto px-6 sm:px-8 h-16 flex items-center justify-center text-center">
<p className="text-[11px] text-white/35 tracking-[0.05em]"> <p className="text-[11px] text-white/35 tracking-[0.05em]">
© {year} C . S . G · All Rights Reserved © {year} CYBER STAR · All Rights Reserved
</p> </p>
</div> </div>
</footer> </footer>

View File

@ -90,8 +90,8 @@ export default function HeroBanner({
<div className="absolute inset-0 max-w-[1500px] mx-auto px-4 sm:px-6 lg:px-8"> <div className="absolute inset-0 max-w-[1500px] mx-auto px-4 sm:px-6 lg:px-8">
{/* Eyebrow 左上 · 紧贴导航下方 */} {/* Eyebrow 左上 · 紧贴导航下方 */}
<div className="absolute top-[6.5rem] sm:top-[7.5rem] left-4 sm:left-6 lg:left-8 z-10"> <div className="absolute top-[6.5rem] sm:top-[7.5rem] left-4 sm:left-6 lg:left-8 z-10">
<p className="font-label text-[10px] sm:text-xs tracking-[0.28em] uppercase text-purple-200/90"> <p className="font-label text-[10px] sm:text-xs tracking-[0.4em] uppercase text-purple-200/90">
C . S . G Top 12 · Cyber Star Debut Survival
</p> </p>
</div> </div>

View File

@ -8,7 +8,7 @@ interface LogoProps {
className?: string; className?: string;
} }
// 高度由 size 控制,宽度按 logo-v4.png 实际比例自适应 // 高度由 size 控制,宽度按 logo.png 实际比例(约 5.41:1单行 CYBER STAR + 星环)自适应
const HEIGHT_PX: Record<LogoSize, number> = { const HEIGHT_PX: Record<LogoSize, number> = {
sm: 24, sm: 24,
md: 44, md: 44,
@ -25,11 +25,11 @@ export default function Logo({
// 用原生 <img> 绕开 Next/Image 的格式转换 —— 某些环境下 sharp 把透明 PNG // 用原生 <img> 绕开 Next/Image 的格式转换 —— 某些环境下 sharp 把透明 PNG
// 转 webp/avif 时会铺白底,导致 logo 在深色 nav 上出现白色矩形。 // 转 webp/avif 时会铺白底,导致 logo 在深色 nav 上出现白色矩形。
// ?v=4 缓存破坏:logo 改版时 +1,浏览器立刻拉新版而不读老缓存。 // ?v=2 缓存破坏:logo 改版时 +1,浏览器立刻拉新版而不读老缓存。
const inner = ( const inner = (
<img <img
src="/logo-v4.png?v=4" src="/logo.png?v=3"
alt="银河初星计划 C . S . G" alt="CYBER STAR"
decoding="async" decoding="async"
draggable={false} draggable={false}
style={{ style={{
@ -46,7 +46,7 @@ export default function Logo({
<Link <Link
href={href} href={href}
className="inline-flex items-center hover:opacity-90 transition-opacity" className="inline-flex items-center hover:opacity-90 transition-opacity"
aria-label="银河初星计划 C . S . G · 首页" aria-label="CYBER STAR · 首页"
style={{ background: "transparent" }} style={{ background: "transparent" }}
> >
{inner} {inner}

View File

@ -2,7 +2,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import Logo from "./Logo";
import NavLinks from "./NavLinks"; import NavLinks from "./NavLinks";
import SearchTrigger from "./SearchTrigger"; import SearchTrigger from "./SearchTrigger";
import AuthMenu from "./auth/AuthMenu"; import AuthMenu from "./auth/AuthMenu";
@ -28,7 +27,6 @@ export default function Navigation() {
// nav 关掉自己的玻璃,避免双重 backdrop-filter 在 y=80 处出现拼接线。 // nav 关掉自己的玻璃,避免双重 backdrop-filter 在 y=80 处出现拼接线。
const filterStuck = useUIStore((s) => s.filterStuck); const filterStuck = useUIStore((s) => s.filterStuck);
const glassOff = isTransparent || filterStuck; const glassOff = isTransparent || filterStuck;
const showLogo = pathname !== "/";
// 维护一个站内导航计数器(per-tab),供 FloatingBackButton 判断 router.back() 是否安全 // 维护一个站内导航计数器(per-tab),供 FloatingBackButton 判断 router.back() 是否安全
useEffect(() => { useEffect(() => {
@ -83,16 +81,7 @@ export default function Navigation() {
)} )}
/> />
<nav className="relative max-w-[1500px] mx-auto h-20 px-4 sm:px-6 lg:px-8 flex items-center gap-4 sm:gap-8"> <nav className="relative max-w-[1500px] mx-auto h-20 px-4 sm:px-6 lg:px-8 flex items-center gap-4 sm:gap-8">
{showLogo && ( {/* 左侧:首页 / 排行榜 / 我的(logo 已移除,所有屏宽都显示在第一行) */}
<div className="hidden sm:flex shrink-0 items-center">
<Logo
size="md"
className="drop-shadow-[0_0_14px_rgba(139,92,246,0.55)]"
/>
</div>
)}
{/* 左侧:首页 / 排行榜 / 我的 */}
<NavLinks /> <NavLinks />
{/* 右侧:搜索 + 今日余票 + 登录/注册 (或 头像+下拉) */} {/* 右侧:搜索 + 今日余票 + 登录/注册 (或 头像+下拉) */}

View File

@ -173,8 +173,8 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps
<div className="flex flex-col items-center mb-6"> <div className="flex flex-col items-center mb-6">
<img <img
src="/logo-v4.png?v=4" src="/logo-v3.png"
alt="银河初星计划 C . S . G" alt="CYBER STAR"
decoding="async" decoding="async"
draggable={false} draggable={false}
className="block select-none h-16 sm:h-20 w-auto" className="block select-none h-16 sm:h-20 w-auto"

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { useSession, signOut } from "next-auth/react"; import { useSession } from "next-auth/react";
import { useVoteStore } from "@/lib/store"; import { useVoteStore } from "@/lib/store";
/** /**
@ -11,10 +11,6 @@ import { useVoteStore } from "@/lib/store";
* - status === "unauthenticated" () * - status === "unauthenticated" ()
* - (uid ) * - (uid )
* *
* session 兜底:NextAuth JWT ,cookie DB user
* /api/me 401() NOT_FOUND(DB user ) ,
* signOut() cookie "假登录"()
*
* localStorage , * localStorage ,
*/ */
export function useSyncMe() { export function useSyncMe() {
@ -49,23 +45,12 @@ export function useSyncMe() {
credentials: "include", credentials: "include",
signal: ctrl.signal, signal: ctrl.signal,
}) })
.then(async (r) => { .then((r) => r.json())
.then((res) => {
if (cancelled) return; if (cancelled) return;
const res = await r.json().catch(() => null); if (res?.ok && Array.isArray(res.data?.votedArtists)) {
if (r.ok && res?.ok && Array.isArray(res.data?.votedArtists)) {
hydrateFromServer(res.data.votedArtists as string[]); hydrateFromServer(res.data.votedArtists as string[]);
lastSyncedUidRef.current = uid; lastSyncedUidRef.current = uid;
return;
}
// 僵尸 session:JWT 还有效,但 DB 里 user 已不存在(或鉴权失效)。
// 直接登出清 cookie,UI 状态切换为未登录,避免"显示已登录但拉不到数据"。
const code: string | undefined = res?.error?.code;
if (r.status === 401 || code === "UNAUTHORIZED" || code === "NOT_FOUND") {
signOut({ redirect: false });
reset();
lastSyncedUidRef.current = null;
} }
}) })
.catch(() => { .catch(() => {