View, Import, and Export attendees!
This commit is contained in:
@@ -61,7 +61,7 @@ export default function AddEventModal(
|
||||
<section
|
||||
class={`fixed ${
|
||||
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>
|
||||
<section class='py-5 my-2 border-y border-slate-500'>
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function ImportEventsModal(
|
||||
<section
|
||||
class={`fixed ${
|
||||
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>
|
||||
<section class='py-5 my-2 border-y border-slate-500'>
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function ViewEventModal(
|
||||
<section
|
||||
class={`fixed ${
|
||||
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>
|
||||
<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>
|
||||
)
|
||||
: 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'>
|
||||
<p>TODO: reminders</p>
|
||||
</section>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Calendar, CalendarEvent } from '../types.ts';
|
||||
import { Calendar, CalendarEvent, CalendarEventAttendee } from '../types.ts';
|
||||
|
||||
export const CALENDAR_COLOR_OPTIONS = [
|
||||
'bg-red-700',
|
||||
@@ -47,6 +47,24 @@ export const CALENDAR_BORDER_COLOR_OPTIONS = [
|
||||
'border-rose-700',
|
||||
] 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
|
||||
export function formatCalendarEventsToVCalendar(
|
||||
calendarEvents: CalendarEvent[],
|
||||
@@ -57,10 +75,17 @@ export function formatCalendarEventsToVCalendar(
|
||||
DTSTAMP:${new Date(calendarEvent.created_at).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}
|
||||
ORGANIZER;CN=:mailto:${calendarEvent.extra.organizer_email}
|
||||
SUMMARY:${calendarEvent.title}
|
||||
TRANSP:${getCalendarEventTransparency(calendarEvent, calendars).toUpperCase()}
|
||||
${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`
|
||||
).join('\n');
|
||||
|
||||
@@ -74,7 +99,18 @@ END:VEVENT`
|
||||
type VCalendarVersion = '1.0' | '2.0';
|
||||
|
||||
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>[] = [];
|
||||
|
||||
@@ -95,7 +131,7 @@ export function parseVCalendarFromTextContents(text: string): Partial<CalendarEv
|
||||
continue;
|
||||
}
|
||||
|
||||
// Finish contact
|
||||
// Finish event
|
||||
if (line.startsWith('END:VEVENT')) {
|
||||
partialCalendarEvents.push(partialCalendarEvent);
|
||||
continue;
|
||||
@@ -209,6 +245,35 @@ export function parseVCalendarFromTextContents(text: string): Partial<CalendarEv
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user