added 1RM and macro calculators to fitness dashboard.
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
'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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user