session-wrap-dashboard 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.env.example +6 -0
  2. package/coverage/base.css +224 -0
  3. package/coverage/block-navigation.js +87 -0
  4. package/coverage/coverage-final.json +6 -0
  5. package/coverage/favicon.png +0 -0
  6. package/coverage/index.html +146 -0
  7. package/coverage/prettify.css +1 -0
  8. package/coverage/prettify.js +2 -0
  9. package/coverage/sort-arrow-sprite.png +0 -0
  10. package/coverage/sorter.js +210 -0
  11. package/coverage/src/auth.ts.html +307 -0
  12. package/coverage/src/components/Header.tsx.html +262 -0
  13. package/coverage/src/components/Sidebar.tsx.html +262 -0
  14. package/coverage/src/components/index.html +131 -0
  15. package/coverage/src/hooks/index.html +131 -0
  16. package/coverage/src/hooks/useAuth.ts.html +292 -0
  17. package/coverage/src/hooks/useWorkspace.ts.html +475 -0
  18. package/coverage/src/index.html +116 -0
  19. package/index.html +13 -0
  20. package/package.json +49 -0
  21. package/playwright.config.ts +38 -0
  22. package/postcss.config.js +6 -0
  23. package/src/App.tsx +67 -0
  24. package/src/api.ts +130 -0
  25. package/src/auth.ts +74 -0
  26. package/src/components/AgentLeaderboard.tsx +84 -0
  27. package/src/components/AnalyticsDashboard.tsx +223 -0
  28. package/src/components/Header.tsx +62 -0
  29. package/src/components/IntegrationManager.tsx +292 -0
  30. package/src/components/RoleManager.tsx +230 -0
  31. package/src/components/Sidebar.tsx +62 -0
  32. package/src/components/TrendChart.tsx +92 -0
  33. package/src/components/WorkspaceSelector.tsx +157 -0
  34. package/src/components/__tests__/Header.test.tsx +64 -0
  35. package/src/components/__tests__/Sidebar.test.tsx +39 -0
  36. package/src/components/index.ts +8 -0
  37. package/src/hooks/__tests__/useAuth.test.ts +88 -0
  38. package/src/hooks/__tests__/useWorkspace.test.ts +101 -0
  39. package/src/hooks/index.ts +4 -0
  40. package/src/hooks/useAnalytics.ts +76 -0
  41. package/src/hooks/useApi.ts +62 -0
  42. package/src/hooks/useAuth.ts +69 -0
  43. package/src/hooks/useWorkspace.ts +130 -0
  44. package/src/index.css +57 -0
  45. package/src/main.tsx +13 -0
  46. package/src/pages/DashboardPage.tsx +13 -0
  47. package/src/pages/HomePage.tsx +156 -0
  48. package/src/pages/IntegrationsPage.tsx +9 -0
  49. package/src/pages/RolesPage.tsx +9 -0
  50. package/src/pages/SettingsPage.tsx +118 -0
  51. package/src/pages/WorkspacesPage.tsx +9 -0
  52. package/src/pages/index.ts +6 -0
  53. package/src/test/setup.ts +31 -0
  54. package/src/test/utils.tsx +15 -0
  55. package/src/types.ts +132 -0
  56. package/tailwind.config.js +11 -0
  57. package/tests/e2e/auth.spec.ts +42 -0
  58. package/tests/e2e/dashboard.spec.ts +52 -0
  59. package/tests/e2e/integrations.spec.ts +91 -0
  60. package/tests/e2e/workspaces.spec.ts +78 -0
  61. package/tsconfig.json +26 -0
  62. package/tsconfig.node.json +10 -0
  63. package/vite.config.ts +26 -0
  64. package/vitest.config.ts +31 -0
@@ -0,0 +1,62 @@
1
+ import { memo } from 'react'
2
+ import { useAuth, useWorkspace } from '../hooks'
3
+ import { LogOut, User } from 'lucide-react'
4
+
5
+ interface HeaderProps {
6
+ title?: string
7
+ }
8
+
9
+ const HeaderComponent = ({ title = 'Dashboard' }: HeaderProps) => {
10
+ const { user, logout } = useAuth()
11
+ const { currentWorkspace, workspaces, setCurrentWorkspace } = useWorkspace()
12
+
13
+ return (
14
+ <header className="bg-white border-b border-slate-200 px-6 py-4 shadow-sm">
15
+ <div className="flex justify-between items-center">
16
+ <div className="flex items-center gap-4">
17
+ <h2 className="text-lg font-semibold text-slate-900">{title}</h2>
18
+
19
+ {/* Workspace Selector */}
20
+ <select
21
+ value={currentWorkspace?.id || ''}
22
+ onChange={(e) => {
23
+ const workspace = workspaces.find(w => w.id === e.target.value)
24
+ setCurrentWorkspace(workspace || null)
25
+ }}
26
+ className="input text-sm"
27
+ >
28
+ <option value="" disabled>
29
+ Select workspace...
30
+ </option>
31
+ {workspaces.map((ws) => (
32
+ <option key={ws.id} value={ws.id}>
33
+ {ws.name}
34
+ </option>
35
+ ))}
36
+ </select>
37
+ </div>
38
+
39
+ {/* User Menu */}
40
+ <div className="flex items-center gap-4">
41
+ <div className="flex items-center gap-2 text-sm text-slate-600">
42
+ <User size={16} />
43
+ <span>{user?.github_login}</span>
44
+ </div>
45
+
46
+ <button
47
+ onClick={() => {
48
+ logout()
49
+ window.location.href = '/login'
50
+ }}
51
+ className="btn btn-secondary btn-sm flex items-center gap-2"
52
+ >
53
+ <LogOut size={16} />
54
+ Logout
55
+ </button>
56
+ </div>
57
+ </div>
58
+ </header>
59
+ )
60
+ }
61
+
62
+ export const Header = memo(HeaderComponent)
@@ -0,0 +1,292 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { useWorkspace } from '../hooks'
3
+ import { integrationAPI } from '../api'
4
+ import { Trash2, Plus, Check, AlertCircle } from 'lucide-react'
5
+
6
+ interface Integration {
7
+ id: string
8
+ service_name: 'slack' | 'github' | 'jira'
9
+ is_active: boolean
10
+ created_at: string
11
+ updated_at: string
12
+ }
13
+
14
+ const ServiceConfig = {
15
+ slack: {
16
+ name: 'Slack',
17
+ icon: '💬',
18
+ description: 'Send notifications to Slack channels',
19
+ fields: [
20
+ { name: 'webhook_url', label: 'Webhook URL', type: 'password' },
21
+ { name: 'channel_id', label: 'Channel ID', type: 'text' }
22
+ ]
23
+ },
24
+ github: {
25
+ name: 'GitHub',
26
+ icon: '🐙',
27
+ description: 'Sync tasks with GitHub Issues',
28
+ fields: [
29
+ { name: 'api_token', label: 'API Token', type: 'password' },
30
+ { name: 'owner', label: 'Repository Owner', type: 'text' },
31
+ { name: 'repo', label: 'Repository Name', type: 'text' }
32
+ ]
33
+ },
34
+ jira: {
35
+ name: 'Jira',
36
+ icon: '📊',
37
+ description: 'Integrate with Jira projects',
38
+ fields: [
39
+ { name: 'api_token', label: 'API Token', type: 'password' },
40
+ { name: 'host', label: 'Jira Host', type: 'text' },
41
+ { name: 'email', label: 'Email', type: 'email' },
42
+ { name: 'project_key', label: 'Project Key', type: 'text' }
43
+ ]
44
+ }
45
+ }
46
+
47
+ export const IntegrationManager = () => {
48
+ const { currentWorkspace, isLoading: wsLoading } = useWorkspace()
49
+ const [integrations, setIntegrations] = useState<Integration[]>([])
50
+ const [isLoading, setIsLoading] = useState(false)
51
+ const [error, setError] = useState<string | null>(null)
52
+ const [showSetupForm, setShowSetupForm] = useState<'slack' | 'github' | 'jira' | null>(null)
53
+ const [formData, setFormData] = useState<Record<string, string>>({})
54
+ const [isSubmitting, setIsSubmitting] = useState(false)
55
+
56
+ // Load integrations
57
+ useEffect(() => {
58
+ if (!currentWorkspace) return
59
+
60
+ const loadIntegrations = async () => {
61
+ try {
62
+ setIsLoading(true)
63
+ const res = await integrationAPI.list(currentWorkspace.id)
64
+ setIntegrations(res.data.integrations || [])
65
+ } catch (err) {
66
+ console.error('Failed to load integrations:', err)
67
+ } finally {
68
+ setIsLoading(false)
69
+ }
70
+ }
71
+
72
+ loadIntegrations()
73
+ }, [currentWorkspace])
74
+
75
+ if (!currentWorkspace) {
76
+ return (
77
+ <div className="flex items-center justify-center h-64">
78
+ <p className="text-slate-600">Please select a workspace</p>
79
+ </div>
80
+ )
81
+ }
82
+
83
+ if (wsLoading || isLoading) {
84
+ return (
85
+ <div className="flex items-center justify-center h-64">
86
+ <div className="loading-spinner"></div>
87
+ </div>
88
+ )
89
+ }
90
+
91
+ const handleSetup = async (e: React.FormEvent, service: 'slack' | 'github' | 'jira') => {
92
+ e.preventDefault()
93
+
94
+ try {
95
+ setIsSubmitting(true)
96
+ setError(null)
97
+
98
+ await integrationAPI.setup(currentWorkspace.id, service, formData)
99
+
100
+ // Reload integrations
101
+ const res = await integrationAPI.list(currentWorkspace.id)
102
+ setIntegrations(res.data.integrations || [])
103
+
104
+ setShowSetupForm(null)
105
+ setFormData({})
106
+ } catch (err) {
107
+ setError(err instanceof Error ? err.message : 'Failed to setup integration')
108
+ } finally {
109
+ setIsSubmitting(false)
110
+ }
111
+ }
112
+
113
+ const handleToggle = async (integrationId: string) => {
114
+ try {
115
+ await integrationAPI.toggle(integrationId)
116
+ const res = await integrationAPI.list(currentWorkspace.id)
117
+ setIntegrations(res.data.integrations || [])
118
+ } catch (err) {
119
+ setError(err instanceof Error ? err.message : 'Failed to toggle integration')
120
+ }
121
+ }
122
+
123
+ const handleDelete = async (integrationId: string) => {
124
+ if (!window.confirm('Are you sure you want to remove this integration?')) return
125
+
126
+ try {
127
+ await integrationAPI.delete(integrationId)
128
+ const res = await integrationAPI.list(currentWorkspace.id)
129
+ setIntegrations(res.data.integrations || [])
130
+ } catch (err) {
131
+ setError(err instanceof Error ? err.message : 'Failed to delete integration')
132
+ }
133
+ }
134
+
135
+ const handleTestIntegration = async (service: 'slack' | 'github' | 'jira') => {
136
+ try {
137
+ if (service === 'slack') {
138
+ await integrationAPI.testSlack(currentWorkspace.id)
139
+ } else if (service === 'github') {
140
+ await integrationAPI.testGitHub(currentWorkspace.id)
141
+ } else if (service === 'jira') {
142
+ await integrationAPI.testJira(currentWorkspace.id)
143
+ }
144
+ alert('Test successful!')
145
+ } catch (err) {
146
+ setError(err instanceof Error ? err.message : 'Test failed')
147
+ }
148
+ }
149
+
150
+ const getIntegration = (service: 'slack' | 'github' | 'jira') => {
151
+ return integrations.find((i) => i.service_name === service)
152
+ }
153
+
154
+ return (
155
+ <div className="space-y-6">
156
+ <div>
157
+ <h1 className="text-2xl font-bold text-slate-900">Integrations</h1>
158
+ <p className="text-slate-600 mt-2">
159
+ Connect with external tools and services to enhance your workflow
160
+ </p>
161
+ </div>
162
+
163
+ {error && (
164
+ <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded flex gap-2">
165
+ <AlertCircle size={18} className="flex-shrink-0 mt-0.5" />
166
+ <div>
167
+ <p className="font-medium">Error</p>
168
+ <p className="text-sm">{error}</p>
169
+ </div>
170
+ </div>
171
+ )}
172
+
173
+ {/* Integration Cards Grid */}
174
+ <div className="grid grid-cols-3 gap-6">
175
+ {(Object.entries(ServiceConfig) as [string, any][]).map(([key, config]) => {
176
+ const integration = getIntegration(key as any)
177
+ const isConfigured = !!integration
178
+
179
+ return (
180
+ <div key={key} className="card space-y-4">
181
+ <div className="flex items-start justify-between">
182
+ <div>
183
+ <div className="text-3xl mb-2">{config.icon}</div>
184
+ <h3 className="text-lg font-semibold text-slate-900">{config.name}</h3>
185
+ <p className="text-sm text-slate-600 mt-1">{config.description}</p>
186
+ </div>
187
+
188
+ {isConfigured && (
189
+ <div className="bg-green-100 text-green-700 px-3 py-1 rounded-full text-xs font-medium flex items-center gap-1">
190
+ <Check size={12} />
191
+ Connected
192
+ </div>
193
+ )}
194
+ </div>
195
+
196
+ <div className="space-y-2 flex-1">
197
+ {isConfigured && (
198
+ <>
199
+ <button
200
+ onClick={() => handleTestIntegration(key as any)}
201
+ className="btn btn-secondary w-full text-sm"
202
+ >
203
+ Test Connection
204
+ </button>
205
+ <button
206
+ onClick={() => handleToggle(integration.id)}
207
+ className={`btn w-full text-sm ${
208
+ integration.is_active
209
+ ? 'bg-yellow-100 text-yellow-800 hover:bg-yellow-200'
210
+ : 'bg-green-100 text-green-800 hover:bg-green-200'
211
+ }`}
212
+ >
213
+ {integration.is_active ? 'Disable' : 'Enable'}
214
+ </button>
215
+ </>
216
+ )}
217
+
218
+ <button
219
+ onClick={() => {
220
+ setShowSetupForm(key as any)
221
+ setFormData({})
222
+ }}
223
+ className="btn btn-primary w-full text-sm flex items-center justify-center gap-2"
224
+ >
225
+ <Plus size={14} />
226
+ {isConfigured ? 'Update' : 'Connect'}
227
+ </button>
228
+
229
+ {isConfigured && (
230
+ <button
231
+ onClick={() => handleDelete(integration.id)}
232
+ className="btn btn-secondary w-full text-sm text-red-600 hover:bg-red-50"
233
+ >
234
+ <Trash2 size={14} className="mx-auto" />
235
+ </button>
236
+ )}
237
+ </div>
238
+ </div>
239
+ )
240
+ })}
241
+ </div>
242
+
243
+ {/* Setup Form Modal */}
244
+ {showSetupForm && (
245
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
246
+ <div className="bg-white rounded-lg p-6 max-w-md w-full mx-4 max-h-screen overflow-y-auto">
247
+ <h2 className="text-xl font-bold mb-4">
248
+ Configure {ServiceConfig[showSetupForm].name}
249
+ </h2>
250
+
251
+ <form
252
+ onSubmit={(e) => handleSetup(e, showSetupForm)}
253
+ className="space-y-4"
254
+ >
255
+ {ServiceConfig[showSetupForm].fields.map((field: any) => (
256
+ <div key={field.name}>
257
+ <label className="block text-sm font-medium text-slate-700 mb-2">
258
+ {field.label}
259
+ </label>
260
+ <input
261
+ type={field.type}
262
+ value={formData[field.name] || ''}
263
+ onChange={(e) =>
264
+ setFormData({ ...formData, [field.name]: e.target.value })
265
+ }
266
+ className="input w-full"
267
+ placeholder={field.label}
268
+ disabled={isSubmitting}
269
+ />
270
+ </div>
271
+ ))}
272
+
273
+ <div className="flex gap-3 pt-4">
274
+ <button type="submit" disabled={isSubmitting} className="btn btn-primary flex-1">
275
+ {isSubmitting ? 'Saving...' : 'Save'}
276
+ </button>
277
+ <button
278
+ type="button"
279
+ onClick={() => setShowSetupForm(null)}
280
+ disabled={isSubmitting}
281
+ className="btn btn-secondary flex-1"
282
+ >
283
+ Cancel
284
+ </button>
285
+ </div>
286
+ </form>
287
+ </div>
288
+ </div>
289
+ )}
290
+ </div>
291
+ )
292
+ }
@@ -0,0 +1,230 @@
1
+ import { useState } from 'react'
2
+ import { useWorkspace } from '../hooks'
3
+ import { Trash2, Edit2, Plus } from 'lucide-react'
4
+
5
+ export const RoleManager = () => {
6
+ const { currentWorkspace, members, roles, addMember, removeMember, updateMember, isLoading } =
7
+ useWorkspace()
8
+ const [showAddForm, setShowAddForm] = useState(false)
9
+ const [editingId, setEditingId] = useState<string | null>(null)
10
+ const [formData, setFormData] = useState({ userId: '', roleName: '' })
11
+ const [isSubmitting, setIsSubmitting] = useState(false)
12
+ const [error, setError] = useState<string | null>(null)
13
+
14
+ if (!currentWorkspace) {
15
+ return (
16
+ <div className="flex items-center justify-center h-64">
17
+ <p className="text-slate-600">Please select a workspace</p>
18
+ </div>
19
+ )
20
+ }
21
+
22
+ if (isLoading) {
23
+ return (
24
+ <div className="flex items-center justify-center h-64">
25
+ <div className="loading-spinner"></div>
26
+ </div>
27
+ )
28
+ }
29
+
30
+ const handleAddMember = async (e: React.FormEvent) => {
31
+ e.preventDefault()
32
+ if (!formData.userId || !formData.roleName) {
33
+ setError('Please fill in all fields')
34
+ return
35
+ }
36
+
37
+ try {
38
+ setIsSubmitting(true)
39
+ setError(null)
40
+ await addMember(formData.userId, formData.roleName)
41
+ setFormData({ userId: '', roleName: '' })
42
+ setShowAddForm(false)
43
+ } catch (err) {
44
+ setError(err instanceof Error ? err.message : 'Failed to add member')
45
+ } finally {
46
+ setIsSubmitting(false)
47
+ }
48
+ }
49
+
50
+ const handleUpdateRole = async (memberId: string, newRole: string) => {
51
+ try {
52
+ setError(null)
53
+ await updateMember(memberId, newRole)
54
+ setEditingId(null)
55
+ } catch (err) {
56
+ setError(err instanceof Error ? err.message : 'Failed to update role')
57
+ }
58
+ }
59
+
60
+ const handleRemoveMember = async (memberId: string) => {
61
+ if (!window.confirm('Are you sure you want to remove this member?')) return
62
+
63
+ try {
64
+ setError(null)
65
+ await removeMember(memberId)
66
+ } catch (err) {
67
+ setError(err instanceof Error ? err.message : 'Failed to remove member')
68
+ }
69
+ }
70
+
71
+ return (
72
+ <div className="space-y-6">
73
+ <div className="flex justify-between items-center">
74
+ <div>
75
+ <h1 className="text-2xl font-bold text-slate-900">Roles & Members</h1>
76
+ <p className="text-slate-600 mt-2">Manage workspace members and their permissions</p>
77
+ </div>
78
+ <button
79
+ onClick={() => setShowAddForm(true)}
80
+ className="btn btn-primary flex items-center gap-2"
81
+ >
82
+ <Plus size={18} />
83
+ Add Member
84
+ </button>
85
+ </div>
86
+
87
+ {error && (
88
+ <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
89
+ {error}
90
+ </div>
91
+ )}
92
+
93
+ {/* Members Table */}
94
+ <div className="card">
95
+ {members.length === 0 ? (
96
+ <div className="text-center py-12">
97
+ <p className="text-slate-600">No members yet. Add one to get started.</p>
98
+ </div>
99
+ ) : (
100
+ <div className="overflow-x-auto">
101
+ <table className="w-full">
102
+ <thead>
103
+ <tr className="border-b border-slate-200">
104
+ <th className="text-left py-3 px-4 font-semibold text-slate-700">Login</th>
105
+ <th className="text-left py-3 px-4 font-semibold text-slate-700">Email</th>
106
+ <th className="text-left py-3 px-4 font-semibold text-slate-700">Role</th>
107
+ <th className="text-center py-3 px-4 font-semibold text-slate-700">Actions</th>
108
+ </tr>
109
+ </thead>
110
+ <tbody>
111
+ {members.map((member) => (
112
+ <tr key={member.id} className="border-b border-slate-100 hover:bg-slate-50">
113
+ <td className="py-3 px-4">
114
+ <div className="flex items-center gap-2">
115
+ {member.avatar_url && (
116
+ <img
117
+ src={member.avatar_url}
118
+ alt={member.github_login}
119
+ className="w-8 h-8 rounded-full"
120
+ />
121
+ )}
122
+ <span className="font-medium text-slate-900">
123
+ {member.github_login}
124
+ </span>
125
+ </div>
126
+ </td>
127
+ <td className="py-3 px-4 text-slate-600">{member.email}</td>
128
+ <td className="py-3 px-4">
129
+ {editingId === member.id ? (
130
+ <select
131
+ value={member.roles[0] || ''}
132
+ onChange={(e) => handleUpdateRole(member.id, e.target.value)}
133
+ className="input text-sm"
134
+ >
135
+ {roles.map((role) => (
136
+ <option key={role.id} value={role.name}>
137
+ {role.name}
138
+ </option>
139
+ ))}
140
+ </select>
141
+ ) : (
142
+ <span className="badge badge-success">{member.roles[0] || 'N/A'}</span>
143
+ )}
144
+ </td>
145
+ <td className="py-3 px-4">
146
+ <div className="flex items-center justify-center gap-2">
147
+ <button
148
+ onClick={() =>
149
+ setEditingId(editingId === member.id ? null : member.id)
150
+ }
151
+ className="text-blue-600 hover:text-blue-700 p-2"
152
+ title="Edit role"
153
+ >
154
+ <Edit2 size={16} />
155
+ </button>
156
+ <button
157
+ onClick={() => handleRemoveMember(member.id)}
158
+ className="text-red-600 hover:text-red-700 p-2"
159
+ title="Remove member"
160
+ >
161
+ <Trash2 size={16} />
162
+ </button>
163
+ </div>
164
+ </td>
165
+ </tr>
166
+ ))}
167
+ </tbody>
168
+ </table>
169
+ </div>
170
+ )}
171
+ </div>
172
+
173
+ {/* Add Member Modal */}
174
+ {showAddForm && (
175
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
176
+ <div className="bg-white rounded-lg p-6 max-w-md w-full mx-4">
177
+ <h2 className="text-xl font-bold mb-4">Add Member</h2>
178
+
179
+ <form onSubmit={handleAddMember} className="space-y-4">
180
+ <div>
181
+ <label className="block text-sm font-medium text-slate-700 mb-2">
182
+ User ID
183
+ </label>
184
+ <input
185
+ type="text"
186
+ value={formData.userId}
187
+ onChange={(e) => setFormData({ ...formData, userId: e.target.value })}
188
+ className="input w-full"
189
+ placeholder="GitHub user ID"
190
+ disabled={isSubmitting}
191
+ />
192
+ </div>
193
+
194
+ <div>
195
+ <label className="block text-sm font-medium text-slate-700 mb-2">Role</label>
196
+ <select
197
+ value={formData.roleName}
198
+ onChange={(e) => setFormData({ ...formData, roleName: e.target.value })}
199
+ className="input w-full"
200
+ disabled={isSubmitting}
201
+ >
202
+ <option value="">Select a role</option>
203
+ {roles.map((role) => (
204
+ <option key={role.id} value={role.name}>
205
+ {role.name}
206
+ </option>
207
+ ))}
208
+ </select>
209
+ </div>
210
+
211
+ <div className="flex gap-3 pt-4">
212
+ <button type="submit" disabled={isSubmitting} className="btn btn-primary flex-1">
213
+ {isSubmitting ? 'Adding...' : 'Add'}
214
+ </button>
215
+ <button
216
+ type="button"
217
+ onClick={() => setShowAddForm(false)}
218
+ disabled={isSubmitting}
219
+ className="btn btn-secondary flex-1"
220
+ >
221
+ Cancel
222
+ </button>
223
+ </div>
224
+ </form>
225
+ </div>
226
+ </div>
227
+ )}
228
+ </div>
229
+ )
230
+ }
@@ -0,0 +1,62 @@
1
+ import { memo } from 'react'
2
+ import { Link, useLocation } from 'react-router-dom'
3
+ import {
4
+ Home,
5
+ BarChart3,
6
+ Folder,
7
+ Shield,
8
+ Zap,
9
+ Settings
10
+ } from 'lucide-react'
11
+
12
+ const navigationItems = [
13
+ { path: '/', label: 'Home', icon: Home },
14
+ { path: '/dashboard', label: 'Analytics', icon: BarChart3 },
15
+ { path: '/workspaces', label: 'Workspaces', icon: Folder },
16
+ { path: '/roles', label: 'Roles & Members', icon: Shield },
17
+ { path: '/integrations', label: 'Integrations', icon: Zap },
18
+ { path: '/settings', label: 'Settings', icon: Settings }
19
+ ]
20
+
21
+ const SidebarComponent = () => {
22
+ const location = useLocation()
23
+
24
+ return (
25
+ <aside className="w-64 bg-slate-900 text-white p-4 border-r border-slate-800 flex flex-col h-screen">
26
+ {/* Logo */}
27
+ <div className="mb-8">
28
+ <h1 className="text-xl font-bold flex items-center gap-2">
29
+ <span className="text-blue-400">🎯</span> Session Wrap
30
+ </h1>
31
+ </div>
32
+
33
+ {/* Navigation */}
34
+ <nav className="space-y-2 flex-1">
35
+ {navigationItems.map(({ path, label, icon: Icon }) => {
36
+ const isActive = location.pathname === path
37
+ return (
38
+ <Link
39
+ key={path}
40
+ to={path}
41
+ className={`flex items-center gap-3 px-4 py-2 rounded-lg transition-colors ${
42
+ isActive
43
+ ? 'bg-blue-600 text-white'
44
+ : 'text-slate-300 hover:bg-slate-800'
45
+ }`}
46
+ >
47
+ <Icon size={18} />
48
+ <span>{label}</span>
49
+ </Link>
50
+ )
51
+ })}
52
+ </nav>
53
+
54
+ {/* Footer */}
55
+ <div className="border-t border-slate-700 pt-4 text-xs text-slate-400">
56
+ <p>v3.9.0</p>
57
+ </div>
58
+ </aside>
59
+ )
60
+ }
61
+
62
+ export const Sidebar = memo(SidebarComponent)