production: reminder cron, dashboard overhaul, shadcn components, setup wizard
- /api/cron/reminders: processes pending reminders every 15min, sends WhatsApp with email fallback - /api/cron/overdue: marks overdue pledges daily (7d deferred, 14d immediate) - /api/pledges: GET handler with filtering, search, pagination, sort by dueDate - Dashboard overview: stats, collection progress bar, needs attention, upcoming payments - Dashboard pledges: proper table with status tabs, search, actions, pagination - New shadcn components: Table, Tabs, DropdownMenu, Progress - Setup wizard: 4-step onboarding (org → bank → event → QR code) - Settings API: PUT handler for org create/update - Org resolver: single-tenant fallback to first org - Cron jobs installed: reminders every 15min, overdue check at 6am - Auto-generates installment dates when not provided - HOSTNAME=0.0.0.0 in compose for multi-network binding
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { LayoutDashboard, Calendar, FileBarChart, Upload, Download, Settings } from "lucide-react"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { LayoutDashboard, Calendar, FileBarChart, Upload, Download, Settings, Plus, ExternalLink } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const navItems = [
|
||||
{ href: "/dashboard", label: "Overview", icon: LayoutDashboard },
|
||||
@@ -11,54 +15,65 @@ const navItems = [
|
||||
]
|
||||
|
||||
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname()
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50/50">
|
||||
{/* Top bar */}
|
||||
<header className="sticky top-0 z-40 border-b bg-white/80 backdrop-blur-xl">
|
||||
<div className="flex h-16 items-center gap-4 px-6">
|
||||
<Link href="/dashboard" className="flex items-center gap-2">
|
||||
<div className="h-8 w-8 rounded-lg bg-trust-blue flex items-center justify-center">
|
||||
<div className="flex h-14 items-center gap-4 px-4 md:px-6">
|
||||
<Link href="/dashboard" className="flex items-center gap-2.5">
|
||||
<div className="h-8 w-8 rounded-xl bg-gradient-to-br from-trust-blue to-blue-600 flex items-center justify-center shadow-lg shadow-trust-blue/20">
|
||||
<span className="text-white font-bold text-sm">P</span>
|
||||
</div>
|
||||
<span className="font-bold text-lg hidden sm:block">Pledge Now, Pay Later</span>
|
||||
<div className="hidden sm:block">
|
||||
<span className="font-black text-sm text-gray-900">PNPL</span>
|
||||
<span className="text-[10px] text-muted-foreground ml-1">Dashboard</span>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="flex-1" />
|
||||
<Link
|
||||
href="/"
|
||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
View Public Site
|
||||
<Link href="/dashboard/events" className="hidden md:block">
|
||||
<button className="inline-flex items-center gap-1.5 rounded-lg bg-trust-blue px-3 py-1.5 text-xs font-semibold text-white hover:bg-trust-blue/90 transition-colors">
|
||||
<Plus className="h-3 w-3" /> New Event
|
||||
</button>
|
||||
</Link>
|
||||
<Link href="/" className="text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1">
|
||||
<ExternalLink className="h-3 w-3" /> <span className="hidden sm:inline">Public Site</span>
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex">
|
||||
{/* Sidebar */}
|
||||
<aside className="hidden md:flex w-64 flex-col border-r bg-white min-h-[calc(100vh-4rem)] p-4">
|
||||
<nav className="space-y-1">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium text-muted-foreground hover:bg-gray-100 hover:text-foreground transition-colors"
|
||||
>
|
||||
<item.icon className="h-4 w-4" />
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
{/* Desktop sidebar */}
|
||||
<aside className="hidden md:flex w-56 flex-col border-r bg-white min-h-[calc(100vh-3.5rem)] py-3 px-2">
|
||||
<nav className="space-y-0.5">
|
||||
{navItems.map((item) => {
|
||||
const isActive = pathname === item.href || (item.href !== "/dashboard" && pathname.startsWith(item.href))
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium transition-colors",
|
||||
isActive
|
||||
? "bg-trust-blue/5 text-trust-blue"
|
||||
: "text-muted-foreground hover:bg-gray-100 hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<item.icon className="h-4 w-4" />
|
||||
{item.label}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Upsell CTA */}
|
||||
<div className="mt-auto pt-4">
|
||||
<div className="rounded-2xl bg-gradient-to-br from-trust-blue/5 to-warm-amber/5 border p-4 space-y-2">
|
||||
<p className="text-sm font-semibold">Need tech leadership?</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<div className="mt-auto px-2 pt-4">
|
||||
<div className="rounded-xl bg-gradient-to-br from-trust-blue/5 to-warm-amber/5 border p-3 space-y-1.5">
|
||||
<p className="text-xs font-semibold">Need help?</p>
|
||||
<p className="text-[10px] text-muted-foreground leading-relaxed">
|
||||
Get a fractional Head of Technology to optimise your charity's digital stack.
|
||||
</p>
|
||||
<Link
|
||||
href="/dashboard/apply"
|
||||
className="inline-block text-xs font-semibold text-trust-blue hover:underline"
|
||||
>
|
||||
<Link href="/dashboard/apply" className="inline-block text-[10px] font-semibold text-trust-blue hover:underline">
|
||||
Learn more →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -66,21 +81,27 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
||||
</aside>
|
||||
|
||||
{/* Mobile bottom nav */}
|
||||
<nav className="md:hidden fixed bottom-0 left-0 right-0 z-40 border-t bg-white/80 backdrop-blur-xl flex justify-around py-2">
|
||||
{navItems.slice(0, 5).map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="flex flex-col items-center gap-1 p-2 text-muted-foreground hover:text-trust-blue transition-colors"
|
||||
>
|
||||
<item.icon className="h-5 w-5" />
|
||||
<span className="text-[10px] font-medium">{item.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
<nav className="md:hidden fixed bottom-0 left-0 right-0 z-40 border-t bg-white/95 backdrop-blur-xl flex justify-around py-1.5 px-1">
|
||||
{navItems.slice(0, 5).map((item) => {
|
||||
const isActive = pathname === item.href || (item.href !== "/dashboard" && pathname.startsWith(item.href))
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-0.5 py-1 px-2 rounded-lg transition-colors",
|
||||
isActive ? "text-trust-blue" : "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<item.icon className="h-5 w-5" />
|
||||
<span className="text-[9px] font-medium">{item.label}</span>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 p-4 md:p-8 pb-20 md:pb-8">
|
||||
<main className="flex-1 p-4 md:p-6 pb-20 md:pb-6 max-w-6xl">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user