upcoming events dynamic
This commit is contained in:
parent
69f054f785
commit
7825858e00
17
app/(defaults)/create-upcoming-event/page.tsx
Normal file
17
app/(defaults)/create-upcoming-event/page.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import CreateUpcomingEventForm from '@/components/gallery/CreateUpcomingEventForm';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Create Upcoming Event',
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreateUpcomingEventPage = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CreateUpcomingEventForm />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateUpcomingEventPage;
|
||||||
17
app/(defaults)/edit-upcoming-event/page.tsx
Normal file
17
app/(defaults)/edit-upcoming-event/page.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import EditUpcomingEventForm from '@/components/gallery/EditUpcomingEventForm';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
import React, { Suspense } from 'react';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Edit Upcoming Event',
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditUpcomingEventPage = () => {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<div className="p-10 text-center">Loading...</div>}>
|
||||||
|
<EditUpcomingEventForm />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditUpcomingEventPage;
|
||||||
17
app/(defaults)/upcoming-events/page.tsx
Normal file
17
app/(defaults)/upcoming-events/page.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import ListOfUpcomingEvents from '@/components/gallery/ListOfUpcomingEvents';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Upcoming Events',
|
||||||
|
};
|
||||||
|
|
||||||
|
const UpcomingEventsPage = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ListOfUpcomingEvents />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UpcomingEventsPage;
|
||||||
377
components/gallery/CreateUpcomingEventForm.tsx
Normal file
377
components/gallery/CreateUpcomingEventForm.tsx
Normal file
@ -0,0 +1,377 @@
|
|||||||
|
'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;
|
||||||
426
components/gallery/EditUpcomingEventForm.tsx
Normal file
426
components/gallery/EditUpcomingEventForm.tsx
Normal file
@ -0,0 +1,426 @@
|
|||||||
|
'use client';
|
||||||
|
import React, { useState, useEffect, ChangeEvent, FormEvent } from 'react';
|
||||||
|
import IconTrashLines from '../icon/icon-trash-lines';
|
||||||
|
import axios from 'axios';
|
||||||
|
import Cookies from 'universal-cookie';
|
||||||
|
import { useRouter, useSearchParams } 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;
|
||||||
|
existingImageUrl: string;
|
||||||
|
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 EditUpcomingEventForm: React.FC = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const id = searchParams.get('id');
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState<FormValues>({
|
||||||
|
title: '',
|
||||||
|
slug: '',
|
||||||
|
date: '',
|
||||||
|
time: '',
|
||||||
|
location: '',
|
||||||
|
image: null,
|
||||||
|
existingImageUrl: '',
|
||||||
|
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 [fetching, setFetching] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
fetchEventDetails(id);
|
||||||
|
} else {
|
||||||
|
showMessage('Invalid Event ID');
|
||||||
|
router.push('/upcoming-events');
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const fetchEventDetails = async (eventId: string) => {
|
||||||
|
try {
|
||||||
|
const cookies = new Cookies();
|
||||||
|
const token = cookies.get('token');
|
||||||
|
const res = await axios.get(buildApiUrl(`upcoming-events/${eventId}`), {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res?.data?.success && res?.data?.data) {
|
||||||
|
const ev = res.data.data;
|
||||||
|
const titleStr = ev.title || ev.eventtitle || '';
|
||||||
|
const computedSlug = (ev.slug && ev.slug.trim()) ? ev.slug.trim() : slugify(titleStr);
|
||||||
|
const computedLink = (ev.link && ev.link.trim()) ? ev.link.trim() : `/upcoming-event/${computedSlug}`;
|
||||||
|
|
||||||
|
setFormData({
|
||||||
|
title: titleStr,
|
||||||
|
slug: computedSlug,
|
||||||
|
date: ev.date || ev.eventdate || '',
|
||||||
|
time: ev.time || '',
|
||||||
|
location: ev.location || '',
|
||||||
|
image: null,
|
||||||
|
existingImageUrl: ev.image || ev.eventimageurl || '',
|
||||||
|
link: computedLink,
|
||||||
|
btn_text: ev.btn_text || ev.btnText || 'Details Coming Soon',
|
||||||
|
admission: ev.admission || '',
|
||||||
|
description: ev.description || ev.desc || ev.eventdescription || '',
|
||||||
|
});
|
||||||
|
setPreviewUrl(ev.image || ev.eventimageurl || null);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching event details:', error);
|
||||||
|
showMessage('Failed to load event details');
|
||||||
|
} finally {
|
||||||
|
setFetching(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(formData.existingImageUrl || 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() || !id) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cookies = new Cookies();
|
||||||
|
const token = cookies.get('token');
|
||||||
|
|
||||||
|
let imageUrl = formData.existingImageUrl;
|
||||||
|
|
||||||
|
// Upload new 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 || imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData = {
|
||||||
|
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.put(buildApiUrl(`upcoming-events/${id}`), updateData, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
showMessage('Upcoming Event Updated Successfully', 'success');
|
||||||
|
router.push('/upcoming-events');
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Update error:', error);
|
||||||
|
showMessage(error?.response?.data?.message || 'Failed to update upcoming event');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImageDelete = () => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
image: null,
|
||||||
|
existingImageUrl: '',
|
||||||
|
}));
|
||||||
|
setPreviewUrl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (fetching) {
|
||||||
|
return (
|
||||||
|
<div className="p-10 text-center text-gray-500">
|
||||||
|
Loading event details...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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">Edit 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}
|
||||||
|
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}
|
||||||
|
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}
|
||||||
|
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}
|
||||||
|
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}
|
||||||
|
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
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="slug"
|
||||||
|
id="slug"
|
||||||
|
value={formData.slug}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full border rounded px-3 py-2 dark:bg-gray-900 dark:border-gray-700 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Event Link */}
|
||||||
|
<div>
|
||||||
|
<label htmlFor="link" className="block font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Event Link
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="link"
|
||||||
|
id="link"
|
||||||
|
value={formData.link}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full border rounded px-3 py-2 dark:bg-gray-900 dark:border-gray-700 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Admission */}
|
||||||
|
<div>
|
||||||
|
<label htmlFor="admission" className="block font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Admission Details
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="admission"
|
||||||
|
id="admission"
|
||||||
|
value={formData.admission}
|
||||||
|
onChange={handleChange}
|
||||||
|
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}
|
||||||
|
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 ? 'Updating...' : 'Update Event'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditUpcomingEventForm;
|
||||||
262
components/gallery/ListOfUpcomingEvents.tsx
Normal file
262
components/gallery/ListOfUpcomingEvents.tsx
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
'use client';
|
||||||
|
import axios from 'axios';
|
||||||
|
import Cookies from 'universal-cookie';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import IconTrashLines from '../icon/icon-trash-lines';
|
||||||
|
import IconPencil from '../icon/icon-pencil';
|
||||||
|
import IconEye from '../icon/icon-eye';
|
||||||
|
import Swal from 'sweetalert2';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { buildApiUrl } from '@/utils/BaseUrl.utils';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Upcoming Events',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ListOfUpcomingEvents = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const [events, setEvents] = useState<any[]>([]);
|
||||||
|
const [selectedEvent, setSelectedEvent] = useState<any | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getEvents();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getEvents = async () => {
|
||||||
|
try {
|
||||||
|
const cookies = new Cookies();
|
||||||
|
const token = cookies.get('token');
|
||||||
|
const eventRes: any = await axios.get(buildApiUrl('upcoming-events'), {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
console.log('Upcoming Events Res:', eventRes);
|
||||||
|
if (eventRes?.data?.success) {
|
||||||
|
setEvents(eventRes?.data?.data || []);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching upcoming events:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (event: any) => {
|
||||||
|
router.push(`/edit-upcoming-event?id=${event.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleView = (event: any) => {
|
||||||
|
setSelectedEvent(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showAlert = async (event: any) => {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Are you sure?',
|
||||||
|
text: `You are about to delete "${event.title || event.eventtitle}". You won't be able to revert this!`,
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'Delete',
|
||||||
|
padding: '2em',
|
||||||
|
customClass: { popup: 'sweet-alerts' },
|
||||||
|
}).then(async (result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
try {
|
||||||
|
const cookies = new Cookies();
|
||||||
|
const token = cookies.get('token');
|
||||||
|
await axios.delete(buildApiUrl(`upcoming-events/${event.id}`), {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Deleted!',
|
||||||
|
text: 'Upcoming event has been deleted.',
|
||||||
|
icon: 'success',
|
||||||
|
customClass: { popup: 'sweet-alerts' },
|
||||||
|
});
|
||||||
|
getEvents();
|
||||||
|
} catch (error) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Error!',
|
||||||
|
text: 'Failed to delete the upcoming event.',
|
||||||
|
icon: 'error',
|
||||||
|
customClass: { popup: 'sweet-alerts' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mt-10 container">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h3 className="text-xl font-bold md:text-3xl">Upcoming Events</h3>
|
||||||
|
<Link
|
||||||
|
href="/create-upcoming-event"
|
||||||
|
className="bg-primary text-white px-4 py-2 rounded-md font-semibold hover:bg-primary/90 transition"
|
||||||
|
>
|
||||||
|
+ Add New Event
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
|
{/* First box: Create New Upcoming Event */}
|
||||||
|
<Link
|
||||||
|
href="/create-upcoming-event"
|
||||||
|
className="flex flex-col items-center justify-center min-h-[300px] rounded-md border border-dashed border-blue-500 bg-blue-50 p-5 text-center shadow hover:bg-blue-100 transition dark:border-blue-800 dark:bg-blue-900/20 dark:hover:bg-blue-900/40"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="mb-3 text-5xl text-blue-600 dark:text-white">+</div>
|
||||||
|
<h5 className="text-lg font-semibold text-blue-800 dark:text-white">Create New Upcoming Event</h5>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* List dynamic event cards */}
|
||||||
|
{events.map((event: any, index: number) => {
|
||||||
|
const title = event.title || event.eventtitle || 'Untitled Event';
|
||||||
|
const date = event.date || event.eventdate || 'Date TBD';
|
||||||
|
const time = event.time || '';
|
||||||
|
const location = event.location || '';
|
||||||
|
const image = event.image || event.eventimageurl || '/assets/images/placeholder.jpg';
|
||||||
|
const description = event.description || event.desc || event.eventdescription || '';
|
||||||
|
const btnText = event.btn_text || event.btnText || 'Details Coming Soon';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={event.id || index}
|
||||||
|
className="relative flex flex-col justify-between rounded-md border border-white-light bg-white p-5 shadow-[0px_0px_2px_0px_rgba(145,158,171,0.20),0px_12px_24px_-4px_rgba(145,158,171,0.12)] dark:border-[#1B2E4B] dark:bg-black"
|
||||||
|
>
|
||||||
|
{/* Top-right action buttons */}
|
||||||
|
<div className="absolute top-5 right-5 z-10 flex gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleView(event)}
|
||||||
|
className="bg-emerald-600 text-white p-1.5 rounded hover:bg-emerald-700 transition"
|
||||||
|
title="View Details"
|
||||||
|
>
|
||||||
|
<IconEye />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleEdit(event)}
|
||||||
|
className="bg-blue-600 text-white p-1.5 rounded hover:bg-blue-700 transition"
|
||||||
|
title="Edit Event"
|
||||||
|
>
|
||||||
|
<IconPencil />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => showAlert(event)}
|
||||||
|
className="bg-red-600 text-white p-1.5 rounded hover:bg-red-700 transition"
|
||||||
|
title="Delete Event"
|
||||||
|
>
|
||||||
|
<IconTrashLines />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{/* Image */}
|
||||||
|
<div className="max-h-48 overflow-hidden rounded-md mt-0 bg-gray-100 dark:bg-gray-800 flex items-center justify-center">
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
alt={title}
|
||||||
|
className="w-full h-48 object-cover"
|
||||||
|
onError={(e: any) => {
|
||||||
|
e.target.src = 'https://via.placeholder.com/400x200?text=Upcoming+Event';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Event content */}
|
||||||
|
<div className="flex-1 mt-4">
|
||||||
|
<h4 className="text-xl mb-1.5 font-bold text-gray-800 dark:text-white line-clamp-1">
|
||||||
|
{title}
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm font-semibold text-blue-600 dark:text-blue-400 mb-1">
|
||||||
|
📅 {date} {time ? `| 🕒 ${time}` : ''}
|
||||||
|
</p>
|
||||||
|
{location && (
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||||
|
📍 {location}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 line-clamp-2 mt-2">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer badge */}
|
||||||
|
<div className="mt-4 pt-3 border-t border-gray-100 dark:border-gray-800 flex justify-between items-center">
|
||||||
|
<span className="text-xs font-semibold px-2.5 py-1 rounded bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300">
|
||||||
|
{btnText}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* View Modal */}
|
||||||
|
{selectedEvent && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4">
|
||||||
|
<div className="relative w-full max-w-2xl rounded-lg bg-white p-6 shadow-xl dark:bg-gray-900 max-h-[90vh] overflow-y-auto">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedEvent(null)}
|
||||||
|
className="absolute top-4 right-4 text-gray-500 hover:text-black dark:hover:text-white text-2xl font-bold"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
<h3 className="text-2xl font-bold mb-4 pr-8 text-gray-900 dark:text-white">
|
||||||
|
{selectedEvent.title || selectedEvent.eventtitle}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{(selectedEvent.image || selectedEvent.eventimageurl) && (
|
||||||
|
<img
|
||||||
|
src={selectedEvent.image || selectedEvent.eventimageurl}
|
||||||
|
alt={selectedEvent.title}
|
||||||
|
className="w-full max-h-64 object-cover rounded-md mb-4"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-3 text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<p><strong>Date:</strong> {selectedEvent.date || selectedEvent.eventdate || 'N/A'}</p>
|
||||||
|
{selectedEvent.time && <p><strong>Time:</strong> {selectedEvent.time}</p>}
|
||||||
|
{selectedEvent.location && <p><strong>Location:</strong> {selectedEvent.location}</p>}
|
||||||
|
{selectedEvent.admission && <p><strong>Admission:</strong> {selectedEvent.admission}</p>}
|
||||||
|
{selectedEvent.link && <p><strong>Link:</strong> {selectedEvent.link}</p>}
|
||||||
|
<p><strong>Button Text:</strong> {selectedEvent.btn_text || selectedEvent.btnText || 'Details Coming Soon'}</p>
|
||||||
|
<div className="pt-2 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<strong>Description:</strong>
|
||||||
|
<p className="mt-1 whitespace-pre-line">{selectedEvent.description || selectedEvent.desc || selectedEvent.eventdescription || 'No description provided.'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const ev = selectedEvent;
|
||||||
|
setSelectedEvent(null);
|
||||||
|
handleEdit(ev);
|
||||||
|
}}
|
||||||
|
className="bg-blue-600 text-white px-4 py-2 rounded font-semibold hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Edit Event
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedEvent(null)}
|
||||||
|
className="bg-gray-500 text-white px-4 py-2 rounded font-semibold hover:bg-gray-600"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListOfUpcomingEvents;
|
||||||
@ -229,10 +229,10 @@ const Sidebar = () => {
|
|||||||
<AnimateHeight duration={300} height={currentMenu === 'upcoming-events' ? 'auto' : 0}>
|
<AnimateHeight duration={300} height={currentMenu === 'upcoming-events' ? 'auto' : 0}>
|
||||||
<ul className="sub-menu text-gray-500">
|
<ul className="sub-menu text-gray-500">
|
||||||
<li>
|
<li>
|
||||||
<Link href="#">{t('List')}</Link>
|
<Link href="/upcoming-events">{t('List')}</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link href="#">{t('Add New')}</Link>
|
<Link href="/create-upcoming-event">{t('Add New')}</Link>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 757 KiB |
Loading…
x
Reference in New Issue
Block a user