From a788456751a9b65c862c61a8ec8fcdba841bca83 Mon Sep 17 00:00:00 2001 From: Bruno Bernardino Date: Mon, 25 Mar 2024 15:50:15 +0000 Subject: [PATCH] Support exporting calendar events Also update Deno and libraries --- .dvmrc | 2 +- .github/workflows/test.yml | 2 +- Dockerfile | 2 +- components/calendar/MainCalendar.tsx | 66 +++++++++---------- fresh.gen.ts | 2 + import_map.json | 4 +- lib/utils.ts | 11 ++-- routes/api/calendar/get-events.tsx | 38 +++++++++++ routes/dav/calendars/[calendarId].tsx | 4 +- .../[calendarId]/[calendarEventId].ics.tsx | 14 +++- 10 files changed, 98 insertions(+), 47 deletions(-) create mode 100644 routes/api/calendar/get-events.tsx diff --git a/.dvmrc b/.dvmrc index 7d47e59..021b2b8 100644 --- a/.dvmrc +++ b/.dvmrc @@ -1 +1 @@ -1.41.0 +1.41.3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7844e97..d90355b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/Dockerfile b/Dockerfile index 5ba2b0c..fc26b69 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM denoland/deno:alpine-1.41.0 +FROM denoland/deno:alpine-1.41.3 EXPOSE 8000 diff --git a/components/calendar/MainCalendar.tsx b/components/calendar/MainCalendar.tsx index dc7a68b..df7855c 100644 --- a/components/calendar/MainCalendar.tsx +++ b/components/calendar/MainCalendar.tsx @@ -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 ( <>
Manage calendars - +
diff --git a/fresh.gen.ts b/fresh.gen.ts index fabfc53..009e9dc 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -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, diff --git a/import_map.json b/import_map.json index 2b31925..d13da9a 100644 --- a/import_map.json +++ b/import_map.json @@ -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/" } } diff --git a/lib/utils.ts b/lib/utils.ts index 54da672..6d0089d 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -651,12 +651,15 @@ export function parseVCardFromTextContents(text: string): Partial[] { } // TODO: Build this -export function formatCalendarEventsToVCalendar(calendarEvents: CalendarEvent[], _calendar: Calendar): string { +export function formatCalendarEventsToVCalendar( + calendarEvents: CalendarEvent[], + _calendars: Pick[], +): 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}` : ''} diff --git a/routes/api/calendar/get-events.tsx b/routes/api/calendar/get-events.tsx new file mode 100644 index 0000000..84e7d67 --- /dev/null +++ b/routes/api/calendar/get-events.tsx @@ -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 = { + 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)); + }, +}; diff --git a/routes/dav/calendars/[calendarId].tsx b/routes/dav/calendars/[calendarId].tsx index f173231..37d6bd1 100644 --- a/routes/dav/calendars/[calendarId].tsx +++ b/routes/dav/calendars/[calendarId].tsx @@ -118,7 +118,7 @@ export const handler: Handler = 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 = async (request, context if (includeVCalendar) { parsedCalendarEvent['d:propstat'][0]['d:prop']['cal:calendar-data'] = escapeXml( - formatCalendarEventsToVCalendar([calendarEvent], calendar!), + formatCalendarEventsToVCalendar([calendarEvent], [calendar!]), ); } diff --git a/routes/dav/calendars/[calendarId]/[calendarEventId].ics.tsx b/routes/dav/calendars/[calendarId]/[calendarEventId].ics.tsx index a213369..e57016a 100644 --- a/routes/dav/calendars/[calendarId]/[calendarEventId].ics.tsx +++ b/routes/dav/calendars/[calendarId]/[calendarEventId].ics.tsx @@ -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 = 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 = async (request, context if (includeVCalendar) { parsedCalendarEvent['d:propstat'][0]['d:prop']['cal:calendar-data'] = escapeXml( - formatCalendarEventsToVCalendar([calendarEvent], calendar!), + formatCalendarEventsToVCalendar([calendarEvent], [calendar!]), ); }