Skip to content

Commit fafc4d6

Browse files
Merge pull request #351 from CrewForm/refactor/admin-subnav
refactor(nav): move admin tabs to sidebar sub-navigation
2 parents 98f0912 + f9b7882 commit fafc4d6

4 files changed

Lines changed: 114 additions & 56 deletions

File tree

src/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export function App() {
8282

8383
{/* Admin — super admin only */}
8484
<Route path="/admin" element={<AdminGuard><AdminPanel /></AdminGuard>} />
85+
<Route path="/admin/:tab" element={<AdminGuard><AdminPanel /></AdminGuard>} />
8586
</Route>
8687

8788
{/* 404 catch-all */}

src/components/layout/TopBar.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ const pageTitles: Record<string, string> = {
3333
'/settings/chat-widget': 'Chat Widget',
3434
'/settings/license': 'License',
3535
'/settings/profile': 'Profile',
36+
'/admin': 'Overview',
37+
'/admin/workspaces': 'Workspaces',
38+
'/admin/abuse': 'Abuse',
39+
'/admin/activity': 'Activity',
40+
'/admin/beta-users': 'Beta Users',
41+
'/admin/licenses': 'Licenses',
42+
'/admin/marketplace': 'Marketplace',
43+
'/admin/review-queue': 'Review Queue',
3644
}
3745

3846
export function TopBar({ onMenuToggle, isMobile }: TopBarProps) {

src/layouts/RootLayout.tsx

Lines changed: 90 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ import {
2929
Link2,
3030
MessageCircle,
3131
User,
32+
Activity,
33+
AlertTriangle,
34+
PackageOpen,
3235
} from 'lucide-react'
3336
import { cn } from '@/lib/utils'
3437
import { useAuth } from '@/hooks/useAuth'
@@ -85,6 +88,18 @@ const settingsSubNav: {
8588
},
8689
]
8790

91+
/** Admin sub-navigation items */
92+
const adminSubNav: { to: string; icon: typeof BarChart3; label: string }[] = [
93+
{ to: '/admin', icon: BarChart3, label: 'Overview' },
94+
{ to: '/admin/workspaces', icon: Building2, label: 'Workspaces' },
95+
{ to: '/admin/abuse', icon: AlertTriangle, label: 'Abuse' },
96+
{ to: '/admin/activity', icon: Activity, label: 'Activity' },
97+
{ to: '/admin/beta-users', icon: Users, label: 'Beta Users' },
98+
{ to: '/admin/licenses', icon: ShieldCheck, label: 'Licenses' },
99+
{ to: '/admin/marketplace', icon: Store, label: 'Marketplace' },
100+
{ to: '/admin/review-queue', icon: PackageOpen, label: 'Review Queue' },
101+
]
102+
88103
export function RootLayout() {
89104
const { user, signOut } = useAuth()
90105
const { isSuperAdmin } = useSuperAdmin()
@@ -96,14 +111,15 @@ export function RootLayout() {
96111
const [sidebarOpen, setSidebarOpen] = useState(false)
97112

98113
const isOnSettings = location.pathname === '/settings' || location.pathname.startsWith('/settings/')
114+
const isOnAdmin = location.pathname === '/admin' || location.pathname.startsWith('/admin/')
99115
const [settingsExpanded, setSettingsExpanded] = useState(isOnSettings)
116+
const [adminExpanded, setAdminExpanded] = useState(isOnAdmin)
100117

101-
// Auto-expand settings when navigating to a settings page
118+
// Auto-expand collapsible sections when navigating to them
102119
useEffect(() => {
103-
if (isOnSettings) {
104-
setSettingsExpanded(true)
105-
}
106-
}, [isOnSettings])
120+
if (isOnSettings) setSettingsExpanded(true)
121+
if (isOnAdmin) setAdminExpanded(true)
122+
}, [isOnSettings, isOnAdmin])
107123

108124
const metadata = (user?.user_metadata ?? {}) as Record<string, unknown>
109125
const displayName = (typeof metadata.full_name === 'string' ? metadata.full_name : null)
@@ -164,11 +180,6 @@ export function RootLayout() {
164180
// Collapsed mode: icon-only on tablet
165181
const collapsed = isTablet
166182

167-
// Build full nav items including admin if super admin
168-
const allNavItems = isSuperAdmin
169-
? [...navItems, { to: '/admin' as const, icon: Shield, label: 'Admin' }]
170-
: navItems
171-
172183
return (
173184
<div className="flex h-screen overflow-hidden bg-gray-950">
174185
{/* Backdrop overlay for mobile sidebar */}
@@ -218,7 +229,7 @@ export function RootLayout() {
218229

219230
{/* Navigation */}
220231
<nav className={cn('flex-1 overflow-y-auto py-4', collapsed ? 'px-2' : 'px-3')}>
221-
{allNavItems.map(({ to, icon: Icon, label }) => (
232+
{navItems.map(({ to, icon: Icon, label }) => (
222233
<NavLink
223234
key={to}
224235
to={to}
@@ -318,6 +329,74 @@ export function RootLayout() {
318329
</div>
319330
</div>
320331
)}
332+
333+
{/* ─── Admin collapsible group (super admin only) ─── */}
334+
{isSuperAdmin && (
335+
collapsed ? (
336+
<NavLink
337+
to="/admin"
338+
className={cn(
339+
'flex items-center justify-center rounded-lg p-2.5 text-sm font-medium transition-colors',
340+
isOnAdmin
341+
? 'bg-gray-800 text-gray-100'
342+
: 'text-gray-400 hover:bg-gray-800/50 hover:text-gray-200',
343+
)}
344+
title="Admin"
345+
>
346+
<Shield className="h-5 w-5 shrink-0" />
347+
</NavLink>
348+
) : (
349+
<div className="mt-1">
350+
<button
351+
type="button"
352+
onClick={() => setAdminExpanded(prev => !prev)}
353+
className={cn(
354+
'flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
355+
isOnAdmin
356+
? 'bg-gray-800 text-gray-100'
357+
: 'text-gray-400 hover:bg-gray-800/50 hover:text-gray-200',
358+
)}
359+
>
360+
<Shield className="h-5 w-5 shrink-0" />
361+
<span className="flex-1 text-left">Admin</span>
362+
<ChevronDown
363+
className={cn(
364+
'h-4 w-4 shrink-0 transition-transform duration-200',
365+
adminExpanded && 'rotate-180',
366+
)}
367+
/>
368+
</button>
369+
370+
<div
371+
className={cn(
372+
'overflow-hidden transition-all duration-200',
373+
adminExpanded ? 'max-h-[400px] opacity-100' : 'max-h-0 opacity-0',
374+
)}
375+
>
376+
<div className="mt-1 space-y-0.5 pl-2">
377+
{adminSubNav.map(({ to, icon: Icon, label }) => (
378+
<NavLink
379+
key={to}
380+
to={to}
381+
end
382+
className={({ isActive }) =>
383+
cn(
384+
'flex items-center gap-2.5 rounded-md px-3 py-1.5 text-[13px] font-medium transition-colors',
385+
isActive
386+
? 'bg-gray-800/80 text-gray-200'
387+
: 'text-gray-500 hover:bg-gray-800/40 hover:text-gray-300',
388+
)
389+
}
390+
>
391+
<Icon className="h-3.5 w-3.5 shrink-0" />
392+
{label}
393+
</NavLink>
394+
))}
395+
</div>
396+
</div>
397+
</div>
398+
)
399+
)}
321400
</nav>
322401

323402
{/* User footer — hidden when collapsed */}

src/pages/AdminPanel.tsx

Lines changed: 15 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
// SPDX-License-Identifier: AGPL-3.0-or-later
22
// Copyright (C) 2026 CrewForm
33

4-
import { useState, useEffect } from 'react'
4+
import { useState, useEffect, useMemo } from 'react'
5+
import { useParams } from 'react-router-dom'
56
import { toast } from 'sonner'
67
import {
7-
Shield, BarChart3, Building2, Search,
8-
Loader2, Users, Bot, ListTodo, PackageOpen, Store, XCircle, ShieldCheck,
9-
Activity, Coins, Zap, UserCheck, Ban, Trash2, ShieldOff, AlertTriangle, KeyRound,
8+
Building2, Search,
9+
Loader2, Users, Bot, ListTodo, XCircle,
10+
Coins, Zap, UserCheck, Ban, Trash2, ShieldOff, AlertTriangle, KeyRound,
1011
TrendingUp, ArrowUpRight,
1112
} from 'lucide-react'
1213
import {
@@ -22,6 +23,10 @@ import { cn } from '@/lib/utils'
2223

2324
type AdminTab = 'overview' | 'workspaces' | 'abuse' | 'beta-users' | 'activity' | 'review-queue' | 'marketplace' | 'licenses'
2425

26+
const validTabs = new Set<string>([
27+
'overview', 'workspaces', 'abuse', 'beta-users', 'activity', 'review-queue', 'marketplace', 'licenses',
28+
])
29+
2530
const PLAN_COLORS: Record<string, string> = {
2631
free: 'text-gray-400 bg-gray-500/10',
2732
pro: 'text-blue-400 bg-blue-500/10',
@@ -33,50 +38,15 @@ const PLAN_COLORS: Record<string, string> = {
3338
* Master Admin Panel — platform overview, workspace management.
3439
*/
3540
export function AdminPanel() {
36-
const [activeTab, setActiveTab] = useState<AdminTab>('overview')
41+
const { tab: tabParam } = useParams<{ tab?: string }>()
42+
43+
const activeTab = useMemo<AdminTab>(() => {
44+
if (tabParam && validTabs.has(tabParam)) return tabParam as AdminTab
45+
return 'overview'
46+
}, [tabParam])
3747

3848
return (
3949
<div className="min-h-screen bg-surface-primary p-6 lg:p-8">
40-
{/* Header */}
41-
<div className="mb-6 flex items-center gap-3">
42-
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-red-500/10">
43-
<Shield className="h-5 w-5 text-red-400" />
44-
</div>
45-
<div>
46-
<h1 className="text-2xl font-semibold text-gray-100">Admin Panel</h1>
47-
<p className="text-xs text-gray-500">Super administrator dashboard</p>
48-
</div>
49-
</div>
50-
51-
{/* Tabs */}
52-
<div className="mb-6 flex overflow-x-auto border-b border-border">
53-
{([
54-
{ key: 'overview' as const, label: 'Overview', icon: BarChart3 },
55-
{ key: 'workspaces' as const, label: 'Workspaces', icon: Building2 },
56-
{ key: 'abuse' as const, label: 'Abuse', icon: AlertTriangle },
57-
{ key: 'activity' as const, label: 'Activity', icon: Activity },
58-
{ key: 'beta-users' as const, label: 'Beta Users', icon: Users },
59-
{ key: 'licenses' as const, label: 'Licenses', icon: ShieldCheck },
60-
{ key: 'marketplace' as const, label: 'Marketplace', icon: Store },
61-
{ key: 'review-queue' as const, label: 'Review Queue', icon: PackageOpen },
62-
] as const).map(({ key, label, icon: Icon }) => (
63-
<button
64-
key={key}
65-
type="button"
66-
onClick={() => setActiveTab(key)}
67-
className={cn(
68-
'flex items-center gap-2 whitespace-nowrap border-b-2 px-4 py-2.5 text-sm font-medium transition-colors',
69-
activeTab === key
70-
? 'border-red-400 text-gray-200'
71-
: 'border-transparent text-gray-500 hover:text-gray-300',
72-
)}
73-
>
74-
<Icon className="h-4 w-4" />
75-
{label}
76-
</button>
77-
))}
78-
</div>
79-
8050
{/* Tab content */}
8151
{activeTab === 'overview' && <OverviewTab />}
8252
{activeTab === 'workspaces' && <WorkspacesTab />}

0 commit comments

Comments
 (0)