feat: add improved pi agent with observatory, dashboard, and pledge-now-pay-later
This commit is contained in:
225
pledge-now-pay-later/src/app/dashboard/events/page.tsx
Normal file
225
pledge-now-pay-later/src/app/dashboard/events/page.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Dialog, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { formatPence } from "@/lib/utils"
|
||||
import { Plus, QrCode, Calendar, MapPin, Target } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
interface EventSummary {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
eventDate: string | null
|
||||
location: string | null
|
||||
goalAmount: number | null
|
||||
status: string
|
||||
pledgeCount: number
|
||||
qrSourceCount: number
|
||||
totalPledged: number
|
||||
totalCollected: number
|
||||
}
|
||||
|
||||
export default function EventsPage() {
|
||||
const [events, setEvents] = useState<EventSummary[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [showCreate, setShowCreate] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/events", { headers: { "x-org-id": "demo" } })
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
if (Array.isArray(data)) setEvents(data)
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [form, setForm] = useState({ name: "", description: "", location: "", eventDate: "", goalAmount: "" })
|
||||
|
||||
const handleCreate = async () => {
|
||||
setCreating(true)
|
||||
try {
|
||||
const res = await fetch("/api/events", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "x-org-id": "demo" },
|
||||
body: JSON.stringify({
|
||||
...form,
|
||||
goalAmount: form.goalAmount ? Math.round(parseFloat(form.goalAmount) * 100) : undefined,
|
||||
eventDate: form.eventDate ? new Date(form.eventDate).toISOString() : undefined,
|
||||
}),
|
||||
})
|
||||
if (res.ok) {
|
||||
const event = await res.json()
|
||||
setEvents((prev) => [{ ...event, pledgeCount: 0, qrSourceCount: 0, totalPledged: 0, totalCollected: 0 }, ...prev])
|
||||
setShowCreate(false)
|
||||
setForm({ name: "", description: "", location: "", eventDate: "", goalAmount: "" })
|
||||
}
|
||||
} catch {
|
||||
// handle error
|
||||
}
|
||||
setCreating(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-extrabold text-gray-900">Events</h1>
|
||||
<p className="text-muted-foreground mt-1">Manage your fundraising events and QR codes</p>
|
||||
</div>
|
||||
<Button onClick={() => setShowCreate(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" /> New Event
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Event cards */}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
{events.map((event) => {
|
||||
const progress = event.goalAmount ? Math.round((event.totalPledged / event.goalAmount) * 100) : 0
|
||||
|
||||
return (
|
||||
<Card key={event.id} className="hover:shadow-md transition-shadow">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-lg">{event.name}</CardTitle>
|
||||
<CardDescription className="flex items-center gap-3 mt-1">
|
||||
{event.eventDate && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="h-3 w-3" />
|
||||
{new Date(event.eventDate).toLocaleDateString("en-GB")}
|
||||
</span>
|
||||
)}
|
||||
{event.location && (
|
||||
<span className="flex items-center gap-1">
|
||||
<MapPin className="h-3 w-3" />
|
||||
{event.location}
|
||||
</span>
|
||||
)}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge variant={event.status === "active" ? "success" : "secondary"}>
|
||||
{event.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<p className="text-xl font-bold">{event.pledgeCount}</p>
|
||||
<p className="text-xs text-muted-foreground">Pledges</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xl font-bold">{formatPence(event.totalPledged)}</p>
|
||||
<p className="text-xs text-muted-foreground">Pledged</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xl font-bold text-success-green">{formatPence(event.totalCollected)}</p>
|
||||
<p className="text-xs text-muted-foreground">Collected</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{event.goalAmount && (
|
||||
<div>
|
||||
<div className="flex justify-between text-xs text-muted-foreground mb-1">
|
||||
<span>{progress}% of goal</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Target className="h-3 w-3" /> {formatPence(event.goalAmount)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 rounded-full bg-gray-100 overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full bg-trust-blue transition-all"
|
||||
style={{ width: `${Math.min(progress, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Link href={`/dashboard/events/${event.id}`} className="flex-1">
|
||||
<Button variant="outline" size="sm" className="w-full">
|
||||
<QrCode className="h-4 w-4 mr-1" /> QR Codes ({event.qrSourceCount})
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href={`/dashboard/pledges?event=${event.id}`} className="flex-1">
|
||||
<Button variant="outline" size="sm" className="w-full">
|
||||
View Pledges
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Create dialog */}
|
||||
<Dialog open={showCreate} onOpenChange={setShowCreate}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Event</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Event Name *</Label>
|
||||
<Input
|
||||
placeholder="e.g. Ramadan Gala 2025"
|
||||
value={form.name}
|
||||
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Description</Label>
|
||||
<Textarea
|
||||
placeholder="Brief description..."
|
||||
value={form.description}
|
||||
onChange={(e) => setForm((f) => ({ ...f, description: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Date</Label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={form.eventDate}
|
||||
onChange={(e) => setForm((f) => ({ ...f, eventDate: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Goal (£)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="50000"
|
||||
value={form.goalAmount}
|
||||
onChange={(e) => setForm((f) => ({ ...f, goalAmount: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Location</Label>
|
||||
<Input
|
||||
placeholder="Venue name and address"
|
||||
value={form.location}
|
||||
onChange={(e) => setForm((f) => ({ ...f, location: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-3 pt-2">
|
||||
<Button variant="outline" onClick={() => setShowCreate(false)} className="flex-1">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleCreate} disabled={!form.name || creating} className="flex-1">
|
||||
{creating ? "Creating..." : "Create Event"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user