feat(admin): integrate Next.js admin panel (admin-next/) from feat/nextjs-admin

- 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)
This commit is contained in:
NW
2026-08-08 01:31:45 +01:00
parent 62534dbe85
commit a4a5fd449d
161 changed files with 25681 additions and 5 deletions

145
admin-next/src/app/page.tsx Executable file
View File

@@ -0,0 +1,145 @@
"use client";
import { useEffect, useState } from "react";
import { useAuthStore } from "@/stores/auth-store";
import { AdminSidebar } from "@/components/layout/admin-sidebar";
import { AdminHeader } from "@/components/layout/admin-header";
import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar";
import { LoginPage } from "@/app/login/page";
import { DashboardPage } from "@/components/dashboard/dashboard-page";
import { CatalogHub } from "@/components/catalog/catalog-hub";
import { UsersPage } from "@/components/users/users-page";
import { UserDetailPage } from "@/components/users/user-detail-page";
import { WalletsPage } from "@/components/wallets/wallets-page";
import { PurchasesPage } from "@/components/purchases/purchases-page";
import { AuditPage } from "@/components/audit/audit-page";
import { SettingsPage } from "@/components/settings/settings-page";
import { LocalesPage } from "@/components/locales/locales-page";
import { SeedPage } from "@/components/seed/seed-page";
import { ChatbotSettingsPage } from "@/components/chatbot/chatbot-settings-page";
import { LeadsPage } from "@/components/leads/leads-page";
import { LeadDetailPage } from "@/components/leads/lead-detail-page";
import { ErrorBoundary } from "@/components/shared/error-boundary";
import { AdminFooter } from "@/components/layout/admin-footer";
import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts";
import { Search } from "lucide-react";
import { Button } from "@/components/ui/button";
export default function AppPage() {
const { isAuthenticated, checkSession } = useAuthStore();
const [ready, setReady] = useState(false);
const [page, setPage] = useState<string>("/");
const [pageParams, setPageParams] = useState<Record<string, string>>({});
useEffect(() => {
checkSession().then(() => setReady(true));
}, [checkSession]);
useEffect(() => {
const handleHash = () => {
const hash = window.location.hash.slice(1) || "/";
const [path, search] = hash.split("?");
const params: Record<string, string> = {};
if (search) {
search.split("&").forEach((pair) => {
const [k, v] = pair.split("=");
if (k && v) params[decodeURIComponent(k)] = decodeURIComponent(v);
});
}
setPage(path);
setPageParams(params);
};
window.addEventListener("hashchange", handleHash);
handleHash();
return () => window.removeEventListener("hashchange", handleHash);
}, []);
useEffect(() => {
const handler = (e: MouseEvent) => {
const target = e.target as HTMLElement;
const link = target.closest("a");
if (!link) return;
const href = link.getAttribute("href");
if (!href) return;
if (href.startsWith("http") || href.startsWith("/api")) return;
e.preventDefault();
window.location.hash = href;
};
document.addEventListener("click", handler);
return () => document.removeEventListener("click", handler);
}, []);
useKeyboardShortcuts();
if (!ready) {
return (
<div className="min-h-screen flex flex-col items-center justify-center bg-background relative overflow-hidden">
{/* Background gradient orbs */}
<div className="absolute top-1/4 left-1/3 w-64 h-64 bg-primary/5 rounded-full blur-3xl" />
<div className="absolute bottom-1/4 right-1/3 w-48 h-48 bg-primary/5 rounded-full blur-3xl" />
<div className="relative flex flex-col items-center gap-4">
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-primary text-primary-foreground text-xl font-bold shadow-lg shadow-primary/20 animate-pulse">
TS
</div>
<div className="text-center">
<h1 className="text-lg font-semibold">TG Shop Admin</h1>
<p className="text-sm text-muted-foreground mt-1">Loading your workspace...</p>
</div>
{/* Progress bar */}
<div className="w-48 h-1 bg-muted rounded-full overflow-hidden">
<div className="h-full w-1/3 bg-primary rounded-full animate-[loading_1.5s_ease-in-out_infinite]" />
</div>
</div>
</div>
);
}
if (!isAuthenticated) {
return <LoginPage />;
}
const renderPage = () => {
if (page === "/") return <DashboardPage />;
if (page === "/catalog") return <CatalogHub />;
if (page === "/users") return <UsersPage />;
if (page.startsWith("/users/")) return <UserDetailPage userId={page.split("/")[2]} />;
if (page === "/wallets") return <WalletsPage />;
if (page === "/purchases") return <PurchasesPage />;
if (page === "/audit") return <AuditPage />;
if (page === "/settings") return <SettingsPage />;
if (page === "/locales") return <LocalesPage />;
if (page === "/chatbot") return <ChatbotSettingsPage />;
if (page === "/leads") return <LeadsPage />;
if (page.startsWith("/leads/")) return <LeadDetailPage leadId={page.split("/")[2]} />;
if (page === "/seed") return <SeedPage />;
return (
<div className="flex flex-col items-center justify-center h-64 page-enter">
<div className="rounded-full bg-muted p-4 mb-4">
<Search className="size-8 text-muted-foreground" />
</div>
<p className="text-lg font-medium">Page not found</p>
<p className="text-sm text-muted-foreground mt-1">The page you're looking for doesn't exist.</p>
<Button variant="outline" size="sm" className="mt-4" onClick={() => { window.location.hash = '/'; }}>
Go to Dashboard
</Button>
</div>
);
};
return (
<SidebarProvider>
<AdminSidebar />
<SidebarInset>
<div className="flex-1 flex flex-col overflow-hidden">
<AdminHeader />
<div className="flex-1 overflow-auto p-4 md:p-6">
<ErrorBoundary>{renderPage()}</ErrorBoundary>
</div>
<AdminFooter />
</div>
</SidebarInset>
</SidebarProvider>
);
}