Initial commit: Gaming Hub foundation
Plugin-based Discord bot framework with web frontend: - Core: Discord.js client, SSE broadcast, JSON persistence - Plugin system: lifecycle hooks (init, onReady, routes, snapshot, destroy) - Web: React 19 + Vite 6 + TypeScript, tab-based navigation - Docker: multi-stage build (Node 24, static ffmpeg, yt-dlp) - GitLab CI: Kaniko with LAN registry caching Ready for plugin development.
This commit is contained in:
parent
1ae431dd2f
commit
ae1c41f0ae
19 changed files with 954 additions and 0 deletions
129
web/src/App.tsx
Normal file
129
web/src/App.tsx
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
interface PluginInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
// Plugin tab components will be registered here
|
||||
const tabComponents: Record<string, React.FC<{ data: any }>> = {};
|
||||
|
||||
export function registerTab(pluginName: string, component: React.FC<{ data: any }>) {
|
||||
tabComponents[pluginName] = component;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [plugins, setPlugins] = useState<PluginInfo[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<string>('');
|
||||
const [pluginData, setPluginData] = useState<Record<string, any>>({});
|
||||
const eventSourceRef = useRef<EventSource | null>(null);
|
||||
|
||||
// Fetch plugin list
|
||||
useEffect(() => {
|
||||
fetch('/api/plugins')
|
||||
.then(r => r.json())
|
||||
.then((list: PluginInfo[]) => {
|
||||
setPlugins(list);
|
||||
if (list.length > 0 && !activeTab) setActiveTab(list[0].name);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
// SSE connection
|
||||
useEffect(() => {
|
||||
let es: EventSource | null = null;
|
||||
let retryTimer: ReturnType<typeof setTimeout>;
|
||||
|
||||
function connect() {
|
||||
es = new EventSource('/api/events');
|
||||
eventSourceRef.current = es;
|
||||
|
||||
es.onopen = () => setConnected(true);
|
||||
|
||||
es.onmessage = (ev) => {
|
||||
try {
|
||||
const msg = JSON.parse(ev.data);
|
||||
if (msg.type === 'snapshot') {
|
||||
setPluginData(prev => ({ ...prev, ...msg }));
|
||||
} else if (msg.plugin) {
|
||||
setPluginData(prev => ({
|
||||
...prev,
|
||||
[msg.plugin]: { ...(prev[msg.plugin] || {}), ...msg },
|
||||
}));
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
es.onerror = () => {
|
||||
setConnected(false);
|
||||
es?.close();
|
||||
retryTimer = setTimeout(connect, 3000);
|
||||
};
|
||||
}
|
||||
|
||||
connect();
|
||||
return () => { es?.close(); clearTimeout(retryTimer); };
|
||||
}, []);
|
||||
|
||||
const TabComponent = activeTab ? tabComponents[activeTab] : null;
|
||||
const version = (import.meta as any).env?.VITE_APP_VERSION ?? '1.0.0';
|
||||
|
||||
// Tab icon mapping
|
||||
const tabIcons: Record<string, string> = {
|
||||
soundboard: '\u{1F3B5}',
|
||||
stats: '\u{1F4CA}',
|
||||
events: '\u{1F4C5}',
|
||||
games: '\u{1F3B2}',
|
||||
gamevote: '\u{1F3AE}',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="hub-app">
|
||||
<header className="hub-header">
|
||||
<div className="hub-header-left">
|
||||
<span className="hub-logo">{'\u{1F3AE}'}</span>
|
||||
<span className="hub-title">Gaming Hub</span>
|
||||
<span className={`hub-conn-dot ${connected ? 'online' : ''}`} />
|
||||
</div>
|
||||
|
||||
<nav className="hub-tabs">
|
||||
{plugins.map(p => (
|
||||
<button
|
||||
key={p.name}
|
||||
className={`hub-tab ${activeTab === p.name ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab(p.name)}
|
||||
title={p.description}
|
||||
>
|
||||
<span className="hub-tab-icon">{tabIcons[p.name] ?? '\u{1F4E6}'}</span>
|
||||
<span className="hub-tab-label">{p.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="hub-header-right">
|
||||
<span className="hub-version">v{version}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="hub-content">
|
||||
{plugins.length === 0 ? (
|
||||
<div className="hub-empty">
|
||||
<span className="hub-empty-icon">{'\u{1F4E6}'}</span>
|
||||
<h2>Keine Plugins geladen</h2>
|
||||
<p>Plugins werden im Server konfiguriert.</p>
|
||||
</div>
|
||||
) : TabComponent ? (
|
||||
<TabComponent data={pluginData[activeTab] || {}} />
|
||||
) : (
|
||||
<div className="hub-empty">
|
||||
<span className="hub-empty-icon">{tabIcons[activeTab] ?? '\u{1F4E6}'}</span>
|
||||
<h2>{activeTab}</h2>
|
||||
<p>Plugin-UI wird geladen...</p>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue