"use client"; import { useEffect, useRef, useState } from "react"; import { api } from "@/lib/api"; import { formatDate } from "@/lib/utils"; import { cn } from "@/lib/utils"; import { Plus, Pencil, Trash2, X, Image as ImageIcon, Copy, MoreHorizontal, Star, Eye, EyeOff, CheckSquare, Square, ChevronUp, ChevronDown, Link as LinkIcon, Check, } from "lucide-react"; import { MediaPickerModal } from "@/components/admin/MediaPickerModal"; interface Meetup { id: string; title: string; description: string; date: string; time: string; location: string; link?: string; imageId?: string; status: string; featured: boolean; visibility: string; createdAt: string; updatedAt: string; } interface MeetupForm { title: string; description: string; date: string; time: string; location: string; link: string; imageId: string; status: string; featured: boolean; visibility: string; } const emptyForm: MeetupForm = { title: "", description: "", date: "", time: "", location: "", link: "", imageId: "", status: "DRAFT", featured: false, visibility: "PUBLIC", }; // Statuses that can be manually set by an admin const EDITABLE_STATUS_OPTIONS = ["DRAFT", "PUBLISHED", "CANCELLED"] as const; type EditableStatus = (typeof EDITABLE_STATUS_OPTIONS)[number]; // Display statuses (includes computed Upcoming/Past from PUBLISHED + date) type DisplayStatus = "DRAFT" | "UPCOMING" | "PAST" | "CANCELLED"; function getDisplayStatus(meetup: { status: string; date: string }): DisplayStatus { if (meetup.status === "CANCELLED") return "CANCELLED"; if (meetup.status === "DRAFT") return "DRAFT"; // PUBLISHED (or legacy UPCOMING/PAST values) → derive from date if (!meetup.date) return "DRAFT"; return new Date(meetup.date) > new Date() ? "UPCOMING" : "PAST"; } const STATUS_LABELS: Record = { DRAFT: "Draft", PUBLISHED: "Published", UPCOMING: "Upcoming", PAST: "Past", CANCELLED: "Cancelled", }; // Badge styles use the computed display status const DISPLAY_STATUS_STYLES: Record = { DRAFT: "bg-surface-container-highest text-on-surface/60", UPCOMING: "bg-green-900/40 text-green-400", PAST: "bg-surface-container-highest text-on-surface/40", CANCELLED: "bg-red-900/30 text-red-400", }; function useOutsideClick(ref: React.RefObject, callback: () => void) { useEffect(() => { function handleClick(e: MouseEvent) { if (ref.current && !ref.current.contains(e.target as Node)) { callback(); } } document.addEventListener("mousedown", handleClick); return () => document.removeEventListener("mousedown", handleClick); }, [ref, callback]); } function MoreMenu({ meetup, onCopyUrl, }: { meetup: Meetup; onCopyUrl: () => void; }) { const [open, setOpen] = useState(false); const [copied, setCopied] = useState(false); const ref = useRef(null); useOutsideClick(ref, () => setOpen(false)); const handleCopy = () => { onCopyUrl(); setCopied(true); setTimeout(() => { setCopied(false); setOpen(false); }, 1500); }; return (
{open && (
)}
); } function StatusDropdown({ meetup, onChange, }: { meetup: { status: string; date: string }; onChange: (v: string) => void; }) { const [open, setOpen] = useState(false); const ref = useRef(null); useOutsideClick(ref, () => setOpen(false)); const displayStatus = getDisplayStatus(meetup); return (
{open && (
{EDITABLE_STATUS_OPTIONS.map((s) => ( ))}
)}
); } export default function EventsPage() { const [meetups, setMeetups] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [showForm, setShowForm] = useState(false); const [editingId, setEditingId] = useState(null); const [form, setForm] = useState(emptyForm); const [saving, setSaving] = useState(false); const [showMediaPicker, setShowMediaPicker] = useState(false); const formRef = useRef(null); // Filters const [filterStatus, setFilterStatus] = useState("ALL"); const [filterCity, setFilterCity] = useState("ALL"); const [sortDir, setSortDir] = useState<"asc" | "desc">("asc"); // Bulk selection const [selected, setSelected] = useState>(new Set()); const [bulkLoading, setBulkLoading] = useState(false); const loadMeetups = async () => { try { const data = await api.getMeetups({ admin: true }); setMeetups(data as Meetup[]); } catch (err: any) { setError(err.message); } finally { setLoading(false); } }; useEffect(() => { loadMeetups(); }, []); const openCreate = () => { setForm(emptyForm); setEditingId(null); setShowForm(true); setTimeout(() => formRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }), 50); }; const openEdit = (meetup: Meetup) => { setForm({ title: meetup.title, description: meetup.description || "", date: meetup.date?.split("T")[0] || meetup.date || "", time: meetup.time || "", location: meetup.location || "", link: meetup.link || "", imageId: meetup.imageId || "", status: meetup.status || "DRAFT", featured: meetup.featured || false, visibility: meetup.visibility || "PUBLIC", }); setEditingId(meetup.id); setShowForm(true); setTimeout(() => formRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }), 50); }; const handleSave = async () => { setSaving(true); setError(""); try { const payload = { title: form.title, description: form.description, date: form.date, time: form.time || "00:00", location: form.location, link: form.link, imageId: form.imageId || null, status: form.status, featured: form.featured, visibility: form.visibility, }; if (editingId) { const updated = await api.updateMeetup(editingId, payload); setMeetups((prev) => prev.map((m) => (m.id === editingId ? updated : m))); } else { const created = await api.createMeetup(payload); setMeetups((prev) => [...prev, created]); } setShowForm(false); setEditingId(null); } catch (err: any) { setError(err.message); } finally { setSaving(false); } }; const handleDelete = async (id: string) => { if (!confirm("Delete this meetup?")) return; setMeetups((prev) => prev.filter((m) => m.id !== id)); try { await api.deleteMeetup(id); } catch (err: any) { setError(err.message); await loadMeetups(); } }; const handleDuplicate = async (id: string) => { try { const dup = await api.duplicateMeetup(id); setMeetups((prev) => [dup, ...prev]); openEdit(dup); } catch (err: any) { setError(err.message); } }; const handlePatch = async (id: string, patch: Partial) => { setMeetups((prev) => prev.map((m) => (m.id === id ? { ...m, ...patch } : m))); try { await api.updateMeetup(id, patch); } catch (err: any) { setError(err.message); await loadMeetups(); } }; const handleCopyUrl = (meetup: Meetup) => { const origin = typeof window !== "undefined" ? window.location.origin : ""; navigator.clipboard.writeText(`${origin}/events/${meetup.id}`); }; const toggleSelect = (id: string) => { setSelected((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; const selectAll = () => { if (selected.size === filtered.length) { setSelected(new Set()); } else { setSelected(new Set(filtered.map((m) => m.id))); } }; const handleBulk = async (action: "delete" | "publish" | "duplicate") => { if (selected.size === 0) return; if (action === "delete" && !confirm(`Delete ${selected.size} meetup(s)?`)) return; setBulkLoading(true); try { const ids = Array.from(selected); if (action === "delete") { await api.bulkMeetupAction("delete", ids); setMeetups((prev) => prev.filter((m) => !ids.includes(m.id))); } else if (action === "publish") { await api.bulkMeetupAction("publish", ids); setMeetups((prev) => prev.map((m) => (ids.includes(m.id) ? { ...m, status: "PUBLISHED" } : m))); } else if (action === "duplicate") { const result = await api.bulkMeetupAction("duplicate", ids); setMeetups((prev) => [...(result as Meetup[]), ...prev]); } setSelected(new Set()); } catch (err: any) { setError(err.message); await loadMeetups(); } finally { setBulkLoading(false); } }; // Derived: unique cities const cities = Array.from(new Set(meetups.map((m) => m.location).filter(Boolean))).sort(); // Filter tabs use computed display status const FILTER_STATUS_OPTIONS: Array<{ value: string; label: string }> = [ { value: "ALL", label: "All" }, { value: "UPCOMING", label: "Upcoming" }, { value: "PAST", label: "Past" }, { value: "DRAFT", label: "Draft" }, { value: "CANCELLED", label: "Cancelled" }, ]; // Filtered + sorted const filtered = meetups .filter((m) => { if (filterStatus !== "ALL" && getDisplayStatus(m) !== filterStatus) return false; if (filterCity !== "ALL" && m.location !== filterCity) return false; return true; }) .sort((a, b) => { const da = a.date || ""; const db = b.date || ""; return sortDir === "asc" ? da.localeCompare(db) : db.localeCompare(da); }); const allSelected = filtered.length > 0 && selected.size === filtered.length; if (loading) { return (
Loading meetups...
); } return (
{/* Header */}

Events

{error && (
{error}
)} {/* Create / Edit Form */} {showForm && (

{editingId ? "Edit Event" : "New Event"}

setForm({ ...form, title: e.target.value })} className="bg-surface-container-highest text-on-surface rounded-lg px-4 py-3 w-full focus:outline-none focus:ring-1 focus:ring-primary/40 md:col-span-2" /> setForm({ ...form, location: e.target.value })} className="bg-surface-container-highest text-on-surface rounded-lg px-4 py-3 w-full focus:outline-none focus:ring-1 focus:ring-primary/40" /> setForm({ ...form, date: e.target.value })} className="bg-surface-container-highest text-on-surface rounded-lg px-4 py-3 w-full focus:outline-none focus:ring-1 focus:ring-primary/40" /> setForm({ ...form, time: e.target.value })} className="bg-surface-container-highest text-on-surface rounded-lg px-4 py-3 w-full focus:outline-none focus:ring-1 focus:ring-primary/40" />
setForm({ ...form, link: e.target.value })} className="bg-surface-container-highest text-on-surface rounded-lg px-4 py-3 w-full focus:outline-none focus:ring-1 focus:ring-primary/40" />