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

View File

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

View File

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

View File

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

View File

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