demo login, super admin view, password reset
- Landing page: 'Try the Demo' button links to /login?demo=1 - Login page: 'Try the Demo — no signup needed' button auto-logs in as demo@pnpl.app - /login?demo=1: auto-triggers demo login on page load - Super Admin page (/dashboard/admin): platform stats, org list, user list, recent pledges - /api/admin: returns cross-org data, gated by super_admin role check - Sidebar shows 'Super Admin' link only for super_admin users - Password reset: omair@quikcue.com = Omair2026!, demo@pnpl.app = demo1234 - omair@quikcue.com confirmed as super_admin role
This commit is contained in:
@@ -1,25 +1,27 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState, useEffect, Suspense } from "react"
|
||||||
import { signIn } from "next-auth/react"
|
import { signIn } from "next-auth/react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter, useSearchParams } from "next/navigation"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
|
||||||
export default function LoginPage() {
|
function LoginForm() {
|
||||||
const [email, setEmail] = useState("")
|
const [email, setEmail] = useState("")
|
||||||
const [password, setPassword] = useState("")
|
const [password, setPassword] = useState("")
|
||||||
const [error, setError] = useState("")
|
const [error, setError] = useState("")
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const isDemo = searchParams.get("demo") === "1"
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const doLogin = async (e?: React.FormEvent, demoEmail?: string, demoPass?: string) => {
|
||||||
e.preventDefault()
|
if (e) e.preventDefault()
|
||||||
setError("")
|
setError("")
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
const result = await signIn("credentials", {
|
const result = await signIn("credentials", {
|
||||||
email,
|
email: demoEmail || email,
|
||||||
password,
|
password: demoPass || password,
|
||||||
redirect: false,
|
redirect: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -31,6 +33,12 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => doLogin(e)
|
||||||
|
|
||||||
|
// Auto-login as demo if ?demo=1
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
useEffect(() => { if (isDemo) doLogin(undefined, "demo@pnpl.app", "demo1234") }, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-trust-blue/5 via-white to-warm-amber/5 p-4">
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-trust-blue/5 via-white to-warm-amber/5 p-4">
|
||||||
<div className="w-full max-w-sm space-y-6">
|
<div className="w-full max-w-sm space-y-6">
|
||||||
@@ -82,6 +90,20 @@ export default function LoginPage() {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-0 flex items-center"><div className="w-full border-t" /></div>
|
||||||
|
<div className="relative flex justify-center text-xs"><span className="bg-gradient-to-br from-trust-blue/5 via-white to-warm-amber/5 px-2 text-muted-foreground">or</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => doLogin(undefined, "demo@pnpl.app", "demo1234")}
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full rounded-xl border-2 border-dashed border-gray-200 px-4 py-3 text-sm font-medium text-muted-foreground hover:border-trust-blue hover:text-trust-blue disabled:opacity-50 transition-all"
|
||||||
|
>
|
||||||
|
🎮 Try the Demo — no signup needed
|
||||||
|
</button>
|
||||||
|
|
||||||
<p className="text-center text-sm text-muted-foreground">
|
<p className="text-center text-sm text-muted-foreground">
|
||||||
Don't have an account?{" "}
|
Don't have an account?{" "}
|
||||||
<Link href="/signup" className="text-trust-blue font-semibold hover:underline">
|
<Link href="/signup" className="text-trust-blue font-semibold hover:underline">
|
||||||
@@ -92,3 +114,11 @@ export default function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<div className="min-h-screen flex items-center justify-center"><div className="animate-spin h-8 w-8 border-2 border-trust-blue border-t-transparent rounded-full" /></div>}>
|
||||||
|
<LoginForm />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
99
pledge-now-pay-later/src/app/api/admin/route.ts
Normal file
99
pledge-now-pay-later/src/app/api/admin/route.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { NextResponse } from "next/server"
|
||||||
|
import prisma from "@/lib/prisma"
|
||||||
|
import { getUser } from "@/lib/session"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/admin — Super admin overview
|
||||||
|
* Returns all orgs, users, pledges across the entire platform.
|
||||||
|
*/
|
||||||
|
export async function GET() {
|
||||||
|
const user = await getUser()
|
||||||
|
if (!user || user.role !== "super_admin") {
|
||||||
|
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prisma) return NextResponse.json({ error: "No DB" }, { status: 503 })
|
||||||
|
|
||||||
|
const [orgs, users, pledgeStats, recentPledges, eventCount] = await Promise.all([
|
||||||
|
prisma.organization.findMany({
|
||||||
|
select: {
|
||||||
|
id: true, name: true, slug: true, country: true, createdAt: true,
|
||||||
|
bankSortCode: true, bankAccountNo: true,
|
||||||
|
_count: { select: { users: true, events: true, pledges: true } },
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
}),
|
||||||
|
prisma.user.findMany({
|
||||||
|
select: {
|
||||||
|
id: true, email: true, name: true, role: true, createdAt: true,
|
||||||
|
organization: { select: { name: true } },
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
}),
|
||||||
|
prisma.pledge.groupBy({
|
||||||
|
by: ["status"],
|
||||||
|
_count: true,
|
||||||
|
_sum: { amountPence: true },
|
||||||
|
}),
|
||||||
|
prisma.pledge.findMany({
|
||||||
|
select: {
|
||||||
|
id: true, reference: true, amountPence: true, status: true,
|
||||||
|
donorName: true, createdAt: true, dueDate: true,
|
||||||
|
event: { select: { name: true } },
|
||||||
|
organization: { select: { name: true } },
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
take: 20,
|
||||||
|
}),
|
||||||
|
prisma.event.count(),
|
||||||
|
])
|
||||||
|
|
||||||
|
const totalPledgedPence = pledgeStats.reduce((s, p) => s + (p._sum.amountPence || 0), 0)
|
||||||
|
const totalCollectedPence = pledgeStats
|
||||||
|
.filter(p => p.status === "paid")
|
||||||
|
.reduce((s, p) => s + (p._sum.amountPence || 0), 0)
|
||||||
|
const totalPledgeCount = pledgeStats.reduce((s, p) => s + p._count, 0)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
platform: {
|
||||||
|
orgs: orgs.length,
|
||||||
|
users: users.length,
|
||||||
|
events: eventCount,
|
||||||
|
pledges: totalPledgeCount,
|
||||||
|
totalPledgedPence,
|
||||||
|
totalCollectedPence,
|
||||||
|
collectionRate: totalPledgedPence > 0 ? Math.round((totalCollectedPence / totalPledgedPence) * 100) : 0,
|
||||||
|
},
|
||||||
|
orgs: orgs.map(o => ({
|
||||||
|
id: o.id,
|
||||||
|
name: o.name,
|
||||||
|
slug: o.slug,
|
||||||
|
country: o.country,
|
||||||
|
hasBank: !!(o.bankSortCode && o.bankAccountNo),
|
||||||
|
users: o._count.users,
|
||||||
|
events: o._count.events,
|
||||||
|
pledges: o._count.pledges,
|
||||||
|
createdAt: o.createdAt,
|
||||||
|
})),
|
||||||
|
users: users.map(u => ({
|
||||||
|
id: u.id,
|
||||||
|
email: u.email,
|
||||||
|
name: u.name,
|
||||||
|
role: u.role,
|
||||||
|
orgName: u.organization.name,
|
||||||
|
createdAt: u.createdAt,
|
||||||
|
})),
|
||||||
|
byStatus: Object.fromEntries(pledgeStats.map(p => [p.status, { count: p._count, amount: p._sum.amountPence || 0 }])),
|
||||||
|
recentPledges: recentPledges.map(p => ({
|
||||||
|
id: p.id,
|
||||||
|
reference: p.reference,
|
||||||
|
amountPence: p.amountPence,
|
||||||
|
status: p.status,
|
||||||
|
donorName: p.donorName,
|
||||||
|
eventName: p.event.name,
|
||||||
|
orgName: p.organization.name,
|
||||||
|
dueDate: p.dueDate,
|
||||||
|
createdAt: p.createdAt,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
}
|
||||||
219
pledge-now-pay-later/src/app/dashboard/admin/page.tsx
Normal file
219
pledge-now-pay-later/src/app/dashboard/admin/page.tsx
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import { useSession } from "next-auth/react"
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "@/components/ui/table"
|
||||||
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
|
||||||
|
import {
|
||||||
|
Shield, Building2, Users, Banknote, Calendar, TrendingUp, Loader2, AlertTriangle
|
||||||
|
} from "lucide-react"
|
||||||
|
|
||||||
|
interface AdminData {
|
||||||
|
platform: { orgs: number; users: number; events: number; pledges: number; totalPledgedPence: number; totalCollectedPence: number; collectionRate: number }
|
||||||
|
orgs: Array<{ id: string; name: string; slug: string; hasBank: boolean; users: number; events: number; pledges: number; createdAt: string }>
|
||||||
|
users: Array<{ id: string; email: string; name: string | null; role: string; orgName: string; createdAt: string }>
|
||||||
|
byStatus: Record<string, { count: number; amount: number }>
|
||||||
|
recentPledges: Array<{ id: string; reference: string; amountPence: number; status: string; donorName: string | null; eventName: string; orgName: string; dueDate: string | null; createdAt: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
const fmt = (p: number) => `£${(p / 100).toLocaleString("en-GB", { minimumFractionDigits: 0 })}`
|
||||||
|
|
||||||
|
export default function AdminPage() {
|
||||||
|
const { data: session } = useSession()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const user = session?.user as any
|
||||||
|
const [data, setData] = useState<AdminData | null>(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState("")
|
||||||
|
const [tab, setTab] = useState("orgs")
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/admin")
|
||||||
|
.then(r => { if (!r.ok) throw new Error("Forbidden"); return r.json() })
|
||||||
|
.then(d => setData(d))
|
||||||
|
.catch(e => setError(e.message))
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (user?.role !== "super_admin") {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-20 space-y-3">
|
||||||
|
<Shield className="h-10 w-10 text-danger-red mx-auto" />
|
||||||
|
<h2 className="text-xl font-bold">Access Denied</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">Super admin access required.</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) return <div className="flex items-center justify-center py-20"><Loader2 className="h-8 w-8 text-trust-blue animate-spin" /></div>
|
||||||
|
if (error || !data) return <div className="text-center py-20"><AlertTriangle className="h-8 w-8 text-danger-red mx-auto mb-2" /><p className="text-muted-foreground">{error}</p></div>
|
||||||
|
|
||||||
|
const p = data.platform
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Shield className="h-6 w-6 text-trust-blue" />
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-black text-gray-900">Super Admin</h1>
|
||||||
|
<p className="text-xs text-muted-foreground">Platform-wide view · {user?.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Platform stats */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-3">
|
||||||
|
{[
|
||||||
|
{ label: "Orgs", value: p.orgs, icon: Building2, color: "text-trust-blue" },
|
||||||
|
{ label: "Users", value: p.users, icon: Users, color: "text-warm-amber" },
|
||||||
|
{ label: "Events", value: p.events, icon: Calendar, color: "text-purple-500" },
|
||||||
|
{ label: "Pledges", value: p.pledges, icon: TrendingUp, color: "text-success-green" },
|
||||||
|
{ label: "Pledged", value: fmt(p.totalPledgedPence), icon: Banknote, color: "text-trust-blue" },
|
||||||
|
{ label: "Collected", value: fmt(p.totalCollectedPence), icon: Banknote, color: "text-success-green" },
|
||||||
|
{ label: "Rate", value: `${p.collectionRate}%`, icon: TrendingUp, color: p.collectionRate > 50 ? "text-success-green" : "text-warm-amber" },
|
||||||
|
].map(s => (
|
||||||
|
<Card key={s.label}>
|
||||||
|
<CardContent className="pt-4 pb-3">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<s.icon className={`h-3.5 w-3.5 ${s.color}`} />
|
||||||
|
<span className="text-[10px] text-muted-foreground">{s.label}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-lg font-black mt-0.5">{s.value}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pipeline */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm">Pipeline</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex gap-3 overflow-x-auto">
|
||||||
|
{Object.entries(data.byStatus).map(([status, { count, amount }]) => (
|
||||||
|
<div key={status} className="flex-shrink-0 rounded-xl bg-muted/50 px-4 py-2 text-center min-w-[100px]">
|
||||||
|
<Badge variant={status === "paid" ? "success" : status === "overdue" ? "destructive" : "secondary"} className="text-[10px]">{status}</Badge>
|
||||||
|
<p className="text-lg font-bold mt-1">{count}</p>
|
||||||
|
<p className="text-[10px] text-muted-foreground">{fmt(amount)}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Tabs value={tab} onValueChange={setTab}>
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="orgs">Organisations ({data.orgs.length})</TabsTrigger>
|
||||||
|
<TabsTrigger value="users">Users ({data.users.length})</TabsTrigger>
|
||||||
|
<TabsTrigger value="pledges">Recent Pledges</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="orgs">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Bank</TableHead>
|
||||||
|
<TableHead>Users</TableHead>
|
||||||
|
<TableHead>Events</TableHead>
|
||||||
|
<TableHead>Pledges</TableHead>
|
||||||
|
<TableHead>Created</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data.orgs.map(o => (
|
||||||
|
<TableRow key={o.id}>
|
||||||
|
<TableCell>
|
||||||
|
<p className="font-medium text-sm">{o.name}</p>
|
||||||
|
<p className="text-[10px] text-muted-foreground font-mono">{o.slug}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{o.hasBank ? <Badge variant="success" className="text-[10px]">✓ Set</Badge> : <Badge variant="warning" className="text-[10px]">Missing</Badge>}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-medium">{o.users}</TableCell>
|
||||||
|
<TableCell className="font-medium">{o.events}</TableCell>
|
||||||
|
<TableCell className="font-medium">{o.pledges}</TableCell>
|
||||||
|
<TableCell className="text-xs text-muted-foreground">{new Date(o.createdAt).toLocaleDateString("en-GB", { day: "numeric", month: "short" })}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="users">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Email</TableHead>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Role</TableHead>
|
||||||
|
<TableHead>Organisation</TableHead>
|
||||||
|
<TableHead>Joined</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data.users.map(u => (
|
||||||
|
<TableRow key={u.id}>
|
||||||
|
<TableCell className="font-mono text-xs">{u.email}</TableCell>
|
||||||
|
<TableCell className="text-sm">{u.name || "—"}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant={u.role === "super_admin" ? "default" : u.role === "org_admin" ? "success" : "secondary"} className="text-[10px]">
|
||||||
|
{u.role === "super_admin" ? "🛡️ Super" : u.role === "org_admin" ? "Admin" : u.role}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-sm">{u.orgName}</TableCell>
|
||||||
|
<TableCell className="text-xs text-muted-foreground">{new Date(u.createdAt).toLocaleDateString("en-GB", { day: "numeric", month: "short" })}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="pledges">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Reference</TableHead>
|
||||||
|
<TableHead>Donor</TableHead>
|
||||||
|
<TableHead>Amount</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead>Event</TableHead>
|
||||||
|
<TableHead>Org</TableHead>
|
||||||
|
<TableHead>Date</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data.recentPledges.map(p => (
|
||||||
|
<TableRow key={p.id}>
|
||||||
|
<TableCell className="font-mono text-xs">{p.reference}</TableCell>
|
||||||
|
<TableCell className="text-sm">{p.donorName || "Anon"}</TableCell>
|
||||||
|
<TableCell className="font-bold text-sm">{fmt(p.amountPence)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant={p.status === "paid" ? "success" : p.status === "overdue" ? "destructive" : "secondary"} className="text-[10px]">{p.status}</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-xs truncate max-w-[120px]">{p.eventName}</TableCell>
|
||||||
|
<TableCell className="text-xs text-muted-foreground">{p.orgName}</TableCell>
|
||||||
|
<TableCell className="text-xs text-muted-foreground">{new Date(p.createdAt).toLocaleDateString("en-GB", { day: "numeric", month: "short" })}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import Link from "next/link"
|
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 { LayoutDashboard, Calendar, FileBarChart, Upload, Download, Settings, Plus, ExternalLink, LogOut } from "lucide-react"
|
import { LayoutDashboard, Calendar, FileBarChart, Upload, Download, Settings, Plus, ExternalLink, LogOut, Shield } from "lucide-react"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
@@ -15,6 +15,8 @@ const navItems = [
|
|||||||
{ href: "/dashboard/settings", label: "Settings", icon: Settings },
|
{ href: "/dashboard/settings", label: "Settings", icon: Settings },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const adminNav = { href: "/dashboard/admin", label: "Super Admin", icon: Shield }
|
||||||
|
|
||||||
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
@@ -76,6 +78,23 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
{user?.role === "super_admin" && (
|
||||||
|
<>
|
||||||
|
<div className="my-2 border-t" />
|
||||||
|
<Link
|
||||||
|
href={adminNav.href}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium transition-colors",
|
||||||
|
pathname === adminNav.href
|
||||||
|
? "bg-trust-blue/5 text-trust-blue"
|
||||||
|
: "text-muted-foreground hover:bg-gray-100 hover:text-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<adminNav.icon className="h-4 w-4" />
|
||||||
|
{adminNav.label}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="mt-auto px-2 pt-4">
|
<div className="mt-auto px-2 pt-4">
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ export default function HomePage() {
|
|||||||
<Link href="/signup" className="rounded-xl bg-trust-blue px-6 py-3.5 text-base font-semibold text-white hover:bg-trust-blue/90 transition-all shadow-lg shadow-trust-blue/20">
|
<Link href="/signup" className="rounded-xl bg-trust-blue px-6 py-3.5 text-base font-semibold text-white hover:bg-trust-blue/90 transition-all shadow-lg shadow-trust-blue/20">
|
||||||
Start Collecting Pledges →
|
Start Collecting Pledges →
|
||||||
</Link>
|
</Link>
|
||||||
<a href="#how" className="rounded-xl border-2 border-gray-200 px-6 py-3.5 text-base font-semibold text-gray-700 hover:border-gray-300 transition-all">
|
<Link href="/login?demo=1" className="rounded-xl border-2 border-gray-200 px-6 py-3.5 text-base font-semibold text-gray-700 hover:border-trust-blue hover:text-trust-blue transition-all">
|
||||||
See How It Works
|
🎮 Try the Demo
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">Free forever · No card needed · 2 minute setup</p>
|
<p className="text-xs text-muted-foreground">Free forever · No card needed · 2 minute setup</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user