217 lines
6.9 KiB
TypeScript
217 lines
6.9 KiB
TypeScript
import { FormEvent, useEffect, useState } from "react";
|
||
import { api, PomodoroHistoryItem, PomodoroStatus } from "../api/client";
|
||
import { phaseLabel } from "../utils/pomodoro";
|
||
import { formatTime } from "../utils/time";
|
||
import "./Pomodoro.css";
|
||
|
||
export default function Pomodoro() {
|
||
const [status, setStatus] = useState<PomodoroStatus | null>(null);
|
||
const [history, setHistory] = useState<PomodoroHistoryItem[]>([]);
|
||
const [duration, setDuration] = useState(25);
|
||
const [taskNote, setTaskNote] = useState("");
|
||
const [result, setResult] = useState("");
|
||
const [completed, setCompleted] = useState(false);
|
||
const [error, setError] = useState("");
|
||
|
||
const refresh = async () => {
|
||
const [current, past] = await Promise.all([api.pomodoroStatus(), api.pomodoroHistory()]);
|
||
setStatus(current);
|
||
setHistory(past);
|
||
if (current.cycle?.work_duration_min) {
|
||
setDuration(current.cycle.work_duration_min);
|
||
}
|
||
if (current.cycle?.task_note) {
|
||
setTaskNote(current.cycle.task_note);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
refresh().catch(console.error);
|
||
const timer = setInterval(() => {
|
||
api.pomodoroStatus().then(setStatus).catch(console.error);
|
||
}, 1000);
|
||
return () => clearInterval(timer);
|
||
}, []);
|
||
|
||
const handleStartWork = async (e: FormEvent) => {
|
||
e.preventDefault();
|
||
setError("");
|
||
try {
|
||
const data = await api.pomodoroStart(duration, taskNote);
|
||
setStatus(data);
|
||
setResult("");
|
||
setCompleted(false);
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : "Ошибка запуска");
|
||
}
|
||
};
|
||
|
||
const handlePause = async () => {
|
||
setError("");
|
||
try {
|
||
setStatus(await api.pomodoroPause());
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : "Ошибка");
|
||
}
|
||
};
|
||
|
||
const handleResume = async () => {
|
||
setError("");
|
||
try {
|
||
setStatus(await api.pomodoroResume());
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : "Ошибка");
|
||
}
|
||
};
|
||
|
||
const handleStop = async () => {
|
||
setError("");
|
||
try {
|
||
await api.pomodoroStop(result, completed);
|
||
await refresh();
|
||
setResult("");
|
||
setCompleted(false);
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : "Ошибка");
|
||
}
|
||
};
|
||
|
||
const handleSkip = async () => {
|
||
setError("");
|
||
try {
|
||
await api.pomodoroSkip();
|
||
await refresh();
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : "Ошибка");
|
||
}
|
||
};
|
||
|
||
const handleReset = async () => {
|
||
setError("");
|
||
try {
|
||
await api.pomodoroResetCycle(false);
|
||
await refresh();
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : "Ошибка");
|
||
}
|
||
};
|
||
|
||
const isActive = status?.status === "running" || status?.status === "paused";
|
||
const displaySeconds = isActive ? (status?.remaining_seconds ?? 0) : duration * 60;
|
||
const progress = status && isActive
|
||
? ((status.duration_min * 60 - status.remaining_seconds) / (status.duration_min * 60)) * 100
|
||
: 0;
|
||
const cycle = status?.cycle;
|
||
const ringColor = status?.phase === "work" ? "#4f7cff" : "#3dbf8f";
|
||
|
||
return (
|
||
<div className="pomodoro-page">
|
||
<section className="timer-card">
|
||
{cycle && (
|
||
<div className="cycle-badge">
|
||
Цикл {cycle.completed_work_sessions}/{cycle.sessions_until_long_break}
|
||
{cycle.auto_advance && " · авто"}
|
||
</div>
|
||
)}
|
||
|
||
<div
|
||
className="timer-ring"
|
||
style={{ background: `conic-gradient(${ringColor} ${progress}%, #1f2633 0)` }}
|
||
>
|
||
<div className="timer-inner">
|
||
<div className="timer-value">{formatTime(displaySeconds)}</div>
|
||
<div className="timer-status">
|
||
{isActive ? phaseLabel(status?.phase ?? "work") : status?.status ?? "idle"}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{status?.task_note && <p className="task-note">Задача: {status.task_note}</p>}
|
||
|
||
{!isActive ? (
|
||
<form className="timer-form" onSubmit={handleStartWork}>
|
||
<label>
|
||
Работа (мин)
|
||
<input
|
||
type="number"
|
||
min={1}
|
||
max={180}
|
||
value={duration}
|
||
onChange={(e) => setDuration(Number(e.target.value))}
|
||
/>
|
||
</label>
|
||
<label>
|
||
Над чем работаем
|
||
<input
|
||
value={taskNote}
|
||
onChange={(e) => setTaskNote(e.target.value)}
|
||
placeholder="Опишите задачу"
|
||
/>
|
||
</label>
|
||
<div className="timer-form-actions">
|
||
<button type="submit" className="primary-btn">
|
||
Старт работы
|
||
</button>
|
||
<button type="button" onClick={() => api.pomodoroStartShortBreak().then(setStatus)}>
|
||
Короткий перерыв
|
||
</button>
|
||
<button type="button" onClick={() => api.pomodoroStartLongBreak().then(setStatus)}>
|
||
Длинный перерыв
|
||
</button>
|
||
</div>
|
||
</form>
|
||
) : (
|
||
<div className="timer-controls">
|
||
{status?.status === "running" ? (
|
||
<button onClick={handlePause}>Пауза</button>
|
||
) : (
|
||
<button onClick={handleResume}>Продолжить</button>
|
||
)}
|
||
<button onClick={handleSkip}>Пропустить фазу</button>
|
||
<div className="stop-form">
|
||
<input
|
||
value={result}
|
||
onChange={(e) => setResult(e.target.value)}
|
||
placeholder="Что успели сделать?"
|
||
/>
|
||
<label>
|
||
<input
|
||
type="checkbox"
|
||
checked={completed}
|
||
onChange={(e) => setCompleted(e.target.checked)}
|
||
/>
|
||
Фаза завершена
|
||
</label>
|
||
<button onClick={handleStop}>Стоп</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<button type="button" className="reset-cycle-btn" onClick={handleReset}>
|
||
Сбросить цикл
|
||
</button>
|
||
|
||
{error && <p className="error">{error}</p>}
|
||
</section>
|
||
|
||
<section className="history-card">
|
||
<h2>История</h2>
|
||
<ul>
|
||
{history.map((item) => (
|
||
<li key={item.id}>
|
||
<div className="history-title">
|
||
{phaseLabel(item.phase)}: {item.task_note || "Без описания"} — {item.status}
|
||
</div>
|
||
<div className="history-meta">
|
||
{item.duration_min} мин ·{" "}
|
||
{item.finished_at ? new Date(item.finished_at).toLocaleString("ru-RU") : ""}
|
||
</div>
|
||
{item.result && <div className="history-result">{item.result}</div>}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|