378 lines
15 KiB
TypeScript
378 lines
15 KiB
TypeScript
'use client';
|
|
import React, { useState, ChangeEvent, FormEvent } from 'react';
|
|
import IconTrashLines from '../icon/icon-trash-lines';
|
|
import axios from 'axios';
|
|
import Cookies from 'universal-cookie';
|
|
import { useRouter } from 'next/navigation';
|
|
import { showMessage } from '@/utils/CommonFunction.utils';
|
|
import { buildApiUrl } from '@/utils/BaseUrl.utils';
|
|
|
|
interface FormValues {
|
|
title: string;
|
|
slug: string;
|
|
date: string;
|
|
time: string;
|
|
location: string;
|
|
image: File | null;
|
|
link: string;
|
|
btn_text: string;
|
|
admission: string;
|
|
description: string;
|
|
}
|
|
|
|
interface FormErrors {
|
|
[key: string]: string;
|
|
}
|
|
|
|
const slugify = (text: string) => {
|
|
if (!text) return '';
|
|
return text
|
|
.toString()
|
|
.toLowerCase()
|
|
.trim()
|
|
.replace(/\s+/g, '-')
|
|
.replace(/[^\w\-]+/g, '')
|
|
.replace(/\-\-+/g, '-');
|
|
};
|
|
|
|
const CreateUpcomingEventForm: React.FC = () => {
|
|
const router = useRouter();
|
|
|
|
const [formData, setFormData] = useState<FormValues>({
|
|
title: '',
|
|
slug: '',
|
|
date: '',
|
|
time: '',
|
|
location: '',
|
|
image: null,
|
|
link: '',
|
|
btn_text: 'Details Coming Soon',
|
|
admission: '',
|
|
description: '',
|
|
});
|
|
|
|
const [errors, setErrors] = useState<FormErrors>({});
|
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
const { name, value } = e.target;
|
|
setFormData(prev => {
|
|
const nextState = { ...prev, [name]: value };
|
|
if (name === 'title') {
|
|
const autoSlug = slugify(value);
|
|
if (!prev.slug || prev.slug === slugify(prev.title)) {
|
|
nextState.slug = autoSlug;
|
|
}
|
|
if (!prev.link || prev.link === `/upcoming-event/${slugify(prev.title)}`) {
|
|
nextState.link = `/upcoming-event/${autoSlug}`;
|
|
}
|
|
}
|
|
return nextState;
|
|
});
|
|
};
|
|
|
|
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0] || null;
|
|
setFormData(prev => ({
|
|
...prev,
|
|
image: file,
|
|
}));
|
|
|
|
if (file) {
|
|
const url = URL.createObjectURL(file);
|
|
setPreviewUrl(url);
|
|
} else {
|
|
setPreviewUrl(null);
|
|
}
|
|
};
|
|
|
|
const validateForm = (): boolean => {
|
|
const newErrors: FormErrors = {};
|
|
|
|
if (!formData.title.trim()) newErrors.title = 'Event title is required';
|
|
if (!formData.date.trim()) newErrors.date = 'Event date is required';
|
|
|
|
if (formData.image && !formData.image.type.startsWith('image/')) {
|
|
newErrors.image = 'Only image files are allowed';
|
|
}
|
|
|
|
setErrors(newErrors);
|
|
return Object.keys(newErrors).length === 0;
|
|
};
|
|
|
|
const handleSubmit = async (e: FormEvent) => {
|
|
e.preventDefault();
|
|
if (!validateForm()) return;
|
|
|
|
setLoading(true);
|
|
|
|
try {
|
|
const cookies = new Cookies();
|
|
const token = cookies.get('token');
|
|
|
|
let imageUrl = '';
|
|
|
|
// Upload image if selected
|
|
if (formData.image && formData.image.type.startsWith('image/')) {
|
|
const data = new FormData();
|
|
data.append('file', formData.image);
|
|
|
|
const imageUpload = await axios.post(buildApiUrl('upload/single'), data, {
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data',
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
imageUrl = imageUpload?.data?.data?.fullUrl || imageUpload?.data?.data?.path || '';
|
|
}
|
|
|
|
const createData = {
|
|
title: formData.title,
|
|
slug: formData.slug,
|
|
date: formData.date,
|
|
time: formData.time,
|
|
location: formData.location,
|
|
image: imageUrl,
|
|
link: formData.link,
|
|
btn_text: formData.btn_text,
|
|
admission: formData.admission,
|
|
description: formData.description,
|
|
};
|
|
|
|
await axios.post(buildApiUrl('upcoming-events'), createData, {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
showMessage('Upcoming Event Created Successfully', 'success');
|
|
router.push('/upcoming-events');
|
|
} catch (error: any) {
|
|
console.error('Create error:', error);
|
|
showMessage(error?.response?.data?.message || 'Failed to create upcoming event');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleImageDelete = () => {
|
|
setFormData(prev => ({
|
|
...prev,
|
|
image: null,
|
|
}));
|
|
setPreviewUrl(null);
|
|
};
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="max-w-4xl mx-auto p-6 bg-white rounded shadow-md dark:bg-black dark:border dark:border-[#1B2E4B]">
|
|
<h2 className="text-2xl font-bold mb-6 text-gray-800 dark:text-white">Create Upcoming Event</h2>
|
|
|
|
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
|
<div className="space-y-4">
|
|
{/* Event Title */}
|
|
<div>
|
|
<label htmlFor="title" className="block font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Event Title <span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="title"
|
|
id="title"
|
|
value={formData.title}
|
|
onChange={handleChange}
|
|
placeholder="e.g. KW Multicultural Festival"
|
|
className="w-full border rounded px-3 py-2 dark:bg-gray-900 dark:border-gray-700 dark:text-white"
|
|
/>
|
|
{errors.title && <p className="text-red-500 text-sm mt-1">{errors.title}</p>}
|
|
</div>
|
|
|
|
{/* Event Date */}
|
|
<div>
|
|
<label htmlFor="date" className="block font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Event Date <span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="date"
|
|
id="date"
|
|
value={formData.date}
|
|
onChange={handleChange}
|
|
placeholder="e.g. Jun 20, 2026 and Jun 21, 2026"
|
|
className="w-full border rounded px-3 py-2 dark:bg-gray-900 dark:border-gray-700 dark:text-white"
|
|
/>
|
|
{errors.date && <p className="text-red-500 text-sm mt-1">{errors.date}</p>}
|
|
</div>
|
|
|
|
{/* Event Time */}
|
|
<div>
|
|
<label htmlFor="time" className="block font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Event Time
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="time"
|
|
id="time"
|
|
value={formData.time}
|
|
onChange={handleChange}
|
|
placeholder="e.g. 2:00 PM to 4:00 PM or Details will be announced"
|
|
className="w-full border rounded px-3 py-2 dark:bg-gray-900 dark:border-gray-700 dark:text-white"
|
|
/>
|
|
</div>
|
|
|
|
{/* Venue / Location */}
|
|
<div>
|
|
<label htmlFor="location" className="block font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Venue / Location
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="location"
|
|
id="location"
|
|
value={formData.location}
|
|
onChange={handleChange}
|
|
placeholder="e.g. Victoria Park, Kitchener"
|
|
className="w-full border rounded px-3 py-2 dark:bg-gray-900 dark:border-gray-700 dark:text-white"
|
|
/>
|
|
</div>
|
|
|
|
{/* Button Text */}
|
|
<div>
|
|
<label htmlFor="btn_text" className="block font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Button Text
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="btn_text"
|
|
id="btn_text"
|
|
value={formData.btn_text}
|
|
onChange={handleChange}
|
|
placeholder="e.g. Details Coming Soon or Learn More"
|
|
className="w-full border rounded px-3 py-2 dark:bg-gray-900 dark:border-gray-700 dark:text-white"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
{/* Slug */}
|
|
<div>
|
|
<label htmlFor="slug" className="block font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Slug (Optional)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="slug"
|
|
id="slug"
|
|
value={formData.slug}
|
|
onChange={handleChange}
|
|
placeholder="e.g. kw-multicultural-festival-2026"
|
|
className="w-full border rounded px-3 py-2 dark:bg-gray-900 dark:border-gray-700 dark:text-white"
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">Leave empty to automatically generate from event title.</p>
|
|
</div>
|
|
|
|
{/* Event Link */}
|
|
<div>
|
|
<label htmlFor="link" className="block font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Event Link (Optional)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="link"
|
|
id="link"
|
|
value={formData.link}
|
|
onChange={handleChange}
|
|
placeholder="e.g. /upcoming-event/kw-multicultural-festival-2026"
|
|
className="w-full border rounded px-3 py-2 dark:bg-gray-900 dark:border-gray-700 dark:text-white"
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">Leave empty to auto-link to the new separate details page (`/upcoming-event/<slug>`).</p>
|
|
</div>
|
|
|
|
{/* Admission */}
|
|
<div>
|
|
<label htmlFor="admission" className="block font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Admission Details (Optional)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="admission"
|
|
id="admission"
|
|
value={formData.admission}
|
|
onChange={handleChange}
|
|
placeholder="e.g. Free Entry / Ticket Required"
|
|
className="w-full border rounded px-3 py-2 dark:bg-gray-900 dark:border-gray-700 dark:text-white"
|
|
/>
|
|
</div>
|
|
|
|
{/* Event Description */}
|
|
<div>
|
|
<label htmlFor="description" className="block font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Event Description
|
|
</label>
|
|
<textarea
|
|
name="description"
|
|
id="description"
|
|
rows={3}
|
|
value={formData.description}
|
|
onChange={handleChange}
|
|
placeholder="More details will be updated soon..."
|
|
className="w-full border rounded px-3 py-2 dark:bg-gray-900 dark:border-gray-700 dark:text-white"
|
|
/>
|
|
</div>
|
|
|
|
{/* Image Upload */}
|
|
<div>
|
|
<label htmlFor="image" className="block font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Event Image
|
|
</label>
|
|
<input
|
|
type="file"
|
|
name="image"
|
|
id="image"
|
|
accept="image/*"
|
|
onChange={handleFileChange}
|
|
className="w-full border rounded px-3 py-2 dark:bg-gray-900 dark:border-gray-700 dark:text-white"
|
|
/>
|
|
{errors.image && <p className="text-red-500 text-sm mt-1">{errors.image}</p>}
|
|
|
|
{/* Preview */}
|
|
{previewUrl && (
|
|
<div className="mt-3 relative w-40 h-40 border rounded overflow-hidden">
|
|
<img src={previewUrl} alt="Preview" className="w-full h-full object-cover" />
|
|
<button
|
|
type="button"
|
|
onClick={handleImageDelete}
|
|
className="absolute top-1 right-1 bg-red-600 text-white p-1 rounded-full text-xs"
|
|
>
|
|
<IconTrashLines />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="mt-8 flex justify-end gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={() => router.push('/upcoming-events')}
|
|
className="px-5 py-2 border rounded font-semibold text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="px-6 py-2 bg-primary text-white rounded font-semibold hover:bg-primary/90 disabled:opacity-50"
|
|
>
|
|
{loading ? 'Creating...' : 'Create Event'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
);
|
|
};
|
|
|
|
export default CreateUpcomingEventForm;
|