Comprehensive admin + user dashboards (production-ready)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user