better-auth-studio 1.0.6 → 1.0.8
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.
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +126 -29
- package/dist/config.js.map +1 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +28 -2
- package/dist/routes.js.map +1 -1
- package/package.json +8 -1
- package/frontend/index.html +0 -13
- package/frontend/package-lock.json +0 -4675
- package/frontend/package.json +0 -52
- package/frontend/pnpm-lock.yaml +0 -4020
- package/frontend/postcss.config.js +0 -6
- package/frontend/src/App.tsx +0 -36
- package/frontend/src/components/CommandPalette.tsx +0 -219
- package/frontend/src/components/Layout.tsx +0 -159
- package/frontend/src/components/ui/badge.tsx +0 -40
- package/frontend/src/components/ui/button.tsx +0 -53
- package/frontend/src/components/ui/card.tsx +0 -78
- package/frontend/src/components/ui/input.tsx +0 -20
- package/frontend/src/components/ui/label.tsx +0 -19
- package/frontend/src/components/ui/select.tsx +0 -71
- package/frontend/src/index.css +0 -130
- package/frontend/src/lib/utils.ts +0 -6
- package/frontend/src/main.tsx +0 -10
- package/frontend/src/pages/Dashboard.tsx +0 -231
- package/frontend/src/pages/OrganizationDetails.tsx +0 -1281
- package/frontend/src/pages/Organizations.tsx +0 -874
- package/frontend/src/pages/Sessions.tsx +0 -623
- package/frontend/src/pages/Settings.tsx +0 -1019
- package/frontend/src/pages/TeamDetails.tsx +0 -666
- package/frontend/src/pages/Users.tsx +0 -728
- package/frontend/tailwind.config.js +0 -75
- package/frontend/tsconfig.json +0 -31
- package/frontend/tsconfig.node.json +0 -10
- package/frontend/vite.config.ts +0 -31
- package/src/auth-adapter.ts +0 -473
- package/src/cli.ts +0 -51
- package/src/config.ts +0 -320
- package/src/data.ts +0 -351
- package/src/routes.ts +0 -1585
- package/src/studio.ts +0 -86
- package/test-project/README.md +0 -0
- package/test-project/better-auth.db +0 -0
- package/test-project/better-auth_migrations/2025-08-27T15-55-04.099Z.sql +0 -7
- package/test-project/better-auth_migrations/2025-09-04T02-33-19.422Z.sql +0 -7
- package/test-project/package.json +0 -29
- package/test-project/pnpm-lock.yaml +0 -1728
- package/test-project/src/auth.ts +0 -47
- package/test-project/src/index.ts +0 -40
- package/tsconfig.json +0 -21
|
@@ -1,666 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react'
|
|
2
|
-
import { useParams, Link } from 'react-router-dom'
|
|
3
|
-
import { toast } from 'sonner'
|
|
4
|
-
import {
|
|
5
|
-
Users,
|
|
6
|
-
ArrowLeft,
|
|
7
|
-
Edit,
|
|
8
|
-
Calendar,
|
|
9
|
-
UserPlus,
|
|
10
|
-
Trash2,
|
|
11
|
-
Search,
|
|
12
|
-
X,
|
|
13
|
-
Building2
|
|
14
|
-
} from 'lucide-react'
|
|
15
|
-
import { Button } from '../components/ui/button'
|
|
16
|
-
import { Badge } from '../components/ui/badge'
|
|
17
|
-
import { Input } from '../components/ui/input'
|
|
18
|
-
import { Label } from '../components/ui/label'
|
|
19
|
-
|
|
20
|
-
interface Team {
|
|
21
|
-
id: string
|
|
22
|
-
name: string
|
|
23
|
-
organizationId: string
|
|
24
|
-
metadata?: any
|
|
25
|
-
createdAt: string
|
|
26
|
-
updatedAt: string
|
|
27
|
-
memberCount?: number
|
|
28
|
-
organization?: {
|
|
29
|
-
id: string
|
|
30
|
-
name: string
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface TeamMember {
|
|
35
|
-
id: string
|
|
36
|
-
userId: string
|
|
37
|
-
teamId: string
|
|
38
|
-
role: string
|
|
39
|
-
joinedAt: string
|
|
40
|
-
user: {
|
|
41
|
-
id: string
|
|
42
|
-
name: string
|
|
43
|
-
email: string
|
|
44
|
-
image?: string
|
|
45
|
-
emailVerified: boolean
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
interface User {
|
|
50
|
-
id: string
|
|
51
|
-
name: string
|
|
52
|
-
email: string
|
|
53
|
-
image?: string
|
|
54
|
-
emailVerified: boolean
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export default function TeamDetails() {
|
|
58
|
-
const { teamId } = useParams<{ teamId: string }>()
|
|
59
|
-
const [team, setTeam] = useState<Team | null>(null)
|
|
60
|
-
const [members, setMembers] = useState<TeamMember[]>([])
|
|
61
|
-
const [loading, setLoading] = useState(true)
|
|
62
|
-
const [activeTab, setActiveTab] = useState<'details' | 'members'>('details')
|
|
63
|
-
|
|
64
|
-
// Modal states
|
|
65
|
-
const [showAddMemberModal, setShowAddMemberModal] = useState(false)
|
|
66
|
-
const [showEditTeamModal, setShowEditTeamModal] = useState(false)
|
|
67
|
-
|
|
68
|
-
// Search and selection states
|
|
69
|
-
const [searchTerm, setSearchTerm] = useState('')
|
|
70
|
-
const [availableUsers, setAvailableUsers] = useState<User[]>([])
|
|
71
|
-
const [selectedUsers, setSelectedUsers] = useState<string[]>([])
|
|
72
|
-
const [teamFormData, setTeamFormData] = useState({ name: '' })
|
|
73
|
-
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
if (teamId) {
|
|
76
|
-
fetchTeam()
|
|
77
|
-
fetchTeamMembers()
|
|
78
|
-
}
|
|
79
|
-
}, [teamId])
|
|
80
|
-
|
|
81
|
-
const fetchTeam = async () => {
|
|
82
|
-
try {
|
|
83
|
-
const response = await fetch(`/api/teams/${teamId}`)
|
|
84
|
-
const data = await response.json()
|
|
85
|
-
|
|
86
|
-
if (data.success) {
|
|
87
|
-
setTeam(data.team)
|
|
88
|
-
setTeamFormData({ name: data.team.name })
|
|
89
|
-
} else {
|
|
90
|
-
toast.error('Team not found')
|
|
91
|
-
}
|
|
92
|
-
} catch (error) {
|
|
93
|
-
console.error('Failed to fetch team:', error)
|
|
94
|
-
toast.error('Failed to load team')
|
|
95
|
-
} finally {
|
|
96
|
-
setLoading(false)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const fetchTeamMembers = async () => {
|
|
101
|
-
try {
|
|
102
|
-
const response = await fetch(`/api/teams/${teamId}/members`)
|
|
103
|
-
const data = await response.json()
|
|
104
|
-
|
|
105
|
-
if (data.success) {
|
|
106
|
-
setMembers(data.members || [])
|
|
107
|
-
}
|
|
108
|
-
} catch (error) {
|
|
109
|
-
console.error('Failed to fetch team members:', error)
|
|
110
|
-
toast.error('Failed to load team members')
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const fetchAvailableUsers = async () => {
|
|
115
|
-
try {
|
|
116
|
-
const response = await fetch('/api/users?limit=10000')
|
|
117
|
-
const data = await response.json()
|
|
118
|
-
|
|
119
|
-
// Filter out users who are already team members
|
|
120
|
-
const memberUserIds = members.map(member => member.userId)
|
|
121
|
-
const available = (data.users || []).filter((user: User) =>
|
|
122
|
-
!memberUserIds.includes(user.id)
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
setAvailableUsers(available)
|
|
126
|
-
} catch (error) {
|
|
127
|
-
console.error('Failed to fetch available users:', error)
|
|
128
|
-
toast.error('Failed to load users')
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const handleUpdateTeam = async () => {
|
|
133
|
-
if (!teamFormData.name) {
|
|
134
|
-
toast.error('Please enter a team name')
|
|
135
|
-
return
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const toastId = toast.loading('Updating team...')
|
|
139
|
-
|
|
140
|
-
try {
|
|
141
|
-
const response = await fetch(`/api/teams/${teamId}`, {
|
|
142
|
-
method: 'PUT',
|
|
143
|
-
headers: { 'Content-Type': 'application/json' },
|
|
144
|
-
body: JSON.stringify({ name: teamFormData.name })
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
const result = await response.json()
|
|
148
|
-
|
|
149
|
-
if (result.success) {
|
|
150
|
-
await fetchTeam()
|
|
151
|
-
setShowEditTeamModal(false)
|
|
152
|
-
toast.success('Team updated successfully!', { id: toastId })
|
|
153
|
-
} else {
|
|
154
|
-
toast.error(`Error updating team: ${result.error || 'Unknown error'}`, { id: toastId })
|
|
155
|
-
}
|
|
156
|
-
} catch (error) {
|
|
157
|
-
console.error('Error updating team:', error)
|
|
158
|
-
toast.error('Error updating team', { id: toastId })
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const handleAddMembers = async () => {
|
|
163
|
-
if (selectedUsers.length === 0) {
|
|
164
|
-
toast.error('Please select at least one user')
|
|
165
|
-
return
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const toastId = toast.loading(`Adding ${selectedUsers.length} members...`)
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
const response = await fetch(`/api/teams/${teamId}/members`, {
|
|
172
|
-
method: 'POST',
|
|
173
|
-
headers: { 'Content-Type': 'application/json' },
|
|
174
|
-
body: JSON.stringify({ userIds: selectedUsers })
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
const result = await response.json()
|
|
178
|
-
|
|
179
|
-
if (result.success) {
|
|
180
|
-
await fetchTeamMembers()
|
|
181
|
-
setShowAddMemberModal(false)
|
|
182
|
-
setSelectedUsers([])
|
|
183
|
-
setSearchTerm('')
|
|
184
|
-
toast.success(`Successfully added ${selectedUsers.length} members!`, { id: toastId })
|
|
185
|
-
} else {
|
|
186
|
-
toast.error(`Error adding members: ${result.error || 'Unknown error'}`, { id: toastId })
|
|
187
|
-
}
|
|
188
|
-
} catch (error) {
|
|
189
|
-
console.error('Error adding members:', error)
|
|
190
|
-
toast.error('Error adding members', { id: toastId })
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const handleRemoveTeamMember = async (memberId: string, userName: string) => {
|
|
195
|
-
const toastId = toast.loading(`Removing ${userName}...`)
|
|
196
|
-
|
|
197
|
-
try {
|
|
198
|
-
const response = await fetch(`/api/team-members/${memberId}`, {
|
|
199
|
-
method: 'DELETE',
|
|
200
|
-
headers: { 'Content-Type': 'application/json' }
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
const result = await response.json()
|
|
204
|
-
|
|
205
|
-
if (result.success) {
|
|
206
|
-
await fetchTeamMembers()
|
|
207
|
-
toast.success(`${userName} removed from team!`, { id: toastId })
|
|
208
|
-
} else {
|
|
209
|
-
toast.error(`Error removing member: ${result.error || 'Unknown error'}`, { id: toastId })
|
|
210
|
-
}
|
|
211
|
-
} catch (error) {
|
|
212
|
-
console.error('Error removing team member:', error)
|
|
213
|
-
toast.error('Error removing team member', { id: toastId })
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const openAddMemberModal = () => {
|
|
218
|
-
fetchAvailableUsers()
|
|
219
|
-
setShowAddMemberModal(true)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const toggleUserSelection = (userId: string) => {
|
|
223
|
-
setSelectedUsers(prev =>
|
|
224
|
-
prev.includes(userId)
|
|
225
|
-
? prev.filter(id => id !== userId)
|
|
226
|
-
: [...prev, userId]
|
|
227
|
-
)
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const filteredUsers = availableUsers.filter(user =>
|
|
231
|
-
user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
232
|
-
user.email.toLowerCase().includes(searchTerm.toLowerCase())
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
if (loading) {
|
|
236
|
-
return (
|
|
237
|
-
<div className="flex items-center justify-center h-64">
|
|
238
|
-
<div className="text-white">Loading team details...</div>
|
|
239
|
-
</div>
|
|
240
|
-
)
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (!team) {
|
|
244
|
-
return (
|
|
245
|
-
<div className="space-y-6 p-6">
|
|
246
|
-
<div className="flex items-center space-x-4">
|
|
247
|
-
<Link to="/organizations">
|
|
248
|
-
<Button variant="ghost" className="text-gray-400 hover:text-white rounded-none">
|
|
249
|
-
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
250
|
-
Back to Organizations
|
|
251
|
-
</Button>
|
|
252
|
-
</Link>
|
|
253
|
-
</div>
|
|
254
|
-
<div className="text-center py-12">
|
|
255
|
-
<Users className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
|
256
|
-
<h2 className="text-xl text-white font-light mb-2">Team Not Found</h2>
|
|
257
|
-
<p className="text-gray-400">The team you're looking for doesn't exist.</p>
|
|
258
|
-
</div>
|
|
259
|
-
</div>
|
|
260
|
-
)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return (
|
|
264
|
-
<div className="space-y-6 p-6">
|
|
265
|
-
{/* Header */}
|
|
266
|
-
<div className="flex items-center justify-between">
|
|
267
|
-
<div className="flex items-center space-x-4">
|
|
268
|
-
<Link to={`/organizations/${team.organizationId}`}>
|
|
269
|
-
<Button variant="ghost" className="text-gray-400 hover:text-white rounded-none">
|
|
270
|
-
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
271
|
-
Back to Organization
|
|
272
|
-
</Button>
|
|
273
|
-
</Link>
|
|
274
|
-
</div>
|
|
275
|
-
|
|
276
|
-
<div className="flex items-center space-x-3">
|
|
277
|
-
<Button
|
|
278
|
-
onClick={openAddMemberModal}
|
|
279
|
-
className="border border-dashed border-white/20 text-white hover:bg-white/10 bg-transparent rounded-none"
|
|
280
|
-
>
|
|
281
|
-
<UserPlus className="w-4 h-4 mr-2" />
|
|
282
|
-
Add Members
|
|
283
|
-
</Button>
|
|
284
|
-
<Button
|
|
285
|
-
onClick={() => setShowEditTeamModal(true)}
|
|
286
|
-
className="bg-white hover:bg-white/90 text-black border border-white/20 rounded-none"
|
|
287
|
-
>
|
|
288
|
-
<Edit className="w-4 h-4 mr-2" />
|
|
289
|
-
Edit Team
|
|
290
|
-
</Button>
|
|
291
|
-
</div>
|
|
292
|
-
</div>
|
|
293
|
-
|
|
294
|
-
<div className="flex items-center space-x-3">
|
|
295
|
-
<div className="w-12 h-12 bg-white/10 border border-dashed border-white/20 rounded-none flex items-center justify-center">
|
|
296
|
-
<Users className="w-6 h-6 text-white" />
|
|
297
|
-
</div>
|
|
298
|
-
<div>
|
|
299
|
-
<h1 className="text-2xl text-white font-light">{team.name}</h1>
|
|
300
|
-
<div className="flex items-center space-x-2">
|
|
301
|
-
{team.organization && (
|
|
302
|
-
<Link to={`/organizations/${team.organizationId}`} className="text-gray-400 hover:text-white text-sm">
|
|
303
|
-
<Building2 className="w-4 h-4 inline mr-1" />
|
|
304
|
-
{team.organization.name}
|
|
305
|
-
</Link>
|
|
306
|
-
)}
|
|
307
|
-
</div>
|
|
308
|
-
</div>
|
|
309
|
-
</div>
|
|
310
|
-
|
|
311
|
-
{/* Tabs */}
|
|
312
|
-
<div className="border-b border-white/10">
|
|
313
|
-
<nav className="flex space-x-8">
|
|
314
|
-
<button
|
|
315
|
-
onClick={() => setActiveTab('details')}
|
|
316
|
-
className={`flex items-center space-x-2 px-3 py-4 text-sm font-medium border-b-2 transition-all duration-200 ${
|
|
317
|
-
activeTab === 'details'
|
|
318
|
-
? 'border-white text-white'
|
|
319
|
-
: 'border-transparent text-gray-400 hover:text-white hover:border-gray-300'
|
|
320
|
-
}`}
|
|
321
|
-
>
|
|
322
|
-
<Users className="w-4 h-4" />
|
|
323
|
-
<span>Details</span>
|
|
324
|
-
</button>
|
|
325
|
-
<button
|
|
326
|
-
onClick={() => setActiveTab('members')}
|
|
327
|
-
className={`flex items-center space-x-2 px-3 py-4 text-sm font-medium border-b-2 transition-all duration-200 ${
|
|
328
|
-
activeTab === 'members'
|
|
329
|
-
? 'border-white text-white'
|
|
330
|
-
: 'border-transparent text-gray-400 hover:text-white hover:border-gray-300'
|
|
331
|
-
}`}
|
|
332
|
-
>
|
|
333
|
-
<Users className="w-4 h-4" />
|
|
334
|
-
<span>Members</span>
|
|
335
|
-
{members.length > 0 && (
|
|
336
|
-
<Badge variant="secondary" className="text-xs bg-white/10 border border-white/20 rounded-sm">
|
|
337
|
-
{members.length}
|
|
338
|
-
</Badge>
|
|
339
|
-
)}
|
|
340
|
-
</button>
|
|
341
|
-
</nav>
|
|
342
|
-
</div>
|
|
343
|
-
|
|
344
|
-
{/* Tab Content */}
|
|
345
|
-
{activeTab === 'details' && (
|
|
346
|
-
<div className="space-y-6">
|
|
347
|
-
{/* Team Information */}
|
|
348
|
-
<div className="bg-black/30 border border-dashed border-white/20 rounded-none p-6">
|
|
349
|
-
<h3 className="text-lg text-white font-light mb-4">Team Information</h3>
|
|
350
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
351
|
-
<div>
|
|
352
|
-
<label className="text-sm text-gray-400 font-light">Name</label>
|
|
353
|
-
<p className="text-white mt-1">{team.name}</p>
|
|
354
|
-
</div>
|
|
355
|
-
<div>
|
|
356
|
-
<label className="text-sm text-gray-400 font-light">Organization</label>
|
|
357
|
-
<p className="text-white mt-1">{team.organization?.name || 'Unknown'}</p>
|
|
358
|
-
</div>
|
|
359
|
-
<div>
|
|
360
|
-
<label className="text-sm text-gray-400 font-light">Created</label>
|
|
361
|
-
<p className="text-white mt-1">
|
|
362
|
-
{new Date(team.createdAt).toLocaleDateString('en-US', {
|
|
363
|
-
year: 'numeric',
|
|
364
|
-
month: 'long',
|
|
365
|
-
day: 'numeric'
|
|
366
|
-
})}
|
|
367
|
-
</p>
|
|
368
|
-
</div>
|
|
369
|
-
<div>
|
|
370
|
-
<label className="text-sm text-gray-400 font-light">Last Updated</label>
|
|
371
|
-
<p className="text-white mt-1">
|
|
372
|
-
{new Date(team.updatedAt).toLocaleDateString('en-US', {
|
|
373
|
-
year: 'numeric',
|
|
374
|
-
month: 'long',
|
|
375
|
-
day: 'numeric'
|
|
376
|
-
})}
|
|
377
|
-
</p>
|
|
378
|
-
</div>
|
|
379
|
-
</div>
|
|
380
|
-
</div>
|
|
381
|
-
|
|
382
|
-
{/* Team Stats */}
|
|
383
|
-
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
384
|
-
<div className="bg-black/30 border border-dashed border-white/20 rounded-none p-6">
|
|
385
|
-
<div className="flex items-center space-x-3">
|
|
386
|
-
<Users className="w-8 h-8 text-white" />
|
|
387
|
-
<div>
|
|
388
|
-
<p className="text-2xl text-white font-light">{members.length}</p>
|
|
389
|
-
<p className="text-sm text-gray-400">Members</p>
|
|
390
|
-
</div>
|
|
391
|
-
</div>
|
|
392
|
-
</div>
|
|
393
|
-
<div className="bg-black/30 border border-dashed border-white/20 rounded-none p-6">
|
|
394
|
-
<div className="flex items-center space-x-3">
|
|
395
|
-
<Calendar className="w-8 h-8 text-white" />
|
|
396
|
-
<div>
|
|
397
|
-
<p className="text-2xl text-white font-light">
|
|
398
|
-
{Math.ceil((new Date().getTime() - new Date(team.createdAt).getTime()) / (1000 * 60 * 60 * 24))}
|
|
399
|
-
</p>
|
|
400
|
-
<p className="text-sm text-gray-400">Days Active</p>
|
|
401
|
-
</div>
|
|
402
|
-
</div>
|
|
403
|
-
</div>
|
|
404
|
-
<div className="bg-black/30 border border-dashed border-white/20 rounded-none p-6">
|
|
405
|
-
<div className="flex items-center space-x-3">
|
|
406
|
-
<Building2 className="w-8 h-8 text-white" />
|
|
407
|
-
<div>
|
|
408
|
-
<p className="text-2xl text-white font-light">1</p>
|
|
409
|
-
<p className="text-sm text-gray-400">Organization</p>
|
|
410
|
-
</div>
|
|
411
|
-
</div>
|
|
412
|
-
</div>
|
|
413
|
-
</div>
|
|
414
|
-
</div>
|
|
415
|
-
)}
|
|
416
|
-
|
|
417
|
-
{activeTab === 'members' && (
|
|
418
|
-
<div className="space-y-6">
|
|
419
|
-
{/* Members Header */}
|
|
420
|
-
<div className="flex items-center justify-between">
|
|
421
|
-
<div>
|
|
422
|
-
<h3 className="text-lg text-white font-light">Team Members ({members.length})</h3>
|
|
423
|
-
<p className="text-gray-400 mt-1">Manage members of this team</p>
|
|
424
|
-
</div>
|
|
425
|
-
</div>
|
|
426
|
-
|
|
427
|
-
{/* Members List */}
|
|
428
|
-
{members.length > 0 ? (
|
|
429
|
-
<div className="bg-black/30 border border-dashed border-white/20 rounded-none">
|
|
430
|
-
<div className="overflow-x-auto">
|
|
431
|
-
<table className="w-full">
|
|
432
|
-
<thead>
|
|
433
|
-
<tr className="border-b border-dashed border-white/10">
|
|
434
|
-
<th className="text-left py-4 px-4 text-white font-light">User</th>
|
|
435
|
-
<th className="text-left py-4 px-4 text-white font-light">Email</th>
|
|
436
|
-
<th className="text-left py-4 px-4 text-white font-light">Role</th>
|
|
437
|
-
<th className="text-left py-4 px-4 text-white font-light">Joined</th>
|
|
438
|
-
<th className="text-right py-4 px-4 text-white font-light">Actions</th>
|
|
439
|
-
</tr>
|
|
440
|
-
</thead>
|
|
441
|
-
<tbody>
|
|
442
|
-
{members.map((member) => (
|
|
443
|
-
<tr key={member.id} className="border-b border-dashed border-white/5 hover:bg-white/5">
|
|
444
|
-
<td className="py-4 px-4">
|
|
445
|
-
<div className="flex items-center space-x-3">
|
|
446
|
-
<img
|
|
447
|
-
src={member.user.image || `https://api.dicebear.com/7.x/avataaars/svg?seed=${member.user.id}`}
|
|
448
|
-
alt={member.user.name}
|
|
449
|
-
className="w-10 h-10 rounded-none border border-dashed border-white/20"
|
|
450
|
-
/>
|
|
451
|
-
<div>
|
|
452
|
-
<div className="text-white font-light">{member.user.name}</div>
|
|
453
|
-
<div className="text-sm text-gray-400">ID: {member.user.id}</div>
|
|
454
|
-
</div>
|
|
455
|
-
</div>
|
|
456
|
-
</td>
|
|
457
|
-
<td className="py-4 px-4 text-white">{member.user.email}</td>
|
|
458
|
-
<td className="py-4 px-4">
|
|
459
|
-
<Badge variant="secondary" className="text-xs bg-blue-900/10 border border-dashed border-blue-500/30 text-blue-400/70 rounded-none capitalize">
|
|
460
|
-
{member.role}
|
|
461
|
-
</Badge>
|
|
462
|
-
</td>
|
|
463
|
-
<td className="py-4 px-4 text-sm text-gray-400">
|
|
464
|
-
{new Date(member.joinedAt).toLocaleDateString()}
|
|
465
|
-
</td>
|
|
466
|
-
<td className="py-4 px-4 text-right">
|
|
467
|
-
<div className="flex items-center justify-end space-x-2">
|
|
468
|
-
<Button
|
|
469
|
-
variant="outline"
|
|
470
|
-
size="sm"
|
|
471
|
-
className="border border-dashed border-red-400/50 text-red-400 hover:bg-red-400/10 rounded-none"
|
|
472
|
-
onClick={() => handleRemoveTeamMember(member.id, member.user.name)}
|
|
473
|
-
>
|
|
474
|
-
<Trash2 className="w-4 h-4 mr-1" />
|
|
475
|
-
Remove
|
|
476
|
-
</Button>
|
|
477
|
-
</div>
|
|
478
|
-
</td>
|
|
479
|
-
</tr>
|
|
480
|
-
))}
|
|
481
|
-
</tbody>
|
|
482
|
-
</table>
|
|
483
|
-
</div>
|
|
484
|
-
</div>
|
|
485
|
-
) : (
|
|
486
|
-
<div className="bg-black/30 border border-dashed border-white/20 rounded-none p-12">
|
|
487
|
-
<div className="text-center">
|
|
488
|
-
<Users className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
|
489
|
-
<h3 className="text-xl text-white font-light mb-2">No Members Yet</h3>
|
|
490
|
-
<p className="text-gray-400 mb-6">
|
|
491
|
-
Add members to this team to get started.
|
|
492
|
-
</p>
|
|
493
|
-
<Button
|
|
494
|
-
onClick={openAddMemberModal}
|
|
495
|
-
className="bg-white hover:bg-white/90 text-black border border-white/20 rounded-none"
|
|
496
|
-
>
|
|
497
|
-
<UserPlus className="w-4 h-4 mr-2" />
|
|
498
|
-
Add First Members
|
|
499
|
-
</Button>
|
|
500
|
-
</div>
|
|
501
|
-
</div>
|
|
502
|
-
)}
|
|
503
|
-
</div>
|
|
504
|
-
)}
|
|
505
|
-
|
|
506
|
-
{/* Add Members Modal */}
|
|
507
|
-
{showAddMemberModal && (
|
|
508
|
-
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
509
|
-
<div className="bg-black/90 border border-dashed border-white/20 p-6 w-full max-w-2xl rounded-none">
|
|
510
|
-
<div className="flex items-center justify-between mb-6">
|
|
511
|
-
<h3 className="text-lg text-white font-light">Add Team Members</h3>
|
|
512
|
-
<Button
|
|
513
|
-
variant="ghost"
|
|
514
|
-
size="sm"
|
|
515
|
-
onClick={() => {
|
|
516
|
-
setShowAddMemberModal(false)
|
|
517
|
-
setSelectedUsers([])
|
|
518
|
-
setSearchTerm('')
|
|
519
|
-
}}
|
|
520
|
-
className="text-gray-400 hover:text-white rounded-none"
|
|
521
|
-
>
|
|
522
|
-
<X className="w-4 h-4" />
|
|
523
|
-
</Button>
|
|
524
|
-
</div>
|
|
525
|
-
|
|
526
|
-
<div className="space-y-4">
|
|
527
|
-
{/* Search */}
|
|
528
|
-
<div className="relative">
|
|
529
|
-
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
|
530
|
-
<Input
|
|
531
|
-
placeholder="Search users by name or email..."
|
|
532
|
-
value={searchTerm}
|
|
533
|
-
onChange={(e) => setSearchTerm(e.target.value)}
|
|
534
|
-
className="pl-10 border border-dashed border-white/20 bg-black/30 text-white rounded-none"
|
|
535
|
-
/>
|
|
536
|
-
</div>
|
|
537
|
-
|
|
538
|
-
{/* Selected Count */}
|
|
539
|
-
{selectedUsers.length > 0 && (
|
|
540
|
-
<div className="bg-blue-900/20 border border-blue-500/30 rounded-none p-3">
|
|
541
|
-
<p className="text-blue-400 text-sm">
|
|
542
|
-
{selectedUsers.length} user{selectedUsers.length !== 1 ? 's' : ''} selected
|
|
543
|
-
</p>
|
|
544
|
-
</div>
|
|
545
|
-
)}
|
|
546
|
-
|
|
547
|
-
{/* User List */}
|
|
548
|
-
<div className="bg-black/30 border border-dashed border-white/20 rounded-none max-h-96 overflow-y-auto">
|
|
549
|
-
{filteredUsers.length > 0 ? (
|
|
550
|
-
<div className="divide-y divide-white/5">
|
|
551
|
-
{filteredUsers.map((user) => (
|
|
552
|
-
<div
|
|
553
|
-
key={user.id}
|
|
554
|
-
className={`flex items-center space-x-3 p-4 cursor-pointer hover:bg-white/5 ${
|
|
555
|
-
selectedUsers.includes(user.id) ? 'bg-blue-900/20' : ''
|
|
556
|
-
}`}
|
|
557
|
-
onClick={() => toggleUserSelection(user.id)}
|
|
558
|
-
>
|
|
559
|
-
<input
|
|
560
|
-
type="checkbox"
|
|
561
|
-
checked={selectedUsers.includes(user.id)}
|
|
562
|
-
onChange={() => toggleUserSelection(user.id)}
|
|
563
|
-
className="rounded border-gray-300"
|
|
564
|
-
/>
|
|
565
|
-
<img
|
|
566
|
-
src={user.image || `https://api.dicebear.com/7.x/avataaars/svg?seed=${user.id}`}
|
|
567
|
-
alt={user.name}
|
|
568
|
-
className="w-10 h-10 rounded-none border border-dashed border-white/20"
|
|
569
|
-
/>
|
|
570
|
-
<div className="flex-1">
|
|
571
|
-
<div className="text-white font-light">{user.name}</div>
|
|
572
|
-
<div className="text-sm text-gray-400">{user.email}</div>
|
|
573
|
-
</div>
|
|
574
|
-
</div>
|
|
575
|
-
))}
|
|
576
|
-
</div>
|
|
577
|
-
) : (
|
|
578
|
-
<div className="p-8 text-center">
|
|
579
|
-
<p className="text-gray-400">No users found matching your search.</p>
|
|
580
|
-
</div>
|
|
581
|
-
)}
|
|
582
|
-
</div>
|
|
583
|
-
</div>
|
|
584
|
-
|
|
585
|
-
<div className="flex justify-end space-x-3 mt-6">
|
|
586
|
-
<Button
|
|
587
|
-
variant="outline"
|
|
588
|
-
onClick={() => {
|
|
589
|
-
setShowAddMemberModal(false)
|
|
590
|
-
setSelectedUsers([])
|
|
591
|
-
setSearchTerm('')
|
|
592
|
-
}}
|
|
593
|
-
className="border border-dashed border-white/20 text-white hover:bg-white/10 rounded-none"
|
|
594
|
-
>
|
|
595
|
-
Cancel
|
|
596
|
-
</Button>
|
|
597
|
-
<Button
|
|
598
|
-
onClick={handleAddMembers}
|
|
599
|
-
disabled={selectedUsers.length === 0}
|
|
600
|
-
className="bg-white hover:bg-white/90 text-black border border-white/20 rounded-none disabled:opacity-50"
|
|
601
|
-
>
|
|
602
|
-
<UserPlus className="w-4 h-4 mr-2" />
|
|
603
|
-
Add {selectedUsers.length} Member{selectedUsers.length !== 1 ? 's' : ''}
|
|
604
|
-
</Button>
|
|
605
|
-
</div>
|
|
606
|
-
</div>
|
|
607
|
-
</div>
|
|
608
|
-
)}
|
|
609
|
-
|
|
610
|
-
{/* Edit Team Modal */}
|
|
611
|
-
{showEditTeamModal && (
|
|
612
|
-
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
613
|
-
<div className="bg-black/90 border border-dashed border-white/20 p-6 w-full max-w-md rounded-none">
|
|
614
|
-
<div className="flex items-center justify-between mb-4">
|
|
615
|
-
<h3 className="text-lg text-white font-light">Edit Team</h3>
|
|
616
|
-
<Button
|
|
617
|
-
variant="ghost"
|
|
618
|
-
size="sm"
|
|
619
|
-
onClick={() => setShowEditTeamModal(false)}
|
|
620
|
-
className="text-gray-400 hover:text-white rounded-none"
|
|
621
|
-
>
|
|
622
|
-
<X className="w-4 h-4" />
|
|
623
|
-
</Button>
|
|
624
|
-
</div>
|
|
625
|
-
<div className="space-y-4">
|
|
626
|
-
<div className="flex items-center space-x-3">
|
|
627
|
-
<div className="w-16 h-16 bg-white/10 border border-dashed border-white/20 rounded-none flex items-center justify-center">
|
|
628
|
-
<Users className="w-8 h-8 text-white" />
|
|
629
|
-
</div>
|
|
630
|
-
<div>
|
|
631
|
-
<div className="text-white font-light">{team.name}</div>
|
|
632
|
-
<div className="text-sm text-gray-400">{team.organization?.name}</div>
|
|
633
|
-
</div>
|
|
634
|
-
</div>
|
|
635
|
-
<div>
|
|
636
|
-
<Label htmlFor="edit-team-name" className="text-sm text-gray-400 font-light">Team Name</Label>
|
|
637
|
-
<Input
|
|
638
|
-
id="edit-team-name"
|
|
639
|
-
value={teamFormData.name}
|
|
640
|
-
onChange={(e) => setTeamFormData({ name: e.target.value })}
|
|
641
|
-
placeholder="e.g. Development Team"
|
|
642
|
-
className="mt-1 border border-dashed border-white/20 bg-black/30 text-white rounded-none"
|
|
643
|
-
/>
|
|
644
|
-
</div>
|
|
645
|
-
</div>
|
|
646
|
-
<div className="flex justify-end space-x-3 mt-6">
|
|
647
|
-
<Button
|
|
648
|
-
variant="outline"
|
|
649
|
-
onClick={() => setShowEditTeamModal(false)}
|
|
650
|
-
className="border border-dashed border-white/20 text-white hover:bg-white/10 rounded-none"
|
|
651
|
-
>
|
|
652
|
-
Cancel
|
|
653
|
-
</Button>
|
|
654
|
-
<Button
|
|
655
|
-
onClick={handleUpdateTeam}
|
|
656
|
-
className="bg-white hover:bg-white/90 text-black border border-white/20 rounded-none"
|
|
657
|
-
>
|
|
658
|
-
Update Team
|
|
659
|
-
</Button>
|
|
660
|
-
</div>
|
|
661
|
-
</div>
|
|
662
|
-
</div>
|
|
663
|
-
)}
|
|
664
|
-
</div>
|
|
665
|
-
)
|
|
666
|
-
}
|