// gallery-app.jsx function ImageCard({ image, onDelete, ratingInfo, refreshRatings, onOpenPreview }) { const user = window.USER || {}; const canDelete = user.isAdmin || user.id === image.uploader_id; const [loaded, setLoaded] = React.useState(false); return (
{/* 📷 Image */} {image.title} setLoaded(true)} onClick={() => onOpenPreview?.()} // ✅ only open preview when image is clicked className={`cursor-pointer w-full h-48 lg:h-64 object-cover transition-opacity duration-700 ${ loaded ? 'opacity-100' : 'opacity-0' }`} /> {/* 📄 Card Content */}
{/* Title + Delete */}

{image.title}

{canDelete && ( )}
{/* Description */}

{image.description}

{/* Bottom Row: uploader + mic + avg */}
{/* by: callsign - Bottom Left */} {image.uploader && (
by:{' '} e.stopPropagation()} // ✅ prevent preview opening > {image.uploader}
)} {/* Mic + average - Bottom Right */}
{ratingInfo && (
⭐ {ratingInfo.avg} average from {ratingInfo.votes} votes
)}
); } function UploadForm() { const fileInputRef = React.useRef(null); const formRef = React.useRef(null); const [uploading, setUploading] = React.useState(false); const [fileSelected, setFileSelected] = React.useState(false); const handleFileChange = () => { setFileSelected(true); }; const handleUploadClick = () => { if (!fileSelected) { fileInputRef.current.click(); return; } const form = formRef.current; const title = form.title.value.trim(); const description = form.description.value.trim(); if (!title || !description) { Swal.fire({ icon: 'warning', title: 'Missing fields', text: 'Please fill in both the title and description.', toast: true, position: 'top-end', timer: 3000, showConfirmButton: false, }); return; } setUploading(true); form.submit(); }; return (
); } function GalleryApp() { const [images, setImages] = React.useState([]); const [loading, setLoading] = React.useState(true); const [currentPage, setCurrentPage] = React.useState(1); const [selectedImageIndex, setSelectedImageIndex] = React.useState(null); const imagesPerPage = 8; const [ratingsMap, setRatingsMap] = React.useState({}); // Refresh ratings after vote const refreshRatings = () => { fetch('/gallery/get-image-ratings.php') .then((res) => res.json()) .then((json) => { if (json.success && json.data) { setRatingsMap(json.data); } }); }; // Load images once React.useEffect(() => { fetch('/gallery/gallery-data.php') .then((res) => res.json()) .then((data) => { setImages(data); setLoading(false); }); }, []); // Load ratings once on mount React.useEffect(() => { refreshRatings(); }, []); const handleDelete = (image) => { Swal.fire({ title: 'Delete this image?', text: `"${image.title}" will be permanently removed.`, icon: 'warning', showCancelButton: true, confirmButtonText: 'Yes, delete it!', cancelButtonText: 'Cancel', confirmButtonColor: '#e3342f', }).then((result) => { if (result.isConfirmed) { fetch('/gallery/delete.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: image.id }), }) .then((res) => res.json()) .then((res) => { if (res.success) { setImages((prev) => prev.filter((img) => img.id !== image.id)); Swal.fire({ toast: true, position: 'top-end', icon: 'success', title: 'Image deleted', showConfirmButton: false, timer: 2000, }); } else { Swal.fire('Error', res.message || 'Could not delete image.', 'error'); } }); } }); }; const totalPages = Math.ceil(images.length / imagesPerPage); const startIndex = (currentPage - 1) * imagesPerPage; const paginatedImages = images.slice(startIndex, startIndex + imagesPerPage); return (
{/* Gallery Section */}
{loading ? (

Loading images...

) : ( <>
{paginatedImages.map((img, i) => (
setSelectedImageIndex(startIndex + i)} />
))}
{totalPages > 1 && (
{Array.from({ length: totalPages }, (_, i) => ( ))}
)} )}

➕ Add New Image

{selectedImageIndex !== null && ( setSelectedImageIndex(null)} ratingInfo={ratingsMap[images[selectedImageIndex].id] || { avg: 0, votes: 0 }} refreshRatings={refreshRatings} /> )}
); } if (!window.__GALLERY_ROOT__) { window.__GALLERY_ROOT__ = ReactDOM.createRoot( document.getElementById('gallery-root') ); } window.__GALLERY_ROOT__.render();