// 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 */}

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 && (
)}
{/* 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();