199 lines
7.1 KiB
TypeScript
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 · ~{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>
|
|
);
|
|
}
|