- Add admin-next/: full Next.js + Prisma + shadcn admin panel (45 API routes, 76 components) - docker-compose: tg_shop_admin service (port 3000, shared db/shop.db, prisma), bot talks to it via ADMIN_CHAT_URL=http://tg_shop_admin:3000/api/chat - tor-proxy now proxies onion admin to tg_shop_admin:3000 (new panel) - chatbotService: ADMIN_CHAT_URL default = http://tg_shop_admin:3000/api/chat (removed localhost:3100 anachronism) - .env admin secrets gitignored (admin-next/.env)
197 lines
6.5 KiB
TypeScript
Executable File
197 lines
6.5 KiB
TypeScript
Executable File
"use client"
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { SidebarTrigger } from "@/components/ui/sidebar";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog";
|
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Moon, Sun, LogOut, User, Search } from "lucide-react";
|
|
import { useTheme } from "next-themes";
|
|
import { useAuthStore } from "@/stores/auth-store";
|
|
import { CommandPalette, openCommandPalette } from "@/components/layout/command-palette";
|
|
import { AppBreadcrumbs } from "@/components/layout/breadcrumbs";
|
|
import { QuickActions } from "@/components/layout/quick-actions";
|
|
import { NotificationsPanel } from "@/components/layout/notifications-panel";
|
|
|
|
const pageTitles: Record<string, string> = {
|
|
"/": "Dashboard",
|
|
"/catalog": "Catalog",
|
|
"/users": "Users",
|
|
"/wallets": "Wallets",
|
|
"/purchases": "Purchases",
|
|
"/audit": "Audit Log",
|
|
"/categories": "Categories",
|
|
"/locations": "Locations",
|
|
"/settings": "Settings",
|
|
"/locales": "Locales",
|
|
"/seed": "Danger Zone",
|
|
"/login": "Sign In",
|
|
};
|
|
|
|
function getInitialTime() {
|
|
const now = new Date();
|
|
const hh = String(now.getHours()).padStart(2, "0");
|
|
const mm = String(now.getMinutes()).padStart(2, "0");
|
|
return `${hh}:${mm}`;
|
|
}
|
|
|
|
function RealtimeClock() {
|
|
const [time, setTime] = useState(getInitialTime);
|
|
|
|
useEffect(() => {
|
|
const interval = setInterval(() => {
|
|
const now = new Date();
|
|
const hh = String(now.getHours()).padStart(2, "0");
|
|
const mm = String(now.getMinutes()).padStart(2, "0");
|
|
setTime(`${hh}:${mm}`);
|
|
}, 1000);
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
const parts = time.split(":");
|
|
|
|
return (
|
|
<span className="text-xs text-muted-foreground font-mono tabular-nums hidden md:flex items-center">
|
|
{parts[0]}<span className="colon-pulse mx-px">:</span>{parts[1]}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
export function AdminHeader() {
|
|
const [hash, setHash] = useState("");
|
|
const { theme, setTheme } = useTheme();
|
|
const { role, logout } = useAuthStore();
|
|
const [logoutOpen, setLogoutOpen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const update = () => setHash(window.location.hash.slice(1) || "/");
|
|
update();
|
|
window.addEventListener("hashchange", update);
|
|
return () => window.removeEventListener("hashchange", update);
|
|
}, []);
|
|
|
|
const title =
|
|
pageTitles[hash] ||
|
|
(hash.startsWith("/users/")
|
|
? "User Detail"
|
|
: hash.split("/").pop()?.charAt(0).toUpperCase() +
|
|
hash.split("/").pop()?.slice(1) ||
|
|
"Page");
|
|
|
|
return (
|
|
<header className="flex h-14 shrink-0 items-center gap-2 border-b-0 px-4 gradient-border-b bg-background/80 backdrop-blur-md relative z-10" style={{ borderBottomStyle: 'solid', borderBottomWidth: '1px' }}>
|
|
<SidebarTrigger className="-ml-1" />
|
|
<Separator orientation="vertical" className="mr-2 h-4" />
|
|
<AppBreadcrumbs />
|
|
<h1 className="text-base font-semibold flex-1 truncate hidden sm:block">
|
|
{title}
|
|
</h1>
|
|
{/* 1. Clock (hidden on mobile) */}
|
|
<RealtimeClock />
|
|
{/* 2. Command palette search button */}
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={openCommandPalette}
|
|
className="shrink-0"
|
|
title="Search (Ctrl+K)"
|
|
>
|
|
<Search className="size-4" />
|
|
<span className="sr-only">Search</span>
|
|
</Button>
|
|
{/* 3. Notifications bell button */}
|
|
<NotificationsPanel />
|
|
{/* 4. Quick actions zap button */}
|
|
<QuickActions />
|
|
{/* 5. Theme toggle */}
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
|
className="shrink-0"
|
|
>
|
|
<Sun className="size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
|
<Moon className="absolute size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
|
<span className="sr-only">Toggle theme</span>
|
|
</Button>
|
|
{/* 6. User dropdown */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
|
<Avatar className="size-8">
|
|
<AvatarFallback className="bg-primary/10 text-primary text-xs">
|
|
{role === "super_admin" ? "SA" : "AD"}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-48">
|
|
<div className="px-2 py-1.5">
|
|
<p className="text-sm font-medium">
|
|
{role === "super_admin" ? "Super Admin" : "Admin"}
|
|
</p>
|
|
<Badge
|
|
variant={role === "super_admin" ? "default" : "secondary"}
|
|
className="text-[10px] px-1.5 py-0 mt-1"
|
|
>
|
|
{role}
|
|
</Badge>
|
|
</div>
|
|
<DropdownMenuItem onClick={() => { window.location.hash = "/settings"; }}>
|
|
<User className="mr-2 size-4" />
|
|
Settings
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={() => setLogoutOpen(true)}
|
|
className="text-destructive"
|
|
>
|
|
<LogOut className="mr-2 size-4" />
|
|
Sign out
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
<AlertDialog open={logoutOpen} onOpenChange={setLogoutOpen}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Sign out</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Are you sure you want to sign out? You will need to
|
|
re-enter your admin token.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={() => {
|
|
logout();
|
|
window.location.hash = "/login";
|
|
}}
|
|
className="bg-destructive text-white hover:bg-destructive/90"
|
|
>
|
|
Sign out
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
<CommandPalette />
|
|
</header>
|
|
);
|
|
}
|