Commit 46b2c51c by PLN (Algolia)

feat(parvagues): UX polish pass — atmosphere, thumbnails, animations

- Fix mobile hero title clipping (clamp min 5rem → 3rem)
- Reduce dead space between streaming and video sections
- Bump hero poster opacity to 20% for more atmosphere
- Add archive.org thumbnails to video cards with gradient overlay
- Add scroll-triggered reveal animations (IntersectionObserver)
- Add subtle grain/noise overlay (SVG texture at 3% opacity)
- Alternate section backgrounds (#0a0a0a / #0e0e0e) for depth
- Upgrade album platform links to pill buttons with neon hover
- Brand-colored hover on booking submit button
- Add fadeIn animation for streaming embed transitions
parent 40d209c6
...@@ -40,7 +40,8 @@ export default function BookingForm() { ...@@ -40,7 +40,8 @@ export default function BookingForm() {
}; };
return ( return (
<section id="booking" className="max-w-5xl mx-auto px-6 py-24 md:py-32"> <section id="booking" className="reveal py-24 md:py-32">
<div className="max-w-5xl mx-auto px-6">
<h2 className="font-display text-2xl md:text-3xl font-bold tracking-[0.15em] uppercase"> <h2 className="font-display text-2xl md:text-3xl font-bold tracking-[0.15em] uppercase">
Booking Booking
</h2> </h2>
...@@ -86,9 +87,9 @@ export default function BookingForm() { ...@@ -86,9 +87,9 @@ export default function BookingForm() {
</div> </div>
<div className="grid sm:grid-cols-2 gap-5"> <div className="grid sm:grid-cols-2 gap-5">
<select name="eventType" required className={selectClass}> <select name="eventType" required defaultValue="" className={selectClass}>
{eventTypes.map((t) => ( {eventTypes.map((t) => (
<option key={t.value} value={t.value} disabled={!t.value}> <option key={t.value} value={t.value} disabled={!t.value} hidden={!t.value}>
{t.label} {t.label}
</option> </option>
))} ))}
...@@ -107,9 +108,9 @@ export default function BookingForm() { ...@@ -107,9 +108,9 @@ export default function BookingForm() {
className={inputClass} className={inputClass}
/> />
<select name="budget" className={selectClass}> <select name="budget" defaultValue="" className={selectClass}>
{budgetRanges.map((b) => ( {budgetRanges.map((b) => (
<option key={b.value} value={b.value} disabled={!b.value}> <option key={b.value} value={b.value} disabled={!b.value} hidden={!b.value}>
{b.label} {b.label}
</option> </option>
))} ))}
...@@ -124,13 +125,14 @@ export default function BookingForm() { ...@@ -124,13 +125,14 @@ export default function BookingForm() {
<button <button
type="submit" type="submit"
className="flex items-center gap-3 px-8 py-3.5 bg-white text-[var(--surface)] font-display font-bold text-sm tracking-[0.15em] uppercase rounded-full hover:shadow-[0_0_30px_rgba(255,255,255,0.15)] transition-all duration-300" className="flex items-center gap-3 px-8 py-3.5 bg-white text-[var(--surface)] font-display font-bold text-sm tracking-[0.15em] uppercase rounded-full hover:bg-[var(--neon-high)] hover:text-white hover:shadow-[0_0_30px_rgba(217,0,255,0.3)] transition-all duration-300"
> >
<FaEnvelope className="w-4 h-4" /> <FaEnvelope className="w-4 h-4" />
Envoyer Envoyer
</button> </button>
</form> </form>
)} )}
</div>
</section> </section>
); );
} }
...@@ -9,7 +9,7 @@ export default function Hero() { ...@@ -9,7 +9,7 @@ export default function Hero() {
src="/images/parvagues/lives/2024/ccc_release_party/poster.png" src="/images/parvagues/lives/2024/ccc_release_party/poster.png"
alt="" alt=""
fill fill
className="object-cover opacity-10" className="object-cover opacity-20"
priority priority
/> />
{/* Heavy overlay to kill poster text bleed */} {/* Heavy overlay to kill poster text bleed */}
...@@ -26,7 +26,7 @@ export default function Hero() { ...@@ -26,7 +26,7 @@ export default function Hero() {
<h1 <h1
className="font-display font-extrabold leading-none tracking-tight" className="font-display font-extrabold leading-none tracking-tight"
style={{ style={{
fontSize: 'clamp(5rem, 15vw, 12rem)', fontSize: 'clamp(3rem, 15vw, 12rem)',
textShadow: '0 0 80px rgba(217,0,255,0.2), 0 0 160px rgba(217,0,255,0.08)', textShadow: '0 0 80px rgba(217,0,255,0.2), 0 0 160px rgba(217,0,255,0.08)',
}} }}
> >
......
...@@ -2,7 +2,7 @@ import Head from 'next/head'; ...@@ -2,7 +2,7 @@ import Head from 'next/head';
import Link from 'next/link'; import Link from 'next/link';
import Image from 'next/image'; import Image from 'next/image';
import { Syne } from 'next/font/google'; import { Syne } from 'next/font/google';
import { useState, useEffect } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { FaEnvelope, FaInstagram, FaYoutube, FaGithub } from 'react-icons/fa'; import { FaEnvelope, FaInstagram, FaYoutube, FaGithub } from 'react-icons/fa';
import { SiBluesky, SiMastodon } from 'react-icons/si'; import { SiBluesky, SiMastodon } from 'react-icons/si';
...@@ -31,8 +31,24 @@ export default function Layout({ children, title = 'ParVagues' }) { ...@@ -31,8 +31,24 @@ export default function Layout({ children, title = 'ParVagues' }) {
return () => window.removeEventListener('scroll', onScroll); return () => window.removeEventListener('scroll', onScroll);
}, []); }, []);
// Scroll-triggered reveal for sections
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
},
{ threshold: 0.1, rootMargin: '0px 0px -50px 0px' }
);
document.querySelectorAll('.reveal').forEach((el) => observer.observe(el));
return () => observer.disconnect();
}, []);
return ( return (
<div className={`${syne.variable} min-h-screen bg-[var(--surface)] text-[var(--text-primary)] selection:bg-[var(--neon-high)]/30 selection:text-white`}> <div className={`${syne.variable} noise-overlay min-h-screen bg-[var(--surface)] text-[var(--text-primary)] selection:bg-[var(--neon-high)]/30 selection:text-white`}>
<Head> <Head>
<title>{title}</title> <title>{title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
......
...@@ -113,14 +113,14 @@ function AlbumCard({ album }) { ...@@ -113,14 +113,14 @@ function AlbumCard({ album }) {
/> />
</div> </div>
<h4 className="font-display font-semibold text-base mb-3">{album.title}</h4> <h4 className="font-display font-semibold text-base mb-3">{album.title}</h4>
<div className="flex flex-wrap gap-x-4 gap-y-1.5"> <div className="flex flex-wrap gap-2">
{album.links.map((link) => ( {album.links.map((link) => (
<a <a
key={link.platform} key={link.platform}
href={link.url} href={link.url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-xs text-[var(--text-muted)] hover:text-[var(--neon-high)] transition-colors duration-200 tracking-wide" className="inline-flex items-center gap-1.5 px-3 py-1.5 text-[11px] text-[var(--text-muted)] border border-white/[0.08] rounded-full hover:text-white hover:border-[var(--neon-high)]/40 hover:bg-[var(--neon-high)]/5 transition-all duration-200 tracking-wide"
> >
{link.platform} {link.platform}
</a> </a>
...@@ -191,14 +191,15 @@ export default function MusicSection() { ...@@ -191,14 +191,15 @@ export default function MusicSection() {
const activePlatform = streamingPlatforms.find((p) => p.id === activeEmbed); const activePlatform = streamingPlatforms.find((p) => p.id === activeEmbed);
return ( return (
<section id="music" className="max-w-5xl mx-auto px-6 py-24 md:py-32"> <section id="music" className="reveal py-24 md:py-32">
<div className="max-w-5xl mx-auto px-6">
{/* Releases */} {/* Releases */}
<h2 className="font-display text-2xl md:text-3xl font-bold tracking-[0.15em] uppercase"> <h2 className="font-display text-2xl md:text-3xl font-bold tracking-[0.15em] uppercase">
Releases Releases
</h2> </h2>
<div className="h-px bg-white/10 mt-4 mb-12" /> <div className="h-px bg-white/10 mt-4 mb-12" />
<div className="grid md:grid-cols-2 gap-10 md:gap-12 mb-28"> <div className="grid md:grid-cols-2 gap-10 md:gap-12 mb-16">
{albums.map((album) => ( {albums.map((album) => (
<AlbumCard key={album.id} album={album} /> <AlbumCard key={album.id} album={album} />
))} ))}
...@@ -233,11 +234,14 @@ export default function MusicSection() { ...@@ -233,11 +234,14 @@ export default function MusicSection() {
</div> </div>
{activePlatform && ( {activePlatform && (
<div className="mt-2 animate-[fadeIn_0.3s_ease-out]">
<StreamingEmbed <StreamingEmbed
platform={activePlatform} platform={activePlatform}
onClose={() => setActiveEmbed(null)} onClose={() => setActiveEmbed(null)}
/> />
</div>
)} )}
</div>
</section> </section>
); );
} }
...@@ -21,7 +21,8 @@ export default function TourTimeline({ lives }) { ...@@ -21,7 +21,8 @@ export default function TourTimeline({ lives }) {
}); });
return ( return (
<section id="tour" className="max-w-5xl mx-auto px-6 py-24 md:py-32"> <section id="tour" className="reveal section-alt py-24 md:py-32">
<div className="max-w-5xl mx-auto px-6">
<h2 className="font-display text-2xl md:text-3xl font-bold tracking-widest uppercase"> <h2 className="font-display text-2xl md:text-3xl font-bold tracking-widest uppercase">
On Tour On Tour
</h2> </h2>
...@@ -96,6 +97,7 @@ export default function TourTimeline({ lives }) { ...@@ -96,6 +97,7 @@ export default function TourTimeline({ lives }) {
</div> </div>
</div> </div>
))} ))}
</div>
</section> </section>
); );
} }
...@@ -63,14 +63,21 @@ function VideoCard({ video }) { ...@@ -63,14 +63,21 @@ function VideoCard({ video }) {
<div> <div>
<button <button
onClick={() => setLoaded(true)} onClick={() => setLoaded(true)}
className="aspect-video w-full bg-white/[0.03] border border-white/[0.06] rounded-xl flex flex-col items-center justify-center gap-3 cursor-pointer group transition-all duration-300 hover:bg-white/[0.05] hover:border-white/[0.12]" className="aspect-video w-full rounded-xl overflow-hidden relative cursor-pointer group transition-all duration-300 hover:ring-1 hover:ring-[var(--neon-high)]/30"
> >
<div className="w-14 h-14 rounded-full bg-white/[0.06] flex items-center justify-center group-hover:bg-[var(--neon-high)]/15 group-hover:scale-110 transition-all duration-300"> {/* Thumbnail from archive.org */}
<FaPlay className="w-4 h-4 text-white/60 group-hover:text-white ml-0.5 transition-colors" /> <img
src={`https://archive.org/services/img/${video.id}`}
alt={video.title}
className="absolute inset-0 w-full h-full object-cover opacity-60 group-hover:opacity-80 transition-opacity duration-300"
loading="lazy"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/30 to-transparent" />
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3">
<div className="w-14 h-14 rounded-full bg-black/40 backdrop-blur-sm border border-white/10 flex items-center justify-center group-hover:bg-[var(--neon-high)]/20 group-hover:scale-110 group-hover:border-[var(--neon-high)]/30 transition-all duration-300">
<FaPlay className="w-4 h-4 text-white/80 group-hover:text-white ml-0.5 transition-colors" />
</div>
</div> </div>
<span className="text-[11px] text-[var(--text-muted)] tracking-wider group-hover:text-white/60 transition-colors">
archive.org
</span>
</button> </button>
<div className="mt-3"> <div className="mt-3">
<h4 className="font-display font-semibold text-sm">{video.title}</h4> <h4 className="font-display font-semibold text-sm">{video.title}</h4>
...@@ -82,7 +89,8 @@ function VideoCard({ video }) { ...@@ -82,7 +89,8 @@ function VideoCard({ video }) {
export default function VideoSection() { export default function VideoSection() {
return ( return (
<section id="video" className="max-w-5xl mx-auto px-6 py-24 md:py-32"> <section id="video" className="reveal section-alt py-24 md:py-32">
<div className="max-w-5xl mx-auto px-6">
<h2 className="font-display text-2xl md:text-3xl font-bold tracking-[0.15em] uppercase"> <h2 className="font-display text-2xl md:text-3xl font-bold tracking-[0.15em] uppercase">
Video Video
</h2> </h2>
...@@ -93,6 +101,7 @@ export default function VideoSection() { ...@@ -93,6 +101,7 @@ export default function VideoSection() {
<VideoCard key={video.id} video={video} /> <VideoCard key={video.id} video={video} />
))} ))}
</div> </div>
</div>
</section> </section>
); );
} }
...@@ -27,6 +27,12 @@ html { ...@@ -27,6 +27,12 @@ html {
} }
/* Fade-in for embeds */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
/* Shine animation for album covers */ /* Shine animation for album covers */
@keyframes shine { @keyframes shine {
0% { 0% {
...@@ -38,6 +44,36 @@ html { ...@@ -38,6 +44,36 @@ html {
} }
} }
/* Noise grain overlay */
.noise-overlay::after {
content: '';
position: fixed;
inset: 0;
z-index: 9999;
pointer-events: none;
opacity: 0.03;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
background-repeat: repeat;
background-size: 256px 256px;
}
/* Alternating section backgrounds */
.section-alt {
background-color: #0e0e0e;
}
/* Scroll-triggered reveal */
.reveal {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}
.reveal.visible {
opacity: 1;
transform: translateY(0);
}
:global(.hover\:shadow-glow:hover) { :global(.hover\:shadow-glow:hover) {
box-shadow: 0 0 15px rgba(217, 0, 255, 0.7); box-shadow: 0 0 15px rgba(217, 0, 255, 0.7);
} }
......
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