From 501e5b7987b14295f81d7585267ea83c510d1bcb Mon Sep 17 00:00:00 2001 From: eric Date: Sun, 15 Mar 2026 23:19:43 -0500 Subject: [PATCH] 'style4' --- app/api/join/route.ts | 11 +- app/components/AvatarStack.tsx | 23 +- app/components/HeroSection.tsx | 71 +++-- app/components/SessionCoverImage.tsx | 4 +- app/globals.css | 18 +- app/join/page.tsx | 430 ++++++++++++++++++--------- app/page.tsx | 142 +++++++-- config/home.config.ts | 25 +- next.config.ts | 1 + 9 files changed, 520 insertions(+), 205 deletions(-) diff --git a/app/api/join/route.ts b/app/api/join/route.ts index b4c5865..af2f248 100644 --- a/app/api/join/route.ts +++ b/app/api/join/route.ts @@ -29,9 +29,18 @@ export async function POST(request: NextRequest) { const session = String(formData.session || "").trim(); const ageRange = String(formData.age_range || "").trim(); const wechatOrPhone = String(formData.wechat_or_phone || "").trim(); - const intro = String(formData.intro || "").trim(); + let intro = String(formData.intro || "").trim(); const firstTime = String(formData.first_time || "").trim(); const agreeRules = !!formData.agree_rules; + const status = String(formData.status || "").trim(); + const activityType = String(formData.activity_type || "").trim(); + const whyJoin = String(formData.why_join || "").trim(); + const region = String(formData.region || "").trim(); + const availableTime = String(formData.available_time || "").trim(); + if (!intro && (status || activityType || whyJoin || region || availableTime)) { + const extra = [status && `状态:${status}`, activityType && `活动偏好:${activityType}`, whyJoin && `原因:${whyJoin}`, region && `区域:${region}`, availableTime && `时间:${availableTime}`].filter(Boolean).join(" | "); + intro = extra; + } if (!nickname || !wechatOrPhone || !intro || !session || !agreeRules) { return NextResponse.json( diff --git a/app/components/AvatarStack.tsx b/app/components/AvatarStack.tsx index e693318..9478e25 100644 --- a/app/components/AvatarStack.tsx +++ b/app/components/AvatarStack.tsx @@ -1,34 +1,31 @@ "use client"; -import Image from "next/image"; import { RECENT_JOINERS } from "@/config/home.config"; +function getInitial(name: string): string { + if (!name || name.length === 0) return "?"; + return name.slice(0, 1); +} + export default function AvatarStack() { return (
-
+
{RECENT_JOINERS.slice(0, 6).map((u, i) => (
- {u.name} + {getInitial(u.name)}
))}
- {RECENT_JOINERS.map((u) => u.name).join(" ")} + {RECENT_JOINERS.slice(0, 3).map((u) => u.name).join("、")} - 等本月新加入 + 等有报名意向
); diff --git a/app/components/HeroSection.tsx b/app/components/HeroSection.tsx index 22b3844..3f69946 100644 --- a/app/components/HeroSection.tsx +++ b/app/components/HeroSection.tsx @@ -19,50 +19,73 @@ export default function HeroSection() {
-

- 📍 深圳 · 广州 · 惠州 -

-

- 周末抽两小时 +

+ 周末留两小时
- 认识几个新朋友 + 去一个舒服的公开场地,认识几位同频的人

-

- 😌 氛围轻松 · 📍 公开场地 · 填表即报 +

+ 深圳 · 广州 · 惠州 · 周末 6–8 人轻社交小组

-
+

+ 咖啡馆 / 书店 / citywalk · 6–8 人小组 · 公开场地 · 填表申请 · 联系方式仅用于活动确认 · 不收费 +

+
- ✨ 报名参加 + 填表申请本周场次 + + + 先看规则
- {mainSession && mainSession.coverImage && ( -
+ {mainSession && ( +
- + {mainSession.coverImage ? ( + + ) : ( +
+ ☕ +
+ )} +
+ + 周末下午 + + + 6–8 人 + + + 公开场地 + +
-
+

- 本周主推 + 本周场次

{mainSession.title}

- {mainSession.date} {mainSession.time} + {mainSession.date} {mainSession.time} · {mainSession.place}

报名这场 → diff --git a/app/components/SessionCoverImage.tsx b/app/components/SessionCoverImage.tsx index 78782a5..759f5f6 100644 --- a/app/components/SessionCoverImage.tsx +++ b/app/components/SessionCoverImage.tsx @@ -6,7 +6,7 @@ import Image from "next/image"; interface SessionCoverImageProps { src: string; alt: string; - type?: "skill-exchange" | "side-hustle"; + type?: "skill-exchange" | "side-hustle" | "cafe" | "bookstore" | "citywalk" | "light-chat"; className?: string; fill?: boolean; sizes?: string; @@ -18,7 +18,7 @@ export default function SessionCoverImage({ src, alt, type, className = "", fill if (error) { return (
- {type === "skill-exchange" ? "🔄" : "☕"} + {type === "bookstore" ? "📚" : type === "citywalk" ? "🚶" : type === "skill-exchange" ? "🔄" : "☕"}
); } diff --git a/app/globals.css b/app/globals.css index da50790..6f9b5ce 100644 --- a/app/globals.css +++ b/app/globals.css @@ -81,10 +81,20 @@ body { .form-input { width: 100%; - padding: 0.5rem 0.75rem; + padding: 0.625rem 0.875rem; border: 1px solid #e2e8f0; border-radius: 0.5rem; - font-size: 0.875rem; + font-size: 0.9375rem; + transition: border-color 0.15s, box-shadow 0.15s; +} + +.form-input:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.form-input::placeholder { + color: #9ca3af; } .dark .form-input { @@ -92,6 +102,10 @@ body { background: #1f2937; } +.dark .form-input::placeholder { + color: #6b7280; +} + .dark .right-item, .dark .community-card { background: #111827; diff --git a/app/join/page.tsx b/app/join/page.tsx index 5865948..c26a7f2 100644 --- a/app/join/page.tsx +++ b/app/join/page.tsx @@ -8,16 +8,46 @@ import CitySelect from "../components/CitySelect"; import ThemeToggle from "../components/ThemeToggle"; const ageOptions = ["18-24", "25-30", "31-35", "36-40", "40+"]; +const statusOptions = [ + { value: "student", label: "学生" }, + { value: "working", label: "上班" }, + { value: "freelance", label: "自由职业" }, + { value: "startup", label: "创业" }, + { value: "other", label: "其他" }, +]; +const activityTypeOptions = [ + { value: "cafe", label: "咖啡馆聊天" }, + { value: "bookstore", label: "书店" }, + { value: "citywalk", label: "Citywalk" }, + { value: "side-hustle", label: "轻副业交流" }, + { value: "any", label: "都可以" }, +]; const firstTimeOptions = ["是,第一次参加", "否,参加过类似活动"]; +const availableTimeOptions = [ + { value: "sat-pm", label: "周六下午" }, + { value: "sun-pm", label: "周日下午" }, + { value: "both", label: "都可以" }, +]; + +const REGIONS: Record = { + 深圳: ["南山区", "福田区", "罗湖区", "宝安区", "龙岗区", "其他"], + 广州: ["天河区", "越秀区", "海珠区", "荔湾区", "白云区", "其他"], + 惠州: ["惠城区", "惠阳区", "其他"], +}; interface FormData { city: string; session: string; - nickname: string; ageRange: string; - wechatOrPhone: string; + status: string; + activityType: string; intro: string; + whyJoin: string; + nickname: string; + wechatOrPhone: string; + region: string; firstTime: string; + availableTime: string; agreeRules: boolean; } @@ -31,14 +61,20 @@ function getSessionInfo(sessionId: string) { } export default function JoinPage() { + const [step, setStep] = useState<1 | 2>(1); const [form, setForm] = useState({ city: "", session: "", - nickname: "", ageRange: "", - wechatOrPhone: "", + status: "", + activityType: "", intro: "", + whyJoin: "", + nickname: "", + wechatOrPhone: "", + region: "", firstTime: "", + availableTime: "", agreeRules: false, }); const [submitted, setSubmitted] = useState(false); @@ -60,11 +96,22 @@ export default function JoinPage() { const set = (key: keyof FormData, value: string | boolean) => setForm((prev) => ({ ...prev, [key]: value })); + const canProceedStep1 = form.city && form.session && form.ageRange && form.status && form.activityType && form.intro.trim() && form.whyJoin.trim(); + const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setError(null); setSubmitting(true); try { + const reasonParts = [ + form.intro, + form.whyJoin, + form.status && `状态: ${statusOptions.find((o) => o.value === form.status)?.label || form.status}`, + form.activityType && `活动偏好: ${activityTypeOptions.find((o) => o.value === form.activityType)?.label || form.activityType}`, + form.region && `常活动区域: ${form.region}`, + form.availableTime && `可参加时间: ${availableTimeOptions.find((o) => o.value === form.availableTime)?.label || form.availableTime}`, + ].filter(Boolean); + const res = await fetch("/api/join", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -74,9 +121,14 @@ export default function JoinPage() { nickname: form.nickname, age_range: form.ageRange, wechat_or_phone: form.wechatOrPhone, - intro: form.intro, + intro: reasonParts.join(" | "), first_time: form.firstTime, agree_rules: form.agreeRules, + status: form.status, + activity_type: form.activityType, + why_join: form.whyJoin, + region: form.region, + available_time: form.availableTime, }), }); const data = await res.json(); @@ -92,6 +144,7 @@ export default function JoinPage() { }; const selectedSession = getSessionInfo(form.session); + const regions = form.city ? (REGIONS[form.city] || []) : []; if (submitted) { return ( @@ -102,7 +155,7 @@ export default function JoinPage() {

报名成功

- 我们会尽快通过您留下的联系方式与您确认 + 我们会尽快通过您留下的联系方式与您确认。不是所有报名都会自动确认,我们会根据场次与匹配度联系合适报名者。

+ {/* Trust bar */} +
+ 📍 公开场地 + 👥 6–8 人小组 + 🆓 不收费 + 🤝 尊重边界 +
+
-
+
-
+
📍 深圳 · 广州 · 惠州
@@ -143,8 +204,20 @@ export default function JoinPage() { 活动报名

- ✍️ 填表即可 · 无需注册 · 不收费 + 先填表,我们会根据场次与匹配度联系合适报名者

+

+ 不是所有报名都会自动确认 +

+ + {/* 进度提示 */} +
+
+ + {step === 1 ? "第一步" : "第二步"} + +
+
@@ -152,7 +225,7 @@ export default function JoinPage() {
{selectedSession.coverImage && (
- +
)}
@@ -167,136 +240,221 @@ export default function JoinPage() { onSubmit={handleSubmit} 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" > -
- - set("city", v)} required /> - + {step === 1 ? ( +
+
+

基本信息

+ + set("city", v)} required /> + + + + + + + + + + + + + +
- - set("intro", e.target.value)} + required + className="form-input" + /> + + + set("whyJoin", e.target.value)} + required + className="form-input" + /> + +
+ +
- -
- - set("nickname", e.target.value)} - required - className="form-input" - /> - - - - - -
+ ← 返回上一步 + -
- - set("wechatOrPhone", e.target.value)} - required - className="form-input" - /> -
+
+

联系方式与确认

+ + set("nickname", e.target.value)} + required + className="form-input" + /> + + +
+ set("wechatOrPhone", e.target.value)} + required + className="form-input" + /> +

+ 仅用于活动确认,不会公开给其他报名者 +

+
+
+ + + + + + + + + +
-
- - set("intro", e.target.value)} - required - className="form-input" - /> -
+
+ +
-
- - -
- -
- -
- -
- {error && ( -

- {error} +

+ 提交后我们会根据场次与匹配度联系合适报名者,通过微信/短信确认地点与时间。若当前场次不匹配,不会反复打扰。

- )} - -
+ +
+ {error && ( +

+ {error} +

+ )} + +
+
+ )}
@@ -317,11 +475,11 @@ function Field({ children: React.ReactNode; }) { return ( -
-
@@ -52,9 +105,9 @@ export default function Home() { href={`/join?session=${encodeURIComponent(s.id)}`} className="group flex flex-col sm:flex-row gap-3 sm:gap-4 p-4 sm:p-5 rounded-xl sm:rounded-2xl bg-white dark:bg-stone-900/80 border border-stone-200/80 dark:border-stone-700/80 hover:border-amber-200 dark:hover:border-amber-900/50 hover:shadow-lg hover:shadow-amber-900/5 transition-all" > -
+
{s.coverImage ? ( - + ) : (
{s.type === "skill-exchange" ? "🔄" : "☕"} @@ -62,11 +115,18 @@ export default function Home() { )}
- {i === 0 && ( - - 主推 - - )} +
+ {s.tags?.map((tag) => ( + + {SESSION_TAGS[tag]} + + ))} + {i === 0 && ( + + 主推 + + )} +
{ACTIVITY_TYPES[s.type]} · {s.city} @@ -85,6 +145,33 @@ export default function Home() {
+ {/* 适合谁 / 不适合谁 */} +
+

+ 🎯 适合谁 / 不适合谁 +

+
+
+

适合

+
    +
  • · 想周末出门认识新朋友
  • +
  • · 接受公开场地的小组活动
  • +
  • · 喜欢舒服聊天,不喜欢太吵场子
  • +
  • · 第一次参加也可以
  • +
+
+
+

不适合

+
    +
  • · 只想线上聊天不见面
  • +
  • · 想直接索要所有人联系方式
  • +
  • · 想销售、引流、推项目
  • +
  • · 不尊重边界感
  • +
+
+
+
+ {/* 为什么来 */}

@@ -110,17 +197,19 @@ export default function Home() {

- {/* FAQ - 可折叠,默认折叠 */} -
+ {/* FAQ */} +

常见问题

{[ - { q: "需要先注册吗?", a: "不需要,填表即可。", icon: "🔐" }, - { q: "需要先付款吗?", a: "不需要,报名不收费。", icon: "💰" }, - { q: "第一次参加可以吗?", a: "可以,很多人都是第一次来。", icon: "✨" }, - { q: "地点什么时候通知?", a: "报名后会通过微信/短信告知,均为公开场地。", icon: "📍" }, + { q: "提交后多久会收到确认?", a: "我们会在活动前 1–2 天根据场次与匹配度联系合适报名者,通过微信或短信确认。若当前场次不匹配,不会反复打扰。", icon: "⏱️" }, + { q: "联系方式会公开给其他人吗?", a: "不会。联系方式仅用于活动确认,不会公开给其他报名者。", icon: "🔒" }, + { q: "第一次参加会不会尴尬?", a: "不会。很多人都是第一次来,6–8 人小组人数可控,公开场地氛围轻松,聊得来就多聊几句。", icon: "✨" }, + { q: "如果临时有事不去了怎么办?", a: "随时可以退出,提前告知即可。我们尊重每个人的选择。", icon: "🔄" }, + { q: "活动地点为什么不是先公开具体地址?", a: "我们会根据报名情况确定具体场地,确认后会通过微信/短信告知。均为公开场地(咖啡馆、书店等),地址提前通知。", icon: "📍" }, + { q: "会不会有人骚扰、硬加联系方式?", a: "我们强调边界感,不强制交换联系方式。公开场地、小组形式也有助于降低此类情况。如有不适可随时离开。", icon: "🛡️" }, ].map((faq) => (
-

{faq.a}

+

{faq.a}

))} @@ -146,8 +235,11 @@ export default function Home() { href="/join" 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" > - ✨ 立即报名 → + 填表申请本周活动 +

+ 查看是否适合参加 · 先填表,我们会根据场次联系 +

@@ -157,7 +249,7 @@ export default function Home() { href="/join" className="block w-full bg-amber-600 hover:bg-amber-700 text-white text-center py-3 rounded-xl font-medium text-sm" > - ✨ 报名参加 + 申请本周场次
diff --git a/config/home.config.ts b/config/home.config.ts index ca9b650..c5a1d03 100644 --- a/config/home.config.ts +++ b/config/home.config.ts @@ -5,12 +5,33 @@ export const CITIES = ["深圳", "广州", "惠州"] as const; export const ACTIVITY_TYPES = { "skill-exchange": "技能交换", "side-hustle": "副业沙龙/茶话会", + "cafe": "咖啡馆聊天", + "bookstore": "书店", + "citywalk": "Citywalk", + "light-chat": "轻话题", } as const; +/** 活动标签 */ +export const SESSION_TAGS = { + "first-friendly": "首次友好", + "public-venue": "公开场地", + "small-group": "6-8 人", + "weekend-pm": "周末下午", + "cafe": "咖啡馆", + "bookstore": "书店", + "citywalk": "Citywalk", + "light-topic": "轻话题", +} as const; + +/** 咖啡馆/书店/城市 占位图 - Unsplash 真实场景 */ +const IMG_CAFE = "https://images.unsplash.com/photo-1501339847302-ac426a4a7cbb?w=400&h=300&fit=crop"; +const IMG_BOOKSTORE = "https://images.unsplash.com/photo-1481627834876-b7833e8f5570?w=400&h=300&fit=crop"; +const IMG_CITY = "https://images.unsplash.com/photo-1519501025264-65ba15a82390?w=400&h=300&fit=crop"; + /** 本周活动场次 */ export const SESSIONS = [ - { 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" }, + { id: "session-1", date: "3月16日 周六", time: "14:00", place: "南山区某咖啡馆", city: "深圳", type: "skill-exchange" as const, title: "周末技能交换", coverImage: IMG_CAFE, tags: ["first-friendly", "public-venue", "small-group", "weekend-pm", "cafe", "light-topic"] as const }, + { id: "session-2", date: "3月17日 周日", time: "15:00", place: "天河区某书店", city: "广州", type: "side-hustle" as const, title: "副业沙龙", coverImage: IMG_BOOKSTORE, tags: ["first-friendly", "public-venue", "small-group", "weekend-pm", "bookstore"] as const }, ]; /** 最近报名展示 - mock 头像从互联网获取 */ diff --git a/next.config.ts b/next.config.ts index c5ea6bf..0460a94 100644 --- a/next.config.ts +++ b/next.config.ts @@ -6,6 +6,7 @@ const nextConfig: NextConfig = { remotePatterns: [ { protocol: "https", hostname: "picsum.photos", pathname: "/**" }, { protocol: "https", hostname: "i.pravatar.cc", pathname: "/**" }, + { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" }, ], }, };