Files
2026-03-16 06:53:23 -05:00

163 lines
9.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useState } from "react";
import Image from "next/image";
import Link from "next/link";
import SessionCoverImage from "./SessionCoverImage";
import AddressLink from "./AddressLink";
import { getSessionsWithDates, RECENT_JOINERS } from "@/config/home.config";
const MOCK_JOINERS = RECENT_JOINERS.slice(0, 3);
export default function HeroSection() {
const sessions = getSessionsWithDates();
const mainSession = sessions[0];
const [stats, setStats] = useState<{ count: number; joiners: { nickname: string }[] } | null>(null);
useEffect(() => {
if (!mainSession) return;
fetch("/api/join/stats")
.then((r) => r.json())
.then((d) => d?.stats?.[mainSession.id] && setStats(d.stats[mainSession.id]))
.catch(() => {});
}, [mainSession?.id]);
return (
<section className="relative overflow-hidden bg-[#faf8f5] dark:bg-stone-950">
<div
className="absolute inset-0"
style={{
backgroundImage: `radial-gradient(ellipse 80% 50% at 50% 0%, rgba(180,83,9,0.08) 0%, transparent 60%),
radial-gradient(ellipse 60% 40% at 80% 80%, rgba(180,83,9,0.05) 0%, transparent 50%)`,
}}
/>
<div className="relative max-w-[1200px] mx-auto px-4 sm:px-6 md:px-8 py-14 sm:py-20 md:py-28">
<div className="flex flex-col lg:flex-row lg:items-center lg:gap-14 xl:gap-20">
<div className="flex-1 min-w-0">
<h1 className="text-3xl sm:text-4xl md:text-5xl lg:text-[3rem] xl:text-[3.5rem] font-bold tracking-tight leading-[1.2]">
<span className="hero-gradient-text">2</span>
<span className="text-stone-800 dark:text-stone-100 whitespace-nowrap"> </span>
</h1>
<p className="mt-4 sm:mt-5 text-stone-600 dark:text-stone-400 text-sm sm:text-base max-w-lg leading-relaxed">
· 48
</p>
<p className="mt-1.5 text-stone-500 dark:text-stone-450 text-xs sm:text-sm max-w-lg">
· · ·
</p>
<div className="mt-6 sm:mt-8 hidden sm:block">
<Link
href="/join"
className="inline-flex items-center justify-center min-w-[180px] sm:min-w-[220px] bg-amber-600 hover:bg-amber-700 text-white px-8 py-2.5 sm:px-10 sm:py-3 rounded-xl text-sm font-medium shadow-lg shadow-amber-900/20 transition-all active:scale-[0.98]"
>
</Link>
</div>
</div>
{mainSession && (
<div className="w-full max-w-[280px] sm:max-w-none lg:w-[520px] xl:w-[580px] shrink-0 mt-6 lg:mt-0 mx-auto lg:mx-0">
<Link
href={`/join?session=${mainSession.id}`}
className="group flex flex-col lg:flex-row rounded-xl lg:rounded-2xl overflow-hidden bg-white dark:bg-stone-900 shadow-lg lg:shadow-xl shadow-stone-200/60 dark:shadow-stone-950/80 border border-stone-100 dark:border-stone-800 hover:shadow-xl lg:hover:shadow-2xl hover:shadow-amber-900/15 transition-all duration-300 hover:-translate-y-0.5 lg:hover:-translate-y-1"
>
{/* 图片区 - 左侧/上方 */}
<div className="relative lg:w-[44%] aspect-[16/10] sm:aspect-[4/3] lg:aspect-auto lg:min-h-[180px] w-full bg-stone-100 dark:bg-stone-800 overflow-hidden shrink-0">
{mainSession.coverImage ? (
<SessionCoverImage
src={mainSession.coverImage}
alt={mainSession.title}
type={mainSession.type}
fill
sizes="(max-width: 1024px) 100vw, 260px"
className="object-cover transition-transform duration-500 group-hover:scale-105"
/>
) : (
<div className="absolute inset-0 flex items-center justify-center text-4xl bg-stone-200 dark:bg-stone-700">
</div>
)}
<div className="absolute inset-0 bg-gradient-to-r from-black/20 via-transparent to-transparent lg:from-transparent lg:via-transparent lg:to-black/30" aria-hidden />
<div className="absolute bottom-1.5 left-1.5 sm:bottom-2 sm:left-2 lg:bottom-3 lg:left-3 flex flex-wrap gap-1 lg:gap-1.5">
{["周末", "4人成行"].map((tag) => (
<span
key={tag}
className="inline-block text-[9px] sm:text-[10px] font-semibold bg-white/95 dark:bg-stone-900/95 backdrop-blur-sm px-2 sm:px-2.5 py-0.5 sm:py-1 rounded text-stone-700 dark:text-stone-200"
>
{tag}
</span>
))}
</div>
</div>
{/* 内容区 - 右侧/下方 */}
<div className="flex-1 flex flex-col justify-center p-3 sm:p-5 lg:p-6 lg:pl-6 min-w-0">
<div className="inline-flex items-center gap-1.5 sm:gap-2 mb-2 sm:mb-3">
<span className="w-0.5 sm:w-1 h-3 sm:h-4 rounded-full bg-amber-500 dark:bg-amber-500" aria-hidden />
<p className="text-[9px] sm:text-[10px] font-semibold text-amber-600 dark:text-amber-400 uppercase tracking-[0.15em] sm:tracking-[0.2em]">
</p>
</div>
<h3 className="font-bold text-stone-800 dark:text-stone-100 text-sm sm:text-lg lg:text-xl leading-tight tracking-tight">
{mainSession.title}
</h3>
<div className="mt-2 sm:mt-3 space-y-1 sm:space-y-1.5">
<p className="text-xs sm:text-sm text-stone-500 dark:text-stone-400">
<span className="inline-block w-12 sm:w-14 text-stone-400 dark:text-stone-500"></span>
{mainSession.date}
</p>
<p className="text-xs sm:text-sm text-stone-500 dark:text-stone-400">
<span className="inline-block w-12 sm:w-14 text-stone-400 dark:text-stone-500"></span>
{mainSession.time}
</p>
<p className="text-xs sm:text-sm text-stone-500 dark:text-stone-400">
<span className="inline-block w-12 sm:w-14 text-stone-400 dark:text-stone-500"></span>
<AddressLink place={mainSession.place} city={mainSession.city} />
</p>
</div>
<div className="mt-3 sm:mt-4 pt-3 sm:pt-4 border-t border-stone-100 dark:border-stone-800 flex items-center justify-between gap-2 sm:gap-4">
<div className="flex items-center gap-2 sm:gap-3 min-w-0">
<div className="flex -space-x-2 sm:-space-x-2.5 shrink-0">
{(stats?.joiners?.length ? stats.joiners : MOCK_JOINERS).slice(0, 5).map((j, i) => {
const nickname = "name" in j ? (j as { name: string }).name : (j as { nickname: string }).nickname;
const avatar = "avatar" in j ? (j as { avatar: string }).avatar : (j as { avatar?: string }).avatar;
const xhUrl = (j as { xiaohongshu_url?: string }).xiaohongshu_url;
const showAvatar = avatar && avatar.length > 0;
return (
<span
key={i}
className={`relative w-6 h-6 sm:w-8 sm:h-8 rounded-full overflow-hidden border-2 border-white dark:border-stone-900 shrink-0 inline-block transition-transform hover:scale-110 ${xhUrl ? "cursor-pointer" : "cursor-default"} ${showAvatar ? "" : "bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center text-[10px] sm:text-xs font-medium text-amber-700 dark:text-amber-300"}`}
title={nickname}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (xhUrl) window.open(xhUrl, "_blank", "noopener,noreferrer");
}}
>
{showAvatar ? (
<Image src={avatar} alt="" fill sizes="32px" className="object-cover" unoptimized />
) : (
<span className="flex items-center justify-center w-full h-full">{nickname.slice(0, 1) || "?"}</span>
)}
</span>
);
})}
</div>
<span className="text-[10px] sm:text-xs text-stone-500 dark:text-stone-400 truncate">
{stats?.count ? `${stats.count} 人已报名` : "有人报名中"}
</span>
</div>
<span className="shrink-0 inline-flex items-center gap-1 sm:gap-1.5 px-2 sm:px-3 py-1 sm:py-1.5 rounded-md sm:rounded-lg bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400 font-semibold text-xs sm:text-sm group-hover:bg-amber-100 dark:group-hover:bg-amber-900/30 transition-all group-hover:gap-2">
<span className="text-amber-500 dark:text-amber-400"></span>
</span>
</div>
</div>
</Link>
</div>
)}
</div>
</div>
</section>
);
}