Files
kadence/frontend/src/components/charts/SessionTimeline.tsx
T
timka123 77b26757bf refactor: enhance TimerPage layout and functionality; update theme and tokens for improved design consistency
- 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.
2026-10-18 23:57:36 +03:00

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>
);
}