Files
2026-03-07 09:34:01 -06:00

431 lines
15 KiB
TypeScript
Raw Permalink 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, useRef, type FormEvent, type ChangeEvent } from "react";
const genderOptions = ["男", "女"];
const singleOptions = ["单身", "恋爱中", "已婚"];
const educationOptions = ["高中及以下", "大专", "本科", "硕士", "博士"];
const graduationYears = Array.from({ length: 41 }, (_, i) => String(2026 - i));
interface FormData {
nickname: string;
gender: string;
single: string;
profession: string;
intro: string;
wechat: string;
education: string;
graduationYear: string;
phone: string;
}
interface MediaFile {
file: File;
preview: string;
type: "image" | "video";
}
export default function JoinPage() {
const [form, setForm] = useState<FormData>({
nickname: "",
gender: "",
single: "",
profession: "",
intro: "",
wechat: "",
education: "",
graduationYear: "",
phone: "",
});
const [media, setMedia] = useState<MediaFile | null>(null);
const fileRef = useRef<HTMLInputElement>(null);
const [submitted, setSubmitted] = useState(false);
const set = (key: keyof FormData, value: string) =>
setForm((prev) => ({ ...prev, [key]: value }));
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
setSubmitted(true);
};
if (submitted) {
return (
<div className="flex min-h-screen items-center justify-center bg-[#f0f4f8] px-4">
<div className="w-full max-w-md animate-fade-in rounded-2xl bg-white p-10 text-center shadow-lg">
<div className="mx-auto mb-5 flex h-20 w-20 items-center justify-center rounded-full bg-emerald-50 text-5xl">
</div>
<h2 className="text-2xl font-bold text-slate-800"></h2>
<p className="mt-3 text-slate-500">
<br />
</p>
<a
href="/"
className="mt-8 inline-flex items-center gap-2 rounded-full bg-sky-500 px-8 py-3 font-semibold text-white transition-colors hover:bg-sky-600"
>
</a>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-[#f0f4f8]">
{/* ── PC: center column / H5: full width ── */}
<div className="mx-auto max-w-2xl px-0 py-0 sm:px-6 sm:py-10 lg:py-14">
{/* ── Banner ── */}
<div className="overflow-hidden bg-white shadow-sm sm:rounded-t-2xl">
{/* Hero image area */}
<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">
Digital Nomad Community
</div>
</div>
{/* Decorative blobs */}
<div className="absolute -top-10 -left-10 h-40 w-40 rounded-full bg-white/10 blur-2xl" />
<div className="absolute -right-10 -bottom-10 h-48 w-48 rounded-full bg-white/10 blur-2xl" />
</div>
{/* Title block */}
<div className="px-5 py-5 text-center sm:px-8 sm:py-7">
<h1 className="text-xl font-bold text-slate-800 sm:text-2xl">
🌍
</h1>
<p className="mt-3 whitespace-pre-line text-sm leading-relaxed text-slate-400 sm:text-base">
{"💼 结识志同道合的伙伴\n📍 线下聚会 + 线上分享\n🤝 互助成长,资源共享"}
</p>
</div>
</div>
{/* ── Form card ── */}
<form
onSubmit={handleSubmit}
className="mt-0 bg-white px-5 pb-8 pt-2 shadow-sm sm:mt-5 sm:rounded-2xl sm:px-8 sm:py-8 sm:shadow-md"
>
{/* ---- Row group: nickname + gender (PC side-by-side) ---- */}
<div className="grid gap-0 sm:grid-cols-2 sm:gap-6">
<Field label="🌟 昵称">
<input
type="text"
maxLength={140}
placeholder="请输入您的昵称"
value={form.nickname}
onChange={(e) => set("nickname", e.target.value)}
required
className="form-input"
/>
</Field>
<Field label="👤 性别">
<select
value={form.gender}
onChange={(e) => set("gender", e.target.value)}
required
className={`form-input appearance-none ${
form.gender ? "text-slate-800" : "text-slate-400"
}`}
>
<option value="" disabled>
</option>
{genderOptions.map((o) => (
<option key={o} value={o}>
{o}
</option>
))}
</select>
<ChevronDown />
</Field>
</div>
{/* ---- Row group: single + profession ---- */}
<div className="grid gap-0 sm:grid-cols-2 sm:gap-6">
<Field label="💕 感情状态">
<select
value={form.single}
onChange={(e) => set("single", e.target.value)}
required
className={`form-input appearance-none ${
form.single ? "text-slate-800" : "text-slate-400"
}`}
>
<option value="" disabled>
</option>
{singleOptions.map((o) => (
<option key={o} value={o}>
{o}
</option>
))}
</select>
<ChevronDown />
</Field>
<Field label="💼 职业">
<input
type="text"
maxLength={140}
placeholder="您的职业或专业"
value={form.profession}
onChange={(e) => set("profession", e.target.value)}
required
className="form-input"
/>
</Field>
</div>
{/* ---- wechat + phone ---- */}
<div className="grid gap-0 sm:grid-cols-2 sm:gap-6">
<Field label="💬 微信号">
<input
type="text"
maxLength={140}
placeholder="用于拉群"
value={form.wechat}
onChange={(e) => set("wechat", e.target.value)}
required
className="form-input"
/>
</Field>
<Field label="📱 手机号">
<input
type="tel"
maxLength={11}
pattern="^1[3-9]\d{9}$"
placeholder="请输入11位中国手机号"
value={form.phone}
onChange={(e) => {
const v = e.target.value.replace(/\D/g, "").slice(0, 11);
set("phone", v);
}}
className="form-input"
/>
</Field>
</div>
{/* ---- Intro (full width) ---- */}
<div className="border-b border-slate-100 py-4 sm:border-b-0 sm:py-5">
<label className="mb-2 block text-base font-bold text-slate-700 sm:text-[17px]">
📝
</label>
<div className="relative">
<textarea
maxLength={500}
rows={5}
placeholder="分享您的经历、兴趣或加入原因,越详细越好哦~"
value={form.intro}
onChange={(e) => set("intro", e.target.value)}
className="form-input min-h-[120px] resize-none sm:min-h-[140px]"
/>
<span className="absolute right-3 bottom-3 text-xs text-slate-300">
{form.intro.length}/500
</span>
</div>
</div>
{/* ---- Row group: education + graduation ---- */}
<div className="grid gap-0 sm:grid-cols-2 sm:gap-6">
<Field label="🎓 学历">
<select
value={form.education}
onChange={(e) => set("education", e.target.value)}
className={`form-input appearance-none ${
form.education ? "text-slate-800" : "text-slate-400"
}`}
>
<option value="" disabled>
</option>
{educationOptions.map((o) => (
<option key={o} value={o}>
{o}
</option>
))}
</select>
<ChevronDown />
</Field>
<Field label="📅 毕业时间">
<select
value={form.graduationYear}
onChange={(e) => set("graduationYear", e.target.value)}
className={`form-input appearance-none ${
form.graduationYear ? "text-slate-800" : "text-slate-400"
}`}
>
<option value="" disabled>
</option>
{graduationYears.map((y) => (
<option key={y} value={y}>
{y}
</option>
))}
</select>
<ChevronDown />
</Field>
</div>
{/* ---- Media upload (optional) ---- */}
<div className="py-4 sm:py-5">
<label className="mb-1 block text-[15px] font-bold text-slate-700 sm:text-[17px]">
📷 /
<span className="ml-2 text-xs font-normal text-slate-400">
</span>
</label>
<p className="mb-3 text-xs text-slate-400">
JPG/PNGMP4 20MB
</p>
<input
ref={fileRef}
type="file"
accept="image/jpeg,image/png,image/webp,video/mp4,video/quicktime"
className="hidden"
onChange={(e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (file.size > 20 * 1024 * 1024) {
alert("文件大小不能超过 20MB");
return;
}
const isVideo = file.type.startsWith("video/");
const preview = URL.createObjectURL(file);
setMedia({ file, preview, type: isVideo ? "video" : "image" });
}}
/>
{media ? (
<div className="relative inline-block">
{media.type === "image" ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={media.preview}
alt="preview"
className="h-32 w-32 rounded-xl border border-slate-200 object-cover sm:h-40 sm:w-40"
/>
) : (
<video
src={media.preview}
className="h-32 w-32 rounded-xl border border-slate-200 object-cover sm:h-40 sm:w-40"
muted
playsInline
controls
/>
)}
<button
type="button"
onClick={() => {
URL.revokeObjectURL(media.preview);
setMedia(null);
if (fileRef.current) fileRef.current.value = "";
}}
className="absolute -top-2 -right-2 flex h-6 w-6 items-center justify-center rounded-full bg-red-500 text-xs text-white shadow-md transition-colors hover:bg-red-600"
>
</button>
</div>
) : (
<button
type="button"
onClick={() => fileRef.current?.click()}
className="flex h-32 w-32 flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed border-slate-200 text-slate-400 transition-colors hover:border-sky-300 hover:text-sky-500 sm:h-40 sm:w-40"
>
<svg className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 4v16m8-8H4" />
</svg>
<span className="text-xs"></span>
</button>
)}
</div>
{/* ── Submit ── */}
<div className="mt-8 sm:mt-10">
<button
type="submit"
className="w-full rounded-full bg-[#1aad19] py-3.5 text-lg font-semibold text-white shadow-md shadow-emerald-500/20 transition-all hover:bg-[#179b16] hover:shadow-lg hover:shadow-emerald-500/25 active:scale-[0.98] sm:py-4"
>
</button>
<p className="mt-4 whitespace-pre-line text-center text-sm leading-relaxed text-amber-500">
{"🔍 提交后将进行审核\n💰 需支付基本场地/管理费"}
</p>
</div>
</form>
{/* ── Back link (PC) ── */}
<div className="mt-6 hidden text-center sm:block">
<a
href="/"
className="text-sm text-slate-400 transition-colors hover:text-sky-500"
>
</a>
</div>
{/* ── Back link (H5, fixed bottom) ── */}
<div className="mt-4 pb-6 text-center sm:hidden">
<a
href="/"
className="text-sm text-slate-400 transition-colors hover:text-sky-500"
>
</a>
</div>
</div>
</div>
);
}
/* ─── Reusable field wrapper ─── */
function Field({
label,
children,
noBorder,
}: {
label: string;
children: React.ReactNode;
noBorder?: boolean;
}) {
return (
<div
className={`relative flex items-center gap-3 py-3.5 sm:flex-col sm:items-stretch sm:gap-0 sm:py-4 ${
noBorder ? "" : "border-b border-slate-100 sm:border-b-0"
}`}
>
<label className="w-[90px] shrink-0 text-[15px] font-bold text-slate-700 sm:mb-2 sm:w-auto sm:text-[17px]">
{label}
</label>
<div className="relative min-w-0 flex-1">{children}</div>
</div>
);
}
/* ─── Chevron icon for selects ─── */
function ChevronDown() {
return (
<svg
className="pointer-events-none absolute top-1/2 right-2 h-4 w-4 -translate-y-1/2 text-slate-400 sm:right-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
);
}