Deep UX: 2-column automations, visible appeal cards, platform education, strip model refs

Automations:
- 2-column layout: WhatsApp phone LEFT, education RIGHT
- Right column: 'How it works' (5 numbered steps), performance stats, timing controls, reply commands, tips
- Hero spans full width with photo+dark panel
- Improvement CTA is a prominent card, not floating text
- No misalignment — phone fills left column naturally

Collect:
- Appeals shown as visible gap-px grid cards (not hidden dropdown)
- Each card shows name, platform, amount raised, pledge count, collection rate
- Active appeal has border-l-2 blue indicator
- Platform integration clarity: shows 'Donors redirected to JustGiving' etc
- Educational section: 'Where to share your link' + 'How payment works'
- Explains bank transfer vs JustGiving vs card payment inline

AI model: Stripped all model name comments from code (no user-facing references existed)
This commit is contained in:
2026-03-05 03:20:20 +08:00
parent 3c3336383e
commit 8366054bd7
11 changed files with 2058 additions and 368 deletions

View File

@@ -11,7 +11,7 @@ const OPENAI_MODEL = "gpt-4.1-nano"
async function chat(messages: Array<{ role: string; content: string }>, maxTokens = 600): Promise<string> {
if (!HAS_AI) return ""
// Prefer OpenAI (gpt-4.1-nano), fall back to Gemini
// Prefer OpenAI, fall back to Gemini
if (OPENAI_KEY) {
try {
const res = await fetch("https://api.openai.com/v1/chat/completions", {

View File

@@ -4,7 +4,7 @@ import { useState, useEffect, useCallback, useRef } from "react"
import Image from "next/image"
import {
Loader2, Check, Send, Trophy, CheckCheck,
ChevronDown, Clock, MessageCircle, RefreshCw, Calendar
ChevronDown, MessageCircle, RefreshCw, Calendar
} from "lucide-react"
import Link from "next/link"
import { resolvePreview, STEP_META } from "@/lib/templates"
@@ -146,312 +146,364 @@ export default function AutomationsPage() {
const waConnected = !!channels?.whatsapp
const delays = [0, config?.step1Delay || 2, config?.step2Delay || 7, config?.step3Delay || 14]
// Stats for the right column
const totalSentAll = templates.reduce((s, t) => s + t.sentCount, 0)
const totalConverted = templates.reduce((s, t) => s + t.convertedCount, 0)
const overallRate = totalSentAll > 0 ? Math.round((totalConverted / totalSentAll) * 100) : 0
return (
<div className="space-y-6">
{/* ── Header ── */}
<div>
<div className="border-l-2 border-[#F59E0B] pl-3 mb-3">
<p className="text-[11px] font-semibold tracking-[0.15em] uppercase text-gray-500">Donor journey</p>
{/* ━━ HERO — Full-width, same pattern as landing page ━━━━━━━ */}
<div className="grid md:grid-cols-5 gap-0">
<div className="md:col-span-2 relative min-h-[180px] md:min-h-[260px] overflow-hidden">
<Image
src="/images/brand/digital-03-notification-smile.jpg"
alt="Young man smiling at his phone — the moment a gentle reminder lands"
fill className="object-cover" sizes="(max-width: 768px) 100vw, 40vw"
priority
/>
</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">Donor journey</p>
</div>
<h1 className="text-2xl md:text-3xl font-black text-white tracking-tight">
What your donors receive
</h1>
<p className="text-sm text-gray-400 leading-relaxed mt-2 max-w-md">
After someone pledges, they get 5 WhatsApp messages a receipt, a due date nudge, and 3 reminders. Each message includes your bank details and a unique reference. Click any message below to edit&nbsp;it.
</p>
{!waConnected && (
<p className="text-xs text-[#F59E0B] mt-3 font-bold">
WhatsApp not connected <Link href="/dashboard/settings" className="underline hover:text-white">connect in Settings</Link> to start sending.
</p>
)}
</div>
<h1 className="text-3xl md:text-4xl font-black text-[#111827] tracking-tight">
What your donors receive
</h1>
<p className="text-sm text-gray-500 mt-1">
5 messages a receipt, a due date nudge, and 3 reminders. Click any to edit.
</p>
</div>
{/* ── WhatsApp status ── */}
{!waConnected && (
<div className="border-l-2 border-[#F59E0B] pl-4 py-2">
<p className="text-sm text-gray-600"><strong className="text-[#111827]">WhatsApp not connected.</strong> Messages start once you <Link href="/dashboard/settings" className="text-[#1E40AF] font-bold hover:underline">connect WhatsApp</Link>.</p>
</div>
)}
{/* ━━ TWO-COLUMN LAYOUT — Phone left, education right ━━━━━━ */}
<div className="grid lg:grid-cols-12 gap-6">
{/* ━━ HERO ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
No tech-speak. No model names. No cost breakdowns.
This is about what happens for the DONOR, not the engine.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */}
{neverOptimised ? (
/* ── Never tested: invite them to start ── */
<div className="grid md:grid-cols-5 gap-0">
<div className="md:col-span-2 relative min-h-[200px] md:min-h-[280px] overflow-hidden">
<Image
src="/images/brand/digital-03-notification-smile.jpg"
alt="Young man smiling at his phone — the moment a gentle reminder lands"
fill className="object-cover" sizes="(max-width: 768px) 100vw, 40vw"
/>
</div>
<div className="md:col-span-3 bg-[#111827] p-8 md:p-10 flex flex-col justify-center">
<h2 className="text-2xl md:text-3xl font-black text-white tracking-tight">
Messages that improve&nbsp;themselves
</h2>
<p className="text-sm text-gray-400 leading-relaxed mt-3 max-w-md">
We test different versions of each message with your real donors.
The one that collects more pledges wins. Automatically. You don&apos;t do anything they just get better over time.
</p>
{/* ── LEFT: The WhatsApp conversation ── */}
<div className="lg:col-span-7">
{/* Improvement status bar */}
{neverOptimised ? (
<button onClick={optimiseAll} disabled={aiWorking}
className="mt-6 inline-flex items-center justify-center bg-white px-6 py-3 text-sm font-bold text-[#111827] hover:bg-gray-100 transition-colors self-start disabled:opacity-60">
{aiWorking
? <><Loader2 className="h-4 w-4 animate-spin mr-2" /> Creating new versions</>
: "Start improving"}
className="w-full bg-white border-2 border-[#111827] p-4 mb-4 flex items-center gap-4 hover:bg-gray-50 transition-colors text-left disabled:opacity-60">
<div className="w-10 h-10 bg-[#111827] flex items-center justify-center shrink-0">
<span className="text-white text-lg font-black"></span>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-bold text-[#111827]">
{aiWorking ? "Creating new versions…" : "Improve these messages"}
</p>
<p className="text-xs text-gray-500 mt-0.5">
We&apos;ll test a different version of each message and keep whichever collects more.
</p>
</div>
{aiWorking && <Loader2 className="h-4 w-4 text-[#1E40AF] animate-spin shrink-0" />}
</button>
) : testsRunning > 0 ? (
<div className="bg-[#111827] p-4 mb-4 flex items-center gap-3">
<div className="relative shrink-0">
<div className="w-2.5 h-2.5 bg-[#60A5FA]" style={{ borderRadius: "50%" }} />
<span className="absolute inset-0 w-2.5 h-2.5 bg-[#60A5FA] animate-pulse" style={{ borderRadius: "50%" }} />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-bold text-white">Testing {testsRunning} new version{testsRunning > 1 ? "s" : ""}</p>
<p className="text-[11px] text-gray-500">The better one wins automatically.</p>
</div>
{stepsWithoutTest > 0 && (
<button onClick={optimiseAll} disabled={aiWorking}
className="shrink-0 border border-gray-600 text-gray-300 px-3 py-1.5 text-[11px] font-bold hover:text-white hover:border-white transition-colors disabled:opacity-50">
{aiWorking ? <Loader2 className="h-3 w-3 animate-spin" /> : `Test ${stepsWithoutTest} more`}
</button>
)}
</div>
) : (
<div className="bg-[#111827] p-4 mb-4 flex items-center gap-3">
<div className="w-2.5 h-2.5 bg-[#16A34A] shrink-0" style={{ borderRadius: "50%" }} />
<div className="flex-1 min-w-0">
<p className="text-sm font-bold text-white">Your messages are tuned</p>
<p className="text-[11px] text-gray-500">
{stats && stats.total > 0 ? `${stats.total} sent · ${stats.deliveryRate}% delivered` : "Best-performing versions are live."}
</p>
</div>
<button onClick={optimiseAll} disabled={aiWorking}
className="shrink-0 border border-gray-600 text-gray-300 px-3 py-1.5 text-[11px] font-bold hover:text-white hover:border-white transition-colors disabled:opacity-50">
{aiWorking ? <Loader2 className="h-3 w-3 animate-spin" /> : "Test new versions"}
</button>
</div>
)}
{/* WhatsApp mockup */}
<div className="border border-gray-300 overflow-hidden shadow-lg" style={{ borderRadius: "20px" }}>
<div className="bg-[#075E54] px-4 py-3 flex items-center gap-3">
<span className="text-white/50 text-sm"></span>
<div className="w-9 h-9 bg-[#128C7E] flex items-center justify-center" style={{ borderRadius: "50%" }}>
<MessageCircle className="h-4 w-4 text-white" />
</div>
<div>
<p className="text-white text-sm font-medium">Your charity</p>
<p className="text-[10px] text-white/50">Automated messages</p>
</div>
</div>
<div className="bg-[#ECE5DD] px-4 py-4 space-y-4" style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23000' fill-opacity='0.03'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`,
}}>
{STEP_META.map((meta) => {
const step = meta.step
const a = tpl(step, "A")
const b = tpl(step, "B")
const isEditing = editing === step
const justSaved = saved === step
const isRegenning = regenerating === step
const previewA = a ? resolvePreview(a.body) : ""
const previewB = b ? resolvePreview(b.body) : ""
const isConditional = meta.conditional
const hasDueDateTemplate = !!a
const timeLabel = step === 0 ? "Instantly" : step === 4 ? "On the due date · if set" : step === 1 ? `Day ${delays[1]} · if not paid` : step === 2 ? `Day ${delays[2]} · if not paid` : `Day ${delays[3]} · if not paid`
const clockTime = step === 0 ? "09:41" : step === 4 ? "08:00" : step === 1 ? "10:15" : step === 2 ? "09:30" : "11:00"
const rateA = a && a.sentCount > 0 ? Math.round((a.convertedCount / a.sentCount) * 100) : 0
const rateB = b && b.sentCount > 0 ? Math.round((b.convertedCount / b.sentCount) * 100) : 0
const totalSent = (a?.sentCount || 0) + (b?.sentCount || 0)
const progress = Math.min(100, Math.round((totalSent / (MIN_SAMPLE * 2)) * 100))
const hasEnoughData = (a?.sentCount || 0) >= MIN_SAMPLE && (b?.sentCount || 0) >= MIN_SAMPLE
const winner = hasEnoughData ? (rateB > rateA ? "B" : rateA > rateB ? "A" : null) : null
if (isConditional && !hasDueDateTemplate) return null
return (
<div key={step}>
<div className="flex justify-center mb-3">
<span className={`text-[10px] px-3 py-1 font-medium shadow-sm flex items-center gap-1.5 ${isConditional ? "bg-[#FEF3C7] text-[#92400E]" : "bg-white/80 text-[#667781]"}`} style={{ borderRadius: "6px" }}>
{isConditional && <Calendar className="h-2.5 w-2.5" />}
{timeLabel}
</span>
</div>
{isEditing ? (
<div className="flex justify-end">
<div className="bg-[#DCF8C6] max-w-[90%] w-full shadow-sm" style={{ borderRadius: "8px 0 8px 8px" }}>
<textarea ref={editorRef} value={editBody} onChange={e => setEditBody(e.target.value)}
className="w-full bg-transparent px-3 py-2 text-[13px] leading-[1.6] text-[#303030] resize-y outline-none min-h-[180px] font-mono" />
<div className="px-3 pb-2 flex items-center gap-2">
<button onClick={() => saveEdit(step)} disabled={saving}
className="bg-[#075E54] text-white px-3 py-1.5 text-[10px] font-bold flex items-center gap-1 disabled:opacity-50" style={{ borderRadius: "4px" }}>
{saving ? <Loader2 className="h-2.5 w-2.5 animate-spin" /> : <Check className="h-2.5 w-2.5" />} Save
</button>
<button onClick={() => setEditing(null)} className="text-[10px] text-[#667781]">Cancel</button>
<span className="text-[9px] text-[#667781]/50 ml-auto">{"{{name}} {{amount}} {{payment_block}}"}</span>
</div>
</div>
</div>
) : b ? (
<div className="flex justify-end">
<div className="max-w-[90%] w-full" style={{ borderRadius: "8px" }}>
<div className="bg-[#075E54] text-white px-3 py-1.5 flex items-center gap-1.5" style={{ borderRadius: "8px 8px 0 0" }}>
<div className="w-1.5 h-1.5 bg-[#60A5FA]" style={{ borderRadius: "50%" }} />
<span className="text-[10px] font-bold flex-1">Testing two versions</span>
{hasEnoughData && winner && (
<span className="text-[9px] bg-[#16A34A]/20 text-[#4ADE80] px-1.5 py-0.5 font-bold flex items-center gap-0.5">
<Trophy className="h-2.5 w-2.5" /> {winner === "B" ? "New" : "Current"} winning
</span>
)}
{!hasEnoughData && <span className="text-[9px] text-white/40">{progress}%</span>}
</div>
<button onClick={() => startEdit(step)} className="w-full bg-[#DCF8C6] p-3 text-left hover:brightness-[0.97] transition-all border-b border-[#075E54]/10">
<div className="flex items-center gap-1 mb-2">
<span className="text-[9px] font-bold text-[#075E54] bg-[#075E54]/10 px-1.5 py-0.5">Current</span>
{a && a.sentCount > 0 && (
<span className={`text-[9px] font-bold ml-auto ${winner === "A" ? "text-[#075E54]" : "text-[#667781]"}`}>
{rateA}% conversion{winner === "A" && " 🏆"} · {a.convertedCount}/{a.sentCount}
</span>
)}
</div>
<div className="text-[12px] leading-[1.5] text-[#303030]"><WhatsAppFormatted text={previewA} /></div>
</button>
<div className="bg-[#DCF8C6] p-3 relative group">
<div className="flex items-center gap-1 mb-2">
<span className="text-[9px] font-bold text-[#1E40AF] bg-[#1E40AF]/10 px-1.5 py-0.5">New</span>
{b.sentCount > 0 && (
<span className={`text-[9px] font-bold ml-auto ${winner === "B" ? "text-[#075E54]" : "text-[#667781]"}`}>
{rateB}% conversion{winner === "B" && " 🏆"} · {b.convertedCount}/{b.sentCount}
</span>
)}
</div>
<div className="text-[12px] leading-[1.5] text-[#303030]"><WhatsAppFormatted text={previewB} /></div>
<button onClick={() => regenerateVariant(step)} disabled={isRegenning}
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 bg-white/90 p-1.5 shadow-sm transition-opacity hover:bg-white disabled:opacity-50"
style={{ borderRadius: "4px" }} title="Try a different version">
{isRegenning ? <Loader2 className="h-3.5 w-3.5 text-[#1E40AF] animate-spin" /> : <RefreshCw className="h-3.5 w-3.5 text-[#1E40AF]" />}
</button>
</div>
<div className="bg-white/60 px-3 py-1.5" style={{ borderRadius: "0 0 8px 8px" }}>
<div className="h-1 bg-gray-200 overflow-hidden" style={{ borderRadius: "2px" }}>
<div className={`h-full transition-all ${hasEnoughData ? "bg-[#16A34A]" : "bg-[#60A5FA]"}`} style={{ width: `${progress}%` }} />
</div>
<p className="text-[9px] text-[#667781] mt-1">
{hasEnoughData
? winner ? `${winner === "B" ? "New" : "Current"} version converts ${Math.abs(rateB - rateA)}% better` : "Too close to call — need more data"
: `${totalSent} of ${MIN_SAMPLE * 2} sends needed`}
</p>
</div>
</div>
</div>
) : (
<div className="flex justify-end">
<button onClick={() => startEdit(step)}
className="bg-[#DCF8C6] max-w-[88%] px-3 py-2 text-left text-[13px] leading-[1.5] text-[#303030] relative shadow-sm cursor-pointer hover:brightness-[0.97] transition-all"
style={{ borderRadius: "8px 0 8px 8px" }}>
{justSaved && (
<div className="absolute -top-2 -right-2 w-5 h-5 bg-[#25D366] flex items-center justify-center" style={{ borderRadius: "50%" }}>
<Check className="h-3 w-3 text-white" />
</div>
)}
<WhatsAppFormatted text={previewA} />
<div className="flex items-center justify-end gap-1 mt-1 -mb-0.5">
<span className="text-[9px] text-[#667781]">{clockTime}</span>
<CheckCheck className="h-3 w-3 text-[#53BDEB]" />
</div>
</button>
</div>
)}
</div>
)
})}
</div>
<div className="bg-[#F0F0F0] px-3 py-2 flex items-center gap-2">
<div className="flex-1 bg-white px-4 py-2 text-[11px] text-[#667781]" style={{ borderRadius: "20px" }}>
Donors can reply: PAID · HELP · CANCEL
</div>
<div className="w-9 h-9 bg-[#075E54] flex items-center justify-center shrink-0" style={{ borderRadius: "50%" }}>
<Send className="h-4 w-4 text-white ml-0.5" />
</div>
</div>
</div>
</div>
) : testsRunning > 0 ? (
/* ── Tests running: quiet confidence ── */
<div className="bg-[#111827] p-5 flex items-center gap-4">
<div className="relative shrink-0">
<div className="w-2.5 h-2.5 bg-[#60A5FA]" style={{ borderRadius: "50%" }} />
<span className="absolute inset-0 w-2.5 h-2.5 bg-[#60A5FA] animate-pulse" style={{ borderRadius: "50%" }} />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-bold text-white">Testing {testsRunning} new version{testsRunning > 1 ? "s" : ""}</p>
<p className="text-[11px] text-gray-500 mt-0.5">The version that converts more donors wins automatically.</p>
</div>
{stepsWithoutTest > 0 && (
<button onClick={optimiseAll} disabled={aiWorking}
className="shrink-0 border border-gray-600 text-gray-300 px-3 py-1.5 text-[11px] font-bold hover:text-white hover:border-white transition-colors disabled:opacity-50">
{aiWorking ? <Loader2 className="h-3 w-3 animate-spin" /> : `Test ${stepsWithoutTest} more`}
{/* Pick winners */}
{testsRunning > 0 && (
<button onClick={pickWinnersAndContinue} disabled={aiWorking}
className="w-full mt-4 bg-[#111827] text-white py-3 text-sm font-bold flex items-center justify-center gap-2 hover:bg-gray-800 transition-colors disabled:opacity-50">
{aiWorking
? <><Loader2 className="h-3.5 w-3.5 animate-spin" /> Picking winners</>
: <><Trophy className="h-3.5 w-3.5" /> Pick the best &amp; test new ones</>}
</button>
)}
</div>
) : (
/* ── All optimised: show results ── */
<div className="bg-[#111827] p-5 flex items-center gap-4">
<div className="w-2.5 h-2.5 bg-[#16A34A] shrink-0" style={{ borderRadius: "50%" }} />
<div className="flex-1 min-w-0">
<p className="text-sm font-bold text-white">Your messages are tuned</p>
<p className="text-[11px] text-gray-500 mt-0.5">
{stats && stats.total > 0 ? `${stats.total} sent · ${stats.deliveryRate}% delivered` : "Best-performing versions are live."}
</p>
</div>
<button onClick={optimiseAll} disabled={aiWorking}
className="shrink-0 border border-gray-600 text-gray-300 px-3 py-1.5 text-[11px] font-bold hover:text-white hover:border-white transition-colors disabled:opacity-50">
{aiWorking ? <Loader2 className="h-3 w-3 animate-spin" /> : "Test new versions"}
</button>
</div>
)}
{/* ━━ THE CONVERSATION ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Full messages always visible. No truncation.
A/B tests stack vertically — Current on top, New below.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */}
<div className="max-w-lg mx-auto">
<div className="border border-gray-300 overflow-hidden shadow-lg" style={{ borderRadius: "20px" }}>
{/* ── RIGHT: Education + context — the landing page voice ── */}
<div className="lg:col-span-5 space-y-6">
{/* WhatsApp header */}
<div className="bg-[#075E54] px-4 py-3 flex items-center gap-3">
<span className="text-white/50 text-sm"></span>
<div className="w-9 h-9 bg-[#128C7E] flex items-center justify-center" style={{ borderRadius: "50%" }}>
<MessageCircle className="h-4 w-4 text-white" />
{/* How it works — numbered steps like landing page */}
<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>
<p className="text-white text-sm font-medium">Your charity</p>
<p className="text-[10px] text-white/50">Automated messages</p>
</div>
</div>
{/* Chat */}
<div className="bg-[#ECE5DD] px-4 py-4 space-y-4" style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23000' fill-opacity='0.03'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`,
}}>
{STEP_META.map((meta) => {
const step = meta.step
const a = tpl(step, "A")
const b = tpl(step, "B")
const isEditing = editing === step
const justSaved = saved === step
const isRegenning = regenerating === step
const previewA = a ? resolvePreview(a.body) : ""
const previewB = b ? resolvePreview(b.body) : ""
const isConditional = meta.conditional
const hasDueDateTemplate = !!a
const timeLabel = step === 0 ? "Instantly" :
step === 4 ? "On the due date · if set" :
step === 1 ? `Day ${delays[1]} · if not paid` :
step === 2 ? `Day ${delays[2]} · if not paid` :
`Day ${delays[3]} · if not paid`
const clockTime = step === 0 ? "09:41" : step === 4 ? "08:00" : step === 1 ? "10:15" : step === 2 ? "09:30" : "11:00"
const rateA = a && a.sentCount > 0 ? Math.round((a.convertedCount / a.sentCount) * 100) : 0
const rateB = b && b.sentCount > 0 ? Math.round((b.convertedCount / b.sentCount) * 100) : 0
const totalSent = (a?.sentCount || 0) + (b?.sentCount || 0)
const progress = Math.min(100, Math.round((totalSent / (MIN_SAMPLE * 2)) * 100))
const hasEnoughData = (a?.sentCount || 0) >= MIN_SAMPLE && (b?.sentCount || 0) >= MIN_SAMPLE
const winner = hasEnoughData ? (rateB > rateA ? "B" : rateA > rateB ? "A" : null) : null
if (isConditional && !hasDueDateTemplate) return null
return (
<div key={step}>
{/* Timestamp */}
<div className="flex justify-center mb-3">
<span className={`text-[10px] px-3 py-1 font-medium shadow-sm flex items-center gap-1.5 ${isConditional ? "bg-[#FEF3C7] text-[#92400E]" : "bg-white/80 text-[#667781]"}`} style={{ borderRadius: "6px" }}>
{isConditional && <Calendar className="h-2.5 w-2.5" />}
{timeLabel}
</span>
<div className="divide-y divide-gray-50">
{[
{ n: "01", title: "Someone pledges", desc: "They scan your QR code or tap your link. Amount, Gift Aid, Zakat — done in 60 seconds." },
{ n: "02", title: "They get a receipt", desc: "Instantly. With your bank details and a unique reference so you can match their payment." },
{ n: "03", title: "Gentle reminders", desc: "Day 2, day 7, day 14 — if they haven't paid. Warm, never pushy. They can reply PAID anytime." },
{ n: "04", title: "Messages improve", desc: "We test different versions and keep whichever converts more pledges into payments. Automatically." },
{ n: "05", title: "You do nothing", desc: "Receipts, reminders, follow-ups — all handled. You focus on the next event." },
].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>
{isEditing ? (
/* ── EDITING ── */
<div className="flex justify-end">
<div className="bg-[#DCF8C6] max-w-[90%] w-full shadow-sm" style={{ borderRadius: "8px 0 8px 8px" }}>
<textarea ref={editorRef} value={editBody} onChange={e => setEditBody(e.target.value)}
className="w-full bg-transparent px-3 py-2 text-[13px] leading-[1.6] text-[#303030] resize-y outline-none min-h-[180px] font-mono" />
<div className="px-3 pb-2 flex items-center gap-2">
<button onClick={() => saveEdit(step)} disabled={saving}
className="bg-[#075E54] text-white px-3 py-1.5 text-[10px] font-bold flex items-center gap-1 disabled:opacity-50" style={{ borderRadius: "4px" }}>
{saving ? <Loader2 className="h-2.5 w-2.5 animate-spin" /> : <Check className="h-2.5 w-2.5" />} Save
</button>
<button onClick={() => setEditing(null)} className="text-[10px] text-[#667781]">Cancel</button>
<span className="text-[9px] text-[#667781]/50 ml-auto">{"{{name}} {{amount}} {{payment_block}}"}</span>
</div>
</div>
</div>
) : b ? (
/* ── A/B TEST — Two versions, stacked ── */
<div className="flex justify-end">
<div className="max-w-[90%] w-full" style={{ borderRadius: "8px" }}>
{/* Header bar */}
<div className="bg-[#075E54] text-white px-3 py-1.5 flex items-center gap-1.5" style={{ borderRadius: "8px 8px 0 0" }}>
<div className="w-1.5 h-1.5 bg-[#60A5FA]" style={{ borderRadius: "50%" }} />
<span className="text-[10px] font-bold flex-1">Testing two versions</span>
{hasEnoughData && winner && (
<span className="text-[9px] bg-[#16A34A]/20 text-[#4ADE80] px-1.5 py-0.5 font-bold flex items-center gap-0.5">
<Trophy className="h-2.5 w-2.5" /> {winner === "B" ? "New" : "Current"} winning
</span>
)}
{!hasEnoughData && <span className="text-[9px] text-white/40">{progress}%</span>}
</div>
{/* Variant A — Current (full message) */}
<button onClick={() => startEdit(step)} className="w-full bg-[#DCF8C6] p-3 text-left hover:brightness-[0.97] transition-all border-b border-[#075E54]/10">
<div className="flex items-center gap-1 mb-2">
<span className="text-[9px] font-bold text-[#075E54] bg-[#075E54]/10 px-1.5 py-0.5">Current</span>
{a && a.sentCount > 0 && (
<span className={`text-[9px] font-bold ml-auto ${winner === "A" ? "text-[#075E54]" : "text-[#667781]"}`}>
{rateA}% conversion{winner === "A" && " 🏆"} · {a.convertedCount}/{a.sentCount}
</span>
)}
</div>
<div className="text-[12px] leading-[1.5] text-[#303030]">
<WhatsAppFormatted text={previewA} />
</div>
</button>
{/* Variant B — New (full message) */}
<div className="bg-[#DCF8C6] p-3 relative group">
<div className="flex items-center gap-1 mb-2">
<span className="text-[9px] font-bold text-[#1E40AF] bg-[#1E40AF]/10 px-1.5 py-0.5">
New
</span>
{b.sentCount > 0 && (
<span className={`text-[9px] font-bold ml-auto ${winner === "B" ? "text-[#075E54]" : "text-[#667781]"}`}>
{rateB}% conversion{winner === "B" && " 🏆"} · {b.convertedCount}/{b.sentCount}
</span>
)}
</div>
<div className="text-[12px] leading-[1.5] text-[#303030]">
<WhatsAppFormatted text={previewB} />
</div>
{/* Regenerate */}
<button onClick={() => regenerateVariant(step)} disabled={isRegenning}
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 bg-white/90 p-1.5 shadow-sm transition-opacity hover:bg-white disabled:opacity-50"
style={{ borderRadius: "4px" }} title="Try a different version">
{isRegenning ? <Loader2 className="h-3.5 w-3.5 text-[#1E40AF] animate-spin" /> : <RefreshCw className="h-3.5 w-3.5 text-[#1E40AF]" />}
</button>
</div>
{/* Progress bar */}
<div className="bg-white/60 px-3 py-1.5" style={{ borderRadius: "0 0 8px 8px" }}>
<div className="h-1 bg-gray-200 overflow-hidden" style={{ borderRadius: "2px" }}>
<div className={`h-full transition-all ${hasEnoughData ? "bg-[#16A34A]" : "bg-[#60A5FA]"}`} style={{ width: `${progress}%` }} />
</div>
<p className="text-[9px] text-[#667781] mt-1">
{hasEnoughData
? winner ? `${winner === "B" ? "New" : "Current"} version converts ${Math.abs(rateB - rateA)}% better` : "Too close to call — need more data"
: `${totalSent} of ${MIN_SAMPLE * 2} sends needed`}
</p>
</div>
</div>
</div>
) : (
/* ── NORMAL MESSAGE (full, no truncation) ── */
<div className="flex justify-end">
<button onClick={() => startEdit(step)}
className="bg-[#DCF8C6] max-w-[88%] px-3 py-2 text-left text-[13px] leading-[1.5] text-[#303030] relative shadow-sm cursor-pointer hover:brightness-[0.97] transition-all"
style={{ borderRadius: "8px 0 8px 8px" }}>
{justSaved && (
<div className="absolute -top-2 -right-2 w-5 h-5 bg-[#25D366] flex items-center justify-center" style={{ borderRadius: "50%" }}>
<Check className="h-3 w-3 text-white" />
</div>
)}
<WhatsAppFormatted text={previewA} />
<div className="flex items-center justify-end gap-1 mt-1 -mb-0.5">
<span className="text-[9px] text-[#667781]">{clockTime}</span>
<CheckCheck className="h-3 w-3 text-[#53BDEB]" />
</div>
</button>
</div>
)}
</div>
)
})}
</div>
{/* Input bar */}
<div className="bg-[#F0F0F0] px-3 py-2 flex items-center gap-2">
<div className="flex-1 bg-white px-4 py-2 text-[11px] text-[#667781]" style={{ borderRadius: "20px" }}>
Donors can reply: PAID · HELP · CANCEL
</div>
<div className="w-9 h-9 bg-[#075E54] flex items-center justify-center shrink-0" style={{ borderRadius: "50%" }}>
<Send className="h-4 w-4 text-white ml-0.5" />
))}
</div>
</div>
</div>
</div>
{/* ── Pick winners ── */}
{testsRunning > 0 && (
<div className="max-w-lg mx-auto">
<button onClick={pickWinnersAndContinue} disabled={aiWorking}
className="w-full bg-[#111827] text-white py-3 text-sm font-bold flex items-center justify-center gap-2 hover:bg-gray-800 transition-colors disabled:opacity-50">
{aiWorking
? <><Loader2 className="h-3.5 w-3.5 animate-spin" /> Picking winners</>
: <><Trophy className="h-3.5 w-3.5" /> Pick the best &amp; test new ones</>}
</button>
</div>
)}
{/* ── Timing ── */}
<div className="max-w-lg mx-auto">
<button onClick={() => setShowTiming(!showTiming)}
className="flex items-center gap-2 text-[11px] text-gray-400 hover:text-gray-600 transition-colors py-1 font-semibold tracking-wide uppercase">
<Clock className="h-3 w-3" /> Change timing
<ChevronDown className={`h-3 w-3 transition-transform ${showTiming ? "rotate-180" : ""}`} />
</button>
{showTiming && (
<div className="grid grid-cols-3 gap-px bg-gray-200 mt-3">
{[
{ label: "First reminder", key: "step1Delay", value: config?.step1Delay || 2 },
{ label: "Second reminder", key: "step2Delay", value: config?.step2Delay || 7 },
{ label: "Final reminder", key: "step3Delay", value: config?.step3Delay || 14 },
].map(t => (
<div key={t.key} className="bg-white p-4">
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-wide mb-2">{t.label}</p>
<select value={t.value} onChange={e => saveTiming(t.key, parseInt(e.target.value))}
className="w-full border-2 border-gray-200 px-2 py-1.5 text-xs font-bold text-[#111827] bg-white focus:border-[#1E40AF] outline-none">
{[1, 2, 3, 5, 7, 10, 14, 21, 28].map(d => <option key={d} value={d}>Day {d}</option>)}
</select>
{/* Performance — if there's data */}
{totalSentAll > 0 && (
<div className="bg-white border border-gray-200">
<div className="border-b border-gray-100 px-5 py-3">
<h3 className="text-sm font-bold text-[#111827]">Performance</h3>
</div>
))}
<div className="grid grid-cols-3 gap-px bg-gray-200">
<div className="bg-white p-4 text-center">
<p className="text-xl font-black text-[#111827]">{totalSentAll}</p>
<p className="text-[9px] text-gray-500 mt-0.5">Messages sent</p>
</div>
<div className="bg-white p-4 text-center">
<p className="text-xl font-black text-[#16A34A]">{totalConverted}</p>
<p className="text-[9px] text-gray-500 mt-0.5">Pledges collected</p>
</div>
<div className="bg-white p-4 text-center">
<p className="text-xl font-black text-[#111827]">{overallRate}%</p>
<p className="text-[9px] text-gray-500 mt-0.5">Conversion rate</p>
</div>
</div>
</div>
)}
{/* Timing controls — always visible in the right column */}
<div className="bg-white border border-gray-200">
<button onClick={() => setShowTiming(!showTiming)}
className="w-full px-5 py-3 flex items-center justify-between text-left border-b border-gray-100">
<h3 className="text-sm font-bold text-[#111827]">Reminder timing</h3>
<ChevronDown className={`h-4 w-4 text-gray-400 transition-transform ${showTiming ? "rotate-180" : ""}`} />
</button>
{showTiming ? (
<div className="p-5 space-y-4">
{[
{ label: "First reminder", key: "step1Delay", value: config?.step1Delay || 2, desc: "Gentle check-in" },
{ label: "Second reminder", key: "step2Delay", value: config?.step2Delay || 7, desc: "Shows impact of their gift" },
{ label: "Final reminder", key: "step3Delay", value: config?.step3Delay || 14, desc: "Last message — no guilt, easy cancel" },
].map(t => (
<div key={t.key} className="flex items-center justify-between gap-3">
<div>
<p className="text-xs font-bold text-[#111827]">{t.label}</p>
<p className="text-[10px] text-gray-400">{t.desc}</p>
</div>
<select value={t.value} onChange={e => saveTiming(t.key, parseInt(e.target.value))}
className="border-2 border-gray-200 px-2 py-1.5 text-xs font-bold text-[#111827] bg-white focus:border-[#1E40AF] outline-none w-24">
{[1, 2, 3, 5, 7, 10, 14, 21, 28].map(d => <option key={d} value={d}>Day {d}</option>)}
</select>
</div>
))}
</div>
) : (
<div className="px-5 py-3 text-xs text-gray-400">
Day {config?.step1Delay || 2} Day {config?.step2Delay || 7} Day {config?.step3Delay || 14}
</div>
)}
</div>
)}
{/* What donors can reply */}
<div className="border-l-2 border-[#25D366] pl-4 space-y-2">
<p className="text-xs font-bold text-[#111827]">Donors can reply to any message</p>
<div className="space-y-1.5">
{[
{ cmd: "PAID", desc: "Marks their pledge as \"said they paid\" — you confirm via bank statement" },
{ cmd: "HELP", desc: "Sends them your bank details and reference again" },
{ cmd: "CANCEL", desc: "Cancels their pledge and stops all messages immediately" },
].map(c => (
<div key={c.cmd} className="flex items-start gap-2">
<code className="text-[10px] font-mono font-bold text-[#111827] bg-gray-100 px-1.5 py-0.5 shrink-0">{c.cmd}</code>
<p className="text-[10px] text-gray-500 leading-relaxed">{c.desc}</p>
</div>
))}
</div>
</div>
{/* Tips */}
<div className="border-l-2 border-[#1E40AF] pl-4 space-y-1.5">
<p className="text-xs font-bold text-[#111827]">Tips for better collection rates</p>
<p className="text-[10px] text-gray-500">Keep messages short 3 lines converts better than 10.</p>
<p className="text-[10px] text-gray-500">Always include the reference donors need it to pay.</p>
<p className="text-[10px] text-gray-500">The final message should make cancelling as easy as paying no guilt means more trust next time.</p>
</div>
</div>
</div>
</div>
)

View File

@@ -4,7 +4,7 @@ import { useState, useEffect, useCallback } from "react"
import { formatPence } from "@/lib/utils"
import {
Plus, Copy, Check, Loader2, MessageCircle, Share2, Mail,
Download, ExternalLink, Users, Trophy, ChevronDown, Link2,
Download, ExternalLink, Users, Trophy, Link2,
ArrowRight, QrCode as QrCodeIcon
} from "lucide-react"
import Image from "next/image"
@@ -64,8 +64,6 @@ export default function CollectPage() {
const [appealTarget, setAppealTarget] = useState("")
const [creatingAppeal, setCreatingAppeal] = useState(false)
const [eventSwitcherOpen, setEventSwitcherOpen] = useState(false)
const baseUrl = typeof window !== "undefined" ? window.location.origin : ""
// Load events
@@ -246,78 +244,100 @@ export default function CollectPage() {
</div>
</div>
{/* ── Appeal context (quiet for single, selector for multi) ── */}
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
{events.length === 1 ? (
<p className="text-sm font-bold text-[#111827]">{activeEvent?.name}</p>
) : (
<div className="relative mt-1">
<button
onClick={() => setEventSwitcherOpen(!eventSwitcherOpen)}
className="inline-flex items-center gap-1.5 text-sm font-bold text-[#1E40AF] hover:underline"
>
{activeEvent?.name} <ChevronDown className={`h-3.5 w-3.5 transition-transform ${eventSwitcherOpen ? "rotate-180" : ""}`} />
</button>
{eventSwitcherOpen && (
<div className="absolute z-20 mt-1 bg-white border border-gray-200 shadow-lg w-72 max-h-64 overflow-y-auto">
{events.map(ev => (
<button
key={ev.id}
onClick={() => { setActiveEventId(ev.id); setEventSwitcherOpen(false) }}
className={`w-full text-left px-4 py-3 flex items-center justify-between hover:bg-gray-50 transition-colors ${ev.id === activeEventId ? "bg-[#1E40AF]/5" : ""}`}
>
<div>
<p className="text-sm font-medium text-[#111827]">{ev.name}</p>
<p className="text-[10px] text-gray-500">{ev.pledgeCount} pledges · {formatPence(ev.totalPledged)}</p>
</div>
{ev.id === activeEventId && <Check className="h-4 w-4 text-[#1E40AF]" />}
</button>
))}
<button
onClick={() => { setShowNewAppeal(true); setEventSwitcherOpen(false) }}
className="w-full text-left px-4 py-3 border-t border-gray-100 text-sm font-semibold text-[#1E40AF] hover:bg-gray-50 flex items-center gap-1.5"
>
<Plus className="h-3.5 w-3.5" /> New appeal
</button>
</div>
)}
</div>
)}
</div>
<div className="flex gap-2">
{events.length === 1 && (
<button onClick={() => setShowNewAppeal(true)} className="text-xs font-semibold text-gray-500 hover:text-[#111827] border border-gray-200 px-3 py-1.5 transition-colors">
+ New appeal
{/* ── Appeals as visible cards (not hidden in a dropdown) ── */}
{events.length > 1 && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h2 className="text-sm font-bold text-[#111827]">Your appeals ({events.length})</h2>
<button onClick={() => setShowNewAppeal(true)} className="text-xs font-semibold text-[#1E40AF] hover:underline flex items-center gap-1">
<Plus className="h-3 w-3" /> New appeal
</button>
)}
<button
onClick={() => setShowCreate(true)}
className="inline-flex items-center gap-1.5 bg-[#111827] px-4 py-2 text-sm font-bold text-white hover:bg-gray-800 transition-colors"
>
<Plus className="h-4 w-4" /> New Link
</button>
</div>
</div>
{/* ── Appeal stats (compact — the appeal is context, not hero) ── */}
{activeEvent && (
<div className="grid grid-cols-4 gap-px bg-gray-200">
{[
{ value: String(activeEvent.pledgeCount), label: "Pledges" },
{ value: formatPence(activeEvent.totalPledged), label: "Promised" },
{ value: formatPence(activeEvent.totalCollected), label: "Received", accent: "text-[#16A34A]" },
{ value: String(sources.length), label: "Links" },
].map(stat => (
<div key={stat.label} className="bg-white p-3 md:p-4">
<p className={`text-lg md:text-xl font-black tracking-tight ${stat.accent || "text-[#111827]"}`}>{stat.value}</p>
<p className="text-[10px] text-gray-500">{stat.label}</p>
</div>
))}
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-px bg-gray-200">
{events.map(ev => {
const isSelected = ev.id === activeEventId
const rate = ev.totalPledged > 0 ? Math.round((ev.totalCollected / ev.totalPledged) * 100) : 0
const platformLabel = ev.externalPlatform
? ev.externalPlatform.charAt(0).toUpperCase() + ev.externalPlatform.slice(1)
: ev.paymentMode === "self" ? "Bank transfer" : "Bank transfer"
return (
<button
key={ev.id}
onClick={() => setActiveEventId(ev.id)}
className={`bg-white p-4 text-left hover:bg-gray-50 transition-colors ${isSelected ? "border-l-2 border-[#1E40AF]" : ""}`}
>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<p className={`text-sm font-bold truncate ${isSelected ? "text-[#1E40AF]" : "text-[#111827]"}`}>{ev.name}</p>
<p className="text-[10px] text-gray-500 mt-0.5">{platformLabel}</p>
</div>
{isSelected && <div className="w-2 h-2 bg-[#1E40AF] shrink-0 mt-1.5" />}
</div>
<div className="flex items-center gap-3 mt-3">
<div>
<p className="text-lg font-black text-[#111827]">{formatPence(ev.totalPledged)}</p>
<p className="text-[9px] text-gray-400">raised</p>
</div>
<div className="text-right ml-auto">
<p className="text-xs font-bold text-[#111827]">{ev.pledgeCount} pledges</p>
<p className="text-[9px] text-gray-400">{rate}% collected</p>
</div>
</div>
</button>
)
})}
</div>
</div>
)}
{/* Single appeal: show name + new appeal button */}
{events.length === 1 && (
<div className="flex items-center justify-between">
<p className="text-sm font-bold text-[#111827]">{activeEvent?.name}</p>
<button onClick={() => setShowNewAppeal(true)} className="text-xs font-semibold text-gray-400 hover:text-[#111827] border border-gray-200 px-3 py-1.5 transition-colors">
+ New appeal
</button>
</div>
)}
{/* ── Appeal stats + payment method clarity ── */}
{activeEvent && (
<div>
<div className="grid grid-cols-4 gap-px bg-gray-200">
{[
{ value: String(activeEvent.pledgeCount), label: "Pledges" },
{ value: formatPence(activeEvent.totalPledged), label: "Promised" },
{ value: formatPence(activeEvent.totalCollected), label: "Received", accent: "text-[#16A34A]" },
{ value: String(sources.length), label: "Links" },
].map(stat => (
<div key={stat.label} className="bg-white p-3 md:p-4">
<p className={`text-lg md:text-xl font-black tracking-tight ${stat.accent || "text-[#111827]"}`}>{stat.value}</p>
<p className="text-[10px] text-gray-500">{stat.label}</p>
</div>
))}
</div>
{/* Payment method indicator — so users know how their platform works */}
{activeEvent.externalPlatform && activeEvent.externalUrl && (
<div className="border-l-2 border-[#1E40AF] bg-[#1E40AF]/5 p-3 mt-2 flex items-center gap-2">
<ExternalLink className="h-3.5 w-3.5 text-[#1E40AF] shrink-0" />
<p className="text-xs text-gray-600">
Donors are redirected to <strong className="text-[#111827]">{activeEvent.externalPlatform.charAt(0).toUpperCase() + activeEvent.externalPlatform.slice(1)}</strong> to pay
</p>
</div>
)}
</div>
)}
{/* ── New Link button ── */}
<div className="flex justify-end">
<button
onClick={() => setShowCreate(true)}
className="inline-flex items-center gap-1.5 bg-[#111827] px-4 py-2 text-sm font-bold text-white hover:bg-gray-800 transition-colors"
>
<Plus className="h-4 w-4" /> New Link
</button>
</div>
{/* ── Inline "create link" — fast, no dialog ── */}
{showCreate && (
<div className="bg-white border-2 border-[#1E40AF] p-4 space-y-3">
@@ -434,18 +454,54 @@ export default function CollectPage() {
</div>
)}
{/* ── Tips (only show when they have links but few pledges) ── */}
{sources.length > 0 && sources.reduce((s, l) => s + l.pledgeCount, 0) < 5 && (
{/* ── How it works — landing page style education ── */}
<div className="grid md:grid-cols-2 gap-6 mt-2">
{/* Tips */}
<div className="border-l-2 border-[#1E40AF] pl-4 space-y-2">
<p className="text-xs font-bold text-[#111827]">Tips to get more pledges</p>
<ul className="text-xs text-gray-600 space-y-1.5">
<li className="flex items-start gap-2"><span className="text-[#1E40AF] font-bold shrink-0">01</span> Give each volunteer their own link friendly competition works</li>
<li className="flex items-start gap-2"><span className="text-[#1E40AF] font-bold shrink-0">02</span> Put the QR code on each table at your event</li>
<li className="flex items-start gap-2"><span className="text-[#1E40AF] font-bold shrink-0">03</span> Share directly to WhatsApp groups it takes 1 tap for them to pledge</li>
<li className="flex items-start gap-2"><span className="text-[#1E40AF] font-bold shrink-0">04</span> Post the link on your Instagram or Facebook story</li>
</ul>
<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: "Give each volunteer their own link — friendly competition works" },
{ n: "05", text: "Add it to your email newsletter or website" },
].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>
)}
{/* How platforms work */}
<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">
When someone pledges, they see your payment details with a unique reference.
Depending on how you set up your appeal:
</p>
<div className="space-y-1.5">
{[
{ label: "Bank transfer", desc: "Donor sees your sort code and account number" },
{ label: "JustGiving / LaunchGood", desc: "Donor is redirected to your fundraising page" },
{ label: "Card payment", desc: "Donor pays by Visa, Mastercard, or Apple Pay via Stripe" },
].map(p => (
<div key={p.label} className="flex items-start gap-2">
<span className="w-1.5 h-1.5 bg-[#F59E0B] shrink-0 mt-1.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>
<p className="text-[10px] text-gray-400">
Set your payment method when creating an appeal. You can change it anytime.
</p>
</div>
</div>
{/* ── New appeal inline form ── */}
{showNewAppeal && <NewAppealForm