This commit is contained in:
2026-06-09 09:36:48 +03:00
parent 8247b7116f
commit f0fda693d8
49 changed files with 5503 additions and 1 deletions
+167
View File
@@ -0,0 +1,167 @@
import { FormEvent, useEffect, useState } from "react";
import { api, PomodoroHistoryItem, PomodoroStatus } from "../api/client";
import "./Pomodoro.css";
function formatTime(seconds: number): string {
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
}
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);
};
useEffect(() => {
refresh().catch(console.error);
const timer = setInterval(() => {
api.pomodoroStatus().then(setStatus).catch(console.error);
}, 1000);
return () => clearInterval(timer);
}, []);
const handleStart = 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();
setTaskNote("");
setResult("");
setCompleted(false);
} 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
? ((status.duration_min * 60 - status.remaining_seconds) / (status.duration_min * 60)) * 100
: 0;
return (
<div className="pomodoro-page">
<section className="timer-card">
<div className="timer-ring" style={{ background: `conic-gradient(#4f7cff ${progress}%, #1f2633 0)` }}>
<div className="timer-inner">
<div className="timer-value">{formatTime(displaySeconds)}</div>
<div className="timer-status">{status?.status ?? "idle"}</div>
</div>
</div>
{status?.task_note && <p className="task-note">Задача: {status.task_note}</p>}
{!isActive ? (
<form className="timer-form" onSubmit={handleStart}>
<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>
<button type="submit" className="primary-btn">
Старт
</button>
</form>
) : (
<div className="timer-controls">
{status?.status === "running" ? (
<button onClick={handlePause}>Пауза</button>
) : (
<button onClick={handleResume}>Продолжить</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>
)}
{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">
{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>
);
}