82acc56b8d
- Install husky v9 + lint-staged: pre-commit runs eslint --fix and prettier on staged files - Tighten ESLint base config: no-console→error, ban-ts-comment (ts-ignore banned, ts-expect-error with description allowed), reportUnusedDisableDirectives→error - Migrate web app from deprecated `next lint` to `eslint src/` with flat config and react-hooks plugin - Convert all 5 @ts-ignore to @ts-expect-error with descriptions, remove stale disable comments - Add NEXT_PUBLIC_SENTRY_DSN to docker-compose.prod.yml and .env.example - Add coverage artifact upload step to CI test job - Pre-existing violations (102 warnings) downgraded to warn in web config for Phase 2 cleanup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
93 lines
2.7 KiB
TypeScript
93 lines
2.7 KiB
TypeScript
"use client";
|
|
|
|
import { clsx } from "clsx";
|
|
import { useMemo } from "react";
|
|
import { addDays, createDatePositionCache } from "~/components/timeline/utils.js";
|
|
import { formatMonthYear } from "~/lib/format.js";
|
|
|
|
export function useTimelineLayout(
|
|
viewStart: Date,
|
|
viewDays: number,
|
|
zoom: "day" | "week" | "month",
|
|
showWeekends: boolean,
|
|
today: Date,
|
|
) {
|
|
const CELL_WIDTH = zoom === "day" ? 40 : zoom === "week" ? 14 : 4;
|
|
|
|
// Visible dates, filtered by showWeekends
|
|
const dates = useMemo(() => {
|
|
const result: Date[] = [];
|
|
for (let i = 0; i < viewDays; i++) {
|
|
const d = addDays(viewStart, i);
|
|
if (!showWeekends && (d.getDay() === 0 || d.getDay() === 6)) continue;
|
|
result.push(d);
|
|
}
|
|
return result;
|
|
}, [viewStart, viewDays, showWeekends]);
|
|
|
|
const visibleDays = dates.length;
|
|
const totalCanvasWidth = visibleDays * CELL_WIDTH;
|
|
|
|
// O(1) position helpers via pre-computed cache
|
|
const { toLeft, toWidth } = useMemo(
|
|
() => createDatePositionCache(viewStart, viewDays, CELL_WIDTH, showWeekends),
|
|
[viewStart, viewDays, CELL_WIDTH, showWeekends],
|
|
);
|
|
|
|
// Grid lines — memoized; identical for every row
|
|
const gridLines = useMemo(
|
|
() =>
|
|
dates.map((date, i) => {
|
|
const isToday = date.toDateString() === today.toDateString();
|
|
const dow = date.getDay();
|
|
const isWeekend = dow === 0 || dow === 6;
|
|
return (
|
|
<div
|
|
key={i}
|
|
className={clsx(
|
|
"absolute top-0 bottom-0 border-r",
|
|
isToday
|
|
? "border-brand-300 dark:border-brand-700 border-r-2"
|
|
: isWeekend
|
|
? "border-brand-200 dark:border-brand-800 bg-brand-50/40 dark:bg-brand-950/20"
|
|
: "border-gray-100 dark:border-gray-800",
|
|
)}
|
|
style={{ left: i * CELL_WIDTH, width: CELL_WIDTH }}
|
|
/>
|
|
);
|
|
}),
|
|
[dates, CELL_WIDTH, today],
|
|
);
|
|
|
|
// Month groups for the month header
|
|
const monthGroups = useMemo(() => {
|
|
const groups: { label: string; colCount: number }[] = [];
|
|
for (const d of dates) {
|
|
const label = formatMonthYear(d);
|
|
const last = groups[groups.length - 1];
|
|
if (last && last.label === label) last.colCount++;
|
|
else groups.push({ label, colCount: 1 });
|
|
}
|
|
return groups;
|
|
}, [dates]);
|
|
|
|
// Convert clientX to a Date on the visible timeline
|
|
function xToDate(clientX: number, rowCanvasRect: DOMRect): Date {
|
|
const x = clientX - rowCanvasRect.left;
|
|
const colIndex = Math.max(0, Math.min(dates.length - 1, Math.floor(x / CELL_WIDTH)));
|
|
return dates[colIndex] ?? today;
|
|
}
|
|
|
|
return {
|
|
CELL_WIDTH,
|
|
dates,
|
|
visibleDays,
|
|
totalCanvasWidth,
|
|
toLeft,
|
|
toWidth,
|
|
gridLines,
|
|
monthGroups,
|
|
xToDate,
|
|
};
|
|
}
|