This repository has been archived on 2026-05-06. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tippspiel/frontend/src/components/DevPanel.tsx
T
Ronny 137e14b3d1 fix: kickoff time centered without 'Uhr', unified countdown badge, DevPanel close button
- Kickoff: centered "21:00" above flags (no 'Uhr' suffix)
- Countdown: always rendered as badge (was unstyled span for <60min)
- DevPanel: added close button (✕) in panel header for reliable closing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 22:46:53 +02:00

250 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import { Match } from '../api/client';
import styles from './DevPanel.module.css';
const DEV_USERS = [
{ id: 1, name: 'Ronny M.', role: 'Editor' },
{ id: 2, name: 'Max M.', role: 'Viewer' },
{ id: 3, name: 'Anna S.', role: 'Viewer' },
];
const TIME_PRESETS = [
{ label: 'In 2 Std.', minutes: 120 },
{ label: 'In 10 Min.', minutes: 10 },
{ label: 'Jetzt +1 Min.', minutes: 1 },
{ label: 'Läuft (30)', minutes: -30 },
{ label: 'Beendet (120)', minutes: -120 },
];
const STATUS_PRESETS = [
{ label: 'TIMED', status: 'TIMED', scoreHome: null, scoreAway: null },
{ label: 'LIVE', status: 'IN_PLAY', scoreHome: 0, scoreAway: 0 },
{ label: 'Pause', status: 'PAUSED', scoreHome: 1, scoreAway: 0 },
{ label: '2:1 Fertig', status: 'FINISHED', scoreHome: 2, scoreAway: 1 },
{ label: '0:0 Fertig', status: 'FINISHED', scoreHome: 0, scoreAway: 0 },
];
interface Props {
currentUser: number;
onUserChange: (userId: number) => void;
matches: Match[];
onRefresh: () => void;
}
export default function DevPanel({ currentUser, onUserChange, matches, onRefresh }: Props) {
const [open, setOpen] = useState(false);
const [selectedMatch, setSelectedMatch] = useState<number | ''>('');
const [busy, setBusy] = useState(false);
const [log, setLog] = useState<string[]>([]);
function addLog(msg: string) {
setLog(prev => [`${new Date().toLocaleTimeString('de-DE')} ${msg}`, ...prev].slice(0, 8));
}
async function applyTime(minutes: number) {
if (!selectedMatch) return;
setBusy(true);
try {
await fetch(`/api/dev/match/${selectedMatch}/set-time`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ minutesFromNow: minutes }),
});
addLog(`✓ Spiel #${selectedMatch}: Zeit → ${minutes > 0 ? `+${minutes}` : minutes} Min.`);
onRefresh();
} catch (e) {
addLog(`✗ Fehler: ${(e as Error).message}`);
} finally {
setBusy(false);
}
}
async function applyStatus(status: string, scoreHome: number | null, scoreAway: number | null) {
if (!selectedMatch) return;
setBusy(true);
try {
await fetch(`/api/dev/match/${selectedMatch}/set-status`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status, scoreHome, scoreAway }),
});
addLog(`✓ Spiel #${selectedMatch}: Status → ${status}`);
onRefresh();
} catch (e) {
addLog(`✗ Fehler: ${(e as Error).message}`);
} finally {
setBusy(false);
}
}
async function resetTips() {
setBusy(true);
try {
await fetch('/api/dev/reset-tips', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: `dev-user-00${currentUser}` }),
});
addLog(`✓ Tipps von User ${currentUser} gelöscht`);
onRefresh();
} catch (e) {
addLog(`✗ Fehler: ${(e as Error).message}`);
} finally {
setBusy(false);
}
}
async function resetMatch(all: boolean) {
setBusy(true);
try {
const body = all ? {} : { matchId: selectedMatch };
const res = await fetch('/api/dev/reset-match', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const data = await res.json() as { count?: number; matchId?: number };
if (all) {
addLog(`${data.count ?? 0} Spiele zurückgesetzt (TIMED)`);
} else {
addLog(`✓ Spiel #${selectedMatch} zurückgesetzt (TIMED)`);
}
onRefresh();
} catch (e) {
addLog(`✗ Fehler: ${(e as Error).message}`);
} finally {
setBusy(false);
}
}
// Nur erste 20 Spiele zur Auswahl anbieten
const selectableMatches = matches.slice(0, 20);
return (
<div className={styles.wrap}>
{/* Toggle Button */}
<button className={styles.toggleBtn} onClick={() => setOpen(o => !o)}>
{open ? '✕' : '🧪'} {!open && <span className={styles.toggleLabel}>Dev</span>}
</button>
{open && (
<div className={styles.panel}>
<div className={styles.panelHeader}>
<span className={styles.panelTitle}>🧪 Simulations-Modus</span>
<button className={styles.closeBtn} onClick={() => setOpen(false)}></button>
</div>
{/* User Switcher */}
<section className={styles.section}>
<div className={styles.sectionLabel}>Aktiver User</div>
<div className={styles.userButtons}>
{DEV_USERS.map(u => (
<button
key={u.id}
className={`${styles.userBtn} ${currentUser === u.id ? styles.userBtnActive : ''}`}
onClick={() => {
onUserChange(u.id);
addLog(`→ Wechsel zu User ${u.id}: ${u.name}`);
}}
>
<span className={styles.userInitial}>{u.name.charAt(0)}</span>
<span className={styles.userName}>{u.name}</span>
<span className={styles.userRole}>{u.role}</span>
</button>
))}
</div>
</section>
{/* Match Selector */}
<section className={styles.section}>
<div className={styles.sectionLabel}>Spiel auswählen</div>
<select
className={styles.select}
value={selectedMatch}
onChange={e => setSelectedMatch(e.target.value ? parseInt(e.target.value) : '')}
>
<option value=""> Spiel wählen </option>
{selectableMatches.map(m => (
<option key={m.id} value={m.id}>
#{m.id} {m.homeTeam.shortName} vs {m.awayTeam.shortName}
</option>
))}
</select>
</section>
{/* Zeit-Manipulation */}
<section className={`${styles.section} ${!selectedMatch ? styles.sectionDisabled : ''}`}>
<div className={styles.sectionLabel}>Anstoßzeit setzen</div>
<div className={styles.presetGrid}>
{TIME_PRESETS.map(p => (
<button
key={p.label}
className={styles.presetBtn}
onClick={() => applyTime(p.minutes)}
disabled={!selectedMatch || busy}
>
{p.label}
</button>
))}
</div>
</section>
{/* Status-Manipulation */}
<section className={`${styles.section} ${!selectedMatch ? styles.sectionDisabled : ''}`}>
<div className={styles.sectionLabel}>Status setzen</div>
<div className={styles.presetGrid}>
{STATUS_PRESETS.map(p => (
<button
key={p.label}
className={`${styles.presetBtn} ${p.status === 'FINISHED' ? styles.presetBtnDanger : p.status === 'IN_PLAY' ? styles.presetBtnLive : ''}`}
onClick={() => applyStatus(p.status, p.scoreHome, p.scoreAway)}
disabled={!selectedMatch || busy}
>
{p.label}
</button>
))}
</div>
</section>
{/* Reset */}
<section className={styles.section}>
<div className={styles.sectionLabel}>Zurücksetzen</div>
<div className={styles.resetGrid}>
<button
className={styles.resetBtn}
onClick={() => resetMatch(false)}
disabled={!selectedMatch || busy}
title="Ausgewähltes Spiel auf TIMED zurücksetzen"
>
Spiel zurücksetzen
</button>
<button
className={`${styles.resetBtn} ${styles.resetBtnAll}`}
onClick={() => resetMatch(true)}
disabled={busy}
title="Alle laufenden/beendeten Spiele zurücksetzen"
>
Alle Spiele
</button>
<button
className={`${styles.resetBtn} ${styles.resetBtnTips}`}
onClick={resetTips}
disabled={busy}
>
🗑 Tipps löschen
</button>
</div>
</section>
{/* Log */}
{log.length > 0 && (
<div className={styles.log}>
{log.map((l, i) => <div key={i} className={styles.logLine}>{l}</div>)}
</div>
)}
</div>
)}
</div>
);
}