/* 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
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 (
{items}
); } 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'}
); } /* ===================================================== 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.alt_text ) : (
Portrait
)}

{lede}

{bioParagraphs.map((p, i) =>

{p}

)} {onPage && ( )}
); } /* ===================================================== 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'} )}
))}
); } 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="" />
{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
{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 (
e.stopPropagation()}> {kind === 'local' ? (
{externalUrl && ( e.stopPropagation()} > {kind === 'yt' ? '↗ Auf YouTube öffnen' : '↗ Auf Instagram öffnen'} )}
); } 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 && ( )} {hasNext && ( )} {img.alt_text 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)}> {img.alt_text
))}
{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="" />

Video Clips

{videoCount !== null ? `${videoCount} videos` : 'Videos'} · YouTube + Instagram

SoundCloud Mixes

SoundCloud Feed · Live @{scHandle} ↗

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.
) : (
{errs._server &&
{errs._server}
}
update('name', e.target.value)} data-invalid={!!errs.name} placeholder="Jane Promoter" /> {errs.name &&
{errs.name}
}
update('email', e.target.value)} data-invalid={!!errs.email} placeholder="jane@yourvenue.de" /> {errs.email &&
{errs.email}
}
update('org', e.target.value)} placeholder="(optional)" />
update('date', e.target.value)} placeholder="e.g. Sat 15 Aug 2026" />