View, Import, and Export recurrence!

This commit is contained in:
Bruno Bernardino
2024-03-30 18:08:19 +00:00
parent 1a6cb96965
commit 87b94e9eec
3 changed files with 183 additions and 6 deletions

View File

@@ -1,4 +1,5 @@
import { Calendar, CalendarEvent } from '/lib/types.ts';
import { convertRRuleToWords } from '/lib/utils/calendar.ts';
interface ViewEventModalProps {
isOpen: boolean;
@@ -51,9 +52,13 @@ export default function ViewEventModal(
title={calendar.color}
/>
</section>
<section class='py-5 my-0 border-b border-slate-500'>
<p>TODO: recurrence</p>
</section>
{calendarEvent.extra.recurring_rrule
? (
<section class='py-5 my-0 border-b border-slate-500'>
<p>Repeats {convertRRuleToWords(calendarEvent.extra.recurring_rrule).toLowerCase()}.</p>
</section>
)
: null}
{calendarEvent.extra.description
? (
<section class='py-5 my-0 border-b border-slate-500'>
@@ -101,7 +106,7 @@ export default function ViewEventModal(
: null}
{Array.isArray(calendarEvent.extra.reminders) && calendarEvent.extra.reminders.length > 0
? (
<section class='py-5 mb-2 border-b border-slate-500 text-xs'>
<section class='py-5 my-0 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{' '}
@@ -111,7 +116,7 @@ export default function ViewEventModal(
</section>
)
: null}
<footer class='flex justify-between'>
<footer class='flex justify-between mt-2'>
<button
class='px-5 py-2 bg-slate-600 hover:bg-red-600 text-white cursor-pointer rounded-md'
onClick={() => onClickDelete(calendarEvent.id)}

View File

@@ -43,6 +43,8 @@ export async function getCalendarEvents(
],
);
// TODO: Fetch initial recurring events and calculate any necessary to create/show for the date range
return calendarEvents;
}
}

View File

@@ -79,7 +79,8 @@ ORGANIZER;CN=:mailto:${calendarEvent.extra.organizer_email}
SUMMARY:${calendarEvent.title.replaceAll('\n', '\\n').replaceAll(',', '\\,')}
TRANSP:${getCalendarEventTransparency(calendarEvent, calendars).toUpperCase()}
${calendarEvent.extra.uid ? `UID:${calendarEvent.extra.uid}` : ''}
SEQUENCE:0
${calendarEvent.extra.recurring_rrule ? `RRULE:${calendarEvent.extra.recurring_rrule}` : ''}
SEQUENCE:${calendarEvent.extra.recurring_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(':', '')
@@ -410,6 +411,37 @@ export function parseVCalendarFromTextContents(text: string): Partial<CalendarEv
continue;
}
if (line.startsWith('RRULE:')) {
const rRule = line.replace('RRULE:', '').trim();
if (!rRule) {
continue;
}
partialCalendarEvent.extra = {
...(partialCalendarEvent.extra! || {}),
recurring_rrule: rRule,
recurring_sequence: partialCalendarEvent.extra?.recurring_sequence || 0,
};
continue;
}
if (line.startsWith('SEQUENCE:')) {
const sequence = line.replace('SEQUENCE:', '').trim();
if (!sequence || sequence === '0') {
continue;
}
partialCalendarEvent.extra = {
...(partialCalendarEvent.extra! || {}),
recurring_sequence: parseInt(sequence, 10),
};
continue;
}
// TODO: Build this ( https://en.wikipedia.org/wiki/ICalendar#List_of_components,_properties,_and_parameters )
}
@@ -517,3 +549,141 @@ export function getCalendarEventColor(
return transparency === 'opaque' ? opaqueColor : transparentColor;
}
type RRuleFrequency = 'DAILY' | 'WEEKLY' | 'MONTHLY';
type RRuleWeekDay = 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU';
type RRuleType = 'FREQ' | 'BYDAY' | 'BYMONTHDAY' | 'BYHOUR' | 'BYMINUTE' | 'COUNT' | 'INTERVAL';
const rRuleToFrequencyOrWeekDay = new Map<RRuleFrequency | RRuleWeekDay, string>([
['DAILY', 'day'],
['WEEKLY', 'week'],
['MONTHLY', 'month'],
['MO', 'Monday'],
['TU', 'Tuesday'],
['WE', 'Wednesday'],
['TH', 'Thursday'],
['FR', 'Friday'],
['SA', 'Saturday'],
['SU', 'Sunday'],
]);
// check if multiple days and format either way
function convertRRuleDaysToWords(day: string | RRuleFrequency | RRuleWeekDay): string {
if (day.includes(',')) {
const days = day.split(',') as (typeof day)[];
return days.map((individualDay) => rRuleToFrequencyOrWeekDay.get(individualDay as RRuleFrequency | RRuleWeekDay))
.join(',');
}
return rRuleToFrequencyOrWeekDay.get(day as RRuleFrequency | RRuleWeekDay)!;
}
// convert to ordinal number
function getOrdinalSuffix(number: number) {
const text = ['th', 'st', 'nd', 'rd'] as const;
const value = number % 100;
return `${number}${(text[(value - 20) % 10] || text[value] || text[0])}`;
}
export function convertRRuleToWords(rRule: string): string {
const rulePart = rRule.replace('RRULE:', '');
const rulePieces = rulePart.split(';');
const parsedRRule: Partial<Record<RRuleType, string>> = {};
rulePieces.forEach(function (rulePiece) {
const keyAndValue = rulePiece.split('=') as [RRuleType, string];
const [key, value] = keyAndValue;
parsedRRule[key] = value;
});
const frequency = parsedRRule.FREQ;
const byDay = parsedRRule.BYDAY;
const byMonthDay = parsedRRule.BYMONTHDAY;
const byHour = parsedRRule.BYHOUR;
const byMinute = parsedRRule.BYMINUTE;
const count = parsedRRule.COUNT;
const interval = parsedRRule.INTERVAL;
// TODO: Remove this
console.log('==== File.method');
console.log(JSON.stringify({}, null, 2));
const words: string[] = [];
if (frequency === 'DAILY') {
if (byHour) {
if (byMinute) {
words.push(`Every day at ${byHour}:${byMinute}`);
} else {
words.push(`Every day at ${byHour}:00`);
}
} else {
words.push(`Every day`);
}
if (count) {
if (count === '1') {
words.push(`for 1 time`);
} else {
words.push(`for ${count} times`);
}
}
return words.join(' ');
}
if (frequency === 'WEEKLY') {
if (byDay) {
if (interval && parseInt(interval) > 1) {
words.push(
`Every ${interval} ${rRuleToFrequencyOrWeekDay.get(frequency)}s on ${convertRRuleDaysToWords(byDay)}`,
);
} else {
words.push(`Every ${rRuleToFrequencyOrWeekDay.get(frequency)} on ${convertRRuleDaysToWords(byDay)}`);
}
}
if (byMonthDay) {
words.push(`the ${getOrdinalSuffix(parseInt(byMonthDay, 10))}`);
}
if (count) {
if (count === '1') {
words.push(`for 1 time`);
} else {
words.push(`for ${count} times`);
}
}
return words.join(' ');
}
// monthly
if (frequency === 'MONTHLY' && byMonthDay) {
if (interval && parseInt(interval) > 1) {
words.push(
`Every ${interval} ${rRuleToFrequencyOrWeekDay.get(frequency)}s on the ${
getOrdinalSuffix(parseInt(byMonthDay, 10))
}`,
);
} else {
words.push(
`Every ${rRuleToFrequencyOrWeekDay.get(frequency)} on the ${getOrdinalSuffix(parseInt(byMonthDay, 10))}`,
);
}
if (count) {
if (count === '1') {
words.push(` for 1 time`);
} else {
words.push(` for ${count} times`);
}
}
return words.join(' ');
}
return words.join(' ');
}