Commit 9b00e9b9 by PLN (Algolia)

Merge feat/parvagues-rebuild: ParVagues SPA rebuild with UX polish

parents 0f62b673 d6e6bbc1
import { FaSpotify, FaDeezer, FaYoutube, FaApple, FaBandcamp } from 'react-icons/fa';
import { SiTidal, SiDeezer } from 'react-icons/si';
const albumsData = [
{
id: '2024_opal',
title: 'Livecoding (Opal Festival 2024)',
image: '/images/parvagues/albums/2024_opal/cover.jpg',
links: [
{ platform: 'Spotify', url: 'https://open.spotify.com/album/1VKLZWeolFNfES2bWzYCWZ', icon: <FaSpotify /> },
{ platform: 'Bandcamp', url: 'https://parvagues.bandcamp.com/album/livecoding-opal-festival-2024', icon: <FaBandcamp /> },
{ platform: 'YouTube', url: 'https://www.youtube.com/playlist?list=OLAK5uy_l4MF3OCIXcdPMpsHGVX2Q9MiX6oU1zT6g', icon: <FaYoutube /> },
{ platform: 'Apple', url: 'https://music.apple.com/fr/album/livecoding-opal-festival-2024/1773790990', icon: <FaApple /> },
{ platform: 'Deezer', url: 'https://www.deezer.com/fr/album/632734951', icon: <SiDeezer /> },
]
},
{
id: '2023_connexion',
title: 'Connexion Etablie EP',
image: '/images/parvagues/albums/2023_connexion/cover.jpg',
links: [
{ platform: 'Spotify', url: 'https://open.spotify.com/album/4uzSN6Uv9IwcYeHdRtkUmM', icon: <FaSpotify /> },
{ platform: 'Bandcamp', url: 'https://parvagues.bandcamp.com/album/connexion-tablie', icon: <FaBandcamp /> },
{ platform: 'YouTube', url: 'https://www.youtube.com/watch?v=VODSdQKrzyw&list=OLAK5uy_nzlx3b7YJYzrbagXF5swhENsCg5vJkT_Q', icon: <FaYoutube /> },
{ platform: 'Apple', url: 'https://music.apple.com/fr/album/_/1711226283', icon: <FaApple /> },
{ platform: 'Deezer', url: 'https://www.deezer.com/fr/album/505854371', icon: <SiDeezer /> },
]
}
];
export default function AlbumCarousel() {
return (
<div className="w-full py-24 bg-gradient-to-b from-black via-purple-900/10 to-black">
<h2 className="text-4xl md:text-6xl font-black text-center mb-16 text-white tracking-tight">
LATEST RELEASES
</h2>
<div className="flex flex-wrap justify-center gap-16 px-4">
{albumsData.map((album) => (
<div key={album.id} className="group relative w-full max-w-md bg-gray-900 rounded-2xl overflow-hidden shadow-2xl transition-all hover:-translate-y-4">
<div className="aspect-square relative overflow-hidden">
<img
src={album.image}
alt={album.title}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110 filter group-hover:brightness-50"
/>
{/* Overlay with links */}
<div className="absolute inset-0 flex items-center justify-center gap-4 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex-wrap p-4">
{album.links.map((link) => (
<a
key={link.platform}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="w-12 h-12 bg-white rounded-full flex items-center justify-center text-black hover:text-purple-600 hover:scale-110 transition-all shadow-lg"
title={link.platform}
>
<span className="text-2xl">{link.icon}</span>
</a>
))}
</div>
</div>
<div className="p-8 text-center bg-gray-900 border-t border-white/5">
<h3 className="text-2xl font-bold text-white mb-2">{album.title}</h3>
<p className="text-gray-400 text-sm uppercase tracking-widest">Listen Now</p>
</div>
</div>
))}
</div>
</div>
);
}
import { useState } from 'react';
import { FaEnvelope } from 'react-icons/fa';
const eventTypes = [
{ value: '', label: 'Type d\'événement' },
{ value: 'festival', label: 'Festival' },
{ value: 'private', label: 'Événement privé' },
{ value: 'corporate', label: 'Corporate' },
{ value: 'collab', label: 'Collaboration artistique' },
{ value: 'other', label: 'Autre' },
];
const budgetRanges = [
{ value: '', label: 'Budget estimé' },
{ value: 'volunteer', label: 'Bénévole / échange' },
{ value: 'small', label: '< 500 €' },
{ value: 'medium', label: '500 – 1 500 €' },
{ value: 'large', label: '1 500 – 5 000 €' },
{ value: 'custom', label: '> 5 000 € / sur mesure' },
];
const inputClass =
'w-full bg-white/[0.04] border border-white/[0.08] rounded-lg px-4 py-3 text-sm text-[var(--text-primary)] placeholder:text-[var(--text-muted)]/60 focus:border-[var(--neon-high)]/40 focus:outline-none focus:ring-1 focus:ring-[var(--neon-high)]/20 transition-all duration-200 appearance-none';
const selectClass =
'w-full bg-white/[0.04] border border-white/[0.08] rounded-lg px-4 py-3 text-sm text-[var(--text-muted)] focus:border-[var(--neon-high)]/40 focus:outline-none focus:ring-1 focus:ring-[var(--neon-high)]/20 transition-all duration-200 appearance-none cursor-pointer';
export default function BookingForm() {
const [submitted, setSubmitted] = useState(false);
const handleSubmit = (e) => {
e.preventDefault();
const data = new FormData(e.target);
const subject = encodeURIComponent(`Booking: ${data.get('eventType') || 'Inquiry'} - ${data.get('venue') || 'TBD'}`);
const body = encodeURIComponent(
`Nom: ${data.get('name')}\nEmail: ${data.get('email')}\nType: ${data.get('eventType')}\nDate: ${data.get('date')}\nLieu: ${data.get('venue')}\nBudget: ${data.get('budget')}\n\n${data.get('message')}`
);
window.location.href = `mailto:parvagues@nech.pl?subject=${subject}&body=${body}`;
setSubmitted(true);
};
return (
<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>
<div className="h-px bg-white/10 mt-4 mb-4" />
<p className="text-sm text-[var(--text-muted)] mb-12 max-w-lg">
Intéressé·e par un live? Remplis le formulaire ci-dessous
ou écris directement à{' '}
<a href="mailto:parvagues@nech.pl" className="text-[var(--neon-high)]/80 hover:text-[var(--neon-high)] transition-colors">
parvagues@nech.pl
</a>
</p>
{submitted ? (
<div className="bg-white/[0.03] border border-white/[0.06] rounded-xl p-12 text-center">
<p className="font-display font-semibold text-lg mb-2">Merci !</p>
<p className="text-sm text-[var(--text-muted)]">
Ton client mail devrait s&apos;ouvrir avec le formulaire pré-rempli.
</p>
<button
onClick={() => setSubmitted(false)}
className="mt-6 text-xs text-[var(--text-muted)] hover:text-white transition-colors tracking-wider underline underline-offset-4"
>
Envoyer un autre message
</button>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-5 max-w-2xl">
<div className="grid sm:grid-cols-2 gap-5">
<input
name="name"
type="text"
placeholder="Nom"
required
className={inputClass}
/>
<input
name="email"
type="email"
placeholder="Email"
required
className={inputClass}
/>
</div>
<div className="grid sm:grid-cols-2 gap-5">
<select name="eventType" required defaultValue="" className={selectClass}>
{eventTypes.map((t) => (
<option key={t.value} value={t.value} disabled={!t.value} hidden={!t.value}>
{t.label}
</option>
))}
</select>
<input
name="date"
type="date"
className={`${inputClass} text-[var(--text-muted)]`}
/>
</div>
<input
name="venue"
type="text"
placeholder="Lieu / Ville"
className={inputClass}
/>
<select name="budget" defaultValue="" className={selectClass}>
{budgetRanges.map((b) => (
<option key={b.value} value={b.value} disabled={!b.value} hidden={!b.value}>
{b.label}
</option>
))}
</select>
<textarea
name="message"
placeholder="Décris ton projet, l'ambiance, tes attentes..."
rows={5}
className={`${inputClass} resize-none`}
/>
<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: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>
);
}
import { useState, useEffect } from 'react';
export default function Countdown({ targetDate, onPhaseChange }) {
const [timeLeft, setTimeLeft] = useState(calculateTimeLeft());
function calculateTimeLeft() {
const difference = +new Date(targetDate) - +new Date();
let timeLeft = {};
if (difference > 0) {
timeLeft = {
days: Math.floor(difference / (1000 * 60 * 60 * 24)),
hours: Math.floor((difference / (1000 * 60 * 60)) % 24),
minutes: Math.floor((difference / 1000 / 60) % 60),
seconds: Math.floor((difference / 1000) % 60),
};
}
return timeLeft;
}
useEffect(() => {
const timer = setTimeout(() => {
setTimeLeft(calculateTimeLeft());
}, 1000);
return () => clearTimeout(timer);
});
const timerComponents = [];
Object.keys(timeLeft).forEach((interval) => {
if (!timeLeft[interval]) {
return;
}
timerComponents.push(
<span key={interval} className="mx-2">
<span className="text-4xl font-bold font-mono text-white">{timeLeft[interval]}</span>
<span className="text-sm text-gray-400 uppercase ml-1">{interval}</span>
</span>
);
});
return (
<div className="flex justify-center items-center p-6 bg-black/50 rounded-xl border border-purple-500/30">
{timerComponents.length ? timerComponents : <span className="text-2xl font-bold text-white">Event Started!</span>}
</div>
);
}
import Image from 'next/image';
export default function Hero() {
return (
<section className="relative h-screen flex items-center justify-center overflow-hidden">
{/* Background */}
<div className="absolute inset-0">
<Image
src="/images/parvagues/lives/2024/ccc_release_party/poster.png"
alt=""
fill
className="object-cover opacity-20"
priority
/>
{/* Heavy overlay to kill poster text bleed */}
<div
className="absolute inset-0"
style={{
background: 'linear-gradient(to bottom, var(--surface) 0%, rgba(10,10,10,0.85) 40%, rgba(10,10,10,0.85) 60%, var(--surface) 100%)',
}}
/>
</div>
{/* Content */}
<div className="relative z-10 text-center px-6 max-w-4xl">
<h1
className="font-display font-extrabold leading-none tracking-tight"
style={{
fontSize: 'clamp(3rem, 15vw, 12rem)',
textShadow: '0 0 80px rgba(217,0,255,0.2), 0 0 160px rgba(217,0,255,0.08)',
}}
>
ParVagues
</h1>
<div
className="mx-auto mt-8 mb-8"
style={{ width: '6rem', height: '1px', background: 'linear-gradient(to right, transparent, rgba(217,0,255,0.4), transparent)' }}
/>
<p className="text-base md:text-lg italic leading-relaxed max-w-xl mx-auto" style={{ color: 'var(--text-muted)' }}>
ParVagues, c&apos;est des ondes qui naissent dans un océan binaire
pour parfois s&apos;échouer sur vos plages sonores.
</p>
<div className="mt-14 flex flex-col sm:flex-row gap-4 justify-center items-center">
<a
href="#tour"
className="inline-block px-8 py-3.5 bg-white font-display font-bold text-sm tracking-widest uppercase rounded-full transition-all duration-300 hover:shadow-lg"
style={{ color: 'var(--surface)' }}
>
On Tour
</a>
<a
href="#music"
className="inline-block px-8 py-3.5 border border-white/25 text-white font-display font-bold text-sm tracking-widest uppercase rounded-full hover:bg-white/10 hover:border-white/50 transition-all duration-300"
>
Écouter
</a>
</div>
</div>
{/* Scroll indicator */}
<div className="absolute bottom-10 left-1/2 -translate-x-1/2 z-10">
<div className="animate-pulse" style={{ width: '1px', height: '3rem', background: 'linear-gradient(to bottom, transparent, rgba(255,255,255,0.3))' }} />
</div>
</section>
);
}
import { useState } from 'react';
import Masonry from 'react-masonry-css';
import styles from '@/styles/parvagues.module.css';
export default function ImageGallery({ images }) {
const [selectedImage, setSelectedImage] = useState(null);
const breakpointColumnsObj = {
default: 3,
1100: 3,
700: 2,
500: 1
};
if (!images || images.length === 0) return null;
return (
<>
<Masonry
breakpointCols={breakpointColumnsObj}
className={styles.galleryGrid}
columnClassName={styles.galleryGridColumn}
>
{images.map((image, index) => (
<div
key={index}
className={`${styles.galleryCard} mb-4 cursor-pointer overflow-hidden rounded-lg border border-transparent hover:border-purple-500/50`}
onClick={() => setSelectedImage(image)}
>
<img
src={image}
alt={`Gallery image ${index + 1}`}
className="w-full h-auto block"
loading="lazy"
/>
</div>
))}
</Masonry>
{selectedImage && (
<div
className={styles.modalOverlay}
onClick={() => setSelectedImage(null)}
>
<div className={styles.modalContent} onClick={e => e.stopPropagation()}>
<button
className={styles.modalCloseButton}
onClick={() => setSelectedImage(null)}
>
&times;
</button>
<img
src={selectedImage}
alt="Full size"
className="max-w-full max-h-[90vh] object-contain"
/>
</div>
</div>
)}
</>
);
}
import Head from 'next/head';
import Link from 'next/link';
import Image from 'next/image';
import { Syne } from 'next/font/google';
import { useState, useEffect, useCallback } from 'react';
import { FaEnvelope, FaInstagram, FaYoutube, FaGithub } from 'react-icons/fa';
import { SiBluesky, SiMastodon } from 'react-icons/si';
const syne = Syne({
subsets: ['latin'],
variable: '--font-syne',
display: 'swap',
weight: ['400', '600', '700', '800'],
});
const socials = [
{ href: 'https://instagram.com/parvagues.mp3', icon: FaInstagram, label: 'Instagram' },
{ href: 'https://bsky.app/profile/nech.pl', icon: SiBluesky, label: 'Bluesky' },
{ href: 'https://github.com/parvagues', icon: FaGithub, label: 'GitHub' },
{ href: 'https://youtube.com/@parvagues', icon: FaYoutube, label: 'YouTube' },
{ href: 'https://chaos.social/@PixelNoir', icon: SiMastodon, label: 'Mastodon' },
];
export default function Layout({ children, title = 'ParVagues' }) {
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 50);
window.addEventListener('scroll', onScroll, { passive: true });
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 (
<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" />
<meta name="description" content="ParVagues - Livecoding de musique électronique. Ondes binaires, plages sonores." />
<meta property="og:title" content={title} />
<meta property="og:type" content="music.musician" />
<link rel="icon" href="/images/parvagues/logo.png" />
</Head>
{/* Header */}
<header
className={`fixed top-0 w-full z-50 transition-all duration-500 ${
scrolled
? 'bg-[var(--surface)]/95 backdrop-blur-md border-b border-white/[0.06]'
: ''
}`}
>
<div className="max-w-5xl mx-auto px-6 h-16 flex items-center justify-between">
<Link href="/parvagues" className="flex items-center gap-3 group">
<Image
src="/images/parvagues/logo.png"
alt="ParVagues"
width={28}
height={28}
className="transition-all duration-300 group-hover:drop-shadow-[0_0_8px_rgba(217,0,255,0.5)]"
/>
<span
className="font-display font-bold text-xs tracking-[0.15em] uppercase transition-opacity duration-500"
style={{ opacity: scrolled ? 1 : 0 }}
>
ParVagues
</span>
</Link>
<nav className="hidden md:flex items-center gap-8 text-[11px] tracking-[0.2em] uppercase">
{['tour', 'music', 'video', 'booking'].map((id) => (
<a
key={id}
href={`#${id}`}
className="text-[var(--text-muted)] hover:text-white transition-colors duration-300"
>
{id}
</a>
))}
</nav>
<a
href="#booking"
className="flex items-center gap-2 px-4 py-2 text-[11px] tracking-[0.15em] uppercase border border-white/20 rounded-full hover:bg-white hover:text-[var(--surface)] transition-all duration-300"
>
<FaEnvelope className="w-3 h-3" />
<span className="hidden sm:inline">Book</span>
</a>
</div>
</header>
<main>{children}</main>
{/* Footer */}
<footer className="border-t border-white/[0.06] py-16">
<div className="max-w-5xl mx-auto px-6">
<div className="flex flex-col items-center gap-8">
<div className="flex items-center gap-6">
{socials.map(({ href, icon: Icon, label }) => (
<a
key={label}
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-[var(--text-muted)] hover:text-white transition-colors duration-300"
aria-label={label}
>
<Icon className="w-5 h-5" />
</a>
))}
</div>
<div className="text-center">
<p className="text-[var(--text-muted)] text-xs tracking-wider">
© {new Date().getFullYear()} ParVagues
</p>
<a
href="mailto:parvagues@nech.pl"
className="text-[var(--text-muted)] hover:text-[var(--neon-high)] text-xs tracking-wider transition-colors"
>
parvagues@nech.pl
</a>
</div>
</div>
</div>
</footer>
</div>
);
}
import Link from 'next/link';
import { format } from 'date-fns';
import { fr } from 'date-fns/locale';
import { FaSoundcloud, FaSpotify, FaYoutube, FaTwitch } from 'react-icons/fa';
// Generate consistent gradient based on string hash
function generateGradient(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
const hue1 = Math.abs(hash % 360);
const hue2 = (hue1 + 60) % 360;
return `linear-gradient(135deg, hsl(${hue1}, 70%, 25%) 0%, hsl(${hue2}, 60%, 15%) 100%)`;
}
// Platform badge component
function PlatformBadge({ url, icon: Icon, label }) {
if (!url) return null;
return (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 px-2 py-1 bg-white/10 hover:bg-white/20 rounded-full text-xs transition-all"
onClick={(e) => e.stopPropagation()}
>
<Icon className="w-3 h-3" />
<span className="hidden sm:inline">{label}</span>
</a>
);
}
export default function LiveList({ lives }) {
const sortedLives = [...lives].sort((a, b) => new Date(b.date) - new Date(a.date));
return (
<div className="w-full max-w-7xl mx-auto py-24 px-4">
<div className="flex items-end justify-between mb-16 border-b border-white/10 pb-4">
<h2 className="text-5xl md:text-7xl font-black text-transparent bg-clip-text bg-gradient-to-r from-white to-gray-600">
LIVE SETS
</h2>
<span className="text-purple-400 font-mono text-sm hidden md:block">
// {sortedLives.length} performances
</span>
</div>
<div className="grid grid-cols-1 gap-4">
{sortedLives.map((live) => {
const date = new Date(live.date);
const isFuture = date > new Date();
const isValid = !isNaN(date.getTime());
const gradient = generateGradient(live.slug || live.title);
return (
<Link href={`/parvagues/live/${live.slug}`} key={live.slug}>
<div className="group relative overflow-hidden rounded-2xl border border-white/10 hover:border-purple-500/50 transition-all duration-300 hover:shadow-[0_0_30px_rgba(168,85,247,0.2)]">
{/* Background gradient */}
<div
className="absolute inset-0 opacity-40 group-hover:opacity-60 transition-opacity"
style={{ background: gradient }}
/>
{/* Noise texture overlay */}
<div className="absolute inset-0 opacity-5 mix-blend-overlay" style={{
backgroundImage: 'url("data:image/svg+xml,%3Csvg viewBox=\'0 0 400 400\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cfilter id=\'noiseFilter\'%3E%3CfeTurbulence type=\'fractalNoise\' baseFrequency=\'0.9\' numOctaves=\'4\' /%3E%3C/filter%3E%3Crect width=\'100%25\' height=\'100%25\' filter=\'url(%23noiseFilter)\' /%3E%3C/svg%3E")'
}} />
{/* Content */}
<div className="relative p-6 flex flex-col md:flex-row items-start md:items-center gap-4">
{/* Date badge */}
<div className="flex-shrink-0">
<div className={`px-4 py-2 rounded-xl font-mono font-bold text-sm ${isFuture
? 'bg-purple-500/30 text-purple-200 border border-purple-400/50'
: 'bg-white/10 text-gray-300 border border-white/20'
}`}>
{isValid ? format(date, 'dd MMM yyyy', { locale: fr }) : 'TBD'}
</div>
{isFuture && (
<div className="mt-2 px-2 py-1 bg-purple-500/20 text-purple-300 text-xs font-bold uppercase tracking-widest rounded text-center">
Upcoming
</div>
)}
</div>
{/* Event info */}
<div className="flex-grow min-w-0">
<h3 className="text-xl md:text-2xl font-bold text-white group-hover:text-purple-300 transition-colors mb-1 truncate">
{live.title}
</h3>
<p className="text-gray-400 text-sm md:text-base group-hover:text-gray-300 transition-colors">
📍 {live.location}
</p>
{/* Platform badges */}
{(live.audio || live.video || live.ctaURL) && (
<div className="flex flex-wrap gap-2 mt-3">
{live.audio && live.audio.includes('soundcloud') && (
<PlatformBadge url={live.audio} icon={FaSoundcloud} label="SoundCloud" />
)}
{live.audio && live.audio.includes('spotify') && (
<PlatformBadge url={live.audio} icon={FaSpotify} label="Spotify" />
)}
{live.video && live.video.includes('youtube') && (
<PlatformBadge url={live.video} icon={FaYoutube} label="YouTube" />
)}
{live.video && live.video.includes('twitch') && (
<PlatformBadge url={live.video} icon={FaTwitch} label="Twitch" />
)}
</div>
)}
</div>
{/* Arrow CTA */}
<div className="flex-shrink-0">
<div className="w-12 h-12 rounded-full border-2 border-white/30 flex items-center justify-center group-hover:bg-purple-500 group-hover:border-purple-500 transition-all transform group-hover:rotate-45 group-hover:scale-110">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
</div>
</div>
</div>
</div>
</Link>
);
})}
</div>
</div>
);
}
import { useState } from 'react';
import Image from 'next/image';
import {
FaBandcamp, FaSpotify, FaYoutube, FaApple, FaSoundcloud, FaInstagram,
} from 'react-icons/fa';
// react-icons doesn't ship a Deezer icon in this version
function DeezerIcon({ className }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
<path d="M18.81 4.16v3.03H24V4.16h-5.19zM6.27 8.38v3.027h5.19V8.38H6.27zm12.54 0v3.027H24V8.38h-5.19zM6.27 12.594v3.027h5.19v-3.027H6.27zm6.27 0v3.027h5.19v-3.027h-5.19zm6.27 0v3.027H24v-3.027h-5.19zM0 16.81v3.029h5.19v-3.03H0zm6.27 0v3.029h5.19v-3.03H6.27zm6.27 0v3.029h5.19v-3.03h-5.19zm6.27 0v3.029H24v-3.03h-5.19z"/>
</svg>
);
}
const albums = [
{
id: '2024_opal',
title: 'Livecoding (Opal Festival 2024)',
image: '/images/parvagues/albums/2024_opal/cover.jpg',
links: [
{ platform: 'Bandcamp', url: 'https://parvagues.bandcamp.com/album/livecoding-opal-festival-2024' },
{ platform: 'Deezer', url: 'https://www.deezer.com/fr/album/632734951' },
{ platform: 'Apple Music', url: 'https://music.apple.com/fr/album/livecoding-opal-festival-2024/1773790990' },
{ platform: 'Spotify', url: 'https://open.spotify.com/album/1VKLZWeolFNfES2bWzYCWZ' },
{ platform: 'YouTube', url: 'https://www.youtube.com/playlist?list=OLAK5uy_l4MF3OCIXcdPMpsHGVX2Q9MiX6oU1zT6g' },
],
},
{
id: '2023_connexion',
title: 'Connexion Établie EP',
image: '/images/parvagues/albums/2023_connexion/cover.jpg',
links: [
{ platform: 'Bandcamp', url: 'https://parvagues.bandcamp.com/album/connexion-tablie' },
{ platform: 'Deezer', url: 'https://www.deezer.com/fr/album/505854371' },
{ platform: 'Apple Music', url: 'https://music.apple.com/fr/album/_/1711226283' },
{ platform: 'Spotify', url: 'https://open.spotify.com/album/4uzSN6Uv9IwcYeHdRtkUmM' },
{ platform: 'YouTube', url: 'https://www.youtube.com/watch?v=VODSdQKrzyw&list=OLAK5uy_nzlx3b7YJYzrbagXF5swhENsCg5vJkT_Q' },
],
},
];
const streamingPlatforms = [
{
id: 'soundcloud',
label: 'SoundCloud',
icon: FaSoundcloud,
color: '#ff5500',
embedUrl: 'https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/users/1084818893&color=%23a700d1&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true',
embedHeight: 450,
profileUrl: 'https://soundcloud.com/parvagues',
},
{
id: 'bandcamp',
label: 'Bandcamp',
icon: FaBandcamp,
color: '#1da0c3',
embedUrl: 'https://bandcamp.com/EmbeddedPlayer/album=3869867806/size=large/bgcol=333333/linkcol=a700d1/tracklist=false/transparent=true/',
embedHeight: 450,
profileUrl: 'https://parvagues.bandcamp.com/',
},
{
id: 'spotify',
label: 'Spotify',
icon: FaSpotify,
color: '#1db954',
embedUrl: 'https://open.spotify.com/embed/artist/0kznTQnx5QRhMwktmZboX4?utm_source=generator&theme=0',
embedHeight: 450,
profileUrl: 'https://open.spotify.com/artist/0kznTQnx5QRhMwktmZboX4',
},
{
id: 'youtube',
label: 'YouTube',
icon: FaYoutube,
color: '#ff0000',
embedUrl: 'https://www.youtube.com/embed?listType=user_uploads&list=@parvagues',
embedHeight: 400,
profileUrl: 'https://www.youtube.com/@parvagues/videos',
isVideo: true,
},
{
id: 'deezer',
label: 'Deezer',
icon: DeezerIcon,
color: '#a238ff',
embedUrl: 'https://widget.deezer.com/widget/dark/artist/103670512/top_tracks',
embedHeight: 450,
profileUrl: 'https://www.deezer.com/fr/artist/103670512',
},
{
id: 'instagram',
label: 'Instagram',
icon: FaInstagram,
color: '#e4405f',
profileUrl: 'https://www.instagram.com/parvagues.mp3/',
isPrivacy: true,
},
];
function AlbumCard({ album }) {
return (
<div className="group">
<div
className="relative rounded-xl overflow-hidden mb-5"
style={{ aspectRatio: '1', backgroundColor: 'var(--surface-raised)' }}
>
<Image
src={album.image}
alt={album.title}
fill
className="object-cover transition-transform duration-700 group-hover:scale-105"
sizes="(max-width: 768px) 100vw, 50vw"
/>
</div>
<h4 className="font-display font-semibold text-base mb-3">{album.title}</h4>
<div className="flex flex-wrap gap-2">
{album.links.map((link) => (
<a
key={link.platform}
href={link.url}
target="_blank"
rel="noopener noreferrer"
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>
))}
</div>
</div>
);
}
function StreamingEmbed({ platform, onClose }) {
if (platform.isPrivacy) {
return (
<div className="bg-white/[0.03] border border-white/[0.06] rounded-xl p-12 text-center">
<FaInstagram className="w-10 h-10 text-[var(--text-muted)] mx-auto mb-4" />
<p className="text-sm text-[var(--text-muted)] mb-6 max-w-sm mx-auto">
Le contenu Instagram se connecte aux serveurs de Meta et peut suivre votre activité.
</p>
<a
href={platform.profileUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 text-sm font-display font-semibold tracking-wider rounded-full border border-white/20 hover:bg-white hover:text-[var(--surface)] transition-all duration-300"
>
Voir sur Instagram
</a>
</div>
);
}
return (
<div className="bg-white/[0.03] border border-white/[0.06] rounded-xl overflow-hidden">
<div style={{ height: platform.isVideo ? undefined : platform.embedHeight }}>
<iframe
src={platform.embedUrl}
width="100%"
height={platform.isVideo ? undefined : platform.embedHeight}
className={platform.isVideo ? 'aspect-video w-full' : ''}
frameBorder="0"
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy"
style={{ borderRadius: '12px' }}
/>
</div>
<div className="px-6 py-4 flex items-center justify-between border-t border-white/[0.06]">
<a
href={platform.profileUrl}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-[var(--text-muted)] hover:text-white transition-colors tracking-wider"
>
Voir le profil complet
</a>
<button
onClick={onClose}
className="text-xs text-[var(--text-muted)] hover:text-white transition-colors tracking-wider"
>
Fermer
</button>
</div>
</div>
);
}
export default function MusicSection() {
const [activeEmbed, setActiveEmbed] = useState(null);
const toggle = (id) => setActiveEmbed(activeEmbed === id ? null : id);
const activePlatform = streamingPlatforms.find((p) => p.id === activeEmbed);
return (
<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-16">
{albums.map((album) => (
<AlbumCard key={album.id} album={album} />
))}
</div>
{/* Streaming */}
<h3 className="font-display text-xl md:text-2xl font-bold tracking-[0.15em] uppercase">
Streaming
</h3>
<div className="h-px bg-white/10 mt-4 mb-8" />
<div className="flex flex-wrap gap-3 mb-8">
{streamingPlatforms.map(({ id, label, icon: Icon, color }) => (
<button
key={id}
onClick={() => toggle(id)}
className={`flex items-center gap-2 px-5 py-2.5 rounded-full text-xs font-display font-semibold tracking-wider transition-all duration-300 ${
activeEmbed === id
? 'text-white shadow-lg scale-105'
: 'bg-white/[0.04] text-[var(--text-muted)] hover:bg-white/[0.08] hover:text-white'
}`}
style={
activeEmbed === id
? { backgroundColor: color, boxShadow: `0 0 20px ${color}30` }
: {}
}
>
<Icon className="w-4 h-4" />
<span className="hidden sm:inline">{label}</span>
</button>
))}
</div>
{activePlatform && (
<div className="mt-2 animate-[fadeIn_0.3s_ease-out]">
<StreamingEmbed
platform={activePlatform}
onClose={() => setActiveEmbed(null)}
/>
</div>
)}
</div>
</section>
);
}
import { FaInstagram, FaTwitter, FaGithub } from 'react-icons/fa';
import { SiBluesky, SiMastodon } from 'react-icons/si';
export default function SocialCTA() {
return (
<div className="flex flex-wrap justify-center gap-6 py-12">
<a
href="https://instagram.com/parvagues"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 bg-gradient-to-br from-purple-600 to-pink-600 px-8 py-4 rounded-full text-white font-bold text-lg hover:from-purple-500 hover:to-pink-500 transition-all transform hover:scale-105 shadow-lg hover:shadow-purple-500/50"
>
<FaInstagram className="text-2xl" />
<span>Instagram</span>
</a>
<a
href="https://bsky.app/profile/nech.pl"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 bg-gray-800 hover:bg-gray-700 px-8 py-4 rounded-full text-white font-bold text-lg transition-all transform hover:scale-105 shadow-lg border border-white/10 hover:border-white/30"
>
<SiBluesky className="text-2xl" />
<span>Bluesky</span>
</a>
<a
href="https://github.com/parvagues"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 bg-black hover:bg-gray-900 px-8 py-4 rounded-full text-white font-bold text-lg transition-all transform hover:scale-105 shadow-lg border border-white/30 hover:border-white"
>
<FaGithub className="text-2xl" />
<span>GitHub</span>
</a>
</div>
);
}
import { useState } from 'react';
import { FaSoundcloud, FaSpotify, FaYoutube, FaInstagram, FaBandcamp } from 'react-icons/fa';
import { SiDeezer } from 'react-icons/si';
export default function StreamingFeeds() {
const [activeTab, setActiveTab] = useState('soundcloud');
const [instagramConsent, setInstagramConsent] = useState(false);
const tabs = [
{ id: 'soundcloud', label: 'SoundCloud', icon: FaSoundcloud, color: 'from-orange-500 to-orange-600' },
{ id: 'spotify', label: 'Spotify', icon: FaSpotify, color: 'from-green-500 to-green-600' },
{ id: 'bandcamp', label: 'Bandcamp', icon: FaBandcamp, color: 'from-cyan-500 to-blue-600' },
{ id: 'youtube', label: 'YouTube', icon: FaYoutube, color: 'from-red-500 to-red-600' },
{ id: 'deezer', label: 'Deezer', icon: SiDeezer, color: 'from-purple-500 to-pink-600' },
{ id: 'instagram', label: 'Instagram', icon: FaInstagram, color: 'from-pink-500 to-purple-600' },
];
return (
<div className="w-full max-w-7xl mx-auto py-24 px-4">
<div className="text-center mb-12">
<h2 className="text-5xl md:text-7xl font-black text-transparent bg-clip-text bg-gradient-to-r from-white to-gray-600 mb-4">
STREAMING
</h2>
<p className="text-gray-400 text-lg">
Explore recent sets, tracks, and performances across platforms
</p>
</div>
{/* Tab navigation */}
<div className="flex flex-wrap justify-center gap-3 mb-8">
{tabs.map(({ id, label, icon: Icon, color }) => (
<button
key={id}
onClick={() => setActiveTab(id)}
className={`flex items-center gap-2 px-4 py-2 md:px-6 md:py-3 rounded-full font-bold transition-all text-sm md:text-base ${activeTab === id
? `bg-gradient-to-r ${color} text-white shadow-lg scale-105`
: 'bg-white/10 text-gray-300 hover:bg-white/20'
}`}
>
<Icon className="w-4 h-4 md:w-5 md:h-5" />
<span className="hidden sm:inline">{label}</span>
</button>
))}
</div>
{/* Content area */}
<div className="bg-white/5 rounded-2xl border border-white/10 p-4 md:p-8 min-h-[400px]">
{activeTab === 'soundcloud' && (
<div className="space-y-4">
<iframe
width="100%"
height="450"
scrolling="no"
frameBorder="no"
allow="autoplay"
src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/users/1084818893&color=%23a700d1&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true"
className="rounded-xl"
/>
<div className="text-center">
<a
href="https://soundcloud.com/parvagues"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-orange-500 to-orange-600 text-white font-bold rounded-full hover:scale-105 transition-transform"
>
<FaSoundcloud className="w-5 h-5" />
View Full Profile
</a>
</div>
</div>
)}
{activeTab === 'spotify' && (
<div className="space-y-4">
<iframe
style={{ borderRadius: '12px' }}
src="https://open.spotify.com/embed/artist/0kznTQnx5QRhMwktmZboX4?utm_source=generator&theme=0"
width="100%"
height="450"
frameBorder="0"
allowFullScreen
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy"
/>
<div className="text-center">
<a
href="https://open.spotify.com/artist/0kznTQnx5QRhMwktmZboX4"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-green-500 to-green-600 text-white font-bold rounded-full hover:scale-105 transition-transform"
>
<FaSpotify className="w-5 h-5" />
Listen on Spotify
</a>
</div>
</div>
)}
{activeTab === 'bandcamp' && (
<div className="space-y-4">
<iframe
style={{ border: 0, width: '100%', height: '450px', borderRadius: '12px' }}
src="https://bandcamp.com/EmbeddedPlayer/album=3869867806/size=large/bgcol=333333/linkcol=a700d1/tracklist=false/transparent=true/"
seamless
/>
<div className="text-center">
<a
href="https://parvagues.bandcamp.com/"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-cyan-500 to-blue-600 text-white font-bold rounded-full hover:scale-105 transition-transform"
>
<FaBandcamp className="w-5 h-5" />
Visit Bandcamp
</a>
</div>
</div>
)}
{activeTab === 'youtube' && (
<div className="space-y-4">
<div className="aspect-video rounded-xl overflow-hidden">
<iframe
width="100%"
height="100%"
src="https://www.youtube.com/embed?listType=user_uploads&list=@parvagues"
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
/>
</div>
<div className="text-center">
<a
href="https://www.youtube.com/@parvagues/videos"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-red-500 to-red-600 text-white font-bold rounded-full hover:scale-105 transition-transform"
>
<FaYoutube className="w-5 h-5" />
Subscribe on YouTube
</a>
</div>
</div>
)}
{activeTab === 'deezer' && (
<div className="space-y-4">
<iframe
title="deezer-widget"
src="https://widget.deezer.com/widget/dark/artist/103670512/top_tracks"
width="100%"
height="450"
frameBorder="0"
allowTransparency
allow="encrypted-media; clipboard-write"
style={{ borderRadius: '12px' }}
/>
<div className="text-center">
<a
href="https://www.deezer.com/fr/artist/103670512"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-purple-500 to-pink-600 text-white font-bold rounded-full hover:scale-105 transition-transform"
>
<SiDeezer className="w-5 h-5" />
Listen on Deezer
</a>
</div>
</div>
)}
{activeTab === 'instagram' && (
<div className="space-y-4">
{!instagramConsent ? (
<div className="flex flex-col items-center justify-center py-16 text-center">
<FaInstagram className="w-16 h-16 text-purple-400 mb-4" />
<h3 className="text-2xl font-bold text-white mb-2">Privacy Notice</h3>
<p className="text-gray-400 mb-6 max-w-md">
Loading Instagram content will connect to Meta's servers and may track your activity.
</p>
<button
onClick={() => setInstagramConsent(true)}
className="px-8 py-4 bg-gradient-to-r from-pink-500 to-purple-600 text-white font-bold rounded-full hover:scale-105 transition-transform"
>
Load Instagram Feed
</button>
</div>
) : (
<div className="space-y-4">
<div className="text-center py-8">
<p className="text-gray-400 mb-4">
Follow @parvagues.mp3 for behind-the-scenes content and live updates
</p>
</div>
<div className="text-center">
<a
href="https://www.instagram.com/parvagues.mp3/"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-pink-500 to-purple-600 text-white font-bold rounded-full hover:scale-105 transition-transform"
>
<FaInstagram className="w-5 h-5" />
Follow on Instagram
</a>
</div>
</div>
)}
</div>
)}
</div>
</div>
);
}
import Link from 'next/link';
import { format } from 'date-fns';
import { fr } from 'date-fns/locale';
export default function TourTimeline({ lives }) {
const now = new Date();
// Group by year, sort desc
const grouped = {};
lives.forEach((live) => {
const d = new Date(live.date + 'T00:00:00');
const year = d.getFullYear();
if (!grouped[year]) grouped[year] = [];
grouped[year].push({ ...live, _date: d });
});
const years = Object.keys(grouped).sort((a, b) => b - a);
years.forEach((y) => {
grouped[y].sort((a, b) => b._date - a._date);
});
return (
<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>
<div style={{ height: '1px', background: 'rgba(255,255,255,0.1)', marginTop: '1rem', marginBottom: '3rem' }} />
{years.map((year) => (
<div key={year} style={{ marginBottom: '3rem' }}>
{/* Year */}
<h3
className="font-display font-extrabold select-none pointer-events-none"
style={{ fontSize: 'clamp(3rem, 10vw, 6rem)', color: 'rgba(255,255,255,0.04)', lineHeight: 1, marginBottom: '-0.5rem' }}
>
{year}
</h3>
{/* Events */}
<div>
{grouped[year].map((live) => {
const isFuture = live._date > now;
const hasMedia = live.audio || live.video || live.archive;
const city = live.location?.includes(',')
? live.location.split(',')[0].trim()
: live.location;
return (
<Link
key={live.slug}
href={`/parvagues/live/${live.slug}`}
className="group flex items-center gap-4 transition-colors duration-200 hover:bg-white/5"
style={{
padding: '0.5rem 0.75rem',
margin: '0 -0.75rem',
borderRadius: '0.5rem',
borderLeft: isFuture ? '2px solid rgba(217,0,255,0.5)' : '2px solid transparent',
}}
>
{/* Date */}
<span
className="font-mono flex-shrink-0"
style={{ fontSize: '11px', color: 'var(--text-muted)', width: '3.5rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}
>
{format(live._date, 'dd MMM', { locale: fr })}
</span>
{/* Title */}
<span
className="font-display font-semibold flex-grow min-w-0 truncate group-hover:text-white transition-colors duration-200"
style={{ fontSize: '0.875rem' }}
>
{live.title}
</span>
{/* City - hidden on mobile */}
<span
className="hidden sm:block flex-shrink-0"
style={{ fontSize: '11px', color: 'var(--text-muted)', letterSpacing: '0.05em' }}
>
{city}
</span>
{/* Media dot */}
{hasMedia && (
<span
className="flex-shrink-0 rounded-full"
style={{ width: '6px', height: '6px', backgroundColor: 'rgba(217,0,255,0.5)' }}
title="Enregistrement disponible"
/>
)}
</Link>
);
})}
</div>
</div>
))}
</div>
</section>
);
}
import { useState } from 'react';
import { FaPlay } from 'react-icons/fa';
const videos = [
{
id: 'toplap-fromscratch-dec2025-parvagues',
title: 'From Scratch: Jungle 🐅',
subtitle: 'TOPLAP Stream · Orléans',
date: 'Déc 2025',
},
{
id: 'toplap-solstice-dec2024-parvagues-',
title: 'Solstice Stream',
subtitle: 'TOPLAP · Les Carroz (Alps)',
date: 'Déc 2024',
},
{
id: 'toplap20-parvagues---z0rg',
title: '20 Years (w/ z0rg)',
subtitle: 'TOPLAP · Grand Paris',
date: 'Fév 2024',
},
{
id: 'latesolstice2023-parvagues',
title: 'Solstice Stream',
subtitle: 'TOPLAP · Grand Paris',
date: 'Déc 2023',
},
];
function VideoCard({ video }) {
const [loaded, setLoaded] = useState(false);
if (loaded) {
return (
<div>
<div className="aspect-video rounded-xl overflow-hidden bg-black">
<iframe
src={`https://archive.org/embed/${video.id}`}
width="100%"
height="100%"
allowFullScreen
className="w-full h-full"
/>
</div>
<div className="mt-3 flex items-start justify-between">
<div>
<h4 className="font-display font-semibold text-sm">{video.title}</h4>
<p className="text-[11px] text-[var(--text-muted)] mt-0.5">{video.subtitle} · {video.date}</p>
</div>
<button
onClick={() => setLoaded(false)}
className="text-[11px] text-[var(--text-muted)] hover:text-white transition-colors tracking-wider mt-1"
>
Fermer
</button>
</div>
</div>
);
}
return (
<div>
<button
onClick={() => setLoaded(true)}
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"
>
{/* 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>
</button>
<div className="mt-3">
<h4 className="font-display font-semibold text-sm">{video.title}</h4>
<p className="text-[11px] text-[var(--text-muted)] mt-0.5">{video.subtitle} · {video.date}</p>
</div>
</div>
);
}
export default function VideoSection() {
return (
<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>
<div className="h-px bg-white/10 mt-4 mb-12" />
<div className="grid sm:grid-cols-2 gap-8 md:gap-10">
{videos.map((video) => (
<VideoCard key={video.id} video={video} />
))}
</div>
</div>
</section>
);
}
---
title: "Live Algolia Fête de la Musique 2022"
date: "2022-06-21"
time: "18:00"
location: "Paris, France"
address: "Algolia HQ"
description: "Fête de la Musique at Algolia HQ."
ctaURL: ""
ctaText: ""
video: ""
audio: ""
archive: ""
tags: ["livecoding", "algolia", "fdlm"]
---
# Live Algolia — Fête de la Musique 2022
Fête de la Musique at Algolia HQ, Paris.
---
title: "Bazurto Live @ Tignes"
date: "2022-03-15"
time: "20:00"
location: "Tignes, France"
address: "Bazurto"
description: "Live set pour les Montagnettes. Apéritif to Digestif."
ctaURL: ""
ctaText: ""
video: ""
audio: ""
archive: ""
tags: ["livecoding", "tignes", "bazurto", "apres-ski"]
---
# Bazurto Live @ Tignes
Pour Montagnettes <3
## _Apéritif_
- _Invoque l'été_
## _Entrée_
- _Solar_
- **Lunar**
## _Accompagnement_
- *Michael*
- *Nightly Repair*
- *Contre visite*
## _Plat de résistance_
- _Burn this Book_
## _Dessert_
- _Break the Loop_
- _Atari-ght: techno retro gaming_
## _Dernier Verre_
- Alerte Verte
## __Digestif__
- It's About Time
---
title: "Live @ CMNY #2"
date: "2023-09-01"
time: "20:00"
location: "Paris, France"
address: "CMNY"
description: "Livecoding set at CMNY #2."
ctaURL: ""
ctaText: ""
video: ""
audio: ""
archive: ""
tags: ["livecoding", "cmny"]
---
# Live @ CMNY #2
## Tracklist
- Intro: We call it AlgoRave
- Solar / Lunar
- Contre Visite
- Empreinte du numérique
- CBOW ✨
- Nightly Repair
- Alerte Verte
- Invoque l'Été
---
title: "DevCon23 Performance"
date: "2023-11-15"
time: "20:00"
location: "Berlin, Germany"
address: "DevCon"
description: "Livecoding performance at DevCon23 Berlin."
ctaURL: ""
ctaText: ""
video: ""
audio: ""
archive: ""
tags: ["livecoding", "conference", "berlin"]
---
# DevCon23 Performance
## Tracklist
- SlowMo
- Lendemain Divin
- Invoque l'Été
- Sessions Break
- Nouveau Soleil
- Première Grillade
- Été à Mauerpark
- VelociTeuf (Ready for Mix / Takeoff)
---
title: "Live @ MephisTeuf"
date: "2023-10-01"
time: "23:00"
location: "Paris, France"
address: "MephisTeuf"
description: "Dark livecoding set Cette nuit t'appartient."
ctaURL: ""
ctaText: ""
video: ""
audio: ""
archive: ""
tags: ["livecoding", "mephisteuf", "dark"]
---
# Live @ MephisTeuf
Cette nuit t'appartient.
---
title: "TOPLAP SOLSTICE 23"
date: "2023-12-21"
time: "20:00"
location: "Online (Grand Paris)"
address: "TOPLAP Stream"
description: "Breaks&Beats TidalCycles+MIDI Cookie Collective winter solstice stream."
ctaURL: ""
ctaText: ""
video: "https://archive.org/details/latesolstice2023-parvagues"
audio: ""
archive: "https://archive.org/details/latesolstice2023-parvagues"
tags: ["livecoding", "toplap", "solstice", "stream", "cookie-collective"]
---
# TOPLAP SOLSTICE 23
Winter Solstice stream performance.
## Tracklist
- Permanence 🫶🫶🫶🫶
- Bain Bouillant 🤿 🤿
- Haunted House 👻 👻
- Salut nu 👋👋👋👋
---
title: "[CCC] Cookie Collective Compilation Release Party"
date: "2024-10-25"
time: "XX:XX"
location: "Lieu tenu secret"
address: "Paris, France"
description: ""
# ctaURL: "https://nech.pl/algorave-lyon"
# ctaText: "Plus d'infos"
# teasing1: |
# # AlgoRave preparation
# ```tidal
# d1 $ degradeBy 0.25 $ sound "jungle:45"
# d2 $ every 3 (fast 2) $ sound "cp"
# ```
# Set in preparation for Lyon
# teasing2: |
# # Week countdown...
# ```tidal
# d8 $ chop 16 $ loopAt 2 $ sound "jungle_breaks:45"
# ```
# Details coming soon
# teasing3: |
# # Final call
# ```tidal
# d1 $ stack [sound "cp", sound "ho:3"]
# ```
# Tonight @ Lyon
# video: ""
# audio: ""
# archive: ""
# tags: ["livecoding", "algorave", "lyon", "tidal"]
# ---
# # AlgoRave Lyon 2025
# Détails à venir...
---
title: "Algolia FDLM 2024"
date: "2024-06-21"
time: "18:00"
location: "Algolia, Paris"
address: "Algolia HQ"
description: "Fête de la Musique set at Algolia. A mix of throwbacks and new 2024 tracks."
ctaURL: ""
ctaText: ""
video: ""
audio: ""
archive: ""
tags: ["livecoding", "algolia", "fdlm", "fetedelamusique"]
---
# Algolia FDLM 2024
Fête de la Musique celebration at Algolia.
## Tracklist
### Intro
- *Lendemain Divin* 🛏️
### 2022 throwback 🕵️
- *Contre Visite*
- *Nightly Repair* 🌆
- *Invoque l'Été* 🌇
### 2024 new thingies
- Venons Ensemble 🧑‍🧑‍🧒
- JeuDrill 🌠
- PERMANENCE ♾️♾️
- Force motrice 🌬️🌬️🌬️🌬️
- *Café Tiède* ☕🥃☕
- *Café Bouillant* ☕☕☕
- *Café Glaçé* 🥃🥃🥃
- *Salut Nu* 👋💛🧡💛👋
- *Nuit agitée* 🌃🎆🌃🎆🌃🎆🌃
- **MauerPark** 🎻🌇🌄🌇🌄🌇🥨
---
title: "Algolia Last All Hands"
date: "2024-12-15"
time: "18:00"
location: "Paris, France"
address: "Algolia"
description: "Closing set for Algolia's last all-hands."
ctaURL: ""
ctaText: ""
video: ""
audio: ""
archive: ""
tags: ["livecoding", "algolia", "paris"]
---
# Algolia — Last All Hands
## Tracklist
- Blue Gold
- Something about Drums
- Sunny Side Up
- Café Bouillant
- Café Tiède
- Force Motrice
- Love First
- Contre Visite
- Nuit Agitée
- Invoque l'Été
- Permanence
---
title: "Bazurto Live @ Tignes"
date: "2024-03-15"
time: "20:00"
location: "Tignes, France"
address: "Montagnettes"
description: "Live set pour les Montagnettes Apéritif to Digestif."
ctaURL: ""
ctaText: ""
video: ""
audio: ""
archive: ""
tags: ["livecoding", "montagne", "tignes"]
---
# Bazurto — Live @ Tignes
Pour Montagnettes <3
## Setlist
- Apéritif — Invoque l'Été
- Entrée — Solar / Lunar
- Accompagnement — Michael / Nightly Repair / Contre Visite
- Plat de résistance — Burn this Book
- Dessert — Break the Loop / Atari-ght
- Dernier Verre — Alerte Verte
- Digestif — It's About Time
---
title: "CCC LIVE - Cookie Collective"
date: "2024-10-01"
time: "20:00"
location: "Online"
address: "Cookie Collective Stream"
description: "Continuous electronic livecoding set for Cookie Collective."
ctaURL: "https://soundcloud.com/parvagues/ccc-live"
ctaText: "Écouter le set"
video: ""
audio: "https://soundcloud.com/parvagues/ccc-live"
archive: ""
tags: ["livecoding", "ccc", "cookiecollective", "stream"]
---
# CCC LIVE
Continuous electronic livecoding set for Cookie Collective.
```text
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡴⠚⣉⡙⠲⠦⠤⠤⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⢀⣴⠛⠉⠉⠀⣾⣷⣿⡆⠀⠀⠀⠐⠛⠿⢟⡲⢦⡀⠀⠀⠀⠀
⠀⠀⠀⠀⣠⢞⣭⠎⠀⠀⠀⠀⠘⠛⠛⠀⠀⢀⡀⠀⠀⠀⠀⠈⠓⠿⣄⠀⠀⠀
⠀⠀⠀⡜⣱⠋⠀⠀⣠⣤⢄⠀⠀⠀⠀⠀⠀⣿⡟⣆⠀⠀⠀⠀⠀⠀⠻⢷⡄⠀
⠀⢀⣜⠜⠁⠀⠀⠀⢿⣿⣷⣵⠀⠀⠀⠀⠀⠿⠿⠿⠀⠀⣴⣶⣦⡀⠀⠰⣹⡆
⢀⡞⠆⠀⣀⡀⠀⠀⠘⠛⠉⠁COOKIE ⢿⣿⣶⠇⠀⢠⢻⡇ ⣸
⢸⠃⠘⣾⣏⡇⠀⠀⠀⠀COLL⡀ECTIVE⠀⣠⣤⣤⡉⠁⠀⠀⠈⠫⣧
⡸⡄⠀⠘⠟⠀⠀⠀⠀⠀⠀⣰⣿⣟⢧⠀⠀⠀⠀⠰⡿⣿⣿⢿⠀⠀⣰⣷⢡⢸
⣿⡇⠀⠀⠀⣰⣿⡻⡆COM⠻⣿⣿⣟PILA⠉⠉⠉TION⠘⢿⡿⣸⡞
⠹⣽⣤⣤⣤⣹⣿⡿⠇⠀⠀⠀⠀⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡔⣸⣽⠀
⠀⠙⢻⡙⠟⣹⠟⢷⣶⣄⢀⣴⣶⣄ParVagues⣤⡦⣄⠀⠀⢠⣾⠏⠀
⠀⠀⠘⠀⠀⠀⣤⠈⢷⢼⣿⡿⡽⠀.. LIVE⠀⠸⣿⣿⣾⣼⡿⣣⠟⠀⠀
⠀⠀⠀⠙⢻⡙⣿⣤⢠⡾⣆⠑⠋⠀⢀⣀⠀CODING⠈⢁⣴⢫⡿⠁⠀⠀
⠀⠀⠀⠀⠀⠀.⣿⠈⠙⣧⣄⡄. ⠴⣿.⣶⣿⢀.⣤⠶⣞⣋⣩⣵⠏⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠙⢻⣿⡙⣿⢯⣭⣭⣯⣯⣥⡵⠿⠟⠛⠉⠉⠀⠀⠀⠀⠀⠀⠀
```
## Tracklist
- L'ACID d'abord <3 (outro only)
- Alerte Verte
- Blue Gold 🌇
- Nuit Agitee 🌃
- Nass Revient de Mars!
- Atelier de Force Motrice
- Cafe Tiede
- Cafe Glace
### ENCORE <3
- Salut Nu
---
title: "Cookie Collective Compilation Release Party"
date: "2024-10-25"
time: "20:00"
location: "Paris, France"
address: "Secret Location"
description: "Release party for the Cookie Collective Compilation."
ctaURL: ""
ctaText: ""
video: ""
audio: ""
archive: ""
tags: ["livecoding", "ccc", "releaseparty", "compilation"]
---
# Cookie Collective Compilation Release Party
Release party for the Cookie Collective Compilation.
```text
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡴⠚⣉⡙⠲⠦⠤⠤⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⢀⣴⠛⠉⠉⠀⣾⣷⣿⡆⠀⠀⠀⠐⠛⠿⢟⡲⢦⡀⠀⠀⠀⠀
⠀⠀⠀⠀⣠⢞⣭⠎⠀⠀⠀⠀⠘⠛⠛⠀⠀⢀⡀⠀⠀⠀⠀⠈⠓⠿⣄⠀⠀⠀
⠀⠀⠀⡜⣱⠋⠀⠀⣠⣤⢄⠀⠀⠀⠀⠀⠀⣿⡟⣆⠀⠀⠀⠀⠀⠀⠻⢷⡄⠀
⠀⢀⣜⠜⠁⠀⠀⠀⢿⣿⣷⣵⠀⠀⠀⠀⠀⠿⠿⠿⠀⠀⣴⣶⣦⡀⠀⠰⣹⡆
⢀⡞⠆⠀⣀⡀⠀⠀⠘⠛⠉⠁COOKIE ⢿⣿⣶⠇⠀⢠⢻⡇ ⣸
⢸⠃⠘⣾⣏⡇⠀⠀⠀⠀COLL⡀ECTIVE⠀⣠⣤⣤⡉⠁⠀⠀⠈⠫⣧
⡸⡄⠀⠘⠟⠀⠀⠀⠀⠀⠀⣰⣿⣟⢧⠀⠀⠀⠀⠰⡿⣿⣿⢿⠀⠀⣰⣷⢡⢸
⣿⡇⠀⠀⠀⣰⣿⡻⡆COM⠻⣿⣿⣟PILA⠉⠉⠉TION⠘⢿⡿⣸⡞
⠹⣽⣤⣤⣤⣹⣿⡿⠇⠀⠀⠀⠀⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡔⣸⣽⠀
⠀⠙⢻⡙⠟⣹⠟⢷⣶⣄⢀⣴⣶⣄ParVagues⣤⡦⣄⠀⠀⢠⣾⠏⠀
⠀⠀⠘⠀⠀⠀⣤⠈⢷⢼⣿⡿⡽⠀ LIVE⠀⠸⣿⣿⣾⣼⡿⣣⠟⠀⠀
⠀⠀⠀⠙⢻⡙⣿⣤⢠⡾⣆⠑⠋⠀⢀⣀⠀CODING⠈⢁⣴⢫⡿⠁⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⣿⠈⠙⣧⣄⡄⠴⣿⣶⣿⢀⣤⠶⣞⣋⣩⣵⠏⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠙⢻⣿⡙⣿⢯⣭⣭⣯⣯⣥⡵⠿⠟⠛⠉⠉⠀⠀⠀⠀⠀⠀⠀
```
## Tracklist
- 03 L'ACID d'abord <3
- 06 Alerte Verte
- 09 Blue Gold 🌇
- 13 Nuit Agitee 🌃
- 16 Nass Revient de Mars!
- 20 Force Motrice
- 23 Cafe Tiede
- 26 Cafe Glace
### ENCORE <3
- ?? Salut Nu
- ?? Fabuleux ✨
---
title: "Divin Live"
date: "2024-06-09"
time: "20:00"
location: "Paris, France"
address: "Divin"
description: "Livecoding rituel du Paradis perdu au Rite Final."
ctaURL: ""
ctaText: ""
video: ""
audio: ""
archive: ""
tags: ["livecoding", "rituel", "paris"]
---
# Divin Live
## Setlist
- Accueil : Green Land
- Paradis perdu : Break Dynasty
- Vénération : Venons Ensemble
- Idoles : Nouveau Soleil
- (Pluie) : Contre Visite
- Nuit : Atari-ght
- Bouquet : It's About Time
- Rite Final : Été à MauerPark
- Bonus : Quart d'heure de politesse — Invoque l'Été / Alerte Verte
---
title: "La French Stack"
date: "2024-09-20"
time: "19:00"
location: "Paris, France"
address: "La French Stack"
description: "Livecoding set for La French Stack collective."
ctaURL: ""
ctaText: ""
video: ""
audio: ""
archive: ""
tags: ["livecoding", "collectif", "paris"]
---
# La French Stack <3
## Tracklist
- Contre Visite
- Bain Électrique
- Salut Nu
- Café Tiède
- Force Motrice
- Sunny Side Up
- Something about Drums
- Permanence
- L'Or Bleu
---
title: "Live @ Toi Toi Mon Toit"
date: "2024-05-10"
time: "19:00"
location: "Paris, France"
address: "Toi Toi Mon Toit"
description: "Rooftop livecoding set."
ctaURL: ""
ctaText: ""
video: ""
audio: ""
archive: ""
tags: ["livecoding", "rooftop", "paris"]
---
# Live @ Toi Toi Mon Toit
## Tracklist
- Première Grillade
- Break Dynasty
- Clameur
- Nouveau Soleil
- It's About Time ❤️
---
title: "TOPLAP 20 Years (w/ z0rg)"
date: "2024-02-24"
time: "09:45"
location: "Grand Paris, France"
address: "TOPLAP Stream"
description: "20 years of TOPLAP 20 breaks, 4 beats. Cookie Collective celebration."
ctaURL: ""
ctaText: ""
video: "https://archive.org/details/toplap20-parvagues---z0rg"
audio: ""
archive: "https://archive.org/details/toplap20-parvagues---z0rg"
tags: ["livecoding", "toplap", "cookie-collective", "collab"]
---
# TOPLAP 20 Years
20 breaks, 4 beats — celebrating 20 years of creativity with z0rg.
Cookie Collective performance.
## Tracklist
- Café Tiède
- Force Motrice
- Café Glacé
- Nuit Agitée
- Permanence
---
title: "TOPLAP Solstice 2024"
date: "2024-12-21"
time: "20:30"
location: "Les Carroz d'Arâches, France"
address: "TOPLAP Stream (from the Alps)"
description: "Breakbeat to techno and more ALL I WANT FOR CHRISTMAS IS LIVECODE. Cookie Collective."
ctaURL: ""
ctaText: ""
video: "https://archive.org/details/toplap-solstice-dec2024-parvagues-"
audio: ""
archive: "https://archive.org/details/toplap-solstice-dec2024-parvagues-"
tags: ["livecoding", "toplap", "solstice", "cookie-collective", "alps"]
---
# TOPLAP Solstice 2024 🎄
ALL I WANT FOR CHRISTMAS IS LIVECODE
Breakbeat to techno and more — performed live from the Alps, Les Carroz d'Arâches.
A Cookie Collective performance.
---
title: "VelociTeuf"
date: "2024-07-05"
time: "22:00"
location: "Paris, France"
address: "VelociTeuf"
description: "Livecoding teuf set."
ctaURL: ""
ctaText: ""
video: ""
audio: ""
archive: ""
tags: ["livecoding", "teuf", "paris"]
---
# VelociTeuf
## Tracklist
- Prestance
- Oct4 Glitch Sauvages
- About Time
- Toxic
- Venons Ensemble
- Reboot
- Première Grillade
- Rainy Day
- Contre Visite
---
title: "Air 2025 ELEMENTEUF"
date: "2025-06-15"
time: "14:00"
location: "Paris, France"
address: "PSC ELEMENTEUF"
description: "Open air set for ELEMENTEUF."
ctaURL: ""
ctaText: ""
video: ""
audio: ""
archive: ""
tags: ["livecoding", "air", "elementeuf", "openair"]
---
# Air 2025 ELEMENTEUF
PSC ELEMENTEUF
## Tracklist
1. **Salut Nu**
2. Sunny Side Up
3. Cafe Bouillant
4. Cafe Tiede
5. **Cafe glace**
6. Septembre 1er
---
title: "Algolia RKO 2025"
date: "2025-01-10"
time: "18:00"
location: "Paris, France"
address: "Algolia"
description: "Dernier rendez-vous RKO."
ctaURL: ""
ctaText: ""
video: ""
audio: ""
archive: ""
tags: ["livecoding", "algolia", "paris"]
---
# Algolia — RKO 2025
## Tracklist
- Café Bouillant
- Café Tiède
- Sunny Side Up
- Something about Drums
- Love First
- Contre Visite
- Nuit Agitée
- Invoque l'Été
- Permanence
---
title: "BUNKER"
date: "2025-10-31"
time: "23:00"
location: "Secret Location"
address: "Bunker"
description: "Underground set."
ctaURL: ""
ctaText: ""
video: ""
audio: ""
archive: ""
tags: ["livecoding", "bunker", "underground", "techno"]
---
# BUNKER
## Tracklist
- Sept1
- Sunny
- Orage
- Force Motrice
- Drifting soul
- Because it's there
- Ere de Jeu
- Acidule
---
title: "FairyTeuf"
date: "2025-08-20"
time: "22:00"
location: "Fairy Land"
address: "FairyTeuf"
description: "Magical set."
ctaURL: ""
ctaText: ""
video: ""
audio: ""
archive: ""
tags: ["livecoding", "fairy", "magic", "party"]
---
# FairyTeuf ✨✨✨
## INTRO FÉÉRIE DU VOYAGE
- JeuDrill
- Sunny Side Up
- **Savoir Voyager**
- Menthe Givrée [s]
## COEUR FÉÉRIE NOCTURNE
- Ouais je funk
- WAP
- Long Way
- Esperluette
- Piment Bresilien
- Biscuit Acide
- Des Efforts!
- AUXiliaire
---
title: "La French Stack"
date: "2025-05-20"
time: "19:00"
location: "Paris, France"
address: "La French Stack"
description: "Livecoding set for La French Stack event."
ctaURL: ""
ctaText: ""
video: ""
audio: ""
archive: ""
tags: ["livecoding", "lafrenchstack", "tech", "paris"]
---
# La French Stack <3
ParVagues Livecoding
## TRACKLIST
- Contre Visite
- Bain Electrique
- Salut Nu
- Cafe Tiede
- Force Motrice
- Sunny Side Up
- Venons Ensemble
- Something about Drums <3
- Permanence.
- L'or Bleu
---
title: "LABENNE LIVE"
date: "2025-07-15"
time: "21:00"
location: "Labenne, France"
address: "Labenne"
description: "Summer live set in Labenne."
ctaURL: ""
ctaText: ""
video: ""
audio: ""
archive: ""
tags: ["livecoding", "labenne", "summer", "techno"]
---
# LABENNE LIVE
## 🌅 Intro: Quel Bonheur 🌅
- 🛩️ Quand on Décolle
- ✨ Fabuleux <3
## 🥁 Les Drums ........ 🥁
- 🪸 Something About Drums
- 👻 Ghosts in the Toilets
## ⛈️ La Tempête ......... ⛈️
- 🌩️ **TechnOrage**
- 🔌 Bain Electrique
- L'Insouciance
- 🌠 **Venons Ensemble**..
- 🌌 Blue Gold
- Sunny Side Up
- Sept1
- 💃 Lady Perplexity
## Outro: Nuit Ambiante
- JeuDrill
- Ambient Cha0s??
- La C.r.e.m.e
---
title: "RAISE AFTERPARTY"
date: "2025-11-15"
time: "01:00"
location: "Paris, France"
address: "Raise Summit Afterparty"
description: "Afterparty set for Raise Summit."
ctaURL: ""
ctaText: ""
video: ""
audio: ""
archive: ""
tags: ["livecoding", "raise", "afterparty", "techno"]
---
# RAISE AFTERPARTY
## Son.
00. RAISE (quand on decolle) [60->120]
01. 🪘 Something About Drums [80->160]
02. 🍳 Sunny side up [120] 11 12
03. 🌇 Sept1 [60<>180]
04. ⛈️ Orage [104]
05. 👻 La Fin de l'Insouciance [120]
06. 🔌 Bain électrique [128]
07. 🍺 Jeudi Drill [140]
08. 🐩 Punkachien [170]
10. 🪅 Nuit Agitéee [160]
12. 🌆 L'or Bleu [124]
13. Lady Perplexity
14. 🥤 Café Glacé [120]
15. Café tiède [125]
16. Atelier de Force Motrice [125]
17. Salut Nu [120]
18. Ton Numero [99bpm]
---
title: "TOPLAP From Scratch Jungle 🐅"
date: "2025-12-07"
time: "21:15"
location: "Orléans, France"
address: "TOPLAP Stream"
description: "A scratch in the Jungle 🐅 starting from jungle_breaks. Cookie Collective."
ctaURL: ""
ctaText: ""
video: "https://archive.org/details/toplap-fromscratch-dec2025-parvagues"
audio: ""
archive: "https://archive.org/details/toplap-fromscratch-dec2025-parvagues"
tags: ["livecoding", "toplap", "fromscratch", "jungle", "cookie-collective"]
---
# TOPLAP From Scratch — Jungle 🐅
A scratch in the Jungle — starting point: `jungle_breaks`.
Live from Orléans, December 2025.
---
title: "Val Thorens 2025"
date: "2025-02-15"
time: "20:00"
location: "Val Thorens, France"
address: "Val Thorens"
description: "Live set at Val Thorens. rdy2shred."
ctaURL: ""
ctaText: ""
video: ""
audio: ""
archive: ""
tags: ["livecoding", "valthorens", "ski", "techno"]
---
# Val Thorens 2025
rdy2shred
## TRACKLIST
### Set1
- *Because it's there* (CHLOE Algo cover)
- **Ere de jeu**
- Cafe glace
- Cafe Bouillant
- Sept1 (remix)
- Sunny side Up
### Set 2
- 🍳 Sunny side Up
- 🪂 Insouciance
- 🤍 So Good
- PunkAChien
- ACIDULE
- Alerte Verte
- Nouveau Punk
- Nuit agitee
- Blue Gold
---
title: "Été Surprise"
date: "2026-03-07"
time: ""
location: "Villejuif, France"
address: "PSC, Villejuif"
description: "Été Surprise at PSC Villejuif."
ctaURL: ""
ctaText: ""
video: ""
audio: ""
archive: ""
tags: ["livecoding", "villejuif"]
---
# Été Surprise
PSC, Villejuif.
---
title: "Live @ Le Vortex"
date: "2026-01-04"
time: ""
location: "Paris, France"
address: "Le Vortex"
description: "Live set at Le Vortex, Paris."
ctaURL: ""
ctaText: ""
video: ""
audio: ""
archive: ""
tags: ["livecoding", "paris"]
---
# Live @ Le Vortex
Paris, January 2026.
......@@ -25,10 +25,10 @@
"classnames": "^2.5.1",
"d3-force": "^3.0.0",
"d3-zoom": "^3.0.0",
"date-fns": "^3.3.1",
"date-fns": "^3.6.0",
"gray-matter": "^4.0.3",
"hydra-synth": "^1.3.29",
"marked": "^15.0.11",
"marked": "^15.0.12",
"minisearch": "^7.1.2",
"next": "^15.3.0",
"prismjs": "^1.30.0",
......@@ -39,6 +39,7 @@
"react-icons": "^5.5.0",
"react-instantsearch": "^7.15.7",
"react-instantsearch-dom": "^6.40.4",
"react-markdown": "^10.1.0",
"react-masonry-css": "^1.0.16",
"react-player": "^2.14.1",
"react-syntax-highlighter": "^15.5.0",
......@@ -50,6 +51,7 @@
},
"devDependencies": {
"@playwright/test": "^1.55.0",
"@tailwindcss/postcss": "^4.1.17",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
......@@ -57,10 +59,14 @@
"@types/jest": "^30.0.0",
"@types/node": "24.4.0",
"@types/react": "^18.2.61",
"autoprefixer": "^10.4.22",
"jest": "^30.1.3",
"jest-environment-jsdom": "^30.1.2",
"jest-pnp-resolver": "^1.2.3",
"next-router-mock": "^1.0.2",
"playwright": "^1.58.2",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.17",
"typescript": "^5.3.3",
"vercel": "^39"
},
......
import Head from 'next/head';
import Link from 'next/link';
import Image from 'next/image';
import { useState, useEffect, useRef } from 'react';
import { getAllLives } from '@/lib/livesData';
import styles from '@/styles/parvagues.module.css';
import dynamic from 'next/dynamic';
import ParVaguesHeader from '@/components/ParVaguesHeader';
import ParVaguesFooter from '@/components/ParVaguesFooter';
import GlitchText from '@/components/GlitchText';
import SyntaxHighlighter from "react-syntax-highlighter";
import { atomOneDark } from "react-syntax-highlighter/dist/cjs/styles/hljs";
// React Icons imports
import { FaSpotify, FaDeezer, FaYoutube, FaApple, FaAmazon, FaInstagram, FaTwitter, FaEnvelope } from 'react-icons/fa';
import { SiTidal, SiBluesky, SiMastodon } from 'react-icons/si';
import { MdPlayArrow, MdPause } from 'react-icons/md';
function CodeBlock({ children, height = '400px', isTerminal = false }) {
return (
<div className={`${styles.codeContainer} ${isTerminal ? styles.terminalContainer : ''}`} style={{ maxHeight: height }}>
{isTerminal && (
<div className={styles.terminalHeader}>
<div className={styles.terminalControls}>
<span className={styles.redCircle}></span>
<span className={styles.yellowCircle}></span>
<span className={styles.greenCircle}></span>
</div>
<div className={styles.terminalTitle}>ParVagues@tidal:~</div>
</div>
)}
<SyntaxHighlighter
language="haskell"
style={atomOneDark}
wrapLongLines={true}
customStyle={{
margin: 0,
borderRadius: isTerminal ? '0 0 8px 8px' : '8px',
height: 'auto',
maxHeight: isTerminal ? `calc(${height} - 30px)` : height,
fontSize: '0.85rem'
}}
>
{children}
</SyntaxHighlighter>
</div>
);
}
// Define all posters and their positions
const posterImages = [
'/images/parvagues/lives/2022/Bazurto/poster.jpg',
'/images/parvagues/lives/2022/OPERATE/poster.png',
'/images/parvagues/lives/2024/ccc_release_party/poster.png',
'/images/parvagues/lives/2025/algorave-lyon/poster.jpeg',
'/images/parvagues/lives/2025/ensad/poster.png',
];
import Layout from '@/components/parvagues/Layout';
import Hero from '@/components/parvagues/Hero';
import TourTimeline from '@/components/parvagues/TourTimeline';
import MusicSection from '@/components/parvagues/MusicSection';
import VideoSection from '@/components/parvagues/VideoSection';
import BookingForm from '@/components/parvagues/BookingForm';
export default function ParVagues({ lives }) {
const [tidalCode, setTidalCode] = useState('');
const [showPlayers, setShowPlayers] = useState({});
const [isPlaying, setIsPlaying] = useState(false);
const [selectedSection, setSelectedSection] = useState('potentiel');
const [sectionImages, setSectionImages] = useState([]);
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const [currentAlbumIndex, setCurrentAlbumIndex] = useState(0);
const backgroundRef = useRef(null);
const audioRef = useRef(null);
// Filter future events and sort them by date in ascending order
const futureEvents = lives.filter(live => {
return new Date(live.date) > new Date();
}).sort((a, b) => new Date(a.date) - new Date(b.date));
// Define section content
const sections = {
potentiel: {
type: 'carousel',
images: [
'/images/parvagues/samples_crop.png',
// '/images/parvagues/code.png',
]
},
composition: {
type: 'carousel',
images: [
'/images/parvagues/gear1_crop.jpg',
]
},
performance: {
type: 'carousel',
images: [
// '/images/parvagues/live.jpg',
'/images/parvagues/hands.jpg'
]
}
};
// Fetch Tidal code with proxy or fallback
useEffect(() => {
// Use fallback code since CORS is blocking
const code = `do
setcps (120/60/4) -- 120 BPM
d1 $ "k . k(<3!3 5>,8)" . "jazz" -- Kick chaloupé
d2 $ "~ s ~ s*<1 2>" # "snare:42" -- Snare régulier
d3 $ whenmod 8 6 (degradeBy 0.2)
$ fast "<1 1 2 <1 2>>"
$ "dr*[8 16]" # "h2ogmhh:2" -- Drumroll
d4 $ note ("<e3 fs3 <gs3 d4> <a3 df4>>" - 12)
# "bassWarsaw" -- BASSLINE`;
setTidalCode(code);
}, []);
// Update section images based on selection
useEffect(() => {
const sectionContent = sections[selectedSection];
if (sectionContent && sectionContent.type === 'carousel') {
setSectionImages(sectionContent.images);
setCurrentImageIndex(0);
}
}, [selectedSection]);
// Auto-toggling for sections (Potentiel, Composition, Performance)
const sectionOrder = ['potentiel', 'composition', 'performance'];
useEffect(() => {
const intervalId = setInterval(() => {
setSelectedSection(currentSection => {
const currentIndex = sectionOrder.indexOf(currentSection);
const nextIndex = (currentIndex + 1) % sectionOrder.length;
return sectionOrder[nextIndex];
});
}, 5000); // 5 seconds
return () => clearInterval(intervalId); // Cleanup on component unmount or when selectedSection changes
}, [selectedSection]); // Re-run effect (and reset timer) when selectedSection changes
// Auto-advance carousel for section images
useEffect(() => {
if (sections[selectedSection]?.images?.length > 1) {
const interval = setInterval(() => {
setCurrentImageIndex(prev => (prev + 1) % sections[selectedSection].images.length);
}, 2500);
return () => clearInterval(interval);
}
}, [selectedSection, sections]);
// Carousel navigation functions
const nextSectionImage = () => {
const imagesArray = sections[selectedSection].images;
setCurrentImageIndex(prev => (prev + 1) % imagesArray.length);
};
const prevSectionImage = () => {
const imagesArray = sections[selectedSection].images;
setCurrentImageIndex(prev => (prev === 0 ? imagesArray.length - 1 : prev - 1));
};
// Carousel effect for albums
useEffect(() => {
if (albums.length > 1) {
const interval = setInterval(() => {
setCurrentAlbumIndex(prev => (prev + 1) % albums.length);
}, 5000);
return () => clearInterval(interval);
}
}, []);
const scrollToSection = (e) => {
e.preventDefault();
const section = document.getElementById('section1');
section?.scrollIntoView({ behavior: 'smooth' });
};
const togglePlayer = (albumId) => {
setShowPlayers(prev => ({
...prev,
[albumId]: !prev[albumId]
}));
};
const toggleAudio = () => {
if (audioRef.current) {
if (isPlaying) {
audioRef.current.pause();
} else {
audioRef.current.play();
}
setIsPlaying(!isPlaying);
}
};
// Define the desired order of platforms
const platformOrder = ['YouTube', 'Deezer', 'Spotify', 'Apple', 'Tidal', 'Amazon'];
const albumsData = [
{
id: '2024_opal',
title: 'Livecoding (Opal Festival 2024)',
image: '/images/parvagues/albums/2024_opal/cover.jpg',
links: [
{ platform: 'YouTube', url: 'https://www.youtube.com/playlist?list=OLAK5uy_l4MF3OCIXcdPMpsHGVX2Q9MiX6oU1zT6g', icon: <FaYoutube /> },
{ platform: 'Deezer', url: 'https://www.deezer.com/us/album/656760591', icon: <FaDeezer /> },
{ platform: 'Spotify', url: 'https://open.spotify.com/album/1VKLZWeolFNfES2bWzYCWZ', icon: <FaSpotify /> },
{ platform: 'Apple', url: 'https://music.apple.com/fr/album/livecoding-opal-festival-2024/1773790990', icon: <FaApple /> },
{ platform: 'Tidal', url: 'https://listen.tidal.com/album/393127518', icon: <SiTidal /> },
{ platform: 'Amazon', url: 'https://amazon.com/dp/B0DK298L1X', icon: <FaAmazon /> }
]
},
{
id: '2023_connexion',
title: 'Connexion Etablie EP',
image: '/images/parvagues/albums/2023_connexion/cover.jpg',
links: [
{ platform: 'YouTube', url: 'https://www.youtube.com/watch?v=VODSdQKrzyw&list=OLAK5uy_nzlx3b7YJYzrbagXF5swhENsCg5vJkT_Q', icon: <FaYoutube /> },
{ platform: 'Spotify', url: 'https://open.spotify.com/album/4uzSN6Uv9IwcYeHdRtkUmM', icon: <FaSpotify /> },
{ platform: 'Deezer', url: 'https://www.deezer.com/album/498443581', icon: <FaDeezer /> },
{ platform: 'Apple', url: 'https://music.apple.com/fr/album/_/1711226283', icon: <FaApple /> },
{ platform: 'Amazon', url: 'https://music.amazon.com/albums/B0CKTZMFDF', icon: <FaAmazon /> }
]
}
];
// Sort the links for each album
const albums = albumsData.map(album => ({
...album,
links: album.links.sort((a, b) => {
const indexA = platformOrder.indexOf(a.platform);
const indexB = platformOrder.indexOf(b.platform);
// If a platform is not in platformOrder, keep its relative order towards the end
if (indexA === -1) return 1;
if (indexB === -1) return -1;
return indexA - indexB;
})
}));
const renderSectionContent = () => {
const sectionContent = sections[selectedSection];
if (!sectionContent) return null;
return (
<div className="w-full flex flex-col items-center">
{/* Image Frame: 4/3 aspect ratio, max-width 50vw */}
<div className="w-full max-w-[50vw] aspect-[4/3] bg-black rounded-lg overflow-hidden shadow-xl mb-4">
<img
src={sectionContent.images[currentImageIndex]}
alt={selectedSection}
className="w-full h-full object-cover"
/>
</div>
{sectionContent.images.length > 1 && (
<div className="flex items-center">
<button
onClick={prevSectionImage}
className="bg-black/30 hover:bg-black/50 p-2 rounded-full transition-colors mx-2"
aria-label="Image précédente"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
<button
onClick={nextSectionImage}
className="bg-black/30 hover:bg-black/50 p-2 rounded-full transition-colors mx-2"
aria-label="Image suivante"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
)}
</div>
);
};
return (
<>
<Head>
<title>ParVagues - Musique Algorithmique</title>
<meta name="description" content="Livecoding de musique open-source avec TidalCycles et contrôleur MIDI" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/images/parvagues/favicon.ico" />
</Head>
<div className="flex flex-col min-h-screen bg-black text-white">
<ParVaguesHeader />
{/* Main content flex-auto to push footer to bottom */}
<main className="flex-auto">
{/* Hero Section */}
<section className={`${styles.heroSection} mt-0`}>
<div className={styles.posterCollage} ref={backgroundRef}>
{posterImages.map((src, i) => (
<img
key={i}
src={src}
alt={`Poster ${i + 1}`}
fill
className={styles.posterImage}
priority={i < 3}
/>
))}
</div>
{/* Audio element */}
<audio
ref={audioRef}
src="/parvagues.mp3"
loop
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
/>
<div className={styles.contentOverlay}>
<div className={styles.heroContent}>
{/* Left side: Title and content */}
<div className="md:w-1/2">
<h1 className={styles.heroTitle}>
<GlitchText
text="ParVagues"
className={styles.glitchEffect}
burstFrequency={4500}
/>
</h1>
<p className={styles.heroSubtitle}>
Livecoding de musique open-source avec TidalCycles et contrôleur MIDI
</p>
<div className="flex flex-col sm:flex-row gap-4 mt-6">
<a href="#section1" onClick={scrollToSection} className={styles.plungeButton}>
Plonger
</a>
{futureEvents.length > 0 && (
<Link href="#performances" className={`${styles.outlineButton} group`}>
<span>LIVE</span>
<div className="absolute inset-x-0 bottom-0 h-0.5 bg-gradient-to-r from-purple-500 to-pink-500 transform scale-x-0 group-hover:scale-x-100 transition-transform origin-left"></div>
</Link>
)}
</div>
</div>
{/* Right side: Code sample with play overlay */}
<div className="md:w-1/2 relative">
<div className="relative">
<CodeBlock className="h-full" isTerminal={true}>
{tidalCode}
</CodeBlock>
{/* Pretty Play overlay */}
<button
onClick={toggleAudio}
className="absolute inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center transition-all duration-300 hover:bg-black/80 group"
>
<div className="bg-gradient-to-br from-purple-500 to-pink-500 rounded-full p-8 shadow-xl shadow-purple-500/50 transition-all duration-300 group-hover:scale-110 group-hover:shadow-2xl group-hover:shadow-purple-500/30">
{isPlaying ? (
<MdPause className="w-16 h-16 text-white" />
) : (
<MdPlayArrow className="w-16 h-16 text-white" />
)}
</div>
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 text-sm text-purple-300 opacity-0 group-hover:opacity-100 transition-opacity">
{isPlaying ? 'Pause' : 'Écouter le mix'}
</div>
</button>
</div>
</div>
</div>
</div>
</section>
{/* Section 1: Interactive Sections */}
<section id="section1" className={styles.sectionContainer}>
<div className={styles.splitSection}>
<div>
<div
className={`cursor-pointer transition-all duration-300 hover:bg-purple-500/10 rounded-lg p-2 ${selectedSection === 'potentiel' ? 'text-purple-400 border-l-2 border-purple-400 pl-4' : 'text-gray-400 border-l-2 border-transparent'}`}
onClick={() => setSelectedSection('potentiel')}
>
<h3 className={`${styles.bulletPoint} text-xl font-semibold mb-2 group relative inline-block ${selectedSection === 'potentiel' ? 'text-purple-400' : 'text-gray-400'}`} style={{ margin: '1em 0', textDecorationLine: 'underline', textDecorationColor: 'darkviolet', textDecorationThickness: '3px' }}>
Potentiel
<span className="absolute bottom-0 left-0 w-0 h-0.5 bg-gradient-to-r from-purple-400 to-pink-500 group-hover:w-full transition-all duration-300"></span>
</h3>
<p className={`${selectedSection === 'potentiel' ? 'text-gray-300' : 'text-gray-500'}`}>Samples glanés et synthés SuperCollider</p>
</div>
<div
className={`cursor-pointer transition-all duration-300 hover:bg-purple-500/10 rounded-lg p-2 ${selectedSection === 'composition' ? 'text-purple-400 border-l-2 border-purple-400 pl-4' : 'text-gray-400 border-l-2 border-transparent'}`}
onClick={() => setSelectedSection('composition')}
>
<h3 className={`${styles.bulletPoint} text-xl font-semibold mb-2 group relative inline-block ${selectedSection === 'composition' ? 'text-purple-400' : 'text-gray-400'}`} style={{ margin: '1em 0', textDecorationLine: 'underline', textDecorationColor: 'darkviolet', textDecorationThickness: '3px' }}>
Composition
<span className="absolute bottom-0 left-0 w-0 h-0.5 bg-gradient-to-r from-purple-400 to-pink-500 group-hover:w-full transition-all duration-300"></span>
</h3>
<p className={`${selectedSection === 'composition' ? 'text-gray-300' : 'text-gray-500'}`}>Code Haskell TidalCycles + input MIDI</p>
</div>
<div
className={`cursor-pointer transition-all duration-300 hover:bg-purple-500/10 rounded-lg p-2 ${selectedSection === 'performance' ? 'text-purple-400 border-l-2 border-purple-400 pl-4' : 'text-gray-400 border-l-2 border-transparent'}`}
onClick={() => setSelectedSection('performance')}
>
<h3 className={`${styles.bulletPoint} text-xl font-semibold mb-2 group relative inline-block ${selectedSection === 'performance' ? 'text-purple-400' : 'text-gray-400'}`} style={{ margin: '1em 0', textDecorationLine: 'underline', textDecorationColor: 'darkviolet', textDecorationThickness: '3px' }}>
Performance
<span className="absolute bottom-0 left-0 w-0 h-0.5 bg-gradient-to-r from-purple-400 to-pink-500 group-hover:w-full transition-all duration-300"></span>
</h3>
<p className={`${selectedSection === 'performance' ? 'text-gray-300' : 'text-gray-500'}`}>Performance live avec improvisation au contrôleur MIDI</p>
</div>
</div>
<div>
{renderSectionContent()}
</div>
</div>
</section>
{/* Section 2: Music */}
<section id="music" className={styles.sectionContainer}>
<h2 className="text-3xl font-bold mb-8 text-center">
<span className="bg-gradient-to-r from-purple-400 to-pink-600 bg-clip-text text-transparent">
Sorties
</span>
</h2>
<div className="grid grid-cols-2 gap-6 justify-center items-stretch max-w-4xl mx-auto">
{albums.map(album => (
<div
key={album.id}
className="flex justify-center w-xs py-4">
<Image
src={album.image}
alt={album.title}
width={480}
height={480}
className="mx-auto mb-4"
/>
<div className="flex flex-col items-center text-center">
<h3 className="text-lg font-bold text-white mb-3 truncate w-full">{album.title}</h3>
<div className="flex flex-wrap gap-2 justify-center">
{album.links.map(link => (
<a
key={link.platform}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="bg-black/50 backdrop-blur-sm hover:bg-purple-500/70 text-white rounded-full p-2 transition-all duration-300 hover:shadow-glow"
title={link.platform}
>
<span className="text-xl">{link.icon}</span>
</a>
))}
</div>
</div>
</div>
))}
</div>
</section>
{/* Section: Performances */}
{futureEvents.length > 0 && (
<section id="performances" className={styles.sectionContainer}>
<h2 className="text-3xl font-bold mb-8 text-center">
<span className="bg-gradient-to-r from-purple-400 to-pink-600 bg-clip-text text-transparent">
Prochains Événements
</span>
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{futureEvents.slice(0, 3).map(live => (
<Link href={`/parvagues/live/${live.slug}`} key={live.slug} legacyBehavior>
<a className="block bg-black/30 border border-purple-500/20 rounded-lg overflow-hidden hover:border-purple-500/70 transition-all hover:shadow-glow hover:-translate-y-1">
<div className="relative h-40">
<img
src={`/images/parvagues/lives/${live.year}/${live.slug}/poster.jpg`}
alt={live.title}
fill
className="object-cover"
/>
<div className="absolute top-2 right-2 bg-black/60 text-white text-xs px-2 py-1 rounded">
{new Date(live.date).toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'short',
year: 'numeric'
})}
</div>
</div>
<div className="p-4">
<h3 className="text-lg font-bold text-white">{live.title}</h3>
<p className="text-purple-300 text-sm mt-1">{live.location}</p>
<p className="text-gray-400 text-sm mt-2 line-clamp-2">{live.description}</p>
</div>
</a>
</Link>
))}
</div>
{/* TODO: Consider a 'all lives' page */}
{/* <div className="flex justify-center mt-10">
<Link href="/parvagues/lives" className={styles.outlineButton}>
Voir tous les événements
</Link>
</div> */}
</section>
)}
{/* Section 3: About */}
<section id="about" className={`${styles.sectionContainer} pb-24`}>
<h2 className="text-3xl font-bold mb-8 text-center">
<span className="bg-gradient-to-r from-purple-400 to-pink-600 bg-clip-text text-transparent">
À propos
</span>
</h2>
<div className="max-w-3xl mx-auto text-gray-300 space-y-4">
<p>
ParVagues, c'est des ondes qui naissent dans un océan binaire pour parfois s'échouer sur vos plages sonores.
</p>
<p>
Codé avec TidalCycles, chaque enregistrement est issu d'une structure algorithmique, au code source libre et réutilisable.
</p>
<p>
En particulier, ParVagues existe grâce :
</p>
<ul className="list-disc pl-6 space-y-2">
<li>à <a href="https://tidalcycles.org" className="text-purple-400 hover:text-purple-300">Yaxu</a> et la <a href="https://tidalcycles.org/community" className="text-purple-400 hover:text-purple-300">communauté TidalCycles</a></li>
<li>à <a href="https://supercollider.github.io" className="text-purple-400 hover:text-purple-300">SuperCollider</a> et les <a href="https://github.com/supercollider/sc3-plugins" className="text-purple-400 hover:text-purple-300">SC3-Plugins</a></li>
<li>à <a href="https://github.com/musikinformatik/SuperDirt" className="text-purple-400 hover:text-purple-300">SuperDirt</a> et ses samples</li>
<li>au Santa Clara Laptop Orchestra (<a href="https://www.scu.edu/cas/music/ensembles/sclork/" className="text-purple-400 hover:text-purple-300">www.scu.edu/cas/music/ensembles/sclork/</a>)</li>
<li>aux projets <a href="https://pickleddiscs.bandcamp.com/album/blood-sport-sample-pack" className="text-purple-400 hover:text-purple-300">BloodSport Samples</a> et <a href="https://hydrogen-music.org/" className="text-purple-400 hover:text-purple-300">Hydrogen</a></li>
</ul>
<p>
Le résultat final est publié sous license CC-BY-SA : <br />
vous êtes libres de les récupérer, modifier et repartager, tant que vous mentionnez leur origine.
</p>
<p>
Le code source final de chaque partition est disponible sur <a href="https://nech.pl/parvagues" className="text-purple-400 hover:text-purple-300">nech.pl/parvagues</a>.<br/>
Les enregistrements originaux sont disponibles sur demande.
</p>
</div>
</section>
</main>
{/* Footer - not sticky */}
<ParVaguesFooter />
</div>
</>
<Layout title="ParVagues - Musique Algorithmique">
<Hero />
<TourTimeline lives={lives} />
<MusicSection />
<VideoSection />
<BookingForm />
</Layout>
);
}
export async function getStaticProps() {
const lives = getAllLives();
const allLives = getAllLives();
// Only send fields needed by the landing page
const lives = allLives.map(({ slug, year, title, date, location, audio, video, archive, ctaURL }) => ({
slug, year, title, date, location,
audio: audio || '',
video: video || '',
archive: archive || '',
ctaURL: ctaURL || '',
}));
return {
props: {
lives,
},
props: { lives },
revalidate: 60,
};
}
/**
* Take screenshots of any page at desktop + mobile viewports.
*
* Usage:
* yarn node scripts/screenshots.js [url] [output-dir]
* (must use `yarn node` for PnP resolution)
*
* Defaults:
* url: http://localhost:3000/parvagues
* output-dir: /tmp/screenshots
*
* Scrolls through the page and captures each viewport-height chunk.
* Also captures anchored sections (#tour, #music, #video, #booking).
*/
// Use @playwright/test's bundled browser launcher
const { chromium } = require('playwright');
const path = require('path');
const fs = require('fs');
const BASE_URL = process.argv[2] || 'http://localhost:3000/parvagues';
const OUT_DIR = process.argv[3] || '/tmp/screenshots';
const VIEWPORTS = [
{ name: 'desktop', width: 1440, height: 900 },
{ name: 'mobile', width: 390, height: 844 },
];
const SECTIONS = ['tour', 'music', 'video', 'booking'];
async function captureViewport({ name, width, height }) {
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width, height } });
await page.goto(BASE_URL, { waitUntil: 'load', timeout: 30000 });
// Wait for content to render
await new Promise((r) => setTimeout(r, 2000));
// Full-page screenshot
await page.screenshot({ path: path.join(OUT_DIR, `${name}-full.png`), fullPage: true });
// Hero (above the fold)
await page.screenshot({ path: path.join(OUT_DIR, `${name}-hero.png`) });
// Scroll-based captures (each viewport height)
const totalHeight = await page.evaluate(() => document.body.scrollHeight);
const chunks = Math.ceil(totalHeight / height);
for (let i = 1; i < Math.min(chunks, 8); i++) {
await page.evaluate((y) => window.scrollTo(0, y), i * height);
await new Promise((r) => setTimeout(r, 400));
await page.screenshot({ path: path.join(OUT_DIR, `${name}-scroll-${i}.png`) });
}
// Section-anchored captures
for (const id of SECTIONS) {
const found = await page.evaluate((sectionId) => {
const el = document.querySelector(`#${sectionId}`);
if (el) { el.scrollIntoView({ behavior: 'instant' }); return true; }
return false;
}, id);
if (found) {
await new Promise((r) => setTimeout(r, 400));
await page.screenshot({ path: path.join(OUT_DIR, `${name}-${id}.png`) });
}
}
await browser.close();
console.log(` ✓ ${name} (${width}×${height}): ${chunks} scroll chunks + ${SECTIONS.length} sections`);
}
(async () => {
fs.mkdirSync(OUT_DIR, { recursive: true });
console.log(`Capturing ${BASE_URL}${OUT_DIR}/\n`);
for (const vp of VIEWPORTS) {
await captureViewport(vp);
}
const files = fs.readdirSync(OUT_DIR).filter((f) => f.endsWith('.png'));
console.log(`\nDone! ${files.length} screenshots in ${OUT_DIR}/`);
})();
/* Add the ParVagues color variables globally */
@import "tailwindcss";
@source "../pages/**/*.{js,ts,jsx,tsx}";
@source "../components/**/*.{js,ts,jsx,tsx}";
/* ParVagues palette */
:root {
--neon-down: #8900b3;
--neon-low: #a700d1;
......@@ -6,23 +11,73 @@
--coral: #ff3d7b;
--biomod: #5bc091;
--cigarette: #ff8c00;
}
--surface: #0a0a0a;
--surface-raised: #111111;
--text-primary: #e5e5e5;
--text-muted: #737373;
}
html {
scroll-behavior: smooth;
scroll-padding-top: 4.5rem;
}
.font-display {
font-family: var(--font-syne), system-ui, sans-serif;
}
/* 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% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
/* 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);
}
:global(.animate-shine) {
animation: shine 1.5s ease-in-out;
}
}
\ No newline at end of file
......@@ -24,6 +24,7 @@
"**/*.tsx"
],
"exclude": [
"node_modules"
"node_modules",
"playwright.config.ts"
]
}
This source diff could not be displayed because it is too large. You can view the blob instead.
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