diff --git a/src/app/auth/error/page.tsx b/src/app/auth/error/page.tsx
index 6a493a3..e638e58 100644
--- a/src/app/auth/error/page.tsx
+++ b/src/app/auth/error/page.tsx
@@ -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 (
- {sanitizedError}
-
)
+ // Sanitize error message to prevent XSS
+ const sanitizedError = errorMessage
+ ? errorMessage.replace(/[<>]/g, "")
+ : "An authentication error occurred";
+
+ return (
+
+ {sanitizedError}
+
+ );
}
export default function AuthErrorPage() {
- return (
-
-
-
- Error
-
-
-
-
-
-
-
-
-
-
-
- )
+ return (
+
+
+
+ Error
+
+
+
+
+
+
+
+
+
+
+
+ );
}
diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx
index 780a8bd..60b08ec 100644
--- a/src/app/auth/signin/page.tsx
+++ b/src/app/auth/signin/page.tsx
@@ -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 (
-
-
-
- Welcome
-
- Sign in to access AI-powered calendar features
-
-
-
-
+ return (
+
+
+
+ Welcome
+
+ Sign in to access AI-powered calendar features
+
+
+
+
-
-
- Continue without signing in
-
-
-
-
-
- )
+
+
+ Continue without signing in
+
+
+
+
+
+ );
}
diff --git a/src/app/auth/signout/page.tsx b/src/app/auth/signout/page.tsx
index 17fc7a1..02f8636 100644
--- a/src/app/auth/signout/page.tsx
+++ b/src/app/auth/signout/page.tsx
@@ -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 (
-
-
-
- Sign Out
-
- Are you sure you want to sign out?
-
-
-
-
-
Currently signed in as
-
{session.user?.name || session.user?.email}
-
+ return (
+
+
+
+ Sign Out
+ Are you sure you want to sign out?
+
+
+
+
+ Currently signed in as
+
+
+ {session.user?.name || session.user?.email}
+
+
-
-
+
+
-
-
-
-
-
- )
+
+
+
+
+
+ );
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 31963c4..9d2e23a 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -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 (
-
-
-
-
- {children}
-
-
-
-
- );
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ );
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 3fdf1bc..d063951 100644
--- a/src/app/page.tsx
+++ b/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([])
- const [dialogOpen, setDialogOpen] = useState(false)
- const [editingId, setEditingId] = useState(null)
- const [isDragOver, setIsDragOver] = useState(false)
+ const [events, setEvents] = useState([]);
+ const [dialogOpen, setDialogOpen] = useState(false);
+ const [editingId, setEditingId] = useState(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(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(
+ undefined,
+ );
- // AI
- const [aiPrompt, setAiPrompt] = useState('')
- const [aiLoading, setAiLoading] = useState(false)
- const [summary, setSummary] = useState(null)
- const [summaryUpdated, setSummaryUpdated] = useState(null)
+ // AI
+ const [aiPrompt, setAiPrompt] = useState("");
+ const [aiLoading, setAiLoading] = useState(false);
+ const [summary, setSummary] = useState(null);
+ const [summaryUpdated, setSummaryUpdated] = useState(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 (
+
+
- return (
-
-
+ setDialogOpen(true)}
+ onImport={handleImport}
+ onExport={handleExport}
+ onClearAll={handleClearAll}
+ />
- setDialogOpen(true)}
- onImport={handleImport}
- onExport={handleExport}
- onClearAll={handleClearAll}
- />
+
-
-
-
-
- )
+
+
+ );
}