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:
145
admin-next/src/app/page.tsx
Executable file
145
admin-next/src/app/page.tsx
Executable 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user