265 lines
9.2 KiB
TypeScript
265 lines
9.2 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
|
|
|
interface Task {
|
|
id: string;
|
|
title: string;
|
|
completed: boolean;
|
|
}
|
|
|
|
interface FocusModeOverlayProps {
|
|
task: Task | null;
|
|
duration: number; // in minutes
|
|
onClose: () => void;
|
|
onComplete: (taskId: string) => void;
|
|
}
|
|
|
|
export default function FocusModeOverlay({ task, duration, onClose, onComplete }: FocusModeOverlayProps) {
|
|
const [timeLeft, setTimeLeft] = useState(duration * 60);
|
|
const [isActive, setIsActive] = useState(false);
|
|
const [isCompleting, setIsCompleting] = useState(false);
|
|
|
|
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (isActive && timeLeft > 0) {
|
|
timerRef.current = setInterval(() => {
|
|
setTimeLeft((prev) => prev - 1);
|
|
}, 1000);
|
|
} else if (timeLeft === 0) {
|
|
if (timerRef.current) clearInterval(timerRef.current);
|
|
setIsActive(false);
|
|
// Play sound?
|
|
}
|
|
|
|
return () => {
|
|
if (timerRef.current) clearInterval(timerRef.current);
|
|
};
|
|
}, [isActive, timeLeft]);
|
|
|
|
const formatTime = (seconds: number) => {
|
|
const m = Math.floor(seconds / 60);
|
|
const s = seconds % 60;
|
|
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
};
|
|
|
|
const handleToggleTimer = () => {
|
|
setIsActive(!isActive);
|
|
};
|
|
|
|
const handleReset = () => {
|
|
setIsActive(false);
|
|
setTimeLeft(duration * 60);
|
|
};
|
|
|
|
const handleComplete = async () => {
|
|
if (!task) return;
|
|
setIsCompleting(true);
|
|
// Small delay for animation
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
onComplete(task.id);
|
|
setIsCompleting(false);
|
|
handleReset(); // Reset timer for next task
|
|
};
|
|
|
|
// Calculate progress for circle
|
|
// Circumference = 2 * PI * r
|
|
// r = 120
|
|
const circumference = 2 * Math.PI * 120;
|
|
const progress = timeLeft / (duration * 60);
|
|
const dashoffset = circumference * (1 - progress);
|
|
|
|
return (
|
|
<div className="focus-overlay">
|
|
<button className="close-btn" onClick={onClose} title="Exit Focus Mode">
|
|
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round">
|
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
</svg>
|
|
</button>
|
|
|
|
<div className="focus-content">
|
|
<h2 className="focus-header">DO THIS NOW</h2>
|
|
|
|
{task ? (
|
|
<div className={`focus-task ${isCompleting ? 'completing' : ''}`}>
|
|
{task.title}
|
|
</div>
|
|
) : (
|
|
<div className="focus-task empty">
|
|
No tasks scheduled for today!
|
|
<div style={{ fontSize: '1rem', marginTop: '1rem', opacity: 0.6 }}>Time to relax or plan ahead.</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="timer-container">
|
|
<svg className="timer-svg" width="260" height="260">
|
|
<circle
|
|
className="timer-circle-bg"
|
|
stroke="#333"
|
|
strokeWidth="8"
|
|
fill="transparent"
|
|
r="120"
|
|
cx="130"
|
|
cy="130"
|
|
/>
|
|
<circle
|
|
className="timer-circle-fg"
|
|
stroke="white"
|
|
strokeWidth="8"
|
|
fill="transparent"
|
|
r="120"
|
|
cx="130"
|
|
cy="130"
|
|
style={{
|
|
strokeDasharray: circumference,
|
|
strokeDashoffset: dashoffset,
|
|
transition: 'stroke-dashoffset 1s linear'
|
|
}}
|
|
/>
|
|
</svg>
|
|
<div className="timer-text">{formatTime(timeLeft)}</div>
|
|
</div>
|
|
|
|
<div className="focus-controls">
|
|
<button className="focus-btn" onClick={handleToggleTimer}>
|
|
{isActive ? 'PAUSE' : 'START'}
|
|
</button>
|
|
<button className="focus-btn secondary" onClick={handleReset}>
|
|
RESET
|
|
</button>
|
|
{task && (
|
|
<button className="focus-btn success" onClick={handleComplete} disabled={isCompleting}>
|
|
{isCompleting ? 'COMPLETING...' : 'COMPLETE TASK'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<style jsx>{`
|
|
.focus-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: #1a1a1a;
|
|
color: white;
|
|
z-index: 2000;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-family: var(--font-inter, sans-serif);
|
|
animation: fadeIn 0.3s ease-out;
|
|
}
|
|
.close-btn {
|
|
position: absolute;
|
|
top: 20px;
|
|
right: 20px;
|
|
background: none;
|
|
border: none;
|
|
color: #666;
|
|
cursor: pointer;
|
|
padding: 10px;
|
|
border-radius: 50%;
|
|
transition: all 0.2s;
|
|
}
|
|
.close-btn:hover {
|
|
color: white;
|
|
background: rgba(255,255,255,0.1);
|
|
}
|
|
.focus-content {
|
|
text-align: center;
|
|
max-width: 600px;
|
|
width: 100%;
|
|
padding: 20px;
|
|
}
|
|
.focus-header {
|
|
font-size: 1rem;
|
|
letter-spacing: 0.2em;
|
|
color: #666;
|
|
margin-bottom: 2rem;
|
|
font-weight: 600;
|
|
}
|
|
.focus-task {
|
|
font-size: 2.5rem;
|
|
font-weight: 700;
|
|
margin-bottom: 3rem;
|
|
line-height: 1.2;
|
|
transition: all 0.5s ease;
|
|
}
|
|
.focus-task.empty {
|
|
font-size: 1.5rem;
|
|
font-weight: 500;
|
|
color: #999;
|
|
}
|
|
.focus-task.completing {
|
|
opacity: 0;
|
|
transform: scale(0.9);
|
|
}
|
|
|
|
.timer-container {
|
|
position: relative;
|
|
width: 260px;
|
|
height: 260px;
|
|
margin: 0 auto 3rem;
|
|
}
|
|
.timer-svg {
|
|
transform: rotate(-90deg);
|
|
}
|
|
.timer-text {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
font-size: 3rem;
|
|
font-family: monospace;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.focus-controls {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 15px;
|
|
}
|
|
.focus-btn {
|
|
background: white;
|
|
color: black;
|
|
border: none;
|
|
padding: 12px 24px;
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
border-radius: 30px;
|
|
cursor: pointer;
|
|
min-width: 100px;
|
|
transition: transform 0.1s;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
.focus-btn:hover {
|
|
transform: scale(1.05);
|
|
}
|
|
.focus-btn:active {
|
|
transform: scale(0.95);
|
|
}
|
|
.focus-btn.secondary {
|
|
background: transparent;
|
|
color: white;
|
|
border: 1px solid #666;
|
|
}
|
|
.focus-btn.secondary:hover {
|
|
border-color: white;
|
|
}
|
|
.focus-btn.success {
|
|
background: #009a9a; /* Weekly Teal */
|
|
color: white;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; transform: scale(0.98); }
|
|
to { opacity: 1; transform: scale(1); }
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
}
|