/* global React, Hedgehog, SectionHead */
const { useState: useStateP, useEffect: useEffectP, useRef: useRefP } = React;
// rAF-based smooth scroll. Native scrollTo({behavior:'smooth'}) is a no-op when
// the site is embedded in the in-app mobile-browser iframe, so all in-page jump
// nav (topbar + media section jumps) would silently fail. This works anywhere.
window.smoothScrollTo = function smoothScrollTo(targetY) {
const startY = window.scrollY;
const maxY = document.documentElement.scrollHeight - window.innerHeight;
const dest = Math.max(0, Math.min(targetY, maxY));
const dist = dest - startY;
if (Math.abs(dist) < 2) return;
const dur = Math.min(900, Math.max(280, Math.abs(dist) * 0.45));
const t0 = performance.now();
function step(now) {
const p = Math.min(1, (now - t0) / dur);
const e = p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
window.scrollTo(0, startY + dist * e);
if (p < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
};
const smoothScrollTo = window.smoothScrollTo;
// Consent-Hook: reagiert auf Cookie-Banner Entscheidung
function useConsent(category) {
const [granted, setGranted] = useStateP(function() {
return window.kwConsent ? window.kwConsent.has(category) : false;
});
useEffectP(function() {
function handler(e) { setGranted(!!e.detail[category]); }
document.addEventListener('kwConsentUpdate', handler);
return function() { document.removeEventListener('kwConsentUpdate', handler); };
}, [category]);
return granted;
}
// SoundCloud Preview-Karte ohne Consent (rein lokal, kein externer Request)
function ScConsentCard({ scHandle, onAllow }) {
const bars = [30,55,40,75,50,90,35,65,45,80,60,85,40,70,30,55,45,80,50,90,35,65];
return (
{bars.map((h, i) => (
))}
SoundCloud
@{scHandle} · Mixes & Sets
▶ SoundCloud aktivieren
Externe Medien erlauben
);
}
// Fallback-Venues falls Settings-API nicht erreichbar
const VENUES_FALLBACK = [
'Great Wall of China — Afterhour Rave, YinYang Music Festival',
'The Mansion, Shanghai', 'Arena, Berlin', 'Subsonic Club, Groningen NL',
'Ritter Butzke, Berlin', 'Elipamanoke, Leipzig', 'Bullitclub, München',
'Pimpernel, München', 'Tante Erna, München',
'Sputnik Springbreak Festival (Mainstage), Leipzig',
'Else Garden, Berlin', 'Watergate, Berlin', 'Bootshaus, Cologne',
'Musikraum, Cologne', 'u.v.m.',
];
function PlayedMarquee({ venues }) {
const list = venues && venues.length ? venues : VENUES_FALLBACK;
const items = [];
for (let dup = 0; dup < 2; dup++) {
items.push(
PLAYED @
);
list.forEach((v, i) => {
items.push(
{v}
);
});
}
return (
);
}
function PageHome({ setPage }) {
const [venues, setVenues] = useStateP([]);
const [tx, setTx] = useStateP({});
useEffectP(() => {
fetch('/api/settings.php')
.then(r => r.json())
.then(d => {
if (d.settings) {
if (Array.isArray(d.settings.venues_marquee)) setVenues(d.settings.venues_marquee);
setTx(d.settings);
}
})
.catch(() => {});
}, []);
return (
{tx.hero_eyebrow || 'Live · Summer Open-Air Series · 2026'}
Kevin
Weigel
{tx.hero_tagline || 'Techno / House / Groove'}
{tx.hero_location || 'Based in Munich, DE'}
setPage('shows')}>
Upcoming Shows →
setPage('media')}>
▶ Listen to mixes
);
}
/* =====================================================
GROOVE
===================================================== */
function PageGroove() {
return (
Try the Engine >} meta="Tap pads · play live" />
);
}
/* =====================================================
ABOUT
===================================================== */
function PageAbout() {
return (
About Kevin >} meta="Est. 2007" />
);
}
const ABOUT_LEDE_DEFAULT = 'Sets that breathe, build, and break — Kevin Weigel has spent a decade carving a sound that lives somewhere between deep-rooted house and forward-leaning techno.';
const ABOUT_BIO_DEFAULT = 'Born in Cologne and raised on the warehouse parties of the late 2010s, Kevin found his voice behind the decks at outdoor parties where the trees did the EQ and sunrise did the lights. His signature: long, evolving sets that hover around 124 BPM, lean into hypnotic loops, and reward the dancers who stay the whole night.\n\nUnder the Electronic Evolution banner, he curates a recurring open-air series in the woodlands outside Cologne — small, considered events that prize sound design and a careful crowd over headcount. Past appearances include festival slots across DE/NL/CH, intimate b2b sessions, and a regular residency at Musikraum.';
function AboutBody({ onPage }) {
const [portrait, setPortrait] = useStateP(null);
const [tx, setTx] = useStateP({});
useEffectP(() => {
fetch('/api/media.php?slot=portrait')
.then(r => r.json())
.then(d => { if (d.images && d.images[0]) setPortrait(d.images[0]); })
.catch(() => {});
fetch('/api/settings.php')
.then(r => r.json())
.then(d => { if (d.settings) setTx(d.settings); })
.catch(() => {});
}, []);
const lede = tx.about_lede || ABOUT_LEDE_DEFAULT;
const bioParagraphs = (tx.about_bio || ABOUT_BIO_DEFAULT)
.split('\n\n').filter(p => p.trim());
return (
{portrait ? (
) : (
Portrait
)}
{lede}
{bioParagraphs.map((p, i) =>
{p}
)}
{onPage && (
Read more →
)}
);
}
/* =====================================================
SHOWS
===================================================== */
function ShowsList({ rows = [], showStatus }) {
return (
{rows.map((s, i) => (
{s.month} {s.year}
{s.day}
{s.venue}
{s.city}
{s.event_type}
{showStatus && (
{s.status === 'available' && 'Tickets available'}
{s.status === 'limited' && 'Last tickets'}
{s.status === 'sold' && 'Sold out'}
{s.status === 'past' && 'Past show'}
)}
{
const url = s.status === 'past' ? s.recap_url : s.ticket_url;
if (url) window.open(url, '_blank', 'noopener');
}}
>
{s.status === 'past' ? 'Recap' : 'Link'}
))}
);
}
function PageShows() {
const [filter, setFilter] = useStateP('upcoming');
const [shows, setShows] = useStateP([]);
const [loading, setLoading] = useStateP(true);
const [showStatus, setShowStatus] = useStateP(false);
useEffectP(() => {
// Ticket-Status-Einstellung aus API laden
fetch('/api/settings.php')
.then(r => r.json())
.then(d => {
if (d.settings) setShowStatus(d.settings.show_ticket_status === '1');
})
.catch(() => {});
}, []);
useEffectP(() => {
setLoading(true);
fetch(`/api/shows.php?filter=${filter}`)
.then(r => r.json())
.then(d => { setShows(d.shows || []); setLoading(false); })
.catch(() => setLoading(false));
}, [filter]);
const upcomingCount = filter === 'upcoming' ? shows.length : '?';
const pastCount = filter === 'past' ? shows.length : '?';
return (
Shows & Dates >} meta="" />
setFilter('upcoming')}>
Upcoming {filter === 'upcoming' ? `(${shows.length})` : ''}
setFilter('past')}>
Past {filter === 'past' ? `(${shows.length})` : ''}
{loading ? (
Lädt…
) : shows.length === 0 ? (
Keine Shows.
) : (
)}
);
}
/* =====================================================
MEDIA
===================================================== */
function FeaturedPlayer() {
const [playing, setPlaying] = useStateP(false);
const [progress, setProgress] = useStateP(32);
useEffectP(() => {
if (!playing) return;
const id = setInterval(() => {
setProgress(p => p >= 100 ? 0 : p + 0.5);
}, 500);
return () => clearInterval(id);
}, [playing]);
const total = 64 * 60 + 12;
const cur = Math.round(total * progress / 100);
const fmt = s => `${String(Math.floor(s/60)).padStart(2,'0')}:${String(s%60).padStart(2,'0')}`;
return (
Mix N°042 · Featured
Forest Frequencies
Wald.Stage Open-Air · 2026 · 64 min · 124 BPM
setPlaying(p => !p)} aria-label={playing ? 'Pause' : 'Play'}>
{playing ? (
) : (
)}
{fmt(cur)}
{
const r = e.currentTarget.getBoundingClientRect();
setProgress(((e.clientX - r.left) / r.width) * 100);
}} />
64:12
);
}
function VideoModal({ kind, src, externalUrl, videoUrl, onClose }) {
useEffectP(() => {
function onKey(e) { if (e.key === 'Escape') onClose(); }
document.addEventListener('keydown', onKey);
document.body.style.overflow = 'hidden';
return () => { document.removeEventListener('keydown', onKey); document.body.style.overflow = ''; };
}, []);
return (
);
}
function IgThumb({ id, consentGranted }) {
const [failed, setFailed] = useStateP(false);
const igIcon = (
);
if (!consentGranted || failed) return igIcon;
return (
setFailed(true)}
/>
);
}
function VideoGrid() {
const [videos, setVideos] = useStateP([]);
const [open, setOpen] = useStateP(null);
const consentGranted = useConsent('externe_medien');
useEffectP(() => {
fetch('/api/videos.php')
.then(r => r.json())
.then(d => setVideos(d.videos || []))
.catch(() => {});
}, []);
function requestConsent() {
if (window.kwConsent) window.kwConsent.open();
}
return (
<>
{videos.map((v, i) => {
const externalUrl = v.kind === 'yt'
? `https://www.youtube.com/watch?v=${v.external_id}`
: v.kind === 'ig'
? `https://www.instagram.com/p/${v.external_id}/`
: v.video_url;
const onClick = (e) => {
e.preventDefault();
if (v.kind === 'local') {
// Lokale Videos: kein Consent nötig (eigener Server)
setOpen({ kind: 'local', src: null, videoUrl: v.video_url, externalUrl: null });
return;
}
if (!consentGranted) { requestConsent(); return; }
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) {
window.open(externalUrl, '_blank', 'noopener');
return;
}
if (v.kind === 'yt') {
setOpen({ kind: 'yt', src: `https://www.youtube-nocookie.com/embed/${v.external_id}?autoplay=1&mute=1&playsinline=1&rel=0&modestbranding=1`, externalUrl });
} else {
setOpen({ kind: 'ig', src: `https://www.instagram.com/p/${v.external_id}/embed/captioned/`, externalUrl });
}
};
// Thumbnail-Logik:
// - Lokale Thumbnails (v.thumb_url) sind eigene Assets → immer zeigen
// - YouTube-Thumbnails (img.youtube.com) nur mit Consent → sonst YT-Placeholder
// - Instagram Live-Thumbnails nur mit Consent → sonst IG-Icon
const thumb = v.kind === 'yt' ? (
consentGranted ? (
) : (
// YouTube-Branded Placeholder (kein externer Request)
{v.title}
Klicken zum Aktivieren
)
) : v.thumb_url ? (
// Lokales Thumbnail (eigenes Asset) → immer sichtbar, kein externer Request
) : v.kind === 'ig' ? (
) : (
▶ Video
);
return (
{thumb}
▶
{v.kind === 'yt' ? 'YouTube' : v.kind === 'ig' ? 'Instagram' : 'Video'}
{v.title}
{v.place}
);
})}
{open &&
setOpen(null)} />}
>
);
}
function GalleryLightbox({ img, onClose, onPrev, onNext, hasPrev, hasNext }) {
useEffectP(() => {
function onKey(e) {
if (e.key === 'Escape') onClose();
if (e.key === 'ArrowLeft' && hasPrev) onPrev();
if (e.key === 'ArrowRight' && hasNext) onNext();
}
document.addEventListener('keydown', onKey);
document.body.style.overflow = 'hidden';
return () => { document.removeEventListener('keydown', onKey); document.body.style.overflow = ''; };
}, [hasPrev, hasNext]);
return (
×
{hasPrev && (
{ e.stopPropagation(); onPrev(); }} aria-label="Vorheriges Bild">‹
)}
{hasNext && (
{ e.stopPropagation(); onNext(); }} aria-label="Nächstes Bild">›
)}
e.stopPropagation()}
/>
);
}
function Gallery() {
const [images, setImages] = useStateP([]);
const [openIdx, setOpenIdx] = useStateP(null);
useEffectP(() => {
fetch('/api/media.php?slot=gallery')
.then(r => r.json())
.then(d => setImages(d.images || []))
.catch(() => {});
}, []);
if (images.length === 0) return null;
return (
<>
{images.map((img, i) => (
setOpenIdx(i)}>
))}
{openIdx !== null && (
0}
hasNext={openIdx < images.length - 1}
onPrev={() => setOpenIdx(i => i - 1)}
onNext={() => setOpenIdx(i => i + 1)}
onClose={() => setOpenIdx(null)}
/>
)}
>
);
}
function PageMedia() {
const [scHandle, setScHandle] = useStateP('kevinweigel');
const [videoCount, setVideoCount] = useStateP(null);
const consentGranted = useConsent('externe_medien');
useEffectP(() => {
fetch('/api/settings.php')
.then(r => r.json())
.then(d => {
if (d.settings && d.settings.soundcloud_handle) setScHandle(d.settings.soundcloud_handle);
})
.catch(() => {});
fetch('/api/videos.php')
.then(r => r.json())
.then(d => setVideoCount((d.videos || []).length))
.catch(() => {});
}, []);
const scUrl = `https://soundcloud.com/${scHandle}`;
const embedSrc = 'https://w.soundcloud.com/player/?url=' + encodeURIComponent(scUrl) +
'&color=%23E5179C&auto_play=false&hide_related=true&show_comments=false' +
'&show_user=true&show_reposts=false&show_teaser=false&visual=false' +
'&download=false&buying=false&sharing=true';
function jump(id) {
const el = document.getElementById(id);
if (el) smoothScrollTo(el.getBoundingClientRect().top + window.scrollY - 80);
}
return (
Mixes & Media >} meta="" />
jump('media-videos')}>01 Video Clips
jump('media-soundcloud')}>02 SoundCloud Mixes
jump('media-gallery')}>03 Gallery
{videoCount !== null ? `${videoCount} videos` : 'Videos'} · YouTube + Instagram
All mixes streamed straight from SoundCloud — new sets land here as soon as they're uploaded.
{consentGranted ? (
) : (
window.kwConsent && window.kwConsent.open()}
/>
)}
);
}
/* =====================================================
CONTACT
===================================================== */
function PageContact() {
const [form, setForm] = useStateP({ name: '', email: '', org: '', event: '', date: '', message: '' });
const [errs, setErrs] = useStateP({});
const [sent, setSent] = useStateP(false);
const [submitting, setSubmitting] = useStateP(false);
const [settings, setSettings] = useStateP({});
useEffectP(() => {
fetch('/api/settings.php')
.then(r => r.json())
.then(d => { if (d.settings) setSettings(d.settings); })
.catch(() => {});
}, []);
const showSide = settings.show_booking_sidebar === '1';
function update(k, v) {
setForm(f => ({ ...f, [k]: v }));
if (errs[k]) setErrs(e => ({ ...e, [k]: null }));
}
async function submit(e) {
e.preventDefault();
const next = {};
if (!form.name.trim()) next.name = 'Required';
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(form.email)) next.email = 'Valid email please';
if (!form.message.trim() || form.message.trim().length < 12) next.message = 'Tell us a bit more (min 12 chars)';
setErrs(next);
if (Object.keys(next).length > 0) return;
setSubmitting(true);
try {
const fd = new FormData();
fd.append('name', form.name);
fd.append('email', form.email);
fd.append('org', form.org);
fd.append('event', form.event);
fd.append('date', form.date);
fd.append('message', form.message);
fd.append('website', ''); // Honeypot — muss leer bleiben
const resp = await fetch('/api/booking.php', { method: 'POST', body: fd });
const data = await resp.json();
if (resp.ok && data.ok) {
setSent(true);
} else if (data.errors) {
setErrs(data.errors);
} else {
setErrs({ _server: data.error || 'Fehler beim Senden. Bitte erneut versuchen.' });
}
} catch {
setErrs({ _server: 'Netzwerkfehler. Bitte erneut versuchen.' });
} finally {
setSubmitting(false);
}
}
return (
Get in touch >} meta="Replies within 48h" />
{sent ? (
▲ Booking enquiry sent — Kevin's management will reply within 48h.
Thanks for the message.
) : (
)}
{showSide && (
{settings.press_email && (
)}
{settings.tech_rider_text && (
Tech rider
{settings.tech_rider_text}
)}
)}
);
}
Object.assign(window, { PageHome, PageGroove, PageAbout, PageShows, PageMedia, PageContact, ShowsList });