Support exporting calendar events
Also update Deno and libraries
This commit is contained in:
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM denoland/deno:alpine-1.41.0
|
||||
FROM denoland/deno:alpine-1.41.3
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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/"
|
||||
}
|
||||
}
|
||||
|
||||
11
lib/utils.ts
11
lib/utils.ts
@@ -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}` : ''}
|
||||
|
||||
38
routes/api/calendar/get-events.tsx
Normal file
38
routes/api/calendar/get-events.tsx
Normal 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));
|
||||
},
|
||||
};
|
||||
@@ -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!]),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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!]),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user