feat: Ergebnis-Banner, Dev-Simulations-Panel & Spiele-Reset
- MatchCard: farbiger Ergebnis-Banner (Exakt/Tendenz/Falsch) ersetzt tipRow, passender Card-Glow je Ergebnis; Lucide-Icons (lucide-react) - MatchCard: Ändern-Button links, vertikal mittig zur Tipp-Box ausgerichtet - DevPanel: Simulationsmodus mit User-Switcher, Zeit- & Status-Presets - DevPanel: Reset-Section mit "Spiel zurücksetzen", "Alle Spiele" und "Tipps löschen" (3 Buttons, farblich differenziert) - Backend: dev.ts mit set-time, set-status, reset-tips, reset-match - reset-match stellt Original-Datum wieder her (original_utc_date Spalte) - set-time sichert Original-Datum per COALESCE beim ersten Aufruf - Supabase Migration: original_utc_date TIMESTAMPTZ zu matches hinzugefügt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
/* Dev Panel — floating bottom right */
|
||||
.wrap {
|
||||
position: fixed;
|
||||
bottom: 80px; /* über der Nav-Bar */
|
||||
right: 16px;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toggleBtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 24px;
|
||||
background: rgba(254,174,50,0.15);
|
||||
border: 1px solid rgba(254,174,50,0.3);
|
||||
color: var(--gold);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.toggleBtn:hover { background: rgba(254,174,50,0.25); }
|
||||
.toggleLabel { font-size: 12px; }
|
||||
|
||||
/* Panel */
|
||||
.panel {
|
||||
width: 300px;
|
||||
background: #111827;
|
||||
border: 1px solid rgba(254,174,50,0.2);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 50px rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.panelHeader {
|
||||
padding: 12px 16px;
|
||||
background: rgba(254,174,50,0.08);
|
||||
border-bottom: 1px solid rgba(254,174,50,0.15);
|
||||
}
|
||||
|
||||
.panelTitle {
|
||||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--gold);
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
.section {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.section:last-child { border-bottom: none; }
|
||||
|
||||
.sectionDisabled { opacity: 0.4; pointer-events: none; }
|
||||
|
||||
.sectionLabel {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* User buttons */
|
||||
.userButtons { display: flex; flex-direction: column; gap: 6px; }
|
||||
|
||||
.userBtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 10px;
|
||||
background: var(--surface-high);
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
text-align: left;
|
||||
}
|
||||
.userBtn:hover { border-color: rgba(75,183,248,0.3); }
|
||||
.userBtnActive {
|
||||
background: rgba(75,183,248,0.12);
|
||||
border-color: rgba(75,183,248,0.4);
|
||||
}
|
||||
|
||||
.userInitial {
|
||||
width: 28px; height: 28px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-dim);
|
||||
color: var(--primary);
|
||||
font-weight: 800;
|
||||
font-size: 12px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.userBtnActive .userInitial { background: rgba(75,183,248,0.2); }
|
||||
|
||||
.userName { font-size: 13px; font-weight: 600; color: var(--text-primary); flex: 1; }
|
||||
.userRole { font-size: 10px; color: var(--text-muted); }
|
||||
|
||||
/* Select */
|
||||
.select {
|
||||
width: 100%;
|
||||
background: var(--surface-high);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
color: var(--text-primary);
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
.select:focus { border-color: var(--primary); }
|
||||
|
||||
/* Preset grid */
|
||||
.presetGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.presetBtn {
|
||||
padding: 7px 8px;
|
||||
border-radius: 8px;
|
||||
background: var(--surface-high);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
text-align: center;
|
||||
}
|
||||
.presetBtn:hover:not(:disabled) { border-color: var(--primary); color: var(--primary); }
|
||||
.presetBtn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.presetBtnLive { color: var(--error) !important; border-color: rgba(248,113,113,0.2) !important; }
|
||||
.presetBtnDanger { color: #34D399 !important; border-color: rgba(52,211,153,0.2) !important; }
|
||||
|
||||
/* Reset */
|
||||
.resetGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* Tipps-löschen nimmt volle Breite (dritte Zeile) */
|
||||
.resetBtnTips {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.resetBtn {
|
||||
padding: 8px 6px;
|
||||
border-radius: 8px;
|
||||
background: rgba(75,183,248,0.08);
|
||||
border: 1px solid rgba(75,183,248,0.18);
|
||||
color: var(--primary);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.resetBtn:hover:not(:disabled) { background: rgba(75,183,248,0.16); }
|
||||
.resetBtn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
|
||||
/* "Alle Spiele" — etwas warnender */
|
||||
.resetBtnAll {
|
||||
background: rgba(254,174,50,0.08);
|
||||
border-color: rgba(254,174,50,0.2);
|
||||
color: var(--gold);
|
||||
}
|
||||
.resetBtnAll:hover:not(:disabled) { background: rgba(254,174,50,0.16); }
|
||||
|
||||
/* Tipps löschen — rot */
|
||||
.resetBtnTips {
|
||||
background: rgba(248,113,113,0.08);
|
||||
border-color: rgba(248,113,113,0.18);
|
||||
color: var(--error);
|
||||
}
|
||||
.resetBtnTips:hover:not(:disabled) { background: rgba(248,113,113,0.18); }
|
||||
|
||||
/* Log */
|
||||
.log {
|
||||
padding: 10px 16px;
|
||||
background: rgba(0,0,0,0.2);
|
||||
border-top: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
.logLine {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
font-family: monospace;
|
||||
line-height: 1.6;
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
import { useState } from 'react';
|
||||
import { api, 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>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -183,6 +183,13 @@
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
/* Wenn tipRow zum Banner wird */
|
||||
.tipRow.resultBanner {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
min-height: unset;
|
||||
}
|
||||
|
||||
.tipBtn {
|
||||
width: 100%;
|
||||
max-width: 260px;
|
||||
@@ -191,20 +198,36 @@
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* Existing tip display */
|
||||
/* Existing tip display — 3-column grid so center stays centered */
|
||||
.tipDisplay {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tipLeft { display: flex; justify-content: flex-start; align-items: center; }
|
||||
.tipRight { display: flex; justify-content: flex-end; align-items: center; }
|
||||
|
||||
/* Banner-Variante: Icon + Label als Zeile */
|
||||
.bannerLeft {
|
||||
gap: 6px;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.tipCenter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tipLabel {
|
||||
font-size: 12px;
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.tipScore {
|
||||
@@ -213,21 +236,90 @@
|
||||
font-size: 17px;
|
||||
color: var(--primary);
|
||||
background: var(--primary-dim);
|
||||
padding: 3px 12px;
|
||||
padding: 3px 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(75,183,248,0.15);
|
||||
}
|
||||
|
||||
.points {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
/* ── Ergebnis-Banner ──────────────────────────────────────────── */
|
||||
.resultBanner {
|
||||
margin: 0 -24px -20px !important;
|
||||
padding: 10px 24px !important;
|
||||
border-bottom-left-radius: 16px;
|
||||
border-bottom-right-radius: 16px;
|
||||
}
|
||||
|
||||
.exact { background: rgba(52,211,153,0.12); color: var(--success); border: 1px solid rgba(52,211,153,0.2); }
|
||||
.tendency { background: rgba(75,183,248,0.12); color: var(--primary); border: 1px solid rgba(75,183,248,0.2); }
|
||||
.wrong { background: rgba(248,113,113,0.10); color: var(--error); border: 1px solid rgba(248,113,113,0.15); }
|
||||
.resultIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.resultLabel {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* tipScore-Variante im Banner: dunkler Hintergrund statt Primary-Blau */
|
||||
.tipScoreBanner {
|
||||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||
font-weight: 800;
|
||||
font-size: 17px;
|
||||
padding: 3px 14px;
|
||||
border-radius: 8px;
|
||||
background: rgba(0,0,0,0.18);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
.resultPoints {
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.01em;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
/* Farb-Varianten Banner */
|
||||
.exact {
|
||||
background: linear-gradient(90deg, rgba(52,211,153,0.18) 0%, rgba(52,211,153,0.08) 100%);
|
||||
color: #4ade80;
|
||||
border-top: 1px solid rgba(52,211,153,0.20);
|
||||
}
|
||||
|
||||
.tendency {
|
||||
background: linear-gradient(90deg, rgba(75,183,248,0.18) 0%, rgba(75,183,248,0.08) 100%);
|
||||
color: var(--primary);
|
||||
border-top: 1px solid rgba(75,183,248,0.20);
|
||||
}
|
||||
|
||||
.wrong {
|
||||
background: linear-gradient(90deg, rgba(248,113,113,0.15) 0%, rgba(248,113,113,0.06) 100%);
|
||||
color: var(--error);
|
||||
border-top: 1px solid rgba(248,113,113,0.18);
|
||||
}
|
||||
|
||||
/* Card-Glow je Ergebnis */
|
||||
.glowExact {
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(52,211,153,0.18),
|
||||
0 10px 30px rgba(52,211,153,0.07),
|
||||
inset 0 1px 0 rgba(255,255,255,0.07) !important;
|
||||
}
|
||||
|
||||
.glowTendency {
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(75,183,248,0.18),
|
||||
0 10px 30px rgba(75,183,248,0.07),
|
||||
inset 0 1px 0 rgba(255,255,255,0.07) !important;
|
||||
}
|
||||
|
||||
.glowWrong {
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(248,113,113,0.15),
|
||||
0 10px 30px rgba(248,113,113,0.05),
|
||||
inset 0 1px 0 rgba(255,255,255,0.07) !important;
|
||||
}
|
||||
|
||||
.editBtn {
|
||||
background: transparent;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Check, TrendingUp, X } from 'lucide-react';
|
||||
import { Match } from '../api/client';
|
||||
import styles from './MatchCard.module.css';
|
||||
|
||||
@@ -47,9 +48,19 @@ export default function MatchCard({ match, onTip }: Props) {
|
||||
const isFinished = match.status === 'FINISHED';
|
||||
const isLive = match.status === 'IN_PLAY' || match.status === 'PAUSED';
|
||||
const hasTip = !!match.userTip;
|
||||
const points = match.userTip?.points ?? null;
|
||||
const resultClass =
|
||||
points === 3 ? styles.exact :
|
||||
points === 1 ? styles.tendency :
|
||||
(points === 0 && isFinished) ? styles.wrong : '';
|
||||
|
||||
const glowClass =
|
||||
isFinished && points === 3 ? styles.glowExact :
|
||||
isFinished && points === 1 ? styles.glowTendency :
|
||||
isFinished && points === 0 ? styles.glowWrong : '';
|
||||
|
||||
return (
|
||||
<div className={`card ${styles.card} ${isLive ? styles.live : ''}`}>
|
||||
<div className={`card ${styles.card} ${isLive ? styles.live : ''} ${glowClass}`}>
|
||||
|
||||
{/* Top row: Status / Kickoff / Badges */}
|
||||
<div className={styles.topRow}>
|
||||
@@ -92,27 +103,56 @@ export default function MatchCard({ match, onTip }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tipp area */}
|
||||
<div className={styles.tipRow}>
|
||||
{/* Tipp area — wird zum farbigen Banner wenn Punkte ausgewertet */}
|
||||
<div className={`${styles.tipRow} ${hasTip && points !== null ? `${styles.resultBanner} ${resultClass}` : ''}`}>
|
||||
{hasTip ? (
|
||||
<div className={styles.tipDisplay}>
|
||||
<span className={styles.tipLabel}>Dein Tipp</span>
|
||||
<span className={styles.tipScore}>
|
||||
{match.userTip!.home} : {match.userTip!.away}
|
||||
</span>
|
||||
{match.userTip!.points !== null && (
|
||||
<span className={`${styles.points} ${
|
||||
match.userTip!.points === 3 ? styles.exact :
|
||||
match.userTip!.points === 1 ? styles.tendency : styles.wrong
|
||||
}`}>
|
||||
{match.userTip!.points === 3 ? '🎯 3 Punkte' :
|
||||
match.userTip!.points === 1 ? '✓ 1 Punkt' : '✗ 0 Punkte'}
|
||||
</span>
|
||||
)}
|
||||
{match.tippable && (
|
||||
<button className={styles.editBtn} onClick={onTip}>Ändern</button>
|
||||
)}
|
||||
</div>
|
||||
points !== null ? (
|
||||
/* ── Auswertungs-Banner ── */
|
||||
<div className={styles.tipDisplay}>
|
||||
{/* Links: Icon + Ergebnis-Label nebeneinander, zentriert zur Tippbox */}
|
||||
<div className={`${styles.tipLeft} ${styles.bannerLeft}`}>
|
||||
<span className={styles.resultIcon}>
|
||||
{points === 3 ? <Check size={14} strokeWidth={3} /> :
|
||||
points === 1 ? <TrendingUp size={14} strokeWidth={2.5} /> :
|
||||
<X size={14} strokeWidth={3} />}
|
||||
</span>
|
||||
<span className={styles.resultLabel}>
|
||||
{points === 3 ? 'Exakt' : points === 1 ? 'Tendenz' : 'Falsch'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Mitte: nur Score, kein Label */}
|
||||
<div className={styles.tipCenter}>
|
||||
<span className={styles.tipScoreBanner}>
|
||||
{match.userTip!.home} : {match.userTip!.away}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Rechts: Punkte */}
|
||||
<div className={styles.tipRight}>
|
||||
<span className={styles.resultPoints}>
|
||||
{points === 0 ? '0 Pkt.' : `+${points} Pkt.`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* ── Tipp vorhanden, noch nicht ausgewertet ── */
|
||||
<div className={styles.tipDisplay}>
|
||||
<div className={styles.tipLeft}>
|
||||
{match.tippable && (
|
||||
<button className={styles.editBtn} onClick={onTip}>Ändern</button>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.tipCenter}>
|
||||
{/* Label nur zeigen wenn kein Ändern-Button da ist, sonst fluchtet der Button nicht */}
|
||||
{!match.tippable && <span className={styles.tipLabel}>DEIN TIPP</span>}
|
||||
<span className={styles.tipScore}>
|
||||
{match.userTip!.home} : {match.userTip!.away}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.tipRight} />
|
||||
</div>
|
||||
)
|
||||
) : match.tippable ? (
|
||||
<button className={`btn-primary ${styles.tipBtn}`} onClick={onTip}>
|
||||
Tipp abgeben
|
||||
|
||||
Reference in New Issue
Block a user