style: format recurrence helpers
This commit is contained in:
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
format,
|
||||
isSameDay,
|
||||
isToday,
|
||||
isTomorrow,
|
||||
parseISO,
|
||||
} from "date-fns";
|
||||
import { format, isSameDay, isToday, isTomorrow, parseISO } from "date-fns";
|
||||
import type { CalendarEvent } from "@/lib/types";
|
||||
|
||||
const getFriendlyDayLabel = (value: Date): string => {
|
||||
@@ -13,7 +7,10 @@ const getFriendlyDayLabel = (value: Date): string => {
|
||||
return format(value, "MMM d, yyyy");
|
||||
};
|
||||
|
||||
export const formatEventStartLabel = (start: string, allDay?: boolean): string => {
|
||||
export const formatEventStartLabel = (
|
||||
start: string,
|
||||
allDay?: boolean,
|
||||
): string => {
|
||||
const parsed = parseISO(start);
|
||||
const dayLabel = getFriendlyDayLabel(parsed);
|
||||
|
||||
@@ -21,12 +18,16 @@ export const formatEventStartLabel = (start: string, allDay?: boolean): string =
|
||||
return `${dayLabel} · ${format(parsed, "HH:mm")}`;
|
||||
};
|
||||
|
||||
export const formatEventRangeLabel = (event: Pick<CalendarEvent, "start" | "end" | "allDay">): string => {
|
||||
export const formatEventRangeLabel = (
|
||||
event: Pick<CalendarEvent, "start" | "end" | "allDay">,
|
||||
): string => {
|
||||
const startDate = parseISO(event.start);
|
||||
const startLabel = getFriendlyDayLabel(startDate);
|
||||
|
||||
if (event.allDay || !event.end) {
|
||||
return event.allDay ? startLabel : `${startLabel} · ${format(startDate, "HH:mm")}`;
|
||||
return event.allDay
|
||||
? startLabel
|
||||
: `${startLabel} · ${format(startDate, "HH:mm")}`;
|
||||
}
|
||||
|
||||
const endDate = parseISO(event.end);
|
||||
|
||||
@@ -39,7 +39,10 @@ const eventFormSchema = z
|
||||
if (value.end) {
|
||||
const startDate = parseISO(value.start);
|
||||
const endDate = parseISO(value.end);
|
||||
if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) {
|
||||
if (
|
||||
Number.isNaN(startDate.getTime()) ||
|
||||
Number.isNaN(endDate.getTime())
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["end"],
|
||||
|
||||
@@ -2,7 +2,11 @@ import { format, parseISO } from "date-fns";
|
||||
import { RRule, rrulestr } from "rrule";
|
||||
import type { Frequency, Weekday } from "@/lib/rfc5545-types";
|
||||
|
||||
export type SupportedRecurrenceFrequency = "NONE" | "DAILY" | "WEEKLY" | "MONTHLY";
|
||||
export type SupportedRecurrenceFrequency =
|
||||
| "NONE"
|
||||
| "DAILY"
|
||||
| "WEEKLY"
|
||||
| "MONTHLY";
|
||||
|
||||
export interface RecurrenceFormValue {
|
||||
freq: SupportedRecurrenceFrequency;
|
||||
@@ -23,7 +27,10 @@ const EMPTY_RECURRENCE: RecurrenceFormValue = {
|
||||
byDay: [],
|
||||
};
|
||||
|
||||
const RULE_FREQUENCIES: Record<Exclude<SupportedRecurrenceFrequency, "NONE">, number> = {
|
||||
const RULE_FREQUENCIES: Record<
|
||||
Exclude<SupportedRecurrenceFrequency, "NONE">,
|
||||
number
|
||||
> = {
|
||||
DAILY: RRule.DAILY,
|
||||
WEEKLY: RRule.WEEKLY,
|
||||
MONTHLY: RRule.MONTHLY,
|
||||
@@ -58,7 +65,10 @@ const sortWeekdays = (days: string[] | undefined): Weekday[] => {
|
||||
if (!days?.length) return [];
|
||||
return [...days]
|
||||
.filter((day): day is Weekday => WEEKDAY_ORDER.includes(day as Weekday))
|
||||
.sort((left, right) => WEEKDAY_ORDER.indexOf(left) - WEEKDAY_ORDER.indexOf(right));
|
||||
.sort(
|
||||
(left, right) =>
|
||||
WEEKDAY_ORDER.indexOf(left) - WEEKDAY_ORDER.indexOf(right),
|
||||
);
|
||||
};
|
||||
|
||||
const toUntilDate = (until: string): Date => {
|
||||
@@ -105,7 +115,10 @@ export const validateRecurrence = (
|
||||
errors.rule = "Interval must be at least 1.";
|
||||
}
|
||||
|
||||
if (value.count !== undefined && (!Number.isInteger(value.count) || value.count < 1)) {
|
||||
if (
|
||||
value.count !== undefined &&
|
||||
(!Number.isInteger(value.count) || value.count < 1)
|
||||
) {
|
||||
errors.count = "Count must be a whole number greater than 0.";
|
||||
}
|
||||
|
||||
@@ -164,14 +177,22 @@ export const serializeRecurrenceRule = (
|
||||
|
||||
const validation = validateRecurrence(value);
|
||||
if (!validation.isValid) {
|
||||
throw new Error(validation.errors.rule || validation.errors.count || validation.errors.until || "Invalid recurrence.");
|
||||
throw new Error(
|
||||
validation.errors.rule ||
|
||||
validation.errors.count ||
|
||||
validation.errors.until ||
|
||||
"Invalid recurrence.",
|
||||
);
|
||||
}
|
||||
|
||||
const options = toRRuleOptions(value, dtstart);
|
||||
if (!options) return undefined;
|
||||
|
||||
const rule = new RRule(options);
|
||||
const ruleLine = rule.toString().split("\n").find((line) => line.startsWith("RRULE:"));
|
||||
const ruleLine = rule
|
||||
.toString()
|
||||
.split("\n")
|
||||
.find((line) => line.startsWith("RRULE:"));
|
||||
if (!ruleLine) return undefined;
|
||||
|
||||
const entries = ruleLine.replace(/^RRULE:/, "").split(";");
|
||||
@@ -181,9 +202,14 @@ export const serializeRecurrenceRule = (
|
||||
const rightKey = right.split("=")[0] ?? "";
|
||||
const leftIndex = orderedKeys.indexOf(leftKey);
|
||||
const rightIndex = orderedKeys.indexOf(rightKey);
|
||||
const normalizedLeftIndex = leftIndex === -1 ? Number.MAX_SAFE_INTEGER : leftIndex;
|
||||
const normalizedRightIndex = rightIndex === -1 ? Number.MAX_SAFE_INTEGER : rightIndex;
|
||||
return normalizedLeftIndex - normalizedRightIndex || leftKey.localeCompare(rightKey);
|
||||
const normalizedLeftIndex =
|
||||
leftIndex === -1 ? Number.MAX_SAFE_INTEGER : leftIndex;
|
||||
const normalizedRightIndex =
|
||||
rightIndex === -1 ? Number.MAX_SAFE_INTEGER : rightIndex;
|
||||
return (
|
||||
normalizedLeftIndex - normalizedRightIndex ||
|
||||
leftKey.localeCompare(rightKey)
|
||||
);
|
||||
});
|
||||
|
||||
return sorted.join(";");
|
||||
@@ -213,7 +239,10 @@ export const formatRecurrenceText = (rule?: string): string | null => {
|
||||
return rrulestr(`RRULE:${rule}`).toText();
|
||||
};
|
||||
|
||||
export const getWeekdayOptions = (): Array<{ value: Weekday; label: string }> => [
|
||||
export const getWeekdayOptions = (): Array<{
|
||||
value: Weekday;
|
||||
label: string;
|
||||
}> => [
|
||||
{ value: "MO", label: "Mon" },
|
||||
{ value: "TU", label: "Tue" },
|
||||
{ value: "WE", label: "Wed" },
|
||||
@@ -228,7 +257,10 @@ export const isWeekdayPreset = (value: RecurrenceFormValue): boolean =>
|
||||
value.interval === 1 &&
|
||||
value.byDay.join(",") === ["MO", "TU", "WE", "TH", "FR"].join(",");
|
||||
|
||||
export const recurrenceFrequencyLabels: Record<SupportedRecurrenceFrequency, string> = {
|
||||
export const recurrenceFrequencyLabels: Record<
|
||||
SupportedRecurrenceFrequency,
|
||||
string
|
||||
> = {
|
||||
NONE: "Does not repeat",
|
||||
DAILY: "Daily",
|
||||
WEEKLY: "Weekly",
|
||||
|
||||
Reference in New Issue
Block a user