Platform overhaul: every dashboard page feels like a landing page

- Layout: Midnight header, white background, editorial sidebar
- Home: Brand photography hero with contextual state (empty/active/collected)
- Automations: ALL tech-speak stripped (no GPT model names, no cost per message, no 'AI optimisation' labels). Hero is about outcomes not engine. 'Current'/'New' labels replace 'Yours'/'AI'.
- Collect: Brand photography hero with event context
- Money: Dark hero with key financials + photography
- Reports: Landing page compliance-style financial hero
- Settings: Dark progress header with brand treatment

Brand DNA applied across all pages:
- Image + dark panel hero sections
- border-l-2 section labels
- gap-px grids for data
- Sharp edges, no rounded corners
- Human language throughout
- 60-30-10 color rule enforced
This commit is contained in:
2026-03-05 03:03:55 +08:00
parent 097f13f7be
commit 3c3336383e
7 changed files with 263 additions and 163 deletions

View File

@@ -3,21 +3,18 @@
import { useState, useEffect, useCallback } from "react"
import { useRouter } from "next/navigation"
import { useSession } from "next-auth/react"
import Image from "next/image"
import { formatPence } from "@/lib/utils"
import { ArrowRight, Loader2, MessageCircle, Copy, Check, Share2, Upload } from "lucide-react"
import Link from "next/link"
/**
* Context-aware dashboard.
* Context-aware dashboard — feels like a landing page for YOUR data.
*
* Instead of a static layout, this page morphs based on what the user
* needs RIGHT NOW. It mirrors their internal monologue:
*
* State 1 (no events): "I need to set up" → redirect to /welcome
* State 2 (0 pledges): "Did it work? How do I share?" → show link + share buttons
* State 3 (has pledges): "Who's pledged? Who's paid?" → show feed + stats
* State 4 (has overdue): "Who needs chasing?" → promote attention items
* State 5 (has "said paid"): "Did the money arrive?" → prompt bank upload
* Every state has:
* 1. A hero section (brand photography + dark panel with the key metric)
* 2. Content sections that use landing page patterns (gap-px, border-l-2)
* 3. Human language throughout
*/
const STATUS_LABELS: Record<string, { label: string; color: string; bg: string }> = {
@@ -60,7 +57,6 @@ export default function DashboardPage() {
setLoading(false)
}, [])
// Redirect community leaders to their scoped dashboard
useEffect(() => {
if (userRole === "community_leader" || userRole === "volunteer") {
router.replace("/dashboard/community")
@@ -73,7 +69,6 @@ export default function DashboardPage() {
return () => clearInterval(i)
}, [fetchAll])
// Fetch the first QR code link
useEffect(() => {
if (events.length > 0) {
fetch(`/api/events/${events[0].id}/qr`)
@@ -88,12 +83,10 @@ export default function DashboardPage() {
}
}, [events])
// ── Loading ──
if (loading) {
return <div className="flex items-center justify-center py-20"><Loader2 className="h-6 w-6 text-[#1E40AF] animate-spin" /></div>
}
// ── State 1: No events → redirect to welcome ──
const hasEvents = events.length > 0
if (!hasEvents && ob) {
const eventDone = ob.steps?.find((s: { id: string; done: boolean }) => s.id === "event" || s.id === "share")?.done
@@ -109,6 +102,8 @@ export default function DashboardPage() {
const topSources = data?.topSources || []
const isEmpty = s.totalPledges === 0
const hasSaidPaid = (byStatus.initiated || 0) > 0
const allCollected = !isEmpty && s.collectionRate === 100
const outstanding = s.totalPledgedPence - s.totalCollectedPence
const recentPledges = pledges.filter((p: { status: string }) => p.status !== "cancelled").slice(0, 8)
const needsAttention = [
@@ -125,57 +120,112 @@ export default function DashboardPage() {
setTimeout(() => setCopied(false), 2000)
}
return (
<div className="space-y-8">
// Pick contextual hero photo
const heroPhoto = allCollected
? "/images/brand/impact-02-thank-you-letter.jpg"
: isEmpty
? "/images/brand/product-setup-05-briefing-volunteers.jpg"
: "/images/brand/event-03-table-conversation.jpg"
{/* ── Context header: shows event name, not generic "Home" ── */}
<div className="flex items-center justify-between">
<div>
const heroAlt = allCollected
? "Woman reading a thank-you card — the quiet moment when every pledge gets through"
: isEmpty
? "Event organiser briefing volunteers before a fundraising dinner"
: "Friends at a charity dinner — where pledges begin"
return (
<div className="space-y-6">
{/* ━━ HERO — Brand photography + the one thing that matters ━━━ */}
<div className="grid md:grid-cols-5 gap-0">
<div className="md:col-span-3 relative min-h-[180px] md:min-h-[240px] overflow-hidden">
<Image
src={heroPhoto}
alt={heroAlt}
fill className="object-cover"
sizes="(max-width: 768px) 100vw, 60vw"
priority
/>
</div>
<div className={`md:col-span-2 p-6 md:p-8 flex flex-col justify-center ${allCollected ? "bg-[#16A34A]" : "bg-[#111827]"}`}>
{activeEvent && (
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-widest mb-1">
{events.length > 1 ? `${events.length} appeals` : "Your appeal"}
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-widest mb-3">
{activeEvent.name}
</p>
)}
<h1 className="text-3xl font-black text-[#111827] tracking-tight">
{activeEvent?.name || "Home"}
</h1>
{waConnected !== null && (
<p className="text-xs text-gray-500 mt-1 flex items-center gap-1">
<MessageCircle className={`h-3 w-3 ${waConnected ? "text-[#25D366]" : "text-gray-400"}`} />
{waConnected ? "WhatsApp connected" : "WhatsApp not connected"}
</p>
{allCollected ? (
<>
<h1 className="text-2xl md:text-3xl font-black text-white tracking-tight">
Every pledge collected
</h1>
<p className="text-sm text-white/70 mt-2">
{formatPence(s.totalCollectedPence)} received from {s.totalPledges} donors
</p>
</>
) : isEmpty ? (
<>
<h1 className="text-2xl md:text-3xl font-black text-white tracking-tight">
Ready to collect pledges
</h1>
<p className="text-sm text-gray-400 mt-2">
Share your link pledges will appear here as they come in.
</p>
{waConnected !== null && (
<p className="text-xs text-gray-500 mt-3 flex items-center gap-1.5">
<MessageCircle className={`h-3 w-3 ${waConnected ? "text-[#25D366]" : "text-gray-500"}`} />
{waConnected ? "WhatsApp connected — reminders active" : "WhatsApp not connected"}
</p>
)}
</>
) : (
<>
<h1 className="text-3xl md:text-4xl font-black text-white tracking-tight">
{formatPence(outstanding)}
</h1>
<p className="text-sm text-gray-400 mt-1">
still to come · {s.collectionRate}% collected
</p>
{/* Contextual CTA — what should they do RIGHT NOW? */}
{hasSaidPaid ? (
<Link href="/dashboard/money" className="mt-4 inline-flex items-center gap-2 bg-white px-4 py-2.5 text-xs font-bold text-[#111827] hover:bg-gray-100 transition-colors self-start">
<Upload className="h-3.5 w-3.5" /> Upload bank statement
</Link>
) : (byStatus.overdue || 0) > 0 ? (
<Link href="/dashboard/money" className="mt-4 inline-flex items-center gap-2 border border-gray-600 px-4 py-2.5 text-xs font-bold text-gray-300 hover:text-white hover:border-white transition-colors self-start">
{byStatus.overdue} {byStatus.overdue === 1 ? "pledge needs" : "pledges need"} a nudge <ArrowRight className="h-3 w-3" />
</Link>
) : null}
</>
)}
</div>
{events.length > 1 && (
<Link href="/dashboard/collect" className="text-xs font-semibold text-[#1E40AF] hover:underline">
Switch appeal
</Link>
)}
</div>
{/* ── State 2: Has event, no pledges → "Share your link" ── */}
{/* ── Empty state: Share your link ── */}
{isEmpty && pledgeLink && (
<div className="space-y-6">
<div className="bg-[#111827] p-6 space-y-4">
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">Your pledge link</p>
<p className="text-white text-sm font-mono break-all">{pledgeLink}</p>
<div className="grid grid-cols-3 gap-2">
<button onClick={copyLink} className="bg-white/10 hover:bg-white/20 py-2.5 text-xs font-bold text-white transition-colors flex items-center justify-center gap-1.5">
<div className="border-l-2 border-[#F59E0B] pl-3">
<p className="text-[11px] font-semibold tracking-[0.15em] uppercase text-gray-500">Your pledge link</p>
</div>
<div className="bg-white border border-gray-200 p-5 space-y-4">
<p className="text-sm font-mono text-gray-500 break-all">{pledgeLink}</p>
<div className="grid grid-cols-3 gap-1.5">
<button onClick={copyLink} className={`py-2.5 text-xs font-bold transition-colors flex items-center justify-center gap-1.5 ${
copied ? "bg-[#16A34A] text-white" : "bg-[#111827] text-white hover:bg-gray-800"
}`}>
{copied ? <><Check className="h-3.5 w-3.5" /> Copied</> : <><Copy className="h-3.5 w-3.5" /> Copy</>}
</button>
<button onClick={() => window.open(`https://wa.me/?text=${encodeURIComponent(`Please pledge for ${activeEvent?.name}:\n${pledgeLink}`)}`, "_blank")} className="bg-[#25D366] hover:bg-[#25D366]/90 py-2.5 text-xs font-bold text-white transition-colors flex items-center justify-center gap-1.5">
<MessageCircle className="h-3.5 w-3.5" /> WhatsApp
</button>
<button onClick={() => navigator.share?.({ url: pledgeLink, title: activeEvent?.name })} className="bg-white/10 hover:bg-white/20 py-2.5 text-xs font-bold text-white transition-colors flex items-center justify-center gap-1.5">
<button onClick={() => navigator.share?.({ url: pledgeLink, title: activeEvent?.name })} className="border border-gray-200 hover:bg-gray-50 py-2.5 text-xs font-bold text-[#111827] transition-colors flex items-center justify-center gap-1.5">
<Share2 className="h-3.5 w-3.5" /> Share
</button>
</div>
</div>
<div className="text-center py-6">
<p className="text-sm text-gray-500">No pledges yet share your link and they&apos;ll appear here</p>
</div>
<div className="border-l-2 border-[#1E40AF] pl-4">
<Link href={`/dashboard/events/${activeEvent?.id}`} className="text-sm font-bold text-[#111827] hover:text-[#1E40AF] transition-colors">
Create more pledge links
@@ -185,10 +235,10 @@ export default function DashboardPage() {
</div>
)}
{/* ── State 3+: Has pledges → stats + feed ── */}
{/* ── Has pledges: Stats + Feed ── */}
{!isEmpty && (
<>
{/* Big numbers */}
{/* Stats — dark inversion like landing page hero */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-px bg-gray-200">
{[
{ value: String(s.totalPledges), label: "Pledges" },
@@ -214,13 +264,13 @@ export default function DashboardPage() {
</div>
<div className="flex justify-between mt-2 text-xs text-gray-500">
<span>{formatPence(s.totalCollectedPence)} received</span>
<span>{formatPence(s.totalPledgedPence - s.totalCollectedPence)} still to come</span>
<span>{formatPence(outstanding)} still to come</span>
</div>
</div>
{/* ── Contextual prompt: "said they paid" → upload bank statement ── */}
{/* ── "Said they paid" prompt ── */}
{hasSaidPaid && (
<Link href="/dashboard/reconcile">
<Link href="/dashboard/money">
<div className="border-l-2 border-[#F59E0B] bg-[#F59E0B]/5 p-4 flex items-center gap-3 hover:bg-[#F59E0B]/10 transition-colors cursor-pointer">
<Upload className="h-5 w-5 text-[#F59E0B] shrink-0" />
<div className="flex-1">
@@ -237,7 +287,7 @@ export default function DashboardPage() {
<div className="lg:col-span-2 space-y-6">
{/* Needs attention */}
{needsAttention.length > 0 && (
<div className="bg-white">
<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]">Needs attention</h3>
<span className="text-[10px] font-bold text-white bg-[#DC2626] px-1.5 py-0.5">{needsAttention.length}</span>
@@ -263,7 +313,7 @@ export default function DashboardPage() {
)}
{/* How pledges are doing */}
<div className="bg-white">
<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>
@@ -280,9 +330,9 @@ export default function DashboardPage() {
</div>
</div>
{/* Top sources */}
{/* Where pledges come from */}
{topSources.length > 0 && (
<div className="bg-white">
<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>
@@ -303,7 +353,7 @@ export default function DashboardPage() {
{/* RIGHT column: Recent pledges */}
<div className="lg:col-span-3">
<div className="bg-white">
<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>
<Link href="/dashboard/money" className="text-xs font-semibold text-[#1E40AF] hover:underline">View all</Link>
@@ -349,14 +399,6 @@ export default function DashboardPage() {
</div>
</>
)}
{/* ── State: all paid → celebration ── */}
{!isEmpty && s.collectionRate === 100 && (
<div className="bg-[#16A34A]/5 border border-[#16A34A]/20 p-6 text-center">
<p className="text-2xl font-black text-[#16A34A]">Every pledge collected</p>
<p className="text-sm text-gray-600 mt-1">{formatPence(s.totalCollectedPence)} received from {s.totalPledges} donors</p>
</div>
)}
</div>
)
}