ocd-website/frontend/src/app/admin/events/[id]/edit/page.tsx
TheMaddax 8e986796e0 feat(admin): Implement comprehensive admin dashboard system
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
2025-03-26 10:13:53 -05:00

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>
);
}