Timer integratrion

This commit is contained in:
Marc Wieland 2026-01-23 11:02:51 +01:00
parent d43866615b
commit 36d7821629
3 changed files with 132 additions and 66 deletions

View File

@ -1,93 +1,158 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Timer, Play, Pause, RotateCcw } from 'lucide-react'; import { Timer, Play, Pause, RotateCcw } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
interface RoundTimerProps { interface RoundTimerProps {
roundNumber: number; roundNumber: number;
} }
export const RoundTimer = ({ roundNumber }: RoundTimerProps) => { export const RoundTimer = ({ roundNumber }: RoundTimerProps) => {
const [seconds, setSeconds] = useState(0); const [totalSeconds, setTotalSeconds] = useState(300); // 5 Min default
const [isRunning, setIsRunning] = useState(true); // Auto-start beim Laden const [remainingSeconds, setRemainingSeconds] = useState(300);
const [isPaused, setIsPaused] = useState(false); const [isRunning, setIsRunning] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [editMinutes, setEditMinutes] = useState('5');
const [editSeconds, setEditSeconds] = useState('0');
useEffect(() => { useEffect(() => {
// Reset timer wenn neue Runde startet // Reset when round changes
setSeconds(0); setRemainingSeconds(totalSeconds);
setIsRunning(true); setIsRunning(false);
setIsPaused(false); }, [roundNumber, totalSeconds]);
}, [roundNumber]);
useEffect(() => { useEffect(() => {
let interval: NodeJS.Timeout | null = null; let interval: NodeJS.Timeout | null = null;
if (isRunning && !isPaused) { if (isRunning && remainingSeconds > 0) {
interval = setInterval(() => { interval = setInterval(() => {
setSeconds((prev) => prev + 1); setRemainingSeconds((prev) => {
if (prev <= 1) {
setIsRunning(false);
return 0;
}
return prev - 1;
});
}, 1000); }, 1000);
} }
return () => { return () => {
if (interval) clearInterval(interval); if (interval) clearInterval(interval);
}; };
}, [isRunning, isPaused]); }, [isRunning, remainingSeconds]);
const togglePause = () => { const togglePause = () => {
setIsPaused((prev) => !prev); setIsRunning((prev) => !prev);
}; };
const reset = () => { const reset = () => {
setSeconds(0); setRemainingSeconds(totalSeconds);
setIsPaused(false); setIsRunning(false);
}; };
const formatTime = (totalSeconds: number): string => { const handleTimeEdit = () => {
const hours = Math.floor(totalSeconds / 3600); const newMinutes = Math.max(0, Math.min(99, parseInt(editMinutes) || 0));
const minutes = Math.floor((totalSeconds % 3600) / 60); const newSeconds = Math.max(0, Math.min(59, parseInt(editSeconds) || 0));
const newTotal = newMinutes * 60 + newSeconds;
setTotalSeconds(newTotal);
setRemainingSeconds(newTotal);
setIsRunning(false);
setIsEditing(false);
};
const startEditing = () => {
const mins = Math.floor(totalSeconds / 60);
const secs = totalSeconds % 60; const secs = totalSeconds % 60;
setEditMinutes(mins.toString());
if (hours > 0) { setEditSeconds(secs.toString());
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; setIsEditing(true);
}
return `${minutes}:${secs.toString().padStart(2, '0')}`;
}; };
const formatTime = (secs: number): string => {
const minutes = Math.floor(secs / 60);
const seconds = secs % 60;
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
};
const isTimeExpired = remainingSeconds === 0;
return ( return (
<div className="card-apple p-4 flex items-center justify-between gap-4"> <div className="flex items-center gap-3 px-4 py-3 card-apple">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-xl transition-colors ${ <div className={`p-2 rounded-xl transition-colors ${
isPaused isTimeExpired
? 'bg-muted text-muted-foreground' ? 'bg-destructive/10 text-destructive'
: 'bg-primary/10 text-primary animate-pulse' : isRunning
? 'bg-primary/10 text-primary animate-pulse'
: 'bg-muted text-muted-foreground'
}`}> }`}>
<Timer className="w-5 h-5" /> <Timer className="w-5 h-5" />
</div> </div>
<div>
{isEditing ? (
<div className="flex items-center gap-2">
<Input
type="number"
min="0"
max="99"
value={editMinutes}
onChange={(e) => setEditMinutes(e.target.value)}
className="w-12 h-8 text-center rounded-lg"
placeholder="0"
/>
<span className="font-bold text-foreground">:</span>
<Input
type="number"
min="0"
max="59"
value={editSeconds}
onChange={(e) => setEditSeconds(e.target.value)}
className="w-12 h-8 text-center rounded-lg"
placeholder="0"
/>
<Button
size="sm"
onClick={handleTimeEdit}
className="rounded-lg h-8 px-2 text-xs"
>
OK
</Button>
</div>
) : (
<div
onClick={startEditing}
className="cursor-pointer hover:opacity-80 transition-opacity"
title="Klicke um Zeit zu bearbeiten"
>
<p className="text-sm font-medium text-muted-foreground">Rundenzeit</p> <p className="text-sm font-medium text-muted-foreground">Rundenzeit</p>
<p className="text-2xl font-bold text-foreground tabular-nums"> <p className={`text-2xl font-bold tabular-nums transition-colors ${
{formatTime(seconds)} isTimeExpired ? 'text-destructive' : 'text-foreground'
}`}>
{formatTime(remainingSeconds)}
</p> </p>
</div> </div>
</div> )}
<div className="flex gap-2"> <div className="flex gap-2 ml-auto">
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
onClick={togglePause} onClick={togglePause}
disabled={isTimeExpired || isEditing}
className="rounded-xl h-9 w-9" className="rounded-xl h-9 w-9"
title={isPaused ? 'Fortsetzen' : 'Pausieren'} title={isRunning ? 'Pausieren' : 'Starten'}
> >
{isPaused ? ( {isRunning ? (
<Play className="w-4 h-4" />
) : (
<Pause className="w-4 h-4" /> <Pause className="w-4 h-4" />
) : (
<Play className="w-4 h-4" />
)} )}
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
onClick={reset} onClick={reset}
disabled={isEditing}
className="rounded-xl h-9 w-9" className="rounded-xl h-9 w-9"
title="Zurücksetzen" title="Zurücksetzen"
> >

View File

@ -99,6 +99,8 @@ export const TournamentView = () => {
{currentRound && ` • Runde ${currentRound.roundNumber} aktiv`} {currentRound && ` • Runde ${currentRound.roundNumber} aktiv`}
</p> </p>
</div> </div>
<div className="flex flex-col sm:flex-row gap-3 items-start sm:items-center">
{currentRound && <RoundTimer roundNumber={currentRound.roundNumber} />}
<div className="flex gap-2"> <div className="flex gap-2">
<ScoreboardModal /> <ScoreboardModal />
{!currentRound ? ( {!currentRound ? (
@ -122,6 +124,7 @@ export const TournamentView = () => {
)} )}
</div> </div>
</div> </div>
</div>
{!currentRound && !hasEnoughTeams && ( {!currentRound && !hasEnoughTeams && (
<div className="card-apple p-6 flex items-center gap-4 border-l-4 border-l-champions"> <div className="card-apple p-6 flex items-center gap-4 border-l-4 border-l-champions">
@ -161,8 +164,6 @@ export const TournamentView = () => {
{currentRound && ( {currentRound && (
<> <>
<RoundTimer roundNumber={currentRound.roundNumber} />
<div className="card-apple p-4 bg-primary/5 border-primary/20"> <div className="card-apple p-4 bg-primary/5 border-primary/20">
<p className="text-sm text-center text-muted-foreground"> <p className="text-sm text-center text-muted-foreground">
<span className="font-semibold text-primary">Runde {currentRound.roundNumber}</span> <span className="font-semibold text-primary">Runde {currentRound.roundNumber}</span>

View File

@ -6,7 +6,7 @@ import path from "path";
export default defineConfig(() => ({ export default defineConfig(() => ({
server: { server: {
host: "::", host: "::",
port: 8080, port: 5173,
hmr: { hmr: {
overlay: false, overlay: false,
}, },