Files
Mace-AIO-Discord-Bot---With…/dashboard/app/dashboard/layout.tsx
2026-07-12 15:16:07 +05:30

446 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import React, { useState, useEffect, useRef } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
LayoutDashboard, Server, ShieldCheck, Ticket, BarChart4, FileText, Settings,
Menu, X, Bell, User, Search, ChevronRight, Star, Sparkles, LogOut,
LifeBuoy, ChevronDown, Bot, Shield
} from "lucide-react";
import { useSession, signIn, signOut } from "next-auth/react";
import { cn, isAdmin } from "@/lib/utils";
import { api } from "@/lib/api";
import { AdminConfig } from "@/types/api";
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [isProfileOpen, setIsProfileOpen] = useState(false);
const pathname = usePathname();
const { data: session, status } = useSession();
const [isNotificationsOpen, setIsNotificationsOpen] = useState(false);
const [globalNotification, setGlobalNotification] = useState<string | null>(null);
const bellRef = useRef<HTMLDivElement>(null);
const profileRef = useRef<HTMLDivElement>(null);
// Close dropdowns on click outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Node;
if (bellRef.current && !bellRef.current.contains(target)) {
setIsNotificationsOpen(false);
}
if (profileRef.current && !profileRef.current.contains(target)) {
setIsProfileOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
// Auto-close sidebar on mobile when navigating
React.useEffect(() => {
setIsSidebarOpen(false);
setIsProfileOpen(false);
}, [pathname]);
React.useEffect(() => {
if (status === "unauthenticated") {
signIn("discord");
}
// Fetch global notification
const fetchNotification = async () => {
try {
const config = await api.getAdminConfig();
setGlobalNotification(config.global_notification);
} catch (err) {
console.error("Failed to fetch notifications:", err);
}
};
fetchNotification();
}, [status]);
if (status === "loading" || status === "unauthenticated") {
return (
<div className="min-h-screen bg-[#0f172a] flex items-center justify-center">
<div className="animate-pulse flex flex-col items-center gap-4">
<div className="h-12 w-12 rounded-xl bg-primary flex items-center justify-center shadow-lg shadow-primary/20">
<span className="font-black text-white italic text-xl">{process.env.NEXT_PUBLIC_BRAND_NAME_WORD || "ZX"}</span>
</div>
<p className="text-slate-400 font-bold tracking-widest uppercase text-xs">
Authenticating...
</p>
</div>
</div>
);
}
const match = pathname.match(/\/dashboard\/guild\/([^\/]+)/);
const currentGuildId = match ? match[1] : null;
// Base sidebar items will be filtered if we are inside a guild
const allSidebarItems = currentGuildId
? [
{ name: "Overview", href: `/dashboard/guild/${currentGuildId}`, icon: LayoutDashboard },
{
name: "Security",
items: [
{ name: "Anti-Nuke", href: `/dashboard/guild/${currentGuildId}/antinuke`, icon: ShieldCheck },
{ name: "Automod", href: `/dashboard/guild/${currentGuildId}/automod`, icon: ShieldCheck },
{ name: "Verification", href: `/dashboard/guild/${currentGuildId}/verification`, icon: User },
],
},
{
name: "Engagement",
items: [
{ name: "Welcome", href: `/dashboard/guild/${currentGuildId}/welcome`, icon: Bell },
{ name: "Leveling", href: `/dashboard/guild/${currentGuildId}/leveling`, icon: BarChart4 },
{ name: "Vanity Roles", href: `/dashboard/guild/${currentGuildId}/vanityroles`, icon: Star },
{ name: "Auto Role", href: `/dashboard/guild/${currentGuildId}/autorole`, icon: Search },
{ name: "Auto React", href: `/dashboard/guild/${currentGuildId}/autoreact`, icon: Settings },
{ name: "Reaction Roles", href: `/dashboard/guild/${currentGuildId}/reactionroles`, icon: Search },
{ name: "Join DM", href: `/dashboard/guild/${currentGuildId}/joindm`, icon: User },
{ name: "Invites", href: `/dashboard/guild/${currentGuildId}/invites`, icon: Search },
{ name: "Tracking", href: `/dashboard/guild/${currentGuildId}/tracking`, icon: BarChart4 },
],
},
{
name: "Utility",
items: [
{ name: "Tickets", href: `/dashboard/guild/${currentGuildId}/tickets`, icon: Ticket },
{ name: "Join to Create", href: `/dashboard/guild/${currentGuildId}/j2c`, icon: Menu },
{ name: "Custom Roles", href: `/dashboard/guild/${currentGuildId}/customroles`, icon: ShieldCheck },
{ name: "Voice Role", href: `/dashboard/guild/${currentGuildId}/invcrole`, icon: Settings },
],
},
{ name: "Settings", href: `/dashboard/guild/${currentGuildId}/settings`, icon: Settings },
{ name: "Back to Servers", href: "/dashboard/guilds", icon: Server },
]
: [
{ name: "Dashboard", href: "/dashboard", icon: LayoutDashboard },
{ name: "Servers", href: "/dashboard/guilds", icon: Server },
...(isAdmin(session?.user?.id)
? [{ name: "Admin Panel", href: "/dashboard/admin", icon: Shield }]
: []),
];
// Separate the "Back to Servers" item when inside a guild
let mainSidebarItems = allSidebarItems;
let backLinkItem: any = null;
if (currentGuildId) {
mainSidebarItems = allSidebarItems.filter(
(item) => !(item.name === "Back to Servers")
);
backLinkItem = allSidebarItems.find((item) => item.name === "Back to Servers");
}
const BackLinkIcon = backLinkItem?.icon || Server;
return (
<div className="min-h-screen bg-[#020617] text-slate-200">
{/* Liquid Background Elements */}
<div className="fixed inset-0 overflow-hidden pointer-events-none z-0">
<div className="absolute top-[-10%] right-[-10%] w-[40%] h-[40%] bg-red-500/5 blur-[120px] rounded-full animate-pulse" />
<div className="absolute bottom-[10%] left-[-5%] w-[30%] h-[30%] bg-indigo-500/5 blur-[100px] rounded-full animate-pulse [animation-delay:2s]" />
</div>
{/* Mobile Sidebar Overlay */}
{isSidebarOpen && (
<div
className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm lg:hidden"
onClick={() => setIsSidebarOpen(false)}
/>
)}
{/* Sidebar - now using flex column */}
<aside
className={cn(
"fixed left-4 top-4 bottom-4 z-50 w-64 transform transition-all duration-500 ease-in-out lg:translate-x-0 glass border border-white/10 rounded-[2.5rem] shadow-2xl shadow-black/40 overflow-hidden flex flex-col",
isSidebarOpen ? "translate-x-0" : "-translate-x-[110%]"
)}
>
{/* Header */}
<div className="flex h-16 items-center px-6 mt-4 flex-shrink-0">
<div className="flex items-center gap-3 group">
<div className="h-9 w-9 rounded-xl bg-gradient-to-br from-red-500 to-red-800 flex items-center justify-center shadow-lg shadow-red-500/20 group-hover:scale-110 transition-transform border border-white/10">
<Bot className="h-5 w-5 text-white" />
</div>
<div className="flex flex-col">
<h1 className="text-lg font-bold tracking-tight text-white font-outfit leading-none">
{process.env.NEXT_PUBLIC_BRAND_NAME || "ZyroX"}
</h1>
<span className="text-[9px] font-black uppercase tracking-[0.2em] text-red-500/80 mt-1">
Dashboard
</span>
</div>
</div>
<button
className="ml-auto p-2 lg:hidden text-slate-400 hover:text-white"
onClick={() => setIsSidebarOpen(false)}
>
<X className="h-5 w-5" />
</button>
</div>
{/* Scrollable Navigation */}
<nav className="mt-8 px-4 space-y-6 overflow-y-auto flex-1 no-scrollbar relative z-10 pb-3">
{mainSidebarItems.map((item: any) => {
if (item.items) {
return (
<div key={item.name} className="space-y-2">
<p className="px-4 text-[10px] font-black uppercase tracking-[0.3em] text-slate-600 mb-3">
{item.name}
</p>
<div className="space-y-1">
{item.items.map((subItem: any) => {
const isActive = pathname === subItem.href;
return (
<Link
key={subItem.name}
href={subItem.href}
className={cn(
"flex items-center gap-3 px-4 py-2.5 rounded-xl transition-all duration-300 group text-[13px] font-bold",
isActive
? "bg-red-500/10 text-red-500 border border-red-500/20 shadow-[0_0_20px_rgba(239,68,68,0.1)]"
: "text-slate-400 hover:bg-white/[0.03] hover:text-slate-200"
)}
>
<subItem.icon
className={cn(
"h-4 w-4 transition-all duration-300",
isActive
? "text-red-500 scale-110"
: "text-slate-600 group-hover:text-slate-400"
)}
/>
{subItem.name}
{isActive && (
<div className="ml-auto w-1.5 h-1.5 rounded-full bg-red-500 animate-pulse shadow-[0_0_10px_rgba(239,68,68,0.5)]" />
)}
</Link>
);
})}
</div>
</div>
);
}
const isActive = pathname === item.href;
return (
<Link
key={item.name}
href={item.href}
className={cn(
"flex items-center gap-4 px-4 py-3 rounded-2xl transition-all duration-300 group text-[14px] font-bold",
isActive
? "bg-red-500/10 text-red-500 border border-red-500/20 shadow-[0_0_20px_rgba(239,68,68,0.1)]"
: "text-slate-400 hover:bg-white/[0.03] hover:text-slate-200"
)}
>
<item.icon
className={cn(
"h-5 w-5 transition-all duration-300",
isActive ? "text-red-500 scale-110" : "text-slate-600 group-hover:text-slate-400"
)}
/>
{item.name}
{isActive ? (
<ChevronRight className="ml-auto h-4 w-4 text-red-500" />
) : (
<ChevronRight className="ml-auto h-4 w-4 opacity-0 group-hover:opacity-30 transition-opacity" />
)}
</Link>
);
})}
</nav>
{/* Fixed "Back to Servers" link (only shown inside a guild) */}
{backLinkItem && (
<div className="px-4 py-2 flex-shrink-0">
<div className="h-px bg-white/5 w-3/4 mx-auto rounded-full mb-2" />
<Link
href={backLinkItem.href || "/dashboard/guilds"}
className={cn(
"flex items-center gap-4 px-4 py-3 rounded-2xl transition-all duration-300 group text-[14px] font-bold",
pathname === backLinkItem.href
? "bg-red-500/10 text-red-500 border border-red-500/20 shadow-[0_0_20px_rgba(239,68,68,0.1)]"
: "text-slate-400 hover:bg-white/[0.03] hover:text-slate-200"
)}
>
<BackLinkIcon
className={cn(
"h-5 w-5 transition-all duration-300",
pathname === backLinkItem.href
? "text-red-500 scale-110"
: "text-slate-600 group-hover:text-slate-400"
)}
/>
{backLinkItem.name}
{pathname === backLinkItem.href ? (
<ChevronRight className="ml-auto h-4 w-4 text-red-500" />
) : (
<ChevronRight className="ml-auto h-4 w-4 opacity-0 group-hover:opacity-30 transition-opacity" />
)}
</Link>
</div>
)}
{/* User Profile - now a normal flex child, no absolute positioning */}
<div className="flex-shrink-0 p-4 border-t border-white/5 glass-red bg-red-500/[0.02]">
<div className="flex items-center gap-3 p-2 bg-white/[0.02] rounded-2xl border border-white/[0.05]">
<div className="h-10 w-10 rounded-full bg-red-500/10 flex items-center justify-center ring-1 ring-white/10 overflow-hidden border border-red-500/20">
{session?.user?.image ? (
<img
src={session.user.image}
alt="User Avatar"
className="h-full w-full object-cover opacity-80"
/>
) : (
<User className="h-6 w-6 text-red-500/50" />
)}
</div>
<div className="overflow-hidden">
<p className="text-sm font-bold text-white truncate font-outfit">
{session?.user?.name || "Administrator"}
</p>
<p className="text-[10px] font-black uppercase text-red-500/60 truncate tracking-widest">
User
</p>
</div>
</div>
</div>
</aside>
{/* Main Content Area (unchanged) */}
<div className="lg:pl-72 flex flex-col min-h-screen relative z-10">
{/* Top Navbar (unchanged) */}
<header className="h-20 sticky top-4 z-30 mx-4 lg:mx-10 flex items-center justify-between border border-white/10 glass bg-white/[0.01] backdrop-blur-3xl px-8 rounded-[2rem] shadow-xl shadow-black/20 mb-6 mt-4">
<button
className="p-2 lg:hidden text-slate-400 hover:bg-white/5 rounded-xl transition-colors"
onClick={() => setIsSidebarOpen(true)}
>
<Menu className="h-6 w-6" />
</button>
<div className="hidden md:flex items-center w-96 max-w-full relative group">
<Search className="absolute left-4 h-4 w-4 text-slate-500 group-focus-within:text-red-500 transition-colors" />
<input
type="text"
placeholder="Query neural network..."
className="w-full bg-white/[0.03] border border-white/5 rounded-2xl py-2.5 pl-12 pr-4 text-xs font-bold text-slate-300 focus:outline-none focus:ring-1 focus:ring-red-500/30 focus:bg-white/[0.05] transition-all placeholder:text-slate-600"
/>
</div>
<div className="flex items-center gap-6">
<div className="relative" ref={bellRef}>
<button
onClick={() => setIsNotificationsOpen(!isNotificationsOpen)}
className="relative p-2.5 text-slate-400 hover:bg-white/5 hover:text-white rounded-xl transition-all group"
>
<Bell className="h-5 w-5" />
{globalNotification && (
<span className="absolute top-2 right-2 h-2 w-2 rounded-full bg-red-500 border-2 border-[#020617] shadow-[0_0_10px_rgba(239,68,68,0.5)]"></span>
)}
</button>
{isNotificationsOpen && (
<div className="absolute right-0 mt-3 w-80 bg-[#0a0f1e]/90 backdrop-blur-3xl border border-white/5 rounded-[24px] shadow-[0_20px_50px_rgba(0,0,0,0.5)] p-4 z-20 animate-in fade-in zoom-in-95 duration-300 origin-top-right">
<div className="flex items-center justify-between mb-4 border-b border-white/5 pb-2">
<p className="text-[10px] font-black text-slate-500 uppercase tracking-[0.2em]">Broadcast Metrics</p>
<button
onClick={() => setGlobalNotification(null)}
className="text-[10px] font-bold text-red-500/60 hover:text-red-500 transition-colors uppercase"
>
Clear
</button>
</div>
{globalNotification ? (
<div className="bg-red-500/5 border border-red-500/10 rounded-2xl p-4">
<div className="flex items-center gap-2 mb-2">
<Sparkles className="h-3 w-3 text-red-500" />
<span className="text-[10px] font-black uppercase text-red-500 tracking-widest">System Broadcast</span>
</div>
<p className="text-xs font-medium text-slate-300 leading-relaxed">
{globalNotification}
</p>
</div>
) : (
<div className="py-8 flex flex-col items-center justify-center text-center">
<div className="h-10 w-10 rounded-full bg-slate-800 flex items-center justify-center mb-3">
<Bell className="h-5 w-5 text-slate-600" />
</div>
<p className="text-xs font-bold text-slate-500">No active broadcasts</p>
<p className="text-[10px] font-medium text-slate-600 mt-1 uppercase tracking-widest">Everything is operating normally</p>
</div>
)}
</div>
)}
</div>
<div className="h-8 w-[1px] bg-white/5 hidden sm:block"></div>
{/* Profile Dropdown (unchanged) */}
<div className="relative" ref={profileRef}>
<button
onClick={() => setIsProfileOpen(!isProfileOpen)}
className="flex items-center gap-3.5 p-1.5 rounded-2xl hover:bg-white/5 transition-all group border border-transparent hover:border-white/10"
>
<div className="h-9 w-9 rounded-full bg-red-500/10 flex items-center justify-center overflow-hidden border border-red-500/20 ring-2 ring-transparent group-hover:ring-red-500/30 transition-all">
{session?.user?.image ? (
<img src={session.user.image} alt="User Avatar" className="h-full w-full object-cover opacity-80" />
) : (
<User className="h-5 w-5 text-red-500/50" />
)}
</div>
<div className="hidden sm:flex flex-col items-start leading-none gap-1">
<span className="text-xs font-bold text-slate-200 group-hover:text-white transition-colors">
{session?.user?.name?.split(' ')[0] || "Admin"}
</span>
<span className="text-[9px] font-black uppercase text-red-500/60 tracking-widest">Active</span>
</div>
<ChevronDown
className={cn("h-4 w-4 text-slate-600 transition-transform hidden sm:block", isProfileOpen && "rotate-180")}
/>
</button>
{isProfileOpen && (
<div className="absolute right-0 mt-3 w-56 bg-[#0a0f1e]/90 backdrop-blur-3xl border border-white/5 rounded-[24px] shadow-[0_20px_50px_rgba(0,0,0,0.5)] p-2 z-20 animate-in fade-in zoom-in-95 duration-300 origin-top-right">
<div className="px-4 py-3 border-b border-white/5 mb-2">
<p className="text-[9px] font-black text-slate-500 uppercase tracking-[0.2em] mb-1">Authenticated As</p>
<p className="text-sm font-bold text-white truncate">{session?.user?.name || "Administrator"}</p>
</div>
<button className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-xs font-bold text-slate-400 hover:bg-white/5 hover:text-white transition-all group/item">
<LifeBuoy className="h-4 w-4 text-slate-600 group-hover/item:text-red-500 transition-colors" />
Support Matrix
</button>
<button
onClick={() => signOut({ callbackUrl: '/' })}
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-xs font-black uppercase tracking-widest text-red-500/80 hover:bg-red-500/10 hover:text-red-500 transition-all group/item"
>
<LogOut className="h-4 w-4" />
Deauthorize
</button>
</div>
)}
</div>
</div>
</header>
{/* Content Area */}
<main className="flex-1 p-6 lg:p-10 animate-in fade-in duration-700 relative z-10">
<div className="max-w-[1600px] mx-auto">{children}</div>
</main>
</div>
</div>
);
}