add app/page.tsx — main broadcast page with Socket.io scenes and chat

This commit is contained in:
Anderson 2026-03-21 20:43:22 +00:00
parent 6e9ebf4eee
commit 72392ac1d7

207
app/page.tsx Normal file
View file

@ -0,0 +1,207 @@
'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>
);
}