Files
calvana/pledge-now-pay-later/src/app/dashboard/collect/page.tsx
Omair Saleh b6384da417 Embed mini widget + prominent back button
Embed mode (?embed=1 or iframe detection):
- Shows sleek mini card (Make a Pledge) instead of full step 1
- 160px at rest, expands to 700px when user starts the flow
- postMessage resize signal for parent iframe auto-height
- Powered-by footer

Back button:
- Moved from hidden bottom bar to fixed top navigation bar
- ChevronLeft + "Back" text, always visible during backable steps
- Org name centered in header, step label on right
- Progress bar integrated into top bar

Embed code updated:
- iframe starts at height=160 (mini widget height)
- Includes resize listener script for auto-expansion
2026-03-05 18:06:08 +08:00

906 lines
48 KiB
TypeScript

"use client"
import { useState, useEffect } from "react"
import { formatPence } from "@/lib/utils"
import {
Plus, Copy, Check, Loader2, MessageCircle, Share2, Mail,
Download, Trophy, Link2,
ArrowRight, QrCode as QrCodeIcon, Code2,
Building2, CreditCard, Globe
} from "lucide-react"
import Image from "next/image"
import Link from "next/link"
import { QRCodeCanvas } from "@/components/qr-code"
/**
* /dashboard/collect — "I want to share my link"
*
* FUNDAMENTAL REDESIGN:
*
* The primary object is the LINK. Not the appeal.
*
* Creating a link = one flow that handles:
* 1. What you're raising for (appeal — created behind the scenes)
* 2. How donors pay (bank / external platform / card)
* 3. Link name
*
* Every link card shows:
* - Payment method badge (so the user always knows how THIS link works)
* - 3 sharing tabs: Link, QR Code, Website Widget
* - Stats
*
* The widget is NOT a separate concept — it's just another way to share
* the same link. Copy the embed code, paste on your website.
*/
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
paymentMode?: string; externalPlatform?: string; externalUrl?: string
}
interface SourceInfo {
id: string; label: string; code: string
volunteerName: string | null; tableName: string | null
scanCount: number; pledgeCount: number; totalPledged: number; totalCollected?: number
}
const PLATFORM_LABELS: Record<string, string> = {
justgiving: "JustGiving", launchgood: "LaunchGood", gofundme: "GoFundMe",
enthuse: "Enthuse", other: "External link"
}
export default function CollectPage() {
const [events, setEvents] = useState<EventSummary[]>([])
const [sources, setSources] = useState<SourceInfo[]>([])
const [loading, setLoading] = useState(true)
const [copiedCode, setCopiedCode] = useState<string | null>(null)
const [copiedEmbed, setCopiedEmbed] = useState<string | null>(null)
const [expandedLink, setExpandedLink] = useState<string | null>(null)
const [shareTab, setShareTab] = useState<Record<string, "link" | "qr" | "embed">>({})
// Unified creation flow
const [showCreate, setShowCreate] = useState(false)
const [createStep, setCreateStep] = useState<"name" | "payment" | "link">("name")
const [newAppealName, setNewAppealName] = useState("")
const [newPaymentMode, setNewPaymentMode] = useState<"bank" | "external" | "card">("bank")
const [newPlatform, setNewPlatform] = useState("")
const [newExternalUrl, setNewExternalUrl] = useState("")
const [newLinkName, setNewLinkName] = useState("Main link")
const [creating, setCreating] = useState(false)
// Quick add link (to existing appeal)
const [showQuickAdd, setShowQuickAdd] = useState(false)
const [quickLinkName, setQuickLinkName] = useState("")
const [quickCreating, setQuickCreating] = useState(false)
const [hasStripe, setHasStripe] = useState(false)
const baseUrl = typeof window !== "undefined" ? window.location.origin : ""
// Load everything
useEffect(() => {
Promise.all([
fetch("/api/events").then(r => r.json()),
fetch("/api/settings").then(r => r.json()).catch(() => ({})),
]).then(([evData, settingsData]) => {
if (Array.isArray(evData)) {
setEvents(evData)
// Load sources for all events
const allSourcePromises = evData.map((ev: EventSummary) =>
fetch(`/api/events/${ev.id}/qr`).then(r => r.json()).catch(() => [])
)
Promise.all(allSourcePromises).then(results => {
const allSources: (SourceInfo & { _eventId: string; _eventName: string; _paymentMode: string; _externalPlatform: string | null })[] = []
results.forEach((srcs, i) => {
if (Array.isArray(srcs)) {
srcs.forEach((s: SourceInfo) => {
allSources.push({
...s,
_eventId: evData[i].id,
_eventName: evData[i].name,
_paymentMode: evData[i].paymentMode || "self",
_externalPlatform: evData[i].externalPlatform || null,
})
})
}
})
setSources(allSources)
})
}
if (settingsData.stripeSecretKey) setHasStripe(true)
}).catch(() => {}).finally(() => setLoading(false))
}, [])
// Quick-add: which appeal does the new link belong to?
// Single appeal → auto-select. Multiple → user picks.
const [quickAddEventId, setQuickAddEventId] = useState<string | null>(null)
const activeEvent = events.length === 1
? events[0]
: events.find(e => e.id === quickAddEventId) || events[0]
// ── Actions ──
const copyLink = async (code: string) => {
await navigator.clipboard.writeText(`${baseUrl}/p/${code}`)
setCopiedCode(code); setTimeout(() => setCopiedCode(null), 2000)
}
const copyEmbed = async (code: string) => {
const snippet = `<iframe src="${baseUrl}/p/${code}?embed=1" width="100%" height="160" style="border:none;max-width:480px;" title="Make a Pledge"></iframe>\n<script>window.addEventListener("message",e=>{if(e.data?.type==="pnpl-resize")document.querySelector('iframe[src*="${code}"]').style.height=e.data.height+"px"});<\/script>`
await navigator.clipboard.writeText(snippet)
setCopiedEmbed(code); setTimeout(() => setCopiedEmbed(null), 2000)
}
const shareWhatsApp = (code: string, label: string) => {
window.open(`https://wa.me/?text=${encodeURIComponent(`Assalamu Alaikum! Please pledge here 🤲\n\n${label}\n${baseUrl}/p/${code}`)}`, "_blank")
}
const shareEmail = (code: string, label: string) => {
window.open(`mailto:?subject=${encodeURIComponent(`Pledge: ${label}`)}&body=${encodeURIComponent(`Please pledge here:\n\n${baseUrl}/p/${code}`)}`)
}
const shareNative = (code: string, label: string) => {
if (navigator.share) navigator.share({ title: label, text: `Pledge here: ${baseUrl}/p/${code}`, url: `${baseUrl}/p/${code}` })
else copyLink(code)
}
// Unified create: makes appeal + first link in one go
const handleCreate = async () => {
if (!newAppealName.trim()) return
setCreating(true)
try {
// Create the appeal
const appealRes = await fetch("/api/events", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: newAppealName.trim(),
paymentMode: newPaymentMode === "card" ? "self" : newPaymentMode === "external" ? "external" : "self",
externalPlatform: newPaymentMode === "external" ? (newPlatform || "other") : undefined,
externalUrl: newPaymentMode === "external" ? newExternalUrl : undefined,
}),
})
if (!appealRes.ok) throw new Error("Failed")
const appeal = await appealRes.json()
// Create the first link
const linkRes = await fetch(`/api/events/${appeal.id}/qr`, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ label: newLinkName.trim() || "Main link" }),
})
if (linkRes.ok) {
const src = await linkRes.json()
const newEvent = { ...appeal, pledgeCount: 0, qrSourceCount: 1, totalPledged: 0, totalCollected: 0, paymentMode: appeal.paymentMode, externalPlatform: appeal.externalPlatform, externalUrl: appeal.externalUrl }
setEvents(prev => [newEvent, ...prev])
const newSource = {
...src, scanCount: 0, pledgeCount: 0, totalPledged: 0, totalCollected: 0,
_eventId: appeal.id, _eventName: appeal.name,
_paymentMode: appeal.paymentMode || "self",
_externalPlatform: appeal.externalPlatform || null,
}
setSources(prev => [newSource, ...prev])
setExpandedLink(src.code)
}
// Reset
setShowCreate(false); setCreateStep("name")
setNewAppealName(""); setNewPaymentMode("bank"); setNewPlatform(""); setNewExternalUrl(""); setNewLinkName("Main link")
} catch { /* */ }
setCreating(false)
}
// Quick add link to existing appeal
const handleQuickAdd = async () => {
if (!quickLinkName.trim() || !activeEvent) return
setQuickCreating(true)
try {
const res = await fetch(`/api/events/${activeEvent.id}/qr`, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ label: quickLinkName.trim() }),
})
if (res.ok) {
const src = await res.json()
const newSource = {
...src, scanCount: 0, pledgeCount: 0, totalPledged: 0, totalCollected: 0,
_eventId: activeEvent.id, _eventName: activeEvent.name,
_paymentMode: activeEvent.paymentMode || "self",
_externalPlatform: activeEvent.externalPlatform || null,
}
setSources(prev => [newSource, ...prev])
setExpandedLink(src.code)
setShowQuickAdd(false); setQuickLinkName("")
}
} catch { /* */ }
setQuickCreating(false)
}
const getShareTab = (code: string) => shareTab[code] || "link"
const setShareTabFor = (code: string, tab: "link" | "qr" | "embed") => setShareTab(prev => ({ ...prev, [code]: tab }))
// Stats
const totalPledges = events.reduce((s, e) => s + e.pledgeCount, 0)
const totalPledged = events.reduce((s, e) => s + e.totalPledged, 0)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const totalConditional = events.reduce((s, e) => s + ((e as any).totalConditional || 0), 0)
// Sort sources by pledges
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sortedSources = [...sources].sort((a: any, b: any) => b.totalPledged - a.totalPledged)
if (loading) return <div className="flex items-center justify-center py-20"><Loader2 className="h-6 w-6 text-[#1E40AF] animate-spin" /></div>
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// EMPTY STATE — Guided first-time setup
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
if (events.length === 0 || showCreate) {
return (
<div className="p-4 md:p-6 lg:p-8">
<div className="grid lg:grid-cols-12 gap-8">
<div className="lg:col-span-7">
{/* Step indicator */}
<div className="flex items-center gap-2 mb-6">
{["What you're raising for", "How donors pay", "Your link"].map((label, i) => {
const steps = ["name", "payment", "link"] as const
const isActive = steps[i] === createStep
const isDone = steps.indexOf(createStep) > i
return (
<div key={i} className="flex items-center gap-2">
{i > 0 && <div className={`w-8 h-px ${isDone ? "bg-[#1E40AF]" : "bg-gray-200"}`} />}
<div className={`flex items-center gap-1.5 px-3 py-1.5 text-xs font-bold ${
isActive ? "bg-[#1E40AF] text-white" : isDone ? "bg-[#1E40AF]/10 text-[#1E40AF]" : "bg-gray-100 text-gray-400"
}`}>
{isDone ? <Check className="h-3 w-3" /> : <span>{i + 1}</span>}
<span className="hidden sm:inline">{label}</span>
</div>
</div>
)
})}
{events.length > 0 && (
<button onClick={() => { setShowCreate(false); setCreateStep("name") }} className="ml-auto text-xs text-gray-400 hover:text-gray-600">Cancel</button>
)}
</div>
{/* Step 1: What are you raising for? */}
{createStep === "name" && (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-black text-[#111827] tracking-tight">What are you raising for?</h1>
<p className="text-sm text-gray-500 mt-1">This could be a gala dinner, Ramadan appeal, building fund, or any cause.</p>
</div>
<div>
<input
value={newAppealName}
onChange={e => setNewAppealName(e.target.value)}
placeholder="e.g. Ramadan Gala Dinner 2026"
autoFocus
onKeyDown={e => e.key === "Enter" && newAppealName.trim() && setCreateStep("payment")}
className="w-full h-14 px-4 border-2 border-gray-200 text-lg placeholder:text-gray-300 focus:border-[#1E40AF] outline-none transition-all"
/>
</div>
<button
onClick={() => setCreateStep("payment")}
disabled={!newAppealName.trim()}
className="bg-[#111827] px-6 py-3 text-sm font-bold text-white hover:bg-gray-800 transition-colors disabled:opacity-30 flex items-center gap-2"
>
Next: How donors pay <ArrowRight className="h-4 w-4" />
</button>
</div>
)}
{/* Step 2: How will donors pay? */}
{createStep === "payment" && (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-black text-[#111827] tracking-tight">How will donors pay?</h1>
<p className="text-sm text-gray-500 mt-1">
We capture the pledge (name, phone, amount, Gift Aid) for all three options.
This is how you tell donors to actually send the money.
</p>
</div>
<div className="space-y-3">
{/* Bank transfer */}
<button
onClick={() => setNewPaymentMode("bank")}
className={`w-full text-left p-5 border-2 transition-all ${
newPaymentMode === "bank" ? "border-[#1E40AF] bg-[#1E40AF]/5" : "border-gray-200 hover:border-gray-300"
}`}
>
<div className="flex items-start gap-4">
<div className={`w-10 h-10 flex items-center justify-center shrink-0 ${newPaymentMode === "bank" ? "bg-[#1E40AF] text-white" : "bg-gray-100 text-gray-400"}`}>
<Building2 className="h-5 w-5" />
</div>
<div>
<p className="text-sm font-bold text-[#111827]">Bank transfer</p>
<p className="text-xs text-gray-500 mt-0.5">
After pledging, donors see your sort code, account number, and a unique reference.
They transfer money directly to your bank account.
</p>
<p className="text-[10px] text-[#1E40AF] font-bold mt-1.5">Most popular · Free · No fees</p>
</div>
</div>
</button>
{/* External platform */}
<button
onClick={() => setNewPaymentMode("external")}
className={`w-full text-left p-5 border-2 transition-all ${
newPaymentMode === "external" ? "border-[#1E40AF] bg-[#1E40AF]/5" : "border-gray-200 hover:border-gray-300"
}`}
>
<div className="flex items-start gap-4">
<div className={`w-10 h-10 flex items-center justify-center shrink-0 ${newPaymentMode === "external" ? "bg-[#1E40AF] text-white" : "bg-gray-100 text-gray-400"}`}>
<Globe className="h-5 w-5" />
</div>
<div>
<p className="text-sm font-bold text-[#111827]">JustGiving, LaunchGood, or any link</p>
<p className="text-xs text-gray-500 mt-0.5">
After pledging, donors are sent to your fundraising page to pay.
We track the pledge and send reminders with your link.
</p>
<p className="text-[10px] text-gray-500 mt-1.5">Works with any platform that has a donation&nbsp;page</p>
</div>
</div>
</button>
{/* Card payment */}
<button
onClick={() => setNewPaymentMode("card")}
className={`w-full text-left p-5 border-2 transition-all ${
newPaymentMode === "card" ? "border-[#1E40AF] bg-[#1E40AF]/5" : "border-gray-200 hover:border-gray-300"
}`}
>
<div className="flex items-start gap-4">
<div className={`w-10 h-10 flex items-center justify-center shrink-0 ${newPaymentMode === "card" ? "bg-[#1E40AF] text-white" : "bg-gray-100 text-gray-400"}`}>
<CreditCard className="h-5 w-5" />
</div>
<div>
<p className="text-sm font-bold text-[#111827]">Card payment (Stripe)</p>
<p className="text-xs text-gray-500 mt-0.5">
Donors pay instantly by Visa, Mastercard, or Apple Pay.
Money goes straight to your Stripe account.
</p>
{!hasStripe && (
<p className="text-[10px] text-[#F59E0B] font-bold mt-1.5">
Requires Stripe <Link href="/dashboard/settings" className="underline">connect in Settings</Link>
</p>
)}
{hasStripe && <p className="text-[10px] text-[#16A34A] font-bold mt-1.5">Stripe connected </p>}
</div>
</div>
</button>
</div>
{/* External platform details */}
{newPaymentMode === "external" && (
<div className="border-l-2 border-[#1E40AF] pl-4 space-y-3">
<div>
<label className="text-[10px] font-bold text-gray-500 uppercase tracking-wide block mb-1.5">Platform</label>
<div className="flex gap-1.5 flex-wrap">
{[
{ value: "justgiving", label: "JustGiving" },
{ value: "launchgood", label: "LaunchGood" },
{ value: "gofundme", label: "GoFundMe" },
{ value: "enthuse", label: "Enthuse" },
{ value: "other", label: "Other" },
].map(p => (
<button key={p.value} onClick={() => setNewPlatform(p.value)}
className={`px-3 py-1.5 text-xs font-bold border-2 transition-colors ${
newPlatform === p.value ? "border-[#1E40AF] bg-[#1E40AF]/5 text-[#1E40AF]" : "border-gray-200 text-gray-400"
}`}>
{p.label}
</button>
))}
</div>
</div>
<div>
<label className="text-[10px] font-bold text-gray-500 uppercase tracking-wide block mb-1.5">Your fundraising page URL</label>
<input
value={newExternalUrl}
onChange={e => setNewExternalUrl(e.target.value)}
placeholder="https://www.justgiving.com/your-page"
className="w-full h-10 px-3 border-2 border-gray-200 text-sm placeholder:text-gray-300 focus:border-[#1E40AF] outline-none"
/>
<p className="text-[10px] text-gray-400 mt-1">Donors are sent to this link after pledging.</p>
</div>
</div>
)}
<div className="flex gap-3">
<button onClick={() => setCreateStep("name")} className="border border-gray-200 px-4 py-3 text-sm font-bold text-gray-500 hover:bg-gray-50">Back</button>
<button
onClick={() => setCreateStep("link")}
disabled={newPaymentMode === "external" && !newExternalUrl.trim()}
className="bg-[#111827] px-6 py-3 text-sm font-bold text-white hover:bg-gray-800 transition-colors disabled:opacity-30 flex items-center gap-2"
>
Next: Name your link <ArrowRight className="h-4 w-4" />
</button>
</div>
</div>
)}
{/* Step 3: Name your link */}
{createStep === "link" && (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-black text-[#111827] tracking-tight">Name your link</h1>
<p className="text-sm text-gray-500 mt-1">
You can create multiple links one per table, per volunteer, or per WhatsApp group.
Start with a main link and add more later.
</p>
</div>
{/* Summary of what they've set up */}
<div className="bg-[#F9FAFB] border border-gray-100 p-4 space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs text-gray-500">Raising for</span>
<span className="text-xs font-bold text-[#111827]">{newAppealName}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-gray-500">Donors pay via</span>
<span className="text-xs font-bold text-[#111827]">
{newPaymentMode === "bank" ? "Bank transfer" :
newPaymentMode === "external" ? PLATFORM_LABELS[newPlatform] || "External link" :
"Card (Stripe)"}
</span>
</div>
</div>
<div>
<input
value={newLinkName}
onChange={e => setNewLinkName(e.target.value)}
placeholder="e.g. Main link, Table 5, Imam Yusuf"
autoFocus
onKeyDown={e => e.key === "Enter" && handleCreate()}
className="w-full h-14 px-4 border-2 border-gray-200 text-lg placeholder:text-gray-300 focus:border-[#1E40AF] outline-none"
/>
</div>
<div className="flex gap-3">
<button onClick={() => setCreateStep("payment")} className="border border-gray-200 px-4 py-3 text-sm font-bold text-gray-500 hover:bg-gray-50">Back</button>
<button
onClick={handleCreate}
disabled={creating}
className="bg-[#1E40AF] px-6 py-3 text-sm font-bold text-white hover:bg-[#1E40AF]/90 transition-colors disabled:opacity-50 flex items-center gap-2"
>
{creating ? <><Loader2 className="h-4 w-4 animate-spin" /> Creating</> : <>Create pledge link <ArrowRight className="h-4 w-4" /></>}
</button>
</div>
</div>
)}
</div>
{/* RIGHT COLUMN — Education */}
<div className="lg:col-span-5 space-y-6">
<div className="border border-gray-200 bg-white">
<div className="border-b border-gray-100 px-5 py-3">
<h3 className="text-sm font-bold text-[#111827]">How it works</h3>
</div>
<div className="divide-y divide-gray-50">
{[
{ n: "01", title: "You create a pledge link", desc: "Give it a name and choose how donors pay. Print the QR, share the link, or embed on your website." },
{ n: "02", title: "Donors pledge in 60 seconds", desc: "They enter name, phone, amount, and Gift Aid. No app, no account, no friction." },
{ n: "03", title: "We tell them how to pay", desc: "Bank details, external link, or card checkout — based on what you chose. Plus a unique reference to track it." },
{ n: "04", title: "Reminders go out automatically", desc: "WhatsApp messages on day 2, 7, and 14. With a link to pay. Stops when they do." },
{ n: "05", title: "You match payments", desc: "Upload your bank statement. We auto-match. Or payments confirm instantly via Stripe/external." },
].map(s => (
<div key={s.n} className="px-5 py-3 flex gap-3">
<span className="text-lg font-black text-gray-200 shrink-0 w-6">{s.n}</span>
<div>
<p className="text-xs font-bold text-[#111827]">{s.title}</p>
<p className="text-[11px] text-gray-500 leading-relaxed mt-0.5">{s.desc}</p>
</div>
</div>
))}
</div>
</div>
{/* Payment method comparison */}
<div className="border-l-2 border-[#F59E0B] pl-4 space-y-3">
<p className="text-xs font-bold text-[#111827]">Which payment method should I choose?</p>
<div className="space-y-2">
<div>
<p className="text-[11px] font-bold text-[#111827]">Bank transfer</p>
<p className="text-[10px] text-gray-500">Best for most charities. No fees. Donors transfer to your account directly. Use the bank statement upload to match payments.</p>
</div>
<div>
<p className="text-[11px] font-bold text-[#111827]">JustGiving / LaunchGood</p>
<p className="text-[10px] text-gray-500">Already have a fundraising page? We capture the pledge and send donors to your page to pay. You get pledge tracking + reminders for free.</p>
</div>
<div>
<p className="text-[11px] font-bold text-[#111827]">Card payment (Stripe)</p>
<p className="text-[10px] text-gray-500">Instant payment. Best conversion rate. Stripe charges 1.4% + 20p per transaction. Connect in Settings first.</p>
</div>
</div>
</div>
{/* Can I mix? */}
<div className="border-l-2 border-[#1E40AF] pl-4 space-y-1.5">
<p className="text-xs font-bold text-[#111827]">Can I mix payment methods?</p>
<p className="text-[10px] text-gray-500">
Yes. Create a separate appeal for each payment method. For example: &quot;Ramadan Bank Transfer&quot; and &quot;Ramadan JustGiving&quot;.
Each gets its own pledge links.
</p>
</div>
</div>
</div>
</div>
)
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// HAS DATA — Links are the primary display
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
return (
<div>
{/* Hero */}
<div className="grid md:grid-cols-5 gap-0">
<div className="md:col-span-2 relative min-h-[140px] md:min-h-[180px] overflow-hidden">
<Image
src="/images/brand/event-05-qr-scanning.jpg"
alt="Guest scanning a QR code at a fundraising event"
fill className="object-cover"
sizes="(max-width: 768px) 100vw, 40vw"
/>
</div>
<div className="md:col-span-3 bg-[#111827] p-6 md:p-8 flex flex-col justify-center">
<div className="border-l-2 border-[#F59E0B] pl-3 mb-3">
<p className="text-[11px] font-semibold tracking-[0.15em] uppercase text-gray-500">Collect</p>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<p className="text-2xl md:text-3xl font-black text-white tracking-tight">{sources.length}</p>
<p className="text-[10px] text-gray-500 mt-0.5">{sources.length === 1 ? "pledge link" : "pledge links"}</p>
</div>
<div>
<p className="text-2xl md:text-3xl font-black text-white tracking-tight">{totalPledges}</p>
<p className="text-[10px] text-gray-500 mt-0.5">pledges</p>
</div>
<div>
<p className="text-2xl md:text-3xl font-black text-[#4ADE80] tracking-tight">{formatPence(totalPledged)}</p>
<p className="text-[10px] text-gray-500 mt-0.5">confirmed</p>
</div>
{totalConditional > 0 && (
<div>
<p className="text-2xl md:text-3xl font-black text-[#FBBF24] tracking-tight">{formatPence(totalConditional)}</p>
<p className="text-[10px] text-gray-500 mt-0.5">conditional</p>
</div>
)}
</div>
</div>
</div>
{/* Content */}
<div className="p-4 md:p-6 lg:p-8 space-y-6">
{/* Action bar */}
<div className="flex items-center justify-between gap-3">
<h2 className="text-sm font-bold text-[#111827]">Your pledge links</h2>
<div className="flex gap-2">
<button onClick={() => setShowQuickAdd(true)}
className="inline-flex items-center gap-1.5 bg-[#111827] px-4 py-2 text-xs font-bold text-white hover:bg-gray-800 transition-colors">
<Plus className="h-3.5 w-3.5" /> New link
</button>
{events.length <= 3 && (
<button onClick={() => setShowCreate(true)}
className="inline-flex items-center gap-1.5 border border-gray-200 px-4 py-2 text-xs font-bold text-gray-600 hover:bg-gray-50 transition-colors">
<Plus className="h-3.5 w-3.5" /> New appeal
</button>
)}
</div>
</div>
{/* Quick add link inline */}
{showQuickAdd && (
<div className="border-2 border-[#1E40AF] bg-[#1E40AF]/[0.02] p-4 space-y-3">
<div className="flex items-center justify-between">
<p className="text-sm font-bold text-[#111827]">Create a new pledge link</p>
<button onClick={() => { setShowQuickAdd(false); setQuickLinkName(""); setQuickAddEventId(null) }}
className="text-xs text-gray-400 hover:text-gray-600">Cancel</button>
</div>
{/* Appeal picker — only shows when there are multiple appeals */}
{events.length > 1 && (
<div>
<label className="text-[10px] font-bold text-gray-500 block mb-1.5">Which appeal is this for?</label>
<div className="flex flex-wrap gap-2">
{events.map(ev => (
<button key={ev.id}
onClick={() => setQuickAddEventId(ev.id)}
className={`px-3 py-1.5 text-xs font-bold border-2 transition-colors ${
activeEvent?.id === ev.id
? "border-[#1E40AF] bg-[#1E40AF]/5 text-[#1E40AF]"
: "border-gray-200 text-gray-500 hover:border-gray-300"
}`}>
{ev.name}
</button>
))}
</div>
</div>
)}
<div className="flex gap-2 items-end">
<div className="flex-1">
<label className="text-[10px] font-bold text-gray-500 block mb-1">
Link name
{events.length === 1 && activeEvent && (
<span className="text-gray-400 font-normal"> · {activeEvent.name}</span>
)}
</label>
<input
value={quickLinkName} onChange={e => setQuickLinkName(e.target.value)}
placeholder="e.g. Table 5, Imam Yusuf, WhatsApp group"
autoFocus onKeyDown={e => e.key === "Enter" && handleQuickAdd()}
className="w-full h-10 px-3 border-2 border-gray-200 text-sm focus:border-[#1E40AF] outline-none"
/>
</div>
<button onClick={handleQuickAdd} disabled={quickCreating || !quickLinkName.trim()}
className="h-10 bg-[#1E40AF] px-4 text-xs font-bold text-white disabled:opacity-40">
{quickCreating ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : "Create"}
</button>
</div>
</div>
)}
{/* TWO-COLUMN: Links left, Education right */}
<div className="grid lg:grid-cols-12 gap-6">
{/* LEFT: Link cards */}
<div className="lg:col-span-7 space-y-3">
{sortedSources.map((src) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const s = src as any
const url = `${baseUrl}/p/${src.code}`
const isCopied = copiedCode === src.code
const isEmbedCopied = copiedEmbed === src.code
const isExpanded = expandedLink === src.code
const conversion = src.scanCount > 0 ? Math.round((src.pledgeCount / src.scanCount) * 100) : 0
const tab = getShareTab(src.code)
const payMode = s._paymentMode || "self"
const platform = s._externalPlatform
const eventName = s._eventName
return (
<div key={src.id} className="bg-white border border-gray-200 hover:border-gray-300 transition-colors">
{/* Header row */}
<button onClick={() => setExpandedLink(isExpanded ? null : src.code)}
className="w-full p-4 md:p-5 text-left">
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-3 min-w-0">
<div className="min-w-0">
<div className="flex items-center gap-2">
<p className="text-sm font-bold text-[#111827] truncate">{src.label}</p>
{/* Payment method badge */}
{payMode === "external" && platform ? (
<span className="text-[8px] font-bold px-1.5 py-0.5 bg-[#1E40AF]/10 text-[#1E40AF] shrink-0 flex items-center gap-0.5">
<Globe className="h-2 w-2" /> {PLATFORM_LABELS[platform] || "External"}
</span>
) : payMode === "self" ? (
<span className="text-[8px] font-bold px-1.5 py-0.5 bg-gray-100 text-gray-500 shrink-0 flex items-center gap-0.5">
<Building2 className="h-2 w-2" /> Bank
</span>
) : null}
</div>
{events.length > 1 && <p className="text-[10px] text-gray-400 truncate mt-0.5">{eventName}</p>}
{src.volunteerName && src.volunteerName !== src.label && (
<p className="text-[10px] text-gray-400 truncate">by {src.volunteerName}</p>
)}
</div>
</div>
<div className="flex gap-px bg-gray-200 shrink-0">
<div className="bg-white px-2.5 py-1.5 text-center">
<p className="text-sm font-black text-[#111827]">{src.scanCount}</p>
<p className="text-[8px] text-gray-500">clicks</p>
</div>
<div className="bg-white px-2.5 py-1.5 text-center">
<p className="text-sm font-black text-[#111827]">{src.pledgeCount}</p>
<p className="text-[8px] text-gray-500">pledges</p>
</div>
<div className="bg-white px-2.5 py-1.5 text-center">
<p className="text-sm font-black text-[#16A34A]">{formatPence(src.totalPledged)}</p>
<p className="text-[8px] text-gray-500">raised</p>
</div>
</div>
</div>
{/* Conversion bar */}
{src.scanCount > 0 && (
<div className="mt-3 flex items-center gap-2">
<div className="flex-1 h-1 bg-gray-100 overflow-hidden">
<div className="h-full bg-[#1E40AF]" style={{ width: `${conversion}%` }} />
</div>
<span className="text-[9px] text-gray-400 font-bold">{conversion}% convert</span>
</div>
)}
</button>
{/* Expanded: Share section */}
{isExpanded && (
<div className="border-t border-gray-100 p-4 md:p-5 space-y-4">
{/* Share tabs */}
<div className="flex gap-px bg-gray-200">
{[
{ key: "link" as const, label: "Link", icon: Link2 },
{ key: "qr" as const, label: "QR Code", icon: QrCodeIcon },
{ key: "embed" as const, label: "Website Widget", icon: Code2 },
].map(t => (
<button key={t.key} onClick={() => setShareTabFor(src.code, t.key)}
className={`flex-1 py-2 text-xs font-bold flex items-center justify-center gap-1.5 transition-colors ${
tab === t.key ? "bg-[#111827] text-white" : "bg-white text-gray-500 hover:bg-gray-50"
}`}>
<t.icon className="h-3 w-3" /> {t.label}
</button>
))}
</div>
{/* Tab content */}
{tab === "link" && (
<div className="space-y-3">
<p className="text-xs font-mono text-gray-500 break-all bg-[#F9FAFB] p-3 border border-gray-100">{url}</p>
<div className="grid grid-cols-4 gap-1.5">
<button onClick={() => copyLink(src.code)} className={`py-2.5 text-[10px] font-bold flex items-center justify-center gap-1 ${
isCopied ? "bg-[#16A34A] text-white" : "bg-[#111827] text-white hover:bg-gray-800"
}`}>
{isCopied ? <><Check className="h-3 w-3" /> Copied</> : <><Copy className="h-3 w-3" /> Copy</>}
</button>
<button onClick={() => shareWhatsApp(src.code, src.label)} className="bg-[#25D366] py-2.5 text-[10px] font-bold text-white flex items-center justify-center gap-1">
<MessageCircle className="h-3 w-3" /> WhatsApp
</button>
<button onClick={() => shareEmail(src.code, src.label)} className="border border-gray-200 py-2.5 text-[10px] font-bold text-gray-600 flex items-center justify-center gap-1 hover:bg-gray-50">
<Mail className="h-3 w-3" /> Email
</button>
<button onClick={() => shareNative(src.code, src.label)} className="border border-gray-200 py-2.5 text-[10px] font-bold text-gray-600 flex items-center justify-center gap-1 hover:bg-gray-50">
<Share2 className="h-3 w-3" /> Share
</button>
</div>
</div>
)}
{tab === "qr" && (
<div className="flex flex-col items-center gap-3">
<div className="bg-white p-4 border border-gray-200 inline-block">
<QRCodeCanvas url={url} size={180} />
</div>
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
<a href={`/api/events/${(src as any)._eventId}/qr/${src.id}/download?code=${src.code}`}
download={`${src.label.replace(/\s+/g, "-")}-qr.png`}
className="bg-[#111827] px-4 py-2 text-xs font-bold text-white hover:bg-gray-800 flex items-center gap-1.5">
<Download className="h-3.5 w-3.5" /> Download QR
</a>
<p className="text-[10px] text-gray-400">Print on table cards, flyers, or banners</p>
</div>
)}
{tab === "embed" && (
<div className="space-y-3">
<p className="text-xs text-gray-600">Add the pledge form to your website. Copy this code and paste it into your HTML:</p>
<div className="bg-[#111827] p-4 overflow-x-auto">
<code className="text-[11px] text-[#4ADE80] font-mono whitespace-pre">{`<iframe\n src="${url}?embed=1"\n width="100%"\n height="160"\n style="border:none;max-width:480px;"\n title="Pledge: ${src.label}"\n></iframe>`}</code>
</div>
<button onClick={() => copyEmbed(src.code)} className={`w-full py-2.5 text-xs font-bold flex items-center justify-center gap-1.5 ${
isEmbedCopied ? "bg-[#16A34A] text-white" : "bg-[#111827] text-white hover:bg-gray-800"
}`}>
{isEmbedCopied ? <><Check className="h-3.5 w-3.5" /> Copied</> : <><Code2 className="h-3.5 w-3.5" /> Copy embed code</>}
</button>
<div className="border-l-2 border-[#1E40AF] pl-3 text-[10px] text-gray-500 space-y-1">
<p>Works on any website WordPress, Squarespace, Wix, custom HTML.</p>
<p>The form adapts to mobile. Donors never leave your site.</p>
</div>
</div>
)}
{/* Payment method note for external */}
{payMode === "external" && platform && (
<div className="border-l-2 border-[#1E40AF] bg-[#1E40AF]/5 p-3 flex items-center gap-2">
<Globe className="h-3.5 w-3.5 text-[#1E40AF] shrink-0" />
<p className="text-[10px] text-gray-600">
After pledging, donors are sent to <strong className="text-[#111827]">{PLATFORM_LABELS[platform] || "your page"}</strong> to pay
</p>
</div>
)}
</div>
)}
</div>
)
})}
</div>
{/* RIGHT: Education */}
<div className="lg:col-span-5 space-y-6">
{/* Leaderboard */}
{sortedSources.filter(s => s.pledgeCount > 0).length >= 3 && (
<div className="bg-white border border-gray-200">
<div className="border-b border-gray-100 px-5 py-3 flex items-center justify-between">
<h3 className="text-sm font-bold text-[#111827] flex items-center gap-1.5">
<Trophy className="h-4 w-4 text-[#F59E0B]" /> Who&apos;s collecting the most
</h3>
</div>
<div className="divide-y divide-gray-50">
{sortedSources.filter(s => s.pledgeCount > 0).slice(0, 5).map((src, i) => {
const medals = ["bg-[#F59E0B]", "bg-gray-400", "bg-[#CD7F32]"]
return (
<div key={src.id} className="px-5 py-3 flex items-center gap-3">
<div className={`w-6 h-6 flex items-center justify-center text-[10px] font-black text-white ${medals[i] || "bg-gray-200 text-gray-500"}`}>{i + 1}</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-[#111827] truncate">{src.volunteerName || src.label}</p>
<p className="text-[10px] text-gray-500">{src.pledgeCount} pledges</p>
</div>
<p className="text-sm font-black text-[#111827]">{formatPence(src.totalPledged)}</p>
</div>
)
})}
</div>
</div>
)}
{/* Where to share */}
<div className="border-l-2 border-[#1E40AF] pl-4 space-y-2">
<p className="text-xs font-bold text-[#111827]">Where to share your link</p>
<div className="space-y-1.5">
{[
{ n: "01", text: "Print the QR code on each table at your event" },
{ n: "02", text: "Send the link to WhatsApp groups — one tap to pledge" },
{ n: "03", text: "Post it on Instagram or Facebook stories" },
{ n: "04", text: "Embed the widget on your mosque or charity website" },
{ n: "05", text: "Give each volunteer their own link — competition works" },
].map(t => (
<div key={t.n} className="flex items-start gap-2">
<span className="text-[#1E40AF] font-bold text-xs shrink-0">{t.n}</span>
<p className="text-xs text-gray-600">{t.text}</p>
</div>
))}
</div>
</div>
{/* Payment methods */}
<div className="border-l-2 border-[#F59E0B] pl-4 space-y-2">
<p className="text-xs font-bold text-[#111827]">How payment works</p>
<p className="text-[11px] text-gray-500 leading-relaxed">
Every pledge link captures the donor&apos;s details first. What happens next depends on how you set up the appeal:
</p>
<div className="space-y-1.5">
{[
{ icon: Building2, label: "Bank transfer", desc: "Donor sees your sort code and account number with a unique reference" },
{ icon: Globe, label: "External platform", desc: "Donor is sent to JustGiving, LaunchGood, or your fundraising page to pay" },
{ icon: CreditCard, label: "Card payment", desc: "Donor pays instantly by Visa/Mastercard. Money goes to your Stripe account" },
].map(p => (
<div key={p.label} className="flex items-start gap-2">
<p.icon className="h-3 w-3 text-[#F59E0B] shrink-0 mt-0.5" />
<div>
<p className="text-[11px] font-bold text-[#111827]">{p.label}</p>
<p className="text-[10px] text-gray-500">{p.desc}</p>
</div>
</div>
))}
</div>
</div>
{/* What's an appeal? */}
{events.length > 0 && (
<div className="border-l-2 border-gray-300 pl-4 space-y-1.5">
<p className="text-xs font-bold text-[#111827]">What&apos;s an appeal?</p>
<p className="text-[10px] text-gray-500">
An appeal is your fundraiser one dinner, one campaign, one cause.
All links within an appeal share the same payment method.
Most organisations only need one appeal with several links.
</p>
{events.length > 1 && (
<div className="mt-2 space-y-1">
<p className="text-[9px] font-bold text-gray-400 uppercase tracking-wide">Your appeals</p>
{events.map(ev => (
<div key={ev.id} className="flex items-center justify-between text-[10px]">
<span className="text-[#111827] font-medium">{ev.name}</span>
<span className="text-gray-400">{ev.paymentMode === "external"
? PLATFORM_LABELS[ev.externalPlatform || ""] || "External"
: "Bank transfer"} · {ev.pledgeCount} pledges</span>
</div>
))}
</div>
)}
</div>
)}
</div>
</div>
</div>
</div>
)
}