summary header + style polish

This commit is contained in:
2025-08-15 21:55:50 -04:00
parent 3ee7be9110
commit b4c59bbde0
2 changed files with 159 additions and 62 deletions

View File

@@ -5,6 +5,7 @@ import { nanoid } from 'nanoid'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Card } from '@/components/ui/card'
import { addEvent, deleteEvent, getAllEvents, clearEvents, getDB } from '@/lib/db' import { addEvent, deleteEvent, getAllEvents, clearEvents, getDB } from '@/lib/db'
import { parseICS, generateICS } from '@/lib/ical' import { parseICS, generateICS } from '@/lib/ical'
@@ -28,6 +29,8 @@ export default function HomePage() {
// 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 [summaryUpdated, setSummaryUpdated] = useState<string | null>(null)
useEffect(() => { useEffect(() => {
(async () => { (async () => {
@@ -62,7 +65,6 @@ export default function HomePage() {
: new Date().toISOString(), : new Date().toISOString(),
lastModified: new Date().toISOString(), lastModified: new Date().toISOString(),
} }
if (editingId) { if (editingId) {
const db = await getDB() const db = await getDB()
if (db) { if (db) {
@@ -110,15 +112,9 @@ export default function HomePage() {
URL.revokeObjectURL(url) URL.revokeObjectURL(url)
} }
// Drag & drop // Drag-and-drop
const handleDragOver = (e: React.DragEvent) => { const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); setIsDragOver(true) }
e.preventDefault() const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false) }
setIsDragOver(true)
}
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault()
setIsDragOver(false)
}
const handleDrop = (e: React.DragEvent) => { const handleDrop = (e: React.DragEvent) => {
e.preventDefault() e.preventDefault()
setIsDragOver(false) setIsDragOver(false)
@@ -132,7 +128,7 @@ export default function HomePage() {
} }
} }
// --- AI CREATE --- // AI Create Event
const handleAiCreate = async () => { const handleAiCreate = async () => {
if (!aiPrompt.trim()) return if (!aiPrompt.trim()) return
setAiLoading(true) setAiLoading(true)
@@ -156,17 +152,18 @@ export default function HomePage() {
} else { } else {
alert('AI could not parse event.') alert('AI could not parse event.')
} }
} catch (err) { } catch {
console.error(err) alert('Error creating event')
alert('AI request error')
} finally { } finally {
setAiLoading(false) setAiLoading(false)
} }
} }
// AI Summarize Events
const handleAiSummarize = async () => { const handleAiSummarize = async () => {
if (events.length === 0) { if (!events.length) {
alert("No events to summarize") setSummary("No events to summarize.")
setSummaryUpdated(new Date().toLocaleString())
return return
} }
setAiLoading(true) setAiLoading(true)
@@ -178,30 +175,30 @@ export default function HomePage() {
}) })
const data = await res.json() const data = await res.json()
if (data.summary) { if (data.summary) {
alert(data.summary) setSummary(data.summary)
setSummaryUpdated(new Date().toLocaleString())
} else { } else {
alert('No summary generated.') setSummary("No summary generated.")
setSummaryUpdated(new Date().toLocaleString())
} }
} catch (err) { } catch {
console.error(err) setSummary("Error summarizing events")
alert('Error summarizing events') setSummaryUpdated(new Date().toLocaleString())
} finally { } finally {
setAiLoading(false) setAiLoading(false)
} }
} }
return ( return (
<div <div onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={`p-4 min-h-[80vh] rounded border-2 border-dashed transition ${isDragOver ? 'border-blue-500 bg-blue-50' : 'border-gray-300' className={`p-4 min-h-[80vh] rounded border-2 border-dashed transition ${isDragOver ? 'border-blue-500 bg-blue-50' : 'border-gray-300'
}`} }`}
> >
<div className="flex gap-2 mb-4"> {/* AI Toolbar */}
<input <div className="flex flex-wrap gap-2 mb-4 items-center">
className="flex-1 border p-2 rounded" <Input
placeholder='Describe an event for AI to create' className="flex-1"
placeholder='Describe event for AI to create'
value={aiPrompt} value={aiPrompt}
onChange={e => setAiPrompt(e.target.value)} onChange={e => setAiPrompt(e.target.value)}
/> />
@@ -209,47 +206,49 @@ export default function HomePage() {
{aiLoading ? 'Thinking...' : 'AI Create'} {aiLoading ? 'Thinking...' : 'AI Create'}
</Button> </Button>
<Button variant="secondary" onClick={handleAiSummarize} disabled={aiLoading}> <Button variant="secondary" onClick={handleAiSummarize} disabled={aiLoading}>
AI Summarize {aiLoading ? 'Summarizing...' : 'AI Summarize'}
</Button> </Button>
</div> </div>
<div className="flex gap-2 flex-wrap mb-4"> {/* Summary Panel */}
{summary && (
<Card className="p-4 mb-4 bg-gray-50 border border-gray-200">
<div className="text-sm text-gray-500 mb-1">
Summary updated {summaryUpdated}
</div>
<div>{summary}</div>
</Card>
)}
{/* Control Toolbar */}
<div className="flex flex-wrap gap-2 mb-4">
<Button onClick={() => setDialogOpen(true)}>Add Event</Button> <Button onClick={() => setDialogOpen(true)}>Add Event</Button>
{events.length > 0 && ( {events.length > 0 && (
<> <>
<Button variant="secondary" onClick={handleExport}> <Button variant="secondary" onClick={handleExport}>Export .ics</Button>
Export .ics <Button variant="destructive" onClick={handleClearAll}>Clear All</Button>
</Button>
<Button variant="destructive" onClick={handleClearAll}>
Clear All
</Button>
</> </>
)} )}
<label className="cursor-pointer"> <label className="cursor-pointer">
<span className="px-3 py-2 bg-blue-500 text-white rounded">Import .ics</span> <span className="px-3 py-2 bg-blue-500 text-white rounded">Import .ics</span>
<input <input type="file" accept=".ics" hidden onChange={e => {
type="file" if (e.target.files?.length) handleImport(e.target.files[0])
accept=".ics" }} />
hidden
onChange={e => {
if (e.target.files?.length) {
handleImport(e.target.files[0])
}
}}
/>
</label> </label>
</div> </div>
{events.length === 0 && <p className="text-gray-500 italic">No events yet.</p>} {/* Event List */}
{events.length === 0 && <p className="text-gray-500 italic">No events yet</p>}
<ul className="space-y-2"> <ul className="space-y-2">
{events.map(ev => ( {events.map(ev => (
<li key={ev.id} className="border p-2 rounded bg-white shadow-sm flex justify-between"> <li key={ev.id} className="p-3 border rounded flex justify-between items-start">
<div> <div>
<strong>{ev.title}</strong> {ev.allDay <div className="font-semibold">{ev.title}</div>
? ev.start.split('T')[0] <div className="text-sm text-gray-500">
: new Date(ev.start).toLocaleString()} {ev.allDay ? ev.start.split('T')[0] : new Date(ev.start).toLocaleString()}
{ev.location && <div className="text-sm text-gray-500">{ev.location}</div>} {ev.location && <span> @ {ev.location}</span>}
</div>
{ev.description && <div className="text-sm mt-1">{ev.description}</div>}
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button size="sm" onClick={() => { <Button size="sm" onClick={() => {
@@ -269,26 +268,32 @@ export default function HomePage() {
))} ))}
</ul> </ul>
{/* Add/Edit Dialog */}
<Dialog open={dialogOpen} onOpenChange={val => { if (!val) resetForm(); setDialogOpen(val) }}> <Dialog open={dialogOpen} onOpenChange={val => { if (!val) resetForm(); setDialogOpen(val) }}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>{editingId ? 'Edit Event' : 'Add Event'}</DialogTitle> <DialogTitle>{editingId ? 'Edit Event' : 'Add Event'}</DialogTitle>
</DialogHeader> </DialogHeader>
<Input placeholder="Title" value={title} onChange={e => setTitle(e.target.value)} /> <Input placeholder="Title" value={title} onChange={e => setTitle(e.target.value)} />
<textarea className="border p-2 rounded w-full" placeholder="Description" value={description} onChange={e => setDescription(e.target.value)}></textarea> <textarea className="border rounded p-2 w-full" placeholder="Description"
value={description} onChange={e => setDescription(e.target.value)} />
<Input placeholder="Location" value={location} onChange={e => setLocation(e.target.value)} /> <Input placeholder="Location" value={location} onChange={e => setLocation(e.target.value)} />
<Input placeholder="URL" value={url} onChange={e => setUrl(e.target.value)} /> <Input placeholder="URL" value={url} onChange={e => setUrl(e.target.value)} />
<label className="flex items-center gap-2 mt-2"> <label className="flex items-center gap-2 mt-2">
<input type="checkbox" checked={allDay} onChange={e => setAllDay(e.target.checked)} /> <input type="checkbox" checked={allDay} onChange={e => setAllDay(e.target.checked)} />
All day event All day event
</label> </label>
{!allDay ? <> {!allDay ? (
<>
<Input type="datetime-local" value={start} onChange={e => setStart(e.target.value)} /> <Input type="datetime-local" value={start} onChange={e => setStart(e.target.value)} />
<Input type="datetime-local" value={end} onChange={e => setEnd(e.target.value)} /> <Input type="datetime-local" value={end} onChange={e => setEnd(e.target.value)} />
</> : <> </>
) : (
<>
<Input type="date" value={start ? start.split('T')[0] : ''} onChange={e => setStart(e.target.value)} /> <Input type="date" value={start ? start.split('T')[0] : ''} onChange={e => setStart(e.target.value)} />
<Input type="date" value={end ? end.split('T')[0] : ''} onChange={e => setEnd(e.target.value)} /> <Input type="date" value={end ? end.split('T')[0] : ''} onChange={e => setEnd(e.target.value)} />
</>} </>
)}
<DialogFooter> <DialogFooter>
<Button onClick={handleSave}>{editingId ? 'Update' : 'Save'}</Button> <Button onClick={handleSave}>{editingId ? 'Update' : 'Save'}</Button>
</DialogFooter> </DialogFooter>

View File

@@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}