/* WOAH! NIGHT CLUB — Main App */ const { useState, useEffect, useRef, useCallback } = React; /* Cache busting — uses window.v from extras.jsx (loaded first). To force reload of all assets after upload, bump window.ASSET_VERSION at the top of extras.jsx */ const v = window.v || ((path) => path); const NAV_LINKS = [ ['Staff','staff'],['Residents','djs'],['Events','events'],['Menu','cocktails'], ['VIP','vip'],['Merch','merch'] ]; /* Bingo is rendered as the 7th nav slot — opens external in a new tab */ const NAV_BINGO = { label:'Bingo', url:'https://djgaiaworld.com/bingo/' }; /* Staff socials — Gaia fills these in; null = render gray placeholder */ const STAFF_SOCIALS = { 'GAIA': { instagram:'https://www.instagram.com/djgaia.ffxiv', twitter:null }, 'KAIEN': { instagram:'https://www.instagram.com/kaiennanami_ffxiv/', twitter:null }, 'MYOUKI': { instagram:null, twitter:null }, 'AEDRIC': { instagram:'https://www.instagram.com/aedric.xiv/', twitter:null }, 'AXEL': { instagram:'https://www.instagram.com/axelvesta_ffxiv/', twitter:null }, 'FELINA': { instagram:'https://www.instagram.com/felina_fleur/', twitter:null }, 'FOX': { instagram:'https://www.instagram.com/foxk.ffxiv/', twitter:null }, 'HYTSUKY': { instagram:'https://www.instagram.com/hytsuky_xiv/', twitter:null }, 'LIVELIA': { instagram:'https://www.instagram.com/livelia_vandern/', twitter:null }, 'MAYLIN': { instagram:'https://www.instagram.com/maymay.ff14/', twitter:null }, 'SUGAR': { instagram:'https://www.instagram.com/sugar.ffxiv/', twitter:null }, 'NOELLE': { instagram:null, twitter:null }, 'MAY': { instagram:null, twitter:null }, 'ZANE': { instagram:null, twitter:null }, 'HEIZU': { instagram:null, twitter:null }, 'KURO': { instagram:null, twitter:null }, 'MIZUKI': { instagram:null, twitter:null }, 'NOIR': { instagram:null, twitter:null }, 'ELLANORE': { instagram:null, twitter:null }, 'AIKA': { instagram:null, twitter:null }, 'KIM': { instagram:null, twitter:null }, 'PEANUT': { instagram:null, twitter:null }, 'RAVEN': { instagram:null, twitter:null }, 'NERU': { instagram:null, twitter:null }, 'ANGIE': { instagram:'https://www.instagram.com/angie_ffxiv/', twitter:null }, 'MANA': { instagram:'https://www.instagram.com/mana.heatherwind.ffxiv/', twitter:null }, }; const DJS = [ { id:'gaia', name:'Gaia', tag:'The Anime Made DJ', jp:'ガイア', pills:['TECHNO','ROCK & METAL','ANIME'], status:'● RESIDENT DJ', bio:'50% Romanian, 50% Japanese DJ and music producer ruling the dance floors of Eorzea. Resident DJ at multiple top-tier FFXIV venues. Specializing in techno, EDM, and rock/metal — every set is a journey from melodic highs to chaotic drops. Follow on Discord, Spotify, and let\u2019s keep the party going. Woah!', twitch:'https://www.twitch.tv/dj_gaia', handle:'@dj_gaia', g:'#FF2E97' }, { id:'aemilia', name:'Aemilia', tag:'The Bunterfly ✦ Full-Time Streamer', jp:'エミリア', pills:['HYPE STUFFS','EDM','DNB'], status:'● RESIDENT DJ', bio:'The Bunterfly. Full-time streamer, content creator, and one of the warmest presences in the WOAH! collective. Wherever Aemilia goes, the vibe follows — a true metamorphosis of energy, color, and heart.', twitch:'https://www.twitch.tv/aemilia', handle:'@aemilia', g:'#5DD3FF' }, { id:'reyalex', name:'Rey Alex', tag:'Owner of Urban Night Club · Manager of DJ Gaia', jp:'レイ・アレックス', pills:['TRANCE','SYNTHWAVE','LO-FI'], status:'● RESIDENT DJ', badge:'👑 WOAH! MANAGEMENT', note:'Bookings via Discord: @reyalexffxiv', bio:'The mastermind behind the curtain. Owner of Urban Night Club and the manager who keeps DJ Gaia\u2019s career — and the entire collective — running smoothly. \u201CI do things and stuff happens.\u201D', twitch:'https://www.twitch.tv/reyalexxx', handle:'@reyalexxx', g:'#FF6BCB' }, { id:'khangomon', name:'Khangomon XIV', tag:'Viet-Asia Mix · VTuber · Owner of Project XIV', jp:'カンゴモン', pills:['HYPE STUFFS','EDM','DNB'], status:'● RESIDENT DJ', bio:'Viet-Asia Mix Streamer, VTuber, occasional TCG enjoyer, and proud member of the FFXIV community. Owner of Project XIV. Every stream is a great time for everyone — Hype or Vibe, DUH!', twitch:'https://www.twitch.tv/khangomon', handle:'@khangomon', g:'#FF3B3B' }, { id:'rekse', name:'Rekse Nintly', tag:'Hype Party Kitty · Owner of The Cherry Blossom', jp:'レクセ・ニントリー', pills:['TECHNO','EDM','TRANCE'], status:'● RESIDENT DJ', bio:'Hype Party Kitty. Resident DJ at The Cherry Blossom, Black Sapphire, WOAH!, End of the Line, and Sinners. Owner of The Cherry Blossom night club. Find her on Endeavor Promotions.', twitch:'https://www.twitch.tv/reksenintly', handle:'@reksenintly', g:'#FFB347' }, { id:'alec', name:'Alec Marston', tag:'Dark, heavy, high-energy. Loud and made to move you.', jp:'アレック・マーストン', pills:['ROCK & METAL','TECHNO','EDM'], status:'● RESIDENT DJ', bio:'Rocking the DJ decks like he rocks his coffee — dark and questionable. Mixing dark, heavy, high-energy sounds from techno to metal. If it hits hard, it\u2019s in. Live sets full of intensity, chaos, and raw emotion.', twitch:'https://www.twitch.tv/alecmarston', handle:'@alecmarston', g:'#9D2EFF' }, { id:'rin', name:'Rin Tsukii', tag:'Owner of Ignite Club · Rising in the FFXIV scene', jp:'リン・ツキ', pills:['HYPE STUFFS','EDM','VINTAGE'], status:'● RESIDENT DJ', bio:'Venturing into DJ\u2019ing in the FFXIV scene — still learning, still growing. Owner of Ignite Club. Thank you for your patience and for being part of the journey. ✨', twitch:'https://www.twitch.tv/rin_tsukii', handle:'@rin_tsukii', g:'#A4DD3A' }, { id:'kiwi', name:'Kiwi', tag:'Co-Owner of Ignite Club · Former NA Raider', jp:'キウイ', pills:['EDM','ROCK & METAL','TRANCE'], status:'● RESIDENT DJ', bio:'Started out as a raider in NA. Went to \u201Cbriefly\u201D help a friend with their club, slipped, and has been stuck in the venue scene ever since. Co-Owner of Ignite Club. 🥝', twitch:'https://www.twitch.tv/kiwinjoyer', handle:'@kiwinjoyer', g:'#7DEB6E' }, { id:'iris', name:'Iris Stellaris', tag:'Melodic DJ from Czechia ✨', jp:'アイリス・ステラリス', pills:['MELODIC TECHNO','PROGRESSIVE','HYPE STUFFS'], status:'● RESIDENT DJ', bio:'Melodic DJ from Czechia. Specializing in progressive, deep, and melodic house, melodic techno — with frequent voyages into trance, uplifting, hard, and mainstage. Recently exploring harder techno territory too. 🫶', twitch:'https://www.twitch.tv/iris_stellaris', handle:'@iris_stellaris', g:'#8B5CF6' }, ]; const EVENT_NAMES = [ { en:'NEON BLOOM', jp:'ネオン ブルーム' }, { en:'KITSUNE FIRE', jp:'狐火' }, { en:'CHROME HEARTS', jp:'クロム・ハーツ' }, { en:'MIDNIGHT SAKURA',jp:'夜桜' }, { en:'ELECTRIC OKAMI', jp:'電気狼' }, { en:'CYBER BLOSSOM', jp:'サイバー桜' } ]; const FIRST_PARTY_DATE = '2026-05-18'; const TWO_WEEKS_MS = 14*24*60*60*1000; function getUpcomingParties() { const now = new Date(); const first = new Date(FIRST_PARTY_DATE+'T20:00:00Z'); let cur = new Date(first), idx = 0; while (cur < now) { cur = new Date(cur.getTime()+TWO_WEEKS_MS); idx++; } const out = []; for (let i=0;i<3;i++) { out.push({date:new Date(cur), idx: idx+i }); cur = new Date(cur.getTime()+TWO_WEEKS_MS); } return out; } const EVENTS_LEGACY = [ { tag:'GUEST DJ', name:'NEON BLOOM', jp:'ネオン ブルーム', date:'2026.05.16 · 22:00 EST', lineup:'DJ GAIA · IRIS STELLARIS · KIWI', hue:320 }, { tag:'B2B SPECIAL', name:'KITSUNE FIRE', jp:'狐火', date:'2026.05.23 · 23:00 EST', lineup:'GAIA × ALEC MARSTON', hue:0 }, { tag:'LADIES NIGHT', name:'CHROME HEARTS', jp:'クロム・ハーツ', date:'2026.05.30 · 22:00 EST', lineup:'AEMILIA · REKSE · RIN', hue:190 }, { tag:'COSPLAY GACHA', name:'ULTRAVIOLET GACHA', jp:'ガチャ・ナイト', date:'2026.06.06 · 22:00 EST', lineup:'KHANGOMON · GAIA', hue:265 }, ]; const STAFF_DIVISIONS = [ { num:'01', name:'MANAGEMENT', icon:'crown', jp:'運営', hue:50, members:[ {n:'GAIA', head:true, photo:v('assets/staff-gaia.png')}, {n:'KAIEN', photo:v('assets/staff-kaien.png')}, {n:'MYOUKI', photo:v('assets/staff-myouki.png')} ] }, { num:'02', name:'PHOTOGRAPHERS', icon:'camera', jp:'撮影', hue:200, members:[ {n:'AEDRIC', head:true, photo:v('assets/staff-aedric.png')}, {n:'AXEL', photo:v('assets/staff-axel.png')}, {n:'FELINA', photo:v('assets/staff-felina.png')}, {n:'FOX', photo:v('assets/staff-fox.png')}, {n:'HYTSUKY', photo:v('assets/staff-hytsuky.png')}, {n:'LIVELIA', photo:v('assets/staff-livelia.png')}, {n:'MAYLIN', photo:v('assets/staff-maylin.png')}, {n:'SUGAR', photo:v('assets/staff-sugar.png')} ] }, { num:'03', name:'BARTENDERS', icon:'wine', jp:'バーテン', hue:30, members:[ {n:'NOELLE', head:true, photo:v('assets/staff-noelle.png')}, {n:'MAY', photo:v('assets/staff-may.png')}, {n:'ZANE', photo:v('assets/staff-zane.png')} ] }, { num:'04', name:'SECURITY / GREETER', icon:'shield', jp:'警備', hue:265, members:[ {n:'HEIZU', photo:v('assets/staff-heizu.png')}, {n:'KURO'}, {n:'MIZUKI', photo:v('assets/staff-mizuki.png')}, {n:'NOIR', photo:v('assets/staff-noir.png')} ] }, { num:'05', name:'HOST / HOSTESS', icon:'star', jp:'ホステス', hue:320, members:[ {n:'ELLANORE', head:true, photo:v('assets/staff-ellanore.png')}, {n:'AIKA', photo:v('assets/staff-aika.png')}, {n:'KIM'}, {n:'PEANUT', photo:v('assets/staff-peanut.png')}, {n:'RAVEN', photo:v('assets/staff-raven.png')} ] }, { num:'06', name:'GAMBLERS', icon:'dices', jp:'賭博', hue:280, members:[ {n:'NERU', photo:v('assets/staff-neru.png')}, {n:'KURO'}, {n:'KAIEN', photo:v('assets/staff-kaien.png')} ] }, { num:'07', name:'PROMOTERS', icon:'megaphone',jp:'広報', hue:160, members:[ {n:'ANGIE', photo:v('assets/staff-angie.png')}, {n:'MANA', photo:v('assets/staff-mana.png')} ] }, ]; const STAFF_LEGACY = [ { name:'Mira Cole', role:'HOSTESS', jp:'ホステス', initials:'MC', hue:320 }, { name:'Vex Lin', role:'PHOTOGRAPHER', jp:'撮影', initials:'VL', hue:190 }, { name:'Kano Reed', role:'BARTENDER', jp:'バーテン', initials:'KR', hue:30 }, { name:'Rey Alex', role:'MANAGER', jp:'マネージャー', initials:'RA', hue:50, mgr:true }, { name:'Zero Vance', role:'SECURITY', jp:'警備', initials:'ZV', hue:265 }, ]; const COCKTAIL_CATS = [ { code:'01', name:'NEON COCKTAILS', sub:'signature alcoholic creations', drinks:[ { code:'001', name:'WOAH! Overdrive', jp:'ウォア・オーバードライブ', ing:'vodka · grenadine · citrus', note:'intense and electric', abv:'18% ABV', price:'10,000 GIL', glass:'martini', c1:'#FF2E55', c2:'#FF8E1D', extras:['ice','spark'], featured:true, photo:v('assets/drink-001-overdrive.jpg') }, { code:'002', name:'Chrome Desire', jp:'クロム・デザイア', ing:'gin · blue curaçao · tonic', note:'cold metallic kiss', abv:'20% ABV', price:'12,000 GIL', glass:'highball', c1:'#5DD3FF', c2:'#C8C8E0', extras:['ice','bubbles'], photo:v('assets/drink-002-chrome.jpg') }, { code:'003', name:'Midnight Upload',jp:'ミッドナイト・アップロード', ing:'black vodka · blackcurrant · lime',note:'darkness in a glass', abv:'22% ABV', price:'14,000 GIL', glass:'coupe', c1:'#5B2D8C', c2:'#0a0a14', extras:['lime'], photo:v('assets/drink-003-midnight.jpg') }, ]}, { code:'02', name:'SYNTH SHOTS', sub:'short, high-impact', drinks:[ { code:'004', name:'Firewall', jp:'ファイアウォール', ing:'tequila · hot sauce · cinnamon', note:'burns like rebellion', abv:'40% ABV', price:'7,500 GIL', glass:'shot', c1:'#FF3B3B', c2:'#FFD700', extras:['steam'], photo:v('assets/drink-004-firewall.jpg') }, { code:'005', name:'Glitch Shot', jp:'グリッチ・ショット', ing:'absinthe · blue layer · green layer', note:'reality breaks', abv:'38% ABV', price:'8,000 GIL', glass:'shot', c1:'#A4DD3A', c2:'#5DD3FF', extras:['glitch'], photo:v('assets/drink-005-glitch.jpg') }, { code:'006', name:'Neural Kick', jp:'ニューラル・キック', ing:'espresso vodka · kahlua · cream', note:'wake the system', abv:'30% ABV', price:'9,000 GIL', glass:'shot', c1:'#3D2418', c2:'#E8D4A8', extras:['ice'], photo:v('assets/drink-006-neural.jpg') }, ]}, { code:'03', name:'DIGITAL MOCKTAILS', sub:'premium non-alcoholic', drinks:[ { code:'007', name:'Pixel Wave', jp:'ピクセル・ウェーブ', ing:'coconut · butterfly pea · lemon', note:'colors that change', abv:'0%', price:'6,000 GIL', glass:'highball', c1:'#5DD3FF', c2:'#8B5CF6', extras:['ice','flower'], photo:v('assets/drink-007-pixelwave.jpg') }, { code:'008', name:'Cyber Bloom', jp:'サイバー・ブルーム', ing:'rose · lychee · sparkling water', note:'floral data stream', abv:'0%', price:'6,500 GIL', glass:'coupe', c1:'#FF6BCB', c2:'#FF2E97', extras:['bubbles','flower'], photo:v('assets/drink-008-cyberbloom.jpg') }, { code:'009', name:'Soft Reboot', jp:'ソフト・リブート', ing:'cucumber · mint · elderflower', note:'reset your night', abv:'0%', price:'5,500 GIL', glass:'martini', c1:'#A4DD3A', c2:'#E8F4E0', extras:['lime'], photo:v('assets/drink-009-softreboot.jpg') }, ]}, { code:'04', name:'POWA DRINKS', sub:'energy boost', drinks:[ { code:'010', name:'Night Mode Energy', jp:'ナイト・モード', ing:'yuzu · taurine · ginseng', note:'unlock the dark', abv:'0%', price:'4,000 GIL', glass:'can', c1:'#0F4C81', c2:'#0a0a14', extras:['spark'], photo:v('assets/drink-010-nightmode.jpg') }, { code:'011', name:'Electric Mate', jp:'エレクトリック', ing:'yerba mate · lemon · mint', note:'natural circuit', abv:'0%', price:'4,000 GIL', glass:'can', c1:'#7CD92F', c2:'#1A4A0F', extras:['spark'], photo:v('assets/drink-011-electric.jpg') }, { code:'012', name:'Code Red Bull', jp:'コード・レッド', ing:'guarana · cherry · citrus', note:'max performance', abv:'0%', price:'4,500 GIL', glass:'can', c1:'#FF2E55', c2:'#FF6BCB', extras:['spark','glitch'], photo:v('assets/drink-012-codered.jpg') }, ]}, ]; const MERCH = [ { code:'M-001', name:'WOAH! Tour Tee', price:'18,000 GIL', shape:0, hue:320, ph:'TEE' }, { code:'M-002', name:'Kitsune Hoodie', price:'42,000 GIL', shape:0, hue:265, ph:'HOODIE' }, { code:'M-003', name:'Eorzea Bucket Hat', price:'14,000 GIL', shape:1, hue:50, ph:'HAT' }, { code:'M-004', name:'Holo Sticker Pack', price:'4,000 GIL', shape:0, hue:190, ph:'STICKERS' }, ]; const MEMORIES = [ { h:240, hue:320, cap:'NEON BLOOM · 2026.04', tags:['ALL','GUEST DJS'] }, { h:300, hue:265, cap:'COSPLAY NIGHT · 2026.04', tags:['ALL','COSPLAY'] }, { h:200, hue:190, cap:'KITSUNE FIRE · 2026.03', tags:['ALL','GUEST DJS'] }, { h:280, hue:30, cap:'COMMUNITY GACHA · 2026.03', tags:['ALL','COMMUNITY'] }, { h:220, hue:50, cap:'GAIA B-DAY · 2026.03', tags:['ALL','COMMUNITY'] }, { h:260, hue:0, cap:'BLACK SAPPHIRE B2B · 2026.02', tags:['ALL','GUEST DJS'] }, { h:200, hue:120, cap:'MASKED RAVE · 2026.02', tags:['ALL','COSPLAY'] }, { h:320, hue:280, cap:'NIGHT 100 · 2026.01', tags:['ALL','COMMUNITY'] }, { h:240, hue:340, cap:'HEART OF EORZEA · 2026.01', tags:['ALL','COMMUNITY'] }, { h:260, hue:200, cap:'CHROME HEARTS · 2025.12', tags:['ALL','GUEST DJS'] }, { h:220, hue:90, cap:'KIWI X RIN OPEN · 2025.12', tags:['ALL','GUEST DJS'] }, { h:280, hue:230, cap:'COSPLAY CONTEST · 2025.11', tags:['ALL','COSPLAY'] }, ]; /* ---------- Components ---------- */ /* Eorzea Time — Madrid time minus 2h, refreshes every minute */ const EorzeaTime = () => { const calc = () => { const madrid = new Date(new Date().toLocaleString('en-US',{timeZone:'Europe/Madrid'})); const t = new Date(madrid.getTime() - 2*60*60*1000); const h = String(t.getHours()).padStart(2,'0'); const m = String(t.getMinutes()).padStart(2,'0'); const days = ['SUN','MON','TUE','WED','THU','FRI','SAT']; // Calculate "cycle" — week of the year for added gacha flair const startOfYear = new Date(t.getFullYear(), 0, 1); const dayOfYear = Math.floor((t - startOfYear) / 86400000); const cycle = String(Math.floor(dayOfYear / 7) + 1).padStart(2,'0'); return { h, m, day: days[t.getDay()], cycle }; }; const [t, setT] = useState(calc); useEffect(()=>{ const tick = () => setT(calc()); const ms = 60000 - (Date.now() % 60000); const to = setTimeout(()=>{ tick(); }, ms); const iv = setInterval(tick, 60000); return () => { clearTimeout(to); clearInterval(iv); }; },[]); return (
{/* HUD corner brackets */} {/* Orbital rune on the left */} {/* Vertical kanji watermark */} {/* Time + day info */}
// EORZEA TIME
{t.h} : {t.m}
{t.day} · CYCLE {t.cycle}
); }; /* Countdown to the next bi-weekly party */ const NextNightCountdown = () => { const parties = (typeof getUpcomingParties === 'function') ? getUpcomingParties() : []; const next = parties[0]; const event = next ? EVENT_NAMES[next.idx % EVENT_NAMES.length] : null; const target = next ? next.date.getTime() : null; const [now, setNow] = useState(Date.now()); const [secFlick, setSecFlick] = useState(0); useEffect(()=>{ const iv = setInterval(()=>{ setNow(Date.now()); setSecFlick(f=>f+1); }, 1000); return () => clearInterval(iv); },[]); if (!next) return null; const diff = target - now; const live = diff <= 0; const D = Math.max(0, Math.floor(diff/86400000)); const H = Math.max(0, Math.floor((diff%86400000)/3600000)); const M = Math.max(0, Math.floor((diff%3600000)/60000)); const S = Math.max(0, Math.floor((diff%60000)/1000)); const d = next.date; const ymd = `${d.getUTCFullYear()}.${String(d.getUTCMonth()+1).padStart(2,'0')}.${String(d.getUTCDate()).padStart(2,'0')}`; return (
// NEXT NIGHT
{live ? (
[ ★ LIVE NOW ★ ]
) : ( <>
{event.en}
{event.jp}
{String(D).padStart(2,'0')}D : {String(H).padStart(2,'0')}H : {String(M).padStart(2,'0')}M : {String(S).padStart(2,'0')}S
{ymd} · 16:00 ST
)}
); }; /* Hero countdown — large flip-style display that counts down to the next opening. Uses the same getUpcomingParties() data source as NextNightCountdown. */ const HeroCountdown = () => { const parties = (typeof getUpcomingParties === 'function') ? getUpcomingParties() : []; const next = parties[0]; const partyEvent = next ? EVENT_NAMES[next.idx % EVENT_NAMES.length] : null; const target = next ? next.date.getTime() : null; const [now, setNow] = useState(Date.now()); useEffect(()=>{ const iv = setInterval(()=>setNow(Date.now()), 1000); return () => clearInterval(iv); },[]); if (!next) return null; const diff = Math.max(0, target - now); const D = Math.floor(diff/86400000); const H = Math.floor((diff%86400000)/3600000); const M = Math.floor((diff%3600000)/60000); const S = Math.floor((diff%60000)/1000); const live = diff <= 0; const d = next.date; const ymd = `${String(d.getUTCDate()).padStart(2,'0')}.${String(d.getUTCMonth()+1).padStart(2,'0')}.${d.getUTCFullYear()}`; if (live) { return (
// LIVE NOW {partyEvent && {partyEvent.en}}
[ ★ DOORS OPEN ★ ]
); } return (
// NEXT OPENING {partyEvent && {partyEvent.en}} {ymd} · 16:00 ST
{String(D).padStart(2,'0')} DAYS
{String(H).padStart(2,'0')} HOURS
{String(M).padStart(2,'0')} MINUTES
{String(S).padStart(2,'0')} SECONDS
); }; const WoahMark = ({ size = 28 }) => ( WOAH! ); /* Real WOAH! logo — used in nav, footer, music player album art */ const WoahLogo = ({ height = 52, alt = 'WOAH! Night Club' }) => ( {alt} ); const Pill = ({ children }) => {children}; const AmbientParticles = () => ( ); const Nav = ({ active, navigate, onOpenMobile, soundOn, onToggleSound, pageIdx, totalPages }) => { const [scrolled, setScrolled] = useState(false); useEffect(()=>{ const onScroll = () => setScrolled(window.scrollY > 80); onScroll(); window.addEventListener('scroll', onScroll, {passive:true}); return () => window.removeEventListener('scroll', onScroll); },[]); const go = (id) => (e) => { e.preventDefault(); navigate(id); }; return ( ); }; const MobileMenu = ({ open, onClose, navigate }) => { const go = (id) => (e) => { e.preventDefault(); onClose(); setTimeout(()=>navigate(id), 220); }; return (
    {NAV_LINKS.map(([label,id],i)=>(
  • {label}
  • ))}
); }; const Hero = () => { const logoRef = useRef(null); const onLogoMove = (e) => { if (window.matchMedia('(pointer:coarse)').matches) return; const el = logoRef.current; if(!el) return; const r = el.getBoundingClientRect(); const x = (e.clientX - r.left)/r.width - .5; const y = (e.clientY - r.top)/r.height - .5; el.style.setProperty('--lry', `${x*14}deg`); el.style.setProperty('--lrx', `${-y*14}deg`); }; const onLogoLeave = () => { const el = logoRef.current; if(!el) return; el.style.setProperty('--lry','0deg'); el.style.setProperty('--lrx','0deg'); }; const particles = Array.from({length:18}).map((_,i)=>{ const colors = ['#FF2E97','#00F0FF','#8B5CF6']; return { left:`${(i*5.7)%100}%`, color:colors[i%3], dur:14+(i%5)*2, delay:-(i*0.9) }; }); // Get next party for the editorial countdown card const upcoming = (typeof getUpcomingParties === 'function') ? getUpcomingParties() : []; const nextParty = upcoming[0]; const nextEvent = nextParty ? EVENT_NAMES[nextParty.idx % EVENT_NAMES.length] : null; const nextDateStr = nextParty ? `${String(nextParty.date.getUTCDate()).padStart(2,'0')}.${String(nextParty.date.getUTCMonth()+1).padStart(2,'0')}.${nextParty.date.getUTCFullYear()}` : ''; return (
{/* Atmospheric layered background */}
); }; const SectionHead = ({ id, title, jp, lead, leadEl }) => (
{/* Vertical floating title — sits on the left edge */} {/* Compact horizontal bar with meta + lead */}
// SECTION {id}

{title}

{jp} {leadEl ? (
{leadEl}
) : (lead &&

{lead}

)}
); const Events = () => { const parties = getUpcomingParties(); const [now, setNow] = useState(Date.now()); useEffect(()=>{ const t=setInterval(()=>setNow(Date.now()),1000); return ()=>clearInterval(t); },[]); const cards = parties.map((p,i)=>{ const nm = EVENT_NAMES[p.idx % EVENT_NAMES.length]; const d = p.date; const ymd = `${d.getUTCFullYear()}.${String(d.getUTCMonth()+1).padStart(2,'0')}.${String(d.getUTCDate()).padStart(2,'0')}`; return { ...nm, date: d, ymd, idx:i, hue:[320,275,200][i] }; }); const next = cards[0]?.date; const fmtCD = (d) => { if (!d) return ''; const ms = d.getTime() - now; if (ms<=0) return 'D-00 · 00:00:00'; const days = Math.floor(ms/86400000); const h = Math.floor((ms%86400000)/3600000); const m = Math.floor((ms%3600000)/60000); const s = Math.floor((ms%60000)/1000); return `D-${String(days).padStart(2,'0')} · ${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`; }; return (
[ NEXT 3 NIGHTS · BI-WEEKLY · MONDAYS ]

Each banner is a different chapter. Bring your crew. Bring your fit.

} />
{cards.map((e,i)=>(
{String(i+1).padStart(2,'0')}
{i===0 &&
★ NEXT NIGHT
} {i===0 &&
{fmtCD(next)}
}
OFFICIAL NIGHT // 0{i+1}
{e.en}
{e.jp}
{e.ymd} · 16:00 ST
EDM·TECHNO·ROCK/METAL·ANIME
RESERVE
))}
); }; /* Staff card socials — gray placeholder unless STAFF_SOCIALS has a link */ const StaffSocials = ({ name }) => { const s = STAFF_SOCIALS[name] || { instagram:null, twitter:null }; const Btn = ({ kind, url }) => { const enabled = !!url; const tip = enabled ? (kind==='instagram'?(url.match(/instagram\.com\/([^\/?#]+)/)?.[1] && '@'+url.match(/instagram\.com\/([^\/?#]+)/)[1]) : (url.match(/(?:twitter|x)\.com\/([^\/?#]+)/)?.[1] && '@'+url.match(/(?:twitter|x)\.com\/([^\/?#]+)/)[1])) : null; return enabled ? ( e.stopPropagation()}> ) : ( ); }; return (
); }; /* DJ photo path convention: assets/dj-{id}.png. If file missing, the onError handler hides the img and the placeholder layer (logo + gradient + scanlines) shows. */ const djPhotoPath = (id) => v(`assets/dj-${id}.png`); /* DJ card — gacha-banner style. Photo cover, identity logo small, info at bottom. */ const DjCard = ({ dj, featured, onOpen }) => { const ref = useRef(null); const [photoOk, setPhotoOk] = useState(true); const onMove = e => { if (window.matchMedia('(pointer:coarse)').matches) return; const r = ref.current.getBoundingClientRect(); const x = (e.clientX - r.left)/r.width - .5; const y = (e.clientY - r.top)/r.height - .5; ref.current.style.setProperty('--ry', `${x*5}deg`); ref.current.style.setProperty('--rx', `${-y*5}deg`); }; const onLeave = () => { ref.current.style.setProperty('--ry','0deg'); ref.current.style.setProperty('--rx','0deg'); }; return (
onOpen(dj)} tabIndex={0} onKeyDown={e=>{ if(e.key==='Enter') onOpen(dj); }}> {/* Photo (bottom layer) — covers whole card. onError → switch to placeholder mode */} {dj.name}setPhotoOk(false)} /> {/* Placeholder layer — visible only when there's no photo: gradient bg + big logo */}
); }; const DjModal = ({ dj, onClose }) => { const [photoFail, setPhotoFail] = useState(false); useEffect(()=>{ setPhotoFail(false); },[dj?.id]); useEffect(()=>{ if (dj) playCardOpen(); },[dj?.id]); useEffect(()=>{ if(!dj) return; const prev = document.body.style.overflow; document.body.style.overflow = 'hidden'; const onKey = e => { if(e.key==='Escape') onClose(); }; window.addEventListener('keydown', onKey); return () => { document.body.style.overflow = prev; window.removeEventListener('keydown', onKey); }; },[dj, onClose]); if(!dj) return null; return ReactDOM.createPortal(
e.stopPropagation()}> {/* Photo background — blurred, with cinematic slow zoom */} {!photoFail && ( setPhotoFail(true)} /> )} {/* Color vignette using DJ's color */} , document.body ); }; const DJs = () => { const [open, setOpen] = useState(null); return (
[ THE WOAH! COLLECTIVE — {DJS.length} ARTISTS ]

The voices behind every drop. The hands that move the night.

} />
{DJS.map(dj=>)}
setOpen(null)}/> ); }; const StaffModal = ({ member, division, onClose }) => { useEffect(()=>{ if (member) playCardOpen(); },[member?.n]); useEffect(()=>{ if(!member) return; const prev = document.body.style.overflow; document.body.style.overflow = 'hidden'; const onKey = e => { if(e.key==='Escape') onClose(); }; window.addEventListener('keydown', onKey); return () => { document.body.style.overflow = prev; window.removeEventListener('keydown', onKey); }; },[member, onClose]); if(!member) return null; const role = (member.role || division.name.split(' / ')[0]).toUpperCase(); const bio = member.bio || 'Bio coming soon — this operative is being briefed for the WOAH! files.'; return ReactDOM.createPortal(
e.stopPropagation()}> {/* Close button */} {/* LEFT SIDE — info */}
{division.name}
{member.head &&
★ HEAD OF DIVISION
}
{role}

{member.n.toUpperCase()}

// ABOUT ME

{bio}

{/* RIGHT SIDE — photo frame */}
{member.photo && ( {member.n}/ )} {!member.photo && ( )} {/* CRT scanlines overlay */}