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: actions/checkout@v4
|
||||||
- uses: denoland/setup-deno@v1
|
- uses: denoland/setup-deno@v1
|
||||||
with:
|
with:
|
||||||
deno-version: v1.41.0
|
deno-version: v1.41.3
|
||||||
- run: |
|
- run: |
|
||||||
make test
|
make test
|
||||||
make build
|
make build
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM denoland/deno:alpine-1.41.0
|
FROM denoland/deno:alpine-1.41.3
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useSignal } from '@preact/signals';
|
import { useSignal } from '@preact/signals';
|
||||||
|
|
||||||
import { Calendar, CalendarEvent } from '/lib/types.ts';
|
import { Calendar, CalendarEvent } from '/lib/types.ts';
|
||||||
import { baseUrl, capitalizeWord } from '/lib/utils.ts';
|
import { baseUrl, capitalizeWord, formatCalendarEventsToVCalendar } from '/lib/utils.ts';
|
||||||
// import { RequestBody as GetRequestBody, ResponseBody as GetResponseBody } from '/routes/api/calendar/get.tsx';
|
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 AddRequestBody, ResponseBody as AddResponseBody } from '/routes/api/calendar/add-event.tsx';
|
||||||
import {
|
import {
|
||||||
RequestBody as DeleteRequestBody,
|
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 dateFormat = new Intl.DateTimeFormat('en-GB', { year: 'numeric', month: 'long' });
|
||||||
const today = new Date().toISOString().substring(0, 10);
|
const today = new Date().toISOString().substring(0, 10);
|
||||||
|
|
||||||
|
const visibleCalendars = calendars.value.filter((calendar) => calendar.is_visible);
|
||||||
|
|
||||||
function onClickAddEvent(startDate = new Date(), isAllDay = false) {
|
function onClickAddEvent(startDate = new Date(), isAllDay = false) {
|
||||||
if (newEventModal.value.isOpen) {
|
if (newEventModal.value.isOpen) {
|
||||||
newEventModal.value = {
|
newEventModal.value = {
|
||||||
@@ -75,7 +77,7 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents,
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const requestBody: AddRequestBody = {
|
const requestBody: AddRequestBody = {
|
||||||
calendarIds: calendars.value.map((calendar) => calendar.id),
|
calendarIds: visibleCalendars.map((calendar) => calendar.id),
|
||||||
calendarView: view,
|
calendarView: view,
|
||||||
calendarStartDate: startDate,
|
calendarStartDate: startDate,
|
||||||
calendarId: newEvent.calendar_id,
|
calendarId: newEvent.calendar_id,
|
||||||
@@ -147,7 +149,7 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents,
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const requestBody: DeleteRequestBody = {
|
const requestBody: DeleteRequestBody = {
|
||||||
calendarIds: calendars.value.map((calendar) => calendar.id),
|
calendarIds: visibleCalendars.map((calendar) => calendar.id),
|
||||||
calendarView: view,
|
calendarView: view,
|
||||||
calendarStartDate: startDate,
|
calendarStartDate: startDate,
|
||||||
calendarEventId,
|
calendarEventId,
|
||||||
@@ -299,7 +301,7 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents,
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClickExportICS() {
|
async function onClickExportICS() {
|
||||||
isImportExportOptionsDropdownOpen.value = false;
|
isImportExportOptionsDropdownOpen.value = false;
|
||||||
|
|
||||||
if (isExporting.value) {
|
if (isExporting.value) {
|
||||||
@@ -308,49 +310,47 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents,
|
|||||||
|
|
||||||
isExporting.value = true;
|
isExporting.value = true;
|
||||||
|
|
||||||
// const fileName = ['calendars-', new Date().toISOString().substring(0, 19).replace(/:/g, '-'), '.ics']
|
const fileName = ['calendar-', new Date().toISOString().substring(0, 19).replace(/:/g, '-'), '.ics']
|
||||||
// .join('');
|
.join('');
|
||||||
|
|
||||||
// try {
|
try {
|
||||||
// const requestBody: GetRequestBody = {};
|
const requestBody: GetRequestBody = { calendarIds: visibleCalendars.map((calendar) => calendar.id) };
|
||||||
// const response = await fetch(`/api/calendar/get`, {
|
const response = await fetch(`/api/calendar/get-events`, {
|
||||||
// method: 'POST',
|
method: 'POST',
|
||||||
// body: JSON.stringify(requestBody),
|
body: JSON.stringify(requestBody),
|
||||||
// });
|
});
|
||||||
// const result = await response.json() as GetResponseBody;
|
const result = await response.json() as GetResponseBody;
|
||||||
|
|
||||||
// if (!result.success) {
|
if (!result.success) {
|
||||||
// throw new Error('Failed to get contact!');
|
throw new Error('Failed to get contact!');
|
||||||
// }
|
}
|
||||||
|
|
||||||
// const exportContents = formatContactToVCard([...result.contacts]);
|
const exportContents = formatCalendarEventsToVCalendar([...result.calendarEvents], calendars.value);
|
||||||
|
|
||||||
// // Add content-type
|
// Add content-type
|
||||||
// const vCardContent = ['data:text/vcard; charset=utf-8,', encodeURIComponent(exportContents)].join('');
|
const vCardContent = ['data:text/calendar; charset=utf-8,', encodeURIComponent(exportContents)].join('');
|
||||||
|
|
||||||
// // Download the file
|
// Download the file
|
||||||
// const data = vCardContent;
|
const data = vCardContent;
|
||||||
// const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
// link.setAttribute('href', data);
|
link.setAttribute('href', data);
|
||||||
// link.setAttribute('download', fileName);
|
link.setAttribute('download', fileName);
|
||||||
// link.click();
|
link.click();
|
||||||
// link.remove();
|
link.remove();
|
||||||
// } catch (error) {
|
} catch (error) {
|
||||||
// console.error(error);
|
console.error(error);
|
||||||
// }
|
}
|
||||||
|
|
||||||
isExporting.value = false;
|
isExporting.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibleCalendars = calendars.value.filter((calendar) => calendar.is_visible);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section class='flex flex-row items-center justify-between mb-4'>
|
<section class='flex flex-row items-center justify-between mb-4'>
|
||||||
<section class='relative inline-block text-left mr-2'>
|
<section class='relative inline-block text-left mr-2'>
|
||||||
<section class='flex flex-row items-center justify-start'>
|
<section class='flex flex-row items-center justify-start'>
|
||||||
<a href='/calendars' class='mr-4 whitespace-nowrap'>Manage calendars</a>
|
<a href='/calendars' class='mr-4 whitespace-nowrap'>Manage calendars</a>
|
||||||
<SearchEvents calendars={calendars.value} onClickOpenEvent={onClickOpenEvent} />
|
<SearchEvents calendars={visibleCalendars} onClickOpenEvent={onClickOpenEvent} />
|
||||||
</section>
|
</section>
|
||||||
</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_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_event from './routes/api/calendar/delete-event.tsx';
|
||||||
import * as $api_calendar_delete from './routes/api/calendar/delete.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_search_events from './routes/api/calendar/search-events.tsx';
|
||||||
import * as $api_calendar_update from './routes/api/calendar/update.tsx';
|
import * as $api_calendar_update from './routes/api/calendar/update.tsx';
|
||||||
import * as $api_contacts_add from './routes/api/contacts/add.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/add.tsx': $api_calendar_add,
|
||||||
'./routes/api/calendar/delete-event.tsx': $api_calendar_delete_event,
|
'./routes/api/calendar/delete-event.tsx': $api_calendar_delete_event,
|
||||||
'./routes/api/calendar/delete.tsx': $api_calendar_delete,
|
'./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/search-events.tsx': $api_calendar_search_events,
|
||||||
'./routes/api/calendar/update.tsx': $api_calendar_update,
|
'./routes/api/calendar/update.tsx': $api_calendar_update,
|
||||||
'./routes/api/contacts/add.tsx': $api_contacts_add,
|
'./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/": "npm:/tailwindcss@3.4.1/",
|
"tailwindcss/": "npm:/tailwindcss@3.4.1/",
|
||||||
"tailwindcss/plugin": "npm:/tailwindcss@3.4.1/plugin.js",
|
"tailwindcss/plugin": "npm:/tailwindcss@3.4.1/plugin.js",
|
||||||
"std/": "https://deno.land/std@0.217.0/",
|
"std/": "https://deno.land/std@0.220.1/",
|
||||||
"$std/": "https://deno.land/std@0.217.0/"
|
"$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
|
// 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) =>
|
const vCalendarText = calendarEvents.map((calendarEvent) =>
|
||||||
`BEGIN:VEVENT
|
`BEGIN:VEVENT
|
||||||
DTSTAMP:${calendarEvent.start_date.toISOString().substring(0, 19).replaceAll('T', '').replaceAll(':', '')}
|
DTSTAMP:${new Date(calendarEvent.start_date).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '')}
|
||||||
DTSTART:${calendarEvent.start_date.toISOString().substring(0, 19).replaceAll('T', '').replaceAll(':', '')}
|
DTSTART:${new Date(calendarEvent.start_date).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '')}
|
||||||
DTEND:${calendarEvent.end_date.toISOString().substring(0, 19).replaceAll('T', '').replaceAll(':', '')}
|
DTEND:${new Date(calendarEvent.end_date).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '')}
|
||||||
ORGANIZER;CN=:MAILTO:${calendarEvent.extra.organizer_email}
|
ORGANIZER;CN=:MAILTO:${calendarEvent.extra.organizer_email}
|
||||||
SUMMARY:${calendarEvent.title}
|
SUMMARY:${calendarEvent.title}
|
||||||
${calendarEvent.extra.uid ? `UID:${calendarEvent.extra.uid}` : ''}
|
${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]);
|
const calendarEvents = await getCalendarEvents(context.state.user.id, [calendar.id]);
|
||||||
|
|
||||||
if (request.method === 'GET') {
|
if (request.method === 'GET') {
|
||||||
const response = new Response(formatCalendarEventsToVCalendar(calendarEvents, calendar), {
|
const response = new Response(formatCalendarEventsToVCalendar(calendarEvents, [calendar]), {
|
||||||
status: 200,
|
status: 200,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ export const handler: Handler<Data, FreshContextState> = async (request, context
|
|||||||
|
|
||||||
if (includeVCalendar) {
|
if (includeVCalendar) {
|
||||||
parsedCalendarEvent['d:propstat'][0]['d:prop']['cal:calendar-data'] = escapeXml(
|
parsedCalendarEvent['d:propstat'][0]['d:prop']['cal:calendar-data'] = escapeXml(
|
||||||
formatCalendarEventsToVCalendar([calendarEvent], calendar!),
|
formatCalendarEventsToVCalendar([calendarEvent], [calendar!]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
formatCalendarEventsToVCalendar,
|
formatCalendarEventsToVCalendar,
|
||||||
parseVCalendarFromTextContents,
|
parseVCalendarFromTextContents,
|
||||||
} from '/lib/utils.ts';
|
} 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';
|
import { createSessionCookie } from '/lib/auth.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
@@ -157,7 +157,15 @@ export const handler: Handler<Data, FreshContextState> = async (request, context
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
if (request.method === 'GET') {
|
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,
|
status: 200,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -195,7 +203,7 @@ export const handler: Handler<Data, FreshContextState> = async (request, context
|
|||||||
|
|
||||||
if (includeVCalendar) {
|
if (includeVCalendar) {
|
||||||
parsedCalendarEvent['d:propstat'][0]['d:prop']['cal:calendar-data'] = escapeXml(
|
parsedCalendarEvent['d:propstat'][0]['d:prop']['cal:calendar-data'] = escapeXml(
|
||||||
formatCalendarEventsToVCalendar([calendarEvent], calendar!),
|
formatCalendarEventsToVCalendar([calendarEvent], [calendar!]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user