add app/page.tsx — main broadcast page with Socket.io scenes and chat
This commit is contained in:
parent
6e9ebf4eee
commit
72392ac1d7
1 changed files with 207 additions and 0 deletions
207
app/page.tsx
Normal file
207
app/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue