@@ -3,6 +3,7 @@ import { Check, Edit, ExternalLink, Plus, X } from 'lucide-react'
33import { useEffect , useState } from 'react'
44import { Badge } from '@/components/ui/badge'
55import { Button } from '@/components/ui/button'
6+ import { Checkbox } from '@/components/ui/checkbox'
67import {
78 Dialog ,
89 DialogContent ,
@@ -22,15 +23,19 @@ import {
2223} from '@/components/ui/select'
2324import { toast } from '@/components/ui/use-toast'
2425import { authClient } from '@/lib/auth-client'
26+ import { ImageUploadField } from './ImageUploadField'
2527
2628const ROLES = [ 'admin' , 'editor' , 'creator' , 'user' ] as const
2729type UserRole = ( typeof ROLES ) [ number ]
2830
2931interface 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