View, Import, and Export attendees!
This commit is contained in:
@@ -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'>
|
||||||
|
|||||||
@@ -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'>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user