244 lines
7.9 KiB
TypeScript
244 lines
7.9 KiB
TypeScript
import { Outlet, Link, useLocation, useNavigate } from "react-router-dom"
|
|
import { useEffect, useState } from "react"
|
|
import {
|
|
LayoutDashboard,
|
|
Users,
|
|
FileText,
|
|
Settings,
|
|
Wrench,
|
|
Megaphone,
|
|
Shield,
|
|
BarChart3,
|
|
Activity,
|
|
Heart,
|
|
Search,
|
|
Bell,
|
|
LogOut,
|
|
} from "lucide-react"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu"
|
|
import { cn } from "@/lib/utils"
|
|
import { authService } from "@/services"
|
|
import { toast } from "sonner"
|
|
|
|
interface User {
|
|
email: string
|
|
firstName?: string
|
|
lastName?: string
|
|
role: string
|
|
}
|
|
|
|
const adminNavigationItems = [
|
|
{ icon: LayoutDashboard, label: "Dashboard", path: "/admin/dashboard" },
|
|
{ icon: Users, label: "Users", path: "/admin/users" },
|
|
{ icon: FileText, label: "Logs", path: "/admin/logs" },
|
|
{ icon: Settings, label: "Settings", path: "/admin/settings" },
|
|
{ icon: Wrench, label: "Maintenance", path: "/admin/maintenance" },
|
|
{ icon: Megaphone, label: "Announcements", path: "/admin/announcements" },
|
|
{ icon: Activity, label: "Audit", path: "/admin/audit" },
|
|
{ icon: Shield, label: "Security", path: "/admin/security" },
|
|
{ icon: BarChart3, label: "Analytics", path: "/admin/analytics" },
|
|
{ icon: Heart, label: "System Health", path: "/admin/health" },
|
|
]
|
|
|
|
export function AppShell() {
|
|
const location = useLocation()
|
|
const navigate = useNavigate()
|
|
const [user, setUser] = useState<User | null>(null)
|
|
const [searchQuery, setSearchQuery] = useState("")
|
|
|
|
useEffect(() => {
|
|
const userStr = localStorage.getItem('user')
|
|
if (userStr) {
|
|
try {
|
|
setUser(JSON.parse(userStr))
|
|
} catch (error) {
|
|
console.error('Failed to parse user data:', error)
|
|
}
|
|
}
|
|
}, [])
|
|
|
|
const isActive = (path: string) => {
|
|
return location.pathname.startsWith(path)
|
|
}
|
|
|
|
const getPageTitle = () => {
|
|
const currentPath = location.pathname
|
|
const item = adminNavigationItems.find((item) =>
|
|
currentPath.startsWith(item.path)
|
|
)
|
|
return item?.label || "Admin Panel"
|
|
}
|
|
|
|
const handleLogout = async () => {
|
|
await authService.logout()
|
|
navigate('/login', { replace: true })
|
|
}
|
|
|
|
const handleSearch = (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (searchQuery.trim()) {
|
|
const currentPath = location.pathname
|
|
navigate(`${currentPath}?search=${encodeURIComponent(searchQuery)}`)
|
|
toast.success(`Searching for: ${searchQuery}`)
|
|
}
|
|
}
|
|
|
|
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
setSearchQuery(e.target.value)
|
|
}
|
|
|
|
const handleNotificationClick = () => {
|
|
console.log('Notification button clicked')
|
|
navigate('/notifications')
|
|
}
|
|
|
|
const handleProfileClick = () => {
|
|
navigate('/admin/settings')
|
|
}
|
|
|
|
const getUserInitials = () => {
|
|
if (user?.firstName && user?.lastName) {
|
|
return `${user.firstName[0]}${user.lastName[0]}`.toUpperCase()
|
|
}
|
|
if (user?.email) {
|
|
return user.email.substring(0, 2).toUpperCase()
|
|
}
|
|
return 'AD'
|
|
}
|
|
|
|
const getUserDisplayName = () => {
|
|
if (user?.firstName && user?.lastName) {
|
|
return `${user.firstName} ${user.lastName}`
|
|
}
|
|
return user?.email || 'Admin User'
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-screen bg-background">
|
|
{/* Sidebar */}
|
|
<aside className="w-64 bg-[#F8F8F8] flex flex-col border-r">
|
|
{/* Logo */}
|
|
<div className="p-6 flex items-center gap-2">
|
|
<div className="w-10 h-10 bg-primary rounded flex items-center justify-center">
|
|
<span className="text-primary-foreground font-bold text-lg">A</span>
|
|
</div>
|
|
<span className="text-foreground font-semibold text-lg">Admin Panel</span>
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<nav className="flex-1 px-4 py-2 space-y-1 overflow-y-auto">
|
|
{adminNavigationItems.map((item) => {
|
|
const Icon = item.icon
|
|
return (
|
|
<Link
|
|
key={item.path}
|
|
to={item.path}
|
|
className={cn(
|
|
"flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors",
|
|
isActive(item.path)
|
|
? "bg-primary text-primary-foreground"
|
|
: "text-foreground/70 hover:bg-accent hover:text-foreground"
|
|
)}
|
|
>
|
|
<Icon className="w-5 h-5" />
|
|
{item.label}
|
|
</Link>
|
|
)
|
|
})}
|
|
</nav>
|
|
|
|
{/* User Section */}
|
|
<div className="p-4 border-t">
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<Avatar>
|
|
<AvatarFallback>{getUserInitials()}</AvatarFallback>
|
|
</Avatar>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium truncate">{getUserDisplayName()}</p>
|
|
<p className="text-xs text-muted-foreground truncate">{user?.email || 'admin@example.com'}</p>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="w-full"
|
|
onClick={handleLogout}
|
|
>
|
|
<LogOut className="w-4 h-4 mr-2" />
|
|
Logout
|
|
</Button>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Main Content */}
|
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
{/* Top Header */}
|
|
<header className="h-16 border-b bg-background flex items-center justify-between px-6">
|
|
<h1 className="text-2xl font-bold">{getPageTitle()}</h1>
|
|
<div className="flex items-center gap-4">
|
|
<form onSubmit={handleSearch} className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="Quick Search..."
|
|
className="pl-10 w-64"
|
|
value={searchQuery}
|
|
onChange={handleSearchChange}
|
|
/>
|
|
</form>
|
|
<Button variant="ghost" size="icon" className="relative" onClick={handleNotificationClick}>
|
|
<Bell className="w-5 h-5" />
|
|
<span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full pointer-events-none" />
|
|
</Button>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon" className="rounded-full">
|
|
<Avatar>
|
|
<AvatarFallback>{getUserInitials()}</AvatarFallback>
|
|
</Avatar>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-56">
|
|
<DropdownMenuLabel>
|
|
<div className="flex flex-col space-y-1">
|
|
<p className="text-sm font-medium">{getUserDisplayName()}</p>
|
|
<p className="text-xs text-muted-foreground">{user?.email}</p>
|
|
</div>
|
|
</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={handleProfileClick}>
|
|
<Settings className="w-4 h-4 mr-2" />
|
|
Profile Settings
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => navigate('/notifications')}>
|
|
<Bell className="w-4 h-4 mr-2" />
|
|
Notifications
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={handleLogout}>
|
|
<LogOut className="w-4 h-4 mr-2" />
|
|
Logout
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Page Content */}
|
|
<main className="flex-1 overflow-auto bg-background p-6">
|
|
<Outlet />
|
|
</main>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|