diff --git a/components/gallery/ListOfEventGallery.tsx b/components/gallery/ListOfEventGallery.tsx index 94ac554..93f7e09 100644 --- a/components/gallery/ListOfEventGallery.tsx +++ b/components/gallery/ListOfEventGallery.tsx @@ -1,5 +1,5 @@ 'use client'; -import React, { useEffect, useState } from 'react'; +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'; @@ -8,8 +8,22 @@ 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; @@ -20,7 +34,12 @@ const ListOfEventsGallery: React.FC = ({ eventId }) => { const [isOpen, setIsOpen] = useState(false); const [photoIndex, setPhotoIndex] = useState(0); - const [eventImages, setEventImages] = useState([]); + const [eventImages, setEventImages] = useState([]); + const latestImagesRef = useRef([]); + + useEffect(() => { + latestImagesRef.current = eventImages; + }, [eventImages]); useEffect(() => { if (eventId) { @@ -31,22 +50,57 @@ const ListOfEventsGallery: React.FC = ({ eventId }) => { const getEventGallery = async () => { try { const res = await axios.get(buildApiUrl(`event-images/event/${eventId}`)); - const formatted = res.data?.data?.map((img: any) => ({ - id: img.id, // ensure ID is preserved for deletion + const formatted: GalleryImage[] = res.data?.data?.map((img: any) => ({ + id: img.id, src: img.imageurl, title: img?.title || '', description: img?.description || '', - })); - setEventImages(formatted || []); + })) || []; + setEventImages(formatted); } catch (error) { - console.log("error", error); + console.log('error', error); } }; - const showAlert = async (e: React.MouseEvent, item: any) => { - e.stopPropagation(); // ✅ Prevents triggering the parent onClick (lightbox) + const handleReorder = (newList: GalleryImage[]) => { + setEventImages(newList); + }; - if (isOpen) setIsOpen(false); // optional: close lightbox if it's open + 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', @@ -66,7 +120,7 @@ const ListOfEventsGallery: React.FC = ({ eventId }) => { icon: 'success', customClass: { popup: 'sweet-alerts' }, }); - getEventGallery(); // refresh after delete + getEventGallery(); } catch (error) { Swal.fire({ title: 'Error!', @@ -81,45 +135,75 @@ const ListOfEventsGallery: React.FC = ({ eventId }) => { return (
-
-
Gallery
+ {/* Header */} +
+
Gallery
-
- {eventImages.map((item, index) => ( -
{ - setPhotoIndex(index); - setIsOpen(true); - }} + {/* Gallery Grid */} + {eventImages.length === 0 ? ( +
+ + + +

No images yet

+

Click "Upload Images" to add some!

+
+ ) : ( + <> +

Drag the ☰ icon to reorder images

+ 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" > - {/* Delete button (top-right corner) */} -
- -
+ {/* Drag Handle */} +
+ +
- {`gallery-${index}`} -
- ))} -
+ {/* Delete Button */} +
+ +
+ + {`gallery-${index}`} e.preventDefault()} + className="w-full h-auto" + /> +
+ ))} + + + )} = 6" } }, + "node_modules/classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==", + "license": "MIT" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -5512,6 +5527,22 @@ } } }, + "node_modules/react-sortablejs": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/react-sortablejs/-/react-sortablejs-6.1.4.tgz", + "integrity": "sha512-fc7cBosfhnbh53Mbm6a45W+F735jwZ1UFIYSrIqcO/gRIFoDyZeMtgKlpV4DdyQfbCzdh5LoALLTDRxhMpTyXQ==", + "license": "MIT", + "dependencies": { + "classnames": "2.3.1", + "tiny-invariant": "1.2.0" + }, + "peerDependencies": { + "@types/sortablejs": "1", + "react": ">=16.9.0", + "react-dom": ">=16.9.0", + "sortablejs": "1" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -5817,6 +5848,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sortablejs": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz", + "integrity": "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==", + "license": "MIT" + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -6218,6 +6255,12 @@ "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" }, + "node_modules/tiny-invariant": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz", + "integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==", + "license": "MIT" + }, "node_modules/tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", diff --git a/package.json b/package.json index 63efa2f..e7683e5 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "react-popper": "^2.3.0", "react-quill": "^2.0.0", "react-redux": "^9.1.2", + "react-sortablejs": "^6.1.4", + "sortablejs": "^1.15.7", "sweetalert2": "^11.22.2", "typescript": "^5.3.3", "universal-cookie": "^7.2.0", diff --git a/styles/tailwind.css b/styles/tailwind.css index 7b7e21b..dcdaedd 100644 --- a/styles/tailwind.css +++ b/styles/tailwind.css @@ -677,3 +677,10 @@ img.dark-img { .dark img.dark-img { @apply !block; } + +/* Gallery drag-and-drop swap highlight */ +.swap-highlight { + outline: 2px solid #f59e0b; + outline-offset: 2px; + opacity: 0.8; +}