diff --git a/src/app/page.tsx b/src/app/page.tsx
index 4803dc7..a832e3e 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -2,127 +2,18 @@
import { useEffect, useState } from 'react'
import { nanoid } from 'nanoid'
-import { Button } from '@/components/ui/button'
-import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
-import { Input } from '@/components/ui/input'
-import { Card, CardHeader, CardContent } from '@/components/ui/card'
-import { Badge } from '@/components/ui/badge'
-import { LucideMapPin, Clock, MoreHorizontal, Calendar1Icon } from 'lucide-react'
-import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, DropdownMenuItem } from '@/components/ui/dropdown-menu'
-import { RecurrencePicker } from '@/components/recurrence-picker'
-import { IcsFilePicker } from '@/components/ics-file-picker'
+import { useSession } from 'next-auth/react'
+import { toast } from 'sonner'
import { saveEvent as addEvent, deleteEvent, getEvents as getAllEvents, clearEvents, updateEvent } from '@/lib/events-db'
import { parseICS, generateICS } from '@/lib/ical'
import type { CalendarEvent } from '@/lib/types'
-import { Textarea } from '@/components/ui/textarea'
-import { useSession } from 'next-auth/react'
-import { toast } from 'sonner'
-// Individual event card component
-const EventCard = ({ event, onEdit, onDelete }: { event: CalendarEvent, onEdit: (event: CalendarEvent) => void, onDelete: (eventId: string) => void }) => {
- 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
- })
- }
-
- return (
-
-
-
-
-
- {event.title}
-
- {event.recurrenceRule && (
-
- Repeats: {event.recurrenceRule}
-
- )}
- {event.description && (
-
- {event.description}
-
- )}
-
-
-
-
-
-
-
- Edit event
-
- onDelete(event.id)}
- className="text-destructive"
- >
- Delete event
-
-
-
-
-
-
-
-
-
-
- {formatDateTime(event.start, event.allDay)}
-
-
- {event.location && (
-
-
- {event.location}
-
- )}
-
-
-
- )
-}
-
-const EventsList = ({ events, onEdit, onDelete }: { events: CalendarEvent[], onEdit: (event: CalendarEvent) => void, onDelete: (eventId: string) => void }) => {
- if (events.length === 0) {
-
- return (
-
-
No events yet
-
Create your first event to get started
-
)
- }
-
- return (
-
- {events.map(event => (
-
- ))}
-
- )
-}
+import { AIToolbar } from '@/components/ai-toolbar'
+import { EventActionsToolbar } from '@/components/event-actions-toolbar'
+import { EventsList } from '@/components/events-list'
+import { EventDialog } from '@/components/event-dialog'
+import { DragDropContainer } from '@/components/drag-drop-container'
export default function HomePage() {
const [events, setEvents] = useState([])
@@ -164,6 +55,7 @@ export default function HomePage() {
setEnd('')
setAllDay(false)
setEditingId(null)
+ setRecurrenceRule(undefined)
}
const handleSave = async () => {
@@ -226,22 +118,6 @@ export default function HomePage() {
URL.revokeObjectURL(url)
}
- // Drag-and-drop
- const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); setIsDragOver(true) }
- 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')) {
- handleImport(file)
- } else {
- toast.warning('Please drop an .ics file')
- }
- }
- }
-
// AI Create Event
const handleAiCreate = async () => {
if (!aiPrompt.trim()) return
@@ -360,130 +236,74 @@ export default function HomePage() {
}
}
+ const handleEdit = (eventData: CalendarEvent) => {
+ setTitle(eventData.title)
+ setDescription(eventData.description || "")
+ setLocation(eventData.location || "")
+ setUrl(eventData.url || "")
+ setStart(eventData.start)
+ setEnd(eventData.end || "")
+ setAllDay(eventData.allDay || false)
+ setEditingId(eventData.id)
+ setRecurrenceRule(eventData.recurrenceRule)
+ setDialogOpen(true)
+ }
+
return (
-
- {/* AI Toolbar */}
- {status === "loading" ?
Loading...
:
- {session?.user ? (
-
-
-
-
-
-
-
- ) : (
-
-
- Sign in to unlock natural language event creation powered by AI
-
-
- )}
-
}
+
+
setDialogOpen(true)}
+ onImport={handleImport}
+ onExport={handleExport}
+ onClearAll={handleClearAll}
+ />
- {/* Summary Panel */}
- {
- summary && (
-
-
- Summary updated {summaryUpdated}
-
- {summary}
-
- )
- }
-
- {/* AI Actions Toolbar */}
- AI actions
-
-
-
-
- {/* Control Toolbar */}
- Event Actions
-
-
- Import .ics
- {events.length > 0 && (
- <>
-
-
- >
- )}
-
-
- {/* Event List */}
- {events.length === 0 && (
- <>
- >)}
{
- setTitle(eventData.title)
- setDescription(eventData.description || "")
- setLocation(eventData.location || "")
- setUrl(eventData.url || "")
- setStart(eventData.start)
- setEnd(eventData.end || "")
- setAllDay(eventData.allDay || false)
- setEditingId(eventData.id)
- setDialogOpen(true)
- }}
+ onEdit={handleEdit}
onDelete={handleDelete}
/>
- {/* Add/Edit Dialog */}
-
-
-
Drag & Drop *.ics here
-
-
+
+
)
}
diff --git a/src/components/ai-toolbar.tsx b/src/components/ai-toolbar.tsx
new file mode 100644
index 0000000..43a1855
--- /dev/null
+++ b/src/components/ai-toolbar.tsx
@@ -0,0 +1,83 @@
+import { Button } from '@/components/ui/button'
+import { Textarea } from '@/components/ui/textarea'
+import { Card } from '@/components/ui/card'
+import { Session } from 'next-auth'
+
+interface AIToolbarProps {
+ session: Session | null
+ status: 'loading' | 'authenticated' | 'unauthenticated'
+ aiPrompt: string
+ setAiPrompt: (prompt: string) => void
+ aiLoading: boolean
+ onAiCreate: () => void
+ onAiSummarize: () => void
+ summary: string | null
+ summaryUpdated: string | null
+}
+
+export const AIToolbar = ({
+ session,
+ status,
+ aiPrompt,
+ setAiPrompt,
+ aiLoading,
+ onAiCreate,
+ onAiSummarize,
+ summary,
+ summaryUpdated
+}: AIToolbarProps) => {
+ return (
+ <>
+ {/* AI Toolbar */}
+ {status === "loading" ? (
+ Loading...
+ ) : (
+
+ {session?.user ? (
+
+
+
+
+
+
+
+ ) : (
+
+
+ Sign in to unlock natural language event creation powered by AI
+
+
+ )}
+
+ )}
+
+ {/* Summary Panel */}
+ {summary && (
+
+
+ Summary updated {summaryUpdated}
+
+ {summary}
+
+ )}
+
+ {/* AI Actions Toolbar */}
+ AI actions
+
+
+
+ >
+ )
+}
diff --git a/src/components/drag-drop-container.tsx b/src/components/drag-drop-container.tsx
new file mode 100644
index 0000000..86aaac2
--- /dev/null
+++ b/src/components/drag-drop-container.tsx
@@ -0,0 +1,55 @@
+import { ReactNode } from 'react'
+import { toast } from 'sonner'
+
+interface DragDropContainerProps {
+ children: ReactNode
+ isDragOver: boolean
+ setIsDragOver: (isDragOver: boolean) => void
+ onImport: (file: File) => void
+}
+
+export const DragDropContainer = ({
+ children,
+ isDragOver,
+ setIsDragOver,
+ onImport
+}: DragDropContainerProps) => {
+ const handleDragOver = (e: React.DragEvent) => {
+ e.preventDefault()
+ setIsDragOver(true)
+ }
+
+ 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')
+ }
+ }
+ }
+
+ return (
+
+ {children}
+
+
Drag & Drop *.ics here
+
+
+ )
+}
diff --git a/src/components/event-actions-toolbar.tsx b/src/components/event-actions-toolbar.tsx
new file mode 100644
index 0000000..7d4afbb
--- /dev/null
+++ b/src/components/event-actions-toolbar.tsx
@@ -0,0 +1,36 @@
+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
+}
+
+export const EventActionsToolbar = ({
+ events,
+ onAddEvent,
+ onImport,
+ onExport,
+ onClearAll
+}: EventActionsToolbarProps) => {
+ return (
+ <>
+ {/* Control Toolbar */}
+ Event Actions
+
+
+ Import .ics
+ {events.length > 0 && (
+ <>
+
+
+ >
+ )}
+
+ >
+ )
+}
diff --git a/src/components/event-card.tsx b/src/components/event-card.tsx
new file mode 100644
index 0000000..1a1a81e
--- /dev/null
+++ b/src/components/event-card.tsx
@@ -0,0 +1,92 @@
+import { Button } from '@/components/ui/button'
+import { Card, CardHeader, CardContent } from '@/components/ui/card'
+import { Badge } from '@/components/ui/badge'
+import { LucideMapPin, Clock, MoreHorizontal } from 'lucide-react'
+import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, DropdownMenuItem } from '@/components/ui/dropdown-menu'
+import type { CalendarEvent } from '@/lib/types'
+
+interface EventCardProps {
+ 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 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 && (
+
+ Repeats: {event.recurrenceRule}
+
+ )}
+ {event.description && (
+
+ {event.description}
+
+ )}
+
+
+
+
+
+
+
+ Edit event
+
+ onDelete(event.id)}
+ className="text-destructive"
+ >
+ Delete event
+
+
+
+
+
+
+
+
+
+
+ {formatDateTime(event.start, event.allDay)}
+
+
+ {event.location && (
+
+
+ {event.location}
+
+ )}
+
+
+
+ )
+}
diff --git a/src/components/event-dialog.tsx b/src/components/event-dialog.tsx
new file mode 100644
index 0000000..6eea8ca
--- /dev/null
+++ b/src/components/event-dialog.tsx
@@ -0,0 +1,96 @@
+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
+}
+
+export const EventDialog = ({
+ 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)
+ }
+
+ return (
+
+ )
+}
diff --git a/src/components/events-list.tsx b/src/components/events-list.tsx
new file mode 100644
index 0000000..cbdd374
--- /dev/null
+++ b/src/components/events-list.tsx
@@ -0,0 +1,34 @@
+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
+}
+
+export const EventsList = ({ events, onEdit, onDelete }: EventsListProps) => {
+ if (events.length === 0) {
+ return (
+
+
+
No events yet
+
Create your first event to get started
+
+ )
+ }
+
+ return (
+
+ {events.map(event => (
+
+ ))}
+
+ )
+}