- Refactored TimerPage component to improve layout and user experience. - Updated theme configuration to utilize design tokens for better maintainability. - Introduced new tokens for colors and typography to align with design principles. - Simplified state management and improved timer functionality. - Enhanced accessibility and responsiveness of UI elements.
108 lines
3.6 KiB
TypeScript
108 lines
3.6 KiB
TypeScript
/**
|
|
* Today, laid out on a clock: every interval in its real place.
|
|
*
|
|
* Three categories (focus / short / long break) so a legend is mandatory, and
|
|
* each block is also labelled on hover — colour never carries identity alone.
|
|
*/
|
|
import { Box, Stack, Tooltip, Typography } from "@mui/material";
|
|
|
|
import { KIND_LABEL } from "../../lib/format";
|
|
import { kindColor, type VizPalette } from "../../lib/viz";
|
|
import ChartFrame from "./ChartFrame";
|
|
|
|
interface Session {
|
|
id: number;
|
|
kind: string;
|
|
status: string;
|
|
started_at: string;
|
|
minutes: number;
|
|
task: string | null;
|
|
}
|
|
|
|
const LANE = 34;
|
|
|
|
export default function SessionTimeline({
|
|
sessions,
|
|
palette,
|
|
}: {
|
|
sessions: Session[];
|
|
palette: VizPalette;
|
|
}) {
|
|
const done = sessions.filter((item) => item.minutes > 0);
|
|
if (!done.length) {
|
|
return (
|
|
<ChartFrame title="Today's timeline" caption="Nothing recorded yet — start a pomodoro.">
|
|
<Box sx={{ height: LANE, borderRadius: 1, border: "1px dashed", borderColor: "divider" }} />
|
|
</ChartFrame>
|
|
);
|
|
}
|
|
|
|
const hourOf = (iso: string) => {
|
|
const date = new Date(iso);
|
|
return date.getHours() + date.getMinutes() / 60;
|
|
};
|
|
const first = Math.floor(Math.min(...done.map((item) => hourOf(item.started_at))));
|
|
const last = Math.ceil(
|
|
Math.max(...done.map((item) => hourOf(item.started_at) + item.minutes / 60)),
|
|
);
|
|
const span = Math.max(last - first, 1);
|
|
|
|
return (
|
|
<ChartFrame
|
|
title="Today's timeline"
|
|
caption={`${first}:00 → ${last}:00`}
|
|
legend={[
|
|
{ label: KIND_LABEL.work, color: palette.work },
|
|
{ label: KIND_LABEL.short_break, color: palette.shortBreak },
|
|
{ label: KIND_LABEL.long_break, color: palette.longBreak },
|
|
]}
|
|
>
|
|
<Box sx={{ position: "relative", height: LANE, borderRadius: 1, bgcolor: palette.empty }}>
|
|
{done.map((item) => {
|
|
const start = (hourOf(item.started_at) - first) / span;
|
|
const width = item.minutes / 60 / span;
|
|
return (
|
|
<Tooltip
|
|
key={item.id}
|
|
title={
|
|
<Box>
|
|
<Typography variant="caption" sx={{ display: "block", fontWeight: 600 }}>
|
|
{KIND_LABEL[item.kind] ?? item.kind} · {item.minutes} min
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ display: "block" }}>
|
|
{new Date(item.started_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
|
|
{item.task ? ` · ${item.task}` : ""}
|
|
{item.status === "abandoned" ? " · abandoned" : ""}
|
|
</Typography>
|
|
</Box>
|
|
}
|
|
>
|
|
<Box
|
|
sx={{
|
|
position: "absolute",
|
|
left: `${start * 100}%`,
|
|
width: `calc(${Math.max(width * 100, 1)}% - 2px)`,
|
|
top: 4,
|
|
bottom: 4,
|
|
borderRadius: "4px",
|
|
bgcolor: kindColor(item.kind, palette),
|
|
opacity: item.status === "abandoned" ? 0.4 : 1,
|
|
border: item.status === "abandoned" ? `1px dashed ${palette.axis}` : "none",
|
|
}}
|
|
/>
|
|
</Tooltip>
|
|
);
|
|
})}
|
|
</Box>
|
|
<Stack direction="row" justifyContent="space-between" sx={{ mt: 0.5 }}>
|
|
<Typography variant="caption" color="text.secondary">
|
|
{String(first).padStart(2, "0")}:00
|
|
</Typography>
|
|
<Typography variant="caption" color="text.secondary">
|
|
{String(last).padStart(2, "0")}:00
|
|
</Typography>
|
|
</Stack>
|
|
</ChartFrame>
|
|
);
|
|
}
|