Create central dashboard with statistics and quick actions Build events management with filtering and CRUD UI Implement members management with bulk actions Develop video management with accessibility indicators Add document repository with visibility controls Create content management with WYSIWYG editor Add settings interface for site configuration Additional improvements: Implement full Events Management System with CRUD Enhance frontend architecture with TypeScript Improve accessibility in navigation menu Add comprehensive test suite for Events API
602 lines
20 KiB
TypeScript
602 lines
20 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { useRouter, useParams } from 'next/navigation';
|
|
import { useEvents, Event } from '../../../../../hooks/useEvents';
|
|
import Link from 'next/link';
|
|
|
|
interface EventFormData {
|
|
title: string;
|
|
description: string;
|
|
date: string;
|
|
time: string;
|
|
location: string;
|
|
category: 'social' | 'athletic' | 'board' | 'general' | 'educational';
|
|
status: 'published' | 'draft' | 'archived';
|
|
registrationRequired: boolean;
|
|
maxAttendees?: number;
|
|
recurring: boolean;
|
|
recurrencePattern?: string;
|
|
tags: string;
|
|
registeredCount: number;
|
|
}
|
|
|
|
interface EventFormErrors {
|
|
title?: string;
|
|
description?: string;
|
|
date?: string;
|
|
time?: string;
|
|
location?: string;
|
|
category?: string;
|
|
status?: string;
|
|
maxAttendees?: string;
|
|
recurrencePattern?: string;
|
|
}
|
|
|
|
export default function EditEventPage() {
|
|
const router = useRouter();
|
|
const params = useParams();
|
|
const eventId = params.id as string;
|
|
|
|
const { getEventById, updateEvent } = useEvents();
|
|
|
|
const [formData, setFormData] = useState<EventFormData>({
|
|
title: '',
|
|
description: '',
|
|
date: '',
|
|
time: '',
|
|
location: '',
|
|
category: 'social',
|
|
status: 'draft',
|
|
registrationRequired: false,
|
|
maxAttendees: undefined,
|
|
recurring: false,
|
|
recurrencePattern: '',
|
|
tags: '',
|
|
registeredCount: 0,
|
|
});
|
|
|
|
const [errors, setErrors] = useState<EventFormErrors>({});
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [loadError, setLoadError] = useState('');
|
|
const [submitError, setSubmitError] = useState('');
|
|
|
|
// Load event data
|
|
useEffect(() => {
|
|
const loadEvent = async () => {
|
|
setIsLoading(true);
|
|
setLoadError('');
|
|
|
|
try {
|
|
const event = await getEventById(eventId);
|
|
|
|
if (event) {
|
|
// Format date for input field (YYYY-MM-DD)
|
|
const dateObj = new Date(event.date);
|
|
const formattedDate = dateObj.toISOString().split('T')[0];
|
|
|
|
// Convert tags array to comma-separated string
|
|
const tagsString = event.tags?.join(', ') || '';
|
|
|
|
setFormData({
|
|
title: event.title,
|
|
description: event.description,
|
|
date: formattedDate,
|
|
time: event.time,
|
|
location: event.location,
|
|
category: event.category,
|
|
status: event.status,
|
|
registrationRequired: event.registrationRequired,
|
|
maxAttendees: event.maxAttendees,
|
|
recurring: event.recurring || false,
|
|
recurrencePattern: event.recurrencePattern || '',
|
|
tags: tagsString,
|
|
registeredCount: event.registeredCount,
|
|
});
|
|
} else {
|
|
setLoadError('Event not found');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading event:', error);
|
|
setLoadError('Failed to load event data. Please try again.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
loadEvent();
|
|
}, [eventId, getEventById]);
|
|
|
|
const validateForm = (): boolean => {
|
|
const newErrors: EventFormErrors = {};
|
|
|
|
// Title validation
|
|
if (!formData.title.trim()) {
|
|
newErrors.title = 'Title is required';
|
|
} else if (formData.title.length < 3) {
|
|
newErrors.title = 'Title must be at least 3 characters';
|
|
}
|
|
|
|
// Description validation
|
|
if (!formData.description.trim()) {
|
|
newErrors.description = 'Description is required';
|
|
}
|
|
|
|
// Date validation
|
|
if (!formData.date) {
|
|
newErrors.date = 'Date is required';
|
|
}
|
|
|
|
// Time validation
|
|
if (!formData.time.trim()) {
|
|
newErrors.time = 'Time is required';
|
|
}
|
|
|
|
// Location validation
|
|
if (!formData.location.trim()) {
|
|
newErrors.location = 'Location is required';
|
|
}
|
|
|
|
// Category validation
|
|
if (!formData.category) {
|
|
newErrors.category = 'Category is required';
|
|
}
|
|
|
|
// Status validation
|
|
if (!formData.status) {
|
|
newErrors.status = 'Status is required';
|
|
}
|
|
|
|
// Max attendees validation (if registration is required)
|
|
if (formData.registrationRequired && formData.maxAttendees !== undefined) {
|
|
if (formData.maxAttendees <= 0) {
|
|
newErrors.maxAttendees = 'Maximum attendees must be greater than 0';
|
|
}
|
|
}
|
|
|
|
// Recurrence pattern validation (if event is recurring)
|
|
if (formData.recurring && !formData.recurrencePattern?.trim()) {
|
|
newErrors.recurrencePattern = 'Recurrence pattern is required for recurring events';
|
|
}
|
|
|
|
setErrors(newErrors);
|
|
return Object.keys(newErrors).length === 0;
|
|
};
|
|
|
|
const handleChange = (
|
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
|
|
) => {
|
|
const { name, value, type } = e.target as HTMLInputElement;
|
|
|
|
if (type === 'checkbox') {
|
|
const checked = (e.target as HTMLInputElement).checked;
|
|
setFormData({
|
|
...formData,
|
|
[name]: checked,
|
|
});
|
|
} else if (name === 'maxAttendees') {
|
|
setFormData({
|
|
...formData,
|
|
[name]: value ? parseInt(value, 10) : undefined,
|
|
});
|
|
} else {
|
|
setFormData({
|
|
...formData,
|
|
[name]: value,
|
|
});
|
|
}
|
|
|
|
// Clear error when user types
|
|
if (errors[name as keyof EventFormErrors]) {
|
|
setErrors({
|
|
...errors,
|
|
[name]: undefined,
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (!validateForm()) {
|
|
return;
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
setSubmitError('');
|
|
|
|
try {
|
|
// Convert form data to the format expected by the API
|
|
const eventData = {
|
|
...formData,
|
|
tags: formData.tags.split(',').map(tag => tag.trim()).filter(tag => tag),
|
|
date: new Date(formData.date).toISOString(),
|
|
};
|
|
|
|
const result = await updateEvent(eventId, eventData);
|
|
|
|
if (result) {
|
|
// Redirect to events list page on success
|
|
router.push('/admin/events');
|
|
} else {
|
|
setSubmitError('Failed to update event. Please try again.');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating event:', error);
|
|
setSubmitError('An unexpected error occurred. Please try again later.');
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="container mx-auto px-4 py-8">
|
|
<div className="flex justify-center items-center h-64">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
|
|
<span className="ml-3 text-lg">Loading event data...</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (loadError) {
|
|
return (
|
|
<div className="container mx-auto px-4 py-8">
|
|
<div className="bg-red-100 dark:bg-red-900 border border-red-400 dark:border-red-600 text-red-700 dark:text-red-200 px-4 py-3 rounded mb-6" role="alert">
|
|
<p className="font-medium">Error</p>
|
|
<p>{loadError}</p>
|
|
<div className="mt-4">
|
|
<Link href="/admin/events" className="btn-primary">
|
|
Return to Events
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-8">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h1 className="text-2xl font-bold">Edit Event</h1>
|
|
<Link
|
|
href="/admin/events"
|
|
className="btn-secondary"
|
|
>
|
|
Cancel
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="card p-6">
|
|
{/* Error message */}
|
|
{submitError && (
|
|
<div className="bg-red-100 dark:bg-red-900 border border-red-400 dark:border-red-600 text-red-700 dark:text-red-200 px-4 py-3 rounded mb-6" role="alert">
|
|
<p className="font-medium">Error</p>
|
|
<p>{submitError}</p>
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
{/* Title field */}
|
|
<div>
|
|
<label htmlFor="title" className="form-label">
|
|
Title <span className="text-red-600">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="title"
|
|
name="title"
|
|
value={formData.title}
|
|
onChange={handleChange}
|
|
className={`form-input w-full ${errors.title ? 'border-red-500 dark:border-red-400' : ''}`}
|
|
placeholder="Event title"
|
|
aria-required="true"
|
|
aria-invalid={!!errors.title}
|
|
aria-describedby={errors.title ? 'title-error' : undefined}
|
|
/>
|
|
{errors.title && (
|
|
<p className="mt-1 text-red-600 dark:text-red-400 text-sm" id="title-error">
|
|
{errors.title}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Description field */}
|
|
<div>
|
|
<label htmlFor="description" className="form-label">
|
|
Description <span className="text-red-600">*</span>
|
|
</label>
|
|
<textarea
|
|
id="description"
|
|
name="description"
|
|
value={formData.description}
|
|
onChange={handleChange}
|
|
rows={4}
|
|
className={`form-input w-full ${errors.description ? 'border-red-500 dark:border-red-400' : ''}`}
|
|
placeholder="Event description"
|
|
aria-required="true"
|
|
aria-invalid={!!errors.description}
|
|
aria-describedby={errors.description ? 'description-error' : undefined}
|
|
></textarea>
|
|
{errors.description && (
|
|
<p className="mt-1 text-red-600 dark:text-red-400 text-sm" id="description-error">
|
|
{errors.description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Date and Time fields */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div>
|
|
<label htmlFor="date" className="form-label">
|
|
Date <span className="text-red-600">*</span>
|
|
</label>
|
|
<input
|
|
type="date"
|
|
id="date"
|
|
name="date"
|
|
value={formData.date}
|
|
onChange={handleChange}
|
|
className={`form-input w-full ${errors.date ? 'border-red-500 dark:border-red-400' : ''}`}
|
|
aria-required="true"
|
|
aria-invalid={!!errors.date}
|
|
aria-describedby={errors.date ? 'date-error' : undefined}
|
|
/>
|
|
{errors.date && (
|
|
<p className="mt-1 text-red-600 dark:text-red-400 text-sm" id="date-error">
|
|
{errors.date}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="time" className="form-label">
|
|
Time <span className="text-red-600">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="time"
|
|
name="time"
|
|
value={formData.time}
|
|
onChange={handleChange}
|
|
className={`form-input w-full ${errors.time ? 'border-red-500 dark:border-red-400' : ''}`}
|
|
placeholder="e.g., 6:00 PM - 9:00 PM"
|
|
aria-required="true"
|
|
aria-invalid={!!errors.time}
|
|
aria-describedby={errors.time ? 'time-error' : undefined}
|
|
/>
|
|
{errors.time && (
|
|
<p className="mt-1 text-red-600 dark:text-red-400 text-sm" id="time-error">
|
|
{errors.time}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Location field */}
|
|
<div>
|
|
<label htmlFor="location" className="form-label">
|
|
Location <span className="text-red-600">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="location"
|
|
name="location"
|
|
value={formData.location}
|
|
onChange={handleChange}
|
|
className={`form-input w-full ${errors.location ? 'border-red-500 dark:border-red-400' : ''}`}
|
|
placeholder="Event location"
|
|
aria-required="true"
|
|
aria-invalid={!!errors.location}
|
|
aria-describedby={errors.location ? 'location-error' : undefined}
|
|
/>
|
|
{errors.location && (
|
|
<p className="mt-1 text-red-600 dark:text-red-400 text-sm" id="location-error">
|
|
{errors.location}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Category and Status fields */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div>
|
|
<label htmlFor="category" className="form-label">
|
|
Category <span className="text-red-600">*</span>
|
|
</label>
|
|
<select
|
|
id="category"
|
|
name="category"
|
|
value={formData.category}
|
|
onChange={handleChange}
|
|
className={`form-input w-full ${errors.category ? 'border-red-500 dark:border-red-400' : ''}`}
|
|
aria-required="true"
|
|
aria-invalid={!!errors.category}
|
|
aria-describedby={errors.category ? 'category-error' : undefined}
|
|
>
|
|
<option value="social">Social</option>
|
|
<option value="athletic">Athletic</option>
|
|
<option value="board">Board</option>
|
|
<option value="general">General</option>
|
|
<option value="educational">Educational</option>
|
|
</select>
|
|
{errors.category && (
|
|
<p className="mt-1 text-red-600 dark:text-red-400 text-sm" id="category-error">
|
|
{errors.category}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="status" className="form-label">
|
|
Status <span className="text-red-600">*</span>
|
|
</label>
|
|
<select
|
|
id="status"
|
|
name="status"
|
|
value={formData.status}
|
|
onChange={handleChange}
|
|
className={`form-input w-full ${errors.status ? 'border-red-500 dark:border-red-400' : ''}`}
|
|
aria-required="true"
|
|
aria-invalid={!!errors.status}
|
|
aria-describedby={errors.status ? 'status-error' : undefined}
|
|
>
|
|
<option value="draft">Draft</option>
|
|
<option value="published">Published</option>
|
|
<option value="archived">Archived</option>
|
|
</select>
|
|
{errors.status && (
|
|
<p className="mt-1 text-red-600 dark:text-red-400 text-sm" id="status-error">
|
|
{errors.status}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Registration fields */}
|
|
<div className="space-y-4">
|
|
<div className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
id="registrationRequired"
|
|
name="registrationRequired"
|
|
checked={formData.registrationRequired}
|
|
onChange={handleChange}
|
|
className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
|
|
/>
|
|
<label htmlFor="registrationRequired" className="ml-2 block text-sm text-gray-900 dark:text-gray-100">
|
|
Registration required
|
|
</label>
|
|
</div>
|
|
|
|
{formData.registrationRequired && (
|
|
<div>
|
|
<label htmlFor="maxAttendees" className="form-label">
|
|
Maximum Attendees
|
|
</label>
|
|
<input
|
|
type="number"
|
|
id="maxAttendees"
|
|
name="maxAttendees"
|
|
value={formData.maxAttendees || ''}
|
|
onChange={handleChange}
|
|
min="1"
|
|
className={`form-input w-full ${errors.maxAttendees ? 'border-red-500 dark:border-red-400' : ''}`}
|
|
placeholder="Leave blank for unlimited"
|
|
aria-invalid={!!errors.maxAttendees}
|
|
aria-describedby={errors.maxAttendees ? 'maxAttendees-error' : undefined}
|
|
/>
|
|
{errors.maxAttendees && (
|
|
<p className="mt-1 text-red-600 dark:text-red-400 text-sm" id="maxAttendees-error">
|
|
{errors.maxAttendees}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{formData.registrationRequired && (
|
|
<div>
|
|
<label htmlFor="registeredCount" className="form-label">
|
|
Current Registrations
|
|
</label>
|
|
<input
|
|
type="number"
|
|
id="registeredCount"
|
|
name="registeredCount"
|
|
value={formData.registeredCount}
|
|
onChange={handleChange}
|
|
min="0"
|
|
className="form-input w-full"
|
|
aria-describedby="registeredCount-help"
|
|
/>
|
|
<p id="registeredCount-help" className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
Number of people currently registered for this event.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Recurring event fields */}
|
|
<div className="space-y-4">
|
|
<div className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
id="recurring"
|
|
name="recurring"
|
|
checked={formData.recurring}
|
|
onChange={handleChange}
|
|
className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
|
|
/>
|
|
<label htmlFor="recurring" className="ml-2 block text-sm text-gray-900 dark:text-gray-100">
|
|
Recurring event
|
|
</label>
|
|
</div>
|
|
|
|
{formData.recurring && (
|
|
<div>
|
|
<label htmlFor="recurrencePattern" className="form-label">
|
|
Recurrence Pattern <span className="text-red-600">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="recurrencePattern"
|
|
name="recurrencePattern"
|
|
value={formData.recurrencePattern || ''}
|
|
onChange={handleChange}
|
|
className={`form-input w-full ${errors.recurrencePattern ? 'border-red-500 dark:border-red-400' : ''}`}
|
|
placeholder="e.g., Weekly on Tuesdays, Monthly on first Monday"
|
|
aria-required={formData.recurring}
|
|
aria-invalid={!!errors.recurrencePattern}
|
|
aria-describedby={errors.recurrencePattern ? 'recurrencePattern-error' : undefined}
|
|
/>
|
|
{errors.recurrencePattern && (
|
|
<p className="mt-1 text-red-600 dark:text-red-400 text-sm" id="recurrencePattern-error">
|
|
{errors.recurrencePattern}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Tags field */}
|
|
<div>
|
|
<label htmlFor="tags" className="form-label">
|
|
Tags (comma separated)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="tags"
|
|
name="tags"
|
|
value={formData.tags}
|
|
onChange={handleChange}
|
|
className="form-input w-full"
|
|
placeholder="e.g., social, family-friendly, outdoor"
|
|
/>
|
|
</div>
|
|
|
|
{/* Submit button */}
|
|
<div className="flex justify-end space-x-3 pt-4">
|
|
<Link
|
|
href="/admin/events"
|
|
className="btn-secondary"
|
|
>
|
|
Cancel
|
|
</Link>
|
|
<button
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
className="btn-primary"
|
|
>
|
|
{isSubmitting ? 'Saving...' : 'Save Changes'}
|
|
</button>
|
|
</div>
|
|
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
<span className="text-red-600">*</span> Required fields
|
|
</p>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|