View, Import, and Export reminders!

This commit is contained in:
Bruno Bernardino
2024-03-30 12:54:49 +00:00
parent 0ffe4e03f1
commit 1a6cb96965
5 changed files with 161 additions and 15 deletions

View File

@@ -57,7 +57,9 @@ export default function ViewEventModal(
{calendarEvent.extra.description {calendarEvent.extra.description
? ( ? (
<section class='py-5 my-0 border-b border-slate-500'> <section class='py-5 my-0 border-b border-slate-500'>
<p>{calendarEvent.extra.description}</p> <article class='overflow-auto max-w-full max-h-80 font-mono text-sm whitespace-pre-wrap'>
{calendarEvent.extra.description}
</article>
</section> </section>
) )
: null} : null}
@@ -97,9 +99,18 @@ export default function ViewEventModal(
</section> </section>
) )
: null} : null}
<section class='py-5 mb-2 border-b border-slate-500'> {Array.isArray(calendarEvent.extra.reminders) && calendarEvent.extra.reminders.length > 0
<p>TODO: reminders</p> ? (
<section class='py-5 mb-2 border-b border-slate-500 text-xs'>
{calendarEvent.extra.reminders.map((reminder) => (
<p class='my-1'>
{reminder.description || 'Reminder'} at {hourFormat.format(new Date(reminder.start_date))} via{' '}
{reminder.type}.
</p>
))}
</section> </section>
)
: null}
<footer class='flex justify-between'> <footer class='flex justify-between'>
<button <button
class='px-5 py-2 bg-slate-600 hover:bg-red-600 text-white cursor-pointer rounded-md' class='px-5 py-2 bg-slate-600 hover:bg-red-600 text-white cursor-pointer rounded-md'

View File

@@ -105,7 +105,6 @@ export function formFields(calendarEvent: CalendarEvent, calendars: Calendar[])
})), })),
required: true, required: true,
}, },
// TODO: More fields, attendees, recurrence
]; ];
return fields; return fields;

View File

@@ -189,6 +189,7 @@ export interface CalendarEventAttendee {
export interface CalendarEventReminder { export interface CalendarEventReminder {
uid?: string; uid?: string;
start_date: string; start_date: string;
type: 'email' | 'sound'; type: 'email' | 'sound' | 'display';
acknowledged_at?: string; acknowledged_at?: string;
description?: string;
} }

View File

@@ -1,4 +1,4 @@
import { Calendar, CalendarEvent, CalendarEventAttendee } from '../types.ts'; import { Calendar, CalendarEvent, CalendarEventAttendee, CalendarEventReminder } from '/lib/types.ts';
export const CALENDAR_COLOR_OPTIONS = [ export const CALENDAR_COLOR_OPTIONS = [
'bg-red-700', 'bg-red-700',
@@ -76,16 +76,40 @@ DTSTAMP:${new Date(calendarEvent.created_at).toISOString().substring(0, 19).repl
DTSTART:${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(':', '')} 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.replaceAll('\n', '\\n').replaceAll(',', '\\,')}
TRANSP:${getCalendarEventTransparency(calendarEvent, calendars).toUpperCase()} TRANSP:${getCalendarEventTransparency(calendarEvent, calendars).toUpperCase()}
${calendarEvent.extra.uid ? `UID:${calendarEvent.extra.uid}` : ''} ${calendarEvent.extra.uid ? `UID:${calendarEvent.extra.uid}` : ''}
SEQUENCE:0
CREATED:${new Date(calendarEvent.created_at).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '')}
LAST-MODIFIED:${
new Date(calendarEvent.updated_at).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '')
}
${ ${
calendarEvent.extra.attendees?.map((attendee) => calendarEvent.extra.attendees?.map((attendee) =>
`ATTENDEE;PARTSTAT=${getVCalendarAttendeeStatus(attendee.status)};CN=${ `ATTENDEE;PARTSTAT=${getVCalendarAttendeeStatus(attendee.status)};CN=${
attendee.name || '' attendee.name?.replaceAll('\n', '\\n').replaceAll(',', '\\,') || ''
}:mailto:${attendee.email}` }:mailto:${attendee.email}`
).join('\n') || '' ).join('\n') || ''
} }
${
calendarEvent.extra.reminders?.map((reminder) =>
`BEGIN:VALARM
ACTION:${reminder.type.toUpperCase()}
${reminder.description ? `DESCRIPTION:${reminder.description.replaceAll('\n', '\\n').replaceAll(',', '\\,')}` : ''}
TRIGGER;VALUE=DATE-TIME:${
new Date(reminder.start_date).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '')
}
${reminder.uid ? `UID:${reminder.uid}` : ''}
${
reminder.acknowledged_at
? `ACKNOWLEDGED:${
new Date(reminder.acknowledged_at).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '')
}`
: ''
}
END:VALARM`
).join('\n') || ''
}
END:VEVENT` END:VEVENT`
).join('\n'); ).join('\n');
@@ -115,6 +139,7 @@ export function parseVCalendarFromTextContents(text: string): Partial<CalendarEv
const partialCalendarEvents: Partial<CalendarEvent>[] = []; const partialCalendarEvents: Partial<CalendarEvent>[] = [];
let partialCalendarEvent: Partial<CalendarEvent> = {}; let partialCalendarEvent: Partial<CalendarEvent> = {};
let partialCalendarReminder: Partial<CalendarEventReminder> = {};
let vCalendarVersion: VCalendarVersion = '2.0'; let vCalendarVersion: VCalendarVersion = '2.0';
// Loop through every line // Loop through every line
@@ -137,6 +162,23 @@ export function parseVCalendarFromTextContents(text: string): Partial<CalendarEv
continue; continue;
} }
// Start new reminder
if (line.startsWith('BEGIN:VALARM')) {
partialCalendarReminder = {};
continue;
}
// Finish reminder
if (line.startsWith('END:VALARM')) {
partialCalendarEvent.extra = {
...(partialCalendarEvent.extra! || {}),
reminders: [...(partialCalendarEvent.extra?.reminders || []), partialCalendarReminder as CalendarEventReminder],
};
partialCalendarReminder = {};
continue;
}
// Select proper vCalendar version // Select proper vCalendar version
if (line.startsWith('VERSION:')) { if (line.startsWith('VERSION:')) {
if (line.startsWith('VERSION:1.0')) { if (line.startsWith('VERSION:1.0')) {
@@ -158,12 +200,18 @@ export function parseVCalendarFromTextContents(text: string): Partial<CalendarEv
} }
if (line.startsWith('UID:')) { if (line.startsWith('UID:')) {
const uid = line.replace('UID:', ''); const uid = line.replace('UID:', '').trim();
if (!uid) { if (!uid) {
continue; continue;
} }
if (Object.keys(partialCalendarReminder).length > 0) {
partialCalendarReminder.uid = uid;
continue;
}
partialCalendarEvent.extra = { partialCalendarEvent.extra = {
...(partialCalendarEvent.extra! || {}), ...(partialCalendarEvent.extra! || {}),
uid, uid,
@@ -172,10 +220,29 @@ export function parseVCalendarFromTextContents(text: string): Partial<CalendarEv
continue; continue;
} }
// TODO: Build this ( https://en.wikipedia.org/wiki/ICalendar#List_of_components,_properties,_and_parameters ) if (line.startsWith('DESCRIPTION:')) {
const description = line.replace('DESCRIPTION:', '').trim().replaceAll('\\n', '\n').replaceAll('\\,', ',');
if (!description) {
continue;
}
if (Object.keys(partialCalendarReminder).length > 0) {
partialCalendarReminder.description = description;
continue;
}
partialCalendarEvent.extra = {
...(partialCalendarEvent.extra! || {}),
description,
};
continue;
}
if (line.startsWith('SUMMARY:')) { if (line.startsWith('SUMMARY:')) {
const title = line.split('SUMMARY:')[1] || ''; const title = (line.split('SUMMARY:')[1] || '').trim().replaceAll('\\n', '\n').replaceAll('\\,', ',');
partialCalendarEvent.title = title; partialCalendarEvent.title = title;
@@ -254,7 +321,7 @@ export function parseVCalendarFromTextContents(text: string): Partial<CalendarEv
(attendeeStatusInfo.split(';')[0] || 'NEEDS-ACTION') as 'ACCEPTED' | 'REJECTED' | 'NEEDS-ACTION', (attendeeStatusInfo.split(';')[0] || 'NEEDS-ACTION') as 'ACCEPTED' | 'REJECTED' | 'NEEDS-ACTION',
); );
const attendeeNameInfo = line.split('CN=')[1] || ''; const attendeeNameInfo = line.split('CN=')[1] || '';
const attendeeName = (attendeeNameInfo.split(';')[0] || '').trim(); const attendeeName = (attendeeNameInfo.split(';')[0] || '').trim().replaceAll('\\n', '\n').replaceAll('\\,', ',');
if (!attendeeEmail) { if (!attendeeEmail) {
continue; continue;
@@ -274,6 +341,76 @@ export function parseVCalendarFromTextContents(text: string): Partial<CalendarEv
attendees: [...(partialCalendarEvent.extra?.attendees || []), attendee], attendees: [...(partialCalendarEvent.extra?.attendees || []), attendee],
}; };
} }
if (line.startsWith('ACTION:')) {
const reminderType =
(line.replace('ACTION:', '').trim().toLowerCase() || 'display') as CalendarEventReminder['type'];
partialCalendarReminder.type = reminderType;
continue;
}
if (line.startsWith('TRIGGER:') || line.startsWith('TRIGGER;')) {
const triggerInfo = line.split(':')[1] || '';
let triggerDate = new Date(partialCalendarEvent.start_date || new Date());
if (line.includes('DATE-TIME')) {
const [dateInfo, hourInfo] = triggerInfo.split('T');
const year = dateInfo.substring(0, 4);
const month = dateInfo.substring(4, 6);
const day = dateInfo.substring(6, 8);
const hours = hourInfo.substring(0, 2);
const minutes = hourInfo.substring(2, 4);
const seconds = hourInfo.substring(4, 6);
triggerDate = new Date(`${year}-${month}-${day}T${hours}:${minutes}:${seconds}.000Z`);
} else {
const triggerHoursMatch = triggerInfo.match(/(\d+(?:H))/);
const triggerMinutesMatch = triggerInfo.match(/(\d+(?:M))/);
const triggerSecondsMatch = triggerInfo.match(/(\d+(?:S))/);
const isNegative = triggerInfo.startsWith('-');
if (triggerHoursMatch && triggerHoursMatch.length > 0) {
const triggerHours = parseInt(triggerHoursMatch[0], 10);
if (isNegative) {
triggerDate.setHours(triggerDate.getHours() - triggerHours);
} else {
triggerDate.setHours(triggerHours);
}
}
if (triggerMinutesMatch && triggerMinutesMatch.length > 0) {
const triggerMinutes = parseInt(triggerMinutesMatch[0], 10);
if (isNegative) {
triggerDate.setMinutes(triggerDate.getMinutes() - triggerMinutes);
} else {
triggerDate.setMinutes(triggerMinutes);
}
}
if (triggerSecondsMatch && triggerSecondsMatch.length > 0) {
const triggerSeconds = parseInt(triggerSecondsMatch[0], 10);
if (isNegative) {
triggerDate.setSeconds(triggerDate.getSeconds() - triggerSeconds);
} else {
triggerDate.setSeconds(triggerSeconds);
}
}
}
partialCalendarReminder.start_date = triggerDate.toISOString();
continue;
}
// TODO: Build this ( https://en.wikipedia.org/wiki/ICalendar#List_of_components,_properties,_and_parameters )
} }
return partialCalendarEvents; return partialCalendarEvents;

View File

@@ -70,8 +70,6 @@ export const handler: Handlers<Data, FreshContextState> = {
calendarEvent.calendar_id = newCalendarId; calendarEvent.calendar_id = newCalendarId;
// TODO: More fields, attendees, recurrence
try { try {
if (!calendarEvent.title) { if (!calendarEvent.title) {
throw new Error(`Title is required.`); throw new Error(`Title is required.`);