'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 (
|
return (
|
||||||
<footer className="border-t border-stone-200 dark:border-stone-800 mt-12 py-8">
|
<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">
|
<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>
|
</div>
|
||||||
</footer>
|
</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";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import SessionCoverImage from "./SessionCoverImage";
|
||||||
import { SESSIONS } from "@/config/home.config";
|
import { SESSIONS } from "@/config/home.config";
|
||||||
|
|
||||||
export default function HeroSection() {
|
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%)`,
|
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="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-16">
|
<div className="flex flex-col lg:flex-row lg:items-center lg:gap-12 xl:gap-16">
|
||||||
<div className="flex-1">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-medium text-amber-700 dark:text-amber-400/90 mb-4 tracking-wide">
|
<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>
|
</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>
|
<span className="hero-gradient-text">周末抽两小时</span>
|
||||||
<br />
|
<br />
|
||||||
<span className="text-stone-800 dark:text-stone-100">在咖啡馆认识几个新朋友</span>
|
<span className="text-stone-800 dark:text-stone-100">认识几个新朋友</span>
|
||||||
</h1>
|
</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>
|
</p>
|
||||||
<div className="mt-8 flex flex-wrap gap-3">
|
<div className="mt-6 sm:mt-8">
|
||||||
<Link
|
<Link
|
||||||
href="/join"
|
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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{mainSession && (
|
{mainSession && mainSession.coverImage && (
|
||||||
<div className="lg:w-[300px] shrink-0 mt-10 lg:mt-0">
|
<div className="w-full lg:w-[260px] xl:w-[300px] shrink-0 mt-6 lg:mt-0">
|
||||||
<Link
|
<Link
|
||||||
href={`/join?session=${mainSession.id}`}
|
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"
|
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">
|
<div className="relative aspect-[4/3] w-full bg-stone-100 dark:bg-stone-800 overflow-hidden">
|
||||||
<span className="text-6xl">☕</span>
|
<SessionCoverImage
|
||||||
|
src={mainSession.coverImage}
|
||||||
|
alt={mainSession.title}
|
||||||
|
type={mainSession.type}
|
||||||
|
fill
|
||||||
|
sizes="(max-width: 1024px) 100vw, 300px"
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-5">
|
<div className="p-4 sm:p-5">
|
||||||
<p className="text-xs font-medium text-amber-600 dark:text-amber-400/90 uppercase tracking-wider mb-1">
|
<p className="text-[10px] sm:text-xs font-medium text-amber-600 dark:text-amber-400/90 uppercase tracking-wider mb-1">
|
||||||
本周主推
|
本周主推
|
||||||
</p>
|
</p>
|
||||||
<h3 className="font-semibold text-stone-800 dark:text-stone-100">{mainSession.title}</h3>
|
<h3 className="font-semibold text-stone-800 dark:text-stone-100 text-sm sm:text-base">{mainSession.title}</h3>
|
||||||
<p className="text-sm text-stone-500 dark:text-stone-400 mt-1">
|
<p className="text-xs sm:text-sm text-stone-500 dark:text-stone-400 mt-0.5">
|
||||||
{mainSession.date} {mainSession.time}
|
{mainSession.date} {mainSession.time}
|
||||||
</p>
|
</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>
|
</span>
|
||||||
</div>
|
</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";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* 使 Tailwind dark: 变体响应 class="dark",与 next-themes 配合 */
|
||||||
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #faf8f5;
|
--background: #faf8f5;
|
||||||
--foreground: #1c1917;
|
--foreground: #1c1917;
|
||||||
@@ -7,11 +10,10 @@
|
|||||||
--accent-hover: #92400e;
|
--accent-hover: #92400e;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
html.dark {
|
||||||
:root {
|
--background: #0c0a09;
|
||||||
--background: #0c0a09;
|
--foreground: #fafaf9;
|
||||||
--foreground: #fafaf9;
|
color-scheme: dark;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -19,6 +21,10 @@ body {
|
|||||||
background: var(--background);
|
background: var(--background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.safe-area-pb {
|
||||||
|
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||||
|
}
|
||||||
|
|
||||||
.hero-gradient-text {
|
.hero-gradient-text {
|
||||||
background: linear-gradient(135deg, #b45309 0%, #d97706 50%, #f59e0b 100%);
|
background: linear-gradient(135deg, #b45309 0%, #d97706 50%, #f59e0b 100%);
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
import { useState, useEffect, type FormEvent } from "react";
|
import { useState, useEffect, type FormEvent } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import SessionCoverImage from "../components/SessionCoverImage";
|
||||||
import { SESSIONS } from "@/config/home.config";
|
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 ageOptions = ["18-24", "25-30", "31-35", "36-40", "40+"];
|
||||||
const firstTimeOptions = ["是,第一次参加", "否,参加过类似活动"];
|
const firstTimeOptions = ["是,第一次参加", "否,参加过类似活动"];
|
||||||
@@ -29,7 +32,7 @@ function getSessionInfo(sessionId: string) {
|
|||||||
|
|
||||||
export default function JoinPage() {
|
export default function JoinPage() {
|
||||||
const [form, setForm] = useState<FormData>({
|
const [form, setForm] = useState<FormData>({
|
||||||
city: "北京",
|
city: "",
|
||||||
session: "",
|
session: "",
|
||||||
nickname: "",
|
nickname: "",
|
||||||
ageRange: "",
|
ageRange: "",
|
||||||
@@ -44,7 +47,14 @@ export default function JoinPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sid = getSessionFromUrl();
|
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) =>
|
const set = (key: keyof FormData, value: string | boolean) =>
|
||||||
@@ -85,18 +95,18 @@ export default function JoinPage() {
|
|||||||
|
|
||||||
if (submitted) {
|
if (submitted) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-[#faf8f5] dark:bg-stone-950 px-4">
|
<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-10 text-center shadow-lg border border-stone-200 dark:border-stone-800">
|
<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-5 flex h-20 w-20 items-center justify-center rounded-full bg-emerald-50 dark:bg-emerald-900/30 text-5xl">
|
<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>
|
</div>
|
||||||
<h2 className="text-2xl font-bold text-slate-800 dark:text-slate-100">报名成功</h2>
|
<h2 className="text-xl sm:text-2xl font-bold text-stone-800 dark:text-stone-100">报名成功</h2>
|
||||||
<p className="mt-3 text-slate-500 dark:text-slate-400">
|
<p className="mt-2 sm:mt-3 text-sm sm:text-base text-stone-500 dark:text-stone-400">
|
||||||
我们会尽快通过您留下的联系方式与您确认
|
我们会尽快通过您留下的联系方式与您确认
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
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>
|
</Link>
|
||||||
@@ -107,69 +117,83 @@ export default function JoinPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#faf8f5] dark:bg-stone-950">
|
<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="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="absolute inset-0 flex flex-col items-center justify-center text-white">
|
||||||
<div className="text-6xl sm:text-7xl">☕</div>
|
<div className="text-5xl sm:text-6xl md:text-7xl" aria-hidden>☕</div>
|
||||||
<div className="mt-2 text-sm font-medium opacity-90">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-5 py-5 text-center sm:px-8 sm:py-7">
|
<div className="px-4 sm:px-6 py-4 sm:py-5 text-center">
|
||||||
<h1 className="text-xl font-bold text-slate-800 dark:text-slate-100 sm:text-2xl">
|
<h1 className="text-lg sm:text-xl md:text-2xl font-bold text-stone-800 dark:text-stone-100">
|
||||||
活动报名
|
活动报名
|
||||||
</h1>
|
</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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedSession && (
|
{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">
|
<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">
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-500 mb-1">已选场次</p>
|
{selectedSession.coverImage && (
|
||||||
<p className="font-medium text-slate-800 dark:text-slate-100">{selectedSession.title}</p>
|
<div className="relative w-16 h-16 sm:w-20 sm:h-20 shrink-0 rounded-lg overflow-hidden">
|
||||||
<p className="text-sm text-slate-500 dark:text-slate-500">{selectedSession.date} {selectedSession.time} · {selectedSession.place}</p>
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
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="📍 城市">
|
<Field label="📍 城市">
|
||||||
<input
|
<CitySelect value={form.city} onChange={(v) => set("city", v)} required />
|
||||||
type="text"
|
|
||||||
placeholder="如:北京"
|
|
||||||
value={form.city}
|
|
||||||
onChange={(e) => set("city", e.target.value)}
|
|
||||||
required
|
|
||||||
className="form-input"
|
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="📅 想参加的场次">
|
<Field label="📅 想参加的场次">
|
||||||
<select
|
<select
|
||||||
value={form.session}
|
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
|
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>
|
<option value="" disabled>请选择</option>
|
||||||
{SESSIONS.map((s) => (
|
{SESSIONS.map((s) => (
|
||||||
<option key={s.id} value={s.id}>
|
<option key={s.id} value={s.id}>
|
||||||
{s.title} · {s.date} {s.time}
|
{s.title} · {s.city} · {s.date} {s.time}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</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="昵称">
|
<Field label="昵称">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -187,7 +211,7 @@ export default function JoinPage() {
|
|||||||
value={form.ageRange}
|
value={form.ageRange}
|
||||||
onChange={(e) => set("ageRange", e.target.value)}
|
onChange={(e) => set("ageRange", e.target.value)}
|
||||||
required
|
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>
|
<option value="" disabled>请选择</option>
|
||||||
{ageOptions.map((o) => (
|
{ageOptions.map((o) => (
|
||||||
@@ -197,8 +221,8 @@ export default function JoinPage() {
|
|||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-b border-slate-100 dark:border-gray-700 py-4 sm:py-5">
|
<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-base font-bold text-slate-700 dark:text-slate-300 sm:text-[17px]">
|
<label className="mb-2 block text-sm sm:text-base font-bold text-stone-700 dark:text-stone-300">
|
||||||
微信号或手机号
|
微信号或手机号
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -212,8 +236,8 @@ export default function JoinPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-b border-slate-100 dark:border-gray-700 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="mb-2 block text-base font-bold text-slate-700 dark:text-slate-300 sm:text-[17px]">
|
<label className="mb-2 block text-sm sm:text-base font-bold text-stone-700 dark:text-stone-300">
|
||||||
一句话自我介绍
|
一句话自我介绍
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -227,15 +251,15 @@ export default function JoinPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-b border-slate-100 dark:border-gray-700 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="mb-2 block text-base font-bold text-slate-700 dark:text-slate-300 sm:text-[17px]">
|
<label className="mb-2 block text-sm sm:text-base font-bold text-stone-700 dark:text-stone-300">
|
||||||
是否第一次参加类似活动
|
是否第一次参加类似活动
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={form.firstTime}
|
value={form.firstTime}
|
||||||
onChange={(e) => set("firstTime", e.target.value)}
|
onChange={(e) => set("firstTime", e.target.value)}
|
||||||
required
|
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>
|
<option value="" disabled>请选择</option>
|
||||||
{firstTimeOptions.map((o) => (
|
{firstTimeOptions.map((o) => (
|
||||||
@@ -244,7 +268,7 @@ export default function JoinPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</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">
|
<label className="flex items-start gap-3 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -253,13 +277,13 @@ export default function JoinPage() {
|
|||||||
required
|
required
|
||||||
className="mt-1 rounded border-stone-300 text-amber-600 focus:ring-amber-500"
|
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>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 sm:mt-10">
|
<div className="mt-6 sm:mt-8 sm:mt-10">
|
||||||
{error && (
|
{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">
|
<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}
|
{error}
|
||||||
@@ -268,15 +292,15 @@ export default function JoinPage() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={submitting}
|
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 ? "提交中…" : "提交报名"}
|
{submitting ? "提交中…" : "提交报名"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="mt-6 text-center">
|
<div className="mt-4 sm:mt-6 text-center px-4">
|
||||||
<Link href="/" className="text-sm text-stone-400 dark:text-stone-500 transition-colors hover:text-amber-600">
|
<Link href="/" className="text-xs sm:text-sm text-stone-400 dark:text-stone-500 transition-colors hover:text-amber-600">
|
||||||
← 返回首页
|
← 返回首页
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -293,8 +317,8 @@ function Field({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
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">
|
<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="w-[90px] shrink-0 text-[15px] font-bold text-slate-700 dark:text-slate-300 sm:mb-2 sm:w-auto sm:text-[17px]">
|
<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}
|
||||||
</label>
|
</label>
|
||||||
<div className="relative min-w-0 flex-1">{children}</div>
|
<div className="relative min-w-0 flex-1">{children}</div>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import ThemeProvider from "./components/ThemeProvider";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "北京周末|同城活动·认识新朋友",
|
title: "深圳广州惠州|同城活动·技能交换·副业沙龙",
|
||||||
description: "周末咖啡局、读书分享,认识同城新朋友。公开场地,填表报名,无需注册。",
|
description: "技能交换、副业沙龙茶话会,认识同城新朋友。公开场地,填表报名,无需注册。",
|
||||||
keywords: "北京周末活动,同城活动,认识新朋友,周末咖啡局,读书分享",
|
keywords: "深圳活动,广州活动,惠州活动,技能交换,副业沙龙,同城活动",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@@ -13,8 +14,10 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="zh">
|
<html lang="zh" suppressHydrationWarning>
|
||||||
<body className="antialiased">{children}</body>
|
<body className="antialiased">
|
||||||
|
<ThemeProvider>{children}</ThemeProvider>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
163
app/page.tsx
163
app/page.tsx
@@ -2,51 +2,81 @@
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import HeroSection from "./components/HeroSection";
|
import HeroSection from "./components/HeroSection";
|
||||||
|
import SessionCoverImage from "./components/SessionCoverImage";
|
||||||
import Footer from "./components/Footer";
|
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() {
|
export default function Home() {
|
||||||
return (
|
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 />
|
<HeroSection />
|
||||||
|
|
||||||
<main className="max-w-[900px] mx-auto px-5 sm:px-8 py-10 sm:py-14 space-y-14">
|
<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-wrap justify-center gap-x-8 gap-y-2 text-sm text-stone-600 dark:text-stone-400">
|
<div className="flex flex-col gap-4 sm:gap-6">
|
||||||
<span>📍 公开场地</span>
|
<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>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
{/* 本周场次 */}
|
{/* 本周场次 */}
|
||||||
<section>
|
<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>
|
</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) => (
|
{SESSIONS.map((s, i) => (
|
||||||
<Link
|
<Link
|
||||||
key={s.id}
|
key={s.id}
|
||||||
href={`/join?session=${encodeURIComponent(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">
|
<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">
|
||||||
{i === 0 ? "☕" : i === 1 ? "📖" : "👋"}
|
{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>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
{i === 0 && (
|
{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>
|
</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}
|
{s.title}
|
||||||
</h3>
|
</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}
|
{s.date} {s.time} · {s.place}
|
||||||
</p>
|
</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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,80 +87,77 @@ export default function Home() {
|
|||||||
|
|
||||||
{/* 为什么来 */}
|
{/* 为什么来 */}
|
||||||
<section>
|
<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>
|
</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="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-6">
|
<div className="grid sm:grid-cols-2 gap-4 sm:gap-6">
|
||||||
<div>
|
{[
|
||||||
<p className="font-medium text-stone-800 dark:text-stone-100">氛围轻松</p>
|
{ icon: "😌", title: "氛围轻松", desc: "咖啡馆、书店里的线下小聚,不用尬聊,聊得来就多聊几句。" },
|
||||||
<p className="text-sm text-stone-500 dark:text-stone-400 mt-1 leading-relaxed">
|
{ icon: "🛡️", title: "安全靠谱", desc: "公开场地,地址提前告知。尊重边界,不追问隐私。" },
|
||||||
咖啡馆、书店里的线下小聚,不用尬聊,聊得来就多聊几句。
|
{ icon: "👍", title: "第一次来也 OK", desc: "很多人都是第一次参加,小组活动人数可控,不会尴尬。" },
|
||||||
</p>
|
{ icon: "✍️", title: "填表就行", desc: "无需注册登录,不用先付款,填个表单即可。" },
|
||||||
</div>
|
].map((item) => (
|
||||||
<div>
|
<div key={item.title} className="flex gap-2.5 sm:gap-3">
|
||||||
<p className="font-medium text-stone-800 dark:text-stone-100">安全靠谱</p>
|
<span className="text-xl sm:text-2xl shrink-0">{item.icon}</span>
|
||||||
<p className="text-sm text-stone-500 dark:text-stone-400 mt-1 leading-relaxed">
|
<div>
|
||||||
公开场地,地址提前告知。尊重边界,不追问隐私。
|
<p className="font-medium text-stone-800 dark:text-stone-100 text-sm sm:text-base">{item.title}</p>
|
||||||
</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>
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* FAQ */}
|
{/* FAQ - 可折叠,默认折叠 */}
|
||||||
<section>
|
<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>
|
</h2>
|
||||||
<div className="space-y-3">
|
<div className="space-y-2 sm:space-y-3">
|
||||||
{[
|
{[
|
||||||
{ q: "需要先注册吗?", a: "不需要,填表即可。" },
|
{ q: "需要先注册吗?", a: "不需要,填表即可。", icon: "🔐" },
|
||||||
{ q: "需要先付款吗?", a: "不需要,报名不收费。" },
|
{ q: "需要先付款吗?", a: "不需要,报名不收费。", icon: "💰" },
|
||||||
{ q: "第一次参加可以吗?", a: "可以,很多人都是第一次来。" },
|
{ q: "第一次参加可以吗?", a: "可以,很多人都是第一次来。", icon: "✨" },
|
||||||
{ q: "地点什么时候通知?", a: "报名后会通过微信/短信告知,均为公开场地。" },
|
{ q: "地点什么时候通知?", a: "报名后会通过微信/短信告知,均为公开场地。", icon: "📍" },
|
||||||
].map((faq, i) => (
|
].map((faq) => (
|
||||||
<div
|
<details
|
||||||
key={i}
|
key={faq.q}
|
||||||
className="rounded-xl bg-white dark:bg-stone-900/80 border border-stone-200/80 dark:border-stone-700/80 p-4"
|
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>
|
<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">
|
||||||
<p className="text-sm text-stone-500 dark:text-stone-400 mt-0.5">{faq.a}</p>
|
<span className="text-base sm:text-lg shrink-0">{faq.icon}</span>
|
||||||
</div>
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* CTA */}
|
{/* CTA */}
|
||||||
<section className="pt-4">
|
<section className="pt-2 sm:pt-4">
|
||||||
<Link
|
<Link
|
||||||
href="/join"
|
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>
|
</Link>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* 移动端悬浮 CTA */}
|
{/* 移动端悬浮 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
|
<Link
|
||||||
href="/join"
|
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>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,26 @@
|
|||||||
/** 本周活动场次 - 同城活动用 */
|
/** 开放城市 - 其他待开放 */
|
||||||
|
export const CITIES = ["深圳", "广州", "惠州"] as const;
|
||||||
|
|
||||||
|
/** 活动类型 */
|
||||||
|
export const ACTIVITY_TYPES = {
|
||||||
|
"skill-exchange": "技能交换",
|
||||||
|
"side-hustle": "副业沙龙/茶话会",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** 本周活动场次 */
|
||||||
export const SESSIONS = [
|
export const SESSIONS = [
|
||||||
{ id: "session-1", date: "3月16日 周六", 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: "海淀区某书店", title: "读书分享" },
|
{ 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" },
|
||||||
{ id: "session-3", date: "3月22日 周六", time: "14:00", place: "东城区某空间", title: "新朋友见面会" },
|
];
|
||||||
|
|
||||||
|
/** 最近报名展示 - 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 = {
|
const nextConfig: NextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{ protocol: "https", hostname: "picsum.photos", pathname: "/**" },
|
||||||
|
{ protocol: "https", hostname: "i.pravatar.cc", pathname: "/**" },
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"pocketbase": "^0.26.8",
|
"pocketbase": "^0.26.8",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3"
|
||||||
|
|||||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
|||||||
next:
|
next:
|
||||||
specifier: 16.1.6
|
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)
|
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:
|
pocketbase:
|
||||||
specifier: ^0.26.8
|
specifier: ^0.26.8
|
||||||
version: 0.26.8
|
version: 0.26.8
|
||||||
@@ -1504,6 +1507,12 @@ packages:
|
|||||||
natural-compare@1.4.0:
|
natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
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:
|
next@16.1.6:
|
||||||
resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==}
|
resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==}
|
||||||
engines: {node: '>=20.9.0'}
|
engines: {node: '>=20.9.0'}
|
||||||
@@ -3484,6 +3493,11 @@ snapshots:
|
|||||||
|
|
||||||
natural-compare@1.4.0: {}
|
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):
|
next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 16.1.6
|
'@next/env': 16.1.6
|
||||||
|
|||||||
Reference in New Issue
Block a user