UI overhaul: fire-gradient theme and visual polish; logic: added answer shuffling and improved Baserow error handling

This commit is contained in:
2026-04-16 06:54:54 +02:00
parent 12d2d51dc3
commit e993eef125
10 changed files with 342 additions and 216 deletions
+29 -25
View File
@@ -17,12 +17,14 @@ function PresentApp() {
const [answerCount, setAnswerCount] = useState(0);
const [result, setResult] = useState(null);
const [leaderboard, setLeaderboard] = useState([]);
const [error, setError] = useState('');
const timerRef = useRef(null);
const wsRef = useRef(null);
useEffect(() => {
const code = location.hash.replace('#', '');
const token = prompt_alternative();
const params = new URLSearchParams(location.search);
const token = params.get('token');
if (!code || !token) {
setPhase('need-config');
@@ -79,15 +81,18 @@ function PresentApp() {
clearInterval(timerRef.current);
break;
case 'error':
setError(msg.message);
setPhase('error');
break;
}
};
ws.onerror = () => setPhase('error');
ws.onclose = () => { if (phase !== 'final') setPhase('error'); };
return () => ws.close();
}, []);
// Timer
useEffect(() => {
clearInterval(timerRef.current);
if (phase === 'question' && timeLimit > 0) {
@@ -98,32 +103,31 @@ function PresentApp() {
}
}, [phase, timeLimit]);
// Simple token retrieval (read from URL param or sessionStorage)
function prompt_alternative() {
const params = new URLSearchParams(location.search);
let t = params.get('token');
if (!t) {
try { t = sessionStorage.getItem('qa_token'); } catch (e) { }
}
return t;
}
if (phase === 'need-config') {
return (
<div className="min-h-screen flex flex-col items-center justify-center p-8">
<h1 className="text-3xl font-bold mb-4">📺 Präsentation</h1>
<p className="text-gray-400 mb-4">Öffne diesen Link aus dem Admin-Panel oder füge <code>?token=PASSWORT#CODE</code> zur URL hinzu.</p>
<div className="text-6xl mb-4">🚨</div>
<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.<br />Die URL muss <code className="bg-gray-800 px-2 py-1 rounded">?token=PASSWORT#CODE</code> enthalten.</p>
</div>
);
}
if (phase === 'error') return <div className="min-h-screen flex items-center justify-center text-red-400 text-3xl">Verbindungsfehler</div>;
if (phase === 'error') return (
<div className="min-h-screen flex flex-col items-center justify-center text-red-400 text-3xl">
<p> Verbindungsfehler</p>
{error && <p className="text-lg mt-2">{error}</p>}
</div>
);
if (phase === 'connect') return <div className="min-h-screen flex items-center justify-center animate-pulse text-2xl text-amber-400">Verbinde...</div>;
if (phase === 'waiting') return (
<div className="min-h-screen flex flex-col items-center justify-center p-8">
<h1 className="text-4xl font-extrabold mb-4 text-transparent bg-clip-text bg-gradient-to-r from-indigo-400 to-pink-500"> Quizalarm</h1>
<div className="text-6xl mb-4">🚨</div>
<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-indigo-400 mb-8">{session?.code}</div>
<div className="text-8xl font-mono font-extrabold tracking-widest text-red-500 mb-8">{session?.code}</div>
<p className="text-2xl mb-4">{session?.players?.length || 0} Teilnehmer</p>
<div className="flex flex-wrap gap-3 justify-center max-w-2xl">
{(session?.players || []).map((n) => (
@@ -141,7 +145,7 @@ function PresentApp() {
<div className="flex justify-between items-center mb-4">
<span className="text-gray-400 text-xl">Frage {qIndex + 1} / {qTotal}</span>
<span className="text-xl">{answerCount} / {session?.players?.length || 0} 💬</span>
{timeLimit > 0 && <span className="text-3xl font-bold">{Math.ceil(timeLeft)}s</span>}
{timeLimit > 0 && <span className="text-3xl font-bold text-amber-400">{Math.ceil(timeLeft)}s</span>}
</div>
{timeLimit > 0 && (
<div className="w-full bg-gray-700 rounded-full h-3 mb-6 overflow-hidden">
@@ -154,7 +158,7 @@ function PresentApp() {
<div className="grid grid-cols-2 gap-4 flex-1 max-h-96">
{(question?.antworten || []).map((a) => (
<div key={a.key} className={`${COLORS[a.key]} rounded-2xl flex items-center justify-center p-6 text-2xl font-bold`}>
<span className="opacity-60 mr-3 text-3xl">{a.key}</span> {a.text}
{a.text}
</div>
))}
</div>
@@ -171,7 +175,7 @@ function PresentApp() {
return (
<div className="min-h-screen flex flex-col p-8">
<h2 className="text-3xl font-bold text-center mb-2">Ergebnis</h2>
<p className="text-center text-xl text-green-400 mb-6">Richtig: {result?.correctAnswer?.join(', ')}</p>
<p className="text-center text-xl text-amber-400 mb-6">Richtig: {result?.correctAnswer?.join(', ')}</p>
<div className="flex gap-4 justify-center items-end mb-8 h-48">
{Object.entries(result?.stats || {}).map(([k, v]) => (
<div key={k} className="flex flex-col items-center flex-1 max-w-32">
@@ -185,8 +189,8 @@ function PresentApp() {
<h3 className="text-2xl font-bold text-center mb-4">🏆 Leaderboard</h3>
<div className="max-w-xl mx-auto">
{leaderboard.slice(0, 8).map((e, i) => (
<div key={e.name} className={`flex justify-between py-3 px-5 rounded-xl mb-2 text-xl ${i === 0 ? 'bg-yellow-600' : i === 1 ? 'bg-gray-500' : i === 2 ? 'bg-amber-700' : 'bg-gray-800'}`}>
<span className="font-bold">{e.rank}. {e.name}</span>
<div key={e.name} className={`flex justify-between py-3 px-5 rounded-xl mb-2 text-xl ${i === 0 ? 'bg-amber-600' : i === 1 ? 'bg-gray-500' : i === 2 ? 'bg-amber-800' : 'bg-gray-800'}`}>
<span className="font-bold">{i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${e.rank}.`} {e.name}</span>
<span className="font-extrabold">{e.score}</span>
</div>
))}
@@ -198,10 +202,10 @@ function PresentApp() {
if (phase === 'final') return (
<div className="min-h-screen flex flex-col items-center justify-center p-8">
<h2 className="text-5xl font-extrabold mb-8">🏁 Quiz beendet!</h2>
<h2 className="text-5xl font-extrabold mb-8 fire-gradient">Quiz beendet!</h2>
<div className="w-full max-w-xl">
{leaderboard.map((e, i) => (
<div key={e.name} className={`flex justify-between py-4 px-6 rounded-xl mb-2 text-2xl ${i === 0 ? 'bg-yellow-600' : i === 1 ? 'bg-gray-500' : i === 2 ? 'bg-amber-700' : 'bg-gray-800'}`}>
<div key={e.name} className={`flex justify-between py-4 px-6 rounded-xl mb-2 text-2xl ${i === 0 ? 'bg-amber-600' : i === 1 ? 'bg-gray-500' : i === 2 ? 'bg-amber-800' : 'bg-gray-800'}`}>
<span className="font-bold">{i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${e.rank}.`} {e.name}</span>
<span className="font-extrabold">{e.score}</span>
</div>
@@ -210,7 +214,7 @@ function PresentApp() {
</div>
);
return <div className="min-h-screen flex items-center justify-center animate-pulse text-2xl">Verbinde...</div>;
return <div className="min-h-screen flex items-center justify-center animate-pulse text-2xl text-amber-400">Verbinde...</div>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<PresentApp />);