510 lines
19 KiB
TypeScript
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>
|
|
);
|
|
}
|