Basic CalDav UI (Calendar)

This implements a basic CalDav UI, titled "Calendar". It allows creating new calendars and events with a start and end date, URL, location, and description.

You can also import and export ICS (VCALENDAR + VEVENT) files.

It allows editing the ICS directly, for power users.

Additionally, you can hide/display events from certain calendars, change their names and their colors. If there's no calendar created yet in your CalDav server (first-time setup), it'll automatically create one, titled "Calendar".

You can also change the display timezone for the calendar from the settings.

Finally, there's some minor documentation fixes and some other minor tweaks.

Closes #56
Closes #89
This commit is contained in:
Bruno Bernardino
2025-09-06 12:46:13 +01:00
parent f14c40d05d
commit 15dcc8803d
39 changed files with 6483 additions and 30 deletions

82
routes/calendar.tsx Normal file
View File

@@ -0,0 +1,82 @@
import { Handlers, PageProps } from 'fresh/server.ts';
import { FreshContextState } from '/lib/types.ts';
import { Calendar, CalendarEvent, CalendarEventModel, CalendarModel } from '/lib/models/calendar.ts';
import CalendarWrapper from '/islands/calendar/CalendarWrapper.tsx';
import { AppConfig } from '/lib/config.ts';
import { getDateRangeForCalendarView } from '/lib/utils/calendar.ts';
interface Data {
userCalendars: Calendar[];
userCalendarEvents: CalendarEvent[];
baseUrl: string;
view: 'day' | 'week' | 'month';
startDate: string;
timezoneId: string;
timezoneUtcOffset: number;
}
export const handler: Handlers<Data, FreshContextState> = {
async GET(request, context) {
if (!context.state.user) {
return new Response('Redirect', { status: 303, headers: { 'Location': `/login` } });
}
const baseUrl = (await AppConfig.getConfig()).auth.baseUrl;
const calendarConfig = await AppConfig.getCalendarConfig();
if (!calendarConfig.enableCalDavServer) {
throw new Error('CalDAV server is not enabled');
}
const userId = context.state.user.id;
const timezoneId = context.state.user.extra.timezone?.id || 'UTC';
const timezoneUtcOffset = context.state.user.extra.timezone?.utcOffset || 0;
const searchParams = new URL(request.url).searchParams;
const view = (searchParams.get('view') as Data['view']) || 'week';
const startDate = searchParams.get('startDate') || new Date().toISOString().substring(0, 10);
let userCalendars = await CalendarModel.list(userId);
// Create default calendar if none exists
if (userCalendars.length === 0) {
await CalendarModel.create(userId, 'Calendar');
userCalendars = await CalendarModel.list(userId);
}
const visibleCalendarIds = userCalendars.filter((calendar) => calendar.isVisible).map((calendar) => calendar.uid!);
const dateRange = getDateRangeForCalendarView(startDate, view);
const userCalendarEvents = await CalendarEventModel.list(userId, visibleCalendarIds, dateRange);
return await context.render({
userCalendars,
userCalendarEvents,
baseUrl,
view,
startDate,
timezoneId,
timezoneUtcOffset,
});
},
};
export default function ContactsPage({ data }: PageProps<Data, FreshContextState>) {
return (
<main>
<CalendarWrapper
initialCalendars={data?.userCalendars || []}
initialCalendarEvents={data?.userCalendarEvents || []}
baseUrl={data.baseUrl}
view={data.view}
startDate={data.startDate}
timezoneId={data.timezoneId}
timezoneUtcOffset={data.timezoneUtcOffset}
/>
</main>
);
}