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,7 +3,7 @@
import { useState, useEffect, useCallback, useRef } from "react" import { useState, useEffect, useCallback, useRef } from "react"
import Image from "next/image" import Image from "next/image"
import { import {
Loader2, Check, Send, Sparkles, Trophy, CheckCheck, Loader2, Check, Send, Trophy, CheckCheck,
ChevronDown, Clock, MessageCircle, RefreshCw, Calendar ChevronDown, Clock, MessageCircle, RefreshCw, Calendar
} from "lucide-react" } from "lucide-react"
import Link from "next/link" import Link from "next/link"
@@ -152,7 +152,7 @@ export default function AutomationsPage() {
{/* ── Header ── */} {/* ── Header ── */}
<div> <div>
<div className="border-l-2 border-[#F59E0B] pl-3 mb-3"> <div className="border-l-2 border-[#F59E0B] pl-3 mb-3">
<p className="text-[11px] font-semibold tracking-[0.15em] uppercase text-gray-500">Automations</p> <p className="text-[11px] font-semibold tracking-[0.15em] uppercase text-gray-500">Donor journey</p>
</div> </div>
<h1 className="text-3xl md:text-4xl font-black text-[#111827] tracking-tight"> <h1 className="text-3xl md:text-4xl font-black text-[#111827] tracking-tight">
What your donors receive What your donors receive
@@ -169,73 +169,74 @@ export default function AutomationsPage() {
</div> </div>
)} )}
{/* ━━ AI HERO ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */} {/* ━━ HERO ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
No tech-speak. No model names. No cost breakdowns.
This is about what happens for the DONOR, not the engine.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */}
{neverOptimised ? ( {neverOptimised ? (
/* ── Never tested: invite them to start ── */
<div className="grid md:grid-cols-5 gap-0"> <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"> <div className="md:col-span-2 relative min-h-[200px] md:min-h-[280px] overflow-hidden">
<Image <Image
src="/images/brand/digital-03-notification-smile.jpg" src="/images/brand/digital-03-notification-smile.jpg"
alt="Young man at a London bus stop smiling at his phone — the moment a gentle WhatsApp reminder lands" alt="Young man smiling at his phone — the moment a gentle reminder lands"
fill className="object-cover" sizes="(max-width: 768px) 100vw, 40vw" fill className="object-cover" sizes="(max-width: 768px) 100vw, 40vw"
/> />
</div> </div>
<div className="md:col-span-3 bg-[#111827] p-8 md:p-10 flex flex-col justify-center"> <div className="md:col-span-3 bg-[#111827] p-8 md:p-10 flex flex-col justify-center">
<div className="flex items-center gap-2 mb-4">
<Sparkles className="h-4 w-4 text-[#60A5FA]" />
<p className="text-[11px] font-semibold tracking-[0.15em] uppercase text-gray-500">AI optimisation</p>
</div>
<h2 className="text-2xl md:text-3xl font-black text-white tracking-tight"> <h2 className="text-2xl md:text-3xl font-black text-white tracking-tight">
Let AI improve your&nbsp;messages Messages that improve&nbsp;themselves
</h2> </h2>
<p className="text-sm text-gray-400 leading-relaxed mt-3 max-w-md"> <p className="text-sm text-gray-400 leading-relaxed mt-3 max-w-md">
AI writes a different version of each message and tests both with real donors. We test different versions of each message with your real donors.
The better version wins automatically. Your messages get better over time without you doing anything. The one that collects more pledges wins. Automatically. You don&apos;t do anything they just get better over time.
</p> </p>
<button onClick={optimiseAll} disabled={aiWorking} <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"> 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 {aiWorking
? <><Loader2 className="h-4 w-4 animate-spin mr-2" /> AI is writing new versions</> ? <><Loader2 className="h-4 w-4 animate-spin mr-2" /> Creating new versions</>
: <><Sparkles className="h-4 w-4 mr-2" /> Start optimising</>} : "Start improving"}
</button> </button>
<p className="text-[11px] text-gray-500 mt-3">Uses GPT-4.1 nano · Costs less than 1p per message</p>
</div> </div>
</div> </div>
) : testsRunning > 0 ? ( ) : testsRunning > 0 ? (
/* ── Tests running: quiet confidence ── */
<div className="bg-[#111827] p-5 flex items-center gap-4"> <div className="bg-[#111827] p-5 flex items-center gap-4">
<div className="relative shrink-0"> <div className="relative shrink-0">
<Sparkles className="h-5 w-5 text-[#60A5FA]" /> <div className="w-2.5 h-2.5 bg-[#60A5FA]" style={{ borderRadius: "50%" }} />
<span className="absolute -top-1 -right-1 w-2.5 h-2.5 bg-[#60A5FA] animate-pulse" style={{ borderRadius: "50%" }} /> <span className="absolute inset-0 w-2.5 h-2.5 bg-[#60A5FA] animate-pulse" style={{ borderRadius: "50%" }} />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-bold text-white">AI is testing {testsRunning} experiment{testsRunning > 1 ? "s" : ""}</p> <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">Each message has two versions. The better one wins automatically.</p> <p className="text-[11px] text-gray-500 mt-0.5">The version that converts more donors wins automatically.</p>
</div> </div>
{stepsWithoutTest > 0 && ( {stepsWithoutTest > 0 && (
<button onClick={optimiseAll} disabled={aiWorking} <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"> 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" /> : `+ ${stepsWithoutTest} more`} {aiWorking ? <Loader2 className="h-3 w-3 animate-spin" /> : `Test ${stepsWithoutTest} more`}
</button> </button>
)} )}
</div> </div>
) : ( ) : (
/* ── All optimised: show results ── */
<div className="bg-[#111827] p-5 flex items-center gap-4"> <div className="bg-[#111827] p-5 flex items-center gap-4">
<Trophy className="h-5 w-5 text-[#16A34A] shrink-0" /> <div className="w-2.5 h-2.5 bg-[#16A34A] shrink-0" style={{ borderRadius: "50%" }} />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-bold text-white">Messages optimised</p> <p className="text-sm font-bold text-white">Your messages are tuned</p>
<p className="text-[11px] text-gray-500 mt-0.5"> <p className="text-[11px] text-gray-500 mt-0.5">
{stats && stats.total > 0 ? `${stats.total} sent this week · ${stats.deliveryRate}% delivered` : "Winning versions are live."} {stats && stats.total > 0 ? `${stats.total} sent · ${stats.deliveryRate}% delivered` : "Best-performing versions are live."}
</p> </p>
</div> </div>
<button onClick={optimiseAll} disabled={aiWorking} <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 flex items-center gap-1.5"> 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" /> : <><Sparkles className="h-3 w-3" /> New round</>} {aiWorking ? <Loader2 className="h-3 w-3 animate-spin" /> : "Test new versions"}
</button> </button>
</div> </div>
)} )}
{/* ━━ THE CONVERSATION ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ {/* ━━ THE CONVERSATION ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Full messages always visible. No truncation. No eclipse. Full messages always visible. No truncation.
A/B tests stack vertically — Yours on top, AI below. A/B tests stack vertically — Current on top, New below.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */}
<div className="max-w-lg mx-auto"> <div className="max-w-lg mx-auto">
<div className="border border-gray-300 overflow-hidden shadow-lg" style={{ borderRadius: "20px" }}> <div className="border border-gray-300 overflow-hidden shadow-lg" style={{ borderRadius: "20px" }}>
@@ -313,25 +314,25 @@ export default function AutomationsPage() {
</div> </div>
</div> </div>
) : b ? ( ) : b ? (
/* ── A/B TEST — FULL MESSAGES, STACKED VERTICALLY ── */ /* ── A/B TEST — Two versions, stacked ── */
<div className="flex justify-end"> <div className="flex justify-end">
<div className="max-w-[90%] w-full" style={{ borderRadius: "8px" }}> <div className="max-w-[90%] w-full" style={{ borderRadius: "8px" }}>
{/* Header bar */} {/* 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="bg-[#075E54] text-white px-3 py-1.5 flex items-center gap-1.5" style={{ borderRadius: "8px 8px 0 0" }}>
<Sparkles className="h-3 w-3 text-[#60A5FA]" /> <div className="w-1.5 h-1.5 bg-[#60A5FA]" style={{ borderRadius: "50%" }} />
<span className="text-[10px] font-bold flex-1">AI is testing</span> <span className="text-[10px] font-bold flex-1">Testing two versions</span>
{hasEnoughData && winner && ( {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"> <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" ? "AI" : "Yours"} winning <Trophy className="h-2.5 w-2.5" /> {winner === "B" ? "New" : "Current"} winning
</span> </span>
)} )}
{!hasEnoughData && <span className="text-[9px] text-white/40">{progress}%</span>} {!hasEnoughData && <span className="text-[9px] text-white/40">{progress}%</span>}
</div> </div>
{/* Variant A — Yours (full message) */} {/* 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"> <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"> <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">Yours</span> <span className="text-[9px] font-bold text-[#075E54] bg-[#075E54]/10 px-1.5 py-0.5">Current</span>
{a && a.sentCount > 0 && ( {a && a.sentCount > 0 && (
<span className={`text-[9px] font-bold ml-auto ${winner === "A" ? "text-[#075E54]" : "text-[#667781]"}`}> <span className={`text-[9px] font-bold ml-auto ${winner === "A" ? "text-[#075E54]" : "text-[#667781]"}`}>
{rateA}% conversion{winner === "A" && " 🏆"} · {a.convertedCount}/{a.sentCount} {rateA}% conversion{winner === "A" && " 🏆"} · {a.convertedCount}/{a.sentCount}
@@ -343,11 +344,11 @@ export default function AutomationsPage() {
</div> </div>
</button> </button>
{/* Variant B — AI (full message) */} {/* Variant B — New (full message) */}
<div className="bg-[#DCF8C6] p-3 relative group"> <div className="bg-[#DCF8C6] p-3 relative group">
<div className="flex items-center gap-1 mb-2"> <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 flex items-center gap-0.5"> <span className="text-[9px] font-bold text-[#1E40AF] bg-[#1E40AF]/10 px-1.5 py-0.5">
<Sparkles className="h-2 w-2" /> AI New
</span> </span>
{b.sentCount > 0 && ( {b.sentCount > 0 && (
<span className={`text-[9px] font-bold ml-auto ${winner === "B" ? "text-[#075E54]" : "text-[#667781]"}`}> <span className={`text-[9px] font-bold ml-auto ${winner === "B" ? "text-[#075E54]" : "text-[#667781]"}`}>
@@ -361,7 +362,7 @@ export default function AutomationsPage() {
{/* Regenerate */} {/* Regenerate */}
<button onClick={() => regenerateVariant(step)} disabled={isRegenning} <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" 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 AI version"> 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]" />} {isRegenning ? <Loader2 className="h-3.5 w-3.5 text-[#1E40AF] animate-spin" /> : <RefreshCw className="h-3.5 w-3.5 text-[#1E40AF]" />}
</button> </button>
</div> </div>
@@ -373,8 +374,8 @@ export default function AutomationsPage() {
</div> </div>
<p className="text-[9px] text-[#667781] mt-1"> <p className="text-[9px] text-[#667781] mt-1">
{hasEnoughData {hasEnoughData
? winner ? `${winner === "B" ? "AI" : "Your"} version converts ${Math.abs(rateB - rateA)}% better` : "Too close to call — need more data" ? 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 for a verdict`} : `${totalSent} of ${MIN_SAMPLE * 2} sends needed`}
</p> </p>
</div> </div>
</div> </div>
@@ -422,7 +423,7 @@ export default function AutomationsPage() {
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"> 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 {aiWorking
? <><Loader2 className="h-3.5 w-3.5 animate-spin" /> Picking winners</> ? <><Loader2 className="h-3.5 w-3.5 animate-spin" /> Picking winners</>
: <><Trophy className="h-3.5 w-3.5" /> Pick winners &amp; start new round</>} : <><Trophy className="h-3.5 w-3.5" /> Pick the best &amp; test new ones</>}
</button> </button>
</div> </div>
)} )}

View File

@@ -7,6 +7,7 @@ import {
Download, ExternalLink, Users, Trophy, ChevronDown, Link2, Download, ExternalLink, Users, Trophy, ChevronDown, Link2,
ArrowRight, QrCode as QrCodeIcon ArrowRight, QrCode as QrCodeIcon
} from "lucide-react" } from "lucide-react"
import Image from "next/image"
import Link from "next/link" import Link from "next/link"
import { QRCodeCanvas } from "@/components/qr-code" import { QRCodeCanvas } from "@/components/qr-code"
@@ -222,13 +223,34 @@ export default function CollectPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* ── Header: Appeal context (quiet for single, selector for multi) ── */} {/* ━━ HERO — Brand photography + context ━━━━━━━━━━━━━━━━━━━ */}
<div className="grid md:grid-cols-5 gap-0 mb-6">
<div className="md:col-span-2 relative min-h-[140px] md:min-h-[180px] overflow-hidden">
<Image
src="/images/brand/event-05-qr-scanning.jpg"
alt="Guest scanning a QR code at a fundraising event"
fill className="object-cover"
sizes="(max-width: 768px) 100vw, 40vw"
/>
</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">Collect</p>
</div>
<h1 className="text-2xl md:text-3xl font-black text-white tracking-tight">
Share your link, collect pledges
</h1>
<p className="text-sm text-gray-400 mt-2">
{activeEvent ? `${sources.length} link${sources.length !== 1 ? "s" : ""} · ${activeEvent.pledgeCount} pledges · ${formatPence(activeEvent.totalPledged)} raised` : "Create an appeal and start collecting"}
</p>
</div>
</div>
{/* ── Appeal context (quiet for single, selector for multi) ── */}
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h1 className="text-3xl font-black text-[#111827] tracking-tight">Collect</h1>
{events.length === 1 ? ( {events.length === 1 ? (
<p className="text-sm text-gray-500 mt-0.5">{activeEvent?.name}</p> <p className="text-sm font-bold text-[#111827]">{activeEvent?.name}</p>
) : ( ) : (
<div className="relative mt-1"> <div className="relative mt-1">
<button <button

View File

@@ -3,6 +3,7 @@
import { useState, useEffect, useCallback } from "react" import { useState, useEffect, useCallback } from "react"
import { formatPence } from "@/lib/utils" import { formatPence } from "@/lib/utils"
import { Download, Loader2, FileText, Shield, Zap, Activity } from "lucide-react" import { Download, Loader2, FileText, Shield, Zap, Activity } from "lucide-react"
import Image from "next/image"
/** /**
* /dashboard/reports — "My treasurer needs numbers" * /dashboard/reports — "My treasurer needs numbers"
@@ -78,15 +79,35 @@ export default function ReportsPage() {
return ( return (
<div className="space-y-8"> <div className="space-y-8">
{/* ── Header ── */} {/* ━━ HERO — Financial summary as a landing page section ━━━ */}
<div> <div className="grid md:grid-cols-5 gap-0">
<h1 className="text-3xl font-black text-[#111827] tracking-tight">Reports</h1> <div className="md:col-span-2 relative min-h-[180px] md:min-h-[240px] overflow-hidden">
<p className="text-sm text-gray-500 mt-0.5">Financial summary, Gift Aid, and data downloads for your treasurer and trustees</p> <Image
src="/images/brand/impact-03-cheque-presentation.jpg"
alt="Cheque presentation at a charity event"
fill className="object-cover"
sizes="(max-width: 768px) 100vw, 40vw"
/>
</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-4">
<p className="text-[11px] font-semibold tracking-[0.15em] uppercase text-gray-500">Reports</p>
</div>
<h1 className="text-3xl md:text-4xl font-black text-white tracking-tight">
{formatPence(s.totalCollectedPence)}
<span className="text-gray-500 text-xl md:text-2xl ml-2">received</span>
</h1>
<p className="text-sm text-gray-400 mt-1">
{formatPence(s.totalPledgedPence)} promised · {s.collectionRate}% collection rate
</p>
<p className="text-xs text-gray-500 mt-3">
Financial summary, Gift Aid declarations, and data downloads for your treasurer.
</p>
</div>
</div> </div>
{/* ── Financial summary — the big picture ── */} {/* ── Financial breakdown ── */}
<div className="bg-[#111827] p-6"> <div className="bg-[#111827] p-6">
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-widest mb-4">Financial Summary</p>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-px bg-gray-700"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-px bg-gray-700">
{[ {[
{ value: formatPence(s.totalPledgedPence), label: "Total promised", color: "text-white" }, { value: formatPence(s.totalPledgedPence), label: "Total promised", color: "text-white" },

View File

@@ -4,15 +4,20 @@ import Link from "next/link"
import { usePathname } from "next/navigation" import { usePathname } from "next/navigation"
import { useSession, signOut } from "next-auth/react" import { useSession, signOut } from "next-auth/react"
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
import { Home, Megaphone, Banknote, FileText, Settings, Plus, LogOut, Shield, AlertTriangle, MessageCircle, Users, Zap } from "lucide-react" import { Home, Megaphone, Banknote, FileText, Settings, LogOut, Shield, AlertTriangle, MessageCircle, Users, Zap } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
/** /**
* Navigation: goal-oriented, not feature-oriented * Dashboard shell — feels like the landing page, not a SaaS cockpit.
* Different nav for different roles: *
* - Admin/Staff: Full nav (Home, Collect, Money, Reports, Settings) * Design DNA from the landing page:
* - Community Leader: Scoped nav (My Community, Collect, Reports) * - Sharp edges (no rounded corners)
* - Midnight header
* - Border-l-2 active states
* - Generous typography
* - White background (not gray)
*/ */
const adminNavItems = [ const adminNavItems = [
{ href: "/dashboard", label: "Home", icon: Home }, { href: "/dashboard", label: "Home", icon: Home },
{ href: "/dashboard/collect", label: "Collect", icon: Megaphone }, { href: "/dashboard/collect", label: "Collect", icon: Megaphone },
@@ -39,7 +44,6 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
const isCommunityLeader = user?.role === "community_leader" const isCommunityLeader = user?.role === "community_leader"
const navItems = isCommunityLeader ? communityNavItems : adminNavItems const navItems = isCommunityLeader ? communityNavItems : adminNavItems
// Map old routes to new ones for active state
const isActive = (href: string) => { const isActive = (href: string) => {
if (href === "/dashboard") return pathname === "/dashboard" if (href === "/dashboard") return pathname === "/dashboard"
if (href === "/dashboard/community") return pathname === "/dashboard/community" || pathname === "/dashboard" if (href === "/dashboard/community") return pathname === "/dashboard/community" || pathname === "/dashboard"
@@ -51,30 +55,23 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
} }
return ( return (
<div className="min-h-screen bg-[#F9FAFB]"> <div className="min-h-screen bg-white">
{/* Top bar — brand-consistent: sharp, midnight, no blur */} {/* Top bar — midnight, sharp, editorial */}
<header className="sticky top-0 z-40 border-b border-gray-200 bg-white"> <header className="sticky top-0 z-40 bg-[#111827]">
<div className="flex h-14 items-center gap-4 px-4 md:px-6"> <div className="flex h-12 items-center gap-4 px-4 md:px-6">
<Link href="/dashboard" className="flex items-center gap-2.5"> <Link href="/dashboard" className="flex items-center gap-2.5">
<div className="h-7 w-7 bg-[#111827] flex items-center justify-center"> <div className="h-6 w-6 bg-white flex items-center justify-center">
<span className="text-white text-xs font-black">P</span> <span className="text-[#111827] text-[10px] font-black">P</span>
</div> </div>
<div className="hidden sm:block"> <div className="hidden sm:block">
<span className="font-black text-sm text-[#111827] tracking-tight">{user?.orgName || "Pledge Now, Pay Later"}</span> <span className="font-black text-sm text-white tracking-tight">{user?.orgName || "Pledge Now, Pay Later"}</span>
</div> </div>
</Link> </Link>
<div className="flex-1" /> <div className="flex-1" />
{!isCommunityLeader && (
<Link href="/dashboard/collect">
<button className="hidden md:inline-flex items-center gap-1.5 bg-[#111827] px-3.5 py-1.5 text-xs font-bold text-white hover:bg-gray-800 transition-colors">
<Plus className="h-3 w-3" /> New Appeal
</button>
</Link>
)}
{session && ( {session && (
<button <button
onClick={() => signOut({ callbackUrl: "/login" })} onClick={() => signOut({ callbackUrl: "/login" })}
className="text-xs text-gray-400 hover:text-[#111827] transition-colors flex items-center gap-1" className="text-xs text-gray-500 hover:text-white transition-colors flex items-center gap-1"
aria-label="Sign out" aria-label="Sign out"
> >
<LogOut className="h-3.5 w-3.5" /> <LogOut className="h-3.5 w-3.5" />
@@ -84,8 +81,8 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
</header> </header>
<div className="flex"> <div className="flex">
{/* Desktop sidebar — brand style: sharp, left-border active state */} {/* Desktop sidebar — editorial, quiet, border-l-2 active states */}
<aside className="hidden md:flex w-48 flex-col border-r border-gray-200 bg-white min-h-[calc(100vh-3.5rem)] py-4 px-2"> <aside className="hidden md:flex w-48 flex-col border-r border-gray-100 bg-white min-h-[calc(100vh-3rem)] py-6 px-2">
<nav className="space-y-0.5"> <nav className="space-y-0.5">
{navItems.map((item) => { {navItems.map((item) => {
const active = isActive(item.href) const active = isActive(item.href)
@@ -97,7 +94,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
"flex items-center gap-2.5 px-3 py-2.5 text-[13px] font-medium transition-colors", "flex items-center gap-2.5 px-3 py-2.5 text-[13px] font-medium transition-colors",
active active
? "bg-[#1E40AF]/5 text-[#1E40AF] border-l-2 border-[#1E40AF] -ml-[2px] pl-[14px]" ? "bg-[#1E40AF]/5 text-[#1E40AF] border-l-2 border-[#1E40AF] -ml-[2px] pl-[14px]"
: "text-gray-500 hover:bg-gray-50 hover:text-[#111827]" : "text-gray-400 hover:bg-gray-50 hover:text-[#111827]"
)} )}
> >
<item.icon className="h-4 w-4" /> <item.icon className="h-4 w-4" />
@@ -114,7 +111,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
"flex items-center gap-2.5 px-3 py-2.5 text-[13px] font-medium transition-colors", "flex items-center gap-2.5 px-3 py-2.5 text-[13px] font-medium transition-colors",
pathname === superAdminNav.href pathname === superAdminNav.href
? "bg-[#1E40AF]/5 text-[#1E40AF] border-l-2 border-[#1E40AF] -ml-[2px] pl-[14px]" ? "bg-[#1E40AF]/5 text-[#1E40AF] border-l-2 border-[#1E40AF] -ml-[2px] pl-[14px]"
: "text-gray-500 hover:bg-gray-50 hover:text-[#111827]" : "text-gray-400 hover:bg-gray-50 hover:text-[#111827]"
)} )}
> >
<superAdminNav.icon className="h-4 w-4" /> <superAdminNav.icon className="h-4 w-4" />
@@ -124,12 +121,12 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
)} )}
</nav> </nav>
{/* Sidebar CTA — brand style, no emoji */} {/* Sidebar footer — brand style */}
<div className="mt-auto px-1.5 pt-4"> <div className="mt-auto px-1.5 pt-6">
<div className="border-l-2 border-[#111827] pl-3 py-2"> <div className="border-l-2 border-[#111827] pl-3 py-2">
<p className="text-xs font-bold text-[#111827]">Need expert help?</p> <p className="text-xs font-bold text-[#111827]">Need expert help?</p>
<p className="text-[10px] text-gray-500 leading-relaxed mt-0.5"> <p className="text-[10px] text-gray-400 leading-relaxed mt-0.5">
Get a fractional CTO for your charity&apos;s digital stack. Fractional CTO for your charity&apos;s digital&nbsp;stack.
</p> </p>
<Link href="/dashboard/apply" className="inline-block text-[10px] font-semibold text-[#1E40AF] hover:underline mt-1"> <Link href="/dashboard/apply" className="inline-block text-[10px] font-semibold text-[#1E40AF] hover:underline mt-1">
Learn more Learn more
@@ -138,7 +135,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
</div> </div>
</aside> </aside>
{/* Mobile bottom nav — 5 items, icon + label */} {/* Mobile bottom nav */}
<nav className="md:hidden fixed bottom-0 left-0 right-0 z-40 border-t border-gray-200 bg-white flex justify-around py-1.5 px-1"> <nav className="md:hidden fixed bottom-0 left-0 right-0 z-40 border-t border-gray-200 bg-white flex justify-around py-1.5 px-1">
{navItems.map((item) => { {navItems.map((item) => {
const active = isActive(item.href) const active = isActive(item.href)
@@ -158,7 +155,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
})} })}
</nav> </nav>
{/* Main content */} {/* Main content — white background, generous padding */}
<main className="flex-1 p-4 md:p-6 lg:p-8 pb-20 md:pb-8 max-w-6xl"> <main className="flex-1 p-4 md:p-6 lg:p-8 pb-20 md:pb-8 max-w-6xl">
<WhatsAppBanner /> <WhatsAppBanner />
{children} {children}
@@ -168,7 +165,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
) )
} }
/** Persistent WhatsApp connection banner — shows until connected */ /** WhatsApp connection banner — shows until connected */
function WhatsAppBanner() { function WhatsAppBanner() {
const [status, setStatus] = useState<string | null>(null) const [status, setStatus] = useState<string | null>(null)
const [dismissed, setDismissed] = useState(false) const [dismissed, setDismissed] = useState(false)
@@ -190,7 +187,7 @@ function WhatsAppBanner() {
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-bold text-[#111827]">WhatsApp not connected reminders won&apos;t send</p> <p className="text-sm font-bold text-[#111827]">WhatsApp not connected reminders won&apos;t send</p>
<p className="text-xs text-gray-600 mt-0.5"> <p className="text-xs text-gray-600 mt-0.5">
Connect your WhatsApp so donors automatically get payment reminders. Takes 60 seconds. Connect your WhatsApp so donors automatically get payment reminders.
</p> </p>
<div className="flex items-center gap-3 mt-2.5"> <div className="flex items-center gap-3 mt-2.5">
<Link <Link

View File

@@ -3,21 +3,18 @@
import { useState, useEffect, useCallback } from "react" import { useState, useEffect, useCallback } from "react"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { useSession } from "next-auth/react" import { useSession } from "next-auth/react"
import Image from "next/image"
import { formatPence } from "@/lib/utils" import { formatPence } from "@/lib/utils"
import { ArrowRight, Loader2, MessageCircle, Copy, Check, Share2, Upload } from "lucide-react" import { ArrowRight, Loader2, MessageCircle, Copy, Check, Share2, Upload } from "lucide-react"
import Link from "next/link" 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 * Every state has:
* needs RIGHT NOW. It mirrors their internal monologue: * 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)
* State 1 (no events): "I need to set up" → redirect to /welcome * 3. Human language throughout
* 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
*/ */
const STATUS_LABELS: Record<string, { label: string; color: string; bg: string }> = { const STATUS_LABELS: Record<string, { label: string; color: string; bg: string }> = {
@@ -60,7 +57,6 @@ export default function DashboardPage() {
setLoading(false) setLoading(false)
}, []) }, [])
// Redirect community leaders to their scoped dashboard
useEffect(() => { useEffect(() => {
if (userRole === "community_leader" || userRole === "volunteer") { if (userRole === "community_leader" || userRole === "volunteer") {
router.replace("/dashboard/community") router.replace("/dashboard/community")
@@ -73,7 +69,6 @@ export default function DashboardPage() {
return () => clearInterval(i) return () => clearInterval(i)
}, [fetchAll]) }, [fetchAll])
// Fetch the first QR code link
useEffect(() => { useEffect(() => {
if (events.length > 0) { if (events.length > 0) {
fetch(`/api/events/${events[0].id}/qr`) fetch(`/api/events/${events[0].id}/qr`)
@@ -88,12 +83,10 @@ export default function DashboardPage() {
} }
}, [events]) }, [events])
// ── Loading ──
if (loading) { if (loading) {
return <div className="flex items-center justify-center py-20"><Loader2 className="h-6 w-6 text-[#1E40AF] animate-spin" /></div> 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 const hasEvents = events.length > 0
if (!hasEvents && ob) { if (!hasEvents && ob) {
const eventDone = ob.steps?.find((s: { id: string; done: boolean }) => s.id === "event" || s.id === "share")?.done 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 topSources = data?.topSources || []
const isEmpty = s.totalPledges === 0 const isEmpty = s.totalPledges === 0
const hasSaidPaid = (byStatus.initiated || 0) > 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 recentPledges = pledges.filter((p: { status: string }) => p.status !== "cancelled").slice(0, 8)
const needsAttention = [ const needsAttention = [
@@ -125,57 +120,112 @@ export default function DashboardPage() {
setTimeout(() => setCopied(false), 2000) setTimeout(() => setCopied(false), 2000)
} }
return ( // Pick contextual hero photo
<div className="space-y-8"> 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" ── */} const heroAlt = allCollected
<div className="flex items-center justify-between"> ? "Woman reading a thank-you card — the quiet moment when every pledge gets through"
<div> : 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 && ( {activeEvent && (
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-widest mb-1"> <p className="text-[10px] font-bold text-gray-400 uppercase tracking-widest mb-3">
{events.length > 1 ? `${events.length} appeals` : "Your appeal"} {activeEvent.name}
</p> </p>
)} )}
<h1 className="text-3xl font-black text-[#111827] tracking-tight">
{activeEvent?.name || "Home"} {allCollected ? (
</h1> <>
{waConnected !== null && ( <h1 className="text-2xl md:text-3xl font-black text-white tracking-tight">
<p className="text-xs text-gray-500 mt-1 flex items-center gap-1"> Every pledge collected
<MessageCircle className={`h-3 w-3 ${waConnected ? "text-[#25D366]" : "text-gray-400"}`} /> </h1>
{waConnected ? "WhatsApp connected" : "WhatsApp not connected"} <p className="text-sm text-white/70 mt-2">
</p> {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> </div>
{events.length > 1 && (
<Link href="/dashboard/collect" className="text-xs font-semibold text-[#1E40AF] hover:underline">
Switch appeal
</Link>
)}
</div> </div>
{/* ── State 2: Has event, no pledges → "Share your link" ── */} {/* ── Empty state: Share your link ── */}
{isEmpty && pledgeLink && ( {isEmpty && pledgeLink && (
<div className="space-y-6"> <div className="space-y-6">
<div className="bg-[#111827] p-6 space-y-4"> <div className="border-l-2 border-[#F59E0B] pl-3">
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">Your pledge link</p> <p className="text-[11px] font-semibold tracking-[0.15em] uppercase text-gray-500">Your pledge link</p>
<p className="text-white text-sm font-mono break-all">{pledgeLink}</p> </div>
<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="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</>} {copied ? <><Check className="h-3.5 w-3.5" /> Copied</> : <><Copy className="h-3.5 w-3.5" /> Copy</>}
</button> </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"> <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 <MessageCircle className="h-3.5 w-3.5" /> WhatsApp
</button> </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 <Share2 className="h-3.5 w-3.5" /> Share
</button> </button>
</div> </div>
</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"> <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"> <Link href={`/dashboard/events/${activeEvent?.id}`} className="text-sm font-bold text-[#111827] hover:text-[#1E40AF] transition-colors">
Create more pledge links Create more pledge links
@@ -185,10 +235,10 @@ export default function DashboardPage() {
</div> </div>
)} )}
{/* ── State 3+: Has pledges → stats + feed ── */} {/* ── Has pledges: Stats + Feed ── */}
{!isEmpty && ( {!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"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-px bg-gray-200">
{[ {[
{ value: String(s.totalPledges), label: "Pledges" }, { value: String(s.totalPledges), label: "Pledges" },
@@ -214,13 +264,13 @@ export default function DashboardPage() {
</div> </div>
<div className="flex justify-between mt-2 text-xs text-gray-500"> <div className="flex justify-between mt-2 text-xs text-gray-500">
<span>{formatPence(s.totalCollectedPence)} received</span> <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>
</div> </div>
{/* ── Contextual prompt: "said they paid" → upload bank statement ── */} {/* ── "Said they paid" prompt ── */}
{hasSaidPaid && ( {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"> <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" /> <Upload className="h-5 w-5 text-[#F59E0B] shrink-0" />
<div className="flex-1"> <div className="flex-1">
@@ -237,7 +287,7 @@ export default function DashboardPage() {
<div className="lg:col-span-2 space-y-6"> <div className="lg:col-span-2 space-y-6">
{/* Needs attention */} {/* Needs attention */}
{needsAttention.length > 0 && ( {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"> <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> <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> <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 */} {/* 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"> <div className="border-b border-gray-100 px-5 py-3">
<h3 className="text-sm font-bold text-[#111827]">How pledges are doing</h3> <h3 className="text-sm font-bold text-[#111827]">How pledges are doing</h3>
</div> </div>
@@ -280,9 +330,9 @@ export default function DashboardPage() {
</div> </div>
</div> </div>
{/* Top sources */} {/* Where pledges come from */}
{topSources.length > 0 && ( {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"> <div className="border-b border-gray-100 px-5 py-3">
<h3 className="text-sm font-bold text-[#111827]">Where pledges come from</h3> <h3 className="text-sm font-bold text-[#111827]">Where pledges come from</h3>
</div> </div>
@@ -303,7 +353,7 @@ export default function DashboardPage() {
{/* RIGHT column: Recent pledges */} {/* RIGHT column: Recent pledges */}
<div className="lg:col-span-3"> <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"> <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> <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> <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> </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> </div>
) )
} }

View File

@@ -8,6 +8,7 @@ import {
ChevronLeft, ChevronRight, Loader2, Upload, AlertTriangle, ChevronLeft, ChevronRight, Loader2, Upload, AlertTriangle,
Clock, HelpCircle, AlertCircle, FileUp, X Clock, HelpCircle, AlertCircle, FileUp, X
} from "lucide-react" } from "lucide-react"
import Image from "next/image"
import { formatPence } from "@/lib/utils" import { formatPence } from "@/lib/utils"
/** /**
@@ -259,12 +260,28 @@ export default function MoneyPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* ── Header ── */} {/* ━━ HERO — Dark stats panel like landing page ━━━━━━━━━━━━ */}
<div> <div className="grid md:grid-cols-5 gap-0 mb-6">
<h1 className="text-3xl font-black text-[#111827] tracking-tight">Money</h1> <div className="md:col-span-2 relative min-h-[140px] md:min-h-[180px] overflow-hidden">
<p className="text-sm text-gray-500 mt-0.5"> <Image
{formatPence(stats.totalCollected)} received of {formatPence(stats.totalPledged)} promised src="/images/brand/ops-06-counting-money.jpg"
</p> alt="Charity treasurer counting donations after an event"
fill className="object-cover"
sizes="(max-width: 768px) 100vw, 40vw"
/>
</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">Money</p>
</div>
<h1 className="text-3xl md:text-4xl font-black text-white tracking-tight">
{formatPence(stats.totalCollected)}
<span className="text-gray-500 text-xl md:text-2xl ml-2">of {formatPence(stats.totalPledged)}</span>
</h1>
<p className="text-sm text-gray-400 mt-1">
{collectionRate}% collected · {stats.total} pledges
</p>
</div>
</div> </div>
{/* ── Stats bar (clickable filters) ── */} {/* ── Stats bar (clickable filters) ── */}

View File

@@ -188,24 +188,24 @@ export default function SettingsPage() {
return ( return (
<div className="space-y-6 max-w-2xl"> <div className="space-y-6 max-w-2xl">
{/* ── Header ── */} {/* ── Header — human progress, not a form page ── */}
<div> <div className={`p-6 mb-6 ${doneCount === totalCount ? "bg-[#16A34A]" : "bg-[#111827]"}`}>
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">{settings.name}</p> <div className="border-l-2 border-[#F59E0B] pl-3 mb-3">
<h1 className="text-3xl font-black text-[#111827] tracking-tight">Settings</h1> <p className="text-[11px] font-semibold tracking-[0.15em] uppercase text-gray-500">Settings</p>
</div> </div>
<h1 className="text-2xl md:text-3xl font-black text-white tracking-tight">
{/* ── Progress — human sentence, not a grid ── */} {settings.name}
<div className="space-y-3"> </h1>
<div className="flex items-center gap-3"> <p className="text-sm text-gray-400 mt-2">{headerMsg}</p>
<div className="flex-1 h-1.5 bg-gray-100 overflow-hidden"> <div className="flex items-center gap-3 mt-4">
<div className="flex-1 h-1.5 bg-white/10 overflow-hidden">
<div <div
className={`h-full transition-all duration-700 ${doneCount === totalCount ? "bg-[#16A34A]" : "bg-[#1E40AF]"}`} className={`h-full transition-all duration-700 ${doneCount === totalCount ? "bg-white" : "bg-[#1E40AF]"}`}
style={{ width: `${Math.round((doneCount / totalCount) * 100)}%` }} style={{ width: `${Math.round((doneCount / totalCount) * 100)}%` }}
/> />
</div> </div>
<span className="text-xs font-bold text-[#111827] shrink-0">{doneCount}/{totalCount}</span> <span className="text-xs font-bold text-white shrink-0">{doneCount}/{totalCount}</span>
</div> </div>
<p className="text-sm text-gray-500">{headerMsg}</p>
</div> </div>
{error && <div className="border-l-2 border-[#DC2626] bg-[#DC2626]/5 p-3 text-sm text-[#DC2626]">{error}</div>} {error && <div className="border-l-2 border-[#DC2626] bg-[#DC2626]/5 p-3 text-sm text-[#DC2626]">{error}</div>}