Files
calvana/pledge-now-pay-later/src/app/dashboard/events/[id]/page.tsx

242 lines
9.1 KiB
TypeScript

"use client"
import { useState, useEffect } from "react"
import { useParams } from "next/navigation"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Dialog, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { formatPence } from "@/lib/utils"
import { Plus, Download, QrCode, ExternalLink, Copy, Check, Loader2, ArrowLeft } from "lucide-react"
import Link from "next/link"
import { QRCodeCanvas } from "@/components/qr-code"
interface QrSourceInfo {
id: string
label: string
code: string
volunteerName: string | null
tableName: string | null
scanCount: number
pledgeCount: number
totalPledged: number
}
export default function EventQRPage() {
const params = useParams()
const eventId = params.id as string
const [qrSources, setQrSources] = useState<QrSourceInfo[]>([])
const [loading, setLoading] = useState(true)
const [showCreate, setShowCreate] = useState(false)
const [copiedCode, setCopiedCode] = useState<string | null>(null)
const [form, setForm] = useState({ label: "", volunteerName: "", tableName: "" })
const [creating, setCreating] = useState(false)
const baseUrl = typeof window !== "undefined" ? window.location.origin : ""
useEffect(() => {
fetch(`/api/events/${eventId}/qr`)
.then((r) => r.json())
.then((data) => {
if (Array.isArray(data)) setQrSources(data)
})
.catch(() => {})
.finally(() => setLoading(false))
}, [eventId])
const copyLink = async (code: string) => {
await navigator.clipboard.writeText(`${baseUrl}/p/${code}`)
setCopiedCode(code)
setTimeout(() => setCopiedCode(null), 2000)
}
const handleCreate = async () => {
setCreating(true)
try {
const res = await fetch(`/api/events/${eventId}/qr`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(form),
})
if (res.ok) {
const qr = await res.json()
setQrSources((prev) => [{ ...qr, scanCount: 0, pledgeCount: 0, totalPledged: 0 }, ...prev])
setShowCreate(false)
setForm({ label: "", volunteerName: "", tableName: "" })
}
} catch {}
setCreating(false)
}
// Auto-generate label
useEffect(() => {
if (form.volunteerName || form.tableName) {
const parts = [form.tableName, form.volunteerName].filter(Boolean)
setForm((f) => ({ ...f, label: parts.join(" - ") }))
}
}, [form.volunteerName, form.tableName])
if (loading) {
return (
<div className="flex items-center justify-center py-20">
<Loader2 className="h-8 w-8 text-trust-blue animate-spin" />
</div>
)
}
const totalScans = qrSources.reduce((s, q) => s + q.scanCount, 0)
const totalPledges = qrSources.reduce((s, q) => s + q.pledgeCount, 0)
const totalAmount = qrSources.reduce((s, q) => s + q.totalPledged, 0)
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<Link
href="/dashboard/events"
className="text-sm text-muted-foreground hover:text-foreground transition-colors inline-flex items-center gap-1 mb-2"
>
<ArrowLeft className="h-3 w-3" /> Back to Events
</Link>
<h1 className="text-3xl font-extrabold text-gray-900">QR Codes</h1>
<p className="text-muted-foreground mt-1">
{qrSources.length} QR code{qrSources.length !== 1 ? "s" : ""} · {totalScans} scans · {totalPledges} pledges · {formatPence(totalAmount)}
</p>
</div>
<Button onClick={() => setShowCreate(true)}>
<Plus className="h-4 w-4 mr-2" /> New QR Code
</Button>
</div>
{/* QR Grid */}
{qrSources.length === 0 ? (
<Card>
<CardContent className="py-12 text-center space-y-4">
<QrCode className="h-12 w-12 text-muted-foreground mx-auto" />
<p className="text-muted-foreground">No QR codes yet. Create one to start collecting pledges!</p>
<Button onClick={() => setShowCreate(true)}>
<Plus className="h-4 w-4 mr-2" /> Create First QR Code
</Button>
</CardContent>
</Card>
) : (
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{qrSources.map((qr) => (
<Card key={qr.id} className="hover:shadow-md transition-shadow">
<CardContent className="pt-6 space-y-4">
{/* QR Code */}
<div className="max-w-[180px] mx-auto bg-white rounded-2xl flex items-center justify-center p-2">
<QRCodeCanvas url={`${baseUrl}/p/${qr.code}`} size={164} />
</div>
<div className="text-center">
<h3 className="font-bold">{qr.label}</h3>
{qr.volunteerName && (
<p className="text-xs text-muted-foreground">Volunteer: {qr.volunteerName}</p>
)}
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-2 text-center text-xs">
<div className="rounded-lg bg-gray-50 p-2">
<p className="font-bold text-sm">{qr.scanCount}</p>
<p className="text-muted-foreground">Scans</p>
</div>
<div className="rounded-lg bg-gray-50 p-2">
<p className="font-bold text-sm">{qr.pledgeCount}</p>
<p className="text-muted-foreground">Pledges</p>
</div>
<div className="rounded-lg bg-gray-50 p-2">
<p className="font-bold text-sm">{formatPence(qr.totalPledged)}</p>
<p className="text-muted-foreground">Total</p>
</div>
</div>
{/* Conversion rate */}
{qr.scanCount > 0 && (
<div className="text-center">
<span className="text-xs text-muted-foreground">
Conversion: <span className="font-semibold text-foreground">{Math.round((qr.pledgeCount / qr.scanCount) * 100)}%</span>
</span>
</div>
)}
{/* Actions */}
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => copyLink(qr.code)}
>
{copiedCode === qr.code ? (
<><Check className="h-3 w-3 mr-1" /> Copied</>
) : (
<><Copy className="h-3 w-3 mr-1" /> Link</>
)}
</Button>
<a href={`/api/events/${eventId}/qr/${qr.id}/download?code=${qr.code}`} download>
<Button variant="outline" size="sm">
<Download className="h-3 w-3 mr-1" /> PNG
</Button>
</a>
<a href={`/p/${qr.code}`} target="_blank">
<Button variant="outline" size="sm">
<ExternalLink className="h-3 w-3" />
</Button>
</a>
</div>
</CardContent>
</Card>
))}
</div>
)}
{/* Create dialog */}
<Dialog open={showCreate} onOpenChange={setShowCreate}>
<DialogHeader>
<DialogTitle>Create QR Code</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Table Name</Label>
<Input
placeholder="e.g. Table 5"
value={form.tableName}
onChange={(e) => setForm((f) => ({ ...f, tableName: e.target.value }))}
/>
</div>
<div className="space-y-2">
<Label>Volunteer Name</Label>
<Input
placeholder="e.g. Ahmed"
value={form.volunteerName}
onChange={(e) => setForm((f) => ({ ...f, volunteerName: e.target.value }))}
/>
</div>
</div>
<div className="space-y-2">
<Label>Label *</Label>
<Input
placeholder="e.g. Table 5 - Ahmed"
value={form.label}
onChange={(e) => setForm((f) => ({ ...f, label: e.target.value }))}
/>
<p className="text-xs text-muted-foreground">Auto-generated from table + volunteer, or enter custom</p>
</div>
<div className="flex gap-3 pt-2">
<Button variant="outline" onClick={() => setShowCreate(false)} className="flex-1">
Cancel
</Button>
<Button onClick={handleCreate} disabled={!form.label || creating} className="flex-1">
{creating ? "Creating..." : "Create QR Code"}
</Button>
</div>
</div>
</Dialog>
</div>
)
}