Files
365devnet/src/components/ui/ImageModal.astro
Richard Bergsma 4321824bf5 Refactor ImageModal component for improved layout and responsiveness
- Updated ImageModal styles to include overflow handling and a maximum height for better usability.
- Adjusted the structure to place the enlarged image at the top, enhancing visual presentation.
- Removed the redundant image container to streamline the component.
2025-06-07 17:29:04 +02:00

183 lines
6.1 KiB
Plaintext

---
// ImageModal.astro - A reusable modal component for displaying enlarged images
---
<div
id="image-modal"
class="fixed inset-0 z-50 flex items-center justify-center opacity-0 pointer-events-none transition-opacity duration-300 ease-in-out"
>
<!-- Backdrop overlay -->
<div
id="modal-backdrop"
class="absolute inset-0 bg-black bg-opacity-75 backdrop-blur-sm transition-opacity duration-300"
>
</div>
<!-- Modal container with animation -->
<div
id="modal-container"
class="fixed top-1/2 left-1/2 z-60 max-w-4xl w-full p-4 transform -translate-x-1/2 -translate-y-1/2 scale-95 transition-all duration-300 ease-in-out flex flex-col items-center overflow-y-auto"
style="max-height: 90vh;"
>
<!-- Close button -->
<button
id="modal-close"
class="absolute top-2 right-2 z-10 bg-white dark:bg-gray-800 rounded-full p-2 shadow-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors duration-200"
aria-label="Close modal"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-gray-600 dark:text-gray-300"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
<!-- Enlarged Image at the Top -->
<img
id="modal-image"
src=""
alt="Enlarged certificate"
class="w-auto object-contain max-h-[60vh] mb-4 mx-auto"
style="vertical-align: middle;"
/>
<!-- Caption -->
<div id="modal-caption" class="mt-2 text-center text-white text-lg font-medium"></div>
</div>
</div>
<script>
// Declare the global function type
declare global {
interface Window {
openImageModal: (imgSrc: string, imgAlt: string) => void;
}
}
// Initialize modal functionality when the DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
const modal = document.getElementById('image-modal');
const modalBackdrop = document.getElementById('modal-backdrop');
const modalContainer = document.getElementById('modal-container');
const modalImage = document.getElementById('modal-image') as HTMLImageElement;
const modalCaption = document.getElementById('modal-caption');
const closeButton = document.getElementById('modal-close');
// Function to open the modal with a specific image
function openModal(imgSrc, imgAlt) {
if (!modal || !modalImage || !modalCaption) return;
// Create a temporary image to get the natural dimensions
const tempImg = new Image();
tempImg.onload = function () {
// Calculate the certification section height (75% of viewport height as defined in the container)
const certSectionHeight = window.innerHeight * 0.75;
const maxHeight = certSectionHeight * 0.75; // 75% of the certification section height
// If the natural image height is smaller than the max height, use the natural height
if (tempImg.height < maxHeight) {
modalImage.style.setProperty('--cert-max-height', `${tempImg.height}px`);
} else {
modalImage.style.setProperty('--cert-max-height', `${maxHeight}px`);
}
// Set the image source and alt text
modalImage.src = imgSrc;
modalCaption.textContent = imgAlt;
// Make the modal visible
modal.classList.remove('opacity-0', 'pointer-events-none');
modal.classList.add('opacity-100', 'pointer-events-auto');
// Animate the container
if (modalContainer) {
modalContainer.classList.remove('scale-95');
modalContainer.classList.add('scale-100');
}
// Set focus to the close button for accessibility
if (closeButton) {
setTimeout(() => closeButton.focus(), 100);
}
};
// Start loading the image
tempImg.src = imgSrc;
// Prevent scrolling on the body
document.body.style.overflow = 'hidden';
}
// Function to close the modal
function closeModal() {
if (!modal || !modalContainer) return;
// Hide the modal with animation
modal.classList.remove('opacity-100', 'pointer-events-auto');
modal.classList.add('opacity-0', 'pointer-events-none');
// Animate the container
modalContainer.classList.remove('scale-100');
modalContainer.classList.add('scale-95');
// Re-enable scrolling
document.body.style.overflow = '';
// Clear the image source after animation completes
setTimeout(() => {
if (modalImage) modalImage.src = '';
if (modalCaption) modalCaption.textContent = '';
}, 300);
}
// Register the openImageModal function globally so it can be called from anywhere
window.openImageModal = openModal;
// Close modal when clicking the close button
if (closeButton) {
closeButton.addEventListener('click', (e) => {
e.preventDefault();
closeModal();
});
}
// Close modal when clicking outside the image container
if (modal) {
modal.addEventListener('click', (e) => {
// Check if the click was on the modal backdrop or the modal itself (not on its children)
if (e.target === modal || e.target === modalBackdrop) {
closeModal();
}
});
}
// Close modal when pressing Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeModal();
}
});
// Handle window resize to recalculate image dimensions
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
if (modalImage && modalImage.src && modal && modal.classList.contains('opacity-100')) {
// Get current image source and recalculate dimensions
const currentSrc = modalImage.src;
const currentAlt = modalCaption?.textContent || '';
// Close and reopen modal to trigger recalculation
closeModal();
setTimeout(() => openModal(currentSrc, currentAlt), 350);
}
}, 200); // Debounce resize events
});
});
</script>