diff --git a/src/app/page.tsx b/src/app/page.tsx index 93bfed3..9ba797a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,26 +1,25 @@ "use client"; -import { useEffect, useState } from "react"; import { nanoid } from "nanoid"; -import { useSession } from "@/lib/auth-client"; +import { useEffect, useState } from "react"; import { toast } from "sonner"; - +import { AIToolbar } from "@/components/ai-toolbar"; +import { DragDropContainer } from "@/components/drag-drop-container"; +import { EventActionsToolbar } from "@/components/event-actions-toolbar"; +import { EventDialog } from "@/components/event-dialog"; +import { EventsList } from "@/components/events-list"; +import { useSession } from "@/lib/auth-client"; +import { IMAGE_MIME_TYPES, MAX_IMAGE_SIZE_BYTES } from "@/lib/constants"; import { saveEvent as addEvent, + clearEvents, deleteEvent, getEvents as getAllEvents, - clearEvents, updateEvent, } from "@/lib/events-db"; -import { parseICS, generateICS } from "@/lib/ical"; +import { generateICS, parseICS } 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"; - const fileToBase64 = (file: File): Promise => new Promise((resolve, reject) => { const reader = new FileReader(); @@ -29,6 +28,16 @@ const fileToBase64 = (file: File): Promise => reader.readAsDataURL(file); }); +const validateImageFile = (file: File): string | null => { + if (!IMAGE_MIME_TYPES.includes(file.type)) { + return "Only PNG, JPEG, and WebP images are supported."; + } + if (file.size > MAX_IMAGE_SIZE_BYTES) { + return "Image must be less than 10MB."; + } + return null; +}; + export default function HomePage() { const [events, setEvents] = useState([]); const [dialogOpen, setDialogOpen] = useState(false); @@ -79,6 +88,11 @@ export default function HomePage() { }; const handleImageSelect = async (file: File) => { + const error = validateImageFile(file); + if (error) { + toast.error(error); + return; + } const base64 = await fileToBase64(file); setImageBase64(base64); setImagePreview(URL.createObjectURL(file)); @@ -154,95 +168,85 @@ export default function HomePage() { URL.revokeObjectURL(url); }; - // AI Create Event + const populateEventForm = (ev: CalendarEvent) => { + setTitle(ev.title || ""); + setDescription(ev.description || ""); + setLocation(ev.location || ""); + setUrl(ev.url || ""); + setStart(ev.start || ""); + setEnd(ev.end || ""); + setAllDay(ev.allDay || false); + setEditingId(null); + setRecurrenceRule(ev.recurrenceRule || undefined); + }; + + const persistAiEvents = async (data: CalendarEvent[]) => { + for (const ev of data) { + const { id: _existingId, ...rest } = ev; + const newEvent = { + id: nanoid(), + ...rest, + createdAt: new Date().toISOString(), + lastModified: new Date().toISOString(), + }; + await addEvent(newEvent); + } + const stored = await getAllEvents(); + setEvents(stored); + }; + + const sendAiRequest = async (): Promise => { + const res = await fetch("/api/ai-event", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + prompt: aiPrompt, + imageBase64: imageBase64 || undefined, + }), + }); + + if (res.status === 401) { + throw new Error("Please sign in to use AI features."); + } + + const data = await res.json(); + + if (!Array.isArray(data) || data.length === 0) { + throw new Error("AI did not return event data."); + } + + return data; + }; + const handleAiCreate = async () => { if (!aiPrompt.trim() && !imageBase64) 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, - imageBase64: imageBase64 || undefined, - }), - }); + const promise = async (): Promise<{ message: string }> => { + const data = await sendAiRequest(); - if (res.status === 401) { - setAiLoading(false); - reject({ - message: "Please sign in to use AI features.", - }); - return; - } + if (data.length === 1) { + populateEventForm(data[0]); + setAiPrompt(""); + setDialogOpen(true); + handleImageClear(); + return { message: "Event has been created!" }; + } - const data = await res.json(); - - if (Array.isArray(data) && data.length > 0) { - if (data.length === 1) { - 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); - handleImageClear(); - resolve({ - message: "Event has been created!", - }); - } else { - 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()); - handleImageClear(); - resolve({ - message: "Events have been created!", - }); - } - } else { - reject({ - message: "AI did not return event data.", - }); - } - } catch (err) { - console.error(err); - reject({ - message: "Error from AI service.", - }); - } - }); + await persistAiEvents(data); + setAiPrompt(""); + setSummary(`Added ${data.length} AI-generated events.`); + setSummaryUpdated(new Date().toLocaleString()); + handleImageClear(); + return { message: "Events have been created!" }; + }; toast.promise(promise, { loading: "Generating event...", - success: ({ message }) => { - return message; - }, - error: ({ message }) => { - return message; - }, + success: ({ message }) => message, + error: ({ message }) => message, + finally: () => setAiLoading(false), }); - - setAiLoading(false); }; // AI Summarize Events