Files
calvana/pledge-now-pay-later/src/app/v/[code]/page.tsx
Omair Saleh fc80399092 brand identity overhaul: match BRAND-IDENTITY.md across all pages
Design system changes (per brand guide):
- ZERO rounded-2xl/3xl remaining (was 131 instances)
- ZERO bg-gradient remaining (was 25) — all solid colors
- ZERO colored shadows (shadow-trust-blue, etc) — flat, no glow
- ZERO backdrop-blur/glass effects — solid backgrounds
- ZERO emoji in logo marks — square P logomark everywhere
- ZERO decorative scale animations (group-hover:scale-105, etc)

Tailwind config:
- Added brand color names: midnight, promise-blue, generosity-gold, fulfilled-green, alert-red, paper
- Kept legacy aliases (trust-blue, etc) for backwards compat
- --radius: 0.75rem → 0.5rem (tighter corners)

CSS:
- Removed glass, glass-dark, card-hover, pulse-ring, bounce-gentle, confetti-fall, scale-in animations
- Kept only purposeful animations: fadeUp, fadeIn, slideDown, shimmer, counter-roll
- --primary tuned to match Promise Blue exactly

Components:
- Button: removed all colored shadows, added 'blue' variant, removed rounded from sizes
- All UI components: rounded-xl/2xl → rounded-lg

Pages updated (41 files):
- Dashboard layout: solid header (no blur), border-l-2 active indicator, midnight logo mark
- Login/Signup: paper bg (no gradient), midnight logo mark, no emoji
- Pledge flow: solid color icons, no gradient progress bars
- All dashboard pages: flat, sharp, editorial
2026-03-03 20:13:22 +08:00

217 lines
7.7 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 { Badge } from "@/components/ui/badge"
import { MessageCircle, Share2, QrCode, TrendingUp, Users, Banknote, Loader2 } from "lucide-react"
interface VolunteerData {
qrSource: {
label: string
volunteerName: string | null
code: string
scanCount: number
}
event: {
name: string
organizationName: string
}
pledges: Array<{
id: string
reference: string
amountPence: number
status: string
donorName: string | null
createdAt: string
giftAid: boolean
}>
stats: {
totalPledges: number
totalPledgedPence: number
totalPaidPence: number
conversionRate: number
}
}
const statusColors: Record<string, "default" | "secondary" | "success" | "warning" | "destructive"> = {
new: "secondary",
initiated: "warning",
paid: "success",
overdue: "destructive",
}
export default function VolunteerPage() {
const params = useParams()
const code = params.code as string
const [data, setData] = useState<VolunteerData | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState("")
useEffect(() => {
fetch(`/api/qr/${code}/volunteer`)
.then((r) => r.json())
.then((d) => {
if (d.error) setError(d.error)
else setData(d)
})
.catch(() => setError("Failed to load"))
.finally(() => setLoading(false))
const interval = setInterval(() => {
fetch(`/api/qr/${code}/volunteer`)
.then((r) => r.json())
.then((d) => { if (!d.error) setData(d) })
.catch(() => {})
}, 15000)
return () => clearInterval(interval)
}, [code])
const handleWhatsAppShare = () => {
if (!data) return
const url = `${window.location.origin}/p/${code}`
const text = `🤲 Pledge to ${data.event.name}!\n\nMake a pledge here — it only takes 15 seconds:\n${url}`
window.open(`https://wa.me/?text=${encodeURIComponent(text)}`, "_blank")
}
const handleShare = async () => {
if (!data) return
const url = `${window.location.origin}/p/${code}`
if (navigator.share) {
await navigator.share({ title: data.event.name, text: `Pledge to ${data.event.name}`, url })
} else {
handleWhatsAppShare()
}
}
const formatPence = (p: number) => `£${(p / 100).toLocaleString("en-GB", { minimumFractionDigits: 0 })}`
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-paper">
<Loader2 className="h-8 w-8 text-trust-blue animate-spin" />
</div>
)
}
if (error || !data) {
return (
<div className="min-h-screen flex items-center justify-center bg-paper p-4">
<div className="text-center space-y-4">
<QrCode className="h-12 w-12 text-muted-foreground mx-auto" />
<h1 className="text-xl font-bold">QR code not found</h1>
<p className="text-muted-foreground">{error}</p>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-paper">
<div className="max-w-lg mx-auto px-4 py-8 space-y-6">
{/* Header */}
<div className="text-center space-y-2">
<p className="text-sm text-muted-foreground">{data.event.organizationName}</p>
<h1 className="text-2xl font-extrabold text-gray-900">{data.event.name}</h1>
<p className="text-lg font-semibold text-trust-blue">
{data.qrSource.volunteerName || data.qrSource.label}&apos;s Pledges
</p>
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-3">
<Card>
<CardContent className="pt-4 pb-3 text-center">
<Users className="h-5 w-5 text-trust-blue mx-auto mb-1" />
<p className="text-2xl font-bold">{data.stats.totalPledges}</p>
<p className="text-xs text-muted-foreground">Pledges</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-4 pb-3 text-center">
<Banknote className="h-5 w-5 text-warm-amber mx-auto mb-1" />
<p className="text-2xl font-bold">{formatPence(data.stats.totalPledgedPence)}</p>
<p className="text-xs text-muted-foreground">Pledged</p>
</CardContent>
</Card>
<Card>
<CardContent className="pt-4 pb-3 text-center">
<TrendingUp className="h-5 w-5 text-success-green mx-auto mb-1" />
<p className="text-2xl font-bold">{formatPence(data.stats.totalPaidPence)}</p>
<p className="text-xs text-muted-foreground">Paid</p>
</CardContent>
</Card>
</div>
{/* Progress bar */}
{data.stats.totalPledgedPence > 0 && (
<div>
<div className="flex justify-between text-xs text-muted-foreground mb-1">
<span>Collection progress</span>
<span>{Math.round((data.stats.totalPaidPence / data.stats.totalPledgedPence) * 100)}%</span>
</div>
<div className="h-3 rounded-full bg-gray-100 overflow-hidden">
<div
className="h-full rounded-full bg-promise-blue transition-all duration-1000"
style={{ width: `${Math.round((data.stats.totalPaidPence / data.stats.totalPledgedPence) * 100)}%` }}
/>
</div>
</div>
)}
{/* Share buttons */}
<div className="flex gap-2">
<Button onClick={handleWhatsAppShare} className="flex-1 bg-[#25D366] hover:bg-[#20BD5A] text-white">
<MessageCircle className="h-4 w-4 mr-2" /> Share on WhatsApp
</Button>
<Button onClick={handleShare} variant="outline" className="flex-1">
<Share2 className="h-4 w-4 mr-2" /> Share Link
</Button>
</div>
{/* Pledge list */}
<div className="space-y-2">
<h2 className="font-bold text-sm text-muted-foreground uppercase tracking-wider">
My Pledges ({data.pledges.length})
</h2>
{data.pledges.length === 0 ? (
<Card>
<CardContent className="py-8 text-center">
<p className="text-muted-foreground">No pledges yet. Share your link to start collecting!</p>
</CardContent>
</Card>
) : (
data.pledges.map((p) => (
<Card key={p.id} className="hover:shadow-sm transition-shadow">
<CardContent className="py-3 px-4">
<div className="flex items-center justify-between">
<div>
<p className="font-semibold text-sm">{p.donorName || "Anonymous"}</p>
<p className="text-xs text-muted-foreground">
{new Date(p.createdAt).toLocaleDateString("en-GB", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" })}
{p.giftAid && " · 🎁 Gift Aid"}
</p>
</div>
<div className="text-right flex items-center gap-2">
<span className="font-bold">{formatPence(p.amountPence)}</span>
<Badge variant={statusColors[p.status] || "secondary"} className="text-xs">
{p.status === "paid" ? "Paid ✓" : p.status}
</Badge>
</div>
</div>
</CardContent>
</Card>
))
)}
</div>
{/* Auto-refresh indicator */}
<p className="text-center text-xs text-muted-foreground">
Updates automatically every 15 seconds
</p>
</div>
</div>
)
}