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 { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import Link from "next/link" import Link from "next/link";
import { useSearchParams } from "next/navigation" import { useSearchParams } from "next/navigation";
import { Suspense } from "react" import { Suspense } from "react";
function Search() { function Search() {
const searchParams = useSearchParams() const searchParams = useSearchParams();
const errorMessage = searchParams.get('error') const errorMessage = searchParams.get("error");
// Sanitize error message to prevent XSS // Sanitize error message to prevent XSS
const sanitizedError = errorMessage const sanitizedError = errorMessage
? errorMessage.replace(/[<>]/g, '') ? errorMessage.replace(/[<>]/g, "")
: 'An authentication error occurred' : "An authentication error occurred";
return (<div className="text-center p-3 bg-background rounded-lg"> return (
<div className="text-center p-3 bg-background rounded-lg">
{sanitizedError} {sanitizedError}
</div>) </div>
);
} }
export default function AuthErrorPage() { export default function AuthErrorPage() {
@@ -39,5 +41,5 @@ export default function AuthErrorPage() {
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
) );
} }

View File

@@ -1,44 +1,50 @@
"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 (
@@ -51,17 +57,25 @@ export default function SignInPage() {
</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
onClick={handleSignIn}
className="w-full"
size="lg"
disabled={isLoading}
>
{isLoading ? "Signing in..." : "Continue with Authentik"} {isLoading ? "Signing in..." : "Continue with Authentik"}
</Button> </Button>
<div className="text-center"> <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 Continue without signing in
</Link> </Link>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
) );
} }

View File

@@ -1,29 +1,35 @@
"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 (
@@ -31,18 +37,24 @@ export default function SignOutPage() {
<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?
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="text-center p-3 bg-muted rounded-lg"> <div className="text-center p-3 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground">Currently signed in as</div> <div className="text-sm text-muted-foreground">
<div className="font-medium">{session.user?.name || session.user?.email}</div> Currently signed in as
</div>
<div className="font-medium">
{session.user?.name || session.user?.email}
</div>
</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
onClick={handleSignOut}
variant="destructive"
className="w-full"
>
Sign Out Sign Out
</Button> </Button>
@@ -53,5 +65,5 @@ export default function SignOutPage() {
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
) );
} }

View File

@@ -5,17 +5,24 @@ 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,
@@ -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"> <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">

View File

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