diff --git a/app/(dashboard)/OverviewClient.tsx b/app/(dashboard)/OverviewClient.tsx index f0dc99c5..8b21755a 100644 --- a/app/(dashboard)/OverviewClient.tsx +++ b/app/(dashboard)/OverviewClient.tsx @@ -1,10 +1,9 @@ "use client"; import Link from "next/link"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { BarChart2 } from "lucide-react"; +import { Card, CardContent } from "@/components/ui/card"; +import { BarChart2, Activity } from "lucide-react"; import { ReactNode } from "react"; -import { Separator } from "@/components/ui/separator"; type StatCard = { label: string; @@ -23,6 +22,36 @@ type TrafficSummary = { blockedPercent: number; } | null; +// Per-position accent colors for stat cards (proxy hosts, certs, access lists, traffic) +const CARD_ACCENTS = [ + { border: "border-l-violet-500", icon: "border-violet-500/30 bg-violet-500/10 text-violet-500", count: "text-violet-600 dark:text-violet-400" }, + { border: "border-l-emerald-500", icon: "border-emerald-500/30 bg-emerald-500/10 text-emerald-500", count: "text-emerald-600 dark:text-emerald-400" }, + { border: "border-l-amber-500", icon: "border-amber-500/30 bg-amber-500/10 text-amber-500", count: "text-amber-600 dark:text-amber-400" }, +]; + +const TRAFFIC_ACCENT = { + border: "border-l-cyan-500", + icon: "border-cyan-500/30 bg-cyan-500/10 text-cyan-500", + count: "text-cyan-600 dark:text-cyan-400", +}; + +function getEventDotColor(summary: string): string { + const lower = summary.toLowerCase(); + if (lower.startsWith("delete") || lower.startsWith("remove")) return "bg-rose-500 shadow-[0_0_6px_rgba(244,63,94,0.5)]"; + if (lower.startsWith("create") || lower.startsWith("add")) return "bg-emerald-500 shadow-[0_0_6px_rgba(16,185,129,0.5)]"; + return "bg-primary shadow-[0_0_6px_var(--primary)]"; +} + +function formatRelativeTime(iso: string): string { + const diff = Date.now() - new Date(iso).getTime(); + const mins = Math.floor(diff / 60000); + if (mins < 1) return "just now"; + if (mins < 60) return `${mins}m ago`; + const hrs = Math.floor(mins / 60); + if (hrs < 24) return `${hrs}h ago`; + return new Date(iso).toLocaleDateString(); +} + export default function OverviewClient({ userName, stats, @@ -36,89 +65,108 @@ export default function OverviewClient({ }) { return (
+ {/* Welcome header */}

- Welcome back, {userName} + Welcome back, {userName}

Everything you need to orchestrate Caddy proxies, certificates, and secure edge services.

+ {/* Stat grid */}
- {stats.map((stat) => ( - - - - - {stat.label} - -
- {stat.icon} -
-
- -
{stat.count}
-
-
- - ))} + {stats.map((stat, i) => { + const accent = CARD_ACCENTS[i % CARD_ACCENTS.length]; + return ( + + + +
+
+ {stat.icon} +
+ + {stat.count} + +
+

{stat.label}

+
+
+ + ); + })} {/* Traffic (24h) card */} - - - - - Traffic (24h) - -
- + + + +
+
+ +
+ + {trafficSummary ? trafficSummary.totalRequests.toLocaleString() : "—"} +
- - - {trafficSummary ? ( - <> -
- {trafficSummary.totalRequests.toLocaleString()} +

Traffic (24h)

+ {trafficSummary && trafficSummary.totalRequests > 0 && ( +
+
+ Blocked + 0 ? "text-rose-500" : "text-muted-foreground"}`}> + {trafficSummary.blockedPercent}% +
- {trafficSummary.totalRequests > 0 && ( -

0 ? "text-destructive" : "text-muted-foreground"}`}> - {trafficSummary.blockedPercent}% blocked -

- )} - - ) : ( -
+
+
+
+
)}
- - - Recent Activity - - - {recentEvents.length === 0 ? ( -

No activity recorded yet.

- ) : ( -
- {recentEvents.map((event, index) => ( -
- {index > 0 && } -
- {event.summary} - - {new Date(event.created_at).toLocaleString()} + {/* Recent Activity */} +
+
+
+ +
+

Recent Activity

+
+ + + + {recentEvents.length === 0 ? ( +

No activity recorded yet.

+ ) : ( +
+ {/* Vertical timeline line */} +
+ {recentEvents.map((event, index) => ( +
+ {/* Dot */} +
+ {event.summary} + + {formatRelativeTime(event.created_at)}
-
- ))} -
- )} - - + ))} +
+ )} +
+
+
); } diff --git a/app/(dashboard)/access-lists/AccessListsClient.tsx b/app/(dashboard)/access-lists/AccessListsClient.tsx index 170612c1..67771cec 100644 --- a/app/(dashboard)/access-lists/AccessListsClient.tsx +++ b/app/(dashboard)/access-lists/AccessListsClient.tsx @@ -1,6 +1,6 @@ "use client"; -import { Trash2 } from "lucide-react"; +import { Shield, Trash2, UserPlus, Plus } from "lucide-react"; import { PageHeader } from "@/components/ui/PageHeader"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; @@ -22,6 +22,44 @@ type Props = { pagination: { total: number; page: number; perPage: number }; }; +// Cycling accent colors per card +const ACCENT_COLORS = [ + { + border: "border-l-violet-500", + icon: "border-violet-500/30 bg-violet-500/10 text-violet-500", + countBadge: "border-violet-500/30 bg-violet-500/10 text-violet-600 dark:text-violet-400", + avatar: "bg-violet-500/15 text-violet-600 dark:text-violet-400", + }, + { + border: "border-l-cyan-500", + icon: "border-cyan-500/30 bg-cyan-500/10 text-cyan-500", + countBadge: "border-cyan-500/30 bg-cyan-500/10 text-cyan-600 dark:text-cyan-400", + avatar: "bg-cyan-500/15 text-cyan-600 dark:text-cyan-400", + }, + { + border: "border-l-emerald-500", + icon: "border-emerald-500/30 bg-emerald-500/10 text-emerald-500", + countBadge: "border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400", + avatar: "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400", + }, + { + border: "border-l-amber-500", + icon: "border-amber-500/30 bg-amber-500/10 text-amber-500", + countBadge: "border-amber-500/30 bg-amber-500/10 text-amber-600 dark:text-amber-400", + avatar: "bg-amber-500/15 text-amber-600 dark:text-amber-400", + }, + { + border: "border-l-rose-500", + icon: "border-rose-500/30 bg-rose-500/10 text-rose-500", + countBadge: "border-rose-500/30 bg-rose-500/10 text-rose-600 dark:text-rose-400", + avatar: "bg-rose-500/15 text-rose-600 dark:text-rose-400", + }, +]; + +function getInitials(username: string): string { + return username.slice(0, 2).toUpperCase(); +} + export default function AccessListsClient({ lists, pagination }: Props) { const router = useRouter(); const pathname = usePathname(); @@ -42,89 +80,124 @@ export default function AccessListsClient({ lists, pagination }: Props) { />
- {lists.map((list) => ( - - -
updateAccessListAction(list.id, formData)} className="flex flex-col gap-3"> -

Access List

-
- - -
-
- -