Files
radadmin/frontend/src/App.tsx
T
shihaam 2957347583
build-and-push / build (push) Failing after 54s
new ui + search clients
2026-08-01 13:31:46 +05:00

214 lines
6.3 KiB
TypeScript

import { useEffect, useState } from 'react'
import { NavLink, Navigate, Route, Routes, useLocation } from 'react-router-dom'
import {
KeyRound,
LogOut,
Menu,
Moon,
Router,
ScrollText,
Sun,
UserCog,
Users as UsersIcon,
Wifi,
X,
} from 'lucide-react'
import { useAuth } from '@/auth/auth'
import { useTheme } from '@/lib/theme'
import { Login } from '@/pages/Login'
import { Clients } from '@/pages/Clients'
import { Devices } from '@/pages/Devices'
import { Vlans } from '@/pages/Vlans'
import { Users } from '@/pages/Users'
import { ApiKeys } from '@/pages/ApiKeys'
import { Logs } from '@/pages/Logs'
import { ChangePassword } from '@/pages/ChangePassword'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
function ThemeToggle() {
const { theme, toggle } = useTheme()
return (
<Button
variant="ghost"
size="icon"
onClick={toggle}
className="text-muted-foreground"
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
aria-label="Toggle theme"
>
{theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</Button>
)
}
type NavItem = { to: string; label: string; icon: typeof Wifi }
const linkClass = ({ isActive }: { isActive: boolean }) =>
cn(
'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
isActive ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-accent',
)
function SidebarContent({
username,
isAdmin,
onNavigate,
}: {
username: string
isAdmin: boolean
onNavigate?: () => void
}) {
const { logout } = useAuth()
const items: NavItem[] = [
{ to: '/clients', label: 'Clients', icon: UsersIcon },
{ to: '/devices', label: 'Devices', icon: Router },
{ to: '/vlans', label: 'VLANs', icon: Wifi },
]
const adminItems: NavItem[] = [
{ to: '/users', label: 'Users', icon: UserCog },
{ to: '/apikeys', label: 'API Keys', icon: KeyRound },
{ to: '/logs', label: 'Activity Log', icon: ScrollText },
]
const renderItem = ({ to, label, icon: Icon }: NavItem) => (
<NavLink key={to} to={to} className={linkClass} onClick={onNavigate}>
<Icon className="h-4 w-4 shrink-0" /> {label}
</NavLink>
)
return (
<div className="flex h-full flex-col">
<div className="flex h-14 items-center gap-2 px-4 font-semibold">
<Wifi className="h-5 w-5 text-primary" />
RADIUS Admin
</div>
<nav className="flex flex-1 flex-col gap-1 overflow-y-auto px-3 py-2">
{items.map(renderItem)}
{isAdmin && (
<>
<div className="mt-4 px-3 pb-1 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Admin
</div>
{adminItems.map(renderItem)}
</>
)}
</nav>
<div className="border-t p-3">
<NavLink to="/account" className={linkClass} onClick={onNavigate} title="Change password">
<UserCog className="h-4 w-4 shrink-0" />
<span className="truncate">{username}</span>
</NavLink>
<Button
variant="ghost"
className="mt-1 w-full justify-start gap-3 px-3 text-muted-foreground"
onClick={logout}
>
<LogOut className="h-4 w-4 shrink-0" /> Sign out
</Button>
</div>
</div>
)
}
function Shell({
username,
isAdmin,
children,
}: {
username: string
isAdmin: boolean
children: React.ReactNode
}) {
const [open, setOpen] = useState(false)
const location = useLocation()
// Close the mobile drawer whenever the route changes.
useEffect(() => {
setOpen(false)
}, [location.pathname])
return (
<div className="min-h-screen bg-muted/20">
{/* Desktop sidebar */}
<aside className="fixed inset-y-0 left-0 z-30 hidden w-60 border-r bg-background md:block">
<SidebarContent username={username} isAdmin={isAdmin} />
</aside>
{/* Mobile drawer */}
{open && (
<div className="fixed inset-0 z-40 md:hidden">
<div
className="absolute inset-0 bg-black/50"
onClick={() => setOpen(false)}
aria-hidden="true"
/>
<aside className="absolute inset-y-0 left-0 w-64 border-r bg-background shadow-lg">
<SidebarContent
username={username}
isAdmin={isAdmin}
onNavigate={() => setOpen(false)}
/>
</aside>
</div>
)}
{/* Mobile top bar */}
<header className="sticky top-0 z-20 flex h-14 items-center gap-2 border-b bg-background px-4 md:hidden">
<Button
variant="ghost"
size="icon"
onClick={() => setOpen((v) => !v)}
aria-label={open ? 'Close menu' : 'Open menu'}
>
{open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</Button>
<div className="flex items-center gap-2 font-semibold">
<Wifi className="h-5 w-5 text-primary" />
RADIUS Admin
</div>
<div className="ml-auto">
<ThemeToggle />
</div>
</header>
<div className="md:pl-60">
{/* Desktop top-right theme toggle */}
<div className="hidden items-center justify-end px-6 pt-4 md:flex">
<ThemeToggle />
</div>
<main className="mx-auto max-w-6xl px-4 py-6">{children}</main>
</div>
</div>
)
}
export default function App() {
const { user } = useAuth()
if (!user) return <Login />
if (user.must_change_password) return <ChangePassword forced />
const admin = user.is_admin
return (
<Shell username={user.username} isAdmin={admin}>
<Routes>
<Route path="/clients" element={<Clients />} />
<Route path="/devices" element={<Devices />} />
<Route path="/vlans" element={<Vlans />} />
<Route path="/account" element={<ChangePassword />} />
<Route path="/users" element={admin ? <Users /> : <Navigate to="/clients" replace />} />
<Route
path="/apikeys"
element={admin ? <ApiKeys /> : <Navigate to="/clients" replace />}
/>
<Route path="/logs" element={admin ? <Logs /> : <Navigate to="/clients" replace />} />
<Route path="*" element={<Navigate to="/clients" replace />} />
</Routes>
</Shell>
)
}