1
1
import {
2
2
Badge ,
3
3
Box ,
4
+ Button ,
4
5
Container ,
5
6
Flex ,
6
7
Heading ,
@@ -13,90 +14,65 @@ import {
13
14
Thead ,
14
15
Tr ,
15
16
} from "@chakra-ui/react"
16
- import { useQueryClient , useSuspenseQuery } from "@tanstack/react-query"
17
- import { createFileRoute } from "@tanstack/react-router"
17
+ import { useQuery , useQueryClient } from "@tanstack/react-query"
18
+ import { createFileRoute , useNavigate } from "@tanstack/react-router"
19
+ import { useEffect } from "react"
20
+ import { z } from "zod"
18
21
19
- import { Suspense } from "react"
20
22
import { type UserPublic , UsersService } from "../../client"
21
23
import AddUser from "../../components/Admin/AddUser"
22
24
import ActionsMenu from "../../components/Common/ActionsMenu"
23
25
import Navbar from "../../components/Common/Navbar"
24
26
27
+ const usersSearchSchema = z . object ( {
28
+ page : z . number ( ) . catch ( 1 ) ,
29
+ } )
30
+
25
31
export const Route = createFileRoute ( "/_layout/admin" ) ( {
26
32
component : Admin ,
33
+ validateSearch : ( search ) => usersSearchSchema . parse ( search ) ,
27
34
} )
28
35
29
- const MembersTableBody = ( ) => {
36
+ const PER_PAGE = 5
37
+
38
+ function getUsersQueryOptions ( { page } : { page : number } ) {
39
+ return {
40
+ queryFn : ( ) =>
41
+ UsersService . readUsers ( { skip : ( page - 1 ) * PER_PAGE , limit : PER_PAGE } ) ,
42
+ queryKey : [ "users" , { page } ] ,
43
+ }
44
+ }
45
+
46
+ function UsersTable ( ) {
30
47
const queryClient = useQueryClient ( )
31
48
const currentUser = queryClient . getQueryData < UserPublic > ( [ "currentUser" ] )
49
+ const { page } = Route . useSearch ( )
50
+ const navigate = useNavigate ( { from : Route . fullPath } )
51
+ const setPage = ( page : number ) =>
52
+ navigate ( { search : ( prev ) => ( { ...prev , page } ) } )
32
53
33
- const { data : users } = useSuspenseQuery ( {
34
- queryKey : [ "users" ] ,
35
- queryFn : ( ) => UsersService . readUsers ( { } ) ,
54
+ const {
55
+ data : users ,
56
+ isPending,
57
+ isPlaceholderData,
58
+ } = useQuery ( {
59
+ ...getUsersQueryOptions ( { page } ) ,
60
+ placeholderData : ( prevData ) => prevData ,
36
61
} )
37
62
38
- return (
39
- < Tbody >
40
- { users . data . map ( ( user ) => (
41
- < Tr key = { user . id } >
42
- < Td color = { ! user . full_name ? "ui.dim" : "inherit" } >
43
- { user . full_name || "N/A" }
44
- { currentUser ?. id === user . id && (
45
- < Badge ml = "1" colorScheme = "teal" >
46
- You
47
- </ Badge >
48
- ) }
49
- </ Td >
50
- < Td > { user . email } </ Td >
51
- < Td > { user . is_superuser ? "Superuser" : "User" } </ Td >
52
- < Td >
53
- < Flex gap = { 2 } >
54
- < Box
55
- w = "2"
56
- h = "2"
57
- borderRadius = "50%"
58
- bg = { user . is_active ? "ui.success" : "ui.danger" }
59
- alignSelf = "center"
60
- />
61
- { user . is_active ? "Active" : "Inactive" }
62
- </ Flex >
63
- </ Td >
64
- < Td >
65
- < ActionsMenu
66
- type = "User"
67
- value = { user }
68
- disabled = { currentUser ?. id === user . id ? true : false }
69
- />
70
- </ Td >
71
- </ Tr >
72
- ) ) }
73
- </ Tbody >
74
- )
75
- }
63
+ const hasNextPage = ! isPlaceholderData && users ?. data . length === PER_PAGE
64
+ const hasPreviousPage = page > 1
76
65
77
- const MembersBodySkeleton = ( ) => {
78
- return (
79
- < Tbody >
80
- < Tr >
81
- { new Array ( 5 ) . fill ( null ) . map ( ( _ , index ) => (
82
- < Td key = { index } >
83
- < SkeletonText noOfLines = { 1 } paddingBlock = "16px" />
84
- </ Td >
85
- ) ) }
86
- </ Tr >
87
- </ Tbody >
88
- )
89
- }
66
+ useEffect ( ( ) => {
67
+ if ( hasNextPage ) {
68
+ queryClient . prefetchQuery ( getUsersQueryOptions ( { page : page + 1 } ) )
69
+ }
70
+ } , [ page , queryClient , hasNextPage ] )
90
71
91
- function Admin ( ) {
92
72
return (
93
- < Container maxW = "full" >
94
- < Heading size = "lg" textAlign = { { base : "center" , md : "left" } } pt = { 12 } >
95
- User Management
96
- </ Heading >
97
- < Navbar type = { "User" } addModalAs = { AddUser } />
73
+ < >
98
74
< TableContainer >
99
- < Table fontSize = "md" size = { { base : "sm" , md : "md" } } >
75
+ < Table size = { { base : "sm" , md : "md" } } >
100
76
< Thead >
101
77
< Tr >
102
78
< Th width = "20%" > Full name</ Th >
@@ -106,11 +82,89 @@ function Admin() {
106
82
< Th width = "10%" > Actions</ Th >
107
83
</ Tr >
108
84
</ Thead >
109
- < Suspense fallback = { < MembersBodySkeleton /> } >
110
- < MembersTableBody />
111
- </ Suspense >
85
+ { isPending ? (
86
+ < Tbody >
87
+ < Tr >
88
+ { new Array ( 4 ) . fill ( null ) . map ( ( _ , index ) => (
89
+ < Td key = { index } >
90
+ < SkeletonText noOfLines = { 1 } paddingBlock = "16px" />
91
+ </ Td >
92
+ ) ) }
93
+ </ Tr >
94
+ </ Tbody >
95
+ ) : (
96
+ < Tbody >
97
+ { users ?. data . map ( ( user ) => (
98
+ < Tr key = { user . id } >
99
+ < Td
100
+ color = { ! user . full_name ? "ui.dim" : "inherit" }
101
+ isTruncated
102
+ maxWidth = "150px"
103
+ >
104
+ { user . full_name || "N/A" }
105
+ { currentUser ?. id === user . id && (
106
+ < Badge ml = "1" colorScheme = "teal" >
107
+ You
108
+ </ Badge >
109
+ ) }
110
+ </ Td >
111
+ < Td isTruncated maxWidth = "150px" >
112
+ { user . email }
113
+ </ Td >
114
+ < Td > { user . is_superuser ? "Superuser" : "User" } </ Td >
115
+ < Td >
116
+ < Flex gap = { 2 } >
117
+ < Box
118
+ w = "2"
119
+ h = "2"
120
+ borderRadius = "50%"
121
+ bg = { user . is_active ? "ui.success" : "ui.danger" }
122
+ alignSelf = "center"
123
+ />
124
+ { user . is_active ? "Active" : "Inactive" }
125
+ </ Flex >
126
+ </ Td >
127
+ < Td >
128
+ < ActionsMenu
129
+ type = "User"
130
+ value = { user }
131
+ disabled = { currentUser ?. id === user . id ? true : false }
132
+ />
133
+ </ Td >
134
+ </ Tr >
135
+ ) ) }
136
+ </ Tbody >
137
+ ) }
112
138
</ Table >
113
139
</ TableContainer >
140
+ < Flex
141
+ gap = { 4 }
142
+ alignItems = "center"
143
+ mt = { 4 }
144
+ direction = "row"
145
+ justifyContent = "flex-end"
146
+ >
147
+ < Button onClick = { ( ) => setPage ( page - 1 ) } isDisabled = { ! hasPreviousPage } >
148
+ Previous
149
+ </ Button >
150
+ < span > Page { page } </ span >
151
+ < Button isDisabled = { ! hasNextPage } onClick = { ( ) => setPage ( page + 1 ) } >
152
+ Next
153
+ </ Button >
154
+ </ Flex >
155
+ </ >
156
+ )
157
+ }
158
+
159
+ function Admin ( ) {
160
+ return (
161
+ < Container maxW = "full" >
162
+ < Heading size = "lg" textAlign = { { base : "center" , md : "left" } } pt = { 12 } >
163
+ Users Management
164
+ </ Heading >
165
+
166
+ < Navbar type = { "User" } addModalAs = { AddUser } />
167
+ < UsersTable />
114
168
</ Container >
115
169
)
116
170
}
0 commit comments