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:
@@ -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 messages
|
Messages that improve 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'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 & start new round</>}
|
: <><Trophy className="h-3.5 w-3.5" /> Pick the best & test new ones</>}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
|||||||
@@ -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's digital stack.
|
Fractional CTO for your charity's digital 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't send</p>
|
<p className="text-sm font-bold text-[#111827]">WhatsApp not connected — reminders won'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
|
||||||
|
|||||||
@@ -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'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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) ── */}
|
||||||
|
|||||||
@@ -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>}
|
||||||
|
|||||||
Reference in New Issue
Block a user