Files
gitlab-instance-0a899031_salon/app/join/page.tsx
2026-03-15 22:00:10 -05:00

304 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect, type FormEvent } from "react";
import Link from "next/link";
import { SESSIONS } from "@/config/home.config";
const ageOptions = ["18-24", "25-30", "31-35", "36-40", "40+"];
const firstTimeOptions = ["是,第一次参加", "否,参加过类似活动"];
interface FormData {
city: string;
session: string;
nickname: string;
ageRange: string;
wechatOrPhone: string;
intro: string;
firstTime: string;
agreeRules: boolean;
}
function getSessionFromUrl(): string {
if (typeof window === "undefined") return "";
return new URLSearchParams(window.location.search).get("session") || "";
}
function getSessionInfo(sessionId: string) {
return SESSIONS.find((s) => s.id === sessionId);
}
export default function JoinPage() {
const [form, setForm] = useState<FormData>({
city: "北京",
session: "",
nickname: "",
ageRange: "",
wechatOrPhone: "",
intro: "",
firstTime: "",
agreeRules: false,
});
const [submitted, setSubmitted] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const sid = getSessionFromUrl();
if (sid) setForm((prev) => ({ ...prev, session: sid }));
}, []);
const set = (key: keyof FormData, value: string | boolean) =>
setForm((prev) => ({ ...prev, [key]: value }));
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setError(null);
setSubmitting(true);
try {
const res = await fetch("/api/join", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
city: form.city,
session: form.session,
nickname: form.nickname,
age_range: form.ageRange,
wechat_or_phone: form.wechatOrPhone,
intro: form.intro,
first_time: form.firstTime,
agree_rules: form.agreeRules,
}),
});
const data = await res.json();
if (!res.ok || !data?.ok) {
throw new Error(data?.error || "提交失败,请稍后重试");
}
setSubmitted(true);
} catch (err) {
setError(err instanceof Error ? err.message : "网络错误,请重试");
} finally {
setSubmitting(false);
}
};
const selectedSession = getSessionInfo(form.session);
if (submitted) {
return (
<div className="flex min-h-screen items-center justify-center bg-[#f0f4f8] dark:bg-gray-950 px-4">
<div className="w-full max-w-md animate-fade-in rounded-2xl bg-white dark:bg-gray-900 p-10 text-center shadow-lg border border-gray-100 dark:border-gray-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>
<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">
</p>
<Link
href="/"
className="mt-8 inline-flex items-center gap-2 rounded-full bg-[#ff4d4f] px-8 py-3 font-semibold text-white transition-colors hover:bg-[#ff7a45]"
>
</Link>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-[#f0f4f8] dark:bg-gray-950">
<div className="mx-auto max-w-2xl px-0 py-0 sm:px-6 sm:py-10 lg:py-14">
<div className="overflow-hidden bg-white dark:bg-gray-900 shadow-sm sm:rounded-t-2xl">
<div className="relative h-40 overflow-hidden bg-gradient-to-br from-sky-400 via-cyan-500 to-teal-400 sm:h-52">
<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-80">
</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">
</h1>
<p className="mt-3 whitespace-pre-line text-sm leading-relaxed text-slate-400 dark:text-slate-500 sm:text-base">
</p>
</div>
</div>
{selectedSession && (
<div className="mt-5 rounded-2xl bg-white dark:bg-gray-900 px-5 py-4 shadow-sm border border-gray-100 dark:border-gray-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>
)}
<form
onSubmit={handleSubmit}
className="mt-0 bg-white dark:bg-gray-900 px-5 pb-8 pt-2 shadow-sm sm:mt-5 sm:rounded-2xl sm:px-8 sm:py-8 sm:shadow-md border border-gray-100 dark:border-gray-800"
>
<div className="grid gap-0 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"
/>
</Field>
<Field label="📅 想参加的场次">
<select
value={form.session}
onChange={(e) => set("session", e.target.value)}
required
className={`form-input appearance-none ${form.session ? "text-slate-800 dark:text-slate-100" : "text-slate-400 dark:text-slate-500"}`}
>
<option value="" disabled></option>
{SESSIONS.map((s) => (
<option key={s.id} value={s.id}>
{s.title} · {s.date} {s.time}
</option>
))}
</select>
</Field>
</div>
<div className="grid gap-0 sm:grid-cols-2 sm:gap-6">
<Field label="昵称">
<input
type="text"
maxLength={40}
placeholder="活动中使用的称呼"
value={form.nickname}
onChange={(e) => set("nickname", e.target.value)}
required
className="form-input"
/>
</Field>
<Field label="年龄段">
<select
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"}`}
>
<option value="" disabled></option>
{ageOptions.map((o) => (
<option key={o} value={o}>{o}</option>
))}
</select>
</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]">
</label>
<input
type="text"
maxLength={50}
placeholder="用于活动联系"
value={form.wechatOrPhone}
onChange={(e) => set("wechatOrPhone", e.target.value)}
required
className="form-input"
/>
</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]">
</label>
<input
type="text"
maxLength={140}
placeholder="简单介绍一下自己"
value={form.intro}
onChange={(e) => set("intro", e.target.value)}
required
className="form-input"
/>
</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]">
</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"}`}
>
<option value="" disabled></option>
{firstTimeOptions.map((o) => (
<option key={o} value={o}>{o}</option>
))}
</select>
</div>
<div className="py-4 sm:py-5">
<label className="flex items-start gap-3 cursor-pointer">
<input
type="checkbox"
checked={form.agreeRules}
onChange={(e) => set("agreeRules", e.target.checked)}
required
className="mt-1 rounded border-gray-300 text-[#ff4d4f] focus:ring-[#ff4d4f]"
/>
<span className="text-sm text-slate-600 dark:text-slate-400">
</span>
</label>
</div>
<div className="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}
</p>
)}
<button
type="submit"
disabled={submitting}
className="w-full rounded-full bg-[#ff4d4f] py-3.5 text-lg font-semibold text-white shadow-md transition-all hover:bg-[#ff3333] active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-70 sm:py-4"
>
{submitting ? "提交中…" : "提交报名"}
</button>
</div>
</form>
<div className="mt-6 text-center">
<Link href="/" className="text-sm text-slate-400 dark:text-slate-500 transition-colors hover:text-[#ff4d4f]">
</Link>
</div>
</div>
</div>
);
}
function Field({
label,
children,
}: {
label: string;
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]">
{label}
</label>
<div className="relative min-w-0 flex-1">{children}</div>
</div>
);
}