style(app): standardize app page file formatting
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
566
src/app/page.tsx
566
src/app/page.tsx
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user