// Roulsen one-screen homepage — app const { useState, useEffect, useRef } = React; const { Eyebrow } = window.RoulsenDesignSystem_aea9bf; const { RL_TRACKS: TRACKS, RL_CITIES: CITIES } = window.RL_DATA; const RL_COLLABS = ['SHINee', 'Monsta X', "Girls' Generation", 'Red Velvet', '時代少年團 TNT', 'MIRROR', 'Timmy Trumpet', '王一博', '张靓颖', '李宇春']; const RL_CREDS_LEAD = ['160+ songs', '“Blackout”', 'Eurovision ’19']; const RL_CREDS_FULL = [...RL_CREDS_LEAD, ...RL_COLLABS].join(' · '); const RL_CREDS_CURATED = [...RL_CREDS_LEAD, 'SHINee', 'Monsta X', "Girls' Generation"].join(' · '); const RL_TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "comp": "Masthead", "mood": "Charcoal", "socials": "Right", "textLayout": "B", "logo": "Left", "wmScale": 1 }/*EDITMODE-END*/; /* ---------- Spotify mini-player ---------- Uses Spotify's IFrame API so selecting a track loads (and plays) it. Falls back to a plain embed iframe if the API doesn't load. */ function SpotifyPlayer({ trackId }) { const hostRef = useRef(null); const controllerRef = useRef(null); const [apiFailed, setApiFailed] = useState(false); const firstId = useRef(trackId); useEffect(() => { let cancelled = false; const timer = setTimeout(() => { if (!controllerRef.current) setApiFailed(true); }, 3500); function init(IFrameAPI) { if (cancelled || !hostRef.current || controllerRef.current) return; IFrameAPI.createController( hostRef.current, { uri: 'spotify:track:' + firstId.current, width: '100%', height: 80 }, (controller) => { controllerRef.current = controller; } ); } if (window.__spotifyIFrameAPI) { init(window.__spotifyIFrameAPI); } else { const prev = window.onSpotifyIframeApiReady; window.onSpotifyIframeApiReady = (IFrameAPI) => { window.__spotifyIFrameAPI = IFrameAPI; if (prev) prev(IFrameAPI); init(IFrameAPI); }; } return () => { cancelled = true; clearTimeout(timer); }; }, []); useEffect(() => { if (controllerRef.current && trackId !== firstId.current) { controllerRef.current.loadUri('spotify:track:' + trackId); controllerRef.current.play(); } }, [trackId]); return (
{apiFailed && ( )}
); } /* ---------- Icons (18px, inherit currentColor) ---------- */ function IconInstagram() { return ( ); } function IconSpotify() { return ( ); } function IconYouTube() { return ( ); } function IconFacebook() { return ( ); } function IconX() { return ( ); } function IconMuso() { return ( ); } function IconClass101() { return ( ); } /* Reusable social cluster — leads with the brand R-roundel, then the platforms */ function SocialCluster({ className = '' }) { return ( ); } /* ---------- Zones ---------- */ function BrandZone({ variant }) { return (

Roulsen

{variant === 'c' && (

{RL_CREDS_CURATED}

)}
); } function WorksZone({ selected, onSelect, variant }) { return (
{variant === 'b' && (

{RL_CREDS_FULL}

)} Selected credits
{TRACKS.map((tr, i) => ( ))}
); } function PhotoZone() { return (
Roulsen, portrait against a brick wall
); } function FootZone({ side }) { return ( ); } /* ---------- App ---------- */ const RL_PARAMS = new URLSearchParams(location.search); const RL_CAP = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1).toLowerCase() : s; function App() { const [t, setTweak] = useTweaks(RL_TWEAK_DEFAULTS); const [selected, setSelected] = useState(TRACKS[0].id); const hideChrome = RL_PARAMS.get('chrome') === 'off'; // URL overrides let the comparison view render fixed variants (?mood=&comp=&chrome=off) const comp = RL_CAP(RL_PARAMS.get('comp')) || t.comp; const mood = RL_CAP(RL_PARAMS.get('mood')) || t.mood; const socials = (RL_PARAMS.get('socials') || t.socials || 'Right').toLowerCase(); const textVariant = (RL_PARAMS.get('text') || t.textLayout || 'B').toLowerCase(); const logoSide = (RL_PARAMS.get('logo') || t.logo || 'Left').toLowerCase(); return (
{!hideChrome && ( setTweak('comp', v)} /> setTweak('mood', v)} /> setTweak('socials', v)} /> setTweak('textLayout', v)} /> setTweak('logo', v)} /> setTweak('wmScale', v)} /> )}
); } ReactDOM.createRoot(document.getElementById('root')).render();