227 lines
8.7 KiB
TypeScript
227 lines
8.7 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import { Link, useTranslation } from "@/i18n/navigation";
|
||
import { cities } from "@/app/data/cities";
|
||
import { apiFetch } from "@/app/lib/api-client";
|
||
|
||
const CITY_OPTIONS = cities.map((c) => ({ value: c.name, label: `${c.icon} ${c.name}` }));
|
||
const CITY_BY_NAME = new Map(cities.map((city) => [city.name, city]));
|
||
|
||
export default function HostMeetupPage() {
|
||
const { t } = useTranslation("hostMeetup");
|
||
const [submitted, setSubmitted] = useState(false);
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [form, setForm] = useState({
|
||
city: "",
|
||
date: "",
|
||
time: "19:00",
|
||
venue: "",
|
||
address: "",
|
||
description: "",
|
||
maxAttendees: "20",
|
||
});
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
const city = CITY_BY_NAME.get(form.city);
|
||
setSubmitting(true);
|
||
try {
|
||
await apiFetch("/api/meetups", {
|
||
method: "POST",
|
||
body: JSON.stringify({
|
||
city: form.city,
|
||
emoji: city?.icon || "📍",
|
||
date: form.date,
|
||
time: form.time,
|
||
venue: form.venue,
|
||
address: form.address,
|
||
description: form.description,
|
||
rsvpCount: 1,
|
||
gradientFrom: city?.gradientFrom || "#ff4d4f",
|
||
gradientTo: city?.gradientTo || "#ff7a45",
|
||
attendees: [],
|
||
isUpcoming: true,
|
||
status: "pending",
|
||
}),
|
||
});
|
||
setSubmitted(true);
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
if (submitted) {
|
||
return (
|
||
<div className="min-h-screen bg-[#fafafa] dark:bg-gray-950">
|
||
<main className="max-w-[600px] mx-auto px-4 sm:px-6 py-12 sm:py-20">
|
||
<div className="bg-white dark:bg-gray-900 rounded-2xl shadow-lg border border-gray-100 dark:border-gray-800 p-8 sm:p-12 text-center">
|
||
<span className="text-6xl mb-6 block">🎉</span>
|
||
<h2 className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-gray-100 mb-3">
|
||
{t("successTitle")}
|
||
</h2>
|
||
<p className="text-gray-500 dark:text-gray-400 mb-8">
|
||
{t("successDesc")}
|
||
</p>
|
||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||
<Link
|
||
href="/meetups/host"
|
||
onClick={() => setSubmitted(false)}
|
||
className="px-6 py-3 rounded-xl border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300 font-medium hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||
>
|
||
{t("createAnother")}
|
||
</Link>
|
||
<Link
|
||
href="/meetups"
|
||
className="px-6 py-3 rounded-xl bg-[#ff4d4f] text-white font-medium hover:bg-[#ff7a45] transition-colors"
|
||
>
|
||
{t("viewMeetups")}
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-[#fafafa] dark:bg-gray-950">
|
||
<main className="max-w-[640px] mx-auto px-4 sm:px-6 py-8 sm:py-12">
|
||
<header className="mb-8">
|
||
<Link
|
||
href="/meetups"
|
||
className="inline-flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 mb-4"
|
||
>
|
||
← {t("backToMeetups")}
|
||
</Link>
|
||
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||
<span className="w-1 h-8 bg-[#ff4d4f] rounded-full" />
|
||
{t("title")}
|
||
</h1>
|
||
<p className="text-gray-500 dark:text-gray-400 mt-1 text-sm sm:text-base">
|
||
{t("subtitle")}
|
||
</p>
|
||
</header>
|
||
|
||
<form
|
||
onSubmit={handleSubmit}
|
||
className="bg-white dark:bg-gray-900 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-800 p-6 sm:p-8 space-y-6"
|
||
>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
{t("city")} *
|
||
</label>
|
||
<select
|
||
required
|
||
value={form.city}
|
||
onChange={(e) => setForm((f) => ({ ...f, city: e.target.value }))}
|
||
className="form-input w-full border border-gray-200 dark:border-gray-700 rounded-xl px-4 py-3 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
|
||
>
|
||
<option value="">{t("selectCity")}</option>
|
||
{CITY_OPTIONS.map((c) => (
|
||
<option key={c.value} value={c.value}>
|
||
{c.label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
{t("date")} *
|
||
</label>
|
||
<input
|
||
type="date"
|
||
required
|
||
value={form.date}
|
||
onChange={(e) => setForm((f) => ({ ...f, date: e.target.value }))}
|
||
className="form-input w-full border border-gray-200 dark:border-gray-700 rounded-xl px-4 py-3 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
{t("time")} *
|
||
</label>
|
||
<input
|
||
type="time"
|
||
required
|
||
value={form.time}
|
||
onChange={(e) => setForm((f) => ({ ...f, time: e.target.value }))}
|
||
className="form-input w-full border border-gray-200 dark:border-gray-700 rounded-xl px-4 py-3 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
{t("venue")} *
|
||
</label>
|
||
<input
|
||
type="text"
|
||
required
|
||
placeholder={t("venuePlaceholder")}
|
||
value={form.venue}
|
||
onChange={(e) => setForm((f) => ({ ...f, venue: e.target.value }))}
|
||
className="form-input w-full border border-gray-200 dark:border-gray-700 rounded-xl px-4 py-3 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-400"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
{t("address")} *
|
||
</label>
|
||
<input
|
||
type="text"
|
||
required
|
||
placeholder={t("addressPlaceholder")}
|
||
value={form.address}
|
||
onChange={(e) => setForm((f) => ({ ...f, address: e.target.value }))}
|
||
className="form-input w-full border border-gray-200 dark:border-gray-700 rounded-xl px-4 py-3 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-400"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
{t("description")}
|
||
</label>
|
||
<textarea
|
||
rows={4}
|
||
placeholder={t("descriptionPlaceholder")}
|
||
value={form.description}
|
||
onChange={(e) => setForm((f) => ({ ...f, description: e.target.value }))}
|
||
className="form-input w-full border border-gray-200 dark:border-gray-700 rounded-xl px-4 py-3 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-400 resize-none"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
{t("maxAttendees")}
|
||
</label>
|
||
<input
|
||
type="number"
|
||
min={2}
|
||
max={100}
|
||
value={form.maxAttendees}
|
||
onChange={(e) => setForm((f) => ({ ...f, maxAttendees: e.target.value }))}
|
||
className="form-input w-full border border-gray-200 dark:border-gray-700 rounded-xl px-4 py-3 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
|
||
/>
|
||
</div>
|
||
|
||
<p className="text-xs text-gray-400 dark:text-gray-500">
|
||
提交后会进入本机 PocketBase 的 meetups 表,审核通过前状态为 pending。
|
||
</p>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={submitting}
|
||
className="w-full py-3.5 rounded-xl bg-[#ff4d4f] text-white font-semibold hover:bg-[#ff7a45] transition-colors"
|
||
>
|
||
{submitting ? "提交中..." : t("submit")}
|
||
</button>
|
||
</form>
|
||
</main>
|
||
</div>
|
||
);
|
||
}
|