fix: match shadcn dashboard visual style

- Remove gradient heading in OverviewClient (violet-to-cyan gradient replaced with plain bold text)
- Remove hardcoded dark navy backgrounds from activity items (replaced with Card + Separator list)
- Stat cards now use shadcn CardHeader/CardContent pattern with small icons + big number
- Sidebar logo: replace violet text with violet pill icon + plain foreground text
- Active nav item: use bg-primary/10 text-primary (subtle violet tint) instead of bg-accent
- Move theme toggle + sign out into sidebar footer row (no more floating top-right toggle)
- Mobile header brand name: remove text-primary, use plain font-semibold

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
fuomag9
2026-03-22 16:52:30 +01:00
parent 98105eba89
commit 513a564aba
3 changed files with 112 additions and 123 deletions

View File

@@ -59,13 +59,16 @@ function NavContent({ pathname, user, onNavigate }: {
return (
<div className="flex flex-col h-full">
{/* Logo */}
<div className="px-4 py-6">
<p className="text-primary font-bold text-base tracking-tight">Caddy Proxy</p>
<p className="text-[10px] text-muted-foreground uppercase tracking-widest">Manager</p>
<div className="px-4 py-5 flex items-center gap-2">
<div className="h-7 w-7 rounded-md bg-primary flex items-center justify-center shrink-0">
<span className="text-primary-foreground font-bold text-xs">C</span>
</div>
<p className="font-semibold text-sm tracking-tight">Caddy Proxy Manager</p>
</div>
<Separator />
{/* Nav items */}
<ScrollArea className="flex-1 px-2">
<ScrollArea className="flex-1 px-2 py-2">
<nav className="flex flex-col gap-0.5">
{NAV_ITEMS.map(({ href, label, icon: Icon }) => {
const active = pathname === href;
@@ -74,10 +77,10 @@ function NavContent({ pathname, user, onNavigate }: {
key={href}
variant="ghost"
className={cn(
"w-full justify-start gap-3 font-medium",
"w-full justify-start gap-3 h-9 px-3",
active
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:text-foreground"
? "bg-primary/10 text-primary font-semibold"
: "text-muted-foreground hover:text-foreground hover:bg-muted/60 font-normal"
)}
asChild
onClick={onNavigate}
@@ -93,32 +96,40 @@ function NavContent({ pathname, user, onNavigate }: {
</ScrollArea>
{/* Footer */}
<div className="p-3 space-y-2">
<Separator />
<Button
variant="ghost"
className="w-full justify-start gap-3 px-2 h-auto py-2"
onClick={() => { router.push("/profile"); onNavigate?.(); }}
>
<Avatar className="h-8 w-8 shrink-0">
<AvatarImage src={user.image ?? undefined} alt={user.name ?? "User"} />
<AvatarFallback>{(user.name?.[0] ?? "U").toUpperCase()}</AvatarFallback>
</Avatar>
<div className="flex flex-col items-start overflow-hidden">
<span className="text-sm font-medium truncate w-full">{user.name ?? "Administrator"}</span>
<span className="text-xs text-muted-foreground truncate w-full">{user.email}</span>
</div>
</Button>
<form action="/api/auth/logout" method="POST">
<div className="p-3 space-y-1">
<Separator className="mb-2" />
<div className="flex items-center justify-between px-1">
<Button
type="submit"
variant="outline"
className="w-full justify-start gap-2 text-muted-foreground"
variant="ghost"
className="flex-1 justify-start gap-3 px-2 h-auto py-2 min-w-0"
onClick={() => { router.push("/profile"); onNavigate?.(); }}
>
<LogOut className="h-4 w-4" />
Sign Out
<Avatar className="h-8 w-8 shrink-0">
<AvatarImage src={user.image ?? undefined} alt={user.name ?? "User"} />
<AvatarFallback className="text-xs bg-primary text-primary-foreground">
{(user.name?.[0] ?? "U").toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex flex-col items-start overflow-hidden min-w-0">
<span className="text-sm font-medium truncate w-full">{user.name ?? "Administrator"}</span>
<span className="text-xs text-muted-foreground truncate w-full">{user.email}</span>
</div>
</Button>
</form>
<div className="flex items-center shrink-0">
<ThemeToggle />
<form action="/api/auth/logout" method="POST">
<Button
type="submit"
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-foreground"
title="Sign Out"
>
<LogOut className="h-4 w-4" />
</Button>
</form>
</div>
</div>
</div>
</div>
);
@@ -141,13 +152,15 @@ export default function DashboardLayoutClient({ user, children }: { user: User;
<Button variant="ghost" size="icon" aria-label="Open navigation" onClick={() => setMobileOpen(true)}>
<Menu className="h-5 w-5" />
</Button>
<span className="text-primary font-bold text-sm">Caddy Proxy Manager</span>
<span className="font-semibold text-sm">Caddy Proxy Manager</span>
<div className="flex items-center gap-1">
<ThemeToggle />
<Button variant="ghost" size="icon" aria-label="Go to profile" onClick={() => router.push("/profile")}>
<Avatar className="h-6 w-6">
<AvatarImage src={user.image ?? undefined} />
<AvatarFallback className="text-[10px]">{(user.name?.[0] ?? "U").toUpperCase()}</AvatarFallback>
<AvatarFallback className="text-[10px] bg-primary text-primary-foreground">
{(user.name?.[0] ?? "U").toUpperCase()}
</AvatarFallback>
</Avatar>
</Button>
</div>
@@ -163,10 +176,6 @@ export default function DashboardLayoutClient({ user, children }: { user: User;
{/* Main content */}
<main className="flex-1 md:ml-64 mt-12 md:mt-0">
<div className="max-w-screen-xl mx-auto px-4 md:px-8 py-6">
{/* Theme toggle for desktop — top-right corner */}
<div className="hidden md:flex justify-end mb-2">
<ThemeToggle />
</div>
{children}
</div>
</main>

View File

@@ -1,9 +1,10 @@
"use client";
import Link from "next/link";
import { Card, CardContent } from "@/components/ui/card";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { BarChart2 } from "lucide-react";
import { ReactNode } from "react";
import { Separator } from "@/components/ui/separator";
type StatCard = {
label: string;
@@ -34,111 +35,90 @@ export default function OverviewClient({
recentEvents: RecentEvent[];
}) {
return (
<div className="flex flex-col gap-10">
<div className="flex flex-col gap-1.5">
<span className="text-xs font-semibold uppercase tracking-[0.25em] text-slate-400/60">
Control Center
</span>
<h1
className="text-3xl font-bold"
style={{
background: "linear-gradient(120deg, rgba(127, 91, 255, 1) 0%, rgba(34, 211, 238, 0.9) 80%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent"
}}
>
<div className="flex flex-col gap-8">
<div>
<h1 className="text-2xl font-bold tracking-tight">
Welcome back, {userName}
</h1>
<p className="text-sm text-muted-foreground max-w-[560px]">
Everything you need to orchestrate Caddy proxies, certificates, and secure edge services lives here.
<p className="text-sm text-muted-foreground mt-1">
Everything you need to orchestrate Caddy proxies, certificates, and secure edge services.
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{stats.map((stat) => (
<Card
key={stat.label}
className="border border-slate-400/10 bg-transparent shadow-none h-full"
>
<Link
href={stat.href}
className="block h-full transition-colors hover:bg-gradient-to-br hover:from-violet-500/10 hover:to-cyan-400/[0.06] rounded-[inherit]"
>
<CardContent className="flex flex-col gap-1 pt-6">
<div className="text-violet-400/80 flex items-center">
<Link key={stat.label} href={stat.href} className="block">
<Card className="hover:bg-muted/50 transition-colors">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
{stat.label}
</CardTitle>
<div className="text-muted-foreground">
{stat.icon}
</div>
<span className="text-3xl font-bold tracking-tight">
{stat.count}
</span>
<span className="text-sm text-muted-foreground font-medium">
{stat.label}
</span>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold">{stat.count}</div>
</CardContent>
</Link>
</Card>
</Card>
</Link>
))}
{/* Traffic (24h) card */}
<Card className="border border-slate-400/10 bg-transparent shadow-none h-full">
<Link
href="/analytics"
className="block h-full transition-colors hover:bg-gradient-to-br hover:from-violet-500/10 hover:to-cyan-400/[0.06] rounded-[inherit]"
>
<CardContent className="flex flex-col gap-1 pt-6">
<div className="text-violet-400/80 flex items-center">
<BarChart2 className="h-8 w-8" />
<Link href="/analytics" className="block">
<Card className="hover:bg-muted/50 transition-colors">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Traffic (24h)
</CardTitle>
<div className="text-muted-foreground">
<BarChart2 className="h-4 w-4" />
</div>
</CardHeader>
<CardContent>
{trafficSummary ? (
<>
<span className="text-3xl font-bold tracking-tight">
<div className="text-3xl font-bold">
{trafficSummary.totalRequests.toLocaleString()}
</span>
<span className="text-sm text-muted-foreground font-medium">
Traffic (24h)
{trafficSummary.totalRequests > 0 && (
<span
className={`ml-1 text-[0.8em] ${trafficSummary.blockedPercent > 0 ? "text-red-400" : "text-muted-foreground"}`}
>
· {trafficSummary.blockedPercent}% blocked
</span>
)}
</span>
</div>
{trafficSummary.totalRequests > 0 && (
<p className={`text-xs mt-1 ${trafficSummary.blockedPercent > 0 ? "text-destructive" : "text-muted-foreground"}`}>
{trafficSummary.blockedPercent}% blocked
</p>
)}
</>
) : (
<>
<span className="text-3xl font-bold tracking-tight"></span>
<span className="text-sm text-muted-foreground font-medium">Traffic (24h)</span>
</>
<div className="text-3xl font-bold"></div>
)}
</CardContent>
</Link>
</Card>
</Card>
</Link>
</div>
<div className="flex flex-col gap-2">
<h2 className="text-lg font-semibold tracking-tight">Recent Activity</h2>
{recentEvents.length === 0 ? (
<div className="p-8 text-center text-muted-foreground rounded-md bg-[rgba(12,18,30,0.7)]">
No activity recorded yet.
</div>
) : (
<div className="flex flex-col gap-1.5">
{recentEvents.map((event, index) => (
<div
key={`${event.created_at}-${index}`}
className="flex justify-between items-center gap-2 rounded-md p-4 border border-slate-400/[0.08]"
style={{ background: "linear-gradient(120deg, rgba(17, 25, 40, 0.9), rgba(15, 23, 42, 0.7))" }}
>
<span className="text-sm font-medium">{event.summary}</span>
<span className="text-sm text-muted-foreground whitespace-nowrap">
{new Date(event.created_at).toLocaleString()}
</span>
</div>
))}
</div>
)}
</div>
<Card>
<CardHeader>
<CardTitle className="text-base font-semibold">Recent Activity</CardTitle>
</CardHeader>
<CardContent className="p-0">
{recentEvents.length === 0 ? (
<p className="px-6 pb-6 text-sm text-muted-foreground">No activity recorded yet.</p>
) : (
<div>
{recentEvents.map((event, index) => (
<div key={`${event.created_at}-${index}`}>
{index > 0 && <Separator />}
<div className="flex justify-between items-center gap-4 px-6 py-3">
<span className="text-sm">{event.summary}</span>
<span className="text-xs text-muted-foreground whitespace-nowrap">
{new Date(event.created_at).toLocaleString()}
</span>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
);
}

View File

@@ -36,9 +36,9 @@ async function loadStats(): Promise<StatCard[]> {
const accessListsCount = accessListCountResult[0]?.value ?? 0;
return [
{ label: "Proxy Hosts", icon: <ArrowLeftRight className="h-8 w-8" />, count: proxyHostsCount, href: "/proxy-hosts" },
{ label: "Certificates", icon: <ShieldCheck className="h-8 w-8" />, count: certificatesCount, href: "/certificates" },
{ label: "Access Lists", icon: <KeyRound className="h-8 w-8" />, count: accessListsCount, href: "/access-lists" }
{ label: "Proxy Hosts", icon: <ArrowLeftRight className="h-4 w-4" />, count: proxyHostsCount, href: "/proxy-hosts" },
{ label: "Certificates", icon: <ShieldCheck className="h-4 w-4" />, count: certificatesCount, href: "/certificates" },
{ label: "Access Lists", icon: <KeyRound className="h-4 w-4" />, count: accessListsCount, href: "/access-lists" }
];
}