Skip to content

Commit fd308bd

Browse files
committed
feat: Enhance admin user editing to include username, display name, profile image, email verification, and real-time username availability checks.
1 parent a830826 commit fd308bd

1 file changed

Lines changed: 122 additions & 6 deletions

File tree

apps/www/src/routes/admin/_components/UsersTab.tsx

Lines changed: 122 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Check, Edit, ExternalLink, Plus, X } from 'lucide-react'
33
import { useEffect, useState } from 'react'
44
import { Badge } from '@/components/ui/badge'
55
import { Button } from '@/components/ui/button'
6+
import { Checkbox } from '@/components/ui/checkbox'
67
import {
78
Dialog,
89
DialogContent,
@@ -22,15 +23,19 @@ import {
2223
} from '@/components/ui/select'
2324
import { toast } from '@/components/ui/use-toast'
2425
import { authClient } from '@/lib/auth-client'
26+
import { ImageUploadField } from './ImageUploadField'
2527

2628
const ROLES = ['admin', 'editor', 'creator', 'user'] as const
2729
type UserRole = (typeof ROLES)[number]
2830

2931
interface AdminUser {
3032
id: string
3133
name: string
34+
username?: string | null
3235
displayUsername?: string | null
3336
email: string
37+
emailVerified?: boolean
38+
image?: string | null
3439
role: string | null
3540
banned: boolean | null
3641
banReason: string | null
@@ -77,11 +82,21 @@ export function UsersTab() {
7782
id: string
7883
name: string
7984
email: string
85+
username: string
86+
displayUsername: string
87+
image: string
88+
emailVerified: boolean
8089
}>({
8190
id: '',
8291
name: '',
83-
email: ''
92+
email: '',
93+
username: '',
94+
displayUsername: '',
95+
image: '',
96+
emailVerified: false
8497
})
98+
const [debouncedEditUsername, setDebouncedEditUsername] = useState('')
99+
const [originalUsername, setOriginalUsername] = useState('')
85100

86101
useEffect(() => {
87102
const timer = setTimeout(() => {
@@ -90,6 +105,13 @@ export function UsersTab() {
90105
return () => clearTimeout(timer)
91106
}, [newUser.username])
92107

108+
useEffect(() => {
109+
const timer = setTimeout(() => {
110+
setDebouncedEditUsername(editUser.username)
111+
}, 300)
112+
return () => clearTimeout(timer)
113+
}, [editUser.username])
114+
93115
const { data, isPending } = useQuery({
94116
queryKey: ['admin', 'users', search],
95117
queryFn: async () => {
@@ -114,6 +136,20 @@ export function UsersTab() {
114136
enabled: debouncedUsername.length >= 2
115137
})
116138

139+
const { data: editUsernameAvailability, isPending: checkingEditUsername } =
140+
useQuery({
141+
queryKey: ['username', 'availability', debouncedEditUsername],
142+
queryFn: async () => {
143+
const result = await authClient.isUsernameAvailable({
144+
username: debouncedEditUsername
145+
})
146+
return result.data
147+
},
148+
enabled:
149+
debouncedEditUsername.length >= 2 &&
150+
debouncedEditUsername !== originalUsername
151+
})
152+
117153
const createUserMutation = useMutation({
118154
mutationFn: async () => {
119155
const email =
@@ -158,7 +194,11 @@ export function UsersTab() {
158194
userId: editUser.id,
159195
data: {
160196
name: editUser.name,
161-
email: editUser.email
197+
email: editUser.email,
198+
username: editUser.username || undefined,
199+
displayUsername: editUser.displayUsername || undefined,
200+
image: editUser.image || undefined,
201+
emailVerified: editUser.emailVerified
162202
}
163203
})
164204
},
@@ -347,9 +387,14 @@ export function UsersTab() {
347387
onClick={() => {
348388
setEditUser({
349389
id: user.id,
350-
name: user.displayUsername || user.name,
351-
email: user.email
390+
name: user.name,
391+
email: user.email,
392+
username: user.username || '',
393+
displayUsername: user.displayUsername || '',
394+
image: user.image || '',
395+
emailVerified: user.emailVerified ?? false
352396
})
397+
setOriginalUsername(user.username || '')
353398
setEditUserDialog(true)
354399
}}>
355400
<Edit className='w-4 h-4' />
@@ -528,12 +573,17 @@ export function UsersTab() {
528573
</Dialog>
529574

530575
<Dialog open={editUserDialog} onOpenChange={setEditUserDialog}>
531-
<DialogContent>
576+
<DialogContent className='max-h-[90vh] overflow-y-auto'>
532577
<DialogHeader>
533578
<DialogTitle>Edit User</DialogTitle>
534579
<DialogDescription>Update user details.</DialogDescription>
535580
</DialogHeader>
536581
<div className='py-4 space-y-4'>
582+
<ImageUploadField
583+
label='Profile Image'
584+
value={editUser.image}
585+
onChange={(url) => setEditUser({ ...editUser, image: url })}
586+
/>
537587
<div className='space-y-2'>
538588
<Label htmlFor='edit-name'>Name</Label>
539589
<Input
@@ -545,6 +595,57 @@ export function UsersTab() {
545595
placeholder='John Doe'
546596
/>
547597
</div>
598+
<div className='space-y-2'>
599+
<Label htmlFor='edit-username'>Username</Label>
600+
<div className='relative'>
601+
<Input
602+
id='edit-username'
603+
value={editUser.username}
604+
onChange={(e) =>
605+
setEditUser({
606+
...editUser,
607+
username: e.target.value.toLowerCase().replace(/\s/g, '')
608+
})
609+
}
610+
placeholder='johndoe'
611+
className='pr-8'
612+
/>
613+
{editUser.username.length >= 2 &&
614+
editUser.username !== originalUsername && (
615+
<div className='absolute -translate-y-1/2 right-2 top-1/2'>
616+
{checkingEditUsername ? (
617+
<div className='w-4 h-4 border-2 rounded-full animate-spin border-muted-foreground border-t-transparent' />
618+
) : editUsernameAvailability?.available ? (
619+
<Check className='w-4 h-4 text-green-500' />
620+
) : (
621+
<X className='w-4 h-4 text-destructive' />
622+
)}
623+
</div>
624+
)}
625+
</div>
626+
{editUser.username.length >= 2 &&
627+
editUser.username !== originalUsername &&
628+
!checkingEditUsername &&
629+
!editUsernameAvailability?.available && (
630+
<p className='text-xs text-destructive'>
631+
Username is already taken
632+
</p>
633+
)}
634+
</div>
635+
<div className='space-y-2'>
636+
<Label htmlFor='edit-display-username'>Display Username</Label>
637+
<Input
638+
id='edit-display-username'
639+
value={editUser.displayUsername}
640+
onChange={(e) =>
641+
setEditUser({ ...editUser, displayUsername: e.target.value })
642+
}
643+
placeholder='JohnDoe123'
644+
/>
645+
<p className='text-xs text-muted-foreground'>
646+
Case-preserved version shown in URLs and profiles
647+
</p>
648+
</div>
548649
<div className='space-y-2'>
549650
<Label htmlFor='edit-email'>Email</Label>
550651
<Input
@@ -557,6 +658,18 @@ export function UsersTab() {
557658
placeholder='john@example.com'
558659
/>
559660
</div>
661+
<div className='flex items-center space-x-2'>
662+
<Checkbox
663+
id='edit-email-verified'
664+
checked={editUser.emailVerified}
665+
onCheckedChange={(checked) =>
666+
setEditUser({ ...editUser, emailVerified: checked === true })
667+
}
668+
/>
669+
<Label htmlFor='edit-email-verified' className='cursor-pointer'>
670+
Email Verified
671+
</Label>
672+
</div>
560673
</div>
561674
<DialogFooter>
562675
<Button
@@ -570,7 +683,10 @@ export function UsersTab() {
570683
disabled={
571684
updateUserMutation.isPending ||
572685
!editUser.name ||
573-
!editUser.email
686+
!editUser.email ||
687+
(editUser.username.length >= 2 &&
688+
editUser.username !== originalUsername &&
689+
!editUsernameAvailability?.available)
574690
}>
575691
{updateUserMutation.isPending ? 'Saving...' : 'Save Changes'}
576692
</Button>

0 commit comments

Comments
 (0)