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() {
};
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">
Booking
</h2>
......@@ -86,9 +87,9 @@ export default function BookingForm() {
</div>
<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) => (
<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}
</option>
))}
......@@ -107,9 +108,9 @@ export default function BookingForm() {
className={inputClass}
/>
<select name="budget" className={selectClass}>
<select name="budget" defaultValue="" className={selectClass}>
{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}
</option>
))}
......@@ -124,13 +125,14 @@ export default function BookingForm() {
<button
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" />
Envoyer
</button>
</form>
)}
</div>
</section>
);
}
......@@ -9,7 +9,7 @@ export default function Hero() {
src="/images/parvagues/lives/2024/ccc_release_party/poster.png"
alt=""
fill
className="object-cover opacity-10"
className="object-cover opacity-20"
priority
/>
{/* Heavy overlay to kill poster text bleed */}
......@@ -26,7 +26,7 @@ export default function Hero() {
<h1
className="font-display font-extrabold leading-none tracking-tight"
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)',
}}
>
......
......@@ -2,7 +2,7 @@ import Head from 'next/head';
import Link from 'next/link';
import Image from 'next/image';
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 { SiBluesky, SiMastodon } from 'react-icons/si';
......@@ -31,8 +31,24 @@ export default function Layout({ children, title = 'ParVagues' }) {
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 (
<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>
<title>{title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
......
......@@ -113,14 +113,14 @@ function AlbumCard({ album }) {
/>
</div>
<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) => (
<a
key={link.platform}
href={link.url}
target="_blank"
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}
</a>
......@@ -191,14 +191,15 @@ export default function MusicSection() {
const activePlatform = streamingPlatforms.find((p) => p.id === activeEmbed);
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 */}
<h2 className="font-display text-2xl md:text-3xl font-bold tracking-[0.15em] uppercase">
Releases
</h2>
<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) => (
<AlbumCard key={album.id} album={album} />
))}
......@@ -233,11 +234,14 @@ export default function MusicSection() {
</div>
{activePlatform && (
<div className="mt-2 animate-[fadeIn_0.3s_ease-out]">
<StreamingEmbed
platform={activePlatform}
onClose={() => setActiveEmbed(null)}
/>
</div>
)}
</div>
</section>
);
}
......@@ -21,7 +21,8 @@ export default function TourTimeline({ lives }) {
});
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">
On Tour
</h2>
......@@ -96,6 +97,7 @@ export default function TourTimeline({ lives }) {
</div>
</div>
))}
</div>
</section>
);
}
......@@ -63,14 +63,21 @@ function VideoCard({ video }) {
<div>
<button
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">
<FaPlay className="w-4 h-4 text-white/60 group-hover:text-white ml-0.5 transition-colors" />
{/* Thumbnail from archive.org */}
<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>
<span className="text-[11px] text-[var(--text-muted)] tracking-wider group-hover:text-white/60 transition-colors">
archive.org
</span>
</button>
<div className="mt-3">
<h4 className="font-display font-semibold text-sm">{video.title}</h4>
......@@ -82,7 +89,8 @@ function VideoCard({ video }) {
export default function VideoSection() {
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">
Video
</h2>
......@@ -93,6 +101,7 @@ export default function VideoSection() {
<VideoCard key={video.id} video={video} />
))}
</div>
</div>
</section>
);
}
......@@ -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 */
@keyframes shine {
0% {
......@@ -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) {
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