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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user