Admin panel sync improvements, Baserow field handling (Single Select support), and UI emoji swap

This commit is contained in:
2026-04-16 17:01:12 +02:00
parent ec7b4d1f71
commit 247cca0803
4 changed files with 111 additions and 43 deletions
+63 -23
View File
@@ -35,7 +35,7 @@ function LoginPage({ onLogin }) {
return (
<div className="min-h-screen flex items-center justify-center bg-stone-100">
<div className="bg-white rounded-2xl shadow-lg p-8 w-full max-w-sm border-t-4 border-red-600">
<img src="/img/logo.png" alt="Quizalarm" className="w-24 h-24 mb-2" />
<div className="text-center text-4xl mb-2">🚨</div>
<h1 className="text-2xl font-bold mb-6 text-center">Quizalarm Admin</h1>
<input type="password" className="w-full p-3 border rounded-lg mb-3 text-center focus:border-red-500 outline-none" placeholder="Passwort" value={pw} onChange={(e) => setPw(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && submit()} autoFocus />
{err && <p className="text-red-500 text-sm mb-3 text-center">{err}</p>}
@@ -250,7 +250,14 @@ function LiveSetupPage({ config, token, onSessionCreated }) {
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === 'session-created') {
onSessionCreated(msg.code, ws);
onSessionCreated(msg.code, ws, {
code: msg.code,
setName: msg.setName,
questionCount: msg.questionCount,
options: msg.options,
players: [],
playerCount: 0,
});
} else if (msg.type === 'error') {
setError(msg.message);
setLoading(false);
@@ -298,11 +305,11 @@ function LiveSetupPage({ config, token, onSessionCreated }) {
}
/* ================================================================== */
/* Live Control Page */
/* Live Control Page (FIX: sends admin-join on mount) */
/* ================================================================== */
function LiveControlPage({ token, code, ws }) {
const [session, setSession] = useState(null);
function LiveControlPage({ token, code, ws, initialSession }) {
const [session, setSession] = useState(initialSession || null);
const [phase, setPhase] = useState('waiting');
const [answerCount, setAnswerCount] = useState(0);
const [result, setResult] = useState(null);
@@ -313,20 +320,26 @@ function LiveControlPage({ token, code, ws }) {
const handler = (e) => {
const msg = JSON.parse(e.data);
console.log('[admin-control] Received:', msg.type);
switch (msg.type) {
case 'session-created':
setSession({ code: msg.code, setName: msg.setName, questionCount: msg.questionCount, options: msg.options, players: [] });
setSession({ code: msg.code, setName: msg.setName, questionCount: msg.questionCount, options: msg.options, players: [], playerCount: 0 });
setPhase('waiting');
break;
case 'admin-joined':
setSession(msg.session);
setLeaderboardData(msg.session.leaderboard || []);
if (msg.session.status === 'waiting') setPhase('waiting');
else if (msg.session.status === 'ended') setPhase('ended');
else setPhase('active');
setLeaderboardData(msg.session.leaderboard);
else if (msg.session.status === 'showing-result') setPhase('result');
else setPhase('question');
break;
case 'player-joined':
case 'player-left':
setSession((s) => s ? { ...s, players: msg.players, playerCount: msg.playerCount } : s);
setSession((s) => {
if (!s) return { code, players: msg.players, playerCount: msg.playerCount };
return { ...s, players: msg.players, playerCount: msg.playerCount };
});
break;
case 'game-start':
setPhase('active');
@@ -355,42 +368,67 @@ function LiveControlPage({ token, code, ws }) {
};
ws.addEventListener('message', handler);
return () => ws.removeEventListener('message', handler);
}, [ws]);
const start = () => ws.send(JSON.stringify({ type: 'admin-start', token, code }));
const showResultAction = () => ws.send(JSON.stringify({ type: 'admin-show-result', token, code }));
const next = () => ws.send(JSON.stringify({ type: 'admin-next', token, code }));
const end = () => ws.send(JSON.stringify({ type: 'admin-end', token, code }));
// Request current session state from server to sync up
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'admin-join', token, code }));
} else {
const onOpen = () => {
ws.send(JSON.stringify({ type: 'admin-join', token, code }));
ws.removeEventListener('open', onOpen);
};
ws.addEventListener('open', onOpen);
}
return () => ws.removeEventListener('message', handler);
}, [ws, code, token]);
const start = () => {
console.log('[admin-control] Sending admin-start');
ws.send(JSON.stringify({ type: 'admin-start', token, code }));
};
const showResultAction = () => {
console.log('[admin-control] Sending admin-show-result');
ws.send(JSON.stringify({ type: 'admin-show-result', token, code }));
};
const next = () => {
console.log('[admin-control] Sending admin-next');
ws.send(JSON.stringify({ type: 'admin-next', token, code }));
};
const end = () => {
console.log('[admin-control] Sending admin-end');
ws.send(JSON.stringify({ type: 'admin-end', token, code }));
};
const presentUrl = `${location.origin}/present?token=${encodeURIComponent(token)}#${code}`;
const playerCount = session?.players?.length || session?.playerCount || 0;
return (
<div className="space-y-4 max-w-lg mx-auto">
<div className="bg-white rounded-xl shadow p-6 text-center border-t-4 border-red-600">
<h2 className="text-lg font-bold mb-2">Session-Code</h2>
<div className="text-5xl font-mono font-extrabold tracking-widest text-red-600 mb-2">{code}</div>
<p className="text-gray-400 text-sm">{session?.setName} {session?.questionCount || '?'} Fragen</p>
<p className="text-gray-400 text-sm">{session?.setName || '...'} {session?.questionCount || '?'} Fragen</p>
<a href={presentUrl} target="_blank" rel="noopener" className="inline-block mt-2 text-sm text-red-500 hover:underline">📺 Präsentations-Fenster öffnen</a>
</div>
<div className="bg-white rounded-xl shadow p-6">
<h3 className="font-bold mb-2">Teilnehmer ({session?.players?.length || 0})</h3>
<h3 className="font-bold mb-2">Teilnehmer ({playerCount})</h3>
<div className="flex flex-wrap gap-2">
{(session?.players || []).map((n) => <span key={n} className="px-3 py-1 bg-red-50 text-red-700 rounded-full text-sm font-medium">{n}</span>)}
</div>
{(!session?.players || session.players.length === 0) && <p className="text-gray-400 text-sm">Noch keine Teilnehmer...</p>}
{playerCount === 0 && <p className="text-gray-400 text-sm">Noch keine Teilnehmer...</p>}
</div>
{phase === 'waiting' && (
<button onClick={start} disabled={!session?.players?.length} className="w-full py-4 bg-green-600 text-white rounded-xl text-xl font-bold hover:bg-green-700 disabled:opacity-50">
Quiz starten
<button onClick={start} disabled={playerCount === 0} className="w-full py-4 bg-green-600 text-white rounded-xl text-xl font-bold hover:bg-green-700 disabled:opacity-50">
Quiz starten ({playerCount} Teilnehmer)
</button>
)}
{phase === 'question' && (
<div className="bg-white rounded-xl shadow p-6 text-center">
<p className="text-lg mb-3">Antworten: <strong className="text-red-600">{answerCount}</strong> / {session?.players?.length || 0}</p>
<p className="text-lg mb-3">Antworten: <strong className="text-red-600">{answerCount}</strong> / {playerCount}</p>
<button onClick={showResultAction} className="w-full py-3 bg-amber-500 text-white rounded-xl font-bold hover:bg-amber-600">Ergebnis zeigen </button>
</div>
)}
@@ -448,6 +486,7 @@ function AdminApp() {
const [config, setConfig] = useState({ sets: [], answersTableId: null });
const [liveCode, setLiveCode] = useState(null);
const [liveWs, setLiveWs] = useState(null);
const [liveInitialSession, setLiveInitialSession] = useState(null);
useEffect(() => {
if (token) {
@@ -460,9 +499,10 @@ function AdminApp() {
try { await api.post('/api/admin/config', c); } catch (e) { console.error(e); }
};
const handleSessionCreated = (code, ws) => {
const handleSessionCreated = (code, ws, sessionData) => {
setLiveCode(code);
setLiveWs(ws);
setLiveInitialSession(sessionData);
setTab('live-control');
};
@@ -491,7 +531,7 @@ function AdminApp() {
<div className="max-w-3xl mx-auto p-4">
{tab === 'config' && <ConfigPage config={config} setConfig={setConfig} saveConfig={saveConfig} />}
{tab === 'live' && <LiveSetupPage config={config} token={token} onSessionCreated={handleSessionCreated} />}
{tab === 'live-control' && <LiveControlPage token={token} code={liveCode} ws={liveWs} />}
{tab === 'live-control' && <LiveControlPage token={token} code={liveCode} ws={liveWs} initialSession={liveInitialSession} />}
{tab === 'stats' && <StatsPage />}
</div>
</div>