summary header + style polish
This commit is contained in:
121
src/app/page.tsx
121
src/app/page.tsx
@@ -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>
|
||||||
|
|||||||
92
src/components/ui/card.tsx
Normal file
92
src/components/ui/card.tsx
Normal 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,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user