fix: update calendar agenda view to auto-scroll to today's events and ensure today's date is included in the groups

This commit is contained in:
Linus Rath
2026-03-19 19:25:34 +01:00
parent ff56245db8
commit 40cf164df3
3 changed files with 51 additions and 17 deletions
+8 -4
View File
@@ -7,7 +7,7 @@ import { Plus } from "lucide-react";
import { import {
startOfMonth, endOfMonth, startOfWeek, endOfWeek, startOfMonth, endOfMonth, startOfWeek, endOfWeek,
addMonths, subMonths, addWeeks, subWeeks, addDays, subDays, addMonths, subMonths, addWeeks, subWeeks, addDays, subDays,
format, parseISO, startOfDay, format, parseISO,
} from "date-fns"; } from "date-fns";
import { useCalendarStore } from "@/stores/calendar-store"; import { useCalendarStore } from "@/stores/calendar-store";
import { isCalendarViewMode } from "@/stores/calendar-store"; import { isCalendarViewMode } from "@/stores/calendar-store";
@@ -156,11 +156,15 @@ export default function CalendarPage() {
start: format(d, "yyyy-MM-dd'T'00:00:00"), start: format(d, "yyyy-MM-dd'T'00:00:00"),
end: format(d, "yyyy-MM-dd'T'23:59:59"), end: format(d, "yyyy-MM-dd'T'23:59:59"),
}; };
case "agenda": case "agenda": {
// Agenda always starts from today at the earliest
const today = startOfDay(new Date());
const agendaStart = d >= today ? d : today;
return { return {
start: format(d, "yyyy-MM-dd'T'00:00:00"), start: format(agendaStart, "yyyy-MM-dd'T'00:00:00"),
end: format(addDays(d, 30), "yyyy-MM-dd'T'23:59:59"), end: format(addDays(agendaStart, 30), "yyyy-MM-dd'T'23:59:59"),
}; };
}
} }
}, [selectedDate, normalizedViewMode, firstDayOfWeek]); }, [selectedDate, normalizedViewMode, firstDayOfWeek]);
+42 -13
View File
@@ -1,8 +1,8 @@
"use client"; "use client";
import { useMemo } from "react"; import { useMemo, useRef, useEffect, useCallback } from "react";
import { useTranslations, useFormatter } from "next-intl"; import { useTranslations, useFormatter } from "next-intl";
import { format, parseISO, isToday, isTomorrow } from "date-fns"; import { format, parseISO, isToday, isTomorrow, startOfDay } from "date-fns";
import { Calendar as CalendarIcon, MapPin, Users } from "lucide-react"; import { Calendar as CalendarIcon, MapPin, Users } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { parseDuration, getEventColor } from "./event-card"; import { parseDuration, getEventColor } from "./event-card";
@@ -27,6 +27,7 @@ interface DayGroup {
} }
export function CalendarAgendaView({ export function CalendarAgendaView({
selectedDate,
events, events,
calendars, calendars,
onSelectEvent, onSelectEvent,
@@ -43,6 +44,9 @@ export function CalendarAgendaView({
return map; return map;
}, [calendars]); }, [calendars]);
const todayRef = useRef<HTMLDivElement>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const grouped = useMemo(() => { const grouped = useMemo(() => {
const sorted = [...events].sort((a, b) => const sorted = [...events].sort((a, b) =>
new Date(a.start).getTime() - new Date(b.start).getTime() new Date(a.start).getTime() - new Date(b.start).getTime()
@@ -69,10 +73,38 @@ export function CalendarAgendaView({
} catch { /* skip invalid dates */ } } catch { /* skip invalid dates */ }
}); });
// Always include today's date in the groups so the view has a "Today" anchor
const todayKey = format(new Date(), "yyyy-MM-dd");
if (!groupMap.has(todayKey)) {
const todayGroup = { date: startOfDay(new Date()), dateKey: todayKey, events: [] as CalendarEvent[] };
groupMap.set(todayKey, todayGroup);
groups.push(todayGroup);
}
groups.sort((a, b) => a.date.getTime() - b.date.getTime()); groups.sort((a, b) => a.date.getTime() - b.date.getTime());
return groups; return groups;
}, [events]); }, [events]);
// Auto-scroll to today's section on mount and when selectedDate changes to today
const scrollToToday = useCallback(() => {
if (todayRef.current) {
todayRef.current.scrollIntoView({ block: "start" });
}
}, []);
useEffect(() => {
// Scroll to today on mount
const frame = requestAnimationFrame(scrollToToday);
return () => cancelAnimationFrame(frame);
}, [scrollToToday]);
useEffect(() => {
// Scroll to today when selectedDate changes to today
if (isToday(selectedDate)) {
scrollToToday();
}
}, [selectedDate, scrollToToday]);
const formatDateHeader = (date: Date): string => { const formatDateHeader = (date: Date): string => {
if (isToday(date)) return t("events.today_header"); if (isToday(date)) return t("events.today_header");
if (isTomorrow(date)) return t("events.tomorrow_header"); if (isTomorrow(date)) return t("events.tomorrow_header");
@@ -86,19 +118,10 @@ export function CalendarAgendaView({
return format(date, "HH:mm"); return format(date, "HH:mm");
}; };
if (grouped.length === 0) {
return (
<div className="flex flex-col items-center justify-center flex-1 text-muted-foreground">
<CalendarIcon className="w-12 h-12 mb-3 opacity-30" />
<p className="text-sm">{t("events.no_events")}</p>
</div>
);
}
return ( return (
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto" ref={scrollContainerRef}>
{grouped.map((group) => ( {grouped.map((group) => (
<div key={group.dateKey}> <div key={group.dateKey} ref={isToday(group.date) ? todayRef : undefined}>
<div className="sticky top-0 bg-muted/80 backdrop-blur-sm px-4 py-2 border-b border-border"> <div className="sticky top-0 bg-muted/80 backdrop-blur-sm px-4 py-2 border-b border-border">
<span className={cn( <span className={cn(
"text-sm font-medium", "text-sm font-medium",
@@ -111,6 +134,11 @@ export function CalendarAgendaView({
</span> </span>
</div> </div>
{group.events.length === 0 ? (
<div className="px-4 py-6 text-center text-sm text-muted-foreground">
{t("events.no_events")}
</div>
) : (
<div className="divide-y divide-border"> <div className="divide-y divide-border">
{group.events.map((ev) => { {group.events.map((ev) => {
const calId = getPrimaryCalendarId(ev); const calId = getPrimaryCalendarId(ev);
@@ -176,6 +204,7 @@ export function CalendarAgendaView({
); );
})} })}
</div> </div>
)}
</div> </div>
))} ))}
</div> </div>
+1
View File
@@ -655,6 +655,7 @@ export const useCalendarStore = create<CalendarStore>()(
return { return {
...mergedState, ...mergedState,
selectedDate: new Date(),
viewMode: getSafeCalendarViewMode(mergedState.viewMode), viewMode: getSafeCalendarViewMode(mergedState.viewMode),
}; };
}, },