// =============================================================
// Galerie Guzman — App
// Site immersif. Toggle allégorique/moderne global.
// Curseur perso, parallaxe douce, fondus longs, lightbox.
// =============================================================
const { useState, useEffect, useRef, useCallback, useMemo } = React;
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"grain": 4,
"tempo": "medium",
"hero": "triptyque",
"glass": false
} /*EDITMODE-END*/;
// ---------- Custom cursor ----------
function CustomCursor() {
const dotRef = useRef(null);
const ringRef = useRef(null);
useEffect(() => {
if (matchMedia('(pointer: coarse)').matches) return;
document.body.classList.add('has-cursor');
let mx = window.innerWidth / 2,my = window.innerHeight / 2;
let rx = mx,ry = my;
let dx = mx,dy = my;
let raf;
const onMove = (e) => {mx = e.clientX;my = e.clientY;};
const tick = () => {
rx += (mx - rx) * 0.18;
ry += (my - ry) * 0.18;
dx += (mx - dx) * 0.42;
dy += (my - dy) * 0.42;
if (ringRef.current) ringRef.current.style.transform = `translate3d(${rx}px, ${ry}px, 0) translate(-50%, -50%)`;
if (dotRef.current) dotRef.current.style.transform = `translate3d(${dx}px, ${dy}px, 0) translate(-50%, -50%)`;
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
const onOver = (e) => {
if (!ringRef.current) return;
const art = e.target.closest('[data-cursor="art"]');
const link = e.target.closest('a, button, [role="button"], input, textarea, select');
ringRef.current.classList.toggle('is-art', !!art);
ringRef.current.classList.toggle('is-link', !art && !!link);
if (dotRef.current) dotRef.current.classList.toggle('is-art', !!art);
};
window.addEventListener('mousemove', onMove);
document.addEventListener('mouseover', onOver);
return () => {
cancelAnimationFrame(raf);
window.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseover', onOver);
document.body.classList.remove('has-cursor');
};
}, []);
return (
<>
>);
}
// ---------- Reveal-on-scroll ----------
function useReveal() {
useEffect(() => {
const els = document.querySelectorAll('.reveal');
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
e.target.classList.add('is-in');
io.unobserve(e.target);
}
});
}, { threshold: 0.12, rootMargin: '0px 0px -10% 0px' });
els.forEach((el) => io.observe(el));
return () => io.disconnect();
});
}
// ---------- Parallax (gentle) ----------
function useParallax() {
useEffect(() => {
let raf;
const elems = () => document.querySelectorAll('[data-parallax]');
const onScroll = () => {
if (raf) return;
raf = requestAnimationFrame(() => {
const vh = window.innerHeight;
elems().forEach((el) => {
const speed = parseFloat(el.dataset.parallax) || 0.08;
const r = el.getBoundingClientRect();
const center = r.top + r.height / 2 - vh / 2;
el.style.transform = `translate3d(0, ${center * -speed}px, 0) scale(1.06)`;
});
raf = 0;
});
};
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
return () => {window.removeEventListener('scroll', onScroll);window.removeEventListener('resize', onScroll);cancelAnimationFrame(raf);};
}, []);
}
// ---------- Header ----------
function Header({ vision, setVision }) {
return (
);
}
// ---------- Intro Hero — common, with auto-cycling carousel ----------
function IntroHero({ setVision }) {
const slides = useMemo(() => [
{ src: 'assets/intro-1.svg', caption: 'lisière du soir, atelier' },
{ src: 'assets/intro-2.svg', caption: 'aube en forêt, Sologne' },
{ src: 'assets/intro-3.svg', caption: 'voile d’eau, étude' },
{ src: 'assets/intro-4.svg', caption: 'partition bleue et verte' }],
[]);
const [active, setActive] = useState(0);
useEffect(() => {
const t = setInterval(() => setActive((a) => (a + 1) % slides.length), 6000);
return () => clearInterval(t);
}, [slides.length]);
const enter = (vision) => {
setVision(vision);
setTimeout(() => {
document.getElementById('salle')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 50);
};
return (
GALERIE YVON GUZMAN
Faire vivrel'impossible.
Une passion pour la création, deux salles ouvertes.
Une exposition à parcourir comme on parcourt un poème.
enter('allegorique')}>
Salle 01
Les Récits
UNIVERS ALLEGORIQUE · 10 TOILES
enter('moderne')}>
Salle 02
Les Formes
UNIVERS MODERNE · 10 ŒUVRES
{slides.map((_, i) =>
setActive(i)}
aria-label={`Image ${i + 1}`}>
)}
Entrer
);
}
// ---------- Hero — triptyque ----------
function HeroTriptyque({ vision, onOpenOeuvre, oeuvres }) {
const featured = useMemo(() => {
const allegoMain = ['passage-soir', 'recits-laiton', 'songe-orange'];
const moderneMain = ['suspension-4', 'blanc-rouge', 'geometrie-jaune'];
const ids = vision === 'allegorique' ? allegoMain : moderneMain;
return ids.map((id) => oeuvres.find((o) => o.id === id)).filter(Boolean);
}, [vision, oeuvres]);
return (
{vision === 'allegorique' ? "Mot de l'artiste" : 'Salle des formes · 2026'}
{vision === 'allegorique' ?
Un regard,3 chefs d'oeuvre.
:
La couleur,posée.
}
{featured.map((o, i) =>
onOpenOeuvre(o.id)}>
)}
);
}
// ---------- Hero — fullbleed ----------
function HeroFullbleed({ vision, onOpenOeuvre, oeuvres }) {
const work = useMemo(() => {
const id = vision === 'allegorique' ? 'songe-orange' : 'geometrie-jaune';
return oeuvres.find((o) => o.id === id);
}, [vision, oeuvres]);
return (
onOpenOeuvre(work.id)}>
{vision === 'allegorique' ? 'Salle des récits · printemps 2026' : 'Salle des formes · printemps 2026'}
Œuvre courante
{work.titre}
{vision === 'allegorique' ?
Le regard,au ralenti.
:
La couleur,posée.
}
);
}
// ---------- Hero — editorial ----------
function HeroEditorial({ vision, onOpenOeuvre, oeuvres }) {
const work = useMemo(() => {
const id = vision === 'allegorique' ? 'recits-laiton' : 'blanc-rouge';
return oeuvres.find((o) => o.id === id);
}, [vision, oeuvres]);
return (
{vision === 'allegorique' ? 'Salle des récits · printemps 2026' : 'Salle des formes · printemps 2026'}
{vision === 'allegorique' ?
Le regard,au ralenti.
:
La couleur,posée.
}
{vision === 'allegorique' ?
"Quinze toiles où la matière raconte. Une exposition lente, à parcourir comme on parcourt un poème." :
"Douze œuvres récentes. Géométrie, couleur, équilibre — l'épure d'un regard contemporain."}
onOpenOeuvre(work.id)} style={{ background: 'var(--bg-elev)', '--reveal-delay': '420ms' }}>
);
}
// ---------- Intent ribbon ----------
function IntentRibbon() {
return (
« L'art a toujours été partie intégrante dans ma vie. Aujourd'hui, je vous partage ma passion »
— YVON GUZMAN
);
}
// ---------- Salle (mur de musée) ----------
function Salle({ vision, onOpenOeuvre, oeuvres }) {
const works = oeuvres.filter((o) => o.salle === vision);
return (
{vision === 'allegorique' ? 'Salle 01 · des récits' : 'Salle 02 · des formes'}
{vision === 'allegorique' ?
<>Dix toiles, qui vous transportent .> :
<>Dix œuvres où la couleur se pose .>
}
{vision === 'allegorique' ?
"Laissez vous transporter dans mes pensées à travers mes toiles. Un style authentique, où chaque couleur trouve sa place. " :
"Une exposition à hauteur d'œil. Chaque tableau cherche un équilibre de couleur, un instant qui tient — comme une partition jouée sans préméditation."}
{works.map((o, i) =>
onOpenOeuvre(o.id)}>
{o.num}
{o.titre}
{vision !== "moderne" && (
<>
{o.technique}
·
{o.dim}
>
)}
{o.statut && {o.statut} }
)}
);
}
// ---------- Intention ----------
function Intention({ data }) {
return (
{data.intention.map((p, i) =>
{p}
)}
);
}
// ---------- Bio + Expositions ----------
function Biographie({ data }) {
return (
Biographie
L'artiste, en quelques mots.
Yvon Guzman vit et travaille à Allauch depuis plus de 30 ans. Il adopte un point de vue unique, partagé entre figuration allégorique et sculptures ou verreries.
{data.bio}
Expositions sélectionnées
{data.expositions.map((e, i) =>
{e.annee}
{e.titre ? {e.titre} : Martinique }
{e.lieu}{e.ville ? (e.lieu ? `, ${e.ville.toUpperCase()}` : e.ville.toUpperCase()) : ''}
{e.type}
)}
);
}
// ---------- Contact ----------
function Contact() {
const [sent, setSent] = useState(false);
const [sending, setSending] = useState(false);
const [error, setError] = useState(null);
const [form, setForm] = useState({ nom: '', email: '', sujet: 'oeuvre', oeuvre: '', message: '' });
const submit = async (e) => {
e.preventDefault();
setSending(true);
setError(null);
try {
const response = await fetch('https://formspree.io/f/xqevvdrw', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form)
});
if (response.ok) {
setSent(true);
setForm({ nom: '', email: '', sujet: 'oeuvre', oeuvre: '', message: '' });
} else {
setError('Erreur lors de l\'envoi. Veuillez réessayer.');
}
} catch (err) {
setError('Problème de connexion. Veuillez réessayer.');
console.error(err);
} finally {
setSending(false);
}
};
return (
);
}
// ---------- Footer ----------
function Footer({ onNavigate }) {
return (
Guzman
Un peintre. Deux salles, un même musée.
Salles
Récits — allégorique
Formes — moderne
);
}
// ---------- Lightbox ----------
function Lightbox({ id, oeuvres, onClose, onNav }) {
const open = id != null;
const current = oeuvres.find((o) => o.id === id);
const sameSalle = current ? oeuvres.filter((o) => o.salle === current.salle) : [];
const idx = current ? sameSalle.findIndex((o) => o.id === id) : 0;
const prevId = sameSalle.length ? sameSalle[(idx - 1 + sameSalle.length) % sameSalle.length].id : null;
const nextId = sameSalle.length ? sameSalle[(idx + 1) % sameSalle.length].id : null;
useEffect(() => {
document.body.classList.toggle('lightbox-open', open);
if (!open) return;
const onKey = (e) => {
if (e.key === 'Escape') onClose();
if (e.key === 'ArrowLeft' && prevId) onNav(prevId);
if (e.key === 'ArrowRight' && nextId) onNav(nextId);
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [open, prevId, nextId, onClose, onNav]);
if (!current) {
return
;
}
return (
{prevId &&
onNav(prevId)} aria-label="Œuvre précédente">
}
{nextId &&
onNav(nextId)} aria-label="Œuvre suivante">
}
{current.salle === 'allegorique' ? 'Salle des récits' : 'Salle des formes'} · n° {current.num}
{current.titre}
{current.note}
{current.salle !== 'moderne' && (
<>
Technique {current.technique}
Dimensions {current.dim}
>
)}
Disponibilité {current.statut}
{current.statut === 'disponible' || current.statut === 'réservée' ?
Contacter pour acquérir :
Cette œuvre a trouvé sa place.
}
);
}
// ---------- Philosophie artistique ----------
function PhilosophieSection() {
const cards = [
{
num: '01',
titre: 'La dualité',
texte: 'Deux univers se côtoient dans mon œuvre — le récit allégorique et la forme abstraite. Non pas comme une contradiction, mais comme deux langages d\'une même pensée.'
},
{
num: '02',
titre: 'L\'authenticité',
texte: 'Peindre sans compromis. Chaque toile est un engagement envers la matière, envers les couleurs, envers ce qui m\'a poussé à poser le premier coup de pinceau. '
},
{
num: '03',
titre: 'L\'innovation',
texte: 'Explorer sans relâche les possibilités de la matière. L\'innovation n\'est pas une rupture, c\'est une curiosité qui dure.'
}];
return (
Ma philosophie artistique
Deux regards,une seule main.
{cards.map((c, i) =>
{c.num}
{c.titre}
{c.texte}
)}
);
}
// ---------- Stats bandeau ----------
function StatsBandeau() {
const stats = [
{ valeur: '+30', unite: '', label: 'ans d\'atelier' },
{ valeur: '20', unite: '+', label: 'expositions' },
{ valeur: '100', unite: '+', label: 'oeuvres' },
{ valeur: '2', unite: '', label: 'outres mers' }];
return (
{stats.map((s, i) =>
{s.valeur}
{s.unite && {s.unite} }
{s.label}
)}
);
}
// ---------- App ----------
function App() {
const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULTS);
const [vision, setVision] = useState('allegorique');
const [lightboxId, setLightboxId] = useState(null);
const [page, setPage] = useState('galerie');
const oeuvres = window.GUZMAN_DATA.oeuvres;
const navigateTo = useCallback((p) => {
setPage(p);
window.scrollTo({ top: 0, behavior: 'smooth' });
}, []);
const backToGalerie = useCallback(() => navigateTo('galerie'), [navigateTo]);
// sync vision to html attr
useEffect(() => {
document.documentElement.setAttribute('data-vision', vision);
}, [vision]);
// sync tweaks to html attrs / css vars
useEffect(() => {
document.documentElement.style.setProperty('--grain', tweaks.grain / 100);
document.documentElement.setAttribute('data-tempo', tweaks.tempo);
document.documentElement.setAttribute('data-glass', tweaks.glass ? 'on' : 'off');
}, [tweaks.grain, tweaks.tempo, tweaks.glass]);
useReveal();
useParallax();
const openOeuvre = useCallback((id) => setLightboxId(id), []);
const closeOeuvre = useCallback(() => setLightboxId(null), []);
// Render hero by variant
let Hero;
if (tweaks.hero === 'fullbleed') Hero = HeroFullbleed;else
if (tweaks.hero === 'editorial') Hero = HeroEditorial;else
Hero = HeroTriptyque;
if (page === 'mentions') {
return (
<>
>
);
}
return (
<>
setTweak('grain', v)} />
setTweak('tempo', v)} />
setTweak('hero', v)} />
setTweak('glass', v)} />
>);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render( );