style(app): standardize app page file formatting

This commit is contained in:
2026-04-07 08:09:56 -04:00
parent 48ef4f60df
commit 954e73c007
5 changed files with 497 additions and 456 deletions

View File

@@ -1,43 +1,45 @@
"use client"
"use client";
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import Link from "next/link"
import { useSearchParams } from "next/navigation"
import { Suspense } from "react"
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { Suspense } from "react";
function Search() {
const searchParams = useSearchParams()
const errorMessage = searchParams.get('error')
// Sanitize error message to prevent XSS
const sanitizedError = errorMessage
? errorMessage.replace(/[<>]/g, '')
: 'An authentication error occurred'
const searchParams = useSearchParams();
const errorMessage = searchParams.get("error");
return (<div className="text-center p-3 bg-background rounded-lg">
{sanitizedError}
</div>)
// Sanitize error message to prevent XSS
const sanitizedError = errorMessage
? errorMessage.replace(/[<>]/g, "")
: "An authentication error occurred";
return (
<div className="text-center p-3 bg-background rounded-lg">
{sanitizedError}
</div>
);
}
export default function AuthErrorPage() {
return (
<div className="min-h-screen flex items-center justify-center bg-background p-4">
<Card className="w-full max-w-md bg-red-400 dark:bg-red-600">
<CardHeader className="text-center">
<CardTitle className="text-2xl font-bold">Error</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Suspense>
<Search />
</Suspense>
<div className="flex flex-row">
<Button variant="secondary" asChild>
<Link href="/">Go back to Homepage</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
)
return (
<div className="min-h-screen flex items-center justify-center bg-background p-4">
<Card className="w-full max-w-md bg-red-400 dark:bg-red-600">
<CardHeader className="text-center">
<CardTitle className="text-2xl font-bold">Error</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Suspense>
<Search />
</Suspense>
<div className="flex flex-row">
<Button variant="secondary" asChild>
<Link href="/">Go back to Homepage</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,67 +1,81 @@
"use client"
"use client";
import { signIn, useSession } from "@/lib/auth-client"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { useEffect, useState } from "react"
import { toast } from "sonner"
import { signIn, useSession } from "@/lib/auth-client";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "sonner";
export default function SignInPage() {
const { data: session, isPending } = useSession()
const router = useRouter()
const [isLoading, setIsLoading] = useState(false)
const { data: session, isPending } = useSession();
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (session?.user) {
router.push("/")
}
}, [session, router])
useEffect(() => {
if (session?.user) {
router.push("/");
}
}, [session, router]);
const handleSignIn = async () => {
setIsLoading(true)
try {
await signIn.oauth2({
providerId: "authentik",
callbackURL: "/",
})
} catch (_error) {
toast.error("Failed to sign in. Please try again.")
} finally {
setIsLoading(false)
}
}
const handleSignIn = async () => {
setIsLoading(true);
try {
await signIn.oauth2({
providerId: "authentik",
callbackURL: "/",
});
} catch (_error) {
toast.error("Failed to sign in. Please try again.");
} finally {
setIsLoading(false);
}
};
if (isPending) {
return null
}
if (isPending) {
return null;
}
if (session?.user) {
return null
}
if (session?.user) {
return null;
}
return (
<div className="min-h-screen flex items-center justify-center bg-background p-4">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<CardTitle className="text-2xl font-bold">Welcome</CardTitle>
<CardDescription>
Sign in to access AI-powered calendar features
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Button onClick={handleSignIn} className="w-full" size="lg" disabled={isLoading}>
{isLoading ? "Signing in..." : "Continue with Authentik"}
</Button>
return (
<div className="min-h-screen flex items-center justify-center bg-background p-4">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<CardTitle className="text-2xl font-bold">Welcome</CardTitle>
<CardDescription>
Sign in to access AI-powered calendar features
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Button
onClick={handleSignIn}
className="w-full"
size="lg"
disabled={isLoading}
>
{isLoading ? "Signing in..." : "Continue with Authentik"}
</Button>
<div className="text-center">
<Link href="/" className="text-sm text-muted-foreground hover:underline">
Continue without signing in
</Link>
</div>
</CardContent>
</Card>
</div>
)
<div className="text-center">
<Link
href="/"
className="text-sm text-muted-foreground hover:underline"
>
Continue without signing in
</Link>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,57 +1,69 @@
"use client"
"use client";
import { signOut, useSession } from "@/lib/auth-client"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { useEffect } from "react"
import { signOut, useSession } from "@/lib/auth-client";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function SignOutPage() {
const { data: session, isPending } = useSession()
const router = useRouter()
const { data: session, isPending } = useSession();
const router = useRouter();
useEffect(() => {
if (!session?.user) {
router.push("/")
}
}, [session, router])
useEffect(() => {
if (!session?.user) {
router.push("/");
}
}, [session, router]);
const handleSignOut = async () => {
await signOut()
router.push("/")
}
const handleSignOut = async () => {
await signOut();
router.push("/");
};
if (isPending || !session?.user) {
return null
}
if (isPending || !session?.user) {
return null;
}
return (
<div className="min-h-screen flex items-center justify-center bg-background p-4">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<CardTitle className="text-2xl font-bold">Sign Out</CardTitle>
<CardDescription>
Are you sure you want to sign out?
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="text-center p-3 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground">Currently signed in as</div>
<div className="font-medium">{session.user?.name || session.user?.email}</div>
</div>
return (
<div className="min-h-screen flex items-center justify-center bg-background p-4">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<CardTitle className="text-2xl font-bold">Sign Out</CardTitle>
<CardDescription>Are you sure you want to sign out?</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="text-center p-3 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground">
Currently signed in as
</div>
<div className="font-medium">
{session.user?.name || session.user?.email}
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<Button onClick={handleSignOut} variant="destructive" className="w-full">
Sign Out
</Button>
<div className="grid grid-cols-2 gap-3">
<Button
onClick={handleSignOut}
variant="destructive"
className="w-full"
>
Sign Out
</Button>
<Button variant="outline" asChild>
<Link href="/">Cancel</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
)
<Button variant="outline" asChild>
<Link href="/">Cancel</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -5,49 +5,56 @@ import { ThemeProvider } from "next-themes";
import { ModeToggle } from "@/components/mode-toggle";
import SignIn from "@/components/sign-in";
import { Toaster } from "@/components/ui/sonner";
import Link from "next/link"
import Link from "next/link";
const geist = Geist({ subsets: ['latin', 'cyrillic'], variable: "--font-geist-sans" })
const geist = Geist({
subsets: ["latin", "cyrillic"],
variable: "--font-geist-sans",
});
const magra = Magra({ subsets: ["latin"], weight: "400", variable: "--font-cascadia-code" })
const magra = Magra({
subsets: ["latin"],
weight: "400",
variable: "--font-cascadia-code",
});
export const metadata: Metadata = {
title: 'Local iCal',
description: 'Local iCal editor for calendar events',
creator: "Dmytro Stanchiev",
}
title: "Local iCal",
description: "Local iCal editor for calendar events",
creator: "Dmytro Stanchiev",
};
export default function RootLayout({
children,
children,
}: Readonly<{
children: React.ReactNode;
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${geist.variable} antialiased min-h-screen flex flex-col dark:text-gray-300 --color-background`}
>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<header className="dark:text-white text-gray-900 px-4 py-3 font-bold flex justify-between items-center-safe">
<Link href={"/"}>
<p className={`${magra.variable}`}>
{metadata.title as string || "iCal PWA"}
</p>
</Link>
<div className="flex flex-row gap-2">
<SignIn />
<ModeToggle />
</div>
</header>
<main className="flex-1 p-4">{children}</main>
<Toaster closeButton richColors />
</ThemeProvider>
</body>
</html>
);
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${geist.variable} antialiased min-h-screen flex flex-col dark:text-gray-300 --color-background`}
>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<header className="dark:text-white text-gray-900 px-4 py-3 font-bold flex justify-between items-center-safe">
<Link href={"/"}>
<p className={`${magra.variable}`}>
{(metadata.title as string) || "iCal PWA"}
</p>
</Link>
<div className="flex flex-row gap-2">
<SignIn />
<ModeToggle />
</div>
</header>
<main className="flex-1 p-4">{children}</main>
<Toaster closeButton richColors />
</ThemeProvider>
</body>
</html>
);
}

View File

@@ -1,309 +1,315 @@
"use client"
"use client";
import { useEffect, useState } from 'react'
import { nanoid } from 'nanoid'
import { useSession } from '@/lib/auth-client'
import { toast } from 'sonner'
import { useEffect, useState } from "react";
import { nanoid } from "nanoid";
import { useSession } from "@/lib/auth-client";
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 {
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 { 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'
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<CalendarEvent[]>([])
const [dialogOpen, setDialogOpen] = useState(false)
const [editingId, setEditingId] = useState<string | null>(null)
const [isDragOver, setIsDragOver] = useState(false)
const [events, setEvents] = useState<CalendarEvent[]>([]);
const [dialogOpen, setDialogOpen] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [isDragOver, setIsDragOver] = useState(false);
// Form fields
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
const [location, setLocation] = useState('')
const [url, setUrl] = useState('')
const [start, setStart] = useState('')
const [end, setEnd] = useState('')
const [allDay, setAllDay] = useState(false)
const [recurrenceRule, setRecurrenceRule] = useState<string | undefined>(undefined)
// Form fields
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [location, setLocation] = useState("");
const [url, setUrl] = useState("");
const [start, setStart] = useState("");
const [end, setEnd] = useState("");
const [allDay, setAllDay] = useState(false);
const [recurrenceRule, setRecurrenceRule] = useState<string | undefined>(
undefined,
);
// AI
const [aiPrompt, setAiPrompt] = useState('')
const [aiLoading, setAiLoading] = useState(false)
const [summary, setSummary] = useState<string | null>(null)
const [summaryUpdated, setSummaryUpdated] = useState<string | null>(null)
// AI
const [aiPrompt, setAiPrompt] = useState("");
const [aiLoading, setAiLoading] = useState(false);
const [summary, setSummary] = useState<string | null>(null);
const [summaryUpdated, setSummaryUpdated] = useState<string | null>(null);
useEffect(() => {
(async () => {
const stored = await getAllEvents()
setEvents(stored)
})()
}, [])
useEffect(() => {
(async () => {
const stored = await getAllEvents();
setEvents(stored);
})();
}, []);
const { data: session, isPending } = useSession()
const { data: session, isPending } = useSession();
const resetForm = () => {
setTitle('')
setDescription('')
setLocation('')
setUrl('')
setStart('')
setEnd('')
setAllDay(false)
setEditingId(null)
setRecurrenceRule(undefined)
}
const resetForm = () => {
setTitle("");
setDescription("");
setLocation("");
setUrl("");
setStart("");
setEnd("");
setAllDay(false);
setEditingId(null);
setRecurrenceRule(undefined);
};
const handleSave = async () => {
const eventData: CalendarEvent = {
id: editingId || nanoid(),
title,
description,
location,
url,
recurrenceRule,
start,
end: end || undefined,
allDay,
createdAt: editingId
? events.find(e => e.id === editingId)?.createdAt
: new Date().toISOString(),
lastModified: new Date().toISOString(),
}
if (editingId) {
await updateEvent(eventData)
setEvents(prev => prev.map(e => (e.id === editingId ? eventData : e)))
} else {
await addEvent(eventData)
setEvents(prev => [...prev, eventData])
}
resetForm()
setDialogOpen(false)
}
const handleSave = async () => {
const eventData: CalendarEvent = {
id: editingId || nanoid(),
title,
description,
location,
url,
recurrenceRule,
start,
end: end || undefined,
allDay,
createdAt: editingId
? events.find((e) => e.id === editingId)?.createdAt
: new Date().toISOString(),
lastModified: new Date().toISOString(),
};
if (editingId) {
await updateEvent(eventData);
setEvents((prev) =>
prev.map((e) => (e.id === editingId ? eventData : e)),
);
} else {
await addEvent(eventData);
setEvents((prev) => [...prev, eventData]);
}
resetForm();
setDialogOpen(false);
};
const handleDelete = async (id: string) => {
await deleteEvent(id)
setEvents(prev => prev.filter(e => e.id !== id))
}
const handleDelete = async (id: string) => {
await deleteEvent(id);
setEvents((prev) => prev.filter((e) => e.id !== id));
};
const handleClearAll = async () => {
await clearEvents()
setEvents([])
}
const handleClearAll = async () => {
await clearEvents();
setEvents([]);
};
const handleImport = async (file: File) => {
const text = await file.text()
const parsed = parseICS(text)
for (const ev of parsed) {
await addEvent(ev)
}
const stored = await getAllEvents()
setEvents(stored)
}
const handleImport = async (file: File) => {
const text = await file.text();
const parsed = parseICS(text);
for (const ev of parsed) {
await addEvent(ev);
}
const stored = await getAllEvents();
setEvents(stored);
};
const handleExport = () => {
const icsData = generateICS(events)
const blob = new Blob([icsData], { type: 'text/calendar' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `icallocal-export-${new Date().toLocaleTimeString()}.ics`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
const handleExport = () => {
const icsData = generateICS(events);
const blob = new Blob([icsData], { type: "text/calendar" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `icallocal-export-${new Date().toLocaleTimeString()}.ics`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// AI Create Event
const handleAiCreate = async () => {
if (!aiPrompt.trim()) return
setAiLoading(true)
// AI Create Event
const handleAiCreate = async () => {
if (!aiPrompt.trim()) return;
setAiLoading(true);
const promise = (): Promise<{ message: string }> => new Promise(async (resolve, reject) => {
try {
const res = await fetch('/api/ai-event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: aiPrompt })
})
const promise = (): Promise<{ message: string }> =>
new Promise(async (resolve, reject) => {
try {
const res = await fetch("/api/ai-event", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: aiPrompt }),
});
if (res.status === 401) {
setAiLoading(false)
reject({
message: 'Please sign in to use AI features.'
})
return
}
if (res.status === 401) {
setAiLoading(false);
reject({
message: "Please sign in to use AI features.",
});
return;
}
const data = await res.json()
const data = await res.json();
if (Array.isArray(data) && data.length > 0) {
if (data.length === 1) {
// Prefill dialog directly (same as before)
const ev = data[0]
setTitle(ev.title || '')
setDescription(ev.description || '')
setLocation(ev.location || '')
setUrl(ev.url || '')
setStart(ev.start || '')
setEnd(ev.end || '')
setAllDay(ev.allDay || false)
setEditingId(null)
setAiPrompt("")
setDialogOpen(true)
setRecurrenceRule(ev.recurrenceRule || undefined)
resolve({
message: 'Event has been created!'
})
if (Array.isArray(data) && data.length > 0) {
if (data.length === 1) {
// Prefill dialog directly (same as before)
const ev = data[0];
setTitle(ev.title || "");
setDescription(ev.description || "");
setLocation(ev.location || "");
setUrl(ev.url || "");
setStart(ev.start || "");
setEnd(ev.end || "");
setAllDay(ev.allDay || false);
setEditingId(null);
setAiPrompt("");
setDialogOpen(true);
setRecurrenceRule(ev.recurrenceRule || undefined);
resolve({
message: "Event has been created!",
});
} else {
// Save them all directly to DB
for (const ev of data) {
const newEvent = {
id: nanoid(),
...ev,
createdAt: new Date().toISOString(),
lastModified: new Date().toISOString(),
};
await addEvent(newEvent);
}
const stored = await getAllEvents();
setEvents(stored);
setAiPrompt("");
setSummary(`Added ${data.length} AI-generated events.`);
setSummaryUpdated(new Date().toLocaleString());
resolve({
message: "Event has been created!",
});
}
} else {
reject({
message: "AI did not return event data.",
});
}
} catch (err) {
console.error(err);
reject({
message: "Error from AI service.",
});
}
});
} else {
// Save them all directly to DB
for (const ev of data) {
const newEvent = {
id: nanoid(),
...ev,
createdAt: new Date().toISOString(),
lastModified: new Date().toISOString(),
}
await addEvent(newEvent)
}
const stored = await getAllEvents()
setEvents(stored)
setAiPrompt("")
setSummary(`Added ${data.length} AI-generated events.`)
setSummaryUpdated(new Date().toLocaleString())
resolve({
message: 'Event has been created!'
})
}
} else {
reject({
message: 'AI did not return event data.'
})
}
} catch (err) {
console.error(err)
reject({
message: 'Error from AI service.'
})
}
})
toast.promise(promise, {
loading: "Generating event...",
success: ({ message }) => {
return message;
},
error: ({ message }) => {
return message;
},
});
toast.promise(promise, {
loading: "Generating event...",
success: ({ message }) => {
return message
},
error: ({ message }) => {
return message
}
})
setAiLoading(false);
};
setAiLoading(false)
}
// AI Summarize Events
const handleAiSummarize = async () => {
if (!events.length) {
setSummary("No events to summarize.");
setSummaryUpdated(new Date().toLocaleString());
return;
}
setAiLoading(true);
try {
const res = await fetch("/api/ai-summary", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ events }),
});
const data = await res.json();
if (data.summary) {
setSummary(data.summary);
setSummaryUpdated(new Date().toLocaleString());
} else {
setSummary("No summary generated.");
setSummaryUpdated(new Date().toLocaleString());
}
} catch {
setSummary("Error summarizing events");
setSummaryUpdated(new Date().toLocaleString());
} finally {
setAiLoading(false);
}
};
// AI Summarize Events
const handleAiSummarize = async () => {
if (!events.length) {
setSummary("No events to summarize.")
setSummaryUpdated(new Date().toLocaleString())
return
}
setAiLoading(true)
try {
const res = await fetch('/api/ai-summary', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ events })
})
const data = await res.json()
if (data.summary) {
setSummary(data.summary)
setSummaryUpdated(new Date().toLocaleString())
} else {
setSummary("No summary generated.")
setSummaryUpdated(new Date().toLocaleString())
}
} catch {
setSummary("Error summarizing events")
setSummaryUpdated(new Date().toLocaleString())
} finally {
setAiLoading(false)
}
}
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);
};
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 (
<DragDropContainer
isDragOver={isDragOver}
setIsDragOver={setIsDragOver}
onImport={handleImport}
>
<AIToolbar
isAuthenticated={!!session?.user}
isPending={isPending}
aiPrompt={aiPrompt}
setAiPrompt={setAiPrompt}
aiLoading={aiLoading}
onAiCreate={handleAiCreate}
onAiSummarize={handleAiSummarize}
summary={summary}
summaryUpdated={summaryUpdated}
/>
return (
<DragDropContainer
isDragOver={isDragOver}
setIsDragOver={setIsDragOver}
onImport={handleImport}
>
<AIToolbar
isAuthenticated={!!session?.user}
isPending={isPending}
aiPrompt={aiPrompt}
setAiPrompt={setAiPrompt}
aiLoading={aiLoading}
onAiCreate={handleAiCreate}
onAiSummarize={handleAiSummarize}
summary={summary}
summaryUpdated={summaryUpdated}
/>
<EventActionsToolbar
events={events}
onAddEvent={() => setDialogOpen(true)}
onImport={handleImport}
onExport={handleExport}
onClearAll={handleClearAll}
/>
<EventActionsToolbar
events={events}
onAddEvent={() => setDialogOpen(true)}
onImport={handleImport}
onExport={handleExport}
onClearAll={handleClearAll}
/>
<EventsList events={events} onEdit={handleEdit} onDelete={handleDelete} />
<EventsList
events={events}
onEdit={handleEdit}
onDelete={handleDelete}
/>
<EventDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
editingId={editingId}
title={title}
setTitle={setTitle}
description={description}
setDescription={setDescription}
location={location}
setLocation={setLocation}
url={url}
setUrl={setUrl}
start={start}
setStart={setStart}
end={end}
setEnd={setEnd}
allDay={allDay}
setAllDay={setAllDay}
recurrenceRule={recurrenceRule}
setRecurrenceRule={setRecurrenceRule}
onSave={handleSave}
onReset={resetForm}
/>
</DragDropContainer>
)
<EventDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
editingId={editingId}
title={title}
setTitle={setTitle}
description={description}
setDescription={setDescription}
location={location}
setLocation={setLocation}
url={url}
setUrl={setUrl}
start={start}
setStart={setStart}
end={end}
setEnd={setEnd}
allDay={allDay}
setAllDay={setAllDay}
recurrenceRule={recurrenceRule}
setRecurrenceRule={setRecurrenceRule}
onSave={handleSave}
onReset={resetForm}
/>
</DragDropContainer>
);
}