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