CognxSafeTrack commited on
Commit
2c400a5
·
1 Parent(s): b168be2

refactor: extract shell and sidebar to MainLayout

Browse files

- Improved component modularity by separating layout from routing
- Cleaned up App.tsx (removed unused imports and complex UI logic)
- Standardized navigation and organization selection in a dedicated Layout

apps/admin/src/App.tsx CHANGED
@@ -1,6 +1,5 @@
1
  import React from 'react';
2
- import { BrowserRouter as Router, Routes, Route, Link, Navigate } from 'react-router-dom';
3
- import { Users, BookOpen, Lightbulb, BarChart2, Mic, Activity, Building2, TrendingUp } from 'lucide-react';
4
 
5
  import { AuthProvider, useAuth } from './lib/auth';
6
  import { TenantProvider } from './lib/tenant';
@@ -24,6 +23,8 @@ import CrmConversationalDashboard from './pages/CrmConversationalDashboard'; //
24
  import { useTenant } from './lib/tenant';
25
  import { api } from './lib/api';
26
 
 
 
27
  function ProtectedRoute({ children }: { children: React.ReactNode }) {
28
  const { token } = useAuth();
29
  if (!token) return <Navigate to="/login" replace />;
@@ -31,8 +32,8 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) {
31
  }
32
 
33
  function AppShell() {
34
- const { logout, token, user } = useAuth();
35
- const { selectedOrgId, setSelectedOrgId, currentOrg } = useTenant();
36
  const [orgs, setOrgs] = React.useState<any[]>([]);
37
 
38
  const isSuperAdmin = user?.role === 'SUPER_ADMIN' || user?.role === 'ADMIN';
@@ -53,114 +54,41 @@ function AppShell() {
53
 
54
  const isCrmMode = currentOrg?.useCase === 'CRM_WHATSAPP';
55
 
56
- const allNavItems = [
57
- { to: '/', label: 'Dashboard', icon: <BarChart2 className="w-4 h-4" /> },
58
- { to: '/analytics', label: 'Statistiques', icon: <TrendingUp className="w-4 h-4 text-amber-500" /> },
59
- { to: '/contacts', label: 'Clients', icon: <Users className="w-4 h-4 text-blue-400" />, show: isCrmMode },
60
- { to: '/content', label: 'Parcours', icon: <BookOpen className="w-4 h-4" />, show: !isCrmMode },
61
- { to: '/live-feed', label: 'Modération', icon: <Mic className="w-4 h-4 text-emerald-500" />, show: !isCrmMode },
62
- { to: '/clients', label: 'Clients B2B', icon: <Building2 className="w-4 h-4 text-indigo-400" />, superOnly: true, show: !isCrmMode },
63
- { to: '/training', label: 'Training Lab', icon: <Activity className="w-4 h-4 text-purple-400" />, superOnly: true, show: !isCrmMode },
64
- { to: '/users', label: 'Utilisateurs', icon: <Users className="w-4 h-4" />, show: !isCrmMode },
65
- { to: '/settings', label: 'Paramètres', icon: <Lightbulb className="w-4 h-4" /> },
66
- ];
67
-
68
- const navItems = allNavItems.filter(item => {
69
- if (item.superOnly && !isSuperAdmin) return false;
70
- if (item.show === false) return false;
71
- return true;
72
- });
73
-
74
  return (
75
- <div className="min-h-screen bg-gray-50 flex">
76
- <aside className="w-64 bg-slate-900 text-white p-6 flex flex-col shrink-0">
77
- <div className="text-xl font-bold mb-8 flex items-center gap-3">
78
- {currentOrg?.brandingData?.logoUrl ? (
79
- <img src={currentOrg.brandingData.logoUrl} className="h-8 w-8 object-contain" alt="Logo" />
80
- ) : (
81
- <span className="text-2xl">🎓</span>
82
- )}
83
- <span className="truncate">{currentOrg?.name || 'EdTech Admin'}</span>
84
- </div>
85
-
86
- {isSuperAdmin && (
87
- <div className="mb-8">
88
- <label className="block text-[10px] uppercase font-bold text-slate-500 tracking-wider mb-2">Gestion Multi-Tenant</label>
89
- <select
90
- value={selectedOrgId || ''}
91
- onChange={e => setSelectedOrgId(e.target.value)}
92
- className="w-full bg-slate-800 text-slate-200 text-xs px-3 py-2.5 rounded-xl outline-none focus:ring-1 focus:ring-slate-600 appearance-none cursor-pointer"
93
- >
94
- <option value="">Sélectionner une école...</option>
95
- {orgs.map(o => (
96
- <option key={o.id} value={o.id}>{o.name}</option>
97
- ))}
98
- </select>
99
- </div>
100
- )}
101
-
102
- <nav className="space-y-1 flex-1">
103
- {navItems.map(n => (
104
- <Link
105
- key={n.to}
106
- to={n.to}
107
- className="flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium text-slate-300 hover:text-white hover:bg-white/10 transition"
108
- >
109
- {n.icon}{n.label}
110
- </Link>
111
- ))}
112
- </nav>
113
-
114
- <div className="pt-6 mt-6 border-t border-slate-800">
115
- <div className="flex items-center gap-3 px-4 mb-4">
116
- <div className="w-8 h-8 rounded-full bg-indigo-500 flex items-center justify-center font-bold text-xs">
117
- {user?.name?.[0] || 'U'}
118
- </div>
119
- <div className="flex-1 min-w-0">
120
- <p className="text-sm font-bold truncate">{user?.name}</p>
121
- <p className="text-[10px] text-slate-500 truncate capitalize">{user?.role?.toLowerCase()}</p>
122
- </div>
123
- </div>
124
- <button onClick={logout} className="w-full flex items-center gap-3 px-4 py-2 text-xs text-slate-500 hover:text-white transition">
125
- 🔓 Se déconnecter
126
- </button>
127
- </div>
128
- </aside>
129
- <main className="flex-1 overflow-auto">
130
- <Routes>
131
- <Route
132
- path="/"
133
- element={
134
- isCrmMode
135
- ? <CrmConversationalDashboard />
136
- : <DashboardPage />
137
- }
138
- />
139
- <Route path="/analytics" element={<AnalyticsPage />} />
140
- <Route
141
- path="/crm"
142
- element={
143
- isCrmMode
144
- ? <CrmConversationalDashboard />
145
- : <ConversationalDashboard />
146
- }
147
- />
148
- <Route path="/contacts" element={<ContactsPage />} />
149
- <Route path="/content" element={<TrackListPage />} />
150
- <Route path="/content/new" element={<TrackFormPage />} />
151
- <Route path="/content/:id" element={<TrackFormPage />} />
152
- <Route path="/content/:trackId/days" element={<TrackDaysPage />} />
153
- <Route path="/live-feed" element={<LiveFeed />} />
154
- <Route path="/ai-setup" element={<AIAgentSetup />} />
155
- <Route path="/clients" element={<ClientsManagementView />} />
156
- <Route path="/training" element={<TrainingLab />} />
157
- <Route path="/users" element={<UserListPage />} />
158
- <Route path="/settings" element={<SettingsPage />} />
159
- <Route path="/onboarding" element={<OnboardingWizard />} />
160
- <Route path="/reset-password" element={<div className="p-8">Page de réinitialisation (À implémenter)</div>} />
161
- </Routes>
162
- </main>
163
- </div>
164
  );
165
  }
166
 
 
1
  import React from 'react';
2
+ import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
 
3
 
4
  import { AuthProvider, useAuth } from './lib/auth';
5
  import { TenantProvider } from './lib/tenant';
 
23
  import { useTenant } from './lib/tenant';
24
  import { api } from './lib/api';
25
 
26
+ import MainLayout from './components/layouts/MainLayout';
27
+
28
  function ProtectedRoute({ children }: { children: React.ReactNode }) {
29
  const { token } = useAuth();
30
  if (!token) return <Navigate to="/login" replace />;
 
32
  }
33
 
34
  function AppShell() {
35
+ const { token, user } = useAuth();
36
+ const { setSelectedOrgId, currentOrg } = useTenant();
37
  const [orgs, setOrgs] = React.useState<any[]>([]);
38
 
39
  const isSuperAdmin = user?.role === 'SUPER_ADMIN' || user?.role === 'ADMIN';
 
54
 
55
  const isCrmMode = currentOrg?.useCase === 'CRM_WHATSAPP';
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  return (
58
+ <MainLayout isSuperAdmin={isSuperAdmin} orgs={orgs}>
59
+ <Routes>
60
+ <Route
61
+ path="/"
62
+ element={
63
+ isCrmMode
64
+ ? <CrmConversationalDashboard />
65
+ : <DashboardPage />
66
+ }
67
+ />
68
+ <Route path="/analytics" element={<AnalyticsPage />} />
69
+ <Route
70
+ path="/crm"
71
+ element={
72
+ isCrmMode
73
+ ? <CrmConversationalDashboard />
74
+ : <ConversationalDashboard />
75
+ }
76
+ />
77
+ <Route path="/contacts" element={<ContactsPage />} />
78
+ <Route path="/content" element={<TrackListPage />} />
79
+ <Route path="/content/new" element={<TrackFormPage />} />
80
+ <Route path="/content/:id" element={<TrackFormPage />} />
81
+ <Route path="/content/:trackId/days" element={<TrackDaysPage />} />
82
+ <Route path="/live-feed" element={<LiveFeed />} />
83
+ <Route path="/ai-setup" element={<AIAgentSetup />} />
84
+ <Route path="/clients" element={<ClientsManagementView />} />
85
+ <Route path="/training" element={<TrainingLab />} />
86
+ <Route path="/users" element={<UserListPage />} />
87
+ <Route path="/settings" element={<SettingsPage />} />
88
+ <Route path="/onboarding" element={<OnboardingWizard />} />
89
+ <Route path="/reset-password" element={<div className="p-8">Page de réinitialisation (À implémenter)</div>} />
90
+ </Routes>
91
+ </MainLayout>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  );
93
  }
94
 
apps/admin/src/components/layouts/MainLayout.tsx ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Link } from 'react-router-dom';
3
+ import { useAuth } from '../../lib/auth';
4
+ import { useTenant } from '../../lib/tenant';
5
+ import { BarChart2, TrendingUp, Users, BookOpen, Mic, Building2, Activity, Lightbulb } from 'lucide-react';
6
+
7
+ interface MainLayoutProps {
8
+ children: React.ReactNode;
9
+ isSuperAdmin: boolean;
10
+ orgs: any[];
11
+ }
12
+
13
+ export default function MainLayout({ children, isSuperAdmin, orgs }: MainLayoutProps) {
14
+ const { logout, user } = useAuth();
15
+ const { selectedOrgId, setSelectedOrgId, currentOrg } = useTenant();
16
+
17
+ const isCrmMode = currentOrg?.useCase === 'CRM_WHATSAPP';
18
+
19
+ const allNavItems = [
20
+ { to: '/', label: 'Dashboard', icon: <BarChart2 className="w-4 h-4" /> },
21
+ { to: '/analytics', label: 'Statistiques', icon: <TrendingUp className="w-4 h-4 text-amber-500" /> },
22
+ { to: '/contacts', label: 'Clients', icon: <Users className="w-4 h-4 text-blue-400" />, show: isCrmMode },
23
+ { to: '/content', label: 'Parcours', icon: <BookOpen className="w-4 h-4" />, show: !isCrmMode },
24
+ { to: '/live-feed', label: 'Modération', icon: <Mic className="w-4 h-4 text-emerald-500" />, show: !isCrmMode },
25
+ { to: '/clients', label: 'Clients B2B', icon: <Building2 className="w-4 h-4 text-indigo-400" />, superOnly: true, show: !isCrmMode },
26
+ { to: '/training', label: 'Training Lab', icon: <Activity className="w-4 h-4 text-purple-400" />, superOnly: true, show: !isCrmMode },
27
+ { to: '/users', label: 'Utilisateurs', icon: <Users className="w-4 h-4" />, show: !isCrmMode },
28
+ { to: '/settings', label: 'Paramètres', icon: <Lightbulb className="w-4 h-4" /> },
29
+ ];
30
+
31
+ const navItems = allNavItems.filter(item => {
32
+ if (item.superOnly && !isSuperAdmin) return false;
33
+ if (item.show === false) return false;
34
+ return true;
35
+ });
36
+
37
+ return (
38
+ <div className="min-h-screen bg-gray-50 flex">
39
+ <aside className="w-64 bg-slate-900 text-white p-6 flex flex-col shrink-0">
40
+ <div className="text-xl font-bold mb-8 flex items-center gap-3">
41
+ {currentOrg?.brandingData?.logoUrl ? (
42
+ <img src={currentOrg.brandingData.logoUrl} className="h-8 w-8 object-contain" alt="Logo" />
43
+ ) : (
44
+ <span className="text-2xl">🎓</span>
45
+ )}
46
+ <span className="truncate">{currentOrg?.name || 'EdTech Admin'}</span>
47
+ </div>
48
+
49
+ {isSuperAdmin && (
50
+ <div className="mb-8">
51
+ <label className="block text-[10px] uppercase font-bold text-slate-500 tracking-wider mb-2">Gestion Multi-Tenant</label>
52
+ <select
53
+ value={selectedOrgId || ''}
54
+ onChange={e => setSelectedOrgId(e.target.value)}
55
+ className="w-full bg-slate-800 text-slate-200 text-xs px-3 py-2.5 rounded-xl outline-none focus:ring-1 focus:ring-slate-600 appearance-none cursor-pointer"
56
+ >
57
+ <option value="">Sélectionner une école...</option>
58
+ {orgs.map(o => (
59
+ <option key={o.id} value={o.id}>{o.name}</option>
60
+ ))}
61
+ </select>
62
+ </div>
63
+ )}
64
+
65
+ <nav className="space-y-1 flex-1">
66
+ {navItems.map(n => (
67
+ <Link
68
+ key={n.to}
69
+ to={n.to}
70
+ className="flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium text-slate-300 hover:text-white hover:bg-white/10 transition"
71
+ >
72
+ {n.icon}{n.label}
73
+ </Link>
74
+ ))}
75
+ </nav>
76
+
77
+ <div className="pt-6 mt-6 border-t border-slate-800">
78
+ <div className="flex items-center gap-3 px-4 mb-4">
79
+ <div className="w-8 h-8 rounded-full bg-indigo-500 flex items-center justify-center font-bold text-xs">
80
+ {user?.name?.[0] || 'U'}
81
+ </div>
82
+ <div className="flex-1 min-w-0">
83
+ <p className="text-sm font-bold truncate">{user?.name}</p>
84
+ <p className="text-[10px] text-slate-500 truncate capitalize">{user?.role?.toLowerCase()}</p>
85
+ </div>
86
+ </div>
87
+ <button onClick={logout} className="w-full flex items-center gap-3 px-4 py-2 text-xs text-slate-500 hover:text-white transition">
88
+ 🔓 Se déconnecter
89
+ </button>
90
+ </div>
91
+ </aside>
92
+ <main className="flex-1 overflow-auto">
93
+ {children}
94
+ </main>
95
+ </div>
96
+ );
97
+ }