Files
Home_assistant/frontend/src/pages/Pomodoro.tsx
T
2026-06-09 11:54:32 +03:00

217 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}