Current state
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
import { Sidebar } from "./sidebar"
|
||||
import { Topbar } from "./topbar"
|
||||
import { SystemMonitor } from "./system-monitor"
|
||||
|
||||
interface AppShellProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function AppShell({ children }: AppShellProps) {
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
|
||||
const [mobileOpen, setMobileOpen] = useState(false)
|
||||
const pathname = usePathname()
|
||||
|
||||
// Close mobile sidebar on route change
|
||||
useEffect(() => {
|
||||
setMobileOpen(false)
|
||||
}, [pathname])
|
||||
|
||||
// Persist sidebar state
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem("sidebar-collapsed")
|
||||
if (saved) setSidebarCollapsed(saved === "true")
|
||||
}, [])
|
||||
|
||||
const toggleSidebar = () => {
|
||||
const next = !sidebarCollapsed
|
||||
setSidebarCollapsed(next)
|
||||
localStorage.setItem("sidebar-collapsed", String(next))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background dark:bg-[#0A0A0A] relative overflow-hidden">
|
||||
|
||||
<SystemMonitor />
|
||||
<Sidebar
|
||||
collapsed={sidebarCollapsed}
|
||||
onToggle={toggleSidebar}
|
||||
mobileOpen={mobileOpen}
|
||||
onMobileClose={() => setMobileOpen(false)}
|
||||
/>
|
||||
|
||||
<div className={cn("transition-all duration-300", sidebarCollapsed ? "lg:ml-16" : "lg:ml-64")}>
|
||||
<Topbar onMenuClick={() => setMobileOpen(true)} />
|
||||
|
||||
<main className="flex-1 p-4 lg:p-6">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={pathname}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -8 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function cn(...classes: (string | boolean | undefined | null)[]) {
|
||||
return classes.filter(Boolean).join(" ")
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client"
|
||||
|
||||
export function CrmIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 64 64"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<g className="fill-[#333333] dark:fill-white/[0.15] dark:stroke-[#e5e7eb] dark:[stroke-width:1.5px]">
|
||||
<path className="dark:[stroke-linejoin:round]" d="m10.35 35.62s-.75.14-1-1.48-.58-8.61-.3-8.87a10 10 0 0 1 2.07-.63s0-7.54 2.67-11.86 5.4-6.09 10.65-7.56a29.38 29.38 0 0 1 15.18.3c4.41 1.18 8.7 4.62 9.89 8.8a49.27 49.27 0 0 1 1.49 9.55 9.53 9.53 0 0 1 2.52.9c.17.33.43 9.12 0 9.38a1.6 1.6 0 0 1 -.87.23s7.86 18 7.59 19-12.93 6.36-30.1 6.23-26.05-5.41-26.36-6.11 6.57-17.88 6.57-17.88z" />
|
||||
</g>
|
||||
<g className="fill-[#777777] dark:fill-white/[0.1] dark:stroke-[#d4d4d4] dark:[stroke-width:1px]">
|
||||
<path d="m13.24 27.92s-.6-7.65 1.3-11.91a19.6 19.6 0 0 1 7.75-8.23 20.47 20.47 0 0 1 6.51-1.45c.05.16.67 10.23.66 11.61a27 27 0 0 1 -.57 4s-4-2.44-8.16-1.38a10.47 10.47 0 0 0 -7.49 7.36z" />
|
||||
<path d="m30.11 22.18s.53-3.69.54-4.89-.8-10.82-.6-10.95a4.11 4.11 0 0 1 1.88 0c.07.16 1.07 11.66 1.28 13.22a9.33 9.33 0 0 1 .27 2.44 2.85 2.85 0 0 1 -3.37.18z" />
|
||||
<path d="m34.8 21.44a76.48 76.48 0 0 1 -1.4-9.35c-.17-3.62-.51-5.65-.06-5.83s6.66.17 10.42 3.45 4.06 5.61 3.81 5.66-2.52-1-2.64-.77-.15.8.06.92 2.69 1.1 2.82 1.26a4.18 4.18 0 0 1 .28.95 16.94 16.94 0 0 0 -3-1.13c-.12.17-.23.92 0 1s3 1.3 3 1.3l.11.79s-2.83-1.18-3-1.14-.36.8-.15.92 3.16 1.26 3.25 1.43.41 1.36.41 1.36l-1.71-.54s.31.87.48 1 1.3.42 1.31.76a31.35 31.35 0 0 1 .2 3.33c-.12 0-2.2-6.45-5.21-7.09s-8.98 1.72-8.98 1.72z" />
|
||||
<path d="m22.44 22.28c2.21 0 7.19 2.45 9.76 1.84s6.89-2.79 9.51-2.81 4.07 2.48 6.36 7.84 7.14 17.34 7 17.34-8.1-17.89-9.66-20.23-2.66-3-5.86-2.77-6.81 1-6.85 1.41 0 7.17.49 7.75 3.06.71 6.18.51 5-.33 5.77-.64a10.14 10.14 0 0 0 1.69-1l7.79 16.48a30.15 30.15 0 0 0 -2.94-2.56c-.21.05-.31.8-.19.88s3.67 3.33 4 3.7a12.43 12.43 0 0 1 1.61 2.63c-.12.17-.45.55-.66.47s-5-5.71-5.39-5.83-.45.6-.45.6 5.18 5.5 5.06 5.63a8.62 8.62 0 0 1 -1.15.57s-4.34-5-4.5-4.94-.69.6-.4.81a45.71 45.71 0 0 1 4.07 4.36 7 7 0 0 1 -1.2.4 46.49 46.49 0 0 0 -3.58-3.72c-.25 0-.61.39-.49.55s3.34 3.29 3.22 3.46a4 4 0 0 1 -1.16.37s-2.47-2.59-2.65-2.59-.74.31-.53.6 2.39 2.19 2.27 2.28a4.86 4.86 0 0 1 -1 .19c-.13 0-2-2-2.22-2s-.78.19-.65.48 1.83 1.79 1.63 1.92-5.17 1.66-15.76 1.51-14.84-1.85-14.84-1.85a46 46 0 0 0 -4.22-3.69c-.37 0-.78.6-.61.72a29.31 29.31 0 0 1 2.86 2.6c-.16 0-3.27-.92-4.32-1.1a7.6 7.6 0 0 1 -2.26-.61c0-.13 6-14.45 6.27-14.41s4.29 10.07 4.29 10.07a2.77 2.77 0 0 0 1.2 2.89 3.9 3.9 0 0 0 3.49-.25l15.51.08c.12 0 2.65 1.27 3.69-.68s.44-2.67.44-2.67 4-11.27 3.76-11.52a5.63 5.63 0 0 0 -1.76-.35s-2.75 10.32-3 10.33-1.26-.3-1.68-.34a3.18 3.18 0 0 0 -.7 0l-5.48-7.62s1.1-6-3.28-6-3.14 6-3.14 6-1-.06-2 1.51-3.39 6.29-3.39 6.29a3.77 3.77 0 0 0 -1.45.2c-.66.27-1.07.49-1.07.49s-3.54-8.29-3.46-8.46.82-2.15 1.2-2.16 2.93.39 3.93.41 1.16-.11 1.2-.28.27-.8.1-.8-5.11-.57-5.48-.57-1.07 2.14-1.36 2.23-1.32.25-1.2 0 2.16-5.13 2.29-5.1 7.53 1.07 9.73.64 2.76-1.11 3.07-2.16a31.45 31.45 0 0 0 .25-6.59 19.1 19.1 0 0 0 -6.2-1.31c-2.79-.09-4.67-.13-5.87 1.82s-10.79 27.71-10.96 27.76a3.64 3.64 0 0 1 -.92-.29s4.91-14 6.78-19.26 3.47-11.56 9.47-11.46z" />
|
||||
<path d="m28.76 40.3a14.69 14.69 0 0 0 2.25 0l2.29-.09s-.39 9.67 0 9.75.67.27.66-.06-.19-7.75.06-7.79.75 1.52.75 1.52-.14 6.42 0 6.46.88.27.84.06 0-4.75 0-4.75l1.43 2.26-.79.06s-.08 2 .17 2a4.75 4.75 0 0 0 .71 0c.12 0 .11 1.16.11 1.16s-7.26-.19-9.8-.13-2.74.11-2.74.11a6.75 6.75 0 0 0 -.55-2l-.62-1.28s4.86-7.15 5.23-7.28z" />
|
||||
</g>
|
||||
<g className="fill-[#333333] dark:fill-white/[0.15] dark:stroke-[#d4d4d4] dark:[stroke-width:0.7px]">
|
||||
<path d="m30.57 41.13c.28-.1.79-.06.84.1s.41 8.41.17 8.63-.88.14-.88 0-.25-8.69-.13-8.73z" />
|
||||
<path d="m28.74 41.26c.21-.09.7-.19.79 0s.55 8.61.38 8.66-.87.18-.87 0-.3-8.66-.3-8.66z" />
|
||||
<path d="m27 44.8c.06-.14.87-.4.87-.23a52.12 52.12 0 0 1 .21 5.25 3 3 0 0 1 -.79.06s-.38-4.88-.29-5.08z" />
|
||||
<path d="m25.25 46.8c0-.17.57-.64.65-.51a14 14 0 0 1 .26 3.45 4 4 0 0 1 -.79.14s-.16-2.88-.12-3.08z" />
|
||||
<path d="m41.23 35.62c.19-.1 2.79-.16 4.58-.37a11 11 0 0 1 1.78-.25c.21 0 .48.87.36 1a17.41 17.41 0 0 1 -4.32.69c-1.46 0-2.29 0-2.29 0s-.48-.9-.11-1.07z" />
|
||||
<path d="m42 38.39c.34-.09 1.41 1.51 1.41 1.51s-.19.79-.4.63a9.86 9.86 0 0 1 -1.5-1.63c-.08-.21.29-.46.49-.51z" />
|
||||
<path d="m40.41 39.68c.29-.1 2.3 1.78 2.22 2s-.15.63-.28.59a19.84 19.84 0 0 1 -2.43-2.11c-.04-.22.37-.44.49-.48z" />
|
||||
<path d="m39.41 41.16s2.55 2 2.47 2.32a2.36 2.36 0 0 1 -.36.67s-2.93-2.38-2.81-2.6.53-.26.7-.39z" />
|
||||
<path d="m14.45 48.4c.2.07 2.81 2.31 2.82 2.72s-.11.63-.24.63a27 27 0 0 1 -2.93-2.38c-.05-.17.14-1.05.35-.97z" />
|
||||
<path d="m13.67 50.5s3.83 3.16 3.83 3.37-.11.67-.23.67a35.1 35.1 0 0 1 -4.08-3.07c-.04-.25.48-.97.48-.97z" />
|
||||
<path d="m16.27 17c-.17-.24.44-2.3 1.57-3.91s2.19-2.52 2.65-2.4.77.61.6.82a7.05 7.05 0 0 0 -2.72 2.86c-.79 1.85-.55 3-.8 3.06a1.86 1.86 0 0 1 -1.3-.43z" />
|
||||
<path d="m44.85 13.23a2.79 2.79 0 0 1 1.6.54c.09.25.27.83-.11.76a10.8 10.8 0 0 1 -1.43-.53c-.17-.06-.31-.72-.06-.77z" />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
"use client"
|
||||
|
||||
export function SidebarNameLogo() {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 192.756 192.756"
|
||||
className="fill-[#333333] dark:fill-[#e5e7eb] w-full h-auto"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<g fill-rule="evenodd" clip-rule="evenodd">
|
||||
<path d="M5.669 81.215v12.65h37.003c4.301 0 9.796-3.977 9.796-10.556 0-2.646 1.012-4.372-2.098-7.872l-4.733-5.608c-2.712-2.53.324-2.53 2.602-2.53h15.623v26.566h12.39V67.299h16.699V55.922H38.877c-6.579 0-9.796 6.317-9.615 9.606.182 3.289.787 7.427 6.254 12.398 4.987 4.533-2.469 3.289-3.218 3.289H5.669zM120.348 55.922H100.36L89.155 93.866h12.47l2.023-5.313h13.156l1.953 5.313h12.215l-10.624-37.944zm-13.916 23.522l4.301-13.916 4.049 13.916h-8.35zM170.443 81.215c-4.807 0-4.807-1.771-4.807-1.771 4.119 0 7.771-6.001 7.771-12.145s-6-11.377-10.809-11.377h-26.891v37.944h13.664v-12.65s5.818 6.831 8.854 9.614c3.037 2.783 3.289 3.036 7.41 3.036h21.449v-12.65c.002-.001-11.834-.001-16.641-.001zm-12.398-8.855h-8.672v-6.832h8.672c3.976 0 4.664 6.832 0 6.832zM5.669 98.672h13.979l3.542 12.652 3.289-12.652h14.675l3.795 12.652 3.796-12.652h12.144l-11.133 37.953H38.624l-4.878-17.965-5.496 17.965H16.865L5.669 98.672zM89.578 98.891H69.59l-11.204 37.943h12.469l2.024-5.312h13.157l1.953 5.312h12.216L89.578 98.891zm-13.915 23.521l4.301-13.916 4.048 13.916h-8.349zM170.695 110.059c-2.275 0-4.756.266-2.043 2.795l4.734 5.609c3.109 3.5 3.059 4.959 3.059 7.607 0 6.578-6.508 10.555-10.809 10.555l-29.896.201c-4.119 0-4.371-.252-7.408-3.035-3.035-2.783-8.855-9.615-8.855-9.615v12.65h-13.662V98.883h26.891c4.807 0 10.809 5.234 10.809 11.377 0 6.145-3.652 12.145-7.773 12.145 0 0 1.812 1.822 4.848 1.822 3.037 0 14.727.012 14.727.012.748 0 8.203 1.244 3.217-3.289-5.467-4.971-6.072-9.107-6.254-12.396s2.662-9.881 9.238-9.881h25.57v11.387h-16.393v-.001zm-42.545 5.261h-8.674v-6.832h8.674c3.977 0 4.664 6.832 0 6.832z" />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Users,
|
||||
Settings,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Building2,
|
||||
PanelLeftClose,
|
||||
MessageSquare,
|
||||
Bot,
|
||||
Facebook,
|
||||
} from "lucide-react"
|
||||
import { COMPANY_NAME } from "@/lib/constants"
|
||||
import { useUser } from "@/providers/user-provider"
|
||||
import { useNotifications } from "@/providers/notification-provider"
|
||||
import { FacebookAccountsDialog } from "@/components/settings/facebook-accounts-dialog"
|
||||
|
||||
const navItems = [
|
||||
{ href: "/dashboard", label: "Dashboard", icon: LayoutDashboard },
|
||||
{ href: "/leads", label: "Leads", icon: Users },
|
||||
{ href: "/chats", label: "Chats", icon: MessageSquare },
|
||||
{ href: "/ai-assistant", label: "AI Assistant", icon: Bot, roles: ["sales", "admin", "super_admin"] },
|
||||
{ href: "/users", label: "Users", icon: Building2 },
|
||||
{ href: "/settings", label: "Settings", icon: Settings },
|
||||
]
|
||||
|
||||
interface SidebarProps {
|
||||
collapsed: boolean
|
||||
onToggle: () => void
|
||||
mobileOpen: boolean
|
||||
onMobileClose: () => void
|
||||
}
|
||||
|
||||
export function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose }: SidebarProps) {
|
||||
const pathname = usePathname()
|
||||
const { user } = useUser()
|
||||
const { unreadChatCount } = useNotifications()
|
||||
const [fbDialogOpen, setFbDialogOpen] = useState(false)
|
||||
if (!user) return null
|
||||
const isAdmin = user.role === "admin" || user.role === "super_admin"
|
||||
const initials = user.name.split(" ").map((n) => n[0]).join("")
|
||||
|
||||
const sidebarContent = (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full flex-col bg-sidebar text-sidebar-foreground transition-all duration-300",
|
||||
collapsed ? "w-16" : "w-64"
|
||||
)}
|
||||
>
|
||||
{/* Logo */}
|
||||
<div className={cn("flex h-16 items-center border-b border-sidebar-border px-4", collapsed ? "justify-center" : "justify-between")}>
|
||||
<Link href="/" className="flex items-center gap-3 overflow-hidden">
|
||||
<img
|
||||
src="/logo/CompanyLogo.png"
|
||||
alt={COMPANY_NAME}
|
||||
className="h-8 w-8 shrink-0 rounded-lg object-contain"
|
||||
/>
|
||||
<AnimatePresence mode="wait">
|
||||
{!collapsed && (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -10 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="text-sm font-semibold whitespace-nowrap"
|
||||
>
|
||||
{COMPANY_NAME}
|
||||
</motion.span>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Link>
|
||||
{!collapsed && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onToggle}
|
||||
className="h-6 w-6 text-sidebar-foreground/60 hover:text-sidebar-foreground"
|
||||
>
|
||||
<PanelLeftClose className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 space-y-1 p-3">
|
||||
{navItems.filter((item) => !item.roles || item.roles.includes(user.role)).map((item) => {
|
||||
const isActive = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href))
|
||||
return collapsed ? (
|
||||
<TooltipProvider key={item.href} delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 items-center justify-center rounded-lg transition-colors",
|
||||
isActive
|
||||
? "bg-sidebar-primary text-sidebar-primary-foreground"
|
||||
: "text-sidebar-foreground/60 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||
)}
|
||||
>
|
||||
<item.icon className="h-5 w-5" />
|
||||
{item.label === "Chats" && unreadChatCount > 0 && (
|
||||
<span className="absolute -right-0.5 -top-0.5 flex h-3 w-3 rounded-full bg-red-500 animate-pulse shadow-[0_0_10px_#ef4444]" />
|
||||
)}
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="ml-2">
|
||||
{item.label}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
|
||||
isActive
|
||||
? "bg-sidebar-primary text-sidebar-primary-foreground"
|
||||
: "text-sidebar-foreground/60 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||
)}
|
||||
>
|
||||
<div className="relative shrink-0">
|
||||
<item.icon className="h-5 w-5" />
|
||||
{item.label === "Chats" && unreadChatCount > 0 && (
|
||||
<span className="absolute -right-1 -top-1 flex h-2.5 w-2.5 rounded-full bg-red-500 animate-pulse shadow-[0_0_8px_#ef4444]" />
|
||||
)}
|
||||
</div>
|
||||
<motion.span
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="truncate"
|
||||
>
|
||||
{item.label}
|
||||
</motion.span>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
{isAdmin && collapsed && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={() => setFbDialogOpen(true)}
|
||||
className="relative flex h-10 w-10 items-center justify-center rounded-lg transition-colors text-sidebar-foreground/60 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||
>
|
||||
<Facebook className="h-5 w-5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="ml-2">
|
||||
Facebook Accounts
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
{isAdmin && !collapsed && (
|
||||
<button
|
||||
onClick={() => setFbDialogOpen(true)}
|
||||
className="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors text-sidebar-foreground/60 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||
>
|
||||
<Facebook className="h-5 w-5 shrink-0" />
|
||||
<motion.span
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="truncate"
|
||||
>
|
||||
Facebook Accounts
|
||||
</motion.span>
|
||||
</button>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{/* Collapse toggle at bottom (only in collapsed mode) */}
|
||||
{collapsed && (
|
||||
<div className="border-t border-sidebar-border p-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onToggle}
|
||||
className="h-10 w-10 text-sidebar-foreground/60 hover:text-sidebar-foreground"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* User info */}
|
||||
<div className={cn("border-t border-sidebar-border p-3", collapsed && "flex justify-center")}>
|
||||
{collapsed ? (
|
||||
<Avatar className="h-10 w-10">
|
||||
<AvatarImage src={user.avatar} />
|
||||
<AvatarFallback className="text-sm font-medium">{initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
) : (
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar className="h-9 w-9">
|
||||
<AvatarImage src={user.avatar} />
|
||||
<AvatarFallback className="text-sm font-medium">{initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<p className="text-sm font-medium truncate">{user.name}</p>
|
||||
<p className="text-xs text-sidebar-foreground/60 truncate capitalize">{user.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Desktop sidebar */}
|
||||
<aside className="hidden lg:fixed lg:inset-y-0 lg:z-30 lg:flex">
|
||||
{sidebarContent}
|
||||
</aside>
|
||||
|
||||
<FacebookAccountsDialog open={fbDialogOpen} onOpenChange={setFbDialogOpen} />
|
||||
|
||||
{/* Mobile sidebar overlay */}
|
||||
<AnimatePresence>
|
||||
{mobileOpen && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={onMobileClose}
|
||||
className="fixed inset-0 z-40 bg-black/60 lg:hidden"
|
||||
/>
|
||||
<motion.aside
|
||||
initial={{ x: -300 }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: -300 }}
|
||||
transition={{ type: "spring", damping: 30, stiffness: 300 }}
|
||||
className="fixed inset-y-0 left-0 z-50 lg:hidden"
|
||||
>
|
||||
{sidebarContent}
|
||||
</motion.aside>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
|
||||
const RAM_LIMIT_MB = 8192
|
||||
const CPU_LIMIT_PCT = 400 // 4 cores * 100%
|
||||
|
||||
export function SystemMonitor() {
|
||||
const [rssMB, setRssMB] = useState(0)
|
||||
const [cpuPct, setCpuPct] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/system/monitor")
|
||||
if (!res.ok) return
|
||||
const data = await res.json()
|
||||
setRssMB(data.rssMB)
|
||||
setCpuPct(data.cpuPct)
|
||||
} catch {
|
||||
console.warn("Failed to fetch system stats")
|
||||
}
|
||||
}
|
||||
|
||||
fetchStats()
|
||||
const interval = setInterval(fetchStats, 3000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
const ramOver = rssMB > RAM_LIMIT_MB
|
||||
const cpuOver = cpuPct > CPU_LIMIT_PCT
|
||||
|
||||
return (
|
||||
<div className="fixed top-0 left-0 z-[9999] flex items-center gap-3 px-3 py-1 text-[11px] font-mono bg-black/80 rounded-br-lg select-none">
|
||||
<span className={ramOver ? "text-red-400" : "text-green-400"}>
|
||||
RAM: {rssMB}MB / 8192MB
|
||||
</span>
|
||||
<span className={cpuOver ? "text-red-400" : "text-green-400"}>
|
||||
CPU: {cpuPct}%
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTheme } from "next-themes";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { useUser } from "@/providers/user-provider";
|
||||
import { useNotifications } from "@/providers/notification-provider";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Search,
|
||||
Bell,
|
||||
Sun,
|
||||
Moon,
|
||||
Menu,
|
||||
LogOut,
|
||||
User,
|
||||
Settings,
|
||||
CheckCheck,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { CrmIcon } from "./crm-icon";
|
||||
|
||||
interface TopbarProps {
|
||||
onMenuClick: () => void;
|
||||
}
|
||||
|
||||
export function Topbar({ onMenuClick }: TopbarProps) {
|
||||
const router = useRouter();
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { user, logout } = useUser();
|
||||
const { notifications, unreadCount, markAsRead, markAllAsRead, dismiss } =
|
||||
useNotifications();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
if (!user) return null;
|
||||
|
||||
function formatTimeAgo(ts: string): string {
|
||||
const diff = Date.now() - new Date(ts).getTime();
|
||||
const mins = Math.floor(diff / 60000);
|
||||
if (mins < 1) return "Just now";
|
||||
if (mins < 60) return `${mins}m ago`;
|
||||
const hrs = Math.floor(mins / 60);
|
||||
if (hrs < 24) return `${hrs}h ago`;
|
||||
const days = Math.floor(hrs / 24);
|
||||
if (days < 7) return `${days}d ago`;
|
||||
return new Date(ts).toLocaleDateString();
|
||||
}
|
||||
const initials = user.name
|
||||
.split(" ")
|
||||
.map((n: string) => n[0])
|
||||
.join("")
|
||||
.toUpperCase();
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-20 flex h-16 items-center gap-4 border-b bg-card dark:bg-[#141414] px-4 lg:px-6 relative overflow-hidden">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-2 mr-2 shrink-0">
|
||||
<CrmIcon />
|
||||
<span className="text-[#CC0000] dark:text-[#FF1111] font-bold text-sm tracking-wide">
|
||||
CRM
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="lg:hidden"
|
||||
onClick={onMenuClick}
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative hidden flex-1 sm:block md:w-80">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#888888] dark:text-[#666666]" />
|
||||
<Input
|
||||
placeholder="Search leads, companies..."
|
||||
className="h-9 w-full max-w-sm pl-9 bg-[#F0F0F0] dark:bg-[#1E1E1E] border-[#E0E0E0] dark:border-[#222222] text-[#111111] dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 items-center justify-end gap-3">
|
||||
{/* Mobile search toggle */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="sm:hidden"
|
||||
onClick={() => setSearchOpen(!searchOpen)}
|
||||
>
|
||||
<Search className="h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
{/* Theme toggle */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
{mounted ? (
|
||||
theme === "dark" ? (
|
||||
<Sun className="h-5 w-5" />
|
||||
) : (
|
||||
<Moon className="h-5 w-5" />
|
||||
)
|
||||
) : (
|
||||
<div className="h-5 w-5" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Notifications */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="relative text-muted-foreground"
|
||||
>
|
||||
<Bell className="h-5 w-5" />
|
||||
<span className="absolute -right-0.5 -top-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-primary text-[10px] font-medium text-primary-foreground">
|
||||
{unreadCount}
|
||||
</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-80">
|
||||
<DropdownMenuLabel className="flex items-center justify-between">
|
||||
<span>Notifications</span>
|
||||
{notifications.length > 0 && (
|
||||
<button
|
||||
onClick={markAllAsRead}
|
||||
className="flex items-center gap-1 text-xs text-primary hover:underline"
|
||||
>
|
||||
<CheckCheck className="h-3 w-3" />
|
||||
Mark all read
|
||||
</button>
|
||||
)}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{notifications.length === 0 ? (
|
||||
<div className="py-6 text-center text-sm text-muted-foreground">
|
||||
No notifications
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-[360px] space-y-1 overflow-y-auto p-2">
|
||||
{notifications.slice(0, 20).map((n) => (
|
||||
<div
|
||||
key={n.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
if (!n.read) markAsRead(n.id);
|
||||
if (n.link) router.push(n.link);
|
||||
}}
|
||||
className={`flex cursor-pointer items-start gap-3 rounded-lg p-2 transition-colors hover:bg-muted/50 ${!n.read ? "bg-muted/30" : ""}`}
|
||||
>
|
||||
<div
|
||||
className={`mt-2 h-2 w-2 shrink-0 rounded-full ${n.read ? "bg-transparent" : "bg-primary"}`}
|
||||
/>
|
||||
<div className="flex-1 space-y-0.5">
|
||||
<p className="text-sm font-medium">{n.title}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{n.description}
|
||||
</p>
|
||||
<p className="text-[10px] text-muted-foreground/60">
|
||||
{formatTimeAgo(n.timestamp)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
dismiss(n.id);
|
||||
}}
|
||||
className="mt-1 shrink-0 text-muted-foreground/40 hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* User profile */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="relative h-9 gap-2 pl-2 pr-3">
|
||||
<Avatar className="h-7 w-7">
|
||||
<AvatarImage src={user.avatar} />
|
||||
<AvatarFallback>{initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="hidden text-sm font-medium md:inline-block">
|
||||
{user.name}
|
||||
</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm font-medium">{user.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{user.email}</p>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="mt-1 w-fit text-xs capitalize"
|
||||
>
|
||||
{user.role}
|
||||
</Badge>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => router.push("/profile")}>
|
||||
<User className="mr-2 h-4 w-4" />
|
||||
Profile
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => router.push("/settings")}>
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
Settings
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="text-destructive" onClick={logout}>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user