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 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"
},
......
/**
* 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