feat(cosmicfest): Add photo modal and adjust player/gallery styles

Implements a modal for viewing V0 gallery photos and makes further
adjustments to audio player and gallery item styling based on feedback.

Key changes:
- SoundCloud Player: Adjusted to 450x450px and updated CSS for flex layout.
- V0 Gallery Photos:
    - Ensured min-height of 150px is respected (current height 200px).
    - Implemented a modal view:
        - Click on photo opens it in a full-size modal.
        - Modal can be closed with Escape key, overlay click, or close button.
        - Uses Next.js Image for optimized display within the modal.
        - Prevents background scroll when modal is open.
        - Includes accessibility attributes (ARIA, keyboard navigation).
parent ff4953db
import Head from 'next/head';
import Image from 'next/image'; // Using Next.js Image component for optimization
import { useState, useEffect } from 'react'; // Added useState and useEffect
import styles from '../../styles/cosmicfest.module.css';
import Countdown from '../../components/cosmicfest/Countdown';
// import ParVaguesFooter from '../../components/ParVaguesFooter'; // Optional: if you want to reuse the main site footer
......@@ -18,6 +19,41 @@ const getNextJune21st = () => {
export default function CosmicFestHome() {
const nextFestivalDate = getNextJune21st();
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalImageSrc, setModalImageSrc] = useState('');
const [modalImageAlt, setModalImageAlt] = useState('');
const openModal = (src, alt) => {
setModalImageSrc(src);
setModalImageAlt(alt);
setIsModalOpen(true);
document.body.style.overflow = 'hidden'; // Prevent background scrolling
};
const closeModal = () => {
setIsModalOpen(false);
setModalImageSrc('');
setModalImageAlt('');
document.body.style.overflow = 'auto'; // Restore background scrolling
};
useEffect(() => {
const handleEsc = (event) => {
if (event.key === 'Escape' && isModalOpen) {
closeModal();
}
};
window.addEventListener('keydown', handleEsc);
// Cleanup function to remove listener
return () => {
window.removeEventListener('keydown', handleEsc);
// Ensure body overflow is reset if component unmounts while modal is open
if (isModalOpen) {
document.body.style.overflow = 'auto';
}
};
}, [isModalOpen]); // Dependency array includes isModalOpen
const lineup2024 = [
{ artist: "Hugo", description: "live guitare, compos et reprises", emojis:"🎸🎙️" },
......@@ -86,9 +122,16 @@ export default function CosmicFestHome() {
<h3>photos v0</h3>
<div className={styles.gallery}>
{v0_content.photos.map((photo, index) => (
<div key={index} className={styles.galleryPhotoItem} style={{backgroundImage: `url(${photo.src})`}}>
{/* <Image src={photo.src} alt={photo.alt} width={200} height={150} objectFit="cover" /> */}
{/* Using div with background for now, as next/image needs actual image dimensions for non-fill */}
<div
key={photo.src} // Using src as key for stability if array order changes but src is unique
className={`${styles.galleryItem} ${styles.galleryPhotoItem}`} // Ensure both classes are applied
style={{backgroundImage: `url(${photo.src})`}}
onClick={() => openModal(photo.src, photo.alt)}
role="button"
tabIndex={0}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openModal(photo.src, photo.alt); } }}
aria-label={`View image: ${photo.alt}`}
>
<span>{photo.alt}</span>
</div>
))}
......@@ -136,7 +179,7 @@ export default function CosmicFestHome() {
<iframe title="Bandcamp player for Parvagues live at Cosmicfest" style={{border: 0, width: "100%", maxWidth: "350px", height: "470px"}} src="https://bandcamp.com/EmbeddedPlayer/album=644862623/size=large/bgcol=333333/linkcol=0f91ff/tracklist=true/artwork=small/transparent=true/" seamless><a href="https://parvagues.bandcamp.com/album/live-cosmicfest">live@cosmicfest by ParVagues</a></iframe>
</div>
<div className={`${styles.audioEmbed} ${styles.soundcloudPlayer}`}>
<iframe title="Soundcloud player for Parvagues live at Cosmicfest" width="100%" height="450" scrolling="no" frameBorder="no" allow="autoplay" src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/2121089043&color=%233e00f7&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true&visual=true"></iframe><div style={{fontSize: "10px", color: "#cccccc",lineBreak: "anywhere",wordBreak: "normal",overflow: "hidden",whiteSpace: "nowrap",textOverflow: "ellipsis", fontFamily: "Interstate,Lucida Grande,Lucida Sans Unicode,Lucida Sans,Garuda,Verdana,Tahoma,sans-serif",fontWeight: "100"}}><a href="https://soundcloud.com/parvagues" title="ParVagues" target="_blank" rel="noopener noreferrer" style={{color: "#cccccc", textDecoration: "none"}}>ParVagues</a> · <a href="https://soundcloud.com/parvagues/cosmicfest" title="Live@cosmicfest 🌊🌅" target="_blank" rel="noopener noreferrer" style={{color: "#cccccc", textDecoration: "none"}}>Live@cosmicfest 🌊🌅</a></div>
<iframe title="Soundcloud player for Parvagues live at Cosmicfest" width="450" height="450" scrolling="no" frameBorder="no" allow="autoplay" src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/2121089043&color=%233e00f7&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true&visual=true"></iframe><div style={{fontSize: "10px", color: "#cccccc",lineBreak: "anywhere",wordBreak: "normal",overflow: "hidden",whiteSpace: "nowrap",textOverflow: "ellipsis", fontFamily: "Interstate,Lucida Grande,Lucida Sans Unicode,Lucida Sans,Garuda,Verdana,Tahoma,sans-serif",fontWeight: "100"}}><a href="https://soundcloud.com/parvagues" title="ParVagues" target="_blank" rel="noopener noreferrer" style={{color: "#cccccc", textDecoration: "none"}}>ParVagues</a> · <a href="https://soundcloud.com/parvagues/cosmicfest" title="Live@cosmicfest 🌊🌅" target="_blank" rel="noopener noreferrer" style={{color: "#cccccc", textDecoration: "none"}}>Live@cosmicfest 🌊🌅</a></div>
</div>
</div>
</section>
......@@ -170,6 +213,23 @@ export default function CosmicFestHome() {
</main>
{isModalOpen && (
<div className={styles.modalOverlay} onClick={closeModal} role="dialog" aria-modal="true" aria-label={modalImageAlt || 'Image viewer'}>
<div className={styles.modalContent} onClick={(e) => e.stopPropagation()}>
<button className={styles.modalCloseButton} onClick={closeModal} aria-label="Close image viewer">
&times;
</button>
{/* For Next/Image, provide intrinsic image width/height if known, or use layout fill with sized parent.
Using layout="responsive" with some default aspect ratio here.
Actual display size will be controlled by CSS for modalImage container. */}
<div className={styles.modalImageContainer}>
<Image src={modalImageSrc} alt={modalImageAlt} layout="intrinsic" width={1200} height={900} objectFit="contain" />
</div>
{modalImageAlt && <p className={styles.modalCaption}>{modalImageAlt}</p>}
</div>
</div>
)}
<footer className={styles.footer}>
<a
href="/parvagues/"
......
......@@ -212,19 +212,27 @@
margin-top: 1.5rem; /* Space above the container */
}
.bandcampPlayer, .soundcloudPlayer {
flex: 1 1 350px; /* Grow, shrink, with a base width related to Bandcamp's max-width */
min-width: 300px; /* Ensure they don't get too squished before wrapping */
margin: 0.5rem; /* Add some margin around each player for spacing */
.bandcampPlayer {
flex: 1 1 350px;
min-width: 300px;
margin: 0.5rem;
}
.soundcloudPlayer {
flex: 1 1 450px; /* Adjusted flex-basis for SoundCloud player */
min-width: 300px; /* Still allow shrinking if necessary */
max-width: 450px; /* Ensure it doesn't grow beyond 450px unless flex grow allows */
margin: 0.5rem;
}
.bandcampPlayer iframe {
max-width: 100%; /* Ensure iframe is responsive within its container */
max-width: 100%; /* Responsive within its container */
/* width and height are set inline on the iframe by Bandcamp's embed usually, or by our style attribute */
}
.soundcloudPlayer iframe {
width: 100%; /* Ensure iframe takes full width of its container */
min-height: 450px; /* Maintain its intended height */
width: 450px;
height: 450px;
max-width: 100%; /* Allows it to shrink if container is smaller than 450px */
}
......@@ -307,6 +315,7 @@
background-position: center;
justify-content: center; /* Center content horizontally (span) */
align-items: flex-end; /* Align content (span) to the bottom */
min-height: 150px; /* Ensure a minimum height as per user feedback, though .galleryItem height should cover this */
}
/* Styling for the text overlay (alt text) on photo items */
......@@ -337,6 +346,85 @@
/* Potentially darken the background image or text background on hover too */
}
/* Styles for Photo Modal */
.modalOverlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.85); /* Darker overlay */
display: flex;
justify-content: center;
align-items: center;
z-index: 1000; /* Ensure it's on top */
cursor: pointer; /* Indicate overlay can be clicked to close */
}
.modalContent {
background-color: #2c3e50; /* Dark theme for modal content box */
padding: 20px;
border-radius: 8px;
box-shadow: 0 5px 25px rgba(0,0,0,0.5);
position: relative; /* For close button positioning */
cursor: default; /* Reset cursor for content area */
max-width: 90vw; /* Max width relative to viewport width */
max-height: 90vh; /* Max height relative to viewport height */
display: flex;
flex-direction: column;
align-items: center;
}
.modalImageContainer {
width: 100%; /* Container takes width of modalContent or image's intrinsic */
max-height: calc(85vh - 80px); /* Adjust based on padding and caption height */
display: flex; /* Helps with centering the image if it's smaller than container */
justify-content: center;
align-items: center;
overflow: hidden; /* In case image is larger than this container */
}
/* next/image specific styling might need to target its internal structure if needed,
but objectFit="contain" and layout="intrinsic" should handle most cases.
The parent .modalImageContainer will constrain its max size. */
.modalImageContainer img { /* Targets the actual img tag from Next/Image */
max-width: 100%;
max-height: 100%; /* Ensure image scales down to fit container */
object-fit: contain; /* Redundant with next/image prop but good fallback */
}
.modalCloseButton {
position: absolute;
top: -10px; /* Position outside slightly or adjust padding */
right: -10px;
background: #e74c3c;
color: white;
border: none;
border-radius: 50%;
width: 35px;
height: 35px;
font-size: 1.8rem;
line-height: 30px; /* Center the '×' */
text-align: center;
cursor: pointer;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
transition: background-color 0.2s ease;
z-index: 1001; /* Above modal content */
}
.modalCloseButton:hover {
background-color: #c0392b;
}
.modalCaption {
color: #ecf0f1; /* Light text for caption */
text-align: center;
margin-top: 10px;
font-size: 0.9rem;
max-width: 100%;
}
/* Styling for the text overlay on video links (if they have a background image) */
.galleryVideoLink span {
color: #fff;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment