tca-admin/components/gallery/ListOfEventGallery.tsx
Ravindranbit ca698ad1df feat: gallery drag-and-drop reorder, grid layout fix, and svg upload support
- Add drag-and-drop image reordering using react-sortablejs (swap mode)
- Add sort_order column to event_images table
- Add PUT /api/event-images/reorder bulk reorder endpoint
- Fix gallery grid layout (items-start to remove bottom gap)
- Fix swapClass whitespace error (single CSS class)
- Increase upload limit from 10 to 50 images
- Add 50-image validation with user-friendly error message
- Add svg file support in multer file filter
- Remove auto-arrange feature
- Add empty state UI for gallery with no images
- Add drag handle and delete button on hover
2026-04-28 22:00:41 +05:30

221 lines
8.7 KiB
TypeScript

'use client';
import React, { useEffect, useRef, useState } from 'react';
import Lightbox from 'yet-another-react-lightbox';
import Captions from 'yet-another-react-lightbox/plugins/captions';
import Zoom from 'yet-another-react-lightbox/plugins/zoom';
import 'yet-another-react-lightbox/styles.css';
import 'yet-another-react-lightbox/plugins/captions.css';
import axios from 'axios';
import { useRouter } from 'next/navigation';
import IconTrashLines from '../icon/icon-trash-lines';
import IconMenu from '../icon/icon-menu';
import Swal from 'sweetalert2';
import { buildApiUrl } from '@/utils/BaseUrl.utils';
import { ReactSortable } from 'react-sortablejs';
import Sortable, { Swap } from 'sortablejs';
if (typeof window !== 'undefined') {
Sortable.mount(new Swap());
}
interface GalleryImage {
id: number;
src: string;
title: string;
description: string;
}
interface EditEventFormProps {
eventId: string | null;
}
const ListOfEventsGallery: React.FC<EditEventFormProps> = ({ eventId }) => {
const router = useRouter();
const [isOpen, setIsOpen] = useState(false);
const [photoIndex, setPhotoIndex] = useState(0);
const [eventImages, setEventImages] = useState<GalleryImage[]>([]);
const latestImagesRef = useRef<GalleryImage[]>([]);
useEffect(() => {
latestImagesRef.current = eventImages;
}, [eventImages]);
useEffect(() => {
if (eventId) {
getEventGallery();
}
}, [eventId]);
const getEventGallery = async () => {
try {
const res = await axios.get(buildApiUrl(`event-images/event/${eventId}`));
const formatted: GalleryImage[] = res.data?.data?.map((img: any) => ({
id: img.id,
src: img.imageurl,
title: img?.title || '',
description: img?.description || '',
})) || [];
setEventImages(formatted);
} catch (error) {
console.log('error', error);
}
};
const handleReorder = (newList: GalleryImage[]) => {
setEventImages(newList);
};
const saveNewOrder = async (list: GalleryImage[]) => {
try {
const images = list.map((item, index) => ({
id: item.id,
sort_order: index,
}));
await axios.put(buildApiUrl('event-images/reorder'), { images });
Swal.fire({
title: 'Saved!',
text: 'Image order saved successfully.',
icon: 'success',
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 2000,
});
} catch (error: any) {
console.error('Failed to save order:', error);
const errMsg = error.response?.data?.message || error.message || 'Unknown error';
Swal.fire({
title: 'Error!',
text: `Failed to save the new order: ${errMsg}`,
icon: 'error',
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 5000,
});
}
};
const showAlert = async (e: React.MouseEvent, item: GalleryImage) => {
e.stopPropagation();
if (isOpen) setIsOpen(false);
Swal.fire({
icon: 'warning',
title: 'Are you sure?',
text: "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 {
await axios.delete(buildApiUrl(`event-images/${item.id}`));
Swal.fire({
title: 'Deleted!',
text: 'Your file has been deleted.',
icon: 'success',
customClass: { popup: 'sweet-alerts' },
});
getEventGallery();
} catch (error) {
Swal.fire({
title: 'Error!',
text: 'Failed to delete the file.',
icon: 'error',
customClass: { popup: 'sweet-alerts' },
});
}
}
});
};
return (
<div className="panel">
{/* Header */}
<div className="flex justify-between items-center mb-4">
<h5 className="text-lg font-semibold dark:text-white-light">Gallery</h5>
<button
type="button"
onClick={() => router.push(`/create-event-gallery?eventid=${eventId}`)}
className="bg-blue-600 text-white px-4 py-2 rounded text-sm font-medium hover:bg-blue-700 transition-colors"
>
Upload Images
</button>
</div>
{/* Gallery Grid */}
{eventImages.length === 0 ? (
<div className="flex flex-col items-center justify-center text-gray-400 py-20">
<svg className="w-16 h-16 mb-4 opacity-30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<p className="text-lg font-medium">No images yet</p>
<p className="text-sm mt-1">Click "Upload Images" to add some!</p>
</div>
) : (
<>
<p className="text-xs text-gray-400 italic mb-4">Drag the icon to reorder images</p>
<ReactSortable
list={eventImages}
setList={handleReorder}
onEnd={() => saveNewOrder(latestImagesRef.current)}
animation={200}
handle=".drag-handle"
swap={true}
swapClass="swap-highlight"
className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 items-start"
>
{eventImages.map((item, index) => (
<div
key={item.id}
className="cursor-pointer relative rounded-md border border-gray-200 dark:border-gray-800 shadow-sm overflow-hidden group"
onClick={() => {
setPhotoIndex(index);
setIsOpen(true);
}}
>
{/* Drag Handle */}
<div className="drag-handle absolute top-2 left-2 z-20 cursor-move bg-white/80 dark:bg-black/80 rounded p-1 opacity-0 group-hover:opacity-100 hover:bg-white dark:hover:bg-black transition-all shadow-sm">
<IconMenu className="w-4 h-4 text-gray-600 dark:text-gray-300" />
</div>
{/* Delete Button */}
<div className="absolute top-2 right-2 flex gap-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={(e) => showAlert(e, item)}
className="bg-red-600/80 hover:bg-red-600 text-white text-xs p-1.5 rounded-full shadow-sm transition-colors"
>
<IconTrashLines className="w-4 h-4" />
</button>
</div>
<img
src={item.src}
alt={`gallery-${index}`}
onDragStart={(e) => e.preventDefault()}
className="w-full h-auto"
/>
</div>
))}
</ReactSortable>
</>
)}
<Lightbox
styles={{ container: { backgroundColor: 'rgba(0,0,0,0.6)' } }}
open={isOpen}
close={() => setIsOpen(false)}
slides={eventImages}
index={photoIndex}
plugins={[Captions, Zoom]}
/>
</div>
);
};
export default ListOfEventsGallery;