Landing page philosophy across ALL dashboard pages

Home:
- Empty state: 2-column with 'How it works' 5-step guide
- Has data: 7/5 grid — pledges left, education right
- Right column: status breakdown, sources, 'What to do next' contextual links, 'What the statuses mean' guide

Money:
- 8/4 two-column layout: table left, education right
- Right column: 'How matching works' 4-step guide, status explainer, collection tips, quick action buttons
- No more wasted right margin

Reports:
- 7/5 two-column layout: downloads left, education right
- Right column: 'For your treasurer' 3-step guide, Gift Aid FAQ, 'Understanding your numbers' explainer
- Activity log moved to right column

Settings:
- Removed max-w-2xl constraint, now uses full width
- 7/5 two-column: checklist left, education right
- Right column: 'What you're setting up' (5 items with Required badges), Privacy & data assurance, Common questions FAQ, 'Need help?' CTA
- Every setting explained in human language
This commit is contained in:
2026-03-05 03:35:08 +08:00
parent 8366054bd7
commit e852250ce0
9 changed files with 1402 additions and 278 deletions

View File

@@ -219,120 +219,171 @@ export default function ReportsPage() {
</div>
)}
{/* ── Downloads ── */}
<div className="grid md:grid-cols-2 gap-4">
{/* Full data */}
<div className="bg-white border border-gray-200 p-6 space-y-4">
<div className="flex items-start gap-3">
<FileText className="h-5 w-5 text-[#111827] mt-0.5 shrink-0" />
<div>
<h3 className="text-base font-bold text-[#111827]">Full data download</h3>
<p className="text-xs text-gray-500 mt-1">Everything in one spreadsheet donor details, amounts, statuses, attribution, consent flags.</p>
</div>
</div>
<div className="border-l-2 border-[#111827] pl-3 grid grid-cols-2 gap-1 text-xs text-gray-600">
<span>Donor name, email, phone</span>
<span>Amount and payment status</span>
<span>Payment method and reference</span>
<span>Appeal name and source</span>
<span>Gift Aid and Zakat flags</span>
<span>Days to collect</span>
</div>
<button
onClick={() => downloadCsv(false)}
className="w-full bg-[#111827] px-4 py-2.5 text-sm font-bold text-white hover:bg-gray-800 transition-colors flex items-center justify-center gap-2"
>
<Download className="h-4 w-4" /> Download CSV
</button>
</div>
{/* ━━ TWO-COLUMN: Downloads left, Education right ━━━━━━ */}
<div className="grid lg:grid-cols-12 gap-6">
{/* Gift Aid */}
<div className="bg-white border border-[#16A34A]/30 p-6 space-y-4">
<div className="flex items-start gap-3">
<Shield className="h-5 w-5 text-[#16A34A] mt-0.5 shrink-0" />
<div>
<h3 className="text-base font-bold text-[#111827]">Gift Aid report</h3>
<p className="text-xs text-gray-500 mt-1">HMRC-ready declarations for tax reclaim. Only donors who ticked Gift Aid and whose payment was received.</p>
</div>
</div>
{/* Gift Aid preview */}
<div className="grid grid-cols-2 gap-px bg-gray-200">
<div className="bg-[#16A34A]/5 p-3">
<p className="text-lg font-black text-[#16A34A]">{giftAidCount ?? ""}</p>
<p className="text-[10px] text-gray-600">Eligible declarations</p>
</div>
<div className="bg-[#16A34A]/5 p-3">
<p className="text-lg font-black text-[#16A34A]">{formatPence(giftAidReclaimable)}</p>
<p className="text-[10px] text-gray-600">Reclaimable from HMRC</p>
</div>
</div>
<div className="bg-[#16A34A]/5 border border-[#16A34A]/20 p-3">
<p className="text-xs text-[#16A34A] font-bold">
Claim 25p for every £1 donated by a UK taxpayer
</p>
</div>
<button
onClick={() => downloadCsv(true)}
className="w-full bg-[#16A34A] px-4 py-2.5 text-sm font-bold text-white hover:bg-[#16A34A]/90 transition-colors flex items-center justify-center gap-2"
>
<Download className="h-4 w-4" /> Download Gift Aid Report
</button>
</div>
</div>
{/* ── API / Zapier ── */}
<div className="bg-white border border-gray-200 p-6 space-y-4">
<div className="flex items-start gap-3">
<Zap className="h-5 w-5 text-[#1E40AF] mt-0.5 shrink-0" />
<div>
<h3 className="text-base font-bold text-[#111827]">Connect to other tools</h3>
<p className="text-xs text-gray-500 mt-1">Pull data into Zapier, Make, or your own systems using our API.</p>
</div>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<p className="text-xs font-bold text-gray-600">Pledges endpoint</p>
<code className="block bg-[#F9FAFB] p-3 text-[11px] font-mono break-all border border-gray-100">GET /api/pledges?status=new&amp;sort=createdAt</code>
<p className="text-[10px] text-gray-500">Returns all pledges with donor contact info, filterable by status.</p>
</div>
<div className="space-y-2">
<p className="text-xs font-bold text-gray-600">Dashboard endpoint</p>
<code className="block bg-[#F9FAFB] p-3 text-[11px] font-mono break-all border border-gray-100">GET /api/dashboard</code>
<p className="text-[10px] text-gray-500">Returns summary stats, status breakdown, top sources, and all pledges.</p>
</div>
</div>
<div className="bg-[#1E40AF]/5 border border-[#1E40AF]/20 p-3">
<p className="text-xs text-[#1E40AF] font-bold">
Connect to Zapier or Make to send automatic reminder emails to donors without WhatsApp
</p>
</div>
</div>
{/* ── Activity log ── */}
{activity.length > 0 && (
<div className="bg-white border border-gray-200">
<div className="border-b border-gray-100 px-5 py-3 flex items-center gap-1.5">
<Activity className="h-4 w-4 text-gray-400" />
<h3 className="text-sm font-bold text-[#111827]">Recent activity</h3>
</div>
<div className="divide-y divide-gray-50 max-h-64 overflow-y-auto">
{activity.map((a, i) => (
<div key={a.id || i} className="px-5 py-2.5 flex items-center gap-3">
<div className="w-1.5 h-1.5 bg-gray-300 shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-xs text-gray-600 truncate">{a.description || a.action}</p>
</div>
<span className="text-[10px] text-gray-400 shrink-0">
{new Date(a.timestamp).toLocaleDateString("en-GB", { day: "numeric", month: "short" })}
</span>
{/* LEFT: Downloads + Data */}
<div className="lg:col-span-7 space-y-4">
{/* Full data */}
<div className="bg-white border border-gray-200 p-6 space-y-4">
<div className="flex items-start gap-3">
<FileText className="h-5 w-5 text-[#111827] mt-0.5 shrink-0" />
<div>
<h3 className="text-base font-bold text-[#111827]">Full data download</h3>
<p className="text-xs text-gray-500 mt-1">Everything in one spreadsheet donor details, amounts, statuses, attribution, consent flags.</p>
</div>
))}
</div>
<div className="border-l-2 border-[#111827] pl-3 grid grid-cols-2 gap-1 text-xs text-gray-600">
<span>Donor name, email, phone</span>
<span>Amount and payment status</span>
<span>Payment method and reference</span>
<span>Appeal name and source</span>
<span>Gift Aid and Zakat flags</span>
<span>Days to collect</span>
</div>
<button
onClick={() => downloadCsv(false)}
className="w-full bg-[#111827] px-4 py-2.5 text-sm font-bold text-white hover:bg-gray-800 transition-colors flex items-center justify-center gap-2"
>
<Download className="h-4 w-4" /> Download CSV
</button>
</div>
{/* Gift Aid */}
<div className="bg-white border border-[#16A34A]/30 p-6 space-y-4">
<div className="flex items-start gap-3">
<Shield className="h-5 w-5 text-[#16A34A] mt-0.5 shrink-0" />
<div>
<h3 className="text-base font-bold text-[#111827]">Gift Aid report</h3>
<p className="text-xs text-gray-500 mt-1">HMRC-ready declarations for tax reclaim. Only donors who ticked Gift Aid and whose payment was received.</p>
</div>
</div>
{/* Gift Aid preview */}
<div className="grid grid-cols-2 gap-px bg-gray-200">
<div className="bg-[#16A34A]/5 p-3">
<p className="text-lg font-black text-[#16A34A]">{giftAidCount ?? ""}</p>
<p className="text-[10px] text-gray-600">Eligible declarations</p>
</div>
<div className="bg-[#16A34A]/5 p-3">
<p className="text-lg font-black text-[#16A34A]">{formatPence(giftAidReclaimable)}</p>
<p className="text-[10px] text-gray-600">Reclaimable from HMRC</p>
</div>
</div>
<button
onClick={() => downloadCsv(true)}
className="w-full bg-[#16A34A] px-4 py-2.5 text-sm font-bold text-white hover:bg-[#16A34A]/90 transition-colors flex items-center justify-center gap-2"
>
<Download className="h-4 w-4" /> Download Gift Aid Report
</button>
</div>
{/* API / Zapier */}
<div className="bg-white border border-gray-200 p-6 space-y-4">
<div className="flex items-start gap-3">
<Zap className="h-5 w-5 text-[#1E40AF] mt-0.5 shrink-0" />
<div>
<h3 className="text-base font-bold text-[#111827]">Connect to other tools</h3>
<p className="text-xs text-gray-500 mt-1">Pull data into Zapier, Make, or your own systems using our API.</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<p className="text-xs font-bold text-gray-600">Pledges endpoint</p>
<code className="block bg-[#F9FAFB] p-3 text-[11px] font-mono break-all border border-gray-100">GET /api/pledges?status=new&amp;sort=createdAt</code>
</div>
<div className="space-y-2">
<p className="text-xs font-bold text-gray-600">Dashboard endpoint</p>
<code className="block bg-[#F9FAFB] p-3 text-[11px] font-mono break-all border border-gray-100">GET /api/dashboard</code>
</div>
</div>
</div>
</div>
)}
{/* RIGHT: Education + Context */}
<div className="lg:col-span-5 space-y-6">
{/* What your treasurer needs */}
<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]">For your treasurer</h3>
</div>
<div className="divide-y divide-gray-50">
{[
{ n: "01", title: "Download the full data CSV", desc: "Every pledge with donor details, amounts, references, and status. Open in Excel or Google Sheets." },
{ n: "02", title: "Download the Gift Aid report", desc: "Only confirmed payments from UK taxpayers who ticked Gift Aid. Ready to submit to HMRC." },
{ n: "03", title: "Submit to HMRC online", desc: "Upload the Gift Aid CSV at gov.uk/claim-gift-aid. HMRC sends the money to your charity account." },
].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>
{/* Understanding Gift Aid */}
<div className="border-l-2 border-[#16A34A] pl-4 space-y-2">
<p className="text-xs font-bold text-[#111827]">Understanding Gift Aid</p>
<div className="space-y-1.5">
{[
{ q: "What is it?", a: "HMRC gives your charity an extra 25p for every £1 donated by a UK taxpayer." },
{ q: "Who's eligible?", a: "Any donor who ticks 'I am a UK taxpayer' when they pledge. They must have paid enough income/capital gains tax." },
{ q: "How much can we claim?", a: `You have ${giftAidCount ?? 0} eligible declarations worth ${formatPence(giftAidReclaimable)} in Gift Aid.` },
{ q: "When to claim?", a: "You can claim anytime. Most charities do it quarterly or after each event." },
].map(item => (
<div key={item.q}>
<p className="text-[11px] font-bold text-[#111827]">{item.q}</p>
<p className="text-[10px] text-gray-500">{item.a}</p>
</div>
))}
</div>
</div>
{/* Understanding your collection rate */}
<div className="border-l-2 border-[#1E40AF] pl-4 space-y-2">
<p className="text-xs font-bold text-[#111827]">Understanding your numbers</p>
<div className="space-y-1.5">
<p className="text-[10px] text-gray-500">
<strong className="text-[#111827]">Collection rate</strong> what percentage of promised money has actually arrived. 70%+ is excellent for pledge-based fundraising.
</p>
<p className="text-[10px] text-gray-500">
<strong className="text-[#111827]">Still outstanding</strong> money promised but not yet received. Reminders are sent automatically for these.
</p>
<p className="text-[10px] text-gray-500">
<strong className="text-[#111827]">Per-appeal breakdown</strong> compare which events or campaigns collected best. Use this to plan your next fundraiser.
</p>
</div>
</div>
{/* Activity log */}
{activity.length > 0 && (
<div className="bg-white border border-gray-200">
<div className="border-b border-gray-100 px-5 py-3 flex items-center gap-1.5">
<Activity className="h-4 w-4 text-gray-400" />
<h3 className="text-sm font-bold text-[#111827]">Recent activity</h3>
</div>
<div className="divide-y divide-gray-50 max-h-64 overflow-y-auto">
{activity.map((a, i) => (
<div key={a.id || i} className="px-5 py-2.5 flex items-center gap-3">
<div className="w-1.5 h-1.5 bg-gray-300 shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-xs text-gray-600 truncate">{a.description || a.action}</p>
</div>
<span className="text-[10px] text-gray-400 shrink-0">
{new Date(a.timestamp).toLocaleDateString("en-GB", { day: "numeric", month: "short" })}
</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
)
}

View File

@@ -235,10 +235,46 @@ export default function DashboardPage() {
</div>
)}
{/* ── Has pledges: Stats + Feed ── */}
{/* ── Empty state: educational guidance ── */}
{isEmpty && !pledgeLink && (
<div className="grid md:grid-cols-2 gap-6">
<div className="border-2 border-dashed border-gray-200 p-8 text-center">
<h3 className="text-base font-bold text-[#111827]">Share your pledge link to get started</h3>
<p className="text-sm text-gray-500 mt-2 max-w-xs mx-auto">
Go to <Link href="/dashboard/collect" className="text-[#1E40AF] font-bold hover:underline">Collect</Link> to
create an appeal and get your pledge link.
</p>
</div>
<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: "Create a pledge link", desc: "Give it a name like 'Table 5' or 'Ramadan 2026'. Print the QR or share the link." },
{ n: "02", title: "Donors pledge in 60 seconds", desc: "Name, phone, amount, Gift Aid — done. No account, no app download." },
{ n: "03", title: "They get your bank details", desc: "Instantly via WhatsApp. With a unique reference so you can match their payment." },
{ n: "04", title: "Reminders go out automatically", desc: "Day 2, day 7, day 14. Warm and never pushy. They stop when the donor pays." },
{ n: "05", title: "Upload your bank statement", desc: "We match payments to pledges automatically. No spreadsheet cross-referencing." },
].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>
</div>
)}
{/* ── Has pledges: Stats + Feed + Education ── */}
{!isEmpty && (
<>
{/* Stats — dark inversion like landing page hero */}
{/* Stats — gap-px grid */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-px bg-gray-200">
{[
{ value: String(s.totalPledges), label: "Pledges" },
@@ -282,9 +318,9 @@ export default function DashboardPage() {
</Link>
)}
<div className="grid lg:grid-cols-5 gap-6">
{/* LEFT column */}
<div className="lg:col-span-2 space-y-6">
<div className="grid lg:grid-cols-12 gap-6">
{/* LEFT column: Data */}
<div className="lg:col-span-7 space-y-6">
{/* Needs attention */}
{needsAttention.length > 0 && (
<div className="bg-white border border-gray-200">
@@ -312,47 +348,7 @@ export default function DashboardPage() {
</div>
)}
{/* How pledges are doing */}
<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]">How pledges are doing</h3>
</div>
<div className="divide-y divide-gray-50">
{Object.entries(byStatus).map(([status, count]) => {
const sl = STATUS_LABELS[status] || STATUS_LABELS.new
return (
<div key={status} className="px-5 py-2.5 flex items-center justify-between">
<span className={`text-xs font-bold px-2 py-0.5 ${sl.bg} ${sl.color}`}>{sl.label}</span>
<span className="text-sm font-black text-[#111827]">{count as number}</span>
</div>
)
})}
</div>
</div>
{/* Where pledges come from */}
{topSources.length > 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]">Where pledges come from</h3>
</div>
<div className="divide-y divide-gray-50">
{topSources.slice(0, 5).map((src: { label: string; count: number; amount: number }, i: number) => (
<div key={i} className="px-5 py-2.5 flex items-center justify-between">
<div className="flex items-center gap-2.5">
<span className="text-xs font-black text-gray-300 w-4">{i + 1}</span>
<span className="text-sm text-[#111827]">{src.label}</span>
</div>
<span className="text-sm font-bold text-[#111827]">{formatPence(src.amount)}</span>
</div>
))}
</div>
</div>
)}
</div>
{/* RIGHT column: Recent pledges */}
<div className="lg:col-span-3">
{/* Recent pledges */}
<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]">Recent pledges</h3>
@@ -396,6 +392,98 @@ export default function DashboardPage() {
</div>
</div>
</div>
{/* RIGHT column: Status + Sources + Guidance */}
<div className="lg:col-span-5 space-y-6">
{/* How pledges are doing */}
<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]">How pledges are doing</h3>
</div>
<div className="divide-y divide-gray-50">
{Object.entries(byStatus).map(([status, count]) => {
const sl = STATUS_LABELS[status] || STATUS_LABELS.new
return (
<div key={status} className="px-5 py-2.5 flex items-center justify-between">
<span className={`text-xs font-bold px-2 py-0.5 ${sl.bg} ${sl.color}`}>{sl.label}</span>
<span className="text-sm font-black text-[#111827]">{count as number}</span>
</div>
)
})}
</div>
</div>
{/* Where pledges come from */}
{topSources.length > 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]">Where pledges come from</h3>
</div>
<div className="divide-y divide-gray-50">
{topSources.slice(0, 5).map((src: { label: string; count: number; amount: number }, i: number) => (
<div key={i} className="px-5 py-2.5 flex items-center justify-between">
<div className="flex items-center gap-2.5">
<span className="text-xs font-black text-gray-300 w-4">{i + 1}</span>
<span className="text-sm text-[#111827]">{src.label}</span>
</div>
<span className="text-sm font-bold text-[#111827]">{formatPence(src.amount)}</span>
</div>
))}
</div>
</div>
)}
{/* What to do next — contextual guidance */}
<div className="border-l-2 border-[#F59E0B] pl-4 space-y-2">
<p className="text-xs font-bold text-[#111827]">What to do next</p>
<div className="space-y-2">
{s.collectionRate < 100 && (byStatus.initiated || 0) > 0 && (
<Link href="/dashboard/money" className="flex items-start gap-2 group">
<span className="text-[#F59E0B] font-bold text-xs shrink-0 mt-0.5"></span>
<p className="text-xs text-gray-600 group-hover:text-[#111827] transition-colors">
<strong>Upload your bank statement</strong> to confirm {byStatus.initiated} {byStatus.initiated === 1 ? "payment" : "payments"} automatically
</p>
</Link>
)}
{s.collectionRate < 50 && (
<Link href="/dashboard/collect" className="flex items-start gap-2 group">
<span className="text-[#F59E0B] font-bold text-xs shrink-0 mt-0.5"></span>
<p className="text-xs text-gray-600 group-hover:text-[#111827] transition-colors">
<strong>Share your link more widely</strong> WhatsApp groups, social media, or print the QR
</p>
</Link>
)}
<Link href="/dashboard/automations" className="flex items-start gap-2 group">
<span className="text-[#F59E0B] font-bold text-xs shrink-0 mt-0.5"></span>
<p className="text-xs text-gray-600 group-hover:text-[#111827] transition-colors">
<strong>Check your messages</strong> see what donors receive and improve wording
</p>
</Link>
<Link href="/dashboard/reports" className="flex items-start gap-2 group">
<span className="text-[#F59E0B] font-bold text-xs shrink-0 mt-0.5"></span>
<p className="text-xs text-gray-600 group-hover:text-[#111827] transition-colors">
<strong>Download for your treasurer</strong> Gift Aid report, full pledge data, HMRC-ready CSV
</p>
</Link>
</div>
</div>
{/* Understanding statuses */}
<div className="border-l-2 border-[#1E40AF] pl-4 space-y-1.5">
<p className="text-xs font-bold text-[#111827]">What the statuses mean</p>
{[
{ label: "Waiting", desc: "Pledged but hasn't paid yet — reminders are being sent" },
{ label: "Said they paid", desc: "Donor replied PAID — upload bank statement to confirm" },
{ label: "Received ✓", desc: "Payment confirmed in your bank account" },
{ label: "Needs a nudge", desc: "It's been a while — you can send a manual reminder" },
].map(s => (
<div key={s.label} className="flex items-start gap-2">
<span className="w-1.5 h-1.5 bg-[#1E40AF] shrink-0 mt-1.5" />
<p className="text-[10px] text-gray-500"><strong className="text-[#111827]">{s.label}</strong> {s.desc}</p>
</div>
))}
</div>
</div>
</div>
</>
)}

View File

@@ -583,134 +583,212 @@ export default function MoneyPage() {
</div>
)}
{/* ── Search + filter ── */}
<div className="flex flex-col sm:flex-row gap-3">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
placeholder="Search name, email, reference..."
value={search}
onChange={e => { setSearch(e.target.value); setPage(0) }}
className="pl-9 w-full h-10 border-2 border-gray-200 text-sm focus:border-[#1E40AF] outline-none transition-colors"
/>
</div>
<div className="flex gap-1.5 overflow-x-auto">
{[
{ value: "all", label: "All" },
{ value: "new", label: "Waiting" },
{ value: "initiated", label: "Said paid" },
{ value: "overdue", label: "Overdue" },
{ value: "paid", label: "Received" },
].map(t => (
<button
key={t.value}
onClick={() => { setFilter(t.value); setPage(0) }}
className={`px-3 py-2 text-xs font-bold whitespace-nowrap transition-colors ${
filter === t.value ? "bg-[#111827] text-white" : "border border-gray-200 text-gray-600 hover:bg-gray-50"
}`}
>
{t.label}
</button>
))}
</div>
</div>
{/* ━━ TWO-COLUMN: Pledges left, Education right ━━━━━━━━ */}
<div className="grid lg:grid-cols-12 gap-6">
{/* ── Pledge table ── */}
{tablePledges.length === 0 ? (
<div className="text-center py-12">
<p className="text-sm font-medium text-[#111827]">No pledges found</p>
<p className="text-xs text-gray-500 mt-1">{search ? `No results for "${search}"` : "Share your pledge links to start collecting"}</p>
</div>
) : (
<div className="bg-white border border-gray-200">
<div className="hidden md:grid grid-cols-12 gap-2 px-5 py-2.5 border-b border-gray-100 text-[10px] font-bold text-gray-400 uppercase tracking-wide">
<div className="col-span-4">Donor</div>
<div className="col-span-2">Amount</div>
<div className="col-span-2">Appeal</div>
<div className="col-span-2">Status</div>
<div className="col-span-1">When</div>
<div className="col-span-1"></div>
{/* LEFT: Search + Table */}
<div className="lg:col-span-8 space-y-4">
{/* Search + filter */}
<div className="flex flex-col sm:flex-row gap-3">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
placeholder="Search name, email, reference..."
value={search}
onChange={e => { setSearch(e.target.value); setPage(0) }}
className="pl-9 w-full h-10 border-2 border-gray-200 text-sm focus:border-[#1E40AF] outline-none transition-colors"
/>
</div>
<div className="flex gap-1.5 overflow-x-auto">
{[
{ value: "all", label: "All" },
{ value: "new", label: "Waiting" },
{ value: "initiated", label: "Said paid" },
{ value: "overdue", label: "Overdue" },
{ value: "paid", label: "Received" },
].map(t => (
<button
key={t.value}
onClick={() => { setFilter(t.value); setPage(0) }}
className={`px-3 py-2 text-xs font-bold whitespace-nowrap transition-colors ${
filter === t.value ? "bg-[#111827] text-white" : "border border-gray-200 text-gray-600 hover:bg-gray-50"
}`}
>
{t.label}
</button>
))}
</div>
</div>
{tablePledges.map((p: Pledge) => {
const sl = STATUS[p.status] || STATUS.new
return (
<div key={p.id} className={`grid grid-cols-12 gap-2 px-5 py-3 border-b border-gray-50 items-center hover:bg-gray-50/50 transition-colors ${updating === p.id ? "opacity-50" : ""}`}>
<div className="col-span-6 md:col-span-4">
<p className="text-sm font-medium text-[#111827] truncate">{p.donorName || "Anonymous"}</p>
<div className="flex items-center gap-1.5 mt-0.5">
<code className="text-[10px] text-gray-400 font-mono">{p.reference}</code>
{p.donorPhone && <MessageCircle className="h-2.5 w-2.5 text-[#25D366]" />}
{/* Pledge table */}
{tablePledges.length === 0 ? (
<div className="text-center py-12">
<p className="text-sm font-medium text-[#111827]">No pledges found</p>
<p className="text-xs text-gray-500 mt-1">{search ? `No results for "${search}"` : "Share your pledge links to start collecting"}</p>
</div>
) : (
<div className="bg-white border border-gray-200">
<div className="hidden md:grid grid-cols-12 gap-2 px-5 py-2.5 border-b border-gray-100 text-[10px] font-bold text-gray-400 uppercase tracking-wide">
<div className="col-span-4">Donor</div>
<div className="col-span-2">Amount</div>
<div className="col-span-2">Appeal</div>
<div className="col-span-2">Status</div>
<div className="col-span-1">When</div>
<div className="col-span-1"></div>
</div>
{tablePledges.map((p: Pledge) => {
const sl = STATUS[p.status] || STATUS.new
return (
<div key={p.id} className={`grid grid-cols-12 gap-2 px-5 py-3 border-b border-gray-50 items-center hover:bg-gray-50/50 transition-colors ${updating === p.id ? "opacity-50" : ""}`}>
<div className="col-span-6 md:col-span-4">
<p className="text-sm font-medium text-[#111827] truncate">{p.donorName || "Anonymous"}</p>
<div className="flex items-center gap-1.5 mt-0.5">
<code className="text-[10px] text-gray-400 font-mono">{p.reference}</code>
{p.donorPhone && <MessageCircle className="h-2.5 w-2.5 text-[#25D366]" />}
</div>
</div>
<div className="col-span-3 md:col-span-2">
<p className="text-sm font-black text-[#111827]">{formatPence(p.amountPence)}</p>
{p.giftAid && <span className="text-[9px] text-[#16A34A] font-bold">+Gift Aid</span>}
{p.installmentTotal && p.installmentTotal > 1 && (
<p className="text-[9px] text-[#F59E0B] font-bold">{p.installmentNumber}/{p.installmentTotal}</p>
)}
</div>
<div className="col-span-2 hidden md:block">
<p className="text-xs text-gray-600 truncate">{p.eventName}</p>
{p.qrSourceLabel && <p className="text-[10px] text-gray-400 truncate">{p.qrSourceLabel}</p>}
</div>
<div className="col-span-2">
<span className={`text-[10px] font-bold px-1.5 py-0.5 inline-block ${sl.bg} ${sl.color}`}>{sl.label}</span>
</div>
<div className="col-span-1 hidden md:block">
<span className="text-xs text-gray-500">{timeAgo(p.createdAt)}</span>
</div>
<div className="col-span-1 text-right">
<DropdownMenu>
<DropdownMenuTrigger className="p-1.5 hover:bg-gray-100 transition-colors">
<MoreVertical className="h-4 w-4 text-gray-400" />
</DropdownMenuTrigger>
<DropdownMenuContent>
{p.status !== "paid" && (
<DropdownMenuItem onClick={() => updateStatus(p.id, "paid")}>
<CheckCircle2 className="h-4 w-4 text-[#16A34A]" /> Mark as received
</DropdownMenuItem>
)}
{p.status !== "initiated" && p.status !== "paid" && (
<DropdownMenuItem onClick={() => updateStatus(p.id, "initiated")}>
<Send className="h-4 w-4 text-[#F59E0B]" /> Mark as &quot;said they paid&quot;
</DropdownMenuItem>
)}
{p.donorPhone && p.status !== "paid" && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => sendReminder(p)}>
<MessageCircle className="h-4 w-4 text-[#25D366]" /> Send WhatsApp reminder
</DropdownMenuItem>
</>
)}
{p.status !== "cancelled" && p.status !== "paid" && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem destructive onClick={() => updateStatus(p.id, "cancelled")}>
<XCircle className="h-4 w-4" /> Cancel pledge
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
)
})}
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-between">
<p className="text-xs text-gray-500">{page * pageSize + 1}{Math.min((page + 1) * pageSize, total)} of {total}</p>
<div className="flex gap-1">
<button disabled={page === 0} onClick={() => setPage(p => p - 1)} className="border border-gray-200 p-1.5 disabled:opacity-30 hover:bg-gray-50"><ChevronLeft className="h-4 w-4" /></button>
<button disabled={page >= totalPages - 1} onClick={() => setPage(p => p + 1)} className="border border-gray-200 p-1.5 disabled:opacity-30 hover:bg-gray-50"><ChevronRight className="h-4 w-4" /></button>
</div>
</div>
)}
</div>
{/* RIGHT: Education + Context */}
<div className="lg:col-span-4 space-y-6">
{/* How matching works */}
<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 matching works</h3>
</div>
<div className="divide-y divide-gray-50">
{[
{ n: "01", title: "Donor pledges", desc: "They receive your bank details with a unique reference like PNPL-A2F4-50." },
{ n: "02", title: "They pay you", desc: "Bank transfer, JustGiving, card — using the reference you gave them." },
{ n: "03", title: "Upload your statement", desc: "Download a CSV from your bank. Drop it above — we read it in seconds." },
{ n: "04", title: "We match automatically", desc: "References, amounts, and dates — matched to the right pledge. No spreadsheet work." },
].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 className="col-span-3 md:col-span-2">
<p className="text-sm font-black text-[#111827]">{formatPence(p.amountPence)}</p>
{p.giftAid && <span className="text-[9px] text-[#16A34A] font-bold">+Gift Aid</span>}
{p.installmentTotal && p.installmentTotal > 1 && (
<p className="text-[9px] text-[#F59E0B] font-bold">{p.installmentNumber}/{p.installmentTotal}</p>
)}
</div>
<div className="col-span-2 hidden md:block">
<p className="text-xs text-gray-600 truncate">{p.eventName}</p>
{p.qrSourceLabel && <p className="text-[10px] text-gray-400 truncate">{p.qrSourceLabel}</p>}
</div>
<div className="col-span-2">
<span className={`text-[10px] font-bold px-1.5 py-0.5 inline-block ${sl.bg} ${sl.color}`}>{sl.label}</span>
</div>
<div className="col-span-1 hidden md:block">
<span className="text-xs text-gray-500">{timeAgo(p.createdAt)}</span>
</div>
<div className="col-span-1 text-right">
<DropdownMenu>
<DropdownMenuTrigger className="p-1.5 hover:bg-gray-100 transition-colors">
<MoreVertical className="h-4 w-4 text-gray-400" />
</DropdownMenuTrigger>
<DropdownMenuContent>
{p.status !== "paid" && (
<DropdownMenuItem onClick={() => updateStatus(p.id, "paid")}>
<CheckCircle2 className="h-4 w-4 text-[#16A34A]" /> Mark as received
</DropdownMenuItem>
)}
{p.status !== "initiated" && p.status !== "paid" && (
<DropdownMenuItem onClick={() => updateStatus(p.id, "initiated")}>
<Send className="h-4 w-4 text-[#F59E0B]" /> Mark as &quot;said they paid&quot;
</DropdownMenuItem>
)}
{p.donorPhone && p.status !== "paid" && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => sendReminder(p)}>
<MessageCircle className="h-4 w-4 text-[#25D366]" /> Send WhatsApp reminder
</DropdownMenuItem>
</>
)}
{p.status !== "cancelled" && p.status !== "paid" && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem destructive onClick={() => updateStatus(p.id, "cancelled")}>
<XCircle className="h-4 w-4" /> Cancel pledge
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
)
})}
</div>
)}
))}
</div>
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-between">
<p className="text-xs text-gray-500">{page * pageSize + 1}{Math.min((page + 1) * pageSize, total)} of {total}</p>
<div className="flex gap-1">
<button disabled={page === 0} onClick={() => setPage(p => p - 1)} className="border border-gray-200 p-1.5 disabled:opacity-30 hover:bg-gray-50"><ChevronLeft className="h-4 w-4" /></button>
<button disabled={page >= totalPages - 1} onClick={() => setPage(p => p + 1)} className="border border-gray-200 p-1.5 disabled:opacity-30 hover:bg-gray-50"><ChevronRight className="h-4 w-4" /></button>
{/* What the statuses mean */}
<div className="border-l-2 border-[#1E40AF] pl-4 space-y-2">
<p className="text-xs font-bold text-[#111827]">What the statuses mean</p>
<div className="space-y-1.5">
{[
{ label: "Waiting", color: "bg-gray-400", desc: "Pledged but hasn't paid yet. Reminders are being sent automatically." },
{ label: "Said they paid", color: "bg-[#F59E0B]", desc: "Donor replied PAID via WhatsApp. Upload a bank statement to confirm." },
{ label: "Received ✓", color: "bg-[#16A34A]", desc: "Payment confirmed — either matched from bank statement or manually marked." },
{ label: "Needs a nudge", color: "bg-[#DC2626]", desc: "It's been a while with no payment. Consider a personal message." },
].map(s => (
<div key={s.label} className="flex items-start gap-2">
<span className={`w-1.5 h-1.5 ${s.color} shrink-0 mt-1.5`} />
<div>
<p className="text-[11px] font-bold text-[#111827]">{s.label}</p>
<p className="text-[10px] text-gray-500">{s.desc}</p>
</div>
</div>
))}
</div>
</div>
{/* Tips */}
<div className="border-l-2 border-[#F59E0B] pl-4 space-y-1.5">
<p className="text-xs font-bold text-[#111827]">Tips for better collection</p>
<p className="text-[10px] text-gray-500">Upload your bank statement weekly the sooner you confirm, the sooner reminders stop.</p>
<p className="text-[10px] text-gray-500">Donors who say they&apos;ve paid usually have but always verify with your bank.</p>
<p className="text-[10px] text-gray-500">If someone asks &quot;where do I pay?&quot; they can reply HELP to any WhatsApp message.</p>
</div>
{/* Quick actions */}
<div className="border border-gray-200 bg-white p-4 space-y-3">
<p className="text-xs font-bold text-[#111827]">Quick actions</p>
<div className="space-y-2">
<button onClick={() => setFilter("initiated")} className="w-full text-left px-3 py-2 text-xs bg-[#F59E0B]/5 hover:bg-[#F59E0B]/10 transition-colors flex items-center gap-2">
<Clock className="h-3.5 w-3.5 text-[#F59E0B]" />
<span>Show &quot;said they paid&quot; ({stats.initiatedCount})</span>
</button>
<button onClick={() => setFilter("overdue")} className="w-full text-left px-3 py-2 text-xs bg-[#DC2626]/5 hover:bg-[#DC2626]/10 transition-colors flex items-center gap-2">
<AlertTriangle className="h-3.5 w-3.5 text-[#DC2626]" />
<span>Show overdue ({stats.overdueCount})</span>
</button>
</div>
</div>
</div>
)}
</div>
</div>
)
}

View File

@@ -186,7 +186,7 @@ export default function SettingsPage() {
: `${totalCount - doneCount} thing${totalCount - doneCount > 1 ? "s" : ""} left before you go live.`
return (
<div className="space-y-6 max-w-2xl">
<div className="space-y-6">
{/* ── Header — human progress, not a form page ── */}
<div className={`p-6 mb-6 ${doneCount === totalCount ? "bg-[#16A34A]" : "bg-[#111827]"}`}>
@@ -210,7 +210,11 @@ export default function SettingsPage() {
{error && <div className="border-l-2 border-[#DC2626] bg-[#DC2626]/5 p-3 text-sm text-[#DC2626]">{error}</div>}
{/* ── The Checklist ── */}
{/* ━━ TWO-COLUMN: Checklist left, Education right ━━━━━━ */}
<div className="grid lg:grid-cols-12 gap-6">
{/* LEFT: The Checklist */}
<div className="lg:col-span-7">
<div className="border border-gray-200 divide-y divide-gray-100 bg-white">
{/* ▸ WhatsApp ─────────────────────────── */}
@@ -501,6 +505,85 @@ export default function SettingsPage() {
</div>
</SettingRow>
</div>
</div>
{/* RIGHT: Education + Context */}
<div className="lg:col-span-5 space-y-6">
{/* What each setting does */}
<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]">What you&apos;re setting up</h3>
</div>
<div className="divide-y divide-gray-50">
{[
{ n: "01", title: "WhatsApp", desc: "Scan a QR code to connect your phone. Donors get receipts and reminders automatically. They can reply PAID, HELP, or CANCEL.", essential: true },
{ n: "02", title: "Bank account", desc: "Your sort code and account number. Shown to donors after they pledge so they know where to send money.", essential: true },
{ n: "03", title: "Your charity", desc: "Name and brand colour shown on pledge pages. Donors see this when they tap your link.", essential: true },
{ n: "04", title: "Card payments", desc: "Connect Stripe to let donors pay by Visa, Mastercard, or Apple Pay. Money goes straight to your account.", essential: false },
{ n: "05", title: "Team", desc: "Invite community leaders and volunteers. They get their own pledge links and can see their own results.", essential: false },
].map(s => (
<div key={s.n} className="px-5 py-3 flex gap-3">
<span className={`text-lg font-black shrink-0 w-6 ${s.essential ? "text-[#1E40AF]" : "text-gray-200"}`}>{s.n}</span>
<div>
<p className="text-xs font-bold text-[#111827] flex items-center gap-1.5">
{s.title}
{s.essential && <span className="text-[8px] font-bold bg-[#1E40AF]/10 text-[#1E40AF] px-1 py-0.5">Required</span>}
</p>
<p className="text-[11px] text-gray-500 leading-relaxed mt-0.5">{s.desc}</p>
</div>
</div>
))}
</div>
</div>
{/* Privacy & data */}
<div className="border-l-2 border-[#1E40AF] pl-4 space-y-2">
<p className="text-xs font-bold text-[#111827]">Privacy &amp; data</p>
<div className="space-y-1.5">
<p className="text-[10px] text-gray-500">
<strong className="text-[#111827]">Your data stays yours.</strong> We never access your Stripe account, bank details, or WhatsApp messages. Everything is stored encrypted.
</p>
<p className="text-[10px] text-gray-500">
<strong className="text-[#111827]">GDPR compliant.</strong> Donor consent is recorded at pledge time. You can export or delete all data anytime.
</p>
<p className="text-[10px] text-gray-500">
<strong className="text-[#111827]">No vendor lock-in.</strong> Download your full data as CSV from Reports. Your donors, your data, always.
</p>
</div>
</div>
{/* Common questions */}
<div className="border-l-2 border-[#F59E0B] pl-4 space-y-2">
<p className="text-xs font-bold text-[#111827]">Common questions</p>
<div className="space-y-2">
{[
{ q: "Do I need Stripe?", a: "No — most charities use bank transfer only. Stripe is optional for orgs that want card payments." },
{ q: "Can I change my bank details later?", a: "Yes. New pledges will show the updated details. Existing pledges keep the original reference." },
{ q: "What happens if WhatsApp disconnects?", a: "Reminders pause until you reconnect. Come back here, scan the QR again. It takes 30 seconds." },
{ q: "Can volunteers see financial data?", a: "No. Volunteers only see their own link performance. Admins see everything." },
].map(item => (
<div key={item.q}>
<p className="text-[11px] font-bold text-[#111827]">{item.q}</p>
<p className="text-[10px] text-gray-500">{item.a}</p>
</div>
))}
</div>
</div>
{/* Need help? */}
<div className="bg-[#111827] p-5">
<p className="text-xs font-bold text-white">Need help setting up?</p>
<p className="text-[11px] text-gray-400 mt-1 leading-relaxed">
Our team can walk you through the setup in 15 minutes. Free, no strings attached.
</p>
<a href="mailto:omair@quikcue.com" className="inline-block mt-3 border border-gray-600 px-3 py-1.5 text-[11px] font-bold text-gray-300 hover:text-white hover:border-white transition-colors">
Get in touch
</a>
</div>
</div>
</div>
</div>
)
}