ocd-website/frontend/src/app/admin/events/create/page.tsx

510 lines
19 KiB
TypeScript

'use client';
import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useEvents } 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;
}
interface EventFormErrors {
title?: string;
description?: string;
date?: string;
time?: string;
location?: string;
category?: string;
status?: string;
maxAttendees?: string;
recurrencePattern?: string;
}
export default function CreateEventPage() {
const router = useRouter();
const { createEvent } = useEvents();
const [formData, setFormData] = useState<EventFormData>({
title: '',
description: '',
date: '',
time: '',
location: '',
category: 'social',
status: 'draft',
registrationRequired: false,
maxAttendees: undefined,
recurring: false,
recurrencePattern: '',
tags: '',
});
const [errors, setErrors] = useState<EventFormErrors>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitError, setSubmitError] = useState('');
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';
} else {
const selectedDate = new Date(formData.date);
const today = new Date();
today.setHours(0, 0, 0, 0);
if (selectedDate < today) {
newErrors.date = 'Date cannot be in the past';
}
}
// 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(),
registeredCount: 0, // Initialize with zero registrations
};
const result = await createEvent(eventData);
if (result) {
// Redirect to events list page on success
router.push('/admin/events');
} else {
setSubmitError('Failed to create event. Please try again.');
}
} catch (error) {
console.error('Error creating event:', error);
setSubmitError('An unexpected error occurred. Please try again later.');
} finally {
setIsSubmitting(false);
}
};
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">Create New 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="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Title <span className="text-red-600">*</span>
</label>
<input
type="text"
id="title"
name="title"
value={formData.title}
onChange={handleChange}
className={`block w-full px-3 py-2 border ${errors.title ? 'border-red-500 dark:border-red-400' : 'border-gray-300 dark:border-gray-700'} dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm`}
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="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Description <span className="text-red-600">*</span>
</label>
<textarea
id="description"
name="description"
value={formData.description}
onChange={handleChange}
rows={4}
className={`block w-full px-3 py-2 border ${errors.description ? 'border-red-500 dark:border-red-400' : 'border-gray-300 dark:border-gray-700'} dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm`}
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="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Date <span className="text-red-600">*</span>
</label>
<input
type="date"
id="date"
name="date"
value={formData.date}
onChange={handleChange}
className={`block w-full px-3 py-2 border ${errors.date ? 'border-red-500 dark:border-red-400' : 'border-gray-300 dark:border-gray-700'} dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm`}
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="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Time <span className="text-red-600">*</span>
</label>
<input
type="text"
id="time"
name="time"
value={formData.time}
onChange={handleChange}
className={`block w-full px-3 py-2 border ${errors.time ? 'border-red-500 dark:border-red-400' : 'border-gray-300 dark:border-gray-700'} dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm`}
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="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Location <span className="text-red-600">*</span>
</label>
<input
type="text"
id="location"
name="location"
value={formData.location}
onChange={handleChange}
className={`block w-full px-3 py-2 border ${errors.location ? 'border-red-500 dark:border-red-400' : 'border-gray-300 dark:border-gray-700'} dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm`}
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="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Category <span className="text-red-600">*</span>
</label>
<select
id="category"
name="category"
value={formData.category}
onChange={handleChange}
className={`block w-full px-3 py-2 border ${errors.category ? 'border-red-500 dark:border-red-400' : 'border-gray-300 dark:border-gray-700'} dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm`}
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="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Status <span className="text-red-600">*</span>
</label>
<select
id="status"
name="status"
value={formData.status}
onChange={handleChange}
className={`block w-full px-3 py-2 border ${errors.status ? 'border-red-500 dark:border-red-400' : 'border-gray-300 dark:border-gray-700'} dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm`}
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="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Maximum Attendees
</label>
<input
type="number"
id="maxAttendees"
name="maxAttendees"
value={formData.maxAttendees || ''}
onChange={handleChange}
min="1"
className={`block w-full px-3 py-2 border ${errors.maxAttendees ? 'border-red-500 dark:border-red-400' : 'border-gray-300 dark:border-gray-700'} dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm`}
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>
)}
</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="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Recurrence Pattern <span className="text-red-600">*</span>
</label>
<input
type="text"
id="recurrencePattern"
name="recurrencePattern"
value={formData.recurrencePattern || ''}
onChange={handleChange}
className={`block w-full px-3 py-2 border ${errors.recurrencePattern ? 'border-red-500 dark:border-red-400' : 'border-gray-300 dark:border-gray-700'} dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm`}
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="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tags (comma separated)
</label>
<input
type="text"
id="tags"
name="tags"
value={formData.tags}
onChange={handleChange}
className="block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 dark:bg-gray-800 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm"
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 ? 'Creating...' : 'Create Event'}
</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>
);
}