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 { 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
|
|
||||||
const sanitizedError = errorMessage
|
|
||||||
? errorMessage.replace(/[<>]/g, '')
|
|
||||||
: 'An authentication error occurred'
|
|
||||||
|
|
||||||
return (<div className="text-center p-3 bg-background rounded-lg">
|
// Sanitize error message to prevent XSS
|
||||||
{sanitizedError}
|
const sanitizedError = errorMessage
|
||||||
</div>)
|
? errorMessage.replace(/[<>]/g, "")
|
||||||
|
: "An authentication error occurred";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="text-center p-3 bg-background rounded-lg">
|
||||||
|
{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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
566
src/app/page.tsx
566
src/app/page.tsx
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user