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

@@ -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