- Add featured_event_id to site_settings (schema + migration) - Backend: featured event logic in /events/next/upcoming with auto-unset when event ends - Site settings: PUT supports featuredEventId, add PUT /featured-event for admin - Admin events: Set as featured checkbox in editor, star toggle in list, featured badge - Admin settings: Featured Event section with current event and remove/change links - API: siteSettingsApi.setFeaturedEvent(), Event.isFeatured, SiteSettings.featuredEventId - Homepage/linktree unchanged: still use getNextUpcoming (now returns featured or fallback)
677 lines
28 KiB
TypeScript
677 lines
28 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import Link from 'next/link';
|
|
import { useLanguage } from '@/context/LanguageContext';
|
|
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 { PlusIcon, PencilIcon, TrashIcon, EyeIcon, PhotoIcon, DocumentDuplicateIcon, ArchiveBoxIcon, StarIcon } from '@heroicons/react/24/outline';
|
|
import { StarIcon as StarIconSolid } from '@heroicons/react/24/solid';
|
|
import toast from 'react-hot-toast';
|
|
import clsx from 'clsx';
|
|
|
|
export default function AdminEventsPage() {
|
|
const { t, locale } = useLanguage();
|
|
const [events, setEvents] = useState<Event[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [showForm, setShowForm] = useState(false);
|
|
const [editingEvent, setEditingEvent] = useState<Event | null>(null);
|
|
const [saving, setSaving] = useState(false);
|
|
const [featuredEventId, setFeaturedEventId] = useState<string | null>(null);
|
|
const [settingFeatured, setSettingFeatured] = useState<string | null>(null);
|
|
|
|
const [formData, setFormData] = useState<{
|
|
title: string;
|
|
titleEs: 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' | 'cancelled' | 'completed' | 'archived';
|
|
bannerUrl: string;
|
|
externalBookingEnabled: boolean;
|
|
externalBookingUrl: string;
|
|
}>({
|
|
title: '',
|
|
titleEs: '',
|
|
description: '',
|
|
descriptionEs: '',
|
|
shortDescription: '',
|
|
shortDescriptionEs: '',
|
|
startDatetime: '',
|
|
endDatetime: '',
|
|
location: '',
|
|
locationUrl: '',
|
|
price: 0,
|
|
currency: 'PYG',
|
|
capacity: 50,
|
|
status: 'draft',
|
|
bannerUrl: '',
|
|
externalBookingEnabled: false,
|
|
externalBookingUrl: '',
|
|
});
|
|
|
|
useEffect(() => {
|
|
loadEvents();
|
|
loadFeaturedEvent();
|
|
}, []);
|
|
|
|
const loadEvents = async () => {
|
|
try {
|
|
const { events } = await eventsApi.getAll();
|
|
setEvents(events);
|
|
} catch (error) {
|
|
toast.error('Failed to load events');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const loadFeaturedEvent = async () => {
|
|
try {
|
|
const { settings } = await siteSettingsApi.get();
|
|
setFeaturedEventId(settings.featuredEventId || null);
|
|
} catch (error) {
|
|
// Ignore error - settings may not exist yet
|
|
}
|
|
};
|
|
|
|
const handleSetFeatured = async (eventId: string | null) => {
|
|
setSettingFeatured(eventId || 'clearing');
|
|
try {
|
|
await siteSettingsApi.setFeaturedEvent(eventId);
|
|
setFeaturedEventId(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(null);
|
|
}
|
|
};
|
|
|
|
const resetForm = () => {
|
|
setFormData({
|
|
title: '',
|
|
titleEs: '',
|
|
description: '',
|
|
descriptionEs: '',
|
|
shortDescription: '',
|
|
shortDescriptionEs: '',
|
|
startDatetime: '',
|
|
endDatetime: '',
|
|
location: '',
|
|
locationUrl: '',
|
|
price: 0,
|
|
currency: 'PYG',
|
|
capacity: 50,
|
|
status: 'draft' as const,
|
|
bannerUrl: '',
|
|
externalBookingEnabled: false,
|
|
externalBookingUrl: '',
|
|
});
|
|
setEditingEvent(null);
|
|
};
|
|
|
|
// Convert ISO UTC string to local datetime-local format (YYYY-MM-DDTHH:MM)
|
|
const isoToLocalDatetime = (isoString: string): string => {
|
|
const date = new Date(isoString);
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
const hours = String(date.getHours()).padStart(2, '0');
|
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
|
};
|
|
|
|
const handleEdit = (event: Event) => {
|
|
setFormData({
|
|
title: event.title,
|
|
titleEs: event.titleEs || '',
|
|
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 || '',
|
|
});
|
|
setEditingEvent(event);
|
|
setShowForm(true);
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setSaving(true);
|
|
|
|
try {
|
|
// Validate external booking URL if enabled
|
|
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 = {
|
|
title: formData.title,
|
|
titleEs: formData.titleEs || undefined,
|
|
description: formData.description,
|
|
descriptionEs: formData.descriptionEs || undefined,
|
|
shortDescription: formData.shortDescription || undefined,
|
|
shortDescriptionEs: formData.shortDescriptionEs || undefined,
|
|
startDatetime: new Date(formData.startDatetime).toISOString(),
|
|
endDatetime: formData.endDatetime ? new Date(formData.endDatetime).toISOString() : 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 (editingEvent) {
|
|
await eventsApi.update(editingEvent.id, eventData);
|
|
toast.success('Event updated');
|
|
} else {
|
|
await eventsApi.create(eventData);
|
|
toast.success('Event created');
|
|
}
|
|
|
|
setShowForm(false);
|
|
resetForm();
|
|
loadEvents();
|
|
} catch (error: any) {
|
|
toast.error(error.message || 'Failed to save event');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (id: string) => {
|
|
if (!confirm('Are you sure you want to delete this event?')) return;
|
|
|
|
try {
|
|
await eventsApi.delete(id);
|
|
toast.success('Event deleted');
|
|
loadEvents();
|
|
} catch (error) {
|
|
toast.error('Failed to delete event');
|
|
}
|
|
};
|
|
|
|
const handleStatusChange = async (event: Event, status: Event['status']) => {
|
|
try {
|
|
await eventsApi.update(event.id, { status });
|
|
toast.success('Status updated');
|
|
loadEvents();
|
|
} catch (error) {
|
|
toast.error('Failed to update status');
|
|
}
|
|
};
|
|
|
|
|
|
const formatDate = (dateStr: string) => {
|
|
return new Date(dateStr).toLocaleDateString(locale === 'es' ? 'es-ES' : 'en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
});
|
|
};
|
|
|
|
const getStatusBadge = (status: string) => {
|
|
const styles: Record<string, string> = {
|
|
draft: 'badge-gray',
|
|
published: 'badge-success',
|
|
cancelled: 'badge-danger',
|
|
completed: 'badge-info',
|
|
archived: 'badge-gray',
|
|
};
|
|
return <span className={`badge ${styles[status] || 'badge-gray'}`}>{status}</span>;
|
|
};
|
|
|
|
const handleDuplicate = async (event: Event) => {
|
|
try {
|
|
await eventsApi.duplicate(event.id);
|
|
toast.success('Event duplicated successfully');
|
|
loadEvents();
|
|
} catch (error) {
|
|
toast.error('Failed to duplicate event');
|
|
}
|
|
};
|
|
|
|
const handleArchive = async (event: Event) => {
|
|
try {
|
|
await eventsApi.update(event.id, { status: 'archived' });
|
|
toast.success('Event archived');
|
|
loadEvents();
|
|
} catch (error) {
|
|
toast.error('Failed to archive event');
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="animate-spin w-8 h-8 border-4 border-primary-yellow border-t-transparent rounded-full" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h1 className="text-2xl font-bold text-primary-dark">{t('admin.events.title')}</h1>
|
|
<Button onClick={() => { resetForm(); setShowForm(true); }}>
|
|
<PlusIcon className="w-5 h-5 mr-2" />
|
|
{t('admin.events.create')}
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Event Form Modal */}
|
|
{showForm && (
|
|
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
|
<Card className="w-full max-w-2xl max-h-[90vh] overflow-y-auto p-6">
|
|
<h2 className="text-xl font-bold mb-6">
|
|
{editingEvent ? t('admin.events.edit') : t('admin.events.create')}
|
|
</h2>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<Input
|
|
label="Title (English)"
|
|
value={formData.title}
|
|
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
|
required
|
|
/>
|
|
<Input
|
|
label="Title (Spanish)"
|
|
value={formData.titleEs}
|
|
onChange={(e) => setFormData({ ...formData, titleEs: e.target.value })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1">Description (English)</label>
|
|
<textarea
|
|
value={formData.description}
|
|
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
|
className="w-full px-4 py-3 rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
|
|
rows={3}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1">Description (Spanish)</label>
|
|
<textarea
|
|
value={formData.descriptionEs}
|
|
onChange={(e) => setFormData({ ...formData, descriptionEs: e.target.value })}
|
|
className="w-full px-4 py-3 rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1">Short Description (English)</label>
|
|
<textarea
|
|
value={formData.shortDescription}
|
|
onChange={(e) => setFormData({ ...formData, shortDescription: e.target.value.slice(0, 300) })}
|
|
className="w-full px-4 py-3 rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
|
|
rows={2}
|
|
maxLength={300}
|
|
placeholder="Brief summary for SEO and cards (max 300 chars)"
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">{formData.shortDescription.length}/300 characters</p>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1">Short Description (Spanish)</label>
|
|
<textarea
|
|
value={formData.shortDescriptionEs}
|
|
onChange={(e) => setFormData({ ...formData, shortDescriptionEs: e.target.value.slice(0, 300) })}
|
|
className="w-full px-4 py-3 rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
|
|
rows={2}
|
|
maxLength={300}
|
|
placeholder="Resumen breve para SEO y tarjetas (máx 300 caracteres)"
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">{formData.shortDescriptionEs.length}/300 characters</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<Input
|
|
label="Start Date & Time"
|
|
type="datetime-local"
|
|
value={formData.startDatetime}
|
|
onChange={(e) => setFormData({ ...formData, startDatetime: e.target.value })}
|
|
required
|
|
/>
|
|
<Input
|
|
label="End Date & Time"
|
|
type="datetime-local"
|
|
value={formData.endDatetime}
|
|
onChange={(e) => setFormData({ ...formData, endDatetime: e.target.value })}
|
|
/>
|
|
</div>
|
|
|
|
<Input
|
|
label="Location"
|
|
value={formData.location}
|
|
onChange={(e) => setFormData({ ...formData, location: e.target.value })}
|
|
required
|
|
/>
|
|
|
|
<Input
|
|
label="Location URL (Google Maps)"
|
|
type="url"
|
|
value={formData.locationUrl}
|
|
onChange={(e) => setFormData({ ...formData, locationUrl: e.target.value })}
|
|
/>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<Input
|
|
label="Price"
|
|
type="number"
|
|
min="0"
|
|
value={formData.price}
|
|
onChange={(e) => setFormData({ ...formData, price: Number(e.target.value) })}
|
|
/>
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1">Currency</label>
|
|
<select
|
|
value={formData.currency}
|
|
onChange={(e) => setFormData({ ...formData, currency: e.target.value })}
|
|
className="w-full px-4 py-3 rounded-btn border border-secondary-light-gray"
|
|
>
|
|
<option value="PYG">PYG</option>
|
|
<option value="USD">USD</option>
|
|
</select>
|
|
</div>
|
|
<Input
|
|
label="Capacity"
|
|
type="number"
|
|
min="1"
|
|
value={formData.capacity}
|
|
onChange={(e) => setFormData({ ...formData, capacity: Number(e.target.value) })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1">Status</label>
|
|
<select
|
|
value={formData.status}
|
|
onChange={(e) => setFormData({ ...formData, status: e.target.value as any })}
|
|
className="w-full px-4 py-3 rounded-btn border border-secondary-light-gray"
|
|
>
|
|
<option value="draft">Draft</option>
|
|
<option value="published">Published</option>
|
|
<option value="cancelled">Cancelled</option>
|
|
<option value="completed">Completed</option>
|
|
<option value="archived">Archived</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* External Booking Section */}
|
|
<div className="border border-secondary-light-gray rounded-lg p-4 space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">External Booking</label>
|
|
<p className="text-xs text-gray-500">Redirect users to an external booking platform</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => setFormData({ ...formData, externalBookingEnabled: !formData.externalBookingEnabled })}
|
|
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-yellow focus:ring-offset-2 ${
|
|
formData.externalBookingEnabled ? 'bg-primary-yellow' : 'bg-gray-200'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
|
formData.externalBookingEnabled ? 'translate-x-5' : 'translate-x-0'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
{formData.externalBookingEnabled && (
|
|
<div>
|
|
<Input
|
|
label="External Booking URL"
|
|
type="url"
|
|
value={formData.externalBookingUrl}
|
|
onChange={(e) => setFormData({ ...formData, externalBookingUrl: e.target.value })}
|
|
placeholder="https://example.com/book"
|
|
required
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">Must be a valid HTTPS URL</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Image Upload / Media Picker */}
|
|
<MediaPicker
|
|
value={formData.bannerUrl}
|
|
onChange={(url) => setFormData({ ...formData, bannerUrl: url })}
|
|
relatedId={editingEvent?.id}
|
|
relatedType="event"
|
|
/>
|
|
|
|
{/* Featured Event Section - Only show for published events when editing */}
|
|
{editingEvent && editingEvent.status === 'published' && (
|
|
<div className="border border-secondary-light-gray rounded-lg p-4 space-y-4 bg-amber-50">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 flex items-center gap-2">
|
|
<StarIcon className="w-5 h-5 text-amber-500" />
|
|
Featured Event
|
|
</label>
|
|
<p className="text-xs text-gray-500">
|
|
Featured events are prominently displayed on the homepage and linktree
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
disabled={settingFeatured !== null}
|
|
onClick={() => handleSetFeatured(
|
|
featuredEventId === editingEvent.id ? null : editingEvent.id
|
|
)}
|
|
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-2 disabled:opacity-50 ${
|
|
featuredEventId === editingEvent.id ? 'bg-amber-500' : 'bg-gray-200'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
|
featuredEventId === editingEvent.id ? 'translate-x-5' : 'translate-x-0'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
{featuredEventId && featuredEventId !== editingEvent.id && (
|
|
<p className="text-xs text-amber-700 bg-amber-100 p-2 rounded">
|
|
Note: Another event is currently featured. Setting this event as featured will replace it.
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex gap-3 pt-4">
|
|
<Button type="submit" isLoading={saving}>
|
|
{editingEvent ? 'Update Event' : 'Create Event'}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => { setShowForm(false); resetForm(); }}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
|
|
{/* Events Table */}
|
|
<Card className="overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead className="bg-secondary-gray">
|
|
<tr>
|
|
<th className="text-left px-6 py-3 text-sm font-medium text-gray-600">Event</th>
|
|
<th className="text-left px-6 py-3 text-sm font-medium text-gray-600">Date</th>
|
|
<th className="text-left px-6 py-3 text-sm font-medium text-gray-600">Capacity</th>
|
|
<th className="text-left px-6 py-3 text-sm font-medium text-gray-600">Status</th>
|
|
<th className="text-right px-6 py-3 text-sm font-medium text-gray-600">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-secondary-light-gray">
|
|
{events.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={5} className="px-6 py-12 text-center text-gray-500">
|
|
No events found. Create your first event!
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
events.map((event) => (
|
|
<tr key={event.id} className={clsx("hover:bg-gray-50", featuredEventId === event.id && "bg-amber-50")}>
|
|
<td className="px-6 py-4">
|
|
<div className="flex items-center gap-3">
|
|
{event.bannerUrl ? (
|
|
<img
|
|
src={event.bannerUrl}
|
|
alt={event.title}
|
|
className="w-12 h-12 rounded-lg object-cover flex-shrink-0"
|
|
/>
|
|
) : (
|
|
<div className="w-12 h-12 rounded-lg bg-secondary-gray flex items-center justify-center flex-shrink-0">
|
|
<PhotoIcon className="w-6 h-6 text-gray-400" />
|
|
</div>
|
|
)}
|
|
<div>
|
|
<div className="flex items-center gap-2">
|
|
<p className="font-medium">{event.title}</p>
|
|
{featuredEventId === event.id && (
|
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800">
|
|
<StarIconSolid className="w-3 h-3" />
|
|
Featured
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className="text-sm text-gray-500 truncate max-w-xs">{event.location}</p>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 text-sm text-gray-600">
|
|
{formatDate(event.startDatetime)}
|
|
</td>
|
|
<td className="px-6 py-4 text-sm">
|
|
{event.bookedCount || 0} / {event.capacity}
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
{getStatusBadge(event.status)}
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<div className="flex items-center justify-end gap-1">
|
|
{event.status === 'draft' && (
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => handleStatusChange(event, 'published')}
|
|
>
|
|
Publish
|
|
</Button>
|
|
)}
|
|
{event.status === 'published' && (
|
|
<button
|
|
onClick={() => handleSetFeatured(featuredEventId === event.id ? null : event.id)}
|
|
disabled={settingFeatured !== null}
|
|
className={clsx(
|
|
"p-2 rounded-btn disabled:opacity-50",
|
|
featuredEventId === event.id
|
|
? "bg-amber-100 text-amber-600 hover:bg-amber-200"
|
|
: "hover:bg-amber-100 text-gray-400 hover:text-amber-600"
|
|
)}
|
|
title={featuredEventId === event.id ? "Remove from featured" : "Set as featured"}
|
|
>
|
|
{featuredEventId === event.id ? (
|
|
<StarIconSolid className="w-4 h-4" />
|
|
) : (
|
|
<StarIcon className="w-4 h-4" />
|
|
)}
|
|
</button>
|
|
)}
|
|
<Link
|
|
href={`/admin/events/${event.id}`}
|
|
className="p-2 hover:bg-primary-yellow/20 text-primary-dark rounded-btn"
|
|
title="Manage Event"
|
|
>
|
|
<EyeIcon className="w-4 h-4" />
|
|
</Link>
|
|
<button
|
|
onClick={() => handleEdit(event)}
|
|
className="p-2 hover:bg-gray-100 rounded-btn"
|
|
title="Edit"
|
|
>
|
|
<PencilIcon className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleDuplicate(event)}
|
|
className="p-2 hover:bg-blue-100 text-blue-600 rounded-btn"
|
|
title="Duplicate"
|
|
>
|
|
<DocumentDuplicateIcon className="w-4 h-4" />
|
|
</button>
|
|
{event.status !== 'archived' && (
|
|
<button
|
|
onClick={() => handleArchive(event)}
|
|
className="p-2 hover:bg-gray-100 text-gray-600 rounded-btn"
|
|
title="Archive"
|
|
>
|
|
<ArchiveBoxIcon className="w-4 h-4" />
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={() => handleDelete(event.id)}
|
|
className="p-2 hover:bg-red-100 text-red-600 rounded-btn"
|
|
title="Delete"
|
|
>
|
|
<TrashIcon className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|