View, Import, and Export attendees!

This commit is contained in:
Bruno Bernardino
2024-03-30 08:18:53 +00:00
parent abd1fdee62
commit 0ffe4e03f1
4 changed files with 86 additions and 7 deletions

View File

@@ -61,7 +61,7 @@ export default function AddEventModal(
<section <section
class={`fixed ${ class={`fixed ${
newEvent.value ? 'block' : 'hidden' newEvent.value ? 'block' : 'hidden'
} z-50 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 bg-slate-600 text-white rounded-md px-8 py-6 drop-shadow-lg`} } z-50 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 bg-slate-600 text-white rounded-md px-8 py-6 drop-shadow-lg overflow-y-scroll max-h-[80%]`}
> >
<h1 class='text-2xl font-semibold my-5'>New Event</h1> <h1 class='text-2xl font-semibold my-5'>New Event</h1>
<section class='py-5 my-2 border-y border-slate-500'> <section class='py-5 my-2 border-y border-slate-500'>

View File

@@ -33,7 +33,7 @@ export default function ImportEventsModal(
<section <section
class={`fixed ${ class={`fixed ${
newCalendarId.value ? 'block' : 'hidden' newCalendarId.value ? 'block' : 'hidden'
} z-50 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 bg-slate-600 text-white rounded-md px-8 py-6 drop-shadow-lg`} } z-50 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 bg-slate-600 text-white rounded-md px-8 py-6 drop-shadow-lg overflow-y-scroll max-h-[80%]`}
> >
<h1 class='text-2xl font-semibold my-5'>Import Events</h1> <h1 class='text-2xl font-semibold my-5'>Import Events</h1>
<section class='py-5 my-2 border-y border-slate-500'> <section class='py-5 my-2 border-y border-slate-500'>

View File

@@ -28,7 +28,7 @@ export default function ViewEventModal(
<section <section
class={`fixed ${ class={`fixed ${
isOpen ? 'block' : 'hidden' isOpen ? 'block' : 'hidden'
} z-50 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 bg-slate-600 text-white rounded-md px-8 py-6 drop-shadow-lg`} } z-50 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 bg-slate-600 text-white rounded-md px-8 py-6 drop-shadow-lg overflow-y-scroll max-h-[80%]`}
> >
<h1 class='text-2xl font-semibold my-5'>{calendarEvent.title}</h1> <h1 class='text-2xl font-semibold my-5'>{calendarEvent.title}</h1>
<header class='py-5 border-t border-b border-slate-500 font-semibold flex justify-between items-center'> <header class='py-5 border-t border-b border-slate-500 font-semibold flex justify-between items-center'>
@@ -83,6 +83,20 @@ export default function ViewEventModal(
</section> </section>
) )
: null} : null}
{Array.isArray(calendarEvent.extra.attendees) && calendarEvent.extra.attendees.length > 0
? (
<section class='py-5 my-0 border-b border-slate-500'>
{calendarEvent.extra.attendees.map((attendee) => (
<p class='my-1'>
<a href={`mailto:${attendee.email}`} target='_blank' rel='noopener noreferrer'>
{attendee.name || attendee.email}
</a>{' '}
- {attendee.status}
</p>
))}
</section>
)
: null}
<section class='py-5 mb-2 border-b border-slate-500'> <section class='py-5 mb-2 border-b border-slate-500'>
<p>TODO: reminders</p> <p>TODO: reminders</p>
</section> </section>

View File

@@ -1,4 +1,4 @@
import { Calendar, CalendarEvent } from '../types.ts'; import { Calendar, CalendarEvent, CalendarEventAttendee } from '../types.ts';
export const CALENDAR_COLOR_OPTIONS = [ export const CALENDAR_COLOR_OPTIONS = [
'bg-red-700', 'bg-red-700',
@@ -47,6 +47,24 @@ export const CALENDAR_BORDER_COLOR_OPTIONS = [
'border-rose-700', 'border-rose-700',
] as const; ] as const;
function getVCalendarAttendeeStatus(status: CalendarEventAttendee['status']) {
if (status === 'accepted' || status === 'rejected') {
return status.toUpperCase();
}
return `NEEDS-ACTION`;
}
function getAttendeeStatusFromVCalendar(
status: 'NEEDS-ACTION' | 'ACCEPTED' | 'REJECTED',
): CalendarEventAttendee['status'] {
if (status === 'ACCEPTED' || status === 'REJECTED') {
return status.toLowerCase() as CalendarEventAttendee['status'];
}
return 'invited';
}
// TODO: Build this // TODO: Build this
export function formatCalendarEventsToVCalendar( export function formatCalendarEventsToVCalendar(
calendarEvents: CalendarEvent[], calendarEvents: CalendarEvent[],
@@ -57,10 +75,17 @@ export function formatCalendarEventsToVCalendar(
DTSTAMP:${new Date(calendarEvent.created_at).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '')} DTSTAMP:${new Date(calendarEvent.created_at).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '')}
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}
TRANSP:${getCalendarEventTransparency(calendarEvent, calendars).toUpperCase()} TRANSP:${getCalendarEventTransparency(calendarEvent, calendars).toUpperCase()}
${calendarEvent.extra.uid ? `UID:${calendarEvent.extra.uid}` : ''} ${calendarEvent.extra.uid ? `UID:${calendarEvent.extra.uid}` : ''}
${
calendarEvent.extra.attendees?.map((attendee) =>
`ATTENDEE;PARTSTAT=${getVCalendarAttendeeStatus(attendee.status)};CN=${
attendee.name || ''
}:mailto:${attendee.email}`
).join('\n') || ''
}
END:VEVENT` END:VEVENT`
).join('\n'); ).join('\n');
@@ -74,7 +99,18 @@ END:VEVENT`
type VCalendarVersion = '1.0' | '2.0'; type VCalendarVersion = '1.0' | '2.0';
export function parseVCalendarFromTextContents(text: string): Partial<CalendarEvent>[] { export function parseVCalendarFromTextContents(text: string): Partial<CalendarEvent>[] {
const lines = text.split('\n').map((line) => line.trim()).filter(Boolean); // Lines that start with a space should be moved to the line above them, as it's the same field/value to parse
const lines = text.split('\n').reduce((previousLines, currentLine) => {
if (currentLine.startsWith(' ')) {
previousLines[previousLines.length - 1] = `${previousLines[previousLines.length - 1]}${
currentLine.substring(1).replaceAll('\r', '')
}`;
} else {
previousLines.push(currentLine.replaceAll('\r', ''));
}
return previousLines;
}, [] as string[]).map((line) => line.trim()).filter(Boolean);
const partialCalendarEvents: Partial<CalendarEvent>[] = []; const partialCalendarEvents: Partial<CalendarEvent>[] = [];
@@ -95,7 +131,7 @@ export function parseVCalendarFromTextContents(text: string): Partial<CalendarEv
continue; continue;
} }
// Finish contact // Finish event
if (line.startsWith('END:VEVENT')) { if (line.startsWith('END:VEVENT')) {
partialCalendarEvents.push(partialCalendarEvent); partialCalendarEvents.push(partialCalendarEvent);
continue; continue;
@@ -209,6 +245,35 @@ export function parseVCalendarFromTextContents(text: string): Partial<CalendarEv
continue; continue;
} }
if (line.startsWith('ATTENDEE;')) {
const attendeeInfo = line.split(':');
const attendeeEmail = attendeeInfo.slice(-1)[0] || '';
const attendeeStatusInfo = line.split('PARTSTAT=')[1] || '';
const attendeeStatus = getAttendeeStatusFromVCalendar(
(attendeeStatusInfo.split(';')[0] || 'NEEDS-ACTION') as 'ACCEPTED' | 'REJECTED' | 'NEEDS-ACTION',
);
const attendeeNameInfo = line.split('CN=')[1] || '';
const attendeeName = (attendeeNameInfo.split(';')[0] || '').trim();
if (!attendeeEmail) {
continue;
}
const attendee: CalendarEventAttendee = {
email: attendeeEmail,
status: attendeeStatus,
};
if (attendeeName) {
attendee.name = attendeeName;
}
partialCalendarEvent.extra = {
...(partialCalendarEvent.extra! || {}),
attendees: [...(partialCalendarEvent.extra?.attendees || []), attendee],
};
}
} }
return partialCalendarEvents; return partialCalendarEvents;