Admin panel sync improvements, Baserow field handling (Single Select support), and UI emoji swap
This commit is contained in:
+63
-23
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user