- Introduced a new `pendingMonth` state in the app store to manage month transitions without clearing current data, enhancing user experience during month navigation. - Updated `useMonthData` hook to load data for the `pendingMonth` when set, preventing empty-frame flicker and ensuring smooth month switching. - Modified `CalendarPage` and `CalendarGrid` components to utilize the new `pendingMonth` state, improving the rendering logic during month changes. - Enhanced `DutyList` to display a loading skeleton while data is being fetched, providing better feedback to users. - Updated relevant tests to cover the new loading behavior and state management for month transitions.
94 lines
2.6 KiB
TypeScript
94 lines
2.6 KiB
TypeScript
/**
|
|
* 6-week (42-cell) calendar grid starting from Monday. Composes CalendarDay cells.
|
|
* Ported from webapp/js/calendar.js renderCalendar.
|
|
*/
|
|
|
|
"use client";
|
|
|
|
import { useMemo } from "react";
|
|
import {
|
|
firstDayOfMonth,
|
|
getMonday,
|
|
localDateString,
|
|
} from "@/lib/date-utils";
|
|
import type { CalendarEvent, DutyWithUser } from "@/types";
|
|
import { dutiesByDate, calendarEventsByDate } from "@/lib/calendar-data";
|
|
import { cn } from "@/lib/utils";
|
|
import { CalendarDay } from "./CalendarDay";
|
|
|
|
export interface CalendarGridProps {
|
|
/** Currently displayed month. */
|
|
currentMonth: Date;
|
|
/** All duties for the visible range (will be grouped by date). */
|
|
duties: DutyWithUser[];
|
|
/** All calendar events for the visible range. */
|
|
calendarEvents: CalendarEvent[];
|
|
/** Called when a day cell is clicked (opens day detail). Receives date key and cell rect for popover. */
|
|
onDayClick: (dateKey: string, anchorRect: DOMRect) => void;
|
|
className?: string;
|
|
}
|
|
|
|
const CELLS = 42;
|
|
|
|
export function CalendarGrid({
|
|
currentMonth,
|
|
duties,
|
|
calendarEvents,
|
|
onDayClick,
|
|
className,
|
|
}: CalendarGridProps) {
|
|
const dutiesByDateMap = useMemo(
|
|
() => dutiesByDate(duties),
|
|
[duties]
|
|
);
|
|
const calendarEventsByDateMap = useMemo(
|
|
() => calendarEventsByDate(calendarEvents),
|
|
[calendarEvents]
|
|
);
|
|
const todayKey = localDateString(new Date());
|
|
|
|
const cells = useMemo(() => {
|
|
const first = firstDayOfMonth(currentMonth);
|
|
const start = getMonday(first);
|
|
const result: { date: Date; key: string; month: number }[] = [];
|
|
const d = new Date(start);
|
|
for (let i = 0; i < CELLS; i++) {
|
|
const key = localDateString(d);
|
|
result.push({ date: new Date(d), key, month: d.getMonth() });
|
|
d.setDate(d.getDate() + 1);
|
|
}
|
|
return result;
|
|
}, [currentMonth]);
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"calendar-grid grid grid-cols-7 gap-1 mb-4 min-h-[var(--calendar-grid-min-height)]",
|
|
className
|
|
)}
|
|
role="grid"
|
|
aria-label="Calendar"
|
|
>
|
|
{cells.map(({ date, key, month }, i) => {
|
|
const isOtherMonth = month !== currentMonth.getMonth();
|
|
const dayDuties = dutiesByDateMap[key] ?? [];
|
|
const eventSummaries = calendarEventsByDateMap[key] ?? [];
|
|
|
|
return (
|
|
<div key={`cell-${i}`} role="gridcell" className="min-h-0">
|
|
<CalendarDay
|
|
dateKey={key}
|
|
dayOfMonth={date.getDate()}
|
|
isToday={key === todayKey}
|
|
isOtherMonth={isOtherMonth}
|
|
duties={dayDuties}
|
|
eventSummaries={eventSummaries}
|
|
onDayClick={onDayClick}
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|