Comprehensive admin + user dashboards (production-ready)

This commit is contained in:
Leon Serfaty
2026-06-07 17:54:30 -04:00
parent 155507f21a
commit f033f00379
122 changed files with 7878 additions and 805 deletions
+146
View File
@@ -0,0 +1,146 @@
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { Command } from "cmdk";
import { useTheme } from "next-themes";
import {
LayoutDashboard,
Mic2,
ListMusic,
BarChart3,
CreditCard,
Users,
KeyRound,
Settings,
Plus,
Moon,
Sun,
Search,
} from "lucide-react";
interface PaletteRoute {
label: string;
href: string;
icon: React.ComponentType<{ className?: string }>;
keywords?: string[];
}
const ROUTES: PaletteRoute[] = [
{ label: "Dashboard", href: "/dashboard", icon: LayoutDashboard, keywords: ["home"] },
{ label: "Episodes", href: "/episodes", icon: Mic2, keywords: ["library", "podcasts"] },
{ label: "Series", href: "/series", icon: ListMusic, keywords: ["season"] },
{ label: "Usage", href: "/usage", icon: BarChart3, keywords: ["limits", "quota"] },
{ label: "Billing", href: "/billing", icon: CreditCard, keywords: ["plan", "upgrade", "subscription"] },
{ label: "Team", href: "/team", icon: Users, keywords: ["members", "workspace", "branding"] },
{ label: "API keys", href: "/api-keys", icon: KeyRound, keywords: ["developer", "token"] },
{ label: "Settings", href: "/settings", icon: Settings, keywords: ["account", "profile"] },
];
/**
* Global ⌘K / Ctrl-K command palette. Provides "New episode", jump-to-route
* navigation, and a theme toggle. Mounted once in the app layout.
*/
export function CommandPalette() {
const router = useRouter();
const { resolvedTheme, setTheme } = useTheme();
const [open, setOpen] = useState(false);
useEffect(() => {
function onKey(e: KeyboardEvent) {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
e.preventDefault();
setOpen((o) => !o);
}
}
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, []);
function run(action: () => void) {
setOpen(false);
action();
}
const isDark = resolvedTheme === "dark";
return (
<Command.Dialog
open={open}
onOpenChange={setOpen}
label="Command palette"
shouldFilter
contentClassName="fixed left-1/2 top-[20vh] z-[61] w-[92vw] max-w-lg -translate-x-1/2 overflow-hidden rounded-2xl border border-border bg-popover text-popover-foreground shadow-lg outline-none data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95"
overlayClassName="fixed inset-0 z-[60] bg-foreground/40 backdrop-blur-sm data-[state=open]:animate-in data-[state=open]:fade-in-0"
>
<div className="flex items-center gap-2 border-b border-border px-4">
<Search className="h-4 w-4 shrink-0 text-muted-foreground" />
<Command.Input
placeholder="Search actions and pages…"
className="h-12 w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
/>
</div>
<Command.List className="max-h-[55vh] overflow-y-auto p-2">
<Command.Empty className="py-8 text-center text-sm text-muted-foreground">
No results found.
</Command.Empty>
<Command.Group
heading="Actions"
className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:text-muted-foreground"
>
<PaletteItem
label="New episode"
keywords={["create", "generate"]}
icon={Plus}
onSelect={() => run(() => router.push("/episodes/new"))}
/>
<PaletteItem
label={isDark ? "Switch to light mode" : "Switch to dark mode"}
keywords={["theme", "dark", "light", "appearance"]}
icon={isDark ? Sun : Moon}
onSelect={() => run(() => setTheme(isDark ? "light" : "dark"))}
/>
</Command.Group>
<Command.Group
heading="Go to"
className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:text-muted-foreground"
>
{ROUTES.map((r) => (
<PaletteItem
key={r.href}
label={r.label}
keywords={r.keywords}
icon={r.icon}
onSelect={() => run(() => router.push(r.href))}
/>
))}
</Command.Group>
</Command.List>
</Command.Dialog>
);
}
function PaletteItem({
label,
keywords,
icon: Icon,
onSelect,
}: {
label: string;
keywords?: string[];
icon: React.ComponentType<{ className?: string }>;
onSelect: () => void;
}) {
return (
<Command.Item
value={`${label} ${(keywords ?? []).join(" ")}`}
onSelect={onSelect}
className="flex cursor-pointer items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm outline-none data-[selected=true]:bg-secondary data-[selected=true]:text-foreground"
>
<Icon className="h-4 w-4 text-muted-foreground" />
{label}
</Command.Item>
);
}