'style3
This commit is contained in:
35
app/components/AvatarStack.tsx
Normal file
35
app/components/AvatarStack.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { RECENT_JOINERS } from "@/config/home.config";
|
||||
|
||||
export default function AvatarStack() {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-3 sm:gap-4">
|
||||
<div className="flex -space-x-2.5 shrink-0">
|
||||
{RECENT_JOINERS.slice(0, 6).map((u, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="relative w-10 h-10 rounded-full overflow-hidden border-2 border-[#faf8f5] dark:border-stone-950 shadow-sm ring-1 ring-stone-200/50 dark:ring-stone-700/50"
|
||||
title={u.name}
|
||||
>
|
||||
<Image
|
||||
src={u.avatar}
|
||||
alt={u.name}
|
||||
fill
|
||||
sizes="40px"
|
||||
className="object-cover"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-sm text-stone-600 dark:text-stone-400 min-w-0">
|
||||
<span className="font-medium text-stone-800 dark:text-stone-200">
|
||||
{RECENT_JOINERS.map((u) => u.name).join(" ")}
|
||||
</span>
|
||||
<span className="ml-1">等本月新加入</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
26
app/components/CitySelect.tsx
Normal file
26
app/components/CitySelect.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { CITIES } from "@/config/home.config";
|
||||
|
||||
interface CitySelectProps {
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
className?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export default function CitySelect({ value, onChange, className = "", required }: CitySelectProps) {
|
||||
return (
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
required={required}
|
||||
className={`form-input appearance-none ${value ? "text-stone-800 dark:text-stone-100" : "text-stone-400 dark:text-stone-500"} ${className}`}
|
||||
>
|
||||
<option value="" disabled>请选择城市</option>
|
||||
{CITIES.map((c) => (
|
||||
<option key={c} value={c}>{c}</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ export default function Footer() {
|
||||
return (
|
||||
<footer className="border-t border-stone-200 dark:border-stone-800 mt-12 py-8">
|
||||
<div className="max-w-[900px] mx-auto px-5 sm:px-8 text-center text-sm text-stone-500 dark:text-stone-400">
|
||||
<p>© {new Date().getFullYear()} Salon. 北京周末新朋友活动。</p>
|
||||
<p>© {new Date().getFullYear()} Salon. 深圳·广州·惠州 同城活动。</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
|
||||
17
app/components/Header.tsx
Normal file
17
app/components/Header.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import ThemeToggle from "./ThemeToggle";
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<header className="sticky top-0 z-40 w-full border-b border-stone-200/80 dark:border-stone-800 bg-[#faf8f5]/95 dark:bg-stone-950/95 backdrop-blur">
|
||||
<div className="max-w-[900px] mx-auto px-4 sm:px-6 h-14 flex items-center justify-between">
|
||||
<Link href="/" className="font-semibold text-stone-800 dark:text-stone-100 text-base">
|
||||
Salon
|
||||
</Link>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import SessionCoverImage from "./SessionCoverImage";
|
||||
import { SESSIONS } from "@/config/home.config";
|
||||
|
||||
export default function HeroSection() {
|
||||
@@ -14,63 +15,56 @@ export default function HeroSection() {
|
||||
radial-gradient(ellipse 60% 40% at 80% 80%, rgba(180,83,9,0.05) 0%, transparent 50%)`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 dark:opacity-60"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.03'/%3E%3C/svg%3E")`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative max-w-[1000px] mx-auto px-5 sm:px-8 py-14 sm:py-20 md:py-24">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:gap-16">
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-amber-700 dark:text-amber-400/90 mb-4 tracking-wide">
|
||||
北京 · 周末线下
|
||||
<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="flex-1 min-w-0">
|
||||
<p className="text-xs sm:text-sm font-medium text-amber-700 dark:text-amber-400/90 mb-3 sm:mb-4 tracking-wide flex items-center gap-1.5">
|
||||
<span>📍</span> 深圳 · 广州 · 惠州
|
||||
</p>
|
||||
<h1 className="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight leading-[1.15]">
|
||||
<h1 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight leading-[1.2]">
|
||||
<span className="hero-gradient-text">周末抽两小时</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-5 text-stone-600 dark:text-stone-400 text-base sm:text-lg max-w-md leading-relaxed">
|
||||
氛围轻松,不用尬聊。公开场地,填表即报。
|
||||
<p className="mt-4 sm:mt-5 text-stone-600 dark:text-stone-400 text-sm sm:text-base lg:text-lg max-w-md leading-relaxed">
|
||||
😌 氛围轻松 · 📍 公开场地 · 填表即报
|
||||
</p>
|
||||
<div className="mt-8 flex flex-wrap gap-3">
|
||||
<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-6 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 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>
|
||||
{mainSession && (
|
||||
<Link
|
||||
href={`/join?session=${mainSession.id}`}
|
||||
className="inline-flex items-center justify-center border border-stone-200 dark:border-stone-700 text-stone-700 dark:text-stone-300 px-6 py-3 rounded-xl text-sm font-medium hover:bg-stone-100 dark:hover:bg-stone-800/50 transition-colors"
|
||||
>
|
||||
{mainSession.title} · {mainSession.date}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{mainSession && (
|
||||
<div className="lg:w-[300px] shrink-0 mt-10 lg:mt-0">
|
||||
{mainSession && mainSession.coverImage && (
|
||||
<div className="w-full lg:w-[260px] xl:w-[300px] shrink-0 mt-6 lg:mt-0">
|
||||
<Link
|
||||
href={`/join?session=${mainSession.id}`}
|
||||
className="block rounded-2xl overflow-hidden border border-stone-200/80 dark:border-stone-700/80 bg-white dark:bg-stone-900/80 shadow-xl shadow-stone-900/5 hover:shadow-2xl hover:shadow-amber-900/10 transition-all"
|
||||
>
|
||||
<div className="aspect-[4/3] bg-gradient-to-br from-amber-500/20 via-amber-100/50 to-stone-100 dark:from-amber-900/30 dark:via-stone-800 dark:to-stone-900 flex items-center justify-center">
|
||||
<span className="text-6xl">☕</span>
|
||||
<div className="relative aspect-[4/3] w-full bg-stone-100 dark:bg-stone-800 overflow-hidden">
|
||||
<SessionCoverImage
|
||||
src={mainSession.coverImage}
|
||||
alt={mainSession.title}
|
||||
type={mainSession.type}
|
||||
fill
|
||||
sizes="(max-width: 1024px) 100vw, 300px"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-5">
|
||||
<p className="text-xs font-medium text-amber-600 dark:text-amber-400/90 uppercase tracking-wider mb-1">
|
||||
<div className="p-4 sm:p-5">
|
||||
<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">{mainSession.title}</h3>
|
||||
<p className="text-sm text-stone-500 dark:text-stone-400 mt-1">
|
||||
<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}
|
||||
</p>
|
||||
<span className="inline-flex items-center gap-1 mt-4 text-amber-600 dark:text-amber-400 font-medium text-sm">
|
||||
<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>
|
||||
|
||||
37
app/components/SessionCoverImage.tsx
Normal file
37
app/components/SessionCoverImage.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
interface SessionCoverImageProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
type?: "skill-exchange" | "side-hustle";
|
||||
className?: string;
|
||||
fill?: boolean;
|
||||
sizes?: string;
|
||||
}
|
||||
|
||||
export default function SessionCoverImage({ src, alt, type, className = "", fill, sizes }: SessionCoverImageProps) {
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-stone-200 dark:bg-stone-700 text-2xl sm:text-3xl">
|
||||
{type === "skill-exchange" ? "🔄" : "☕"}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
src={src}
|
||||
alt={alt}
|
||||
fill={fill}
|
||||
sizes={sizes}
|
||||
className={className}
|
||||
unoptimized
|
||||
onError={() => setError(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
18
app/components/ThemeProvider.tsx
Normal file
18
app/components/ThemeProvider.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
|
||||
export default function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<NextThemesProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
enableColorScheme
|
||||
storageKey="salon-theme"
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
</NextThemesProvider>
|
||||
);
|
||||
}
|
||||
45
app/components/ThemeToggle.tsx
Normal file
45
app/components/ThemeToggle.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function ThemeToggle() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
useEffect(() => setMounted(true), []);
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="切换主题"
|
||||
className="p-2.5 rounded-lg text-stone-500 hover:bg-stone-100 dark:hover:bg-stone-800 transition-colors"
|
||||
>
|
||||
<span className="w-5 h-5 block" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const isDark = theme === "dark";
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={isDark ? "切换到日间模式" : "切换到夜间模式"}
|
||||
onClick={() => setTheme(isDark ? "light" : "dark")}
|
||||
className="p-2.5 rounded-lg text-stone-500 hover:text-stone-700 hover:bg-stone-100 dark:text-stone-400 dark:hover:text-stone-200 dark:hover:bg-stone-800 transition-colors"
|
||||
>
|
||||
{isDark ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-5 h-5">
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-5 h-5">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* 使 Tailwind dark: 变体响应 class="dark",与 next-themes 配合 */
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
:root {
|
||||
--background: #faf8f5;
|
||||
--foreground: #1c1917;
|
||||
@@ -7,11 +10,10 @@
|
||||
--accent-hover: #92400e;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0c0a09;
|
||||
--foreground: #fafaf9;
|
||||
}
|
||||
html.dark {
|
||||
--background: #0c0a09;
|
||||
--foreground: #fafaf9;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -19,6 +21,10 @@ body {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.safe-area-pb {
|
||||
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||
}
|
||||
|
||||
.hero-gradient-text {
|
||||
background: linear-gradient(135deg, #b45309 0%, #d97706 50%, #f59e0b 100%);
|
||||
-webkit-background-clip: text;
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
import { useState, useEffect, type FormEvent } from "react";
|
||||
import Link from "next/link";
|
||||
import SessionCoverImage from "../components/SessionCoverImage";
|
||||
import { SESSIONS } from "@/config/home.config";
|
||||
import CitySelect from "../components/CitySelect";
|
||||
import ThemeToggle from "../components/ThemeToggle";
|
||||
|
||||
const ageOptions = ["18-24", "25-30", "31-35", "36-40", "40+"];
|
||||
const firstTimeOptions = ["是,第一次参加", "否,参加过类似活动"];
|
||||
@@ -29,7 +32,7 @@ function getSessionInfo(sessionId: string) {
|
||||
|
||||
export default function JoinPage() {
|
||||
const [form, setForm] = useState<FormData>({
|
||||
city: "北京",
|
||||
city: "",
|
||||
session: "",
|
||||
nickname: "",
|
||||
ageRange: "",
|
||||
@@ -44,7 +47,14 @@ export default function JoinPage() {
|
||||
|
||||
useEffect(() => {
|
||||
const sid = getSessionFromUrl();
|
||||
if (sid) setForm((prev) => ({ ...prev, session: sid }));
|
||||
if (sid) {
|
||||
const s = SESSIONS.find((x) => x.id === sid);
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
session: sid,
|
||||
city: s?.city || prev.city,
|
||||
}));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const set = (key: keyof FormData, value: string | boolean) =>
|
||||
@@ -85,18 +95,18 @@ export default function JoinPage() {
|
||||
|
||||
if (submitted) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-[#faf8f5] dark:bg-stone-950 px-4">
|
||||
<div className="w-full max-w-md animate-fade-in rounded-2xl bg-white dark:bg-stone-900 p-10 text-center shadow-lg border border-stone-200 dark:border-stone-800">
|
||||
<div className="mx-auto mb-5 flex h-20 w-20 items-center justify-center rounded-full bg-emerald-50 dark:bg-emerald-900/30 text-5xl">
|
||||
<div className="flex min-h-screen items-center justify-center bg-[#faf8f5] dark:bg-stone-950 px-4 py-8">
|
||||
<div className="w-full max-w-md animate-fade-in rounded-2xl bg-white dark:bg-stone-900 p-6 sm:p-10 text-center shadow-lg border border-stone-200 dark:border-stone-800">
|
||||
<div className="mx-auto mb-4 sm:mb-5 flex h-16 w-16 sm:h-20 sm:w-20 items-center justify-center rounded-full bg-emerald-50 dark:bg-emerald-900/30 text-4xl sm:text-5xl">
|
||||
✅
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-slate-800 dark:text-slate-100">报名成功</h2>
|
||||
<p className="mt-3 text-slate-500 dark:text-slate-400">
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-stone-800 dark:text-stone-100">报名成功</h2>
|
||||
<p className="mt-2 sm:mt-3 text-sm sm:text-base text-stone-500 dark:text-stone-400">
|
||||
我们会尽快通过您留下的联系方式与您确认
|
||||
</p>
|
||||
<Link
|
||||
href="/"
|
||||
className="mt-8 inline-flex items-center gap-2 rounded-xl bg-amber-600 px-8 py-3 font-medium text-white transition-colors hover:bg-amber-700"
|
||||
className="mt-6 sm:mt-8 inline-flex items-center gap-2 rounded-xl bg-amber-600 px-6 sm:px-8 py-2.5 sm:py-3 font-medium text-white text-sm sm:text-base transition-colors hover:bg-amber-700"
|
||||
>
|
||||
← 返回首页
|
||||
</Link>
|
||||
@@ -107,69 +117,83 @@ export default function JoinPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#faf8f5] dark:bg-stone-950">
|
||||
<div className="mx-auto max-w-2xl px-0 py-0 sm:px-6 sm:py-10 lg:py-14">
|
||||
{/* 顶部栏 */}
|
||||
<header className="sticky top-0 z-40 w-full border-b border-stone-200/80 dark:border-stone-800 bg-[#faf8f5]/95 dark:bg-stone-950/95 backdrop-blur">
|
||||
<div className="max-w-2xl mx-auto px-4 h-12 sm:h-14 flex items-center justify-between">
|
||||
<Link href="/" className="text-sm font-medium text-stone-600 dark:text-stone-400">
|
||||
← 返回
|
||||
</Link>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="mx-auto max-w-2xl px-0 py-0 sm:px-6 sm:py-8 lg:py-10">
|
||||
<div className="overflow-hidden bg-white dark:bg-stone-900 shadow-sm sm:rounded-t-2xl">
|
||||
<div className="relative h-40 overflow-hidden bg-gradient-to-br from-amber-500 via-amber-600 to-amber-700 sm:h-52">
|
||||
<div className="relative h-32 sm:h-40 md:h-52 overflow-hidden bg-gradient-to-br from-amber-500 via-amber-600 to-amber-700">
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center text-white">
|
||||
<div className="text-6xl sm:text-7xl">☕</div>
|
||||
<div className="mt-2 text-sm font-medium opacity-90">
|
||||
北京周末新朋友活动
|
||||
<div className="text-5xl sm:text-6xl md:text-7xl" aria-hidden>☕</div>
|
||||
<div className="mt-1.5 sm:mt-2 text-xs sm:text-sm font-medium opacity-90 flex items-center gap-1.5">
|
||||
<span>📍</span> 深圳 · 广州 · 惠州
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-5 py-5 text-center sm:px-8 sm:py-7">
|
||||
<h1 className="text-xl font-bold text-slate-800 dark:text-slate-100 sm:text-2xl">
|
||||
<div className="px-4 sm:px-6 py-4 sm:py-5 text-center">
|
||||
<h1 className="text-lg sm:text-xl md:text-2xl font-bold text-stone-800 dark:text-stone-100">
|
||||
活动报名
|
||||
</h1>
|
||||
<p className="mt-3 whitespace-pre-line text-sm leading-relaxed text-slate-500 dark:text-slate-400 sm:text-base">
|
||||
填个简单表单即可,无需注册登录,不收费
|
||||
<p className="mt-2 sm:mt-3 text-xs sm:text-sm leading-relaxed text-stone-500 dark:text-stone-400">
|
||||
✍️ 填表即可 · 无需注册 · 不收费
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedSession && (
|
||||
<div className="mt-5 rounded-xl bg-white dark:bg-stone-900 px-5 py-4 shadow-sm border border-stone-200 dark:border-stone-800">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-500 mb-1">已选场次</p>
|
||||
<p className="font-medium text-slate-800 dark:text-slate-100">{selectedSession.title}</p>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-500">{selectedSession.date} {selectedSession.time} · {selectedSession.place}</p>
|
||||
<div className="mt-4 sm:mt-5 mx-4 sm:mx-0 rounded-xl bg-white dark:bg-stone-900 px-4 sm:px-5 py-3 sm:py-4 shadow-sm border border-stone-200 dark:border-stone-800 flex gap-3">
|
||||
{selectedSession.coverImage && (
|
||||
<div className="relative w-16 h-16 sm:w-20 sm:h-20 shrink-0 rounded-lg overflow-hidden">
|
||||
<SessionCoverImage src={selectedSession.coverImage} alt="" type={selectedSession.type} fill sizes="80px" className="object-cover" />
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-0 flex-1 text-left">
|
||||
<p className="text-[10px] sm:text-xs text-stone-500 dark:text-stone-500 mb-0.5">已选场次</p>
|
||||
<p className="font-medium text-stone-800 dark:text-stone-100 text-sm sm:text-base">{selectedSession.title}</p>
|
||||
<p className="text-xs sm:text-sm text-stone-500 dark:text-stone-500">{selectedSession.date} {selectedSession.time} · {selectedSession.place}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="mt-0 bg-white dark:bg-stone-900 px-5 pb-8 pt-2 shadow-sm sm:mt-5 sm:rounded-xl sm:px-8 sm:py-8 sm:shadow-md border border-stone-200 dark:border-stone-800"
|
||||
className="mt-0 sm:mt-5 bg-white dark:bg-stone-900 px-4 sm:px-6 md:px-8 pb-8 pt-6 sm:pt-8 shadow-sm sm:rounded-b-2xl sm:shadow-md border border-t-0 sm:border border-stone-200 dark:border-stone-800"
|
||||
>
|
||||
<div className="grid gap-0 sm:grid-cols-2 sm:gap-6">
|
||||
<div className="space-y-4 sm:space-y-0 sm:grid sm:grid-cols-2 sm:gap-6">
|
||||
<Field label="📍 城市">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="如:北京"
|
||||
value={form.city}
|
||||
onChange={(e) => set("city", e.target.value)}
|
||||
required
|
||||
className="form-input"
|
||||
/>
|
||||
<CitySelect value={form.city} onChange={(v) => set("city", v)} required />
|
||||
</Field>
|
||||
|
||||
<Field label="📅 想参加的场次">
|
||||
<select
|
||||
value={form.session}
|
||||
onChange={(e) => set("session", e.target.value)}
|
||||
onChange={(e) => {
|
||||
const sid = e.target.value;
|
||||
const s = SESSIONS.find((x) => x.id === sid);
|
||||
setForm((prev) => ({ ...prev, session: sid, city: s?.city || prev.city }));
|
||||
}}
|
||||
required
|
||||
className={`form-input appearance-none ${form.session ? "text-slate-800 dark:text-slate-100" : "text-slate-400 dark:text-slate-500"}`}
|
||||
className={`form-input appearance-none ${form.session ? "text-stone-800 dark:text-stone-100" : "text-stone-400 dark:text-stone-500"}`}
|
||||
>
|
||||
<option value="" disabled>请选择</option>
|
||||
{SESSIONS.map((s) => (
|
||||
<option key={s.id} value={s.id}>
|
||||
{s.title} · {s.date} {s.time}
|
||||
{s.title} · {s.city} · {s.date} {s.time}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-0 sm:grid-cols-2 sm:gap-6">
|
||||
<div className="mt-4 sm:mt-6 space-y-4 sm:space-y-0 sm:grid sm:grid-cols-2 sm:gap-6">
|
||||
<Field label="昵称">
|
||||
<input
|
||||
type="text"
|
||||
@@ -187,7 +211,7 @@ export default function JoinPage() {
|
||||
value={form.ageRange}
|
||||
onChange={(e) => set("ageRange", e.target.value)}
|
||||
required
|
||||
className={`form-input appearance-none ${form.ageRange ? "text-slate-800 dark:text-slate-100" : "text-slate-400 dark:text-slate-500"}`}
|
||||
className={`form-input appearance-none ${form.ageRange ? "text-stone-800 dark:text-stone-100" : "text-stone-400 dark:text-stone-500"}`}
|
||||
>
|
||||
<option value="" disabled>请选择</option>
|
||||
{ageOptions.map((o) => (
|
||||
@@ -197,8 +221,8 @@ export default function JoinPage() {
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div className="border-b border-slate-100 dark:border-gray-700 py-4 sm:py-5">
|
||||
<label className="mb-2 block text-base font-bold text-slate-700 dark:text-slate-300 sm:text-[17px]">
|
||||
<div className="mt-4 sm:mt-6 pt-4 sm:pt-5 border-t border-stone-100 dark:border-stone-700">
|
||||
<label className="mb-2 block text-sm sm:text-base font-bold text-stone-700 dark:text-stone-300">
|
||||
微信号或手机号
|
||||
</label>
|
||||
<input
|
||||
@@ -212,8 +236,8 @@ export default function JoinPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border-b border-slate-100 dark:border-gray-700 py-4 sm:py-5">
|
||||
<label className="mb-2 block text-base font-bold text-slate-700 dark:text-slate-300 sm:text-[17px]">
|
||||
<div className="mt-4 sm:mt-5 pt-4 sm:pt-5 border-t border-stone-100 dark:border-stone-700">
|
||||
<label className="mb-2 block text-sm sm:text-base font-bold text-stone-700 dark:text-stone-300">
|
||||
一句话自我介绍
|
||||
</label>
|
||||
<input
|
||||
@@ -227,15 +251,15 @@ export default function JoinPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border-b border-slate-100 dark:border-gray-700 py-4 sm:py-5">
|
||||
<label className="mb-2 block text-base font-bold text-slate-700 dark:text-slate-300 sm:text-[17px]">
|
||||
<div className="mt-4 sm:mt-5 pt-4 sm:pt-5 border-t border-stone-100 dark:border-stone-700">
|
||||
<label className="mb-2 block text-sm sm:text-base font-bold text-stone-700 dark:text-stone-300">
|
||||
是否第一次参加类似活动
|
||||
</label>
|
||||
<select
|
||||
value={form.firstTime}
|
||||
onChange={(e) => set("firstTime", e.target.value)}
|
||||
required
|
||||
className={`form-input appearance-none ${form.firstTime ? "text-slate-800 dark:text-slate-100" : "text-slate-400 dark:text-slate-500"}`}
|
||||
className={`form-input appearance-none ${form.firstTime ? "text-stone-800 dark:text-stone-100" : "text-stone-400 dark:text-stone-500"}`}
|
||||
>
|
||||
<option value="" disabled>请选择</option>
|
||||
{firstTimeOptions.map((o) => (
|
||||
@@ -244,7 +268,7 @@ export default function JoinPage() {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="py-4 sm:py-5">
|
||||
<div className="mt-4 sm:mt-5 pt-4 sm:pt-5 border-t border-stone-100 dark:border-stone-700">
|
||||
<label className="flex items-start gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -253,13 +277,13 @@ export default function JoinPage() {
|
||||
required
|
||||
className="mt-1 rounded border-stone-300 text-amber-600 focus:ring-amber-500"
|
||||
/>
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
<span className="text-xs sm:text-sm text-stone-600 dark:text-stone-400">
|
||||
我已阅读并同意活动规则(公开场地、尊重边界、如有不适可随时离开)
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 sm:mt-10">
|
||||
<div className="mt-6 sm:mt-8 sm:mt-10">
|
||||
{error && (
|
||||
<p className="mb-4 rounded-lg bg-red-50 dark:bg-red-900/30 px-4 py-3 text-center text-sm text-red-600 dark:text-red-400">
|
||||
{error}
|
||||
@@ -268,15 +292,15 @@ export default function JoinPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="w-full rounded-xl bg-amber-600 py-3.5 text-lg font-medium text-white transition-colors hover:bg-amber-700 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-70 sm:py-4"
|
||||
className="w-full rounded-xl bg-amber-600 py-3 sm:py-3.5 sm:py-4 text-base sm:text-lg font-medium text-white transition-colors hover:bg-amber-700 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-70"
|
||||
>
|
||||
{submitting ? "提交中…" : "提交报名"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<Link href="/" className="text-sm text-stone-400 dark:text-stone-500 transition-colors hover:text-amber-600">
|
||||
<div className="mt-4 sm:mt-6 text-center px-4">
|
||||
<Link href="/" className="text-xs sm:text-sm text-stone-400 dark:text-stone-500 transition-colors hover:text-amber-600">
|
||||
← 返回首页
|
||||
</Link>
|
||||
</div>
|
||||
@@ -293,8 +317,8 @@ function Field({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative flex items-center gap-3 py-3.5 sm:flex-col sm:items-stretch sm:gap-0 sm:py-4 border-b border-slate-100 dark:border-gray-700 sm:border-b-0">
|
||||
<label className="w-[90px] shrink-0 text-[15px] font-bold text-slate-700 dark:text-slate-300 sm:mb-2 sm:w-auto sm:text-[17px]">
|
||||
<div className="relative flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-0 py-3 sm:py-4 border-b border-stone-100 dark:border-stone-700 sm:border-b-0 last:border-b-0">
|
||||
<label className="shrink-0 text-sm sm:text-base font-bold text-stone-700 dark:text-stone-300 sm:mb-0 sm:w-[100px]">
|
||||
{label}
|
||||
</label>
|
||||
<div className="relative min-w-0 flex-1">{children}</div>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import ThemeProvider from "./components/ThemeProvider";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "北京周末|同城活动·认识新朋友",
|
||||
description: "周末咖啡局、读书分享,认识同城新朋友。公开场地,填表报名,无需注册。",
|
||||
keywords: "北京周末活动,同城活动,认识新朋友,周末咖啡局,读书分享",
|
||||
title: "深圳广州惠州|同城活动·技能交换·副业沙龙",
|
||||
description: "技能交换、副业沙龙茶话会,认识同城新朋友。公开场地,填表报名,无需注册。",
|
||||
keywords: "深圳活动,广州活动,惠州活动,技能交换,副业沙龙,同城活动",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -13,8 +14,10 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="zh">
|
||||
<body className="antialiased">{children}</body>
|
||||
<html lang="zh" suppressHydrationWarning>
|
||||
<body className="antialiased">
|
||||
<ThemeProvider>{children}</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
163
app/page.tsx
163
app/page.tsx
@@ -2,51 +2,81 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import HeroSection from "./components/HeroSection";
|
||||
import SessionCoverImage from "./components/SessionCoverImage";
|
||||
import Footer from "./components/Footer";
|
||||
import { SESSIONS } from "@/config/home.config";
|
||||
import Header from "./components/Header";
|
||||
import AvatarStack from "./components/AvatarStack";
|
||||
import { SESSIONS, ACTIVITY_TYPES } from "@/config/home.config";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#faf8f5] dark:bg-stone-950 pb-24 sm:pb-12">
|
||||
<div className="min-h-screen bg-[#faf8f5] dark:bg-stone-950 pb-20 sm:pb-12">
|
||||
<Header />
|
||||
<HeroSection />
|
||||
|
||||
<main className="max-w-[900px] mx-auto px-5 sm:px-8 py-10 sm:py-14 space-y-14">
|
||||
{/* 信任标签 */}
|
||||
<div className="flex flex-wrap justify-center gap-x-8 gap-y-2 text-sm text-stone-600 dark:text-stone-400">
|
||||
<span>📍 公开场地</span>
|
||||
<span>👥 小组活动</span>
|
||||
<span>✍️ 填表即报</span>
|
||||
<span>🆓 不收费</span>
|
||||
<main className="max-w-[900px] mx-auto px-4 sm:px-6 md:px-8 py-8 sm:py-12 md:py-14 space-y-10 sm:space-y-14">
|
||||
{/* 信任标签 + 新成员 */}
|
||||
<div className="flex flex-col gap-4 sm:gap-6">
|
||||
<div className="flex flex-wrap justify-center gap-x-6 gap-y-1.5 sm:gap-x-8 text-xs sm:text-sm text-stone-600 dark:text-stone-400">
|
||||
<span>📍 公开场地</span>
|
||||
<span>👥 小组活动</span>
|
||||
<span>✍️ 填表即报</span>
|
||||
<span>🆓 不收费</span>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl sm:rounded-2xl bg-white dark:bg-stone-900/80 border border-stone-200/80 dark:border-stone-700/80 p-4 sm:p-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] sm:text-xs font-medium text-amber-600 dark:text-amber-400/90 uppercase tracking-wider mb-1.5 sm:mb-2">
|
||||
👋 本月新加入
|
||||
</p>
|
||||
<AvatarStack />
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 sm:gap-2 text-stone-500 dark:text-stone-400 text-xs sm:text-sm shrink-0">
|
||||
<span className="font-semibold text-amber-600 dark:text-amber-400">50+</span>
|
||||
<span>人已报名</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 本周场次 */}
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold text-stone-800 dark:text-stone-100 mb-5">
|
||||
本周活动
|
||||
<h2 className="text-lg sm:text-xl font-semibold text-stone-800 dark:text-stone-100 mb-4 sm:mb-5 flex items-center gap-2">
|
||||
<span>📅</span> 本周活动
|
||||
</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="grid gap-3 sm:gap-4 sm:grid-cols-2">
|
||||
{SESSIONS.map((s, i) => (
|
||||
<Link
|
||||
key={s.id}
|
||||
href={`/join?session=${encodeURIComponent(s.id)}`}
|
||||
className="group flex gap-4 p-5 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"
|
||||
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="w-14 h-14 shrink-0 rounded-xl bg-amber-100/80 dark:bg-amber-900/20 flex items-center justify-center text-2xl">
|
||||
{i === 0 ? "☕" : i === 1 ? "📖" : "👋"}
|
||||
<div className="relative w-full sm:w-20 sm:h-20 shrink-0 aspect-[16/10] sm:aspect-square rounded-lg sm:rounded-xl overflow-hidden bg-stone-100 dark:bg-stone-800">
|
||||
{s.coverImage ? (
|
||||
<SessionCoverImage src={s.coverImage} alt={s.title} type={s.type} fill sizes="(max-width: 640px) 100vw, 80px" className="object-cover" />
|
||||
) : (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-2xl">
|
||||
{s.type === "skill-exchange" ? "🔄" : "☕"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
{i === 0 && (
|
||||
<span className="inline-block text-[10px] font-medium text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20 px-2 py-0.5 rounded mb-2">
|
||||
<span className="inline-block text-[9px] sm:text-[10px] font-medium text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20 px-1.5 py-0.5 rounded mb-1.5">
|
||||
主推
|
||||
</span>
|
||||
)}
|
||||
<h3 className="font-semibold text-stone-800 dark:text-stone-100 group-hover:text-amber-700 dark:group-hover:text-amber-400">
|
||||
<span className="text-[10px] sm:text-xs text-amber-600 dark:text-amber-400/90">
|
||||
{ACTIVITY_TYPES[s.type]} · {s.city}
|
||||
</span>
|
||||
<h3 className="font-semibold text-stone-800 dark:text-stone-100 text-sm sm:text-base group-hover:text-amber-700 dark:group-hover:text-amber-400 mt-0.5">
|
||||
{s.title}
|
||||
</h3>
|
||||
<p className="text-sm text-stone-500 dark:text-stone-400 mt-0.5">
|
||||
<p className="text-xs sm:text-sm text-stone-500 dark:text-stone-400 mt-0.5">
|
||||
{s.date} {s.time} · {s.place}
|
||||
</p>
|
||||
<span className="inline-flex items-center gap-1 mt-3 text-amber-600 dark:text-amber-400 text-sm font-medium">
|
||||
<span className="inline-flex items-center gap-1 mt-2 sm:mt-3 text-amber-600 dark:text-amber-400 text-xs sm:text-sm font-medium">
|
||||
报名 →
|
||||
</span>
|
||||
</div>
|
||||
@@ -57,80 +87,77 @@ export default function Home() {
|
||||
|
||||
{/* 为什么来 */}
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold text-stone-800 dark:text-stone-100 mb-5">
|
||||
为什么很多人愿意来
|
||||
<h2 className="text-lg sm:text-xl font-semibold text-stone-800 dark:text-stone-100 mb-4 sm:mb-5 flex items-center gap-2">
|
||||
<span>💡</span> 为什么很多人愿意来
|
||||
</h2>
|
||||
<div className="rounded-2xl bg-white dark:bg-stone-900/80 border border-stone-200/80 dark:border-stone-700/80 p-6 sm:p-8">
|
||||
<div className="grid sm:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p className="font-medium text-stone-800 dark:text-stone-100">氛围轻松</p>
|
||||
<p className="text-sm text-stone-500 dark:text-stone-400 mt-1 leading-relaxed">
|
||||
咖啡馆、书店里的线下小聚,不用尬聊,聊得来就多聊几句。
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-stone-800 dark:text-stone-100">安全靠谱</p>
|
||||
<p className="text-sm text-stone-500 dark:text-stone-400 mt-1 leading-relaxed">
|
||||
公开场地,地址提前告知。尊重边界,不追问隐私。
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-stone-800 dark:text-stone-100">第一次来也 OK</p>
|
||||
<p className="text-sm text-stone-500 dark:text-stone-400 mt-1 leading-relaxed">
|
||||
很多人都是第一次参加,小组活动人数可控,不会尴尬。
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-stone-800 dark:text-stone-100">填表就行</p>
|
||||
<p className="text-sm text-stone-500 dark:text-stone-400 mt-1 leading-relaxed">
|
||||
无需注册登录,不用先付款,填个表单即可。
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl sm:rounded-2xl bg-white dark:bg-stone-900/80 border border-stone-200/80 dark:border-stone-700/80 p-4 sm:p-6 md:p-8">
|
||||
<div className="grid sm:grid-cols-2 gap-4 sm:gap-6">
|
||||
{[
|
||||
{ icon: "😌", title: "氛围轻松", desc: "咖啡馆、书店里的线下小聚,不用尬聊,聊得来就多聊几句。" },
|
||||
{ icon: "🛡️", title: "安全靠谱", desc: "公开场地,地址提前告知。尊重边界,不追问隐私。" },
|
||||
{ icon: "👍", title: "第一次来也 OK", desc: "很多人都是第一次参加,小组活动人数可控,不会尴尬。" },
|
||||
{ icon: "✍️", title: "填表就行", desc: "无需注册登录,不用先付款,填个表单即可。" },
|
||||
].map((item) => (
|
||||
<div key={item.title} className="flex gap-2.5 sm:gap-3">
|
||||
<span className="text-xl sm:text-2xl shrink-0">{item.icon}</span>
|
||||
<div>
|
||||
<p className="font-medium text-stone-800 dark:text-stone-100 text-sm sm:text-base">{item.title}</p>
|
||||
<p className="text-xs sm:text-sm text-stone-500 dark:text-stone-400 mt-0.5 leading-relaxed">{item.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ */}
|
||||
{/* FAQ - 可折叠,默认折叠 */}
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold text-stone-800 dark:text-stone-100 mb-5">
|
||||
常见问题
|
||||
<h2 className="text-lg sm:text-xl font-semibold text-stone-800 dark:text-stone-100 mb-4 sm:mb-5 flex items-center gap-2">
|
||||
<span>❓</span> 常见问题
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2 sm:space-y-3">
|
||||
{[
|
||||
{ q: "需要先注册吗?", a: "不需要,填表即可。" },
|
||||
{ q: "需要先付款吗?", a: "不需要,报名不收费。" },
|
||||
{ q: "第一次参加可以吗?", a: "可以,很多人都是第一次来。" },
|
||||
{ q: "地点什么时候通知?", a: "报名后会通过微信/短信告知,均为公开场地。" },
|
||||
].map((faq, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="rounded-xl bg-white dark:bg-stone-900/80 border border-stone-200/80 dark:border-stone-700/80 p-4"
|
||||
{ q: "需要先注册吗?", a: "不需要,填表即可。", icon: "🔐" },
|
||||
{ q: "需要先付款吗?", a: "不需要,报名不收费。", icon: "💰" },
|
||||
{ q: "第一次参加可以吗?", a: "可以,很多人都是第一次来。", icon: "✨" },
|
||||
{ q: "地点什么时候通知?", a: "报名后会通过微信/短信告知,均为公开场地。", icon: "📍" },
|
||||
].map((faq) => (
|
||||
<details
|
||||
key={faq.q}
|
||||
className="group rounded-lg sm:rounded-xl bg-white dark:bg-stone-900/80 border border-stone-200/80 dark:border-stone-700/80 overflow-hidden"
|
||||
>
|
||||
<p className="font-medium text-stone-800 dark:text-stone-100 text-sm">{faq.q}</p>
|
||||
<p className="text-sm text-stone-500 dark:text-stone-400 mt-0.5">{faq.a}</p>
|
||||
</div>
|
||||
<summary className="flex items-center gap-2.5 sm:gap-3 p-3 sm:p-4 cursor-pointer list-none [&::-webkit-details-marker]:hidden select-none">
|
||||
<span className="text-base sm:text-lg shrink-0">{faq.icon}</span>
|
||||
<p className="font-medium text-stone-800 dark:text-stone-100 text-xs sm:text-sm flex-1">{faq.q}</p>
|
||||
<span className="shrink-0 text-stone-400 text-xs transition-transform duration-200 group-open:rotate-180">▼</span>
|
||||
</summary>
|
||||
<div className="px-3 pb-3 sm:px-4 sm:pb-4 pt-0 flex gap-2.5 sm:gap-3">
|
||||
<span className="w-[1.25rem] sm:w-[1.5rem] shrink-0" aria-hidden />
|
||||
<p className="text-xs sm:text-sm text-stone-500 dark:text-stone-400">{faq.a}</p>
|
||||
</div>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<section className="pt-4">
|
||||
<section className="pt-2 sm:pt-4">
|
||||
<Link
|
||||
href="/join"
|
||||
className="block w-full bg-amber-600 hover:bg-amber-700 text-white text-center py-4 rounded-xl font-semibold shadow-lg shadow-amber-900/20 transition-all"
|
||||
className="block w-full bg-amber-600 hover:bg-amber-700 text-white text-center py-3.5 sm:py-4 rounded-xl font-semibold text-sm sm:text-base shadow-lg shadow-amber-900/20 transition-all"
|
||||
>
|
||||
立即报名 →
|
||||
✨ 立即报名 →
|
||||
</Link>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{/* 移动端悬浮 CTA */}
|
||||
<div className="fixed bottom-0 left-0 right-0 p-4 bg-white/95 dark:bg-stone-900/95 backdrop-blur border-t border-stone-200 dark:border-stone-800 sm:hidden z-50">
|
||||
<div className="fixed bottom-0 left-0 right-0 p-3 sm:p-4 bg-white/95 dark:bg-stone-900/95 backdrop-blur border-t border-stone-200 dark:border-stone-800 sm:hidden z-50 safe-area-pb">
|
||||
<Link
|
||||
href="/join"
|
||||
className="block w-full bg-amber-600 hover:bg-amber-700 text-white text-center py-3.5 rounded-xl font-medium"
|
||||
className="block w-full bg-amber-600 hover:bg-amber-700 text-white text-center py-3 rounded-xl font-medium text-sm"
|
||||
>
|
||||
报名参加
|
||||
✨ 报名参加
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
/** 本周活动场次 - 同城活动用 */
|
||||
/** 开放城市 - 其他待开放 */
|
||||
export const CITIES = ["深圳", "广州", "惠州"] as const;
|
||||
|
||||
/** 活动类型 */
|
||||
export const ACTIVITY_TYPES = {
|
||||
"skill-exchange": "技能交换",
|
||||
"side-hustle": "副业沙龙/茶话会",
|
||||
} as const;
|
||||
|
||||
/** 本周活动场次 */
|
||||
export const SESSIONS = [
|
||||
{ id: "session-1", date: "3月16日 周六", time: "14:00", place: "朝阳区某咖啡馆", title: "周末咖啡局" },
|
||||
{ id: "session-2", date: "3月17日 周日", time: "15:00", place: "海淀区某书店", title: "读书分享" },
|
||||
{ id: "session-3", date: "3月22日 周六", time: "14:00", place: "东城区某空间", title: "新朋友见面会" },
|
||||
{ id: "session-1", date: "3月16日 周六", time: "14:00", place: "南山区某咖啡馆", city: "深圳", type: "skill-exchange" as const, title: "周末技能交换", coverImage: "https://picsum.photos/seed/s1/400/300" },
|
||||
{ id: "session-2", date: "3月17日 周日", time: "15:00", place: "天河区某书店", city: "广州", type: "side-hustle" as const, title: "副业沙龙", coverImage: "https://picsum.photos/seed/s2/400/300" },
|
||||
];
|
||||
|
||||
/** 最近报名展示 - mock 头像从互联网获取 */
|
||||
export const RECENT_JOINERS = [
|
||||
{ name: "晓琪", avatar: "https://i.pravatar.cc/80?img=1" },
|
||||
{ name: "雨晴", avatar: "https://i.pravatar.cc/80?img=5" },
|
||||
{ name: "子豪", avatar: "https://i.pravatar.cc/80?img=11" },
|
||||
{ name: "林晓", avatar: "https://i.pravatar.cc/80?img=9" },
|
||||
{ name: "佳琪", avatar: "https://i.pravatar.cc/80?img=20" },
|
||||
{ name: "浩然", avatar: "https://i.pravatar.cc/80?img=12" },
|
||||
{ name: "心怡", avatar: "https://i.pravatar.cc/80?img=23" },
|
||||
{ name: "阿杰", avatar: "https://i.pravatar.cc/80?img=15" },
|
||||
];
|
||||
|
||||
@@ -2,6 +2,12 @@ import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
reactStrictMode: true,
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{ protocol: "https", hostname: "picsum.photos", pathname: "/**" },
|
||||
{ protocol: "https", hostname: "i.pravatar.cc", pathname: "/**" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "16.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"pocketbase": "^0.26.8",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
|
||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
||||
next:
|
||||
specifier: 16.1.6
|
||||
version: 16.1.6(@babel/core@7.29.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
pocketbase:
|
||||
specifier: ^0.26.8
|
||||
version: 0.26.8
|
||||
@@ -1504,6 +1507,12 @@ packages:
|
||||
natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
|
||||
next-themes@0.4.6:
|
||||
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
|
||||
next@16.1.6:
|
||||
resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
@@ -3484,6 +3493,11 @@ snapshots:
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
next-themes@0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
dependencies:
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
|
||||
next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
dependencies:
|
||||
'@next/env': 16.1.6
|
||||
|
||||
Reference in New Issue
Block a user