's'
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { HONGSHAN_COORDS } from "@/config/home.config";
|
||||
|
||||
interface AddressLinkProps {
|
||||
place: string;
|
||||
city: string;
|
||||
@@ -8,8 +10,10 @@ interface AddressLinkProps {
|
||||
|
||||
/** 可点击的地址,点击打开地图(使用 span 避免嵌套在 Link 内时的 a 标签冲突) */
|
||||
export default function AddressLink({ place, city, className = "" }: AddressLinkProps) {
|
||||
const query = encodeURIComponent(`${city}${place}`);
|
||||
const mapUrl = `https://www.google.com/maps/search/?api=1&query=${query}`;
|
||||
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}`)}`;
|
||||
|
||||
return (
|
||||
<span
|
||||
|
||||
@@ -5,12 +5,13 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import SessionCoverImage from "./SessionCoverImage";
|
||||
import AddressLink from "./AddressLink";
|
||||
import { SESSIONS, RECENT_JOINERS } from "@/config/home.config";
|
||||
import { getSessionsWithDates, RECENT_JOINERS } from "@/config/home.config";
|
||||
|
||||
const MOCK_JOINERS = RECENT_JOINERS.slice(0, 3);
|
||||
|
||||
export default function HeroSection() {
|
||||
const mainSession = SESSIONS[0];
|
||||
const sessions = getSessionsWithDates();
|
||||
const mainSession = sessions[0];
|
||||
const [stats, setStats] = useState<{ count: number; joiners: { nickname: string }[] } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -34,96 +35,121 @@ export default function HeroSection() {
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:gap-12 xl:gap-16">
|
||||
<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]">
|
||||
<span className="hero-gradient-text">周末留两小时</span>
|
||||
<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">认识更多有趣的人</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">
|
||||
深圳 · 6–8 人小圈子
|
||||
深圳 · 4–8 人小圈子
|
||||
</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">
|
||||
<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]"
|
||||
>
|
||||
填表申请本周场次
|
||||
立即报名
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{mainSession && (
|
||||
<div className="w-full lg:w-[280px] xl:w-[300px] shrink-0 mt-8 lg:mt-0">
|
||||
<div className="w-full lg:w-[520px] xl:w-[580px] shrink-0 mt-8 lg:mt-0">
|
||||
<Link
|
||||
href={`/join?session=${mainSession.id}`}
|
||||
className="block rounded-2xl overflow-hidden border border-stone-200 dark:border-stone-700 bg-white dark:bg-stone-900 shadow-lg shadow-stone-900/5 hover:shadow-xl hover:shadow-amber-900/10 transition-all ring-1 ring-stone-100 dark:ring-stone-800"
|
||||
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"
|
||||
>
|
||||
<div className="relative aspect-[4/3] w-full bg-stone-100 dark:bg-stone-800 overflow-hidden">
|
||||
{/* 图片区 - 左侧/上方 */}
|
||||
<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">
|
||||
{mainSession.coverImage ? (
|
||||
<SessionCoverImage
|
||||
src={mainSession.coverImage}
|
||||
alt={mainSession.title}
|
||||
type={mainSession.type}
|
||||
fill
|
||||
sizes="(max-width: 1024px) 100vw, 300px"
|
||||
className="object-cover"
|
||||
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-3xl bg-stone-200 dark:bg-stone-700">
|
||||
<div className="absolute inset-0 flex items-center justify-center text-4xl bg-stone-200 dark:bg-stone-700">
|
||||
☕
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute top-2 left-2 right-2 flex flex-wrap gap-1">
|
||||
<span className="inline-block text-[10px] font-medium bg-white/90 dark:bg-stone-900/90 backdrop-blur px-2 py-0.5 rounded text-stone-700 dark:text-stone-200">
|
||||
周末下午
|
||||
</span>
|
||||
<span className="inline-block text-[10px] font-medium bg-white/90 dark:bg-stone-900/90 backdrop-blur px-2 py-0.5 rounded text-stone-700 dark:text-stone-200">
|
||||
6–8 人
|
||||
</span>
|
||||
<span className="inline-block text-[10px] font-medium bg-white/90 dark:bg-stone-900/90 backdrop-blur px-2 py-0.5 rounded text-stone-700 dark:text-stone-200">
|
||||
公开场地
|
||||
</span>
|
||||
<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">
|
||||
{["周末", "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"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 sm:p-5 border-t border-stone-100 dark:border-stone-800">
|
||||
<p className="text-[10px] sm:text-xs font-medium text-amber-600 dark:text-amber-400/90 uppercase tracking-wider mb-1">
|
||||
本周场次
|
||||
</p>
|
||||
<h3 className="font-semibold text-stone-800 dark:text-stone-100 text-sm sm:text-base">{mainSession.title}</h3>
|
||||
<p className="text-xs sm:text-sm text-stone-500 dark:text-stone-400 mt-0.5">
|
||||
{mainSession.date} {mainSession.time} · <AddressLink place={mainSession.place} city={mainSession.city} />
|
||||
</p>
|
||||
<span className="inline-flex items-center gap-1 mt-3 text-amber-600 dark:text-amber-400 font-medium text-xs sm:text-sm">
|
||||
报名这场 →
|
||||
</span>
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<div className="flex -space-x-2 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-7 h-7 rounded-full overflow-hidden border-2 border-white dark:border-stone-900 ring-1 ring-stone-200/50 dark:ring-stone-700/50 shrink-0 inline-block 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"}`}
|
||||
title={nickname}
|
||||
data-xiaohongshu-url={xhUrl}
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
{showAvatar ? (
|
||||
<Image src={avatar} alt="" fill sizes="28px" className="object-cover" unoptimized />
|
||||
) : (
|
||||
<span className="flex items-center justify-center w-full h-full">{nickname.slice(0, 1) || "?"}</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 内容区 - 右侧/下方 */}
|
||||
<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]">
|
||||
本周场次
|
||||
</p>
|
||||
</div>
|
||||
<h3 className="font-bold text-stone-800 dark:text-stone-100 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>
|
||||
{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>
|
||||
{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>
|
||||
<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">
|
||||
{(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-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"}`}
|
||||
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-xs text-stone-500 dark:text-stone-400 truncate">
|
||||
{stats?.count ? `${stats.count} 人已报名` : "有人报名中"}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-stone-500 dark:text-stone-400">
|
||||
{stats?.count ? `${stats.count} 人已报名` : "有人报名中"}
|
||||
<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="text-amber-500 dark:text-amber-400">→</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import SessionCoverImage from "./SessionCoverImage";
|
||||
import AddressLink from "./AddressLink";
|
||||
import { SESSIONS, ACTIVITY_TYPES, SESSION_TAGS, RECENT_JOINERS } from "@/config/home.config";
|
||||
import { getSessionsWithDates, ACTIVITY_TYPES, SESSION_TAGS, RECENT_JOINERS } from "@/config/home.config";
|
||||
|
||||
interface JoinStats {
|
||||
count: number;
|
||||
@@ -18,7 +18,7 @@ export default function SessionCard({
|
||||
session,
|
||||
index,
|
||||
}: {
|
||||
session: (typeof SESSIONS)[0];
|
||||
session: (ReturnType<typeof getSessionsWithDates>)[0];
|
||||
index: number;
|
||||
}) {
|
||||
const [stats, setStats] = useState<JoinStats | null>(null);
|
||||
@@ -35,7 +35,7 @@ export default function SessionCard({
|
||||
href={`/join?session=${encodeURIComponent(session.id)}`}
|
||||
className="group flex flex-col sm:flex-row 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 hover:shadow-amber-900/5 transition-all"
|
||||
>
|
||||
<div className="relative w-full sm:w-24 sm:h-24 shrink-0 aspect-[16/10] sm:aspect-square rounded-lg sm:rounded-xl overflow-hidden bg-stone-100 dark:bg-stone-800">
|
||||
<div className="relative w-full sm:w-32 sm:min-w-[128px] shrink-0 aspect-[16/10] rounded-lg sm:rounded-xl overflow-hidden bg-stone-100 dark:bg-stone-800">
|
||||
{session.coverImage ? (
|
||||
<SessionCoverImage
|
||||
src={session.coverImage}
|
||||
@@ -89,10 +89,17 @@ export default function SessionCard({
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
className={`relative w-7 h-7 rounded-full overflow-hidden border-2 border-white dark:border-stone-900 ring-1 ring-stone-200/50 dark:ring-stone-700/50 shrink-0 inline-block 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-7 h-7 rounded-full overflow-hidden border-2 border-white dark:border-stone-900 ring-1 ring-stone-200/50 dark:ring-stone-700/50 shrink-0 inline-block ${xhUrl ? "cursor-pointer" : "cursor-default"}`}
|
||||
title={nickname}
|
||||
data-xiaohongshu-url={xhUrl}
|
||||
onClick={(e) => e.preventDefault()}
|
||||
onClick={(e) => {
|
||||
if (xhUrl) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.open(xhUrl, "_blank", "noopener,noreferrer");
|
||||
} else {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{showAvatar ? (
|
||||
<Image src={avatar} alt="" fill sizes="28px" className="object-cover" unoptimized />
|
||||
|
||||
Reference in New Issue
Block a user