learning_ai_clock/web/src/components/PomodoroView.tsx

199 lines
7.1 KiB
TypeScript

'use client';
import { useTimerStore } from '@/lib/store';
import { getRemainingMs } from '@/lib/timer-engine';
import type { Timer } from '@/lib/timer-engine';
import { CountdownRing } from './CountdownRing';
import { formatDuration } from '@/lib/format';
import { Coffee, Pause, Play, X, SkipForward, Trophy } from 'lucide-react';
import { showToast } from './Toast';
interface PomodoroViewProps {
timer: Timer;
}
export function PomodoroView({ timer }: PomodoroViewProps) {
const now = useTimerStore((s) => s.now);
const { pause, resume, dismiss, advancePom } = useTimerStore();
const remaining = getRemainingMs(timer, now);
const duration = timer.duration ?? 1;
const progress = Math.max(0, remaining / duration);
const pomState = timer.pomodoroState;
const config = timer.pomodoroConfig;
if (!pomState || !config) return null;
const isBreak = pomState.isBreak || pomState.isLongBreak;
const isPaused = timer.state === 'paused';
const isFiring = timer.state === 'firing';
const isCompleted = timer.state === 'completed';
const roundLabel = isBreak
? pomState.isLongBreak ? 'Long Break' : 'Break'
: `Round ${pomState.currentRound} of ${config.rounds}`;
const ringColor = isBreak ? 'var(--cm-accent-secondary)' : 'var(--cm-accent)';
// Session complete celebration
if (isCompleted) {
const totalMinutes = config.workMinutes * config.rounds + config.breakMinutes * (config.rounds - 1) + config.longBreakMinutes;
return (
<div
className="rounded-2xl border p-8 text-center"
style={{
backgroundColor: 'var(--cm-surface-card)',
borderColor: 'var(--cm-border)',
background: 'linear-gradient(135deg, var(--cm-surface-card) 0%, rgba(52,211,153,0.08) 100%)',
}}
>
<div className="flex justify-center mb-4">
<div
className="w-16 h-16 rounded-full flex items-center justify-center"
style={{ backgroundColor: 'rgba(52,211,153,0.15)' }}
>
<Trophy size={32} style={{ color: 'var(--cm-success)' }} />
</div>
</div>
<h3 className="text-xl font-bold mb-2" style={{ color: 'var(--cm-success)' }}>
Session Complete!
</h3>
<p className="text-sm mb-1" style={{ color: 'var(--cm-text-primary)' }}>
{timer.label}
</p>
<p className="text-xs mb-4" style={{ color: 'var(--cm-text-tertiary)' }}>
{config.rounds} rounds &middot; ~{totalMinutes} minutes of focused work
</p>
<div className="flex justify-center gap-1.5 mb-4">
{Array.from({ length: config.rounds }).map((_, i) => (
<div
key={i}
className="w-3 h-3 rounded-full"
style={{ backgroundColor: 'var(--cm-success)' }}
/>
))}
</div>
<button
onClick={() => dismiss(timer.id)}
className="px-6 py-2.5 rounded-xl text-sm font-medium cursor-pointer"
style={{ backgroundColor: 'var(--cm-surface-muted)', color: 'var(--cm-text-secondary)' }}
>
Close
</button>
</div>
);
}
return (
<div
className="rounded-2xl border p-6 text-center"
style={{
backgroundColor: 'var(--cm-surface-card)',
borderColor: 'var(--cm-border)',
}}
>
{/* Header */}
<div className="flex items-center justify-center gap-2 mb-4">
<Coffee size={18} style={{ color: isBreak ? 'var(--cm-accent-secondary)' : 'var(--cm-accent)' }} />
<span className="text-sm font-medium" style={{ color: 'var(--cm-text-secondary)' }}>
{timer.label}
</span>
</div>
{/* Round indicator */}
<div className="flex justify-center gap-1.5 mb-6">
{Array.from({ length: config.rounds }).map((_, i) => (
<div
key={i}
className="w-3 h-3 rounded-full transition-colors"
style={{
backgroundColor:
i < pomState.completedRounds
? 'var(--cm-success)'
: i === pomState.currentRound - 1 && !isBreak
? 'var(--cm-accent)'
: 'var(--cm-surface-muted)',
}}
/>
))}
</div>
{/* Countdown ring */}
<div className="flex justify-center mb-4">
<CountdownRing progress={progress} size={220} strokeWidth={10} color={ringColor}>
<div className="text-center">
<div
className="text-4xl font-mono font-bold tabular-nums"
style={{ color: 'var(--cm-text-primary)' }}
>
{formatDuration(remaining)}
</div>
<div className="text-sm mt-1" style={{ color: 'var(--cm-text-tertiary)' }}>
{roundLabel}
</div>
</div>
</CountdownRing>
</div>
{/* Status */}
{isPaused && (
<p className="text-sm mb-4 font-medium" style={{ color: 'var(--cm-warning)' }}>
Paused
</p>
)}
{isFiring && (
<p className="text-sm mb-4 font-medium animate-pulse" style={{ color: 'var(--cm-critical)' }}>
{isBreak ? 'Break over! Start next round?' : 'Time\'s up! Take a break?'}
</p>
)}
{/* Actions */}
<div className="flex justify-center gap-3">
{(timer.state === 'active' || timer.state === 'warning') && (
<button
onClick={() => pause(timer.id)}
className="flex items-center gap-2 px-5 py-2.5 rounded-xl text-sm font-medium transition-colors cursor-pointer"
style={{ backgroundColor: 'var(--cm-surface-muted)', color: 'var(--cm-text-secondary)' }}
>
<Pause size={16} /> Pause
</button>
)}
{isPaused && (
<button
onClick={() => resume(timer.id)}
className="flex items-center gap-2 px-5 py-2.5 rounded-xl text-sm font-medium transition-colors cursor-pointer"
style={{ backgroundColor: 'var(--cm-accent)', color: '#fff' }}
>
<Play size={16} /> Resume
</button>
)}
{isFiring && (
<button
onClick={() => advancePom(timer.id)}
className="flex items-center gap-2 px-5 py-2.5 rounded-xl text-sm font-medium transition-colors cursor-pointer"
style={{ backgroundColor: 'var(--cm-accent)', color: '#fff' }}
>
<SkipForward size={16} /> {isBreak ? 'Start Work' : 'Start Break'}
</button>
)}
<button
onClick={() => dismiss(timer.id)}
className="flex items-center gap-2 px-5 py-2.5 rounded-xl text-sm font-medium transition-colors cursor-pointer"
style={{ backgroundColor: 'rgba(255,71,87,0.15)', color: 'var(--cm-danger)' }}
>
<X size={16} /> End
</button>
</div>
{/* Stats */}
<div className="mt-4 flex justify-center gap-6 text-xs" style={{ color: 'var(--cm-text-tertiary)' }}>
<span>Completed: {pomState.completedRounds}/{config.rounds}</span>
<span>Work: {config.workMinutes}m</span>
<span>Break: {config.breakMinutes}m</span>
</div>
</div>
);
}