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