fix: smooth sidebar transitions and header UI improvements
This commit is contained in:
1 parent
0e1b774be9
commit
7777b62305
3 files changed
+113
-65
No files matched your search
@@ -3,11 +3,15 @@
|
||||
import { ReactNode, useState, useEffect } from "react";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
import { Header } from "./Header";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export function DashboardLayout({ children }: { children: ReactNode }) {
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [lastUpdate, setLastUpdate] = useState<string>("—");
|
||||
const [intervalText, setIntervalText] = useState<string>("");
|
||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
||||
|
||||
const pathname = usePathname();
|
||||
|
||||
const fetchDataIntervalRange = () => {
|
||||
fetch('/api/dashboard/data-interval')
|
||||
@@ -26,7 +30,6 @@ export function DashboardLayout({ children }: { children: ReactNode }) {
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
};
|
||||
// e.g. "1 Jul 2026 04:57 - 1 Jul 2026 08:42"
|
||||
setIntervalText(`${sDate.toLocaleString('id-ID', options)} - ${eDate.toLocaleString('id-ID', options)}`);
|
||||
}
|
||||
}
|
||||
@@ -42,7 +45,6 @@ export function DashboardLayout({ children }: { children: ReactNode }) {
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setIsRefreshing(true);
|
||||
// Poll updates on backend
|
||||
try {
|
||||
await fetch('/api/dashboard/refresh', { method: 'POST' });
|
||||
} catch {}
|
||||
@@ -53,17 +55,19 @@ export function DashboardLayout({ children }: { children: ReactNode }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-transparent transition-colors duration-500">
|
||||
<Sidebar />
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<div className="flex h-screen overflow-hidden bg-transparent transition-colors duration-500 relative">
|
||||
<Sidebar isCollapsed={isSidebarCollapsed} />
|
||||
|
||||
<div className="flex flex-1 flex-col overflow-hidden w-full min-w-0 relative transition-all duration-500 ease-in-out">
|
||||
<Header
|
||||
lastUpdate={lastUpdate}
|
||||
isRefreshing={isRefreshing}
|
||||
onRefresh={handleRefresh}
|
||||
intervalText={intervalText}
|
||||
onMenuClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
|
||||
/>
|
||||
<main className="flex-1 overflow-y-auto bg-transparent p-6 transition-colors duration-500">
|
||||
<div className="mx-auto max-w-7xl space-y-6">
|
||||
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-transparent p-4 md:p-6 transition-all duration-500 ease-in-out w-full relative">
|
||||
<div className="mx-auto w-full max-w-7xl space-y-6">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useMemo, useRef } from "react";
|
||||
import { Bell, RefreshCw, Calendar, AlertTriangle, Info, Clock, Check } from "lucide-react";
|
||||
import { Bell, RefreshCw, Calendar, AlertTriangle, Info, Clock, Check, Menu } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useThreats, useEvents } from "@/lib/api";
|
||||
|
||||
@@ -49,11 +49,13 @@ export function Header({
|
||||
isRefreshing = false,
|
||||
onRefresh = () => {},
|
||||
intervalText = "",
|
||||
onMenuClick,
|
||||
}: {
|
||||
lastUpdate?: string;
|
||||
isRefreshing?: boolean;
|
||||
onRefresh?: () => void;
|
||||
intervalText?: string;
|
||||
onMenuClick?: () => void;
|
||||
}) {
|
||||
const [user, setUser] = useState<{ role?: string; agent_uuid?: string } | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -188,19 +190,32 @@ export function Header({
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="flex min-h-[4rem] h-auto py-3 lg:py-0 lg:h-16 shrink-0 flex-col lg:flex-row items-center justify-between border-b border-border bg-background px-4 lg:px-6 gap-3">
|
||||
<div className="flex flex-wrap items-center justify-center lg:justify-start gap-2 lg:gap-4">
|
||||
<header className="flex min-h-[4rem] h-auto py-3 shrink-0 flex-row flex-wrap items-center justify-between border-b border-border bg-background px-4 lg:px-6 gap-3 z-20">
|
||||
<div className="flex flex-wrap items-center justify-start gap-2 lg:gap-4 w-full lg:w-auto">
|
||||
<button
|
||||
onClick={onMenuClick}
|
||||
className="p-2 -ml-2 mr-1 text-muted-foreground hover:text-white hover:bg-secondary/50 rounded-lg transition-colors shrink-0"
|
||||
>
|
||||
<Menu className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-2 rounded-full border border-border bg-card px-3 py-1 text-xs text-muted-foreground whitespace-nowrap shadow-sm">
|
||||
<div className="h-2 w-2 rounded-full bg-green-500 animate-pulse shrink-0" />
|
||||
<span className="truncate max-w-[100px] sm:max-w-[150px] md:max-w-none">
|
||||
Site: {process.env.NEXT_PUBLIC_SITE_ID || "Unknown Site"}
|
||||
<span className="truncate max-w-[200px] sm:max-w-[250px] md:max-w-none">
|
||||
Site: {process.env.NEXT_PUBLIC_SITE_ID || "Unknown Site"} - {(() => {
|
||||
if (user?.role === 'SUPER_ADMIN') return 'Admin';
|
||||
if (user?.role === 'AGENT_VIEWER') {
|
||||
const label = AGENT_LABELS[user.agent_uuid || ''];
|
||||
return label ? `Network Agent (${label})` : 'Network Agent';
|
||||
}
|
||||
return 'User';
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
{intervalText && (
|
||||
<div className="flex items-center gap-1.5 rounded-full bg-blue-500/10 border border-blue-500/20 px-3 py-1 text-xs text-blue-400 font-medium whitespace-nowrap">
|
||||
<Calendar className="w-3.5 h-3.5 shrink-0" />
|
||||
<span className="truncate max-w-[140px] sm:max-w-[250px] md:max-w-none">
|
||||
<span className="hidden lg:inline">Rentang Data: </span>
|
||||
{intervalText}
|
||||
</span>
|
||||
</div>
|
||||
@@ -208,21 +223,8 @@ export function Header({
|
||||
<RealtimeClock />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between lg:justify-end gap-2 lg:gap-4 w-full lg:w-auto">
|
||||
{user?.role === 'AGENT_VIEWER' && (
|
||||
<div className="flex items-center gap-2 rounded-full border border-blue-500/30 bg-blue-950/40 px-3 py-1 text-xs text-blue-300 shadow-[0_0_10px_rgba(59,130,246,0.1)] whitespace-nowrap">
|
||||
<span className="font-semibold hidden sm:inline">Agent:</span>
|
||||
<span className="truncate max-w-[120px] md:max-w-none">
|
||||
{AGENT_LABELS[user.agent_uuid || ''] || user.agent_uuid}
|
||||
</span>
|
||||
<span className="text-[10px] opacity-60 hidden md:inline">({user.agent_uuid})</span>
|
||||
</div>
|
||||
)}
|
||||
{user?.role === 'SUPER_ADMIN' && (
|
||||
<div className="flex items-center gap-2 rounded-full border border-red-500/30 bg-red-950/40 px-3 py-1 text-xs text-red-300 shadow-[0_0_10px_rgba(239,68,68,0.1)] whitespace-nowrap">
|
||||
<span className="font-semibold">Admin:</span> All Agents
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center justify-start lg:justify-end gap-2 lg:gap-4 w-full lg:w-auto mt-2 lg:mt-0">
|
||||
|
||||
<div className="text-xs text-muted-foreground whitespace-nowrap">
|
||||
Update: {lastUpdate}
|
||||
</div>
|
||||
@@ -293,7 +295,7 @@ export function Header({
|
||||
</span>
|
||||
<span className="text-[9px] text-slate-500 whitespace-nowrap flex items-center gap-0.5">
|
||||
<Clock className="w-2.5 h-2.5" />
|
||||
{new Date(n.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
{new Date(n.timestamp).toLocaleString('id-ID', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-300 leading-normal line-clamp-2">
|
||||
|
||||
@@ -47,7 +47,7 @@ const navigation = [
|
||||
{ name: "DPI Analytics", href: "/dpi-analytics", icon: Activity },
|
||||
];
|
||||
|
||||
export function Sidebar() {
|
||||
export function Sidebar({ isCollapsed = false }: { isCollapsed?: boolean }) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const { theme, setTheme } = useTheme();
|
||||
@@ -281,49 +281,66 @@ export function Sidebar() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-64 flex-col border-r border-border/50 bg-sidebar/80 backdrop-blur-2xl relative">
|
||||
<div className={cn(
|
||||
"flex h-full flex-col border-r border-border/50 bg-sidebar/80 backdrop-blur-2xl transition-all duration-500 ease-in-out relative z-30 overflow-x-hidden",
|
||||
isCollapsed ? "w-[72px]" : "w-64"
|
||||
)}>
|
||||
{/* Subtle glow effect behind sidebar */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-primary/5 via-transparent to-transparent pointer-events-none" />
|
||||
|
||||
<div className="flex h-[72px] shrink-0 items-center px-6 border-b border-border/50 relative z-10">
|
||||
<img src="/backone-logo.png" alt="BackOne Logo" className="w-8 h-8 mr-3 rounded-full drop-shadow-[0_0_10px_rgba(255,0,0,0.3)]" />
|
||||
<div className="flex flex-col">
|
||||
<div className="flex h-[72px] shrink-0 items-center border-b border-border/50 relative z-10 w-full overflow-hidden">
|
||||
<div className="w-[72px] shrink-0 flex justify-center items-center">
|
||||
<img src="/backone-logo.png" alt="BackOne Logo" className="w-8 h-8 rounded-full drop-shadow-[0_0_10px_rgba(255,0,0,0.3)]" />
|
||||
</div>
|
||||
<div className={cn(
|
||||
"flex flex-col whitespace-nowrap transition-opacity duration-500",
|
||||
isCollapsed ? "opacity-0" : "opacity-100"
|
||||
)}>
|
||||
<span className="text-lg font-bold font-backone text-white tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-white/70 leading-tight">
|
||||
BackOne
|
||||
</span>
|
||||
<span className="text-[10px] font-medium font-backone text-muted-foreground tracking-wide mt-0.5">
|
||||
<span className="text-[10px] font-medium font-backone text-muted-foreground mt-0.5 truncate w-[160px]">
|
||||
Deep Package Inspection
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="py-6 flex flex-col items-center border-b border-border/50 relative z-10">
|
||||
<div className="py-6 flex flex-col items-center border-b border-border/50 relative z-10 overflow-hidden w-full">
|
||||
<div className="relative">
|
||||
<div className="h-16 w-16 rounded-full bg-gradient-to-tr from-primary to-blue-400 p-0.5 shadow-lg shadow-primary/20 overflow-hidden">
|
||||
<div className={cn(
|
||||
"rounded-full bg-gradient-to-tr from-primary to-blue-400 p-0.5 shadow-lg shadow-primary/20 overflow-hidden transition-all duration-500 ease-in-out shrink-0 mx-auto",
|
||||
isCollapsed ? "h-10 w-10" : "h-16 w-16"
|
||||
)}>
|
||||
<div className="flex h-full w-full items-center justify-center rounded-full bg-background overflow-hidden relative">
|
||||
{user?.profile_picture ? (
|
||||
<img src={`/api/uploads/${user.profile_picture}`} alt="Profile" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<span className="text-xl font-bold text-white uppercase">
|
||||
<span className={cn("font-bold text-white uppercase transition-all duration-500", isCollapsed ? "text-base" : "text-xl")}>
|
||||
{user?.account_name ? user.account_name.charAt(0) : (user?.username ? user.username.charAt(0) : (user?.role === 'SUPER_ADMIN' ? 'A' : 'V'))}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -bottom-1 -right-1 h-4 w-4 rounded-full border-2 border-background bg-green-500 shadow-sm" />
|
||||
<div className={cn(
|
||||
"absolute rounded-full border-2 border-background bg-green-500 shadow-sm transition-all duration-500",
|
||||
isCollapsed ? "-bottom-0.5 -right-0.5 h-3 w-3" : "-bottom-1 -right-1 h-4 w-4"
|
||||
)} />
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-col items-center w-full px-4 space-y-1.5">
|
||||
<span className="font-semibold text-white tracking-wide text-center leading-snug break-words max-w-full">
|
||||
<div className={cn(
|
||||
"flex flex-col items-center transition-all duration-500 ease-in-out overflow-hidden whitespace-nowrap",
|
||||
isCollapsed ? "opacity-0 max-h-0 mt-0 max-w-0" : "opacity-100 max-h-[100px] mt-4 w-[256px]"
|
||||
)}>
|
||||
<span className="font-semibold text-white tracking-wide text-center leading-snug truncate w-full px-4">
|
||||
{user?.account_name || user?.username || 'Loading...'}
|
||||
</span>
|
||||
<span className="rounded-full bg-primary/20 px-2.5 py-0.5 text-[10px] font-medium text-primary border border-primary/30 shrink-0">
|
||||
<span className="mt-1.5 rounded-full bg-primary/20 px-2.5 py-0.5 text-[10px] font-medium text-primary border border-primary/30 shrink-0">
|
||||
{user?.role === 'SUPER_ADMIN' ? 'PRO' : 'STD'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col overflow-y-auto relative z-10 [&::-webkit-scrollbar]:w-1 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:bg-white/10 [&::-webkit-scrollbar-thumb]:rounded-full hover:[&::-webkit-scrollbar-thumb]:bg-white/20 transition-colors">
|
||||
<nav className="flex-1 space-y-1.5 px-3 py-6">
|
||||
<nav className="flex-1 py-6 flex flex-col items-center overflow-hidden w-full space-y-1.5">
|
||||
{filteredNavigation.map((item) => {
|
||||
const isActive = pathname === item.href;
|
||||
return (
|
||||
@@ -331,31 +348,38 @@ export function Sidebar() {
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"group relative flex items-center rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200 overflow-hidden",
|
||||
"group relative flex items-center rounded-lg py-2.5 text-sm font-medium transition-all duration-500 overflow-hidden shrink-0",
|
||||
isCollapsed ? "w-[48px]" : "w-[232px]",
|
||||
isActive
|
||||
? "bg-primary/10 text-white shadow-[0_0_10px_rgba(59,130,246,0.1)]"
|
||||
: "text-muted-foreground hover:bg-secondary/50 hover:text-white"
|
||||
)}
|
||||
title={isCollapsed ? item.name : undefined}
|
||||
>
|
||||
{/* Active indicator bar */}
|
||||
{isActive && (
|
||||
<div className="absolute left-0 top-0 bottom-0 w-1 bg-primary rounded-r-full shadow-[0_0_10px_rgba(59,130,246,0.8)]" />
|
||||
)}
|
||||
|
||||
<item.icon
|
||||
className={cn(
|
||||
"mr-3 h-5 w-5 shrink-0 transition-all duration-300",
|
||||
isActive
|
||||
? "text-primary drop-shadow-[0_0_8px_rgba(59,130,246,0.5)] scale-110"
|
||||
: "text-muted-foreground group-hover:text-white group-hover:scale-110"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="relative z-10">{item.name}</span>
|
||||
<div className="w-[48px] shrink-0 flex justify-center items-center">
|
||||
<item.icon
|
||||
className={cn(
|
||||
"h-5 w-5 transition-all duration-300",
|
||||
isActive
|
||||
? "text-primary drop-shadow-[0_0_8px_rgba(59,130,246,0.5)] scale-110"
|
||||
: "text-muted-foreground group-hover:text-white group-hover:scale-110"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<span className={cn(
|
||||
"relative z-10 transition-opacity duration-500 whitespace-nowrap",
|
||||
isCollapsed ? "opacity-0" : "opacity-100"
|
||||
)}>{item.name}</span>
|
||||
|
||||
{/* Hover background effect */}
|
||||
{!isActive && (
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-white/0 via-white/5 to-white/0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none translate-x-[-100%] group-hover:translate-x-[100%] transition-transform" />
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-white/0 via-white/5 to-white/0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none translate-x-[-100%] group-hover:translate-x-[100%]" />
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
@@ -363,7 +387,7 @@ export function Sidebar() {
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="px-3 py-4 border-t border-border/50 relative z-10 shrink-0 space-y-1.5 flex flex-col">
|
||||
<div className="py-4 border-t border-border/50 relative z-10 shrink-0 space-y-1.5 flex flex-col items-center w-full">
|
||||
<button
|
||||
onClick={() => {
|
||||
setErrorMsg("");
|
||||
@@ -381,21 +405,39 @@ export function Sidebar() {
|
||||
setActiveTab("account_name");
|
||||
setIsAccountOpen(true);
|
||||
}}
|
||||
className="flex w-full items-center rounded-lg px-3 py-2.5 text-sm font-medium text-slate-400 hover:bg-secondary/50 hover:text-white transition-all duration-200"
|
||||
className={cn(
|
||||
"relative flex items-center rounded-lg py-2.5 text-sm font-medium text-slate-400 hover:bg-secondary/50 hover:text-white transition-all duration-500 overflow-hidden shrink-0",
|
||||
isCollapsed ? "w-[48px]" : "w-[232px]"
|
||||
)}
|
||||
title={isCollapsed ? "Account Settings" : undefined}
|
||||
>
|
||||
<User className="mr-3 h-5 w-5" />
|
||||
Account Settings
|
||||
<div className="w-[48px] shrink-0 flex justify-center items-center">
|
||||
<User className="h-5 w-5 transition-all duration-500" />
|
||||
</div>
|
||||
<span className={cn(
|
||||
"transition-opacity duration-500 whitespace-nowrap",
|
||||
isCollapsed ? "opacity-0" : "opacity-100"
|
||||
)}>Account Settings</span>
|
||||
</button>
|
||||
<div className="flex w-full space-x-1.5">
|
||||
<div className={cn("flex transition-all duration-500", isCollapsed ? "w-[48px] flex-col space-y-1.5" : "w-[232px] space-x-1.5")}>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex flex-1 items-center rounded-lg px-3 py-2.5 text-sm font-medium text-red-400 hover:bg-red-500/10 hover:text-red-300 transition-all duration-200"
|
||||
className={cn(
|
||||
"relative flex items-center rounded-lg py-2.5 text-sm font-medium text-red-400 hover:bg-red-500/10 hover:text-red-300 transition-all duration-500 overflow-hidden shrink-0",
|
||||
isCollapsed ? "w-[48px]" : "flex-1"
|
||||
)}
|
||||
title={isCollapsed ? "Logout" : undefined}
|
||||
>
|
||||
<LogOut className="mr-3 h-5 w-5" />
|
||||
Logout
|
||||
<div className="w-[48px] shrink-0 flex justify-center items-center">
|
||||
<LogOut className="h-5 w-5 transition-all duration-500" />
|
||||
</div>
|
||||
<span className={cn(
|
||||
"transition-opacity duration-500 whitespace-nowrap",
|
||||
isCollapsed ? "opacity-0" : "opacity-100"
|
||||
)}>Logout</span>
|
||||
</button>
|
||||
|
||||
{mounted && (
|
||||
{mounted && !isCollapsed && (
|
||||
<button
|
||||
onClick={() => {
|
||||
const nextTheme = theme === 'dark' ? 'light' : 'dark';
|
||||
@@ -407,7 +449,7 @@ export function Sidebar() {
|
||||
setTheme(nextTheme);
|
||||
});
|
||||
}}
|
||||
className="flex items-center justify-center rounded-lg px-3 py-2.5 text-slate-400 hover:bg-secondary/50 hover:text-white transition-all duration-200"
|
||||
className="flex items-center justify-center rounded-lg px-3 py-2.5 text-slate-400 hover:bg-secondary/50 hover:text-white transition-all duration-200 shrink-0"
|
||||
title="Toggle Theme"
|
||||
>
|
||||
{theme === 'dark' ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
|
||||
|
||||
Reference in new issue
Block a user