Files
parths-life-manager/src/components/modules/PomodoroView.tsx
T

304 lines
10 KiB
TypeScript

'use client';
import React, { useState, useEffect, useRef } from 'react';
import {
Play,
Pause,
RotateCcw,
SkipForward,
Flame,
Brain,
Coffee,
CheckCircle2,
Sparkles
} from 'lucide-react';
import { Task } from '@/lib/types';
import { playNotificationChime } from '@/lib/audio';
interface PomodoroViewProps {
tasks: Task[];
onCompleteSession?: () => void;
}
type TimerMode = 'focus' | 'shortBreak' | 'longBreak';
const MODE_CONFIG: Record<TimerMode, { label: string; duration: number; icon: typeof Brain; color: string; ringColor: string }> = {
focus: {
label: 'Focus Session',
duration: 25 * 60,
icon: Brain,
color: 'text-amber-400 bg-amber-500/10 border-amber-500/30',
ringColor: '#f59e0b',
},
shortBreak: {
label: 'Short Rest',
duration: 5 * 60,
icon: Coffee,
color: 'text-cyan-400 bg-cyan-500/10 border-cyan-500/30',
ringColor: '#06b6d4',
},
longBreak: {
label: 'Long Recovery',
duration: 15 * 60,
icon: Coffee,
color: 'text-emerald-400 bg-emerald-500/10 border-emerald-500/30',
ringColor: '#10b981',
},
};
export const PomodoroView: React.FC<PomodoroViewProps> = ({ tasks }) => {
const [mode, setMode] = useState<TimerMode>('focus');
const [timeLeft, setTimeLeft] = useState<number>(MODE_CONFIG.focus.duration);
const [isActive, setIsActive] = useState<boolean>(false);
const [completedSessions, setCompletedSessions] = useState<number>(3);
const [selectedTaskId, setSelectedTaskId] = useState<string>('');
const timerRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
if (isActive) {
timerRef.current = setInterval(() => {
setTimeLeft((prev) => {
if (prev <= 1) {
clearInterval(timerRef.current!);
setIsActive(false);
playNotificationChime('complete');
if (mode === 'focus') {
setCompletedSessions((c) => c + 1);
}
return 0;
}
return prev - 1;
});
}, 1000);
} else if (timerRef.current) {
clearInterval(timerRef.current);
}
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}, [isActive, mode]);
const switchMode = (newMode: TimerMode) => {
setIsActive(false);
setMode(newMode);
setTimeLeft(MODE_CONFIG[newMode].duration);
playNotificationChime('click');
};
const handleToggleTimer = () => {
setIsActive(!isActive);
playNotificationChime('click');
};
const handleReset = () => {
setIsActive(false);
setTimeLeft(MODE_CONFIG[mode].duration);
playNotificationChime('click');
};
const handleSkip = () => {
setIsActive(false);
if (mode === 'focus') {
switchMode('shortBreak');
} else {
switchMode('focus');
}
};
const minutes = Math.floor(timeLeft / 60);
const seconds = timeLeft % 60;
const timeFormatted = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
const currentDuration = MODE_CONFIG[mode].duration;
const progressPercent = ((currentDuration - timeLeft) / currentDuration) * 100;
// SVG ring properties
const radius = 130;
const circumference = 2 * Math.PI * radius;
const strokeDashoffset = circumference - (progressPercent / 100) * circumference;
const currentTask = tasks.find((t) => t.id === selectedTaskId);
return (
<div className="max-w-4xl mx-auto space-y-8">
{/* Mode Switcher */}
<div className="flex items-center justify-center">
<div className="flex items-center p-1.5 rounded-2xl bg-slate-900 border border-slate-800 shadow-xl">
{(['focus', 'shortBreak', 'longBreak'] as const).map((m) => {
const isSelected = mode === m;
const Icon = MODE_CONFIG[m].icon;
return (
<button
key={m}
onClick={() => switchMode(m)}
className={`flex items-center gap-2 px-5 py-2.5 rounded-xl text-xs font-bold transition-all cursor-pointer ${
isSelected
? MODE_CONFIG[m].color
: 'text-slate-400 hover:text-slate-200'
}`}
>
<Icon className="w-4 h-4" />
<span>{MODE_CONFIG[m].label}</span>
</button>
);
})}
</div>
</div>
{/* Center Circular Countdown Card */}
<div className="glass-card p-8 md:p-12 flex flex-col items-center justify-center relative overflow-hidden border-slate-800/80 shadow-2xl">
{/* Ambient background glow */}
<div
className="absolute w-72 h-72 rounded-full blur-[100px] opacity-20 pointer-events-none transition-all duration-700"
style={{ backgroundColor: MODE_CONFIG[mode].ringColor }}
/>
{/* Circular SVG Timer */}
<div className="relative flex items-center justify-center">
<svg className="w-72 h-72 md:w-80 md:h-80 -rotate-90 transform">
{/* Background Track */}
<circle
cx="50%"
cy="50%"
r={radius}
className="text-slate-800/60 stroke-current"
strokeWidth="10"
fill="transparent"
/>
{/* Animated Progress Ring */}
<circle
cx="50%"
cy="50%"
r={radius}
stroke={MODE_CONFIG[mode].ringColor}
strokeWidth="12"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
fill="transparent"
className="transition-all duration-500"
style={{
filter: `drop-shadow(0 0 10px ${MODE_CONFIG[mode].ringColor}80)`,
}}
/>
</svg>
{/* Time Display Inside Circle */}
<div className="absolute flex flex-col items-center justify-center text-center">
<span className="font-mono text-5xl md:text-6xl font-extrabold text-white tracking-tighter">
{timeFormatted}
</span>
<span className="text-xs uppercase font-bold tracking-widest text-slate-400 mt-2">
{isActive ? 'Session In Progress' : 'Paused'}
</span>
{currentTask && (
<span className="mt-3 px-3 py-1 rounded-full bg-slate-900 border border-slate-800 text-[11px] text-cyan-400 font-medium max-w-[200px] truncate">
🎯 {currentTask.title}
</span>
)}
</div>
</div>
{/* Control Buttons */}
<div className="mt-8 flex items-center gap-4 z-10">
<button
onClick={handleReset}
className="p-3.5 rounded-2xl bg-slate-900/80 hover:bg-slate-800 text-slate-400 hover:text-white border border-slate-800 transition-all cursor-pointer shadow-md"
title="Reset timer"
>
<RotateCcw className="w-5 h-5" />
</button>
<button
onClick={handleToggleTimer}
className={`flex items-center gap-2.5 px-8 py-4 rounded-2xl text-slate-950 font-extrabold text-base shadow-xl transition-all hover:scale-105 active:scale-95 cursor-pointer`}
style={{
backgroundColor: MODE_CONFIG[mode].ringColor,
boxShadow: `0 10px 25px -5px ${MODE_CONFIG[mode].ringColor}60`,
}}
>
{isActive ? (
<>
<Pause className="w-5 h-5 fill-slate-950" />
<span>Pause</span>
</>
) : (
<>
<Play className="w-5 h-5 fill-slate-950" />
<span>Start Focus</span>
</>
)}
</button>
<button
onClick={handleSkip}
className="p-3.5 rounded-2xl bg-slate-900/80 hover:bg-slate-800 text-slate-400 hover:text-white border border-slate-800 transition-all cursor-pointer shadow-md"
title="Skip to next session"
>
<SkipForward className="w-5 h-5" />
</button>
</div>
</div>
{/* Focus Session Target & Task Pairing */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Daily Focus Sessions Card */}
<div className="glass-card p-5 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-3 rounded-xl bg-amber-500/10 text-amber-400">
<Flame className="w-6 h-6 fill-amber-400/20" />
</div>
<div>
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">
Sessions Completed
</span>
<div className="text-2xl font-extrabold text-white mt-0.5">
{completedSessions} / 6 Sessions
</div>
</div>
</div>
<div className="text-right">
<span className="text-xs font-bold text-amber-400">{completedSessions * 25} Mins</span>
<p className="text-[11px] text-slate-400">Deep Work Today</p>
</div>
</div>
{/* Task Attachment Picker */}
<div className="glass-card p-5 flex flex-col justify-between">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider flex items-center gap-1.5">
<Sparkles className="w-3.5 h-3.5 text-cyan-400" />
Pair with Task
</span>
{selectedTaskId && (
<button
onClick={() => setSelectedTaskId('')}
className="text-[11px] text-slate-500 hover:text-slate-300"
>
Clear
</button>
)}
</div>
<select
value={selectedTaskId}
onChange={(e) => setSelectedTaskId(e.target.value)}
className="w-full px-3 py-2 rounded-xl bg-slate-900 border border-slate-800 text-xs text-slate-200 focus:outline-none focus:border-amber-500"
>
<option value="">No task linked (Freestyle Focus)</option>
{tasks
.filter((t) => t.status !== 'completed')
.map((t) => (
<option key={t.id} value={t.id}>
[{t.priority.toUpperCase()}] {t.title}
</option>
))}
</select>
</div>
</div>
</div>
);
};