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
161 lines
5.2 KiB
TypeScript
161 lines
5.2 KiB
TypeScript
import { Handlers, PageProps } from 'fresh/server.ts';
|
|
|
|
import { FreshContextState } from '/lib/types.ts';
|
|
import { Calendar, CalendarEvent, CalendarEventModel, CalendarModel } from '/lib/models/calendar.ts';
|
|
import { convertFormDataToObject } from '/lib/utils/misc.ts';
|
|
import { updateIcs } from '/lib/utils/calendar.ts';
|
|
import { getFormDataField } from '/lib/form-utils.tsx';
|
|
import ViewCalendarEvent, { formFields } from '/islands/calendar/ViewCalendarEvent.tsx';
|
|
import { AppConfig } from '/lib/config.ts';
|
|
|
|
interface Data {
|
|
calendarEvent: CalendarEvent;
|
|
calendars: Calendar[];
|
|
error?: string;
|
|
notice?: string;
|
|
formData: Record<string, any>;
|
|
}
|
|
|
|
export const handler: Handlers<Data, FreshContextState> = {
|
|
async GET(request, context) {
|
|
if (!context.state.user) {
|
|
return new Response('Redirect', { status: 303, headers: { 'Location': `/login` } });
|
|
}
|
|
|
|
const calendarConfig = await AppConfig.getCalendarConfig();
|
|
|
|
if (!calendarConfig.enableCalDavServer) {
|
|
throw new Error('CalDAV server is not enabled');
|
|
}
|
|
|
|
let { calendarEventId } = context.params;
|
|
|
|
const searchParams = new URL(request.url).searchParams;
|
|
const calendarId = searchParams.get('calendarId') || undefined;
|
|
|
|
if (!calendarId) {
|
|
return new Response('Bad request', { status: 400 });
|
|
}
|
|
|
|
// When editing a recurring event, we only allow the master
|
|
if (calendarEventId.includes(':')) {
|
|
calendarEventId = calendarEventId.split(':')[0];
|
|
}
|
|
|
|
const calendarEvent = await CalendarEventModel.get(context.state.user.id, calendarId, calendarEventId);
|
|
|
|
if (!calendarEvent) {
|
|
return new Response('Not found', { status: 404 });
|
|
}
|
|
|
|
const calendars = await CalendarModel.list(context.state.user.id);
|
|
|
|
return await context.render({ calendarEvent, calendars, formData: {} });
|
|
},
|
|
async POST(request, context) {
|
|
if (!context.state.user) {
|
|
return new Response('Redirect', { status: 303, headers: { 'Location': `/login` } });
|
|
}
|
|
|
|
const calendarConfig = await AppConfig.getCalendarConfig();
|
|
|
|
if (!calendarConfig.enableCalDavServer) {
|
|
throw new Error('CalDAV server is not enabled');
|
|
}
|
|
|
|
const { calendarEventId } = context.params;
|
|
|
|
const searchParams = new URL(request.url).searchParams;
|
|
const calendarId = searchParams.get('calendarId') || undefined;
|
|
|
|
if (!calendarId) {
|
|
return new Response('Bad request', { status: 400 });
|
|
}
|
|
|
|
const calendarEvent = await CalendarEventModel.get(context.state.user.id, calendarId, calendarEventId);
|
|
|
|
if (!calendarEvent) {
|
|
return new Response('Not found', { status: 404 });
|
|
}
|
|
|
|
const calendars = await CalendarModel.list(context.state.user.id);
|
|
|
|
const formData = await request.formData();
|
|
|
|
const updateType = getFormDataField(formData, 'update-type') as 'raw' | 'ui';
|
|
|
|
calendarEvent.title = getFormDataField(formData, 'title');
|
|
calendarEvent.startDate = new Date(`${getFormDataField(formData, 'startDate')}:00.000Z`);
|
|
calendarEvent.endDate = new Date(`${getFormDataField(formData, 'endDate')}:00.000Z`);
|
|
calendarEvent.isAllDay = getFormDataField(formData, 'isAllDay') === 'true';
|
|
calendarEvent.status = getFormDataField(formData, 'status') as CalendarEvent['status'];
|
|
|
|
calendarEvent.description = getFormDataField(formData, 'description') || undefined;
|
|
calendarEvent.eventUrl = getFormDataField(formData, 'eventUrl') || undefined;
|
|
calendarEvent.location = getFormDataField(formData, 'location') || undefined;
|
|
calendarEvent.transparency = getFormDataField(formData, 'transparency') as CalendarEvent['transparency'] ||
|
|
'opaque';
|
|
const rawIcs = getFormDataField(formData, 'ics');
|
|
|
|
try {
|
|
if (!calendarEvent.title) {
|
|
throw new Error(`Title is required.`);
|
|
}
|
|
|
|
formFields(calendarEvent, calendars, updateType).forEach((field) => {
|
|
if (field.required) {
|
|
const value = formData.get(field.name);
|
|
|
|
if (!value) {
|
|
throw new Error(`${field.label} is required`);
|
|
}
|
|
}
|
|
});
|
|
|
|
let updatedIcs = '';
|
|
|
|
if (updateType === 'raw') {
|
|
updatedIcs = rawIcs;
|
|
} else if (updateType === 'ui') {
|
|
if (!calendarEvent.title || !calendarEvent.startDate || !calendarEvent.endDate) {
|
|
throw new Error(`Title, start date, and end date are required.`);
|
|
}
|
|
|
|
updatedIcs = updateIcs(calendarEvent.data || '', calendarEvent);
|
|
}
|
|
|
|
await CalendarEventModel.update(context.state.user.id, calendarEvent.url!, updatedIcs);
|
|
|
|
return await context.render({
|
|
calendarEvent,
|
|
calendars,
|
|
notice: 'Event updated successfully!',
|
|
formData: convertFormDataToObject(formData),
|
|
});
|
|
} catch (error) {
|
|
console.error(error);
|
|
|
|
return await context.render({
|
|
calendarEvent,
|
|
calendars,
|
|
error: (error as Error).toString(),
|
|
formData: convertFormDataToObject(formData),
|
|
});
|
|
}
|
|
},
|
|
};
|
|
|
|
export default function CalendarEventPage({ data }: PageProps<Data, FreshContextState>) {
|
|
return (
|
|
<main>
|
|
<ViewCalendarEvent
|
|
initialCalendarEvent={data.calendarEvent}
|
|
calendars={data.calendars}
|
|
formData={data.formData}
|
|
error={data.error}
|
|
notice={data.notice}
|
|
/>
|
|
</main>
|
|
);
|
|
}
|