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,23 +1,25 @@
"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')
const searchParams = useSearchParams();
const errorMessage = searchParams.get("error");
// Sanitize error message to prevent XSS
const sanitizedError = errorMessage
? errorMessage.replace(/[<>]/g, '')
: 'An authentication error occurred'
? errorMessage.replace(/[<>]/g, "")
: "An authentication error occurred";
return (<div className="text-center p-3 bg-background rounded-lg">
return (
<div className="text-center p-3 bg-background rounded-lg">
{sanitizedError}
</div>)
</div>
);
}
export default function AuthErrorPage() {
@@ -39,5 +41,5 @@ export default function AuthErrorPage() {
</CardContent>
</Card>
</div>
)
);
}

View File

@@ -1,44 +1,50 @@
"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("/")
router.push("/");
}
}, [session, router])
}, [session, router]);
const handleSignIn = async () => {
setIsLoading(true)
setIsLoading(true);
try {
await signIn.oauth2({
providerId: "authentik",
callbackURL: "/",
})
});
} catch (_error) {
toast.error("Failed to sign in. Please try again.")
toast.error("Failed to sign in. Please try again.");
} finally {
setIsLoading(false)
}
setIsLoading(false);
}
};
if (isPending) {
return null
return null;
}
if (session?.user) {
return null
return null;
}
return (
@@ -51,17 +57,25 @@ export default function SignInPage() {
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Button onClick={handleSignIn} className="w-full" size="lg" disabled={isLoading}>
<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">
<Link
href="/"
className="text-sm text-muted-foreground hover:underline"
>
Continue without signing in
</Link>
</div>
</CardContent>
</Card>
</div>
)
);
}

View File

@@ -1,29 +1,35 @@
"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("/")
router.push("/");
}
}, [session, router])
}, [session, router]);
const handleSignOut = async () => {
await signOut()
router.push("/")
}
await signOut();
router.push("/");
};
if (isPending || !session?.user) {
return null
return null;
}
return (
@@ -31,18 +37,24 @@ export default function SignOutPage() {
<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>
<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 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">
<Button
onClick={handleSignOut}
variant="destructive"
className="w-full"
>
Sign Out
</Button>
@@ -53,5 +65,5 @@ export default function SignOutPage() {
</CardContent>
</Card>
</div>
)
);
}

View File

@@ -5,17 +5,24 @@ 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',
title: "Local iCal",
description: "Local iCal editor for calendar events",
creator: "Dmytro Stanchiev",
}
};
export default function RootLayout({
children,
@@ -36,7 +43,7 @@ export default function RootLayout({
<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"}
{(metadata.title as string) || "iCal PWA"}
</p>
</Link>
<div className="flex flex-row gap-2">

View File

@@ -1,62 +1,70 @@
"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)
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)
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)
})()
}, [])
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)
}
setTitle("");
setDescription("");
setLocation("");
setUrl("");
setStart("");
setEnd("");
setAllDay(false);
setEditingId(null);
setRecurrenceRule(undefined);
};
const handleSave = async () => {
const eventData: CalendarEvent = {
@@ -70,96 +78,98 @@ export default function HomePage() {
end: end || undefined,
allDay,
createdAt: editingId
? events.find(e => e.id === editingId)?.createdAt
? 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)))
await updateEvent(eventData);
setEvents((prev) =>
prev.map((e) => (e.id === editingId ? eventData : e)),
);
} else {
await addEvent(eventData)
setEvents(prev => [...prev, eventData])
}
resetForm()
setDialogOpen(false)
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))
}
await deleteEvent(id);
setEvents((prev) => prev.filter((e) => e.id !== id));
};
const handleClearAll = async () => {
await clearEvents()
setEvents([])
}
await clearEvents();
setEvents([]);
};
const handleImport = async (file: File) => {
const text = await file.text()
const parsed = parseICS(text)
const text = await file.text();
const parsed = parseICS(text);
for (const ev of parsed) {
await addEvent(ev)
}
const stored = await getAllEvents()
setEvents(stored)
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 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)
if (!aiPrompt.trim()) return;
setAiLoading(true);
const promise = (): Promise<{ message: string }> => new Promise(async (resolve, reject) => {
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 res = await fetch("/api/ai-event", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: aiPrompt }),
});
if (res.status === 401) {
setAiLoading(false)
setAiLoading(false);
reject({
message: 'Please sign in to use AI features.'
})
return
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)
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!'
})
message: "Event has been created!",
});
} else {
// Save them all directly to DB
for (const ev of data) {
@@ -168,86 +178,86 @@ export default function HomePage() {
...ev,
createdAt: new Date().toISOString(),
lastModified: new Date().toISOString(),
};
await addEvent(newEvent);
}
await addEvent(newEvent)
}
const stored = await getAllEvents()
setEvents(stored)
setAiPrompt("")
setSummary(`Added ${data.length} AI-generated events.`)
setSummaryUpdated(new Date().toLocaleString())
const stored = await getAllEvents();
setEvents(stored);
setAiPrompt("");
setSummary(`Added ${data.length} AI-generated events.`);
setSummaryUpdated(new Date().toLocaleString());
resolve({
message: 'Event has been created!'
})
message: "Event has been created!",
});
}
} else {
reject({
message: 'AI did not return event data.'
})
message: "AI did not return event data.",
});
}
} catch (err) {
console.error(err)
console.error(err);
reject({
message: 'Error from AI service.'
})
message: "Error from AI service.",
});
}
})
});
toast.promise(promise, {
loading: "Generating event...",
success: ({ message }) => {
return message
return message;
},
error: ({ message }) => {
return 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
setSummary("No events to summarize.");
setSummaryUpdated(new Date().toLocaleString());
return;
}
setAiLoading(true)
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()
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())
setSummary(data.summary);
setSummaryUpdated(new Date().toLocaleString());
} else {
setSummary("No summary generated.")
setSummaryUpdated(new Date().toLocaleString())
setSummary("No summary generated.");
setSummaryUpdated(new Date().toLocaleString());
}
} catch {
setSummary("Error summarizing events")
setSummaryUpdated(new Date().toLocaleString())
setSummary("Error summarizing events");
setSummaryUpdated(new Date().toLocaleString());
} finally {
setAiLoading(false)
}
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)
}
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
@@ -275,11 +285,7 @@ export default function HomePage() {
onClearAll={handleClearAll}
/>
<EventsList
events={events}
onEdit={handleEdit}
onDelete={handleDelete}
/>
<EventsList events={events} onEdit={handleEdit} onDelete={handleDelete} />
<EventDialog
open={dialogOpen}
@@ -305,5 +311,5 @@ export default function HomePage() {
onReset={resetForm}
/>
</DragDropContainer>
)
);
}