Unverified Commit 418d84ca by Paul-Louis NECH Committed by GitHub

Merge pull request #1 from PLNech/refact/gemini

ParVagues overhaul
parents 58c84ea4 9d190f0f
---
description:
globs: *.css
alwaysApply: false
---
# AVoid global css:
Global CSS cannot be imported from files other than your Custom <App>. Due to the Global nature of stylesheets, and to avoid conflicts, Please move all first-party global CSS imports to pages/_app.js. Or convert the import to Component-Level CSS (CSS Modules).
...@@ -28,3 +28,6 @@ yarn-error.log* ...@@ -28,3 +28,6 @@ yarn-error.log*
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local
# LLM exchanges
code2prompt.json
# Dependencies to Install
These packages need to be installed for the improved ParVagues landing:
```bash
npm install @react-icons/all-files
```
## New icons dependencies:
- **@react-icons/all-files**: For streaming platform icons
## Alternative approach (if size matters):
```bash
npm install react-icons
```
Then import specific icons only.
import { useState, useEffect, useRef } from 'react';
export default function GlitchText({ text, className, burstFrequency = 30000 }) {
const [displayText, setDisplayText] = useState(text);
const [isBursting, setIsBursting] = useState(false);
const burstTimeoutRef = useRef(null);
const glitchIntervalRef = useRef(null);
// Collection of diacritical marks to add to characters
const diacritics = [
'\u0301', // Acute accent
'\u0300', // Grave accent
'\u0308', // Diaeresis
'\u0303', // Tilde
'\u0327', // Cedilla
'\u0306', // Breve
'\u0304', // Macron
'\u0302', // Circumflex
'\u030C', // Caron
'\u0307', // Dot above
'\u0328', // Ogonek
'\u0323', // Dot below
'\u0331', // Macron below
'\u0337', // Short overlay
];
// More intense effects for burst mode
const cursedCombiningMarks = [
'\u035C', // Double breve below
'\u035F', // Combining double macron below
'\u0360', // Combining double tilde
'\u0361', // Combining double inverted breve
'\u0362', // Combining double rightwards arrow below
'\u0489', // Combining cyrillic millions sign
'\u036F', // Combining latin small letter x
'\u033E', // Combining vertical tilde
'\u035D', // Combining double breve
'\u0346', // Combining bridge above
'\u031A', // Combining left angle above
'\u0359', // Combining asterisk below
];
const applyRandomDiacritic = (char) => {
// Don't apply diacritics to spaces
if (char === ' ') return char;
const shouldAddDiacritic = Math.random() < (isBursting ? 0.8 : 0.1);
if (!shouldAddDiacritic) return char;
// During bursts, possibly apply multiple diacritics
if (isBursting && Math.random() < 0.4) {
const numMarks = Math.floor(Math.random() * 3) + 1;
let result = char;
const allMarks = [...diacritics, ...cursedCombiningMarks];
for (let i = 0; i < numMarks; i++) {
const mark = allMarks[Math.floor(Math.random() * allMarks.length)];
result += mark;
}
return result;
}
// Normal mode - just add a single diacritic
const diacritic = diacritics[Math.floor(Math.random() * diacritics.length)];
return char + diacritic;
};
const glitchText = () => {
const glitchIntensity = isBursting ? 0.8 : 0.05;
// Apply glitch to the text
const glitchedText = Array.from(text).map(char => {
// Chance to apply a diacritic
if (Math.random() < glitchIntensity) {
return applyRandomDiacritic(char);
}
return char;
}).join('');
setDisplayText(glitchedText);
};
const startBurst = () => {
setIsBursting(true);
// Clear any existing interval
if (glitchIntervalRef.current) {
clearInterval(glitchIntervalRef.current);
}
// Create a faster interval during burst
glitchIntervalRef.current = setInterval(glitchText, 100);
// End burst after 1-2 seconds
setTimeout(() => {
setIsBursting(false);
clearInterval(glitchIntervalRef.current);
glitchIntervalRef.current = setInterval(glitchText, 2000);
}, 1000 + Math.random() * 1000);
};
// Setup effect - runs when component mounts or when burstFrequency changes
useEffect(() => {
// Clean up any existing intervals and timeouts
if (glitchIntervalRef.current) {
clearInterval(glitchIntervalRef.current);
}
if (burstTimeoutRef.current) {
clearTimeout(burstTimeoutRef.current);
}
// Initial setup - slow glitch every 2 seconds
glitchIntervalRef.current = setInterval(glitchText, 2000);
// Set up random bursts
const scheduleBurst = () => {
const nextBurstTime = burstFrequency + (Math.random() * burstFrequency * 0.5);
burstTimeoutRef.current = setTimeout(() => {
startBurst();
scheduleBurst();
}, nextBurstTime);
};
// Trigger immediate glitch to show effect immediately
glitchText();
// Schedule the first burst
scheduleBurst();
// Cleanup on unmount or when dependencies change
return () => {
clearInterval(glitchIntervalRef.current);
clearTimeout(burstTimeoutRef.current);
};
}, [burstFrequency]); // Add burstFrequency as a dependency
return (
<span className={className} data-text={text}>
{displayText}
</span>
);
}
\ No newline at end of file
// next/components/ImageGallery.js
import { useState } from 'react';
import Image from 'next/image';
import Masonry from 'react-masonry-css';
export default function ImageGallery({ images, slug }) {
const [selectedImage, setSelectedImage] = useState(null);
const breakpointColumns = {
default: 3,
768: 2,
480: 1
};
return (
<>
<div className="mt-8">
<h3 className="text-xl font-semibold mb-4 text-purple-400">Galerie</h3>
<Masonry
breakpointCols={breakpointColumns}
className="masonry-grid" // Ensure this class or its child provides relative positioning for 'fill'
columnClassName="masonry-grid_column"
>
{images.map((imageSrc, i) => (
<div
key={i}
className="mb-4 cursor-pointer hover:opacity-75 transition-opacity"
onClick={() => setSelectedImage(imageSrc)}
>
{/* Ensure this div is the relatively positioned parent for fill */}
<div className="relative aspect-square rounded-lg overflow-hidden"> {/* Tailwind's aspect-square utility */}
<Image
src={imageSrc}
alt={`${slug} image ${i + 1}`}
fill
className="object-cover" // object-cover will fill the square, cropping if necessary
/>
</div>
</div>
))}
</Masonry>
</div>
{/* Lightbox */}
{selectedImage && (
<div
className="fixed inset-0 z-50 bg-black/90 flex items-center justify-center cursor-zoom-out"
onClick={() => setSelectedImage(null)}
>
<div className="relative max-w-[90vw] max-h-[90vh]">
<Image
src={selectedImage}
alt="Selected image"
width={1200} // These are for the lightbox, not the gallery thumbs
height={800} // These define the max dimensions and aspect ratio for the lightbox image
className="max-w-full max-h-full object-contain" // object-contain is good for lightbox
/>
<button
className="absolute top-4 right-4 text-white text-2xl hover:text-purple-400 transition-colors"
onClick={(e) => {
e.stopPropagation();
setSelectedImage(null);
}}
>
×
</button>
</div>
</div>
)}
</>
);
}
\ No newline at end of file
import React from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { FaEnvelope, FaInstagram, FaTwitter } from 'react-icons/fa';
import { SiMastodon, SiBluesky } from 'react-icons/si';
import styles from '@/styles/parvagues.module.css';
export default function ParVaguesFooter() {
const socialLinks = [
{ icon: <FaEnvelope />, label: 'email', url: 'mailto:parvagues@nech.pl' },
{ icon: <SiMastodon />, label: 'mastodon', url: 'https://chaos.social/@PixelNoir' },
{ icon: <FaTwitter />, label: 'twitter', url: 'https://x.com/ParVagues' },
{ icon: <SiBluesky />, label: 'bluesky', url: '#' },
{ icon: <FaInstagram />, label: 'instagram', url: 'https://instagram.com/parvagues.mp3' }
];
const year = new Date().getFullYear();
return (
<footer className="bg-black border-t border-[#d900ff]/20 py-8 relative overflow-hidden">
<div className={styles.neonGradient}></div>
<div className="max-w-6xl mx-auto px-4 relative z-10">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* About Column */}
<div>
<p className="text-gray-400 text-sm mb-4">
Livecoding de musique libre<br />
Performances algorithmiques en direct.
</p>
</div>
{/* Social Column */}
<div className="flex flex-col items-center md:items-end">
<div className="flex justify-center w-full">
<div className="relative">
<Image
src="/images/parvagues/logo.png"
alt="ParVagues Logo"
width={240}
height={240}
className="mx-auto mb-4"
/>
</div>
</div>
<div className="flex flex-wrap gap-4 justify-center md:justify-end">
{socialLinks.map((link, index) => (
<a
key={index}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="bg-gray-800 hover:bg-[#8900b3]/60 text-white p-3 rounded-full transition-colors shadow-md hover:shadow-[#d900ff]/40"
aria-label={link.label}
>
{link.icon}
</a>
))}
</div>
</div>
</div>
{/* Navigation Links - more compact */}
<div className="w-full bg-black py-4">
<div className="flex justify-center items-center space-x-8 text-xs tracking-widest uppercase flex-wrap">
<Link href="/parvagues#music" className="text-gray-300 hover:text-[#ff3d7b] transition-colors tracking-wider px-3">
MUSIQUE
</Link>
<Link href="/parvagues#performances" className="text-gray-300 hover:text-[#ff3d7b] transition-colors tracking-wider px-3">
PERFORMANCES
</Link>
<Link href="/parvagues#about" className="text-gray-300 hover:text-[#ff3d7b] transition-colors tracking-wider px-3">
À PROPOS
</Link>
<a
href="mailto:parvagues@nech.pl?subject=Booking Request"
className="text-gray-300 hover:text-[#ff3d7b] transition-colors tracking-wider px-3"
>
RÉSERVER
</a>
</div>
</div>
</div>
</footer>
);
}
\ No newline at end of file
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { FaEnvelope } from 'react-icons/fa';
import styles from '@/styles/parvagues.module.css';
import { useRouter } from 'next/router';
// Custom hook to track scroll position
function useScrolledPast(threshold = 100) {
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const onScroll = () => {
setScrolled(window.scrollY > threshold);
};
// Initial check
onScroll();
window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
}, [threshold]);
return scrolled;
}
export default function ParVaguesHeader({ eventName = null, title = null }) {
const router = useRouter();
const isHome = router.pathname === '/parvagues';
const showInHeader = useScrolledPast(300);
const headerTitle = title || eventName || 'ParVagues';
return (
<header className="sticky top-0 left-0 w-full z-50 bg-black/80 backdrop-blur-md border-b border-[#d900ff]/20">
<div className={`${styles.neonGradient} opacity-5 absolute inset-0`}></div>
<div className="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between">
{/* Logo and Title with Navigation */}
<div className="flex items-center justify-between w-full">
<Link href="/parvagues" className="flex items-center group">
<div className="h-10 w-10 relative flex-shrink-0">
<Image
src="/images/parvagues/logo.png"
alt="ParVagues Logo"
width={48}
height={48}
className="object-contain transition-all duration-300 group-hover:filter group-hover:drop-shadow-[0_0_8px_rgba(217,0,255,0.7)]"
/>
</div>
<div className="overflow-hidden ml-2">
<span
className={`text-white font-bold transition-all duration-500 ${
showInHeader || !isHome ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-8'
}`}
style={{
textShadow: '0 0 5px rgba(217, 0, 255, 0.7), 0 0 10px rgba(217, 0, 255, 0.5)',
color: 'var(--neon-high)'
}}
>
{headerTitle}
</span>
</div>
</Link>
{/* Navigation and CTA */}
<div className="flex items-center space-x-6">
{/* Navigation Links */}
<nav className="flex items-center space-x-6 text-sm tracking-wider">
<Link href="/parvagues#music" className="text-gray-300 hover:text-[#ff3d7b] transition-colors">
Music
</Link>
<Link href="/parvagues#performances" className="text-gray-300 hover:text-[#ff3d7b] transition-colors">
Performances
</Link>
<Link href="/parvagues#about" className="text-gray-300 hover:text-[#ff3d7b] transition-colors">
About
</Link>
</nav>
{/* CTA button */}
<Link
href="/book"
className={`${styles.outlineButton} py-2 px-4 text-sm flex items-center whitespace-nowrap`}
>
<FaEnvelope className="mr-2 flex-shrink-0" />
<span>Book</span>
</Link>
</div>
</div>
</div>
</header>
);
}
---
title: "[38C3] Secret Toilet Rave"
date: "2024-12-28"
time: "01:37"
location: "Hamburg, Germany"
address: "CCH, Hamburg"
description: "TODO"
ctaURL: "https://soundcloud.com/parvagues/live-38c3-secret-toilet-rave"
ctaText: "SECRETSecret Toilet Algorave Set ㊙️🕺🪠"
video: ""
audio: "https://soundcloud.com/parvagues/live-38c3-secret-toilet-rave"
archive: "https://soundcloud.com/parvagues/live-38c3-secret-toilet-rave"
tags: ["livecoding", "170BPM", "DNB", "Techno", "live"]
---
Secret Toilet Algorave Set ㊙️🕺🪠
Played live at the CCC 38C3 edition (Ghosts in the Toilets composed on the spot 🤟)
Tracklist:
- Ghosts in the Toilets
- Nouveau Punk
- Pitbul Punk
- Acidulé
- L'or Bleu
\ No newline at end of file
---
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: "AlgoRave Lyon 2025"
date: "2025-05-25"
time: "XX:XX"
location: "GR TBD"
address: " GRRRND ZERO VAULX, 56 - 60 Avenue Bohlen, Vaulx-en-Velin"
description: "AlgoRave 18h-06h avec musique installations et performances audiovisuelles."
ctaURL: "https://www.grrrndzero.org/index.php/2672-sam-24-05-algorave"
# 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: "LIVE CODING @ENSAD"
date: "2025-05-22"
time: "18:30-20:00"
location: "ENSAD Paris"
address: "20 cours Saint Vincent, Issy"
description: "breakbeat.nujazz.hybrid()"
ctaURL: "https://nech.pl/ensad"
ctaText: "Sur inscription gratuite"
teasing1: |
# Loading breaks, please wait...
```tidal
d1 $ s "bazart:22.05" # orbit 8
d2 $ cookies # "collab" # gain 1.2
```
LIVE CODING @ensad.paris
22.05 > 18:30-23h30
BAZART // cookie collective
w/ @bubobubo @azertype @cyber_flemme
@z0rg_ @neon_delice @incontinentlab
teasing2: |
# SNEAK PEEK // BAZART '25 // NEXT WEEK
```tidal
d12 $ gM3 $ gF3 -- TODO: DRINK ME <3
-- $ slice 16 (slow 8 $ rev $ run 16)
$ (note "<d3 d3 <g3!3 fs3> <fs3!3 a4>>")
# "moogBass"
# cut 12
# pan (slow 16 $ range 1 0.6 saw)
# room 0.8 # dry 0.4
```
20 cours Saint Vincent
Jeudi 22 mai
teasing3: |
# C'est jeudi :D
```tidal
d8 $ gF1 $ gM1 -- BROKEN BEAT | BREAKS STACK
$ loopAt 2 $ chop 16
$ midiOn "^92" (ply 2)
$ midiOn "^60" ( -- Broken Beat!
whenmod 8 "<7 7 7 3>" rev
. splice 8 "0 3 2 7 . [1 5]*<1!3 2> [4 6]*<1 2>")
$ midiOn "^36" (# n "29")
$ midiOn "^56" (# n "25")
$ "jungle_breaks:24"
```
18:30 live @ ENSAD Issy-les-Moulineaux
https://nech.pl/ensad for details
video: ""
audio: ""
archive: ""
tags: ["livecoding", "bazart", "ensad", "nujazz", "breakbeat", "paris", "issy"]
---
# BAZART @ ENSAD
Session live coding breakbeat/nujazz au sein de l'exposition BAZART Numerique à l'ENSAD Paris lors du festival VivaIssy.
Une exploration sonore à la croisée du breakbeat et du nujazz, avec un setup live coding TidalCycles.
## Programme
- 16:00 : Ouverture des portes, demos des travaux de l'ENSAD sur la VR et les UX hommes-machines
- 18:00 : Discours de lancement
- 18:30 : Live coding session
- 20:00 : ???
- 23:30 : PROFIT
# ParVagues Live Events System
## Quick Start
1. **Install dependencies**:
```bash
npm install marked prismjs react-masonry-css
```
2. **Create a new event**:
```bash
# Create markdown file in appropriate year directory
# Follow the _template.md format
cp content/lives/_template.md content/lives/2025/my-event-YYYY-MM-DD.md
```
3. **Add images**:
```bash
# Create directory matching event slug
mkdir -p public/images/parvagues/lives/2025/my-event-YYYY-MM-DD/
# Add jpg/png/gif/webp files to this directory
```
4. **Test locally**:
```bash
npm run dev
# Visit: http://localhost:3000/parvagues
# Check: http://localhost:3000/parvagues/live/my-event-YYYY-MM-DD
```
## How it works
### Event Lifecycle
1. **Pre-event**: Shows countdown with teasings based on days remaining
- J-14+: teasing1
- J-7 to J-3: teasing2
- J-3 to event: teasing3
2. **Post-event**: Shows full content with:
- Video/audio links (if provided)
- Image gallery (automatic)
- Collapsible teasings (for nostalgia)
### Event Metadata Structure
```yaml
---
title: "Event Title"
date: "2025-05-22"
time: "18:30-20:00"
location: "Venue Name"
address: "Full Address"
description: "Brief description"
ctaURL: "https://..."
ctaText: "RSVP"
# ... teasings, video, audio etc.
---
```
### File Structure
```
content/lives/
├── 2025/
│ ├── ensad-2025-05-22.md
│ └── algorave-lyon-2025-XX-XX.md
└── 2022/
└── (historical events)
public/images/parvagues/lives/
├── 2025/
│ ├── ensad-2025-05-22/
│ │ ├── photo1.jpg
│ │ └── photo2.jpg
│ └── algorave-lyon-2025-XX-XX/
└── 2022/
└── (historical images)
```
## URL Structure
- Landing: `/parvagues`
- Event page: `/parvagues/live/[slug]`
- Slug format: `event-name-YYYY-MM-DD`
## URL Shortener Integration
Create short URLs for events:
- `nech.pl/ensad``/parvagues/live/ensad-2025-05-22`
- `nech.pl/lyon``/parvagues/live/algorave-lyon-2025-XX-XX`
## Development Notes
- Page regenerates every minute for countdown accuracy
- Images are loaded dynamically from filesystem
- Support markdown in all text fields
- Code blocks use Haskell syntax highlighting
---
title: "Event Title"
date: "2025-XX-XX"
time: "XX:XX"
location: "Location Name"
address: "Full Address"
description: "Brief event description"
ctaURL: "https://nech.pl/shortlink"
ctaText: "RSVP / TICKETS"
teasing1: |
# Teasing 1 - J-14
Markdown content with code blocks, images, etc.
```tidal
d1 $ sound "cp"
```
teasing2: |
# Teasing 2 - J-7
Another teasing markdown content
teasing3: |
# Teasing 3 - J-3
Final teasing content
video: ""
audio: ""
archive: ""
tags: ["livecoding", "algorave", "nujazz"]
---
# Event Description
Main event content goes here...
# ParVagues Site Upgrade - Implementation Checklist
## Phase 1: File Structure Setup
- [x] Create `/content/lives/` directory
- [x] Create `/content/lives/2025/` subdirectory
- [x] Create `/content/lives/2022/` subdirectory
- [x] Create `/content/lives/_template.md`
- [x] Create event metadata files:
- [x] `/content/lives/2025/ensad-2025-05-22.md`
- [x] `/content/lives/2025/algorave-lyon-2025-XX-XX.md`
- [x] Create image directories:
- [x] `/public/images/parvagues/live-placeholder.jpg`
- [x] `/public/images/parvagues/code-sample-1.png`
- [x] `/public/images/parvagues/setup-placeholder.jpg`
- [x] `/public/images/parvagues/lives/2025/ensad-2025-05-22/`
- [x] `/public/images/parvagues/lives/2025/algorave-lyon-2025-XX-XX/`
## Phase 2: Dynamic Live Page Implementation
- [x] Create `/pages/parvagues/live/[id].js`
- [x] Implement getStaticPaths to read lives directory
- [x] Implement getStaticProps to load markdown metadata
- [x] Create countdown component
- [x] Create teasing display logic
- [x] Create post-event archive layout
- [x] Add collapsible teasing section
- [x] Implement gallery component (masonry layout)
- [ ] Add responsive design
## Phase 3: Landing Page Redesign
- [x] Backup existing `/pages/parvagues.js`
- [x] Create new hero section with cyberpunk theme
- [x] Add code sample section with highlight.js
- [x] Implement live events sidebar/list
- [x] Add dark theme with purple/pink gradients
- [x] Add subtle rain/glitch effects
- [ ] Create responsive layout
- [ ] Add scroll animations
## Phase 4: Content Creation
- [x] Create ENSAD event metadata
- [x] Create AlgoRave Lyon metadata (placeholder date)
- [x] Write teasing content for both events
- [x] Add placeholder images with descriptive names
- [x] Create sample TidalCycles code snippets
## Phase 5: Testing & Verification
- [ ] Test countdown functionality
- [ ] Test gallery display
- [ ] Test responsive design
- [ ] Verify markdown rendering
- [ ] Test live page routing
- [ ] Verify image loading
- [ ] Test date logic for pre/post event states
## Phase 6: Polish & Deployment
- [ ] Add accessibility attributes
- [ ] Optimize image loading
- [ ] Add loading states
- [ ] Test SEO metadata
- [ ] Verify all links work
- [ ] Final visual polish
## Dependencies to Install
- [x] marked (for markdown rendering)
- [x] prismjs (for syntax highlighting)
- [x] react-masonry-css (for gallery layout)
- [ ] NOTE: Run `npm install marked prismjs react-masonry-css`
---
*Last updated: May 11, 2025*
# Stratégie Web ParVagues - Mai 2025
## Vue d'ensemble
Mise en place d'un système complet de gestion d'événements live sur le site ParVagues avec :
- Landing page redessinée (cyberpunk aesthetic)
- Pages d'événements dynamiques avec countdown
- Système de teasings programmés
- Galerie de photos automatique
## Architecture Implémentée
### Structure des Fichiers
```
next/
├── pages/
│ ├── parvagues.js (nouvelle landing)
│ └── parvagues/live/[id].js (pages événements)
├── components/
│ └── ImageGallery.js (gallery masonry)
├── lib/
│ └── livesData.js (logique de chargement)
├── content/lives/
│ ├── 2025/
│ │ ├── ensad-2025-05-22.md
│ │ └── algorave-lyon-2025-XX-XX.md
│ └── _template.md
└── public/images/parvagues/lives/
└── [year]/[event-slug]/
```
### Fonctionnalités Clés
#### 1. Système d'Events Live
- **URL pattern**: `/parvagues/live/[slug]`
- **Countdown dynamique** : J-14, J-7, J-3 avec teasings différents
- **Mode pre/post** : Affichage conditionnel selon la date
- **Metadata YAML** : title, date, location, teasings, etc.
#### 2. Landing Page
- **Cyberpunk design** : Dark theme + purple/pink gradients
- **Rain effects** : Animations canvas pour atmosphère
- **Code samples** : Syntax highlighting TidalCycles
- **Events sidebar** : Liste des événements à venir/passés
#### 3. Galerie de Photos
- **Masonry layout** : Disposition automatique des images
- **Lightbox** : Vue agrandie des photos
- **Auto-detection** : Scan du dossier pour images
## Événements Programmés
### ENSAD Paris - 22 Mai 2025
- **Date**: 2025-05-22, 18:30-20:00
- **URL**: `/parvagues/live/ensad-2025-05-22`
- **Short link**: `nech.pl/ensad`
- **Teasings**: 3 phases programmées avec code TidalCycles
### AlgoRave Lyon - Date TBD
- **URL**: `/parvagues/live/algorave-lyon-2025-XX-XX`
- **Short link**: `nech.pl/lyon`
- **Status**: Template créé, à finaliser
## Workflow d'Utilisation
### Ajouter un Nouvel Événement
1. **Créer le fichier markdown**:
```bash
cp content/lives/_template.md content/lives/2025/mon-event-YYYY-MM-DD.md
```
2. **Ajouter les images**:
```bash
mkdir -p public/images/parvagues/lives/2025/mon-event-YYYY-MM-DD/
# Puis copier les photos (jpg/png/gif/webp)
```
3. **Configurer l'URL courte**:
- Créer `nech.pl/monlien``/parvagues/live/mon-event-YYYY-MM-DD`
## Stratégie de Communication
### Timeline Promo ENSAD
- **J-7**: Post Instagram avec teasing1 + lien nech.pl/ensad
- **J-3**: Stories avec countdown
- **J-DAY**: Live updates sur Instagram/Bluesky/Mastodon
### Content Strategy
- **Pre-event**: Focus sur l'attente, mystery, code samples
- **Post-event**: Archives, photos, liens streaming
- **Teasings**: Progression narrative avec révélations
## Next Steps
### Technique
1. **Installer dépendances**:
```bash
npm install marked prismjs react-masonry-css
```
2. **Remplacer placeholders** par vraies images
3. **Setup URL shortener** pour nech.pl/{ensad,lyon,parvagues}
### Content
1. **Photos ENSAD**: Préparer visuels pour le 22 mai
2. **Code samples**: Sélectionner meilleurs exemples TidalCycles
3. **Bio/description**: Finaliser texte de présentation
### Marketing
1. **Cross-platform**: Instagram → Mastodon → Bluesky
2. **Tracking**: Mesurer trafic via URL courtes
3. **Community**: Engager Cookie collective
## Architecture Technique
### Frontend
- **Next.js** : Static generation avec revalidation
- **Prism.js** : Syntax highlighting pour code Haskell
- **Masonry CSS** : Layout gallery responsive
- **Canvas animations** : Effets cyberpunk
### Data Flow
1. Pages pre-renders avec getStaticProps
2. Countdown updates côté client
3. Images chargées dynamiquement
4. Revalidation toutes les minutes
## Optimisations SEO
### Meta Tags
- Title dynamique par événement
- Description basée sur metadata
- Open Graph pour partage social
### Performance
- Images lazy loading
- Static generation
- Minimal JavaScript
## Maintenance
### Ajout d'Événement
- Simple : Copier template + ajouter images
- Auto-détection par filesystem
- Zero config pour nouveaux événements
### Archivage
- Passage auto pre→post event
- Photos visibles immédiatement
- Teasings cachés mais accessibles
---
**Status**: Prêt pour déploiement
**Last Update**: 11 Mai 2025
**Dependencies**: `marked prismjs react-masonry-css`
# Dependencies to Install
These packages need to be installed for the ParVagues site upgrade:
```bash
npm install marked prismjs react-masonry-css
```
## New dependencies:
- **marked**: For markdown rendering
- **prismjs**: For syntax highlighting
- **react-masonry-css**: For gallery layout
## Already installed:
- **gray-matter**: For frontmatter parsing ✓
- **next**: For the Next.js framework ✓
- **react**: For React components ✓
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
const livesDirectory = path.join(process.cwd(), 'content/lives');
export function getAllLives() {
const lives = [];
// Read all years
const years = fs.readdirSync(livesDirectory).filter(item =>
fs.statSync(path.join(livesDirectory, item)).isDirectory()
);
years.forEach(year => {
const yearPath = path.join(livesDirectory, year);
const yearFiles = fs.readdirSync(yearPath);
yearFiles.forEach(fileName => {
if (fileName.endsWith('.md')) {
const slug = fileName.replace(/\.md$/, '');
const fullPath = path.join(yearPath, fileName);
const fileContents = fs.readFileSync(fullPath, 'utf8');
const { data } = matter(fileContents);
lives.push({
slug,
year,
...data,
});
}
});
});
// Sort by date, most recent first
return lives.sort((a, b) => new Date(b.date) - new Date(a.date));
}
export async function getLiveData(slug) {
// Find the file across all year directories
const years = fs.readdirSync(livesDirectory).filter(item =>
fs.statSync(path.join(livesDirectory, item)).isDirectory()
);
for (const year of years) {
const filePath = path.join(livesDirectory, year, `${slug}.md`);
if (fs.existsSync(filePath)) {
const fileContents = fs.readFileSync(filePath, 'utf8');
const { data, content } = matter(fileContents);
return {
slug,
year,
frontmatter: data,
content,
};
}
}
throw new Error(`Live with slug "${slug}" not found`);
}
export function getLivesImages(slug) {
const years = fs.readdirSync(livesDirectory).filter(item =>
fs.statSync(path.join(livesDirectory, item)).isDirectory()
);
for (const year of years) {
const imagesPath = path.join(process.cwd(), 'public/images/parvagues/lives', year, slug);
if (fs.existsSync(imagesPath)) {
const files = fs.readdirSync(imagesPath);
return files
.filter(file => /\.(jpg|jpeg|png|gif|webp)$/i.test(file))
.map(file => `/images/parvagues/lives/${year}/${slug}/${file}`);
}
}
return [];
}
...@@ -11,18 +11,26 @@ ...@@ -11,18 +11,26 @@
"node": ">=18.17.0" "node": ">=18.17.0"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/aspect-ratio": "^0.4.2",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"date-fns": "^3.3.1", "date-fns": "^3.3.1",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"hydra-synth": "^1.3.29", "hydra-synth": "^1.3.29",
"marked": "^15.0.11",
"next": "^15.3.0", "next": "^15.3.0",
"prismjs": "^1.30.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-icons": "^5.5.0",
"react-instantsearch": "^7.15.7",
"react-instantsearch-dom": "^6.40.4",
"react-masonry-css": "^1.0.16",
"react-player": "^2.14.1", "react-player": "^2.14.1",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"remark": "^14.0.0", "remark": "^14.0.0",
"remark-html": "^15.0.0" "remark-html": "^15.0.0",
"swiper": "^11.2.6"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.61", "@types/react": "^18.2.61",
......
import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap/dist/css/bootstrap.css'
import '../styles/globals.css'
import '../styles/main.css' import '../styles/main.css'
import '../styles/masonry.css'
export default function MyApp({ Component, pageProps }) { export default function MyApp({ Component, pageProps }) {
return <Component {...pageProps} /> return <Component {...pageProps} />
......
...@@ -39,7 +39,6 @@ export async function getStaticPaths() { ...@@ -39,7 +39,6 @@ export async function getStaticPaths() {
} }
export default function Hydra({ hydraData, sourceCode }) { export default function Hydra({ hydraData, sourceCode }) {
const [showCode, setShowCode] = useState(false);
return ( return (
<Layout> <Layout>
...@@ -74,31 +73,15 @@ export default function Hydra({ hydraData, sourceCode }) { ...@@ -74,31 +73,15 @@ export default function Hydra({ hydraData, sourceCode }) {
); );
} }
// Suggestion for later: // Suggestion for later:
// // TODO: refactor to Dynamic import with SSR enabled.
// Dynamic import with SSR disabled // DISCUSS: What would be the benefit of this?
// AI opinion: It would be better to use a dynamic import with SSR enabled,
// >because the HydraSynth component is not needed on the server side.
// >It's a client-side component that needs to be rendered on the client side.
// >So it's better to use a dynamic import with SSR enabled.
// Human: Ok -> TODO for next time someone changes this file, please do refactor to Dynamic import with SSR enabled.
// const HydraSynth = dynamic( // const HydraSynth = dynamic(
// () => import('../../components/hydra-view'), // () => import('../../components/hydra-view'),
// { ssr: false } // { ssr: false }
// ) // )
\ No newline at end of file
//
// export default function Hydra({ hydraData, sourceCode }) {
// const canvasRef = useRef(null);
//
// return (
// <Layout>
// {/* ... rest of your component ... */}
//
// {/* Now this will only run on the client side */}
// <HydraSynth
// width={700}
// height={475}
// canvasRef={canvasRef}
// source={hydraData.source}
// />
//
// {/* ... rest of your component ... */}
// </Layout>
// );
// }
import Image from "next/image";
import Link from "next/link";
import Head from "next/head";
import Layout from "../components/layout";
import utilStyles from "../styles/utils.module.css";
import SyntaxHighlighter from "react-syntax-highlighter";
import React from "react";
import ReactPlayer from "react-player";
export async function getStaticProps(context) {
const tidalSampleUrl =
"https://git.plnech.fr/pln/Tidal/raw/f5bfbc74e68dcaac0f6afa93f2b47d35321274c8/live/dnb/automne_electrique.tidal";
const response = await fetch(tidalSampleUrl);
const source = await response.text();
// Remove working title
const sourceClean = source.split("\n").slice(1).join("\n");
return {
props: {
urlSC: "https://soundcloud.com/parvagues/",
urlTwitch: "https://twitch.tv/parvagues/",
urlTwitchExample: "https://www.twitch.tv/videos/965233250",
urlAutomne: "https://soundcloud.com/parvagues/automne-electrique",
tidalSample: sourceClean,
},
};
}
export default function ParVagues({
urlSC,
urlTwitch,
urlTwitchExample,
tidalSample,
}) {
return (
<Layout>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ParVagues</title>
</Head>
<div>
<section className={utilStyles.headingMd}>
<h1>I create music with patterns</h1>
<h4>
<i>
ParVagues, c'est des ondes sonores qui naissent dans un océan
binaire pour parfois s'échouer sur vos plages sonores.
</i>
</h4>
{/*<Image
alt="ParVagues performing"
src="/images/ParVagues.jpg"
layout="fill"
width={700}
height={475}
/>*/}
</section>
<section className={utilStyles.headingMd}>
<h5>
A source sample: the code behind <a href="">Automne Électrique</a>:
</h5>
<SyntaxHighlighter
className="source-code"
width="64em"
language="haskell"
wrapLines={true}
>
{tidalSample}
</SyntaxHighlighter>
</section>
<section className={utilStyles.headingMd}>
<h4>
I sometimes post recordings on <a href={urlSC}>SoundCloud</a>
</h4>
<div className="player-wrapper">
<ReactPlayer
className="react-player"
url={urlSC}
width="100%"
height="32em"
controls={true}
config={{
soundcloud: {
options: {
auto_play: false,
},
},
}}
/>
</div>
</section>
<section className={utilStyles.headingMd}>
<h4>
I sometimes do live performances on <a href={urlTwitch}>Twitch</a>
</h4>
<div className="player-wrapper">
<ReactPlayer
className="react-player"
url={urlTwitchExample}
width="100%"
height="32em"
controls={true}
/>
</div>
</section>
</div>
</Layout>
);
}
import { useEffect } from 'react';
import { useRouter } from 'next/router';
export default function LiveRedirect() {
const router = useRouter();
useEffect(() => {
router.replace('/parvagues');
}, [router]);
return (
<div className="min-h-screen bg-black text-white flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl mb-4">Redirection...</h1>
<p>Vous allez être redirigé vers la page principale de ParVagues.</p>
</div>
</div>
);
}
\ No newline at end of file
This image diff could not be displayed because it is too large. You can view the blob instead.
/* Add the ParVagues color variables globally */
:root {
--neon-down: #8900b3;
--neon-low: #a700d1;
--neon-high: #d900ff;
--coral: #ff3d7b;
--biomod: #5bc091;
--cigarette: #ff8c00;
}
/* Shine animation for album covers */
@keyframes shine {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
: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;
}
.masonry-grid {
display: flex;
margin-left: -1rem; /* gutter size offset */
width: auto;
}
.masonry-grid_column {
padding-left: 1rem; /* gutter size */
background-clip: padding-box;
}
/* Style different sized items */
.masonry-grid_column > div {
margin-bottom: 1rem;
}
...@@ -365,7 +365,6 @@ ...@@ -365,7 +365,6 @@
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
} }
/* Hover Popup Styles */ /* Hover Popup Styles */
.hoverReveal { .hoverReveal {
position: relative; position: relative;
...@@ -474,12 +473,18 @@ ...@@ -474,12 +473,18 @@
.hoverPopup { .hoverPopup {
position: absolute; position: absolute;
left: 50%; left: 50%;
bottom: calc(100% + 15px);
transform: translateX(-50%); transform: translateX(-50%);
z-index: 100; z-index: 100;
min-width: 380px; min-width: 250px;
max-width: 500px; max-width: 30vw;
width: max-content;
max-height: 80vh;
overflow: hidden;
top: 10vh;
bottom: auto;
margin: 0 10vw;
animation: fadeIn 0.2s ease-in-out; animation: fadeIn 0.2s ease-in-out;
} }
...@@ -573,12 +578,25 @@ ...@@ -573,12 +578,25 @@
/* Mobile responsiveness */ /* Mobile responsiveness */
@media (max-width: 768px) { @media (max-width: 768px) {
.hoverPopup { .hoverPopup {
min-width: 280px; min-width: auto;
max-width: 340px; max-width: calc(100vw - 40px); /* 20px margin on each side */
width: max-content;
left: 50%;
transform: translateX(-50%);
} }
.popupContent { .popupContent {
padding: 0.8rem; padding: 0.8rem;
font-size: 0.9rem; font-size: 0.9rem;
} }
.popupLinks {
flex-direction: column;
align-items: stretch;
}
.popupLinks a {
text-align: center;
width: 100%;
}
} }
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