Files
calvana/pledge-now-pay-later/src/app/dashboard/events/page.tsx
Omair Saleh 0236867c88 feat: remove FPX, add UK charity persona features
- Remove FPX payment rail entirely (Malaysian, not UK)
- Add volunteer portal (/v/[code]) with live pledge tracking
- Add public event page (/e/[slug]) with progress bar + social proof
- Add fundraiser leaderboard (/dashboard/events/[id]/leaderboard)
- Add WhatsApp share buttons on confirmation, bank instructions, volunteer view
- Enhanced Gift Aid UX with +25% bonus display and HMRC declaration text
- Gift Aid report export (HMRC-ready CSV filter)
- Volunteer view link + WhatsApp share on QR code cards
- Updated home page: 4 personas, 3 UK payment rails, 8 features
- Public event API endpoint with privacy-safe donor name truncation
- Volunteer API with stats, conversion rate, auto-refresh
2026-03-03 03:47:18 +08:00

227 lines
8.6 KiB
TypeScript

"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[]>([])
// eslint-disable-next-line @typescript-eslint/no-unused-vars
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>
)
}