'use client'; import { useEffect, useState } from 'react'; import { eventsApi, siteSettingsApi, Event } from '@/lib/api'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; import Input from '@/components/ui/Input'; import MediaPicker from '@/components/MediaPicker'; import { StarIcon, TrashIcon, XMarkIcon } from '@heroicons/react/24/outline'; import toast from 'react-hot-toast'; import { useLanguage } from '@/context/LanguageContext'; import { parseDate, EVENT_TIMEZONE } from '@/lib/utils'; interface EventFormData { title: string; titleEs: string; slug: string; description: string; descriptionEs: string; shortDescription: string; shortDescriptionEs: string; startDatetime: string; endDatetime: string; location: string; locationUrl: string; price: number; currency: string; capacity: number; status: 'draft' | 'published' | 'unlisted' | 'cancelled' | 'completed' | 'archived'; bannerUrl: string; externalBookingEnabled: boolean; externalBookingUrl: string; } const EMPTY_FORM: EventFormData = { title: '', titleEs: '', slug: '', description: '', descriptionEs: '', shortDescription: '', shortDescriptionEs: '', startDatetime: '', endDatetime: '', location: '', locationUrl: '', price: 0, currency: 'PYG', capacity: 50, status: 'draft', bannerUrl: '', externalBookingEnabled: false, externalBookingUrl: '', }; function isoToLocalDatetime(isoString: string): string { const date = parseDate(isoString); const parts = new Intl.DateTimeFormat('en-US', { timeZone: EVENT_TIMEZONE, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false, }).formatToParts(date); const get = (type: string) => parts.find(p => p.type === type)!.value; const h = get('hour') === '24' ? '00' : get('hour'); return `${get('year')}-${get('month')}-${get('day')}T${h}:${get('minute')}`; } interface EventFormModalProps { /** When true the modal is rendered. */ open: boolean; /** The event being edited, or null to create a new event. */ event: Event | null; /** Currently featured event id (owned by the parent page). */ featuredEventId: string | null; /** Notify the parent when the featured event changes. */ onFeaturedChange: (id: string | null) => void; /** Close the modal without saving. */ onClose: () => void; /** Called after a successful create/update so the parent can refresh. */ onSaved: () => void; } /** * Shared create/edit event modal used by both the events list page and the * single-event detail page. Owns its own form state so it can be dropped in * anywhere; the parent only supplies the event to edit and refresh callbacks. */ export default function EventFormModal({ open, event, featuredEventId, onFeaturedChange, onClose, onSaved, }: EventFormModalProps) { const { t } = useLanguage(); const [formData, setFormData] = useState(EMPTY_FORM); const [slugAliases, setSlugAliases] = useState<{ slug: string; createdAt: string }[]>([]); const [saving, setSaving] = useState(false); const [settingFeatured, setSettingFeatured] = useState(false); useEffect(() => { if (!open) return; if (event) { setFormData({ title: event.title, titleEs: event.titleEs || '', slug: event.slug || '', description: event.description, descriptionEs: event.descriptionEs || '', shortDescription: event.shortDescription || '', shortDescriptionEs: event.shortDescriptionEs || '', startDatetime: isoToLocalDatetime(event.startDatetime), endDatetime: event.endDatetime ? isoToLocalDatetime(event.endDatetime) : '', location: event.location, locationUrl: event.locationUrl || '', price: event.price, currency: event.currency, capacity: event.capacity, status: event.status, bannerUrl: event.bannerUrl || '', externalBookingEnabled: event.externalBookingEnabled || false, externalBookingUrl: event.externalBookingUrl || '', }); loadSlugAliases(event.id); } else { setFormData(EMPTY_FORM); setSlugAliases([]); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, event]); const loadSlugAliases = async (eventId: string) => { try { const { aliases } = await eventsApi.getSlugAliases(eventId); setSlugAliases(aliases); } catch (error) { setSlugAliases([]); } }; const handleRemoveAlias = async (slug: string) => { if (!event) return; if (!confirm(`Remove alias "${slug}"? The old URL /events/${slug} will stop working.`)) return; try { await eventsApi.deleteSlugAlias(event.id, slug); toast.success('Alias removed'); setSlugAliases((prev) => prev.filter((a) => a.slug !== slug)); } catch (error: any) { toast.error(error.message || 'Failed to remove alias'); } }; const handleSetFeatured = async (eventId: string | null) => { setSettingFeatured(true); try { await siteSettingsApi.setFeaturedEvent(eventId); onFeaturedChange(eventId); toast.success(eventId ? 'Event set as featured' : 'Featured event removed'); } catch (error: any) { toast.error(error.message || 'Failed to update featured event'); } finally { setSettingFeatured(false); } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setSaving(true); try { if (formData.externalBookingEnabled && !formData.externalBookingUrl) { toast.error('External booking URL is required when external booking is enabled'); setSaving(false); return; } if (formData.externalBookingEnabled && !formData.externalBookingUrl.startsWith('https://')) { toast.error('External booking URL must be a valid HTTPS link'); setSaving(false); return; } const eventData: Partial = { title: formData.title, titleEs: formData.titleEs || undefined, description: formData.description, descriptionEs: formData.descriptionEs || undefined, shortDescription: formData.shortDescription || undefined, shortDescriptionEs: formData.shortDescriptionEs || undefined, startDatetime: formData.startDatetime, endDatetime: formData.endDatetime || undefined, location: formData.location, locationUrl: formData.locationUrl || undefined, price: formData.price, currency: formData.currency, capacity: formData.capacity, status: formData.status, bannerUrl: formData.bannerUrl || undefined, externalBookingEnabled: formData.externalBookingEnabled, externalBookingUrl: formData.externalBookingEnabled ? formData.externalBookingUrl : undefined, }; if (event) { // Only send slug when editing so creates still auto-generate from title eventData.slug = formData.slug || undefined; await eventsApi.update(event.id, eventData); toast.success('Event updated'); } else { await eventsApi.create(eventData); toast.success('Event created'); } onSaved(); } catch (error: any) { toast.error(error.message || 'Failed to save event'); } finally { setSaving(false); } }; if (!open) return null; return (

{event ? t('admin.events.edit') : t('admin.events.create')}

setFormData({ ...formData, title: e.target.value })} required /> setFormData({ ...formData, titleEs: e.target.value })} />
{event && (
setFormData({ ...formData, slug: e.target.value })} placeholder="auto-generated from title" />

Public URL: /events/{formData.slug || '...'} . Changing the slug keeps the old one as a redirecting alias.

{slugAliases.length > 0 && (

URL aliases

Old URLs that still redirect to the current slug. Removing one breaks those links.

    {slugAliases.map((alias) => (
  • /events/{alias.slug}
  • ))}
)}
)}