diff --git a/src/components/ai-toolbar.tsx b/src/components/ai-toolbar.tsx
index 6d74585..342a34e 100644
--- a/src/components/ai-toolbar.tsx
+++ b/src/components/ai-toolbar.tsx
@@ -1,80 +1,84 @@
-import { Button } from '@/components/ui/button'
-import { Textarea } from '@/components/ui/textarea'
-import { Card } from '@/components/ui/card'
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import { Card } from "@/components/ui/card";
interface AIToolbarProps {
- isAuthenticated: boolean
- isPending: boolean
- aiPrompt: string
- setAiPrompt: (prompt: string) => void
- aiLoading: boolean
- onAiCreate: () => void
- onAiSummarize: () => void
- summary: string | null
- summaryUpdated: string | null
+ isAuthenticated: boolean;
+ isPending: boolean;
+ aiPrompt: string;
+ setAiPrompt: (prompt: string) => void;
+ aiLoading: boolean;
+ onAiCreate: () => void;
+ onAiSummarize: () => void;
+ summary: string | null;
+ summaryUpdated: string | null;
}
export const AIToolbar = ({
- isAuthenticated,
- isPending,
- aiPrompt,
- setAiPrompt,
- aiLoading,
- onAiCreate,
- onAiSummarize,
- summary,
- summaryUpdated
+ isAuthenticated,
+ isPending,
+ aiPrompt,
+ setAiPrompt,
+ aiLoading,
+ onAiCreate,
+ onAiSummarize,
+ summary,
+ summaryUpdated,
}: AIToolbarProps) => {
- return (
- <>
- {isPending ? (
-
Loading...
- ) : (
-
- {isAuthenticated ? (
-
-
-
-
-
-
-
- ) : (
-
-
- Sign in to unlock natural language event creation powered by AI
-
-
- )}
-
- )}
+ return (
+ <>
+ {isPending ? (
+
+ Loading...
+
+ ) : (
+
+ {isAuthenticated ? (
+
+
+
+
+
+
+
+ ) : (
+
+
+ Sign in to unlock natural language event creation powered by AI
+
+
+ )}
+
+ )}
- {/* Summary Panel */}
- {summary && (
-
-
- Summary updated {summaryUpdated}
-
- {summary}
-
- )}
+ {/* Summary Panel */}
+ {summary && (
+
+ Summary updated {summaryUpdated}
+ {summary}
+
+ )}
- {/* AI Actions Toolbar */}
- AI actions
-
-
-
- >
- )
-}
+ {/* AI Actions Toolbar */}
+ AI actions
+
+
+
+ >
+ );
+};
diff --git a/src/components/drag-drop-container.tsx b/src/components/drag-drop-container.tsx
index 86aaac2..8b98ff1 100644
--- a/src/components/drag-drop-container.tsx
+++ b/src/components/drag-drop-container.tsx
@@ -1,55 +1,55 @@
-import { ReactNode } from 'react'
-import { toast } from 'sonner'
+import { ReactNode } from "react";
+import { toast } from "sonner";
interface DragDropContainerProps {
- children: ReactNode
- isDragOver: boolean
- setIsDragOver: (isDragOver: boolean) => void
- onImport: (file: File) => void
+ children: ReactNode;
+ isDragOver: boolean;
+ setIsDragOver: (isDragOver: boolean) => void;
+ onImport: (file: File) => void;
}
export const DragDropContainer = ({
- children,
- isDragOver,
- setIsDragOver,
- onImport
+ children,
+ isDragOver,
+ setIsDragOver,
+ onImport,
}: DragDropContainerProps) => {
- const handleDragOver = (e: React.DragEvent) => {
- e.preventDefault()
- setIsDragOver(true)
- }
+ const handleDragOver = (e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragOver(true);
+ };
- const handleDragLeave = (e: React.DragEvent) => {
- e.preventDefault()
- setIsDragOver(false)
- }
+ const handleDragLeave = (e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragOver(false);
+ };
- const handleDrop = (e: React.DragEvent) => {
- e.preventDefault()
- setIsDragOver(false)
- if (e.dataTransfer.files?.length) {
- const file = e.dataTransfer.files[0]
- if (file.name.endsWith('.ics')) {
- onImport(file)
- } else {
- toast.warning('Please drop an .ics file')
- }
- }
- }
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragOver(false);
+ if (e.dataTransfer.files?.length) {
+ const file = e.dataTransfer.files[0];
+ if (file.name.endsWith(".ics")) {
+ onImport(file);
+ } else {
+ toast.warning("Please drop an .ics file");
+ }
+ }
+ };
- return (
-
- {children}
-
-
Drag & Drop *.ics here
-
-
- )
-}
+ return (
+
+ {children}
+
+
Drag & Drop *.ics here
+
+
+ );
+};
diff --git a/src/components/event-actions-toolbar.tsx b/src/components/event-actions-toolbar.tsx
index 7d4afbb..805663e 100644
--- a/src/components/event-actions-toolbar.tsx
+++ b/src/components/event-actions-toolbar.tsx
@@ -1,36 +1,42 @@
-import { Button } from '@/components/ui/button'
-import { IcsFilePicker } from '@/components/ics-file-picker'
-import type { CalendarEvent } from '@/lib/types'
+import { Button } from "@/components/ui/button";
+import { IcsFilePicker } from "@/components/ics-file-picker";
+import type { CalendarEvent } from "@/lib/types";
interface EventActionsToolbarProps {
- events: CalendarEvent[]
- onAddEvent: () => void
- onImport: (file: File) => void
- onExport: () => void
- onClearAll: () => void
+ events: CalendarEvent[];
+ onAddEvent: () => void;
+ onImport: (file: File) => void;
+ onExport: () => void;
+ onClearAll: () => void;
}
export const EventActionsToolbar = ({
- events,
- onAddEvent,
- onImport,
- onExport,
- onClearAll
+ events,
+ onAddEvent,
+ onImport,
+ onExport,
+ onClearAll,
}: EventActionsToolbarProps) => {
- return (
- <>
- {/* Control Toolbar */}
- Event Actions
-
-
- Import .ics
- {events.length > 0 && (
- <>
-
-
- >
- )}
-
- >
- )
-}
+ return (
+ <>
+ {/* Control Toolbar */}
+ Event Actions
+
+
+
+ Import .ics
+
+ {events.length > 0 && (
+ <>
+
+
+ >
+ )}
+
+ >
+ );
+};
diff --git a/src/components/event-card.tsx b/src/components/event-card.tsx
index eadf020..0d434f9 100644
--- a/src/components/event-card.tsx
+++ b/src/components/event-card.tsx
@@ -1,92 +1,95 @@
-import { Button } from '@/components/ui/button'
-import { Card, CardHeader, CardContent } from '@/components/ui/card'
-import { LucideMapPin, Clock, MoreHorizontal } from 'lucide-react'
-import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, DropdownMenuItem } from '@/components/ui/dropdown-menu'
-import { RRuleDisplay } from '@/components/rrule-display'
-import type { CalendarEvent } from '@/lib/types'
+import { Button } from "@/components/ui/button";
+import { Card, CardHeader, CardContent } from "@/components/ui/card";
+import { LucideMapPin, Clock, MoreHorizontal } from "lucide-react";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuTrigger,
+ DropdownMenuItem,
+} from "@/components/ui/dropdown-menu";
+import { RRuleDisplay } from "@/components/rrule-display";
+import type { CalendarEvent } from "@/lib/types";
interface EventCardProps {
- event: CalendarEvent
- onEdit: (event: CalendarEvent) => void
- onDelete: (eventId: string) => void
+ event: CalendarEvent;
+ onEdit: (event: CalendarEvent) => void;
+ onDelete: (eventId: string) => void;
}
export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => {
- const formatDateTime = (dateStr: string, allDay: boolean | undefined) => {
- return allDay
- ? new Date(dateStr).toLocaleDateString()
- : new Date(dateStr).toLocaleString()
- }
+ const formatDateTime = (dateStr: string, allDay: boolean | undefined) => {
+ return allDay
+ ? new Date(dateStr).toLocaleDateString()
+ : new Date(dateStr).toLocaleString();
+ };
- const handleEdit = () => {
- onEdit({
- id: event.id,
- title: event.title,
- description: event.description || '',
- location: event.location || '',
- url: event.url || '',
- start: event.start,
- end: event.end || '',
- allDay: event.allDay || false
- })
- }
+ const handleEdit = () => {
+ onEdit({
+ id: event.id,
+ title: event.title,
+ description: event.description || "",
+ location: event.location || "",
+ url: event.url || "",
+ start: event.start,
+ end: event.end || "",
+ allDay: event.allDay || false,
+ });
+ };
- return (
-
-
-
-
-
- {event.title}
-
- {event.recurrenceRule && (
-
-
-
- )}
- {event.description && (
-
- {event.description}
-
- )}
-
-
-
-
-
-
-
- Edit
-
- onDelete(event.id)}
- className="text-destructive"
- >
- Delete
-
-
-
-
-
+ return (
+
+
+
+
+
+ {event.title}
+
+ {event.recurrenceRule && (
+
+
+
+ )}
+ {event.description && (
+
+ {event.description}
+
+ )}
+
+
+
+
+
+
+ Edit
+ onDelete(event.id)}
+ className="text-destructive"
+ >
+ Delete
+
+
+
+
+
-
-
-
-
- {formatDateTime(event.start, event.allDay)}
-
+
+
+
+
+ {formatDateTime(event.start, event.allDay)}
+
- {event.location && (
-
-
- {event.location}
-
- )}
-
-
-
- )
-}
+ {event.location && (
+
+
+ {event.location}
+
+ )}
+
+
+
+ );
+};
diff --git a/src/components/event-dialog.tsx b/src/components/event-dialog.tsx
index 6eea8ca..e4989f2 100644
--- a/src/components/event-dialog.tsx
+++ b/src/components/event-dialog.tsx
@@ -1,96 +1,134 @@
-import { Button } from '@/components/ui/button'
-import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
-import { Input } from '@/components/ui/input'
-import { RecurrencePicker } from '@/components/recurrence-picker'
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { RecurrencePicker } from "@/components/recurrence-picker";
interface EventDialogProps {
- open: boolean
- onOpenChange: (open: boolean) => void
- editingId: string | null
- title: string
- setTitle: (title: string) => void
- description: string
- setDescription: (description: string) => void
- location: string
- setLocation: (location: string) => void
- url: string
- setUrl: (url: string) => void
- start: string
- setStart: (start: string) => void
- end: string
- setEnd: (end: string) => void
- allDay: boolean
- setAllDay: (allDay: boolean) => void
- recurrenceRule: string | undefined
- setRecurrenceRule: (rule: string | undefined) => void
- onSave: () => void
- onReset: () => void
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ editingId: string | null;
+ title: string;
+ setTitle: (title: string) => void;
+ description: string;
+ setDescription: (description: string) => void;
+ location: string;
+ setLocation: (location: string) => void;
+ url: string;
+ setUrl: (url: string) => void;
+ start: string;
+ setStart: (start: string) => void;
+ end: string;
+ setEnd: (end: string) => void;
+ allDay: boolean;
+ setAllDay: (allDay: boolean) => void;
+ recurrenceRule: string | undefined;
+ setRecurrenceRule: (rule: string | undefined) => void;
+ onSave: () => void;
+ onReset: () => void;
}
export const EventDialog = ({
- open,
- onOpenChange,
- editingId,
- title,
- setTitle,
- description,
- setDescription,
- location,
- setLocation,
- url,
- setUrl,
- start,
- setStart,
- end,
- setEnd,
- allDay,
- setAllDay,
- recurrenceRule,
- setRecurrenceRule,
- onSave,
- onReset
+ open,
+ onOpenChange,
+ editingId,
+ title,
+ setTitle,
+ description,
+ setDescription,
+ location,
+ setLocation,
+ url,
+ setUrl,
+ start,
+ setStart,
+ end,
+ setEnd,
+ allDay,
+ setAllDay,
+ recurrenceRule,
+ setRecurrenceRule,
+ onSave,
+ onReset,
}: EventDialogProps) => {
- const handleOpenChange = (val: boolean) => {
- if (!val) onReset()
- onOpenChange(val)
- }
+ const handleOpenChange = (val: boolean) => {
+ if (!val) onReset();
+ onOpenChange(val);
+ };
- return (
-
+ );
+};
diff --git a/src/components/events-list.tsx b/src/components/events-list.tsx
index cbdd374..0a39276 100644
--- a/src/components/events-list.tsx
+++ b/src/components/events-list.tsx
@@ -1,34 +1,38 @@
-import { Calendar1Icon } from 'lucide-react'
-import { EventCard } from './event-card'
-import type { CalendarEvent } from '@/lib/types'
+import { Calendar1Icon } from "lucide-react";
+import { EventCard } from "./event-card";
+import type { CalendarEvent } from "@/lib/types";
interface EventsListProps {
- events: CalendarEvent[]
- onEdit: (event: CalendarEvent) => void
- onDelete: (eventId: string) => void
+ events: CalendarEvent[];
+ onEdit: (event: CalendarEvent) => void;
+ onDelete: (eventId: string) => void;
}
export const EventsList = ({ events, onEdit, onDelete }: EventsListProps) => {
- if (events.length === 0) {
- return (
-
-
-
No events yet
-
Create your first event to get started
-
- )
- }
+ if (events.length === 0) {
+ return (
+
+
+
+ No events yet
+
+
+ Create your first event to get started
+
+
+ );
+ }
- return (
-
- {events.map(event => (
-
- ))}
-
- )
-}
+ return (
+
+ {events.map((event) => (
+
+ ))}
+
+ );
+};
diff --git a/src/components/ics-file-picker.tsx b/src/components/ics-file-picker.tsx
index f0789f3..d842ff7 100644
--- a/src/components/ics-file-picker.tsx
+++ b/src/components/ics-file-picker.tsx
@@ -1,58 +1,68 @@
-"use client"
+"use client";
-import type React from "react"
+import type React from "react";
-import { useRef } from "react"
-import { Button } from "@/components/ui/button"
-import { Calendar } from "lucide-react"
+import { useRef } from "react";
+import { Button } from "@/components/ui/button";
+import { Calendar } from "lucide-react";
interface IcsFilePickerProps {
- onFileSelect?: (file: File) => void
- className?: string
- children?: React.ReactNode
- variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link"
- size?: "default" | "sm" | "lg" | "icon"
+ onFileSelect?: (file: File) => void;
+ className?: string;
+ children?: React.ReactNode;
+ variant?:
+ | "default"
+ | "destructive"
+ | "outline"
+ | "secondary"
+ | "ghost"
+ | "link";
+ size?: "default" | "sm" | "lg" | "icon";
}
export function IcsFilePicker({
- onFileSelect,
- className,
- children,
- variant = "default",
- size = "default",
+ onFileSelect,
+ className,
+ children,
+ variant = "default",
+ size = "default",
}: IcsFilePickerProps) {
- const fileInputRef = useRef(null)
+ const fileInputRef = useRef(null);
- const handleButtonClick = () => {
- fileInputRef.current?.click()
- }
+ const handleButtonClick = () => {
+ fileInputRef.current?.click();
+ };
- const handleFileChange = (event: React.ChangeEvent) => {
- const file = event.target.files?.[0]
- if (file && onFileSelect) {
- onFileSelect(file)
- }
- }
+ const handleFileChange = (event: React.ChangeEvent) => {
+ const file = event.target.files?.[0];
+ if (file && onFileSelect) {
+ onFileSelect(file);
+ }
+ };
- return (
- <>
-
-
- >
- )
+ return (
+ <>
+
+
+ >
+ );
}
-
diff --git a/src/components/mode-toggle.tsx b/src/components/mode-toggle.tsx
index d6504c9..d593135 100644
--- a/src/components/mode-toggle.tsx
+++ b/src/components/mode-toggle.tsx
@@ -1,77 +1,75 @@
-"use client"
+"use client";
-import * as React from "react"
-import { Moon, Sun, Monitor } from "lucide-react"
-import { useTheme } from "next-themes"
+import * as React from "react";
+import { Moon, Sun, Monitor } from "lucide-react";
+import { useTheme } from "next-themes";
-import { Button } from "@/components/ui/button"
+import { Button } from "@/components/ui/button";
import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
type ThemeIconProps = {
- theme?: string
-}
+ theme?: string;
+};
const ThemeIcon = ({ theme }: ThemeIconProps) => {
+ const [mounted, setMounted] = React.useState(false);
- const [mounted, setMounted] = React.useState(false)
+ React.useEffect(() => {
+ setMounted(true);
+ }, []);
- React.useEffect(() => {
- setMounted(true)
- }, [])
+ if (!mounted) {
+ return null;
+ }
- if (!mounted) {
- return null
- }
-
- switch (theme) {
- case "light":
- return (
-
- )
- case "dark":
- return (
-
- )
- case "system":
- return (
-
- )
- default:
- return (<>
-
-
- >)
- }
-}
+ switch (theme) {
+ case "light":
+ return (
+
+ );
+ case "dark":
+ return (
+
+ );
+ case "system":
+ return ;
+ default:
+ return (
+ <>
+
+
+ >
+ );
+ }
+};
export function ModeToggle() {
- const { setTheme, theme } = useTheme()
+ const { setTheme, theme } = useTheme();
- return (
-
-
-
-
-
- setTheme("light")}>
- Light
-
- setTheme("dark")}>
- Dark
-
- setTheme("system")}>
- System
-
-
-
- )
+ return (
+
+
+
+
+
+ setTheme("light")}>
+ Light
+
+ setTheme("dark")}>
+ Dark
+
+ setTheme("system")}>
+ System
+
+
+
+ );
}
-
diff --git a/src/components/recurrence-picker.tsx b/src/components/recurrence-picker.tsx
index 58f371f..39564ed 100644
--- a/src/components/recurrence-picker.tsx
+++ b/src/components/recurrence-picker.tsx
@@ -1,153 +1,184 @@
-"use client"
+"use client";
-import { useState } from "react"
-import { Input } from "@/components/ui/input"
-import { Label } from "@/components/ui/label"
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
-import { Checkbox } from "@/components/ui/checkbox"
+import { useState } from "react";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Checkbox } from "@/components/ui/checkbox";
type Recurrence = {
- freq: "NONE" | "DAILY" | "WEEKLY" | "MONTHLY"
- interval: number
- byDay?: string[]
- count?: number
- until?: string
-}
+ freq: "NONE" | "DAILY" | "WEEKLY" | "MONTHLY";
+ interval: number;
+ byDay?: string[];
+ count?: number;
+ until?: string;
+};
interface Props {
- value?: string
- onChange: (rrule: string | undefined) => void
+ value?: string;
+ onChange: (rrule: string | undefined) => void;
}
export function RecurrencePicker({ value, onChange }: Props) {
- const [rec, setRec] = useState(() => {
- // If existing rrule, parse minimally (for simplicity we only rehydrate FREQ and INTERVAL)
- if (value) {
- const parts = Object.fromEntries(value.split(";").map((p) => p.split("=")))
- return {
- freq: parts.FREQ || "NONE",
- interval: parts.INTERVAL ? Number.parseInt(parts.INTERVAL, 10) : 1,
- byDay: parts.BYDAY ? parts.BYDAY.split(",") : [],
- count: parts.COUNT ? Number.parseInt(parts.COUNT, 10) : undefined,
- until: parts.UNTIL,
- }
- }
- return { freq: "NONE", interval: 1 }
- })
+ const [rec, setRec] = useState(() => {
+ // If existing rrule, parse minimally (for simplicity we only rehydrate FREQ and INTERVAL)
+ if (value) {
+ const parts = Object.fromEntries(
+ value.split(";").map((p) => p.split("=")),
+ );
+ return {
+ freq: parts.FREQ || "NONE",
+ interval: parts.INTERVAL ? Number.parseInt(parts.INTERVAL, 10) : 1,
+ byDay: parts.BYDAY ? parts.BYDAY.split(",") : [],
+ count: parts.COUNT ? Number.parseInt(parts.COUNT, 10) : undefined,
+ until: parts.UNTIL,
+ };
+ }
+ return { freq: "NONE", interval: 1 };
+ });
- const update = (updates: Partial) => {
- const newRec = { ...rec, ...updates }
- setRec(newRec)
+ const update = (updates: Partial) => {
+ const newRec = { ...rec, ...updates };
+ setRec(newRec);
- if (newRec.freq === "NONE") {
- onChange(undefined)
- return
- }
+ if (newRec.freq === "NONE") {
+ onChange(undefined);
+ return;
+ }
- // Build RRULE string
- let rrule = `FREQ=${newRec.freq};INTERVAL=${newRec.interval}`
- if (newRec.freq === "WEEKLY" && newRec.byDay?.length) {
- rrule += `;BYDAY=${newRec.byDay.join(",")}`
- }
- if (newRec.count) rrule += `;COUNT=${newRec.count}`
- if (newRec.until) rrule += `;UNTIL=${newRec.until.replace(/-/g, "")}T000000Z`
+ // Build RRULE string
+ let rrule = `FREQ=${newRec.freq};INTERVAL=${newRec.interval}`;
+ if (newRec.freq === "WEEKLY" && newRec.byDay?.length) {
+ rrule += `;BYDAY=${newRec.byDay.join(",")}`;
+ }
+ if (newRec.count) rrule += `;COUNT=${newRec.count}`;
+ if (newRec.until)
+ rrule += `;UNTIL=${newRec.until.replace(/-/g, "")}T000000Z`;
- onChange(rrule)
- }
+ onChange(rrule);
+ };
- const toggleDay = (day: string) => {
- const byDay = rec.byDay || []
- const newByDay = byDay.includes(day) ? byDay.filter((d) => d !== day) : [...byDay, day]
- update({ byDay: newByDay })
- }
+ const toggleDay = (day: string) => {
+ const byDay = rec.byDay || [];
+ const newByDay = byDay.includes(day)
+ ? byDay.filter((d) => d !== day)
+ : [...byDay, day];
+ update({ byDay: newByDay });
+ };
- const dayLabels = {
- MO: "Mon",
- TU: "Tue",
- WE: "Wed",
- TH: "Thu",
- FR: "Fri",
- SA: "Sat",
- SU: "Sun",
- }
+ const dayLabels = {
+ MO: "Mon",
+ TU: "Tue",
+ WE: "Wed",
+ TH: "Thu",
+ FR: "Fri",
+ SA: "Sat",
+ SU: "Sun",
+ };
- return (
-
-
-
-
-
+ return (
+
+
+
+
+
- {rec.freq !== "NONE" && (
- <>
-
-
- update({ interval: Number.parseInt(e.target.value, 10) || 1 })}
- className="w-24"
- />
-
+ {rec.freq !== "NONE" && (
+ <>
+
+
+
+ update({ interval: Number.parseInt(e.target.value, 10) || 1 })
+ }
+ className="w-24"
+ />
+
- {rec.freq === "WEEKLY" && (
-
-
-
- {["MO", "TU", "WE", "TH", "FR", "SA", "SU"].map((day) => (
-
- toggleDay(day)}
- />
-
-
- ))}
-
-
- )}
+ {rec.freq === "WEEKLY" && (
+
+
+
+ {["MO", "TU", "WE", "TH", "FR", "SA", "SU"].map((day) => (
+
+ toggleDay(day)}
+ />
+
+
+ ))}
+
+
+ )}
-
- >
- )}
-
- )
+
+ >
+ )}
+
+ );
}
diff --git a/src/components/rrule-display.tsx b/src/components/rrule-display.tsx
index d491afa..621f73e 100644
--- a/src/components/rrule-display.tsx
+++ b/src/components/rrule-display.tsx
@@ -1,244 +1,306 @@
-import { Badge } from "@/components/ui/badge"
-import type { RecurrenceRule } from "@/lib/rfc5545-types"
+import { Badge } from "@/components/ui/badge";
+import type { RecurrenceRule } from "@/lib/rfc5545-types";
interface RRuleDisplayProps {
- rrule: string | RecurrenceRule
- className?: string
+ 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}
-
- )
+ const parsedRule =
+ typeof rrule === "string" ? parseRRuleString(rrule) : rrule;
+ const humanText = formatRRuleToHuman(parsedRule);
+
+ return (
+
+ {humanText}
+
+ );
}
interface RRuleDisplayDetailedProps {
- rrule: string | RecurrenceRule
- className?: string
- showBadges?: boolean
+ 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}
-
- ))}
-
- )}
-
-
- )
+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'],
- }
+ 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
+ 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
+ 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])
+ 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]}`
+ 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)
- }
- }
+ 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),
+ };
+ }
}
diff --git a/src/components/sign-in.tsx b/src/components/sign-in.tsx
index c3fcff6..76cb235 100644
--- a/src/components/sign-in.tsx
+++ b/src/components/sign-in.tsx
@@ -1,44 +1,44 @@
-"use client"
+"use client";
-import { signOut, useSession } from "@/lib/auth-client"
-import { Button } from "@/components/ui/button"
-import { useRouter } from "next/navigation"
-import { toast } from "sonner"
+import { signOut, useSession } from "@/lib/auth-client";
+import { Button } from "@/components/ui/button";
+import { useRouter } from "next/navigation";
+import { toast } from "sonner";
export default function SignIn() {
- const { data: session, isPending } = useSession()
- const router = useRouter()
+ const { data: session, isPending } = useSession();
+ const router = useRouter();
- const handleSignOut = async () => {
- try {
- await signOut()
- router.push("/")
- } catch (_error) {
- toast.error("Failed to sign out. Please try again.")
- }
- }
+ const handleSignOut = async () => {
+ try {
+ await signOut();
+ router.push("/");
+ } catch (_error) {
+ toast.error("Failed to sign out. Please try again.");
+ }
+ };
- if (isPending) {
- return
- }
+ if (isPending) {
+ return ;
+ }
- if (session?.user) {
- return (
-
-
-
- )
- }
+ if (session?.user) {
+ return (
+
+
+
+ );
+ }
- return (
-
- )
+ return (
+
+ );
}
diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx
index 6a1ffe4..f6b22ae 100644
--- a/src/components/theme-provider.tsx
+++ b/src/components/theme-provider.tsx
@@ -1,11 +1,11 @@
-"use client"
+"use client";
-import * as React from "react"
-import { ThemeProvider as NextThemesProvider } from "next-themes"
+import * as React from "react";
+import { ThemeProvider as NextThemesProvider } from "next-themes";
export function ThemeProvider({
- children,
- ...props
+ children,
+ ...props
}: React.ComponentProps) {
- return {children}
+ return {children};
}