Support exporting calendar events

Also update Deno and libraries
This commit is contained in:
Bruno Bernardino
2024-03-25 15:50:15 +00:00
parent 5b3217cdfc
commit a788456751
10 changed files with 98 additions and 47 deletions

2
.dvmrc
View File

@@ -1 +1 @@
1.41.0
1.41.3

View File

@@ -9,7 +9,7 @@ jobs:
- uses: actions/checkout@v4
- uses: denoland/setup-deno@v1
with:
deno-version: v1.41.0
deno-version: v1.41.3
- run: |
make test
make build

View File

@@ -1,4 +1,4 @@
FROM denoland/deno:alpine-1.41.0
FROM denoland/deno:alpine-1.41.3
EXPOSE 8000

View File

@@ -1,8 +1,8 @@
import { useSignal } from '@preact/signals';
import { Calendar, CalendarEvent } from '/lib/types.ts';
import { baseUrl, capitalizeWord } from '/lib/utils.ts';
// import { RequestBody as GetRequestBody, ResponseBody as GetResponseBody } from '/routes/api/calendar/get.tsx';
import { baseUrl, capitalizeWord, formatCalendarEventsToVCalendar } from '/lib/utils.ts';
import { RequestBody as GetRequestBody, ResponseBody as GetResponseBody } from '/routes/api/calendar/get-events.tsx';
import { RequestBody as AddRequestBody, ResponseBody as AddResponseBody } from '/routes/api/calendar/add-event.tsx';
import {
RequestBody as DeleteRequestBody,
@@ -42,6 +42,8 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents,
const dateFormat = new Intl.DateTimeFormat('en-GB', { year: 'numeric', month: 'long' });
const today = new Date().toISOString().substring(0, 10);
const visibleCalendars = calendars.value.filter((calendar) => calendar.is_visible);
function onClickAddEvent(startDate = new Date(), isAllDay = false) {
if (newEventModal.value.isOpen) {
newEventModal.value = {
@@ -75,7 +77,7 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents,
try {
const requestBody: AddRequestBody = {
calendarIds: calendars.value.map((calendar) => calendar.id),
calendarIds: visibleCalendars.map((calendar) => calendar.id),
calendarView: view,
calendarStartDate: startDate,
calendarId: newEvent.calendar_id,
@@ -147,7 +149,7 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents,
try {
const requestBody: DeleteRequestBody = {
calendarIds: calendars.value.map((calendar) => calendar.id),
calendarIds: visibleCalendars.map((calendar) => calendar.id),
calendarView: view,
calendarStartDate: startDate,
calendarEventId,
@@ -299,7 +301,7 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents,
};
}
function onClickExportICS() {
async function onClickExportICS() {
isImportExportOptionsDropdownOpen.value = false;
if (isExporting.value) {
@@ -308,49 +310,47 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents,
isExporting.value = true;
// const fileName = ['calendars-', new Date().toISOString().substring(0, 19).replace(/:/g, '-'), '.ics']
// .join('');
const fileName = ['calendar-', new Date().toISOString().substring(0, 19).replace(/:/g, '-'), '.ics']
.join('');
// try {
// const requestBody: GetRequestBody = {};
// const response = await fetch(`/api/calendar/get`, {
// method: 'POST',
// body: JSON.stringify(requestBody),
// });
// const result = await response.json() as GetResponseBody;
try {
const requestBody: GetRequestBody = { calendarIds: visibleCalendars.map((calendar) => calendar.id) };
const response = await fetch(`/api/calendar/get-events`, {
method: 'POST',
body: JSON.stringify(requestBody),
});
const result = await response.json() as GetResponseBody;
// if (!result.success) {
// throw new Error('Failed to get contact!');
// }
if (!result.success) {
throw new Error('Failed to get contact!');
}
// const exportContents = formatContactToVCard([...result.contacts]);
const exportContents = formatCalendarEventsToVCalendar([...result.calendarEvents], calendars.value);
// // Add content-type
// const vCardContent = ['data:text/vcard; charset=utf-8,', encodeURIComponent(exportContents)].join('');
// Add content-type
const vCardContent = ['data:text/calendar; charset=utf-8,', encodeURIComponent(exportContents)].join('');
// // Download the file
// const data = vCardContent;
// const link = document.createElement('a');
// link.setAttribute('href', data);
// link.setAttribute('download', fileName);
// link.click();
// link.remove();
// } catch (error) {
// console.error(error);
// }
// Download the file
const data = vCardContent;
const link = document.createElement('a');
link.setAttribute('href', data);
link.setAttribute('download', fileName);
link.click();
link.remove();
} catch (error) {
console.error(error);
}
isExporting.value = false;
}
const visibleCalendars = calendars.value.filter((calendar) => calendar.is_visible);
return (
<>
<section class='flex flex-row items-center justify-between mb-4'>
<section class='relative inline-block text-left mr-2'>
<section class='flex flex-row items-center justify-start'>
<a href='/calendars' class='mr-4 whitespace-nowrap'>Manage calendars</a>
<SearchEvents calendars={calendars.value} onClickOpenEvent={onClickOpenEvent} />
<SearchEvents calendars={visibleCalendars} onClickOpenEvent={onClickOpenEvent} />
</section>
</section>

View File

@@ -11,6 +11,7 @@ import * as $api_calendar_add_event from './routes/api/calendar/add-event.tsx';
import * as $api_calendar_add from './routes/api/calendar/add.tsx';
import * as $api_calendar_delete_event from './routes/api/calendar/delete-event.tsx';
import * as $api_calendar_delete from './routes/api/calendar/delete.tsx';
import * as $api_calendar_get_events from './routes/api/calendar/get-events.tsx';
import * as $api_calendar_search_events from './routes/api/calendar/search-events.tsx';
import * as $api_calendar_update from './routes/api/calendar/update.tsx';
import * as $api_contacts_add from './routes/api/contacts/add.tsx';
@@ -70,6 +71,7 @@ const manifest = {
'./routes/api/calendar/add.tsx': $api_calendar_add,
'./routes/api/calendar/delete-event.tsx': $api_calendar_delete_event,
'./routes/api/calendar/delete.tsx': $api_calendar_delete,
'./routes/api/calendar/get-events.tsx': $api_calendar_get_events,
'./routes/api/calendar/search-events.tsx': $api_calendar_search_events,
'./routes/api/calendar/update.tsx': $api_calendar_update,
'./routes/api/contacts/add.tsx': $api_contacts_add,

View File

@@ -13,7 +13,7 @@
"tailwindcss": "npm:tailwindcss@3.4.1",
"tailwindcss/": "npm:/tailwindcss@3.4.1/",
"tailwindcss/plugin": "npm:/tailwindcss@3.4.1/plugin.js",
"std/": "https://deno.land/std@0.217.0/",
"$std/": "https://deno.land/std@0.217.0/"
"std/": "https://deno.land/std@0.220.1/",
"$std/": "https://deno.land/std@0.220.1/"
}
}

View File

@@ -651,12 +651,15 @@ export function parseVCardFromTextContents(text: string): Partial<Contact>[] {
}
// TODO: Build this
export function formatCalendarEventsToVCalendar(calendarEvents: CalendarEvent[], _calendar: Calendar): string {
export function formatCalendarEventsToVCalendar(
calendarEvents: CalendarEvent[],
_calendars: Pick<Calendar, 'id' | 'color' | 'is_visible'>[],
): string {
const vCalendarText = calendarEvents.map((calendarEvent) =>
`BEGIN:VEVENT
DTSTAMP:${calendarEvent.start_date.toISOString().substring(0, 19).replaceAll('T', '').replaceAll(':', '')}
DTSTART:${calendarEvent.start_date.toISOString().substring(0, 19).replaceAll('T', '').replaceAll(':', '')}
DTEND:${calendarEvent.end_date.toISOString().substring(0, 19).replaceAll('T', '').replaceAll(':', '')}
DTSTAMP:${new Date(calendarEvent.start_date).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '')}
DTSTART:${new Date(calendarEvent.start_date).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '')}
DTEND:${new Date(calendarEvent.end_date).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '')}
ORGANIZER;CN=:MAILTO:${calendarEvent.extra.organizer_email}
SUMMARY:${calendarEvent.title}
${calendarEvent.extra.uid ? `UID:${calendarEvent.extra.uid}` : ''}

View File

@@ -0,0 +1,38 @@
import { Handlers } from 'fresh/server.ts';
import { CalendarEvent, FreshContextState } from '/lib/types.ts';
import { getCalendarEvents } from '/lib/data/calendar.ts';
interface Data {}
export interface RequestBody {
calendarIds: string[];
}
export interface ResponseBody {
success: boolean;
calendarEvents: CalendarEvent[];
}
export const handler: Handlers<Data, FreshContextState> = {
async POST(request, context) {
if (!context.state.user) {
return new Response('Unauthorized', { status: 401 });
}
const requestBody = await request.clone().json() as RequestBody;
if (!requestBody.calendarIds) {
return new Response('Bad Request', { status: 400 });
}
const calendarEvents = await getCalendarEvents(
context.state.user.id,
requestBody.calendarIds,
);
const responseBody: ResponseBody = { success: true, calendarEvents };
return new Response(JSON.stringify(responseBody));
},
};

View File

@@ -118,7 +118,7 @@ export const handler: Handler<Data, FreshContextState> = async (request, context
const calendarEvents = await getCalendarEvents(context.state.user.id, [calendar.id]);
if (request.method === 'GET') {
const response = new Response(formatCalendarEventsToVCalendar(calendarEvents, calendar), {
const response = new Response(formatCalendarEventsToVCalendar(calendarEvents, [calendar]), {
status: 200,
});
@@ -172,7 +172,7 @@ export const handler: Handler<Data, FreshContextState> = async (request, context
if (includeVCalendar) {
parsedCalendarEvent['d:propstat'][0]['d:prop']['cal:calendar-data'] = escapeXml(
formatCalendarEventsToVCalendar([calendarEvent], calendar!),
formatCalendarEventsToVCalendar([calendarEvent], [calendar!]),
);
}

View File

@@ -10,7 +10,7 @@ import {
formatCalendarEventsToVCalendar,
parseVCalendarFromTextContents,
} from '/lib/utils.ts';
import { getCalendar, getCalendarEvent, getCalendarEvents } from '/lib/data/calendar.ts';
import { getCalendar, getCalendarEvent, getCalendarEvents, updateCalendarEvent } from '/lib/data/calendar.ts';
import { createSessionCookie } from '/lib/auth.ts';
interface Data {}
@@ -157,7 +157,15 @@ export const handler: Handler<Data, FreshContextState> = async (request, context
// }
if (request.method === 'GET') {
const response = new Response(formatCalendarEventsToVCalendar([calendarEvent], calendar), {
// Set a UID if there isn't one
if (!calendarEvent.extra.uid) {
calendarEvent.extra.uid = crypto.randomUUID();
await updateCalendarEvent(calendarEvent);
calendarEvent = await getCalendarEvent(calendarEventId, context.state.user.id);
}
const response = new Response(formatCalendarEventsToVCalendar([calendarEvent], [calendar]), {
status: 200,
});
@@ -195,7 +203,7 @@ export const handler: Handler<Data, FreshContextState> = async (request, context
if (includeVCalendar) {
parsedCalendarEvent['d:propstat'][0]['d:prop']['cal:calendar-data'] = escapeXml(
formatCalendarEventsToVCalendar([calendarEvent], calendar!),
formatCalendarEventsToVCalendar([calendarEvent], [calendar!]),
);
}