From b65908e328ff3402eb8188c86ca4b0b1f9e75cbe Mon Sep 17 00:00:00 2001 From: root Date: Mon, 8 Jun 2026 10:02:16 +0800 Subject: [PATCH] s --- app/[locale]/city/[slug]/volunteer/page.tsx | 392 ++++++++++++++++++ app/[locale]/dating/page.tsx | 25 +- app/[locale]/discuss/page.tsx | 19 +- app/[locale]/meetups/page.tsx | 21 +- app/components/CityCard.tsx | 36 +- app/components/CityModal.tsx | 222 +++++++++- app/components/HeroSection.tsx | 23 +- app/data/avatars.ts | 16 + app/data/city-social.ts | 271 ++++++++++++ app/data/mock.ts | 14 +- app/lib/city-slugs.ts | 60 +++ .../__pycache__/enhanced_seed.cpython-312.pyc | Bin 6932 -> 7867 bytes backend/__pycache__/init_db.cpython-312.pyc | Bin 15947 -> 18760 bytes backend/__pycache__/main.cpython-312.pyc | Bin 77712 -> 116312 bytes backend/__pycache__/seed_data.cpython-312.pyc | Bin 28884 -> 31738 bytes backend/__pycache__/settings.cpython-312.pyc | Bin 2209 -> 9388 bytes backend/avatar_assets.py | 13 + backend/enhanced_seed.py | 7 +- backend/init_db.py | 19 + backend/main.py | 88 +++- backend/seed_data.py | 16 +- config/home.config.ts | 51 +-- messages/en.json | 8 +- messages/zh.json | 8 +- 24 files changed, 1216 insertions(+), 93 deletions(-) create mode 100644 app/[locale]/city/[slug]/volunteer/page.tsx create mode 100644 app/data/avatars.ts create mode 100644 app/data/city-social.ts create mode 100644 app/lib/city-slugs.ts create mode 100644 backend/avatar_assets.py diff --git a/app/[locale]/city/[slug]/volunteer/page.tsx b/app/[locale]/city/[slug]/volunteer/page.tsx new file mode 100644 index 0000000..1eb1dd7 --- /dev/null +++ b/app/[locale]/city/[slug]/volunteer/page.tsx @@ -0,0 +1,392 @@ +"use client"; + +import { use, useState, type FormEvent } from "react"; +import { Link, useLocale } from "@/i18n/navigation"; +import { apiFetch } from "@/app/lib/api-client"; +import { citySlug, findCityBySlug } from "@/app/lib/city-slugs"; + +interface CityVolunteerPageProps { + params: Promise<{ slug: string }>; +} + +interface VolunteerForm { + applicantName: string; + email: string; + wechat: string; + phone: string; + currentCity: string; + availability: string; + localExperience: string; + motivation: string; + links: string; + roles: string[]; +} + +const ROLE_OPTIONS = { + zh: ["城市资料维护", "新成员 CoffeeChat", "地陪路线协助", "Citywalk/徒步带队", "同城活动协助", "线上社群答疑"], + en: ["City data updates", "New member CoffeeChat", "Local guide routes", "Citywalk / hiking host", "City event support", "Online community support"], +}; + +const copy = { + zh: { + notFound: "城市不存在", + back: "返回首页", + kicker: "城市志愿者招募", + titleSuffix: "城市志愿者申请", + subtitle: "帮助维护城市资料、迎新 CoffeeChat、组织同城活动或带 Citywalk。提交后会进入审核状态。", + status: "提交后状态:待审核", + applicantName: "姓名 / 昵称", + applicantNamePlaceholder: "方便社区联系你的称呼", + email: "邮箱", + wechat: "微信", + phone: "手机", + currentCity: "当前所在城市", + availability: "可投入时间", + availabilityPlaceholder: "例如:每周 2 小时 / 周末可带活动", + roles: "想参与的志愿者方向", + localExperience: "你和这个城市的关系", + localExperiencePlaceholder: "例如:已在这里居住 1 年,熟悉咖啡馆、短租和徒步路线。", + motivation: "申请说明", + motivationPlaceholder: "你希望如何帮助这个城市的新成员?", + links: "补充资料链接", + linksPlaceholder: "可选:个人主页、社群资料、作品链接等", + contactHint: "邮箱、微信、手机至少填写一项。", + roleHint: "至少选择一个志愿者方向。", + submit: "提交申请", + submitting: "提交中...", + successTitle: "申请已提交", + successDesc: "当前状态:待审核。审核通过后,社区会把你加入该城市的志愿者协作名单。", + submitAnother: "再提交一份", + viewCity: "返回城市列表", + }, + en: { + notFound: "City not found", + back: "Back home", + kicker: "City volunteer recruiting", + titleSuffix: "Volunteer application", + subtitle: "Help maintain city data, welcome new members, support local events, or host citywalks. Applications enter pending review after submission.", + status: "After submission: pending review", + applicantName: "Name / nickname", + applicantNamePlaceholder: "How the community should contact you", + email: "Email", + wechat: "WeChat", + phone: "Phone", + currentCity: "Current city", + availability: "Availability", + availabilityPlaceholder: "e.g. 2 hours weekly / weekends for events", + roles: "Volunteer areas", + localExperience: "Your connection to this city", + localExperiencePlaceholder: "e.g. I have lived here for one year and know cafes, short rentals, and hiking routes.", + motivation: "Application note", + motivationPlaceholder: "How would you help new members in this city?", + links: "Supporting links", + linksPlaceholder: "Optional: profile, community record, portfolio links", + contactHint: "Please provide at least one contact: email, WeChat, or phone.", + roleHint: "Please select at least one volunteer area.", + submit: "Submit application", + submitting: "Submitting...", + successTitle: "Application submitted", + successDesc: "Current status: pending review. Once approved, the community will add you to this city's volunteer roster.", + submitAnother: "Submit another", + viewCity: "Back to cities", + }, +}; + +function getCopy(locale: string) { + return locale === "zh" ? copy.zh : copy.en; +} + +const initialForm: VolunteerForm = { + applicantName: "", + email: "", + wechat: "", + phone: "", + currentCity: "", + availability: "", + localExperience: "", + motivation: "", + links: "", + roles: [], +}; + +export default function CityVolunteerPage({ params }: CityVolunteerPageProps) { + const { slug } = use(params); + const city = findCityBySlug(slug); + const locale = useLocale(); + const c = getCopy(locale); + const roleOptions = locale === "zh" ? ROLE_OPTIONS.zh : ROLE_OPTIONS.en; + const [form, setForm] = useState(initialForm); + const [submitted, setSubmitted] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + if (!city) { + return ( +
+
+

{c.notFound}

+ + {c.back} + +
+
+ ); + } + + const slugToSubmit = citySlug(city); + + const setField = (field: K, value: VolunteerForm[K]) => { + setForm((current) => ({ ...current, [field]: value })); + }; + + const toggleRole = (role: string) => { + setForm((current) => { + const selected = new Set(current.roles); + if (selected.has(role)) selected.delete(role); + else selected.add(role); + return { ...current, roles: Array.from(selected) }; + }); + }; + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + setError(null); + if (!form.email.trim() && !form.wechat.trim() && !form.phone.trim()) { + setError(c.contactHint); + return; + } + if (form.roles.length === 0) { + setError(c.roleHint); + return; + } + setSubmitting(true); + try { + await apiFetch(`/api/cities/${slugToSubmit}/volunteer-applications`, { + method: "POST", + body: JSON.stringify({ + citySlug: slugToSubmit, + cityName: city.name, + ...form, + }), + }); + setSubmitted(true); + } catch (err) { + setError(err instanceof Error ? err.message : "Submit failed"); + } finally { + setSubmitting(false); + } + }; + + if (submitted) { + return ( +
+
+
+ OK +
+

{city.name}

+

{c.successTitle}

+

{c.successDesc}

+
+ + + {c.viewCity} + +
+
+
+ ); + } + + return ( +
+
+
+ + {c.back} + +

{c.kicker}

+
+
+

+ {city.name} {c.titleSuffix} +

+

{c.subtitle}

+
+ + {c.status} + +
+
+
+ +
+
+
+ setField("applicantName", value)} + /> + setField("currentCity", value)} + /> +
+ +
+ setField("email", value)} /> + setField("wechat", value)} /> + setField("phone", value)} /> +
+ + setField("availability", value)} + /> + +
+ {c.roles} * +
+ {roleOptions.map((role) => { + const checked = form.roles.includes(role); + return ( + + ); + })} +
+
+ +