1616 */
1717
1818import React , { useState } from "react" ;
19+ import { useRouter , useSearchParams } from "next/navigation" ;
20+ import { useQueryClient } from "@tanstack/react-query" ;
1921import { Button } from "@/components/ui/button" ;
22+ import { Input } from "@/components/ui/input" ;
2023import config from "@/lib/config" ;
2124import { Card , CardContent , CardHeader } from "@/components/ui/card" ;
2225import { Badge } from "@/components/ui/badge" ;
@@ -64,13 +67,71 @@ const AccountRequestsClient = ({
6467 successMessage,
6568 errorMessage,
6669} : AccountRequestsClientProps ) => {
70+ const router = useRouter ( ) ;
71+ const searchParamsHook = useSearchParams ( ) ;
72+ const queryClient = useQueryClient ( ) ;
73+
74+ // Get current search params from URL
75+ const currentSearch = searchParamsHook . get ( "search" ) || "" ;
76+
77+ const [ localSearch , setLocalSearch ] = useState ( currentSearch ) ;
78+ const lastSyncedSearchRef = React . useRef ( currentSearch ) ;
79+
80+ // Sync localSearch with URL params when they change externally (e.g., browser back/forward)
81+ // Only sync if the change didn't come from our own debounced update
82+ React . useEffect ( ( ) => {
83+ // Only sync if:
84+ // 1. currentSearch changed from an external source (not our debounce)
85+ // 2. localSearch matches the last synced value (user isn't actively typing)
86+ // This prevents overwriting user input while typing
87+ if (
88+ currentSearch !== lastSyncedSearchRef . current &&
89+ localSearch === lastSyncedSearchRef . current
90+ ) {
91+ setLocalSearch ( currentSearch ) ;
92+ lastSyncedSearchRef . current = currentSearch ;
93+ }
94+ } , [ currentSearch , localSearch ] ) ;
95+
96+ // Debounce search input for instant filtering
97+ React . useEffect ( ( ) => {
98+ const timer = setTimeout ( ( ) => {
99+ if ( localSearch !== currentSearch ) {
100+ const params = new URLSearchParams ( searchParamsHook . toString ( ) ) ;
101+ const trimmedSearch = localSearch . trim ( ) ;
102+
103+ if ( trimmedSearch ) {
104+ params . set ( "search" , trimmedSearch ) ;
105+ } else {
106+ params . delete ( "search" ) ;
107+ }
108+
109+ const newUrl = `/admin/account-requests?${ params . toString ( ) } ` ;
110+ // Update ref before navigation to prevent sync effect from overwriting
111+ lastSyncedSearchRef . current = trimmedSearch ;
112+ queryClient . invalidateQueries ( { queryKey : [ "pending-users" ] } ) ;
113+ router . replace ( newUrl , { scroll : false } ) ;
114+ }
115+ } , 300 ) ; // 300ms debounce
116+
117+ return ( ) => clearTimeout ( timer ) ;
118+ } , [ localSearch , currentSearch , searchParamsHook , queryClient , router ] ) ;
119+
120+ // Check if any filters are active
121+ const hasActiveFilters = currentSearch ;
122+
123+ // Only use initialData on first load (when no filters are active)
124+ const initialUsersData = ! hasActiveFilters && initialUsers
125+ ? initialUsers
126+ : undefined ;
127+
67128 // React Query hook with SSR initial data
68129 const {
69130 data : usersData ,
70131 isLoading : usersLoading ,
71132 isError : usersError ,
72133 error : usersErrorData ,
73- } = usePendingUsers ( initialUsers ) ;
134+ } = usePendingUsers ( initialUsersData , currentSearch || undefined ) ;
74135
75136 // React Query mutations
76137 const approveUserMutation = useApproveUser ( ) ;
@@ -83,6 +144,29 @@ const AccountRequestsClient = ({
83144 // usePendingUsers returns User[] directly (not wrapped in UsersListResponse)
84145 const users : UserType [ ] = ( ( usersData ?? initialUsers ) || [ ] ) as UserType [ ] ;
85146
147+ // Update search params in URL and trigger refetch
148+ const updateSearchParams = ( newParams : Record < string , string > ) => {
149+ const params = new URLSearchParams ( searchParamsHook . toString ( ) ) ;
150+
151+ Object . entries ( newParams ) . forEach ( ( [ key , value ] ) => {
152+ if ( value && value !== "all" ) {
153+ params . set ( key , value ) ;
154+ } else {
155+ params . delete ( key ) ;
156+ }
157+ } ) ;
158+
159+ queryClient . invalidateQueries ( { queryKey : [ "pending-users" ] } ) ;
160+ router . replace ( `/admin/account-requests?${ params . toString ( ) } ` , {
161+ scroll : false ,
162+ } ) ;
163+ } ;
164+
165+ const clearFilters = ( ) => {
166+ setLocalSearch ( "" ) ;
167+ router . push ( "/admin/account-requests" ) ;
168+ } ;
169+
86170 // Handler functions for mutations
87171 const handleApproveUser = async ( userId : string ) => {
88172 const user = users . find ( ( u ) => u . id === userId ) ;
@@ -153,7 +237,7 @@ const AccountRequestsClient = ({
153237 < div className = "mx-auto max-w-7xl" >
154238 { /* Header */ }
155239 < div className = "mb-8" >
156- < div className = "flex items-center justify-between" >
240+ < div className = "mb-6 flex flex-col gap-4 sm:flex-row sm: items-center sm: justify-between" >
157241 < div >
158242 < h1 className = "text-3xl font-bold text-gray-900" >
159243 Account Requests
@@ -162,11 +246,30 @@ const AccountRequestsClient = ({
162246 Review and approve pending user registrations
163247 </ p >
164248 </ div >
165- < div className = "flex items-center space-x-2" >
166- < div className = "rounded-full bg-orange-100 px-3 py-1" >
167- < span className = "text-sm font-medium text-orange-800" >
168- { users . length } Pending
169- </ span >
249+ < div className = "flex flex-col gap-3 sm:flex-row sm:items-center" >
250+ { /* Search Input */ }
251+ < form
252+ onSubmit = { ( e ) => {
253+ e . preventDefault ( ) ;
254+ const trimmedSearch = localSearch . trim ( ) ;
255+ updateSearchParams ( { search : trimmedSearch } ) ;
256+ } }
257+ className = "flex-1 sm:min-w-[250px]"
258+ >
259+ < Input
260+ type = "text"
261+ placeholder = "Search by name, email, ID..."
262+ value = { localSearch }
263+ onChange = { ( e ) => setLocalSearch ( e . target . value ) }
264+ className = "w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-sm text-gray-900 placeholder:text-gray-500 focus:border-gray-300 focus:outline-none focus:ring-1 focus:ring-gray-300"
265+ />
266+ </ form >
267+ < div className = "flex items-center space-x-2" >
268+ < div className = "rounded-full bg-orange-100 px-3 py-1" >
269+ < span className = "text-sm font-medium text-orange-800" >
270+ { users . length } Pending
271+ </ span >
272+ </ div >
170273 </ div >
171274 </ div >
172275 </ div >
@@ -210,11 +313,24 @@ const AccountRequestsClient = ({
210313 < User className = "size-12 text-gray-400" />
211314 </ div >
212315 < h3 className = "mb-2 text-lg font-medium text-gray-900" >
213- No Pending Requests
316+ { hasActiveFilters
317+ ? "No pending requests found matching your criteria."
318+ : "No Pending Requests" }
214319 </ h3 >
215- < p className = "text-gray-500" >
216- All account requests have been processed.
320+ < p className = "mb-4 text-gray-500" >
321+ { hasActiveFilters
322+ ? "Try adjusting your search terms."
323+ : "All account requests have been processed." }
217324 </ p >
325+ { hasActiveFilters && (
326+ < Button
327+ variant = "outline"
328+ onClick = { clearFilters }
329+ className = "mt-2 border-gray-300 text-gray-700 hover:bg-gray-100"
330+ >
331+ Clear All Filters
332+ </ Button >
333+ ) }
218334 </ CardContent >
219335 </ Card >
220336 ) : (
@@ -269,30 +385,32 @@ const AccountRequestCard = ({
269385 return (
270386 < Card className = "group border-0 shadow-md transition-all duration-300 hover:shadow-lg" >
271387 < CardHeader className = "pb-4" >
272- < div className = "flex items-start" >
273- < div className = "flex flex-1 items-center space-x-3" >
388+ < div className = "space-y-3" >
389+ { /* Badge on its own row */ }
390+ < div className = "flex justify-start" >
391+ < Badge
392+ variant = "pending"
393+ className = "flex items-center space-x-1"
394+ >
395+ < Clock className = "size-3" />
396+ < span > PENDING</ span >
397+ </ Badge >
398+ </ div >
399+ { /* Avatar and user info with full width */ }
400+ < div className = "flex items-center space-x-3" >
274401 < Avatar className = "size-12" >
275402 < AvatarImage src = "" />
276403 < AvatarFallback className = "bg-gradient-to-br from-blue-500 to-purple-600 font-semibold text-white" >
277404 { getInitials ( user . fullName ) }
278405 </ AvatarFallback >
279406 </ Avatar >
280- < div className = "flex-1" >
281- < div className = "flex items-center justify-between" >
282- < h3 className = "text-lg font-semibold text-gray-900" >
283- { user . fullName }
284- </ h3 >
285- < Badge
286- variant = "pending"
287- className = "ml-2 flex items-center space-x-1"
288- >
289- < Clock className = "size-3" />
290- < span > PENDING</ span >
291- </ Badge >
292- </ div >
407+ < div className = "flex-1 min-w-0" >
408+ < h3 className = "text-lg font-semibold text-gray-900 truncate" >
409+ { user . fullName }
410+ </ h3 >
293411 < div className = "flex items-center space-x-1 text-sm text-gray-500" >
294- < Mail className = "size-3" />
295- < span > { user . email } </ span >
412+ < Mail className = "size-3 flex-shrink-0 " />
413+ < span className = "truncate" > { user . email } </ span >
296414 </ div >
297415 </ div >
298416 </ div >
0 commit comments