This commit is contained in:
eric
2026-03-16 05:41:47 -05:00
parent ff31ef8001
commit c54d46517c
16 changed files with 796 additions and 286 deletions

View File

@@ -5,16 +5,7 @@ import Link from "next/link";
import ThemeToggle from "../components/ThemeToggle";
import { DIGITAL_NOMAD_WECHAT_QR } from "@/config/home.config";
/** 各学历对应的典型毕业年龄,用于推算年龄 */
const EDUCATION_GRAD_AGE: Record<string, number> = {
"高中及以下": 18,
"大专": 21,
"本科": 22,
"硕士": 25,
"博士": 28,
};
const educationOptions = ["高中及以下", "大专", "本科", "硕士", "博士"];
const graduationYears = Array.from({ length: 41 }, (_, i) => String(2026 - i));
const ageRangeOptions = ["18-22", "23-27", "28-32", "33-37", "38+"];
function getDisplayCount(text?: string, value?: number | string, suffix = ""): string | null {
if (typeof text === "string" && text.trim()) {
@@ -36,10 +27,8 @@ export default function JoinPage() {
const [xiaohongshuUrl, setXiaohongshuUrl] = useState("");
const [profession, setProfession] = useState("");
const [wechat, setWechat] = useState("");
const [phone, setPhone] = useState("");
const [intro, setIntro] = useState("");
const [education, setEducation] = useState("");
const [graduationYear, setGraduationYear] = useState("");
const [gender, setGender] = useState("");
const [ageRange, setAgeRange] = useState("");
const [agreeRules, setAgreeRules] = useState(false);
const [urlError, setUrlError] = useState<string | null>(null);
const [profile, setProfile] = useState<{
@@ -129,28 +118,24 @@ export default function JoinPage() {
e.preventDefault();
setError(null);
if (!xiaohongshuUrl.trim()) {
setError("请填写小红书主页链接");
return;
}
if (urlError) {
if (xiaohongshuUrl.trim() && urlError) {
setError("请先修正小红书链接");
return;
}
if (!education || !graduationYear) {
setError("请选择学历和毕业时间");
return;
}
if (!profession.trim()) {
setError("请填写职业");
return;
}
if (!wechat.trim() && !phone.trim()) {
setError("请填写微信号或手机号");
if (!wechat.trim()) {
setError("请填写微信号");
return;
}
if (!intro.trim()) {
setError("请填写自我介绍");
if (!gender) {
setError("请选择性别");
return;
}
if (!ageRange) {
setError("请选择年龄区间");
return;
}
if (!agreeRules) {
@@ -160,29 +145,26 @@ export default function JoinPage() {
setSubmitting(true);
try {
const gradAge = EDUCATION_GRAD_AGE[education] ?? 22;
const estimatedAge = new Date().getFullYear() - parseInt(graduationYear, 10) + gradAge;
const ageOk = estimatedAge < 28;
const isFemale = profile?.gender === "female";
const genderLabel = getGenderLabel(profile?.gender);
const postCountOk = (profile?.postCount ?? 0) > 5;
const postCountLabel = getDisplayCount(profile?.postCountText, profile?.postCount) ?? "0";
const submitXiaohongshuUrl = profile?.resolvedUrl || xiaohongshuUrl.trim();
const formGenderFemale = gender === "女" || gender === "female";
const xiaohongshuGenderFemale = profile?.gender === "female";
const postCountOk = (profile?.postCount ?? 0) > 10;
const ipInGuangdong = profile?.ipLocation?.includes("广东") ?? false;
const qualify =
formGenderFemale &&
xiaohongshuGenderFemale &&
postCountOk &&
ipInGuangdong;
const reasonParts = [
intro,
`学历:${education}`,
`毕业:${graduationYear}`,
profile?.gender && `性别:${profile.gender}`,
`发帖:${profile?.postCount ?? 0}`,
].filter(Boolean);
const genderLabel = gender === "女" ? "女" : gender === "男" ? "男" : "无";
const postCountLabel = getDisplayCount(profile?.postCountText, profile?.postCount) ?? "0";
const submitXiaohongshuUrl = profile?.resolvedUrl || xiaohongshuUrl.trim() || "";
const submitReasonParts = [
reasonParts[0],
`\u5b66\u5386:${education}`,
`\u6bd5\u4e1a:${graduationYear}`,
`\u6027\u522b:${genderLabel}`,
`\u53d1\u5e16:${postCountLabel}`,
`职业:${profession.trim()}`,
`性别:${genderLabel}`,
`年龄:${ageRange}`,
profile && `发帖:${postCountLabel}`,
profile?.ipLocation && `IP:${profile.ipLocation}`,
].filter(Boolean);
const res = await fetch("/api/join", {
@@ -191,16 +173,17 @@ export default function JoinPage() {
body: JSON.stringify({
nickname: profile?.nickname || "匿名",
wechat: wechat.trim(),
phone: phone.trim(),
phone: "",
profession: profession.trim(),
xiaohongshu_url: submitXiaohongshuUrl,
xiaohongshu_avatar_url: profile?.avatarUrl || "",
gender: genderLabel,
intro: submitReasonParts.join(" | "),
age_range: ageRange,
city: "深圳",
session: "digital-nomad",
education,
graduation_year: graduationYear,
education: "",
graduation_year: "",
agree_rules: true,
first_time: "",
status: "",
@@ -216,7 +199,7 @@ export default function JoinPage() {
throw new Error(data?.error || "提交失败,请稍后重试");
}
setQualify(isFemale && ageOk && postCountOk);
setQualify(qualify);
setSubmitted(true);
} catch (err) {
setError(err instanceof Error ? err.message : "网络错误,请重试");
@@ -229,7 +212,6 @@ export default function JoinPage() {
const followersDisplay = getDisplayCount(profile?.followersText, profile?.followers);
const likesDisplay = getDisplayCount(profile?.likesAndCollectsText, profile?.likesAndCollects);
const safePostCountDisplay = getDisplayCount(profile?.postCountText, profile?.postCount, " \u7bc7");
const postCountDisplay = getDisplayCount(profile?.postCountText, profile?.postCount, " 篇");
const profileGenderLabel = getGenderLabel(profile?.gender);
@@ -240,13 +222,13 @@ export default function JoinPage() {
{qualify ? (
<>
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-emerald-50 dark:bg-emerald-900/30 text-4xl">
✅
🎉
</div>
<h2 className="text-xl sm:text-2xl font-bold text-stone-800 dark:text-stone-100">
恭喜,符合条件
审核通过
</h2>
<p className="mt-2 text-sm text-stone-500 dark:text-stone-400">
扫码添加微信,加入活动群
扫码添加好友,加入活动群
</p>
<div className="mt-6 flex justify-center">
<img
@@ -256,7 +238,7 @@ export default function JoinPage() {
/>
</div>
<p className="mt-4 text-xs text-stone-400 dark:text-stone-500">
添加时请备注:活动报名
📝 添加时请备注:活动报名
</p>
</>
) : (
@@ -268,15 +250,19 @@ export default function JoinPage() {
资料审核中
</h2>
<p className="mt-2 text-sm text-stone-500 dark:text-stone-400">
我们会在 1-3 个工作日内审核您的资料,通过后将通过小红书私信联系您。
⏰ 通常会在 24 小时内完成审核,通过后用微信联系你。
</p>
</>
)}
<p className="mt-6 text-xs text-stone-400 dark:text-stone-500 leading-relaxed">
💰 本场收取少量场地分摊费29/人,用于覆盖民宿/场地基础成本。<br />
通过审核后再确认名额,未确认无需支付。
</p>
<Link
href="/"
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>
</div>
</div>
@@ -306,8 +292,11 @@ export default function JoinPage() {
<form onSubmit={handleSubmit} className="space-y-5">
<div>
<label className="block text-sm font-medium text-stone-700 dark:text-stone-300 mb-2">
小红书主页链接 <span className="text-red-500">*</span>
📱 小红书主页链接
</label>
<p className="text-xs text-stone-500 dark:text-stone-400 mb-1.5">
💡 建议填写,可加快审核
</p>
<input
type="url"
value={xiaohongshuUrl}
@@ -317,18 +306,17 @@ export default function JoinPage() {
}}
onBlur={handleUrlBlur}
placeholder="https://www.xiaohongshu.com/user/profile/xxx"
required
className="form-input"
/>
{validating && (
<p className="mt-1.5 text-xs text-amber-600 dark:text-amber-400">校验中…</p>
<p className="mt-1.5 text-xs text-amber-600 dark:text-amber-400">🔄 校验中…</p>
)}
{urlError && !validating && (
<p className="mt-1.5 text-xs text-red-600 dark:text-red-400">{urlError}</p>
<p className="mt-1.5 text-xs text-red-600 dark:text-red-400">⚠️ {urlError}</p>
)}
{profile && !urlError && (
<p className="mt-1.5 text-xs text-emerald-600 dark:text-emerald-400">
{"\u2713 \u5df2\u83b7\u53d6\uff1a"}{profile.nickname}
✅ 已获取:{profile.nickname}
{profile.redbookId && ` \u00b7 @${profile.redbookId}`}
{profile.ipLocation && ` \u00b7 ${profile.ipLocation}`}
{profile.personalLink && ` \u00b7 ${profile.personalLink}`}
@@ -339,24 +327,11 @@ export default function JoinPage() {
{` \u00b7 ${profileGenderLabel}`}
</p>
)}
{((profile) => false ? (
<p className="mt-1.5 text-xs text-emerald-600 dark:text-emerald-400">
✓ 已获取:{profile.nickname}
{profile.redbookId && ` · @${profile.redbookId}`}
{profile.ipLocation && ` · ${profile.ipLocation}`}
{profile.personalLink && ` · ${profile.personalLink}`}
{profile.following != null && ` · 关注 ${profile.following}`}
{profile.followers != null && ` · 粉丝 ${profile.followers}`}
{profile.likesAndCollects != null && ` · 获赞与收藏 ${profile.likesAndCollects}`}
{profile.postCount != null && ` · 发帖 ${profile.postCount} 篇`}
{profile.gender && ` · ${profile.gender === "female" ? "女" : "男"}`}
</p>
) : null)(profile as NonNullable<typeof profile>)}
</div>
<div>
<label className="block text-sm font-medium text-stone-700 dark:text-stone-300 mb-2">
职业 <span className="text-red-500">*</span>
💼 职业 <span className="text-red-500">*</span>
</label>
<input
type="text"
@@ -369,94 +344,54 @@ export default function JoinPage() {
/>
</div>
<div className="grid sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-stone-700 dark:text-stone-300 mb-2">
微信号
</label>
<input
type="text"
maxLength={140}
placeholder="用于拉群"
value={wechat}
onChange={(e) => setWechat(e.target.value)}
className="form-input"
/>
</div>
<div>
<label className="block text-sm font-medium text-stone-700 dark:text-stone-300 mb-2">
手机号
</label>
<input
type="tel"
maxLength={11}
pattern="^\d{11}$"
placeholder="请输入11位手机号"
value={phone}
onChange={(e) => setPhone(e.target.value.replace(/\D/g, "").slice(0, 11))}
className="form-input"
/>
</div>
</div>
<p className="text-xs text-stone-500 dark:text-stone-400 -mt-2">
微信号和手机号至少填写一项
</p>
<div>
<label className="block text-sm font-medium text-stone-700 dark:text-stone-300 mb-2">
自我介绍 <span className="text-red-500">*</span>
💬 微信号 <span className="text-red-500">*</span>
</label>
<textarea
maxLength={500}
rows={5}
placeholder="分享您的经历、兴趣或加入原因,越详细越好哦~"
value={intro}
onChange={(e) => setIntro(e.target.value)}
<input
type="text"
maxLength={140}
placeholder="用于拉群"
value={wechat}
onChange={(e) => setWechat(e.target.value)}
required
className="form-input min-h-[120px] resize-none"
className="form-input"
/>
<span className="text-xs text-stone-400 dark:text-stone-500">
{intro.length}/500
</span>
</div>
<div className="grid sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-stone-700 dark:text-stone-300 mb-2">
学历 <span className="text-red-500">*</span>
👤 性别 <span className="text-red-500">*</span>
</label>
<select
value={education}
onChange={(e) => setEducation(e.target.value)}
value={gender}
onChange={(e) => setGender(e.target.value)}
required
className={`form-input appearance-none ${education ? "text-stone-800 dark:text-stone-100" : "text-stone-400 dark:text-stone-500"}`}
className={`form-input appearance-none ${gender ? "text-stone-800 dark:text-stone-100" : "text-stone-400 dark:text-stone-500"}`}
>
<option value="">请选择</option>
{educationOptions.map((o) => (
<option value="女">女</option>
<option value="男">男</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-stone-700 dark:text-stone-300 mb-2">
📅 年龄区间 <span className="text-red-500">*</span>
</label>
<select
value={ageRange}
onChange={(e) => setAgeRange(e.target.value)}
required
className={`form-input appearance-none ${ageRange ? "text-stone-800 dark:text-stone-100" : "text-stone-400 dark:text-stone-500"}`}
>
<option value="">请选择</option>
{ageRangeOptions.map((o) => (
<option key={o} value={o}>{o}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-stone-700 dark:text-stone-300 mb-2">
毕业时间 <span className="text-red-500">*</span>
</label>
<select
value={graduationYear}
onChange={(e) => setGraduationYear(e.target.value)}
required
className={`form-input appearance-none ${graduationYear ? "text-stone-800 dark:text-stone-100" : "text-stone-400 dark:text-stone-500"}`}
>
<option value="">请选择</option>
{graduationYears.map((y) => (
<option key={y} value={y}>{y}</option>
))}
</select>
</div>
</div>
<p className="text-xs text-stone-500 dark:text-stone-400 -mt-2">
年龄将根据学历和毕业时间推算
</p>
<div>
<label className="flex items-start gap-3 cursor-pointer">
@@ -468,31 +403,31 @@ export default function JoinPage() {
className="mt-1 rounded border-stone-300 text-amber-600 focus:ring-amber-500"
/>
<span className="text-xs sm:text-sm text-stone-600 dark:text-stone-400">
我已阅读并同意社区规则
📋 我已阅读并同意社区规则
</span>
</label>
</div>
{error && (
<p className="rounded-lg bg-red-50 dark:bg-red-900/30 px-4 py-3 text-sm text-red-600 dark:text-red-400">
{error}
<p className="rounded-lg bg-red-50 dark:bg-red-900/30 px-4 py-3 text-sm text-red-600 dark:text-red-400 flex items-center gap-2">
⚠️ {error}
</p>
)}
<button
type="submit"
disabled={submitting || !!urlError || validating}
className="w-full rounded-xl bg-amber-600 py-3.5 text-base font-medium text-white transition-colors hover:bg-amber-700 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50"
className="w-full rounded-xl bg-amber-600 py-3.5 text-base font-medium text-white transition-colors hover:bg-amber-700 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50 flex items-center justify-center gap-2"
>
{submitting ? "提交中…" : "提交报名"}
{submitting ? "🔄 提交中…" : "📤 提交报名"}
</button>
</form>
</div>
</div>
<div className="mt-4 text-center">
<Link href="/" className="text-xs sm:text-sm text-stone-400 dark:text-stone-500 hover:text-amber-600">
← 返回首页
<Link href="/" className="text-xs sm:text-sm text-stone-400 dark:text-stone-500 hover:text-amber-600 inline-flex items-center gap-1">
🏠 返回首页
</Link>
</div>
</div>