This commit is contained in:
eric
2026-03-16 06:53:23 -05:00
parent c54d46517c
commit c54c236d1b
13 changed files with 538 additions and 359 deletions

View File

@@ -8,12 +8,16 @@ interface AddressLinkProps {
className?: string;
}
/** 红山地址展示:龙华区 | 🚇红山地铁站 | Loft民宿 */
const HONGSHAN_DISPLAY = "龙华区 | 🚇 红山地铁站 | Loft民宿";
/** 可点击的地址,点击打开地图(使用 span 避免嵌套在 Link 内时的 a 标签冲突) */
export default function AddressLink({ place, city, className = "" }: AddressLinkProps) {
const isHongshan = place.includes("红山");
const mapUrl = isHongshan
? `https://www.google.com/maps?q=${HONGSHAN_COORDS.lat},${HONGSHAN_COORDS.lng}`
: `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(`${city}${place}`)}`;
? `https://api.map.baidu.com/marker?location=${HONGSHAN_COORDS.lat},${HONGSHAN_COORDS.lng}&title=${encodeURIComponent(place)}&output=html`
: `https://map.baidu.com/search?query=${encodeURIComponent(`${city}${place}`)}`;
const displayText = isHongshan ? HONGSHAN_DISPLAY : place;
return (
<span
@@ -35,7 +39,7 @@ export default function AddressLink({ place, city, className = "" }: AddressLink
title="点击打开地图"
>
<span className="inline-block" aria-hidden>📍</span>
{place}
{displayText}
</span>
);
}

View File

@@ -31,24 +31,23 @@ export default function HeroSection() {
}}
/>
<div className="relative max-w-[1000px] mx-auto px-4 sm:px-6 md:px-8 py-10 sm:py-16 md:py-20">
<div className="flex flex-col lg:flex-row lg:items-center lg:gap-12 xl:gap-16">
<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-2xl sm:text-3xl md:text-4xl lg:text-[2.5rem] xl:text-[2.75rem] font-bold tracking-tight leading-[1.25]">
<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>
<br />
<span className="text-stone-800 dark:text-stone-100"></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">
<div className="mt-6 sm:mt-8 hidden sm:block">
<Link
href="/join"
className="inline-flex items-center justify-center bg-amber-600 hover:bg-amber-700 text-white px-5 py-2.5 sm:px-6 sm:py-3 rounded-xl text-sm font-medium shadow-lg shadow-amber-900/20 transition-all active:scale-[0.98]"
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>
@@ -56,13 +55,13 @@ export default function HeroSection() {
</div>
{mainSession && (
<div className="w-full lg:w-[520px] xl:w-[580px] shrink-0 mt-8 lg:mt-0">
<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-2xl overflow-hidden bg-white dark:bg-stone-900 shadow-xl shadow-stone-200/60 dark:shadow-stone-950/80 border border-stone-100 dark:border-stone-800 hover:shadow-2xl hover:shadow-amber-900/15 transition-all duration-300 hover:-translate-y-1"
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-[4/3] lg:aspect-auto lg:min-h-[180px] w-full bg-stone-100 dark:bg-stone-800 overflow-hidden shrink-0">
<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}
@@ -78,11 +77,11 @@ export default function HeroSection() {
</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-2 left-2 flex flex-wrap gap-1.5 lg:bottom-3 lg:left-3">
<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-[10px] font-semibold bg-white/95 dark:bg-stone-900/95 backdrop-blur-sm px-2.5 py-1 rounded-md text-stone-700 dark:text-stone-200"
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>
@@ -91,33 +90,33 @@ export default function HeroSection() {
</div>
{/* 内容区 - 右侧/下方 */}
<div className="flex-1 flex flex-col justify-center p-5 lg:p-6 lg:pl-6 min-w-0">
<div className="inline-flex items-center gap-2 mb-3">
<span className="w-1 h-4 rounded-full bg-amber-500 dark:bg-amber-500" aria-hidden />
<p className="text-[10px] font-semibold text-amber-600 dark:text-amber-400 uppercase tracking-[0.2em]">
<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-lg lg:text-xl leading-tight tracking-tight">
<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-3 space-y-1.5">
<p className="text-sm text-stone-500 dark:text-stone-400">
<span className="inline-block w-14 text-stone-400 dark:text-stone-500"></span>
<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-sm text-stone-500 dark:text-stone-400">
<span className="inline-block w-14 text-stone-400 dark:text-stone-500"></span>
<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-sm text-stone-500 dark:text-stone-400">
<span className="inline-block w-14 text-stone-400 dark:text-stone-500"></span>
<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-4 pt-4 border-t border-stone-100 dark:border-stone-800 flex items-center justify-between gap-4">
<div className="flex items-center gap-3 min-w-0">
<div className="flex -space-x-2.5 shrink-0">
<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;
@@ -126,7 +125,7 @@ export default function HeroSection() {
return (
<span
key={i}
className={`relative w-8 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-xs font-medium text-amber-700 dark:text-amber-300"}`}
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();
@@ -143,11 +142,11 @@ export default function HeroSection() {
);
})}
</div>
<span className="text-xs text-stone-500 dark:text-stone-400 truncate">
<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.5 px-3 py-1.5 rounded-lg bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400 font-semibold text-sm group-hover:bg-amber-100 dark:group-hover:bg-amber-900/30 transition-all group-hover:gap-2">
<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>

View File

@@ -0,0 +1,57 @@
"use client";
import Link from "next/link";
import Image from "next/image";
interface HostLinkProps {
host: { id: string; name: string; avatar: string; bio: string; link?: string };
/** 志愿者招募:无审核通过时显示「立即申请」+ 免费标识 */
ctaText?: string;
showFreeBadge?: boolean;
}
/** 移动端优先尝试唤醒小红书 appPC 新窗口打开 */
export default function HostLink({ host, ctaText, showFreeBadge }: HostLinkProps) {
const href = "link" in host ? host.link : `/host/${host.id}`;
const isXiaohongshu = typeof href === "string" && (href.includes("xhslink.com") || href.includes("xiaohongshu.com"));
const handleClick = (e: React.MouseEvent) => {
if (!isXiaohongshu || typeof href !== "string") return;
const isMobile = /Android|iPhone|iPad|iPod|webOS|Mobile/i.test(navigator.userAgent);
if (isMobile) {
e.preventDefault();
// 移动端同窗口打开xhslink.com 会尝试唤起小红书 app未安装则打开网页
window.location.assign(href);
}
};
if (typeof href !== "string") return null;
return (
<Link
href={href}
target={href.startsWith("http") ? "_blank" : undefined}
rel={href.startsWith("http") ? "noopener noreferrer" : undefined}
onClick={handleClick}
className="flex gap-3 sm:gap-4 p-4 sm:p-5 rounded-xl sm:rounded-2xl bg-white dark:bg-stone-900/80 border border-stone-200/80 dark:border-stone-700/80 hover:border-amber-200 dark:hover:border-amber-900/50 hover:shadow-lg transition-all"
>
<div className="relative w-14 h-14 sm:w-16 sm:h-16 shrink-0 rounded-full overflow-hidden border-2 border-amber-100 dark:border-amber-900/30">
<Image src={host.avatar} alt={host.name} fill sizes="64px" className="object-cover" unoptimized />
</div>
<div className="min-w-0 flex-1 text-left">
<div className="flex items-center gap-2 flex-wrap">
<h3 className="font-semibold text-stone-800 dark:text-stone-100 text-sm sm:text-base">{host.name}</h3>
{showFreeBadge && (
<span className="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[10px] font-medium bg-emerald-100 dark:bg-emerald-900/40 text-emerald-700 dark:text-emerald-300">
FREE
</span>
)}
</div>
<p className="mt-0.5 text-xs sm:text-sm text-stone-500 dark:text-stone-400 line-clamp-2">{host.bio}</p>
<span className="inline-flex items-center gap-1 mt-2 text-amber-600 dark:text-amber-400 text-xs font-medium">
{ctaText ?? "查看主页"}
</span>
</div>
</Link>
);
}