import { Badge } from "@/components/ui/badge" import type { RecurrenceRule } from "@/lib/rfc5545-types" interface RRuleDisplayProps { rrule: string | RecurrenceRule className?: string } export function RRuleDisplay({ rrule, className }: RRuleDisplayProps) { const parsedRule = typeof rrule === 'string' ? parseRRuleString(rrule) : rrule const humanText = formatRRuleToHuman(parsedRule) return (
{humanText}
) } interface RRuleDisplayDetailedProps { rrule: string | RecurrenceRule className?: string showBadges?: boolean } export function RRuleDisplayDetailed({ rrule, className, showBadges = true }: RRuleDisplayDetailedProps) { const parsedRule = typeof rrule === 'string' ? parseRRuleString(rrule) : rrule const humanText = formatRRuleToHuman(parsedRule) const details = getRRuleDetails(parsedRule) return (
{humanText}
{showBadges && details.length > 0 && (
{details.map((detail, index) => ( {detail} ))}
)}
) } function parseRRuleString(rruleString: string): RecurrenceRule { const parts = Object.fromEntries(rruleString.split(";").map(p => p.split("="))) return { freq: parts.FREQ as RecurrenceRule['freq'], until: parts.UNTIL ? new Date(parts.UNTIL.replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z?/, '$1-$2-$3T$4:$5:$6Z')).toISOString() : undefined, count: parts.COUNT ? parseInt(parts.COUNT, 10) : undefined, interval: parts.INTERVAL ? parseInt(parts.INTERVAL, 10) : undefined, bySecond: parts.BYSECOND ? parts.BYSECOND.split(",").map((n: string) => parseInt(n, 10)) : undefined, byMinute: parts.BYMINUTE ? parts.BYMINUTE.split(",").map((n: string) => parseInt(n, 10)) : undefined, byHour: parts.BYHOUR ? parts.BYHOUR.split(",").map((n: string) => parseInt(n, 10)) : undefined, byDay: parts.BYDAY ? parts.BYDAY.split(",") : undefined, byMonthDay: parts.BYMONTHDAY ? parts.BYMONTHDAY.split(",").map((n: string) => parseInt(n, 10)) : undefined, byYearDay: parts.BYYEARDAY ? parts.BYYEARDAY.split(",").map((n: string) => parseInt(n, 10)) : undefined, byWeekNo: parts.BYWEEKNO ? parts.BYWEEKNO.split(",").map((n: string) => parseInt(n, 10)) : undefined, byMonth: parts.BYMONTH ? parts.BYMONTH.split(",").map((n: string) => parseInt(n, 10)) : undefined, bySetPos: parts.BYSETPOS ? parts.BYSETPOS.split(",").map((n: string) => parseInt(n, 10)) : undefined, wkst: parts.WKST as RecurrenceRule['wkst'], } } function formatRRuleToHuman(rule: RecurrenceRule): string { const { freq, interval = 1, count, until, byDay, byMonthDay, byMonth, byHour, byMinute, bySecond } = rule let text = "" // Base frequency switch (freq) { case 'SECONDLY': text = interval === 1 ? "Every second" : `Every ${interval} seconds` break case 'MINUTELY': text = interval === 1 ? "Every minute" : `Every ${interval} minutes` break case 'HOURLY': text = interval === 1 ? "Every hour" : `Every ${interval} hours` break case 'DAILY': text = interval === 1 ? "Daily" : `Every ${interval} days` break case 'WEEKLY': text = interval === 1 ? "Weekly" : `Every ${interval} weeks` break case 'MONTHLY': text = interval === 1 ? "Monthly" : `Every ${interval} months` break case 'YEARLY': text = interval === 1 ? "Yearly" : `Every ${interval} years` break } // Add day specifications if (byDay?.length) { const dayNames = { 'SU': 'Sunday', 'MO': 'Monday', 'TU': 'Tuesday', 'WE': 'Wednesday', 'TH': 'Thursday', 'FR': 'Friday', 'SA': 'Saturday' } const days = byDay.map(day => { // Handle numbered days like "2TU" (second Tuesday) const match = day.match(/^(-?\d+)?([A-Z]{2})$/) if (match) { const [, num, dayCode] = match const dayName = dayNames[dayCode as keyof typeof dayNames] if (num) { const ordinal = getOrdinal(parseInt(num)) return `${ordinal} ${dayName}` } return dayName } return day }) if (freq === 'WEEKLY') { text += ` on ${formatList(days)}` } else { text += ` on ${formatList(days)}` } } // Add month day specifications if (byMonthDay?.length) { const days = byMonthDay.map(day => { if (day < 0) { return `${getOrdinal(Math.abs(day))} to last day` } return getOrdinal(day) }) text += ` on the ${formatList(days)}` } // Add month specifications if (byMonth?.length) { const monthNames = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ] const months = byMonth.map(month => monthNames[month - 1]) text += ` in ${formatList(months)}` } // Add time specifications if (byHour?.length || byMinute?.length || bySecond?.length) { const timeSpecs = [] if (byHour?.length) { const hours = byHour.map(h => `${h.toString().padStart(2, '0')}:00`) timeSpecs.push(`at ${formatList(hours)}`) } if (byMinute?.length && !byHour?.length) { timeSpecs.push(`at minute ${formatList(byMinute.map(String))}`) } if (bySecond?.length && !byHour?.length && !byMinute?.length) { timeSpecs.push(`at second ${formatList(bySecond.map(String))}`) } if (timeSpecs.length) { text += ` ${timeSpecs.join(' ')}` } } // Add end conditions if (count) { text += `, ${count} time${count === 1 ? '' : 's'}` } else if (until) { const date = new Date(until) text += `, until ${date.toLocaleDateString()}` } return text } function getRRuleDetails(rule: RecurrenceRule): string[] { const details: string[] = [] if (rule.wkst && rule.wkst !== 'MO') { const dayNames = { 'SU': 'Sunday', 'MO': 'Monday', 'TU': 'Tuesday', 'WE': 'Wednesday', 'TH': 'Thursday', 'FR': 'Friday', 'SA': 'Saturday' } details.push(`Week starts ${dayNames[rule.wkst]}`) } if (rule.byWeekNo?.length) { details.push(`Week ${formatList(rule.byWeekNo.map(String))}`) } if (rule.byYearDay?.length) { details.push(`Day ${formatList(rule.byYearDay.map(String))} of year`) } if (rule.bySetPos?.length) { const positions = rule.bySetPos.map(pos => { if (pos < 0) { return `${getOrdinal(Math.abs(pos))} to last` } return getOrdinal(pos) }) details.push(`Position ${formatList(positions)}`) } return details } function getOrdinal(num: number): string { const suffix = ['th', 'st', 'nd', 'rd'] const v = num % 100 return num + (suffix[(v - 20) % 10] || suffix[v] || suffix[0]) } function formatList(items: string[]): string { if (items.length === 0) return '' if (items.length === 1) return items[0] if (items.length === 2) return `${items[0]} and ${items[1]}` return `${items.slice(0, -1).join(', ')}, and ${items[items.length - 1]}` } // Hook for easy usage in components export function useRRuleDisplay(rrule?: string) { if (!rrule) return null try { const parsedRule = parseRRuleString(rrule) return { humanText: formatRRuleToHuman(parsedRule), details: getRRuleDetails(parsedRule), parsedRule } } catch (error) { return { humanText: "Invalid recurrence rule", details: [], parsedRule: null, error: error instanceof Error ? error.message : String(error) } } }