207 lines
No EOL
7 KiB
TypeScript
207 lines
No EOL
7 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState, useRef } from 'react';
|
|
import { io, Socket } from 'socket.io-client';
|
|
import { AETHEX } from '@/config/aethex';
|
|
import {
|
|
SceneOakdale,
|
|
SceneForge,
|
|
ScenePassport,
|
|
SceneOffline,
|
|
} from '@/components/scenes';
|
|
|
|
// ── Inline scenes for Live / Music / Kael ──────────────────────────────────
|
|
|
|
function SceneLive({ videoId }: { videoId: string | null }) {
|
|
return (
|
|
<div className="scene scene-live">
|
|
<div className="scene-eyebrow" style={{ color: 'var(--corp)' }}>LIVE NOW</div>
|
|
<div className="scene-headline" style={{ color: 'var(--corp)' }}>MRPIGLR ON AIR</div>
|
|
{videoId ? (
|
|
<iframe
|
|
className="live-embed"
|
|
src={`https://www.youtube.com/embed/${videoId}?autoplay=1&mute=0`}
|
|
allow="autoplay; fullscreen"
|
|
allowFullScreen
|
|
/>
|
|
) : (
|
|
<div className="scene-tagline">Connecting to stream…</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SceneMusic() {
|
|
return (
|
|
<div className="scene scene-music">
|
|
<div className="scene-eyebrow" style={{ color: 'var(--corp)' }}>NOW PLAYING</div>
|
|
<div className="scene-headline" style={{ color: 'var(--corp)' }}>RANCH RECORDS</div>
|
|
<div className="scene-tagline">Frank Tango · Independent Country</div>
|
|
<div className="scene-body" style={{ marginTop: '1.5rem', color: 'var(--muted)' }}>
|
|
🎵 Music interlude in progress
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SceneKael({ kaelResponse }: { kaelResponse: string }) {
|
|
return (
|
|
<div className="scene scene-kael">
|
|
<div className="scene-eyebrow" style={{ color: 'var(--cyber)' }}>AETHEX AI</div>
|
|
<div className="scene-headline" style={{ color: 'var(--cyber)' }}>KAEL</div>
|
|
<div className="scene-tagline">Ask KAEL anything — type /kael [question]</div>
|
|
{kaelResponse && (
|
|
<div className="kael-response">
|
|
<div className="kael-label">KAEL</div>
|
|
<div className="kael-text">{kaelResponse}</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── Types ──────────────────────────────────────────────────────────────────
|
|
|
|
type Scene = 'live' | 'music' | 'kael' | 'oakdale' | 'forge' | 'offline' | 'passport';
|
|
|
|
interface ChatMessage {
|
|
id: string;
|
|
username: string;
|
|
message: string;
|
|
type: 'chat' | 'command';
|
|
timestamp: number;
|
|
}
|
|
|
|
// ── Main Page ──────────────────────────────────────────────────────────────
|
|
|
|
export default function LivePage() {
|
|
const [scene, setScene] = useState<Scene>('offline');
|
|
const [liveVideoId, setLiveVideoId] = useState<string | null>(null);
|
|
const [viewers, setViewers] = useState(0);
|
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
const [input, setInput] = useState('');
|
|
const [username] = useState(() => 'viewer' + Math.floor(Math.random() * 9000 + 1000));
|
|
const [kaelResponse, setKaelResponse] = useState('');
|
|
const socketRef = useRef<Socket | null>(null);
|
|
const chatRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const socket = io({ path: '/socket.io' });
|
|
socketRef.current = socket;
|
|
|
|
socket.on('state:init', ({ scene, liveVideoId }) => {
|
|
setScene(scene);
|
|
setLiveVideoId(liveVideoId);
|
|
});
|
|
socket.on('scene:change', ({ scene, liveVideoId }) => {
|
|
setScene(scene);
|
|
if (liveVideoId !== undefined) setLiveVideoId(liveVideoId);
|
|
});
|
|
socket.on('viewers:update', (count: number) => setViewers(count));
|
|
socket.on('chat:message', (msg: ChatMessage) => {
|
|
setMessages(prev => [...prev.slice(-49), msg]);
|
|
});
|
|
socket.on('kael:response', ({ text }: { text: string }) => {
|
|
setKaelResponse(text);
|
|
});
|
|
|
|
return () => { socket.disconnect(); };
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (chatRef.current) {
|
|
chatRef.current.scrollTop = chatRef.current.scrollHeight;
|
|
}
|
|
}, [messages]);
|
|
|
|
function sendMessage(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
if (!input.trim() || !socketRef.current) return;
|
|
socketRef.current.emit('chat:send', { message: input.trim(), username });
|
|
setInput('');
|
|
}
|
|
|
|
function renderScene() {
|
|
switch (scene) {
|
|
case 'live': return <SceneLive videoId={liveVideoId} />;
|
|
case 'music': return <SceneMusic />;
|
|
case 'kael': return <SceneKael kaelResponse={kaelResponse} />;
|
|
case 'oakdale': return <SceneOakdale />;
|
|
case 'forge': return <SceneForge />;
|
|
case 'passport':return <ScenePassport />;
|
|
default: return <SceneOffline />;
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="broadcast-root">
|
|
{/* Header bar */}
|
|
<header className="broadcast-header">
|
|
<div className="header-logo">
|
|
<span className="logo-hex">⬡</span>
|
|
<span className="logo-name">AeThex <span className="logo-live">LIVE</span></span>
|
|
</div>
|
|
<div className="header-scene-pills">
|
|
{AETHEX.scenes.map(s => (
|
|
<button
|
|
key={s}
|
|
className={`scene-pill ${scene === s ? 'active' : ''}`}
|
|
onClick={() => socketRef.current?.emit('scene:request', { scene: s, username })}
|
|
>
|
|
{s.toUpperCase()}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className="header-meta">
|
|
<span className="viewer-count">👁 {viewers}</span>
|
|
{scene === 'live' && <span className="live-badge">● LIVE</span>}
|
|
</div>
|
|
</header>
|
|
|
|
{/* Main scene */}
|
|
<main className="broadcast-main">
|
|
{renderScene()}
|
|
</main>
|
|
|
|
{/* Chat overlay */}
|
|
<aside className="chat-overlay">
|
|
<div className="chat-messages" ref={chatRef}>
|
|
{messages.map(m => (
|
|
<div key={m.id} className={`chat-msg ${m.type === 'command' ? 'chat-cmd' : ''}`}>
|
|
<span className="chat-user">{m.username}</span>
|
|
<span className="chat-text">{m.message}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<form className="chat-form" onSubmit={sendMessage}>
|
|
<input
|
|
className="chat-input"
|
|
value={input}
|
|
onChange={e => setInput(e.target.value)}
|
|
placeholder="Type a message or /command…"
|
|
autoComplete="off"
|
|
/>
|
|
<button className="chat-send" type="submit">→</button>
|
|
</form>
|
|
<div className="chat-commands">
|
|
{AETHEX.commands.map(c => (
|
|
<button
|
|
key={c.cmd}
|
|
className="cmd-chip"
|
|
style={{ borderColor: c.color, color: c.color }}
|
|
onClick={() => {
|
|
setInput(c.cmd + ' ');
|
|
socketRef.current?.emit('chat:send', { message: c.cmd, username });
|
|
}}
|
|
>
|
|
{c.cmd}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Trinity footer bar */}
|
|
<div className="trinity-bar" />
|
|
</div>
|
|
);
|
|
} |