Files
duty-teller/webapp-next/src/components/calendar/CalendarGrid.tsx
Nikolay Tatarinov 6e2188787e feat: implement pending month handling in calendar components
- 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.
2026-03-03 17:20:23 +03:00

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