Branding & Code Refresh: Reverted to image assets (logo.png/bg.png) and applied consistent formatting
This commit is contained in:
+1
-2
@@ -5,8 +5,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Quizalarm — Admin</title>
|
||||
<link rel="icon"
|
||||
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🚨</text></svg>">
|
||||
<link rel="icon" href="/img/logo.png">
|
||||
<script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
|
||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
|
||||
+2
-6
@@ -5,8 +5,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Quizalarm</title>
|
||||
<link rel="icon"
|
||||
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🚨</text></svg>">
|
||||
<link rel="icon" href="/img/logo.png">
|
||||
<script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
|
||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
@@ -14,10 +13,7 @@
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background: #111111;
|
||||
background-image:
|
||||
radial-gradient(ellipse at 20% 50%, rgba(220, 38, 38, 0.08) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 80% 50%, rgba(245, 158, 11, 0.06) 0%, transparent 50%);
|
||||
background: #111111 url('/img/bg.png') center/cover fixed;
|
||||
}
|
||||
|
||||
/* Answer button base colors */
|
||||
|
||||
+2
-2
@@ -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">
|
||||
<div className="text-center text-4xl mb-2">🚨</div>
|
||||
<img src="/img/logo.png" alt="Quizalarm" className="w-16 h-16 mx-auto mb-2" />
|
||||
<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>}
|
||||
@@ -565,7 +565,7 @@ function AdminApp() {
|
||||
<div className="min-h-screen bg-stone-100">
|
||||
<div className="bg-white shadow border-b-2 border-red-600">
|
||||
<div className="max-w-3xl mx-auto px-4 py-3 flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold flex items-center gap-2"><span>🚨</span> Quizalarm Admin</h1>
|
||||
<h1 className="text-xl font-bold flex items-center gap-2"><img src="/img/logo.png" alt="" className="w-8 h-8 inline-block" /> Quizalarm Admin</h1>
|
||||
<a href="/" className="text-gray-400 hover:text-gray-600 text-sm">← Zur App</a>
|
||||
</div>
|
||||
<div className="max-w-3xl mx-auto px-4 flex gap-1 overflow-x-auto">
|
||||
|
||||
+13
-20
@@ -34,6 +34,11 @@ function normalizeText(t) {
|
||||
.replace(/[^a-z0-9]/g, '');
|
||||
}
|
||||
|
||||
function Logo({ size }) {
|
||||
const cls = size === 'lg' ? 'w-24 h-24' : size === 'md' ? 'w-16 h-16' : 'w-10 h-10';
|
||||
return <img src="/img/logo.png" alt="Quizalarm" className={`${cls} mx-auto`} />;
|
||||
}
|
||||
|
||||
/* ================================================================== */
|
||||
/* Shared Components */
|
||||
/* ================================================================== */
|
||||
@@ -148,8 +153,8 @@ function BackButton({ onClick, label }) {
|
||||
function HomePage({ navigate }) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center p-6">
|
||||
<div className="text-6xl mb-2">🚨</div>
|
||||
<h1 className="text-5xl font-extrabold mb-2 fire-gradient">Quizalarm</h1>
|
||||
<Logo size="lg" />
|
||||
<h1 className="text-5xl font-extrabold mb-2 mt-4 fire-gradient">Quizalarm</h1>
|
||||
<p className="text-gray-400 mb-10 text-lg">Lernen. Quizzen. Wissen.</p>
|
||||
<div className="w-full max-w-sm space-y-4">
|
||||
<button onClick={() => navigate('join')} className="w-full py-4 bg-red-600 hover:bg-red-700 rounded-xl text-xl font-bold transition-all shadow-lg shadow-red-900/30">🎮 Live-Quiz beitreten</button>
|
||||
@@ -177,7 +182,7 @@ function JoinPage({ navigate }) {
|
||||
<div className="min-h-screen flex flex-col items-center justify-center p-6">
|
||||
<h2 className="text-3xl font-bold mb-6">🎮 Live-Quiz beitreten</h2>
|
||||
<div className="w-full max-w-sm space-y-4">
|
||||
<input className="w-full p-4 rounded-xl bg-gray-800 text-white text-center text-2xl tracking-widest border-2 border-gray-700 focus:border-red-500 outline-none uppercase" placeholder="CODE" value={code} onChange={(e) => setCode(e.target.value)} maxLength={6} />
|
||||
<input className="w-full p-4 rounded-xl bg-gray-800 text-white text-center text-2xl tracking-widest border-2 border-gray-700 focus:border-red-500 outline-none uppercase" placeholder="CODE" value={code} onChange={(e) => setCode(e.target.value)} maxLength={20} />
|
||||
<input className="w-full p-4 rounded-xl bg-gray-800 text-white text-center text-xl border-2 border-gray-700 focus:border-red-500 outline-none" placeholder="Dein Name" value={name} onChange={(e) => setName(e.target.value)} maxLength={20} onKeyDown={(e) => e.key === 'Enter' && handleJoin()} />
|
||||
{error && <p className="text-red-400 text-center">{error}</p>}
|
||||
<button onClick={handleJoin} className="w-full py-4 bg-red-600 hover:bg-red-700 rounded-xl text-xl font-bold shadow-lg shadow-red-900/30">Beitreten</button>
|
||||
@@ -341,7 +346,7 @@ function LivePlayPage({ params, navigate }) {
|
||||
}
|
||||
|
||||
/* ================================================================== */
|
||||
/* Solo Select Page — with progress check */
|
||||
/* Solo Select Page */
|
||||
/* ================================================================== */
|
||||
|
||||
function SoloSelectPage({ navigate }) {
|
||||
@@ -366,7 +371,6 @@ function SoloSelectPage({ navigate }) {
|
||||
navigate('solo-quiz', { setId, setName: setName2, name: name.trim(), shuffle: doShuffle });
|
||||
}
|
||||
} catch (e) {
|
||||
// If progress check fails, just start fresh
|
||||
navigate('solo-quiz', { setId, setName: setName2, name: name.trim(), shuffle: doShuffle });
|
||||
}
|
||||
setChecking(false);
|
||||
@@ -402,7 +406,6 @@ function SoloSelectPage({ navigate }) {
|
||||
</label>
|
||||
{error && <p className="text-red-400">{error}</p>}
|
||||
|
||||
{/* Progress dialog */}
|
||||
{progressDialog && (
|
||||
<div className="bg-gray-800 rounded-xl p-5 border-2 border-amber-500 space-y-3">
|
||||
<h3 className="font-bold text-lg text-amber-400">📌 Gespeicherter Fortschritt</h3>
|
||||
@@ -433,7 +436,7 @@ function SoloSelectPage({ navigate }) {
|
||||
}
|
||||
|
||||
/* ================================================================== */
|
||||
/* Solo Quiz Page — immediate save per answer + continue support */
|
||||
/* Solo Quiz Page */
|
||||
/* ================================================================== */
|
||||
|
||||
function SoloQuizPage({ params, navigate }) {
|
||||
@@ -457,11 +460,8 @@ function SoloQuizPage({ params, navigate }) {
|
||||
useEffect(() => {
|
||||
api.get(`/api/sets/${params.setId}/questions`).then((qs) => {
|
||||
setAllQuestions(qs);
|
||||
// Filter out already answered questions
|
||||
let remaining = qs.filter((q) => !priorIds.current.has(q.id));
|
||||
if (params.shuffle && remaining.length > 0) {
|
||||
remaining = shuffleArray(remaining);
|
||||
}
|
||||
if (params.shuffle && remaining.length > 0) remaining = shuffleArray(remaining);
|
||||
setQuestions(remaining);
|
||||
}).catch((e) => setError(e.message));
|
||||
}, []);
|
||||
@@ -476,7 +476,6 @@ function SoloQuizPage({ params, navigate }) {
|
||||
|
||||
if (error) return <div className="min-h-screen flex items-center justify-center text-red-400 text-xl p-6 text-center">{error}</div>;
|
||||
if (!questions.length && allQuestions.length > 0) {
|
||||
// All questions already answered → go straight to results
|
||||
const allResults = priorResults.map((a) => ({ Frage_ID: a.Frage_ID, Richtig: a.Richtig, Punkte: a.Punkte }));
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center p-6">
|
||||
@@ -502,10 +501,7 @@ function SoloQuizPage({ params, navigate }) {
|
||||
antworten: shuffledAnswers,
|
||||
};
|
||||
|
||||
const feedback = showFeedback ? {
|
||||
correctKeys, correctText: typ === 'freitext' ? correctText : null, isCorrect,
|
||||
} : null;
|
||||
|
||||
const feedback = showFeedback ? { correctKeys, correctText: typ === 'freitext' ? correctText : null, isCorrect } : null;
|
||||
const totalProgress = priorIds.current.size + index + 1;
|
||||
const totalAll = allQuestions.length;
|
||||
|
||||
@@ -521,7 +517,6 @@ function SoloQuizPage({ params, navigate }) {
|
||||
const selectedSet = new Set(selected.map((s) => s.toUpperCase()));
|
||||
ok = correct.length === selectedSet.size && correct.every((c) => selectedSet.has(c));
|
||||
}
|
||||
|
||||
setCorrectKeys(correct);
|
||||
setIsCorrect(ok);
|
||||
setShowFeedback(true);
|
||||
@@ -537,7 +532,6 @@ function SoloQuizPage({ params, navigate }) {
|
||||
Session_ID: sessionId.current, Modus: 'Solo', Zeitstempel: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Save immediately to Baserow
|
||||
setSaving(true);
|
||||
api.post('/api/results/single', answerData)
|
||||
.catch((e) => console.error('Save error:', e))
|
||||
@@ -594,7 +588,7 @@ function SoloQuizPage({ params, navigate }) {
|
||||
}
|
||||
|
||||
/* ================================================================== */
|
||||
/* Solo Result Page — matches by Frage_ID */
|
||||
/* Solo Result Page */
|
||||
/* ================================================================== */
|
||||
|
||||
function SoloResultPage({ params, navigate }) {
|
||||
@@ -603,7 +597,6 @@ function SoloResultPage({ params, navigate }) {
|
||||
const correct = results.filter((r) => r.Richtig).length;
|
||||
const pct = total > 0 ? Math.round((correct / total) * 100) : 0;
|
||||
|
||||
// Map questions by ID for category lookup
|
||||
const qMap = {};
|
||||
(questions || []).forEach((q) => { qMap[q.id] = q; });
|
||||
|
||||
|
||||
+19
-66
@@ -21,71 +21,33 @@ function PresentApp() {
|
||||
useEffect(() => {
|
||||
const code = location.hash.replace('#', '');
|
||||
const params = new URLSearchParams(location.search);
|
||||
// Accept both ?pt= (present token) and ?token= (legacy/admin password)
|
||||
const token = params.get('pt') || params.get('token');
|
||||
|
||||
if (!code || !token) {
|
||||
setPhase('need-config');
|
||||
return;
|
||||
}
|
||||
if (!code || !token) { setPhase('need-config'); return; }
|
||||
|
||||
const ws = new WebSocket(wsUrl());
|
||||
|
||||
ws.onopen = () => {
|
||||
ws.send(JSON.stringify({ type: 'present-join', token, code }));
|
||||
};
|
||||
|
||||
ws.onopen = () => { ws.send(JSON.stringify({ type: 'present-join', token, code })); };
|
||||
ws.onmessage = (e) => {
|
||||
const msg = JSON.parse(e.data);
|
||||
switch (msg.type) {
|
||||
case 'present-joined':
|
||||
setSession(msg.session);
|
||||
setLeaderboard(msg.session.leaderboard || []);
|
||||
setPhase('waiting');
|
||||
break;
|
||||
case 'player-joined':
|
||||
case 'player-left':
|
||||
setSession((s) => s ? { ...s, players: msg.players, playerCount: msg.playerCount } : s);
|
||||
break;
|
||||
case 'game-start':
|
||||
setQTotal(msg.totalQuestions);
|
||||
break;
|
||||
case 'present-joined': setSession(msg.session); setLeaderboard(msg.session.leaderboard || []); setPhase('waiting'); break;
|
||||
case 'player-joined': case 'player-left':
|
||||
setSession((s) => s ? { ...s, players: msg.players, playerCount: msg.playerCount } : s); break;
|
||||
case 'game-start': setQTotal(msg.totalQuestions); break;
|
||||
case 'question':
|
||||
setQuestion(msg.question);
|
||||
setQIndex(msg.index);
|
||||
setQTotal(msg.total);
|
||||
setTimeLimit(msg.timeLimit);
|
||||
setTimeLeft(msg.timeLimit);
|
||||
setAnswerCount(0);
|
||||
setResult(null);
|
||||
setPhase('question');
|
||||
break;
|
||||
case 'answer-count':
|
||||
setAnswerCount(msg.count);
|
||||
break;
|
||||
case 'time-up':
|
||||
clearInterval(timerRef.current);
|
||||
break;
|
||||
setQuestion(msg.question); setQIndex(msg.index); setQTotal(msg.total);
|
||||
setTimeLimit(msg.timeLimit); setTimeLeft(msg.timeLimit);
|
||||
setAnswerCount(0); setResult(null); setPhase('question'); break;
|
||||
case 'answer-count': setAnswerCount(msg.count); break;
|
||||
case 'time-up': clearInterval(timerRef.current); break;
|
||||
case 'question-result':
|
||||
setResult(msg);
|
||||
setLeaderboard(msg.leaderboard);
|
||||
setPhase('result');
|
||||
clearInterval(timerRef.current);
|
||||
break;
|
||||
setResult(msg); setLeaderboard(msg.leaderboard); setPhase('result'); clearInterval(timerRef.current); break;
|
||||
case 'game-end':
|
||||
setLeaderboard(msg.leaderboard);
|
||||
setPhase('final');
|
||||
clearInterval(timerRef.current);
|
||||
break;
|
||||
case 'error':
|
||||
setError(msg.message);
|
||||
setPhase('error');
|
||||
break;
|
||||
setLeaderboard(msg.leaderboard); setPhase('final'); clearInterval(timerRef.current); break;
|
||||
case 'error': setError(msg.message); setPhase('error'); break;
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = () => setPhase('error');
|
||||
|
||||
return () => ws.close();
|
||||
}, []);
|
||||
|
||||
@@ -99,15 +61,13 @@ function PresentApp() {
|
||||
}
|
||||
}, [phase, timeLimit]);
|
||||
|
||||
if (phase === 'need-config') {
|
||||
return (
|
||||
if (phase === 'need-config') return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center p-8">
|
||||
<div className="text-6xl mb-4">🚨</div>
|
||||
<img src="/img/logo.png" alt="Quizalarm" className="w-24 h-24 mb-4" />
|
||||
<h1 className="text-3xl font-bold mb-4 fire-gradient">Quizalarm — Präsentation</h1>
|
||||
<p className="text-gray-400 mb-4 text-center">Öffne diesen Link aus dem Admin-Panel.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (phase === 'error') return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center text-red-400 text-3xl">
|
||||
@@ -120,7 +80,7 @@ function PresentApp() {
|
||||
|
||||
if (phase === 'waiting') return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center p-8">
|
||||
<div className="text-6xl mb-4">🚨</div>
|
||||
<img src="/img/logo.png" alt="Quizalarm" className="w-24 h-24 mb-4" />
|
||||
<h1 className="text-4xl font-extrabold mb-4 fire-gradient">Quizalarm</h1>
|
||||
<p className="text-gray-400 text-xl mb-6">{session?.setName}</p>
|
||||
<div className="text-8xl font-mono font-extrabold tracking-widest text-red-500 mb-8">{session?.code}</div>
|
||||
@@ -155,9 +115,7 @@ function PresentApp() {
|
||||
{(question?.antworten || []).map((a) => {
|
||||
const colors = { A: 'bg-red-600', B: 'bg-blue-600', C: 'bg-amber-600', D: 'bg-green-600' };
|
||||
return (
|
||||
<div key={a.key} className={`${colors[a.key] || 'bg-gray-600'} rounded-2xl flex items-center justify-center p-6 text-2xl font-bold`}>
|
||||
{a.text}
|
||||
</div>
|
||||
<div key={a.key} className={`${colors[a.key] || 'bg-gray-600'} rounded-2xl flex items-center justify-center p-6 text-2xl font-bold`}>{a.text}</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -176,7 +134,6 @@ function PresentApp() {
|
||||
<h2 className="text-2xl text-gray-400 text-center mb-2">Frage {qIndex + 1} / {qTotal}</h2>
|
||||
<h2 className="text-3xl font-bold text-center mb-6">{question?.frage}</h2>
|
||||
{question?.bild && <img src={question.bild} className="max-h-48 mx-auto rounded-xl mb-6" alt="" />}
|
||||
|
||||
{question?.typ?.toLowerCase() !== 'freitext' && (
|
||||
<div className="grid grid-cols-2 gap-4 mb-8">
|
||||
{(question?.antworten || []).map((a) => {
|
||||
@@ -192,15 +149,11 @@ function PresentApp() {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{question?.typ?.toLowerCase() === 'freitext' && (
|
||||
<div className="text-center mb-8">
|
||||
<div className="inline-block bg-green-600 ring-4 ring-green-400 rounded-2xl px-8 py-4 text-2xl font-bold">
|
||||
✓ {result?.correctAnswer?.[0]}
|
||||
</div>
|
||||
<div className="inline-block bg-green-600 ring-4 ring-green-400 rounded-2xl px-8 py-4 text-2xl font-bold">✓ {result?.correctAnswer?.[0]}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1">
|
||||
<h3 className="text-2xl font-bold text-center mb-4">🏆 Leaderboard</h3>
|
||||
<div className="max-w-xl mx-auto">
|
||||
|
||||
+2
-6
@@ -5,8 +5,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Quizalarm — Präsentation</title>
|
||||
<link rel="icon"
|
||||
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🚨</text></svg>">
|
||||
<link rel="icon" href="/img/logo.png">
|
||||
<script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
|
||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
@@ -14,10 +13,7 @@
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background: #111111;
|
||||
background-image:
|
||||
radial-gradient(ellipse at 20% 50%, rgba(220, 38, 38, 0.08) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 80% 50%, rgba(245, 158, 11, 0.06) 0%, transparent 50%);
|
||||
background: #111111 url('/img/bg.png') center/cover fixed;
|
||||
}
|
||||
|
||||
/* Answer colors for presentation view */
|
||||
|
||||
Reference in New Issue
Block a user