refactor(calendar): extract shared constants and event filtering utilities

Phase 1 of calendar refactoring:
- Create calendarConstants.ts with HOUR_HEIGHT_PX, SNAP_INTERVAL_MINUTES, etc.
- Create eventFiltering.ts with reusable filter functions:
  - getVisibleTimedEvents, getVisibleAllDayEvents, getVisibleOverflowEvents
  - filterByVisibleCalendars, filterByHourRange, getEventMinutes
- Update WeekView, DayView, MultiDayView, MonthView to use new utilities
- Remove ~200 lines of duplicated filtering logic

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-12-14 21:07:52 +01:00
parent cdc3cd3ec8
commit c4fe9ea192
6 changed files with 335 additions and 205 deletions

View file

@ -13,6 +13,13 @@
useCurrentTimeIndicator,
} from '$lib/composables/useVisibleHours.svelte';
import { toDate } from '$lib/utils/eventDateHelpers';
import { HOUR_HEIGHT_PX, SNAP_INTERVAL_MINUTES } from '$lib/utils/calendarConstants';
import {
getVisibleTimedEvents,
getVisibleAllDayEvents,
getVisibleOverflowEvents,
type OverflowEvents,
} from '$lib/utils/eventFiltering';
import TaskBlock from './TaskBlock.svelte';
import EventContextMenu from '$lib/components/event/EventContextMenu.svelte';
import { goto } from '$app/navigation';
@ -29,9 +36,9 @@
let { onQuickCreate, onEventClick, onTaskClick }: Props = $props();
// Constants
const HOUR_HEIGHT = 60; // pixels per hour
const SNAP_MINUTES = 15; // snap to 15-minute intervals
// Use shared constants
const HOUR_HEIGHT = HOUR_HEIGHT_PX;
const SNAP_MINUTES = SNAP_INTERVAL_MINUTES;
// Use composables for hour filtering and time indicator
const visibleHours = useVisibleHours();
@ -48,75 +55,37 @@
let currentTimePosition = $derived(minutesToPercent(timeIndicator.currentMinutes));
// Get timed events, filtering out those outside visible range when hour filter is enabled
let timedEvents = $derived.by(() => {
// Filter by visible calendars first
const visibleCalendarIds = new Set(calendarsStore.visibleCalendars.map((c) => c.id));
const allEvents = eventsStore
.getEventsForDay(viewStore.currentDate)
.filter((e) => !e.isAllDay && visibleCalendarIds.has(e.calendarId));
if (settingsStore.filterHoursEnabled) {
const visibleStartMinutes = settingsStore.dayStartHour * 60;
const visibleEndMinutes = settingsStore.dayEndHour * 60;
return allEvents.filter((event) => {
const start = toDate(event.startTime);
const end = toDate(event.endTime);
const eventStartMinutes = start.getHours() * 60 + start.getMinutes();
const eventEndMinutes = end.getHours() * 60 + end.getMinutes();
// Event overlaps with visible range
return eventStartMinutes < visibleEndMinutes && eventEndMinutes > visibleStartMinutes;
});
}
return allEvents;
});
let timedEvents = $derived(
getVisibleTimedEvents(
eventsStore.getEventsForDay(viewStore.currentDate),
calendarsStore.visibleCalendars,
{
filterHoursEnabled: settingsStore.filterHoursEnabled,
dayStartHour: settingsStore.dayStartHour,
dayEndHour: settingsStore.dayEndHour,
}
)
);
// Get events that are completely outside the visible time range
let overflowEvents = $derived.by(() => {
let overflowEvents = $derived.by((): OverflowEvents => {
if (!settingsStore.filterHoursEnabled) {
return { before: [] as CalendarEvent[], after: [] as CalendarEvent[] };
return { before: [], after: [] };
}
// Filter by visible calendars
const visibleCalendarIds = new Set(calendarsStore.visibleCalendars.map((c) => c.id));
const allEvents = eventsStore
.getEventsForDay(viewStore.currentDate)
.filter((e) => !e.isAllDay && visibleCalendarIds.has(e.calendarId));
const before: CalendarEvent[] = [];
const after: CalendarEvent[] = [];
const visibleStartMinutes = settingsStore.dayStartHour * 60;
const visibleEndMinutes = settingsStore.dayEndHour * 60;
for (const event of allEvents) {
const start = toDate(event.startTime);
const end = toDate(event.endTime);
const eventStartMinutes = start.getHours() * 60 + start.getMinutes();
const eventEndMinutes = end.getHours() * 60 + end.getMinutes();
// Event ends before visible range starts
if (eventEndMinutes <= visibleStartMinutes) {
before.push(event);
}
// Event starts after visible range ends
else if (eventStartMinutes >= visibleEndMinutes) {
after.push(event);
}
}
return { before, after };
return getVisibleOverflowEvents(
eventsStore.getEventsForDay(viewStore.currentDate),
calendarsStore.visibleCalendars,
settingsStore.dayStartHour,
settingsStore.dayEndHour
);
});
let allDayEvents = $derived.by(() => {
const visibleCalendarIds = new Set(calendarsStore.visibleCalendars.map((c) => c.id));
return eventsStore
.getEventsForDay(viewStore.currentDate)
.filter((e) => e.isAllDay && visibleCalendarIds.has(e.calendarId));
});
let allDayEvents = $derived(
getVisibleAllDayEvents(
eventsStore.getEventsForDay(viewStore.currentDate),
calendarsStore.visibleCalendars
)
);
// Get display mode for an event (per-event override takes precedence over global setting)
function getEventDisplayMode(event: CalendarEvent): 'header' | 'block' {

View file

@ -30,6 +30,7 @@
} from 'date-fns';
import { de } from 'date-fns/locale';
import { _ } from 'svelte-i18n';
import { filterByVisibleCalendars } from '$lib/utils/eventFiltering';
import type { CalendarEvent } from '@calendar/shared';
@ -194,17 +195,18 @@
// ============================================================================
// Event Handlers
// ============================================================================
function getEventsForDay(day: Date) {
const visibleCalendarIds = new Set(calendarsStore.visibleCalendars.map((c) => c.id));
return eventsStore
.getEventsForDay(day)
.filter((e) => visibleCalendarIds.has(e.calendarId))
.slice(0, 3); // Max 3 events shown
function getEventsForDay(day: Date): CalendarEvent[] {
return filterByVisibleCalendars(
eventsStore.getEventsForDay(day),
calendarsStore.visibleCalendars
).slice(0, 3); // Max 3 events shown
}
function getAllEventsForDay(day: Date) {
const visibleCalendarIds = new Set(calendarsStore.visibleCalendars.map((c) => c.id));
return eventsStore.getEventsForDay(day).filter((e) => visibleCalendarIds.has(e.calendarId));
function getAllEventsForDay(day: Date): CalendarEvent[] {
return filterByVisibleCalendars(
eventsStore.getEventsForDay(day),
calendarsStore.visibleCalendars
);
}
function handleDayClick(day: Date, e: MouseEvent) {

View file

@ -11,6 +11,13 @@
useCurrentTimeIndicator,
} from '$lib/composables/useVisibleHours.svelte';
import { toDate } from '$lib/utils/eventDateHelpers';
import { HOUR_HEIGHT_PX, SNAP_INTERVAL_MINUTES } from '$lib/utils/calendarConstants';
import {
getVisibleTimedEvents,
getVisibleAllDayEvents,
getVisibleOverflowEvents,
type OverflowEvents,
} from '$lib/utils/eventFiltering';
import TaskBlock from './TaskBlock.svelte';
import { goto } from '$app/navigation';
import {
@ -27,9 +34,9 @@
import { de, enUS, fr, es, it } from 'date-fns/locale';
import { locale } from 'svelte-i18n';
// Constants
const HOUR_HEIGHT = 60; // px - should match CSS --hour-height
const MINUTES_PER_SLOT = 15; // Snap to 15-minute intervals
// Use shared constants
const HOUR_HEIGHT = HOUR_HEIGHT_PX;
const MINUTES_PER_SLOT = SNAP_INTERVAL_MINUTES;
import type { CalendarEvent } from '@calendar/shared';
@ -119,75 +126,35 @@
// Reference to the days container for position calculations
let daysContainerEl: HTMLDivElement;
function getEventsForDay(day: Date) {
// Filter by visible calendars first
const visibleCalendarIds = new Set(calendarsStore.visibleCalendars.map((c) => c.id));
const allEvents = eventsStore
.getEventsForDay(day)
.filter((e) => !e.isAllDay && visibleCalendarIds.has(e.calendarId));
// If hour filtering is enabled, only show events that overlap with visible range
if (settingsStore.filterHoursEnabled) {
const visibleStartMinutes = settingsStore.dayStartHour * 60;
const visibleEndMinutes = settingsStore.dayEndHour * 60;
return allEvents.filter((event) => {
const start = toDate(event.startTime);
const end = toDate(event.endTime);
const eventStartMinutes = start.getHours() * 60 + start.getMinutes();
const eventEndMinutes = end.getHours() * 60 + end.getMinutes();
// Event overlaps with visible range
return eventStartMinutes < visibleEndMinutes && eventEndMinutes > visibleStartMinutes;
});
}
return allEvents;
function getEventsForDay(day: Date): CalendarEvent[] {
return getVisibleTimedEvents(
eventsStore.getEventsForDay(day),
calendarsStore.visibleCalendars,
{
filterHoursEnabled: settingsStore.filterHoursEnabled,
dayStartHour: settingsStore.dayStartHour,
dayEndHour: settingsStore.dayEndHour,
}
);
}
// Get events that are completely outside the visible time range
function getOverflowEventsForDay(day: Date): { before: CalendarEvent[]; after: CalendarEvent[] } {
function getOverflowEventsForDay(day: Date): OverflowEvents {
if (!settingsStore.filterHoursEnabled) {
return { before: [], after: [] };
}
// Filter by visible calendars
const visibleCalendarIds = new Set(calendarsStore.visibleCalendars.map((c) => c.id));
const allEvents = eventsStore
.getEventsForDay(day)
.filter((e) => !e.isAllDay && visibleCalendarIds.has(e.calendarId));
const before: CalendarEvent[] = [];
const after: CalendarEvent[] = [];
const visibleStartMinutes = settingsStore.dayStartHour * 60;
const visibleEndMinutes = settingsStore.dayEndHour * 60;
for (const event of allEvents) {
const start = toDate(event.startTime);
const end = toDate(event.endTime);
const eventStartMinutes = start.getHours() * 60 + start.getMinutes();
const eventEndMinutes = end.getHours() * 60 + end.getMinutes();
// Event ends before visible range starts
if (eventEndMinutes <= visibleStartMinutes) {
before.push(event);
}
// Event starts after visible range ends
else if (eventStartMinutes >= visibleEndMinutes) {
after.push(event);
}
}
return { before, after };
return getVisibleOverflowEvents(
eventsStore.getEventsForDay(day),
calendarsStore.visibleCalendars,
settingsStore.dayStartHour,
settingsStore.dayEndHour
);
}
function getAllDayEventsForDay(day: Date) {
const visibleCalendarIds = new Set(calendarsStore.visibleCalendars.map((c) => c.id));
return eventsStore
.getEventsForDay(day)
.filter((e) => e.isAllDay && visibleCalendarIds.has(e.calendarId));
function getAllDayEventsForDay(day: Date): CalendarEvent[] {
return getVisibleAllDayEvents(
eventsStore.getEventsForDay(day),
calendarsStore.visibleCalendars
);
}
// Get display mode for an event (per-event override takes precedence over global setting)

View file

@ -13,6 +13,13 @@
useCurrentTimeIndicator,
} from '$lib/composables/useVisibleHours.svelte';
import { toDate } from '$lib/utils/eventDateHelpers';
import { HOUR_HEIGHT_PX, SNAP_INTERVAL_MINUTES } from '$lib/utils/calendarConstants';
import {
getVisibleTimedEvents,
getVisibleAllDayEvents,
getVisibleOverflowEvents,
type OverflowEvents,
} from '$lib/utils/eventFiltering';
import TaskBlock from './TaskBlock.svelte';
import EventContextMenu from '$lib/components/event/EventContextMenu.svelte';
import { goto } from '$app/navigation';
@ -41,9 +48,9 @@
let { onQuickCreate, onEventClick, onTaskClick }: Props = $props();
// Constants
const HOUR_HEIGHT = 60; // px - should match CSS --hour-height
const MINUTES_PER_SLOT = 15; // Snap to 15-minute intervals
// Use shared constants
const HOUR_HEIGHT = HOUR_HEIGHT_PX;
const MINUTES_PER_SLOT = SNAP_INTERVAL_MINUTES;
// Get date-fns locale based on current app locale
const dateLocales = { de, en: enUS, fr, es, it };
@ -147,75 +154,35 @@
selectedBirthday = null;
}
function getEventsForDay(day: Date) {
// Filter by visible calendars first
const visibleCalendarIds = new Set(calendarsStore.visibleCalendars.map((c) => c.id));
const allEvents = eventsStore
.getEventsForDay(day)
.filter((e) => !e.isAllDay && visibleCalendarIds.has(e.calendarId));
// If hour filtering is enabled, only show events that overlap with visible range
if (settingsStore.filterHoursEnabled) {
const visibleStartMinutes = settingsStore.dayStartHour * 60;
const visibleEndMinutes = settingsStore.dayEndHour * 60;
return allEvents.filter((event) => {
const start = toDate(event.startTime);
const end = toDate(event.endTime);
const eventStartMinutes = start.getHours() * 60 + start.getMinutes();
const eventEndMinutes = end.getHours() * 60 + end.getMinutes();
// Event overlaps with visible range
return eventStartMinutes < visibleEndMinutes && eventEndMinutes > visibleStartMinutes;
});
}
return allEvents;
function getEventsForDay(day: Date): CalendarEvent[] {
return getVisibleTimedEvents(
eventsStore.getEventsForDay(day),
calendarsStore.visibleCalendars,
{
filterHoursEnabled: settingsStore.filterHoursEnabled,
dayStartHour: settingsStore.dayStartHour,
dayEndHour: settingsStore.dayEndHour,
}
);
}
// Get events that are completely outside the visible time range
function getOverflowEventsForDay(day: Date): { before: CalendarEvent[]; after: CalendarEvent[] } {
function getOverflowEventsForDay(day: Date): OverflowEvents {
if (!settingsStore.filterHoursEnabled) {
return { before: [], after: [] };
}
// Filter by visible calendars
const visibleCalendarIds = new Set(calendarsStore.visibleCalendars.map((c) => c.id));
const allEvents = eventsStore
.getEventsForDay(day)
.filter((e) => !e.isAllDay && visibleCalendarIds.has(e.calendarId));
const before: CalendarEvent[] = [];
const after: CalendarEvent[] = [];
const visibleStartMinutes = settingsStore.dayStartHour * 60;
const visibleEndMinutes = settingsStore.dayEndHour * 60;
for (const event of allEvents) {
const start = toDate(event.startTime);
const end = toDate(event.endTime);
const eventStartMinutes = start.getHours() * 60 + start.getMinutes();
const eventEndMinutes = end.getHours() * 60 + end.getMinutes();
// Event ends before visible range starts
if (eventEndMinutes <= visibleStartMinutes) {
before.push(event);
}
// Event starts after visible range ends
else if (eventStartMinutes >= visibleEndMinutes) {
after.push(event);
}
}
return { before, after };
return getVisibleOverflowEvents(
eventsStore.getEventsForDay(day),
calendarsStore.visibleCalendars,
settingsStore.dayStartHour,
settingsStore.dayEndHour
);
}
function getAllDayEventsForDay(day: Date) {
const visibleCalendarIds = new Set(calendarsStore.visibleCalendars.map((c) => c.id));
return eventsStore
.getEventsForDay(day)
.filter((e) => e.isAllDay && visibleCalendarIds.has(e.calendarId));
function getAllDayEventsForDay(day: Date): CalendarEvent[] {
return getVisibleAllDayEvents(
eventsStore.getEventsForDay(day),
calendarsStore.visibleCalendars
);
}
// Get display mode for an event (per-event override takes precedence over global setting)

View file

@ -0,0 +1,60 @@
/**
* Shared calendar constants
* Single source of truth for magic numbers used across calendar views
*/
/**
* Height of one hour in pixels (should match CSS --hour-height variable)
*/
export const HOUR_HEIGHT_PX = 60;
/**
* Snap interval for drag/drop and resize operations in minutes
*/
export const SNAP_INTERVAL_MINUTES = 15;
/**
* Default event duration in minutes when creating quick events
*/
export const DEFAULT_EVENT_DURATION_MINUTES = 60;
/**
* Minimum event height as percentage of visible hours
*/
export const MIN_EVENT_HEIGHT_PERCENT = 1.5;
/**
* Maximum number of event dots to show in month view cells
*/
export const MAX_EVENT_DOTS = 5;
/**
* Days buffer for infinite scroll in date strip
*/
export const DATE_STRIP_BUFFER_DAYS = 60;
/**
* Default visible hours range
*/
export const DEFAULT_DAY_START_HOUR = 0;
export const DEFAULT_DAY_END_HOUR = 24;
/**
* Week starts on (0 = Sunday, 1 = Monday)
*/
export const DEFAULT_WEEK_STARTS_ON = 1;
/**
* All constants as a single object for convenient destructuring
*/
export const CALENDAR_CONSTANTS = {
HOUR_HEIGHT_PX,
SNAP_INTERVAL_MINUTES,
DEFAULT_EVENT_DURATION_MINUTES,
MIN_EVENT_HEIGHT_PERCENT,
MAX_EVENT_DOTS,
DATE_STRIP_BUFFER_DAYS,
DEFAULT_DAY_START_HOUR,
DEFAULT_DAY_END_HOUR,
DEFAULT_WEEK_STARTS_ON,
} as const;

View file

@ -0,0 +1,165 @@
/**
* Event Filtering Utilities
* Reusable functions for filtering calendar events by visibility, time range, etc.
*/
import type { CalendarEvent } from '@calendar/shared';
import type { Calendar } from '@calendar/shared';
import { toDate } from './eventDateHelpers';
/**
* Create a Set of visible calendar IDs for efficient lookup
*/
export function getVisibleCalendarIds(visibleCalendars: Calendar[]): Set<string> {
return new Set(visibleCalendars.map((c) => c.id));
}
/**
* Filter events to only include those from visible calendars
*/
export function filterByVisibleCalendars(
events: CalendarEvent[],
visibleCalendars: Calendar[]
): CalendarEvent[] {
const visibleIds = getVisibleCalendarIds(visibleCalendars);
return events.filter((e) => visibleIds.has(e.calendarId));
}
/**
* Filter events to only include timed (non-all-day) events
*/
export function filterTimedEvents(events: CalendarEvent[]): CalendarEvent[] {
return events.filter((e) => !e.isAllDay);
}
/**
* Filter events to only include all-day events
*/
export function filterAllDayEvents(events: CalendarEvent[]): CalendarEvent[] {
return events.filter((e) => e.isAllDay);
}
/**
* Get event time in minutes from midnight
*/
export function getEventMinutes(event: CalendarEvent): { start: number; end: number } {
const start = toDate(event.startTime);
const end = toDate(event.endTime);
return {
start: start.getHours() * 60 + start.getMinutes(),
end: end.getHours() * 60 + end.getMinutes(),
};
}
/**
* Check if an event overlaps with a given time range (in minutes from midnight)
*/
export function eventOverlapsTimeRange(
event: CalendarEvent,
startMinutes: number,
endMinutes: number
): boolean {
const { start: eventStart, end: eventEnd } = getEventMinutes(event);
return eventStart < endMinutes && eventEnd > startMinutes;
}
/**
* Filter timed events that overlap with a visible hour range
*/
export function filterByHourRange(
events: CalendarEvent[],
dayStartHour: number,
dayEndHour: number
): CalendarEvent[] {
const startMinutes = dayStartHour * 60;
const endMinutes = dayEndHour * 60;
return events.filter((event) => eventOverlapsTimeRange(event, startMinutes, endMinutes));
}
/**
* Result type for overflow events
*/
export interface OverflowEvents {
before: CalendarEvent[];
after: CalendarEvent[];
}
/**
* Get events that are outside the visible hour range
* Returns events that end before the visible range starts (before)
* and events that start after the visible range ends (after)
*/
export function getOverflowEvents(
events: CalendarEvent[],
dayStartHour: number,
dayEndHour: number
): OverflowEvents {
const startMinutes = dayStartHour * 60;
const endMinutes = dayEndHour * 60;
const before: CalendarEvent[] = [];
const after: CalendarEvent[] = [];
for (const event of events) {
const { start: eventStart, end: eventEnd } = getEventMinutes(event);
if (eventEnd <= startMinutes) {
before.push(event);
} else if (eventStart >= endMinutes) {
after.push(event);
}
}
return { before, after };
}
/**
* Combined filter: Get visible timed events for a day with optional hour filtering
*/
export function getVisibleTimedEvents(
events: CalendarEvent[],
visibleCalendars: Calendar[],
options?: {
filterHoursEnabled?: boolean;
dayStartHour?: number;
dayEndHour?: number;
}
): CalendarEvent[] {
let filtered = filterByVisibleCalendars(events, visibleCalendars);
filtered = filterTimedEvents(filtered);
if (
options?.filterHoursEnabled &&
options.dayStartHour !== undefined &&
options.dayEndHour !== undefined
) {
filtered = filterByHourRange(filtered, options.dayStartHour, options.dayEndHour);
}
return filtered;
}
/**
* Combined filter: Get visible all-day events for a day
*/
export function getVisibleAllDayEvents(
events: CalendarEvent[],
visibleCalendars: Calendar[]
): CalendarEvent[] {
let filtered = filterByVisibleCalendars(events, visibleCalendars);
return filterAllDayEvents(filtered);
}
/**
* Combined filter: Get overflow events for visible calendars
*/
export function getVisibleOverflowEvents(
events: CalendarEvent[],
visibleCalendars: Calendar[],
dayStartHour: number,
dayEndHour: number
): OverflowEvents {
let filtered = filterByVisibleCalendars(events, visibleCalendars);
filtered = filterTimedEvents(filtered);
return getOverflowEvents(filtered, dayStartHour, dayEndHour);
}