|
|
@ -1,6 +1,7 @@ |
|
|
|
import { |
|
|
|
Badge, |
|
|
|
Box, |
|
|
|
Button, |
|
|
|
Container, |
|
|
|
Flex, |
|
|
|
Heading, |
|
|
@ -13,90 +14,65 @@ import { |
|
|
|
Thead, |
|
|
|
Tr, |
|
|
|
} from "@chakra-ui/react" |
|
|
|
import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query" |
|
|
|
import { createFileRoute } from "@tanstack/react-router" |
|
|
|
import { useQuery, useQueryClient } from "@tanstack/react-query" |
|
|
|
import { createFileRoute, useNavigate } from "@tanstack/react-router" |
|
|
|
import { useEffect } from "react" |
|
|
|
import { z } from "zod" |
|
|
|
|
|
|
|
import { Suspense } from "react" |
|
|
|
import { type UserPublic, UsersService } from "../../client" |
|
|
|
import AddUser from "../../components/Admin/AddUser" |
|
|
|
import ActionsMenu from "../../components/Common/ActionsMenu" |
|
|
|
import Navbar from "../../components/Common/Navbar" |
|
|
|
|
|
|
|
const usersSearchSchema = z.object({ |
|
|
|
page: z.number().catch(1), |
|
|
|
}) |
|
|
|
|
|
|
|
export const Route = createFileRoute("/_layout/admin")({ |
|
|
|
component: Admin, |
|
|
|
validateSearch: (search) => usersSearchSchema.parse(search), |
|
|
|
}) |
|
|
|
|
|
|
|
const MembersTableBody = () => { |
|
|
|
const PER_PAGE = 5 |
|
|
|
|
|
|
|
function getUsersQueryOptions({ page }: { page: number }) { |
|
|
|
return { |
|
|
|
queryFn: () => |
|
|
|
UsersService.readUsers({ skip: (page - 1) * PER_PAGE, limit: PER_PAGE }), |
|
|
|
queryKey: ["users", { page }], |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function UsersTable() { |
|
|
|
const queryClient = useQueryClient() |
|
|
|
const currentUser = queryClient.getQueryData<UserPublic>(["currentUser"]) |
|
|
|
const { page } = Route.useSearch() |
|
|
|
const navigate = useNavigate({ from: Route.fullPath }) |
|
|
|
const setPage = (page: number) => |
|
|
|
navigate({ search: (prev) => ({ ...prev, page }) }) |
|
|
|
|
|
|
|
const { data: users } = useSuspenseQuery({ |
|
|
|
queryKey: ["users"], |
|
|
|
queryFn: () => UsersService.readUsers({}), |
|
|
|
const { |
|
|
|
data: users, |
|
|
|
isPending, |
|
|
|
isPlaceholderData, |
|
|
|
} = useQuery({ |
|
|
|
...getUsersQueryOptions({ page }), |
|
|
|
placeholderData: (prevData) => prevData, |
|
|
|
}) |
|
|
|
|
|
|
|
return ( |
|
|
|
<Tbody> |
|
|
|
{users.data.map((user) => ( |
|
|
|
<Tr key={user.id}> |
|
|
|
<Td color={!user.full_name ? "ui.dim" : "inherit"}> |
|
|
|
{user.full_name || "N/A"} |
|
|
|
{currentUser?.id === user.id && ( |
|
|
|
<Badge ml="1" colorScheme="teal"> |
|
|
|
You |
|
|
|
</Badge> |
|
|
|
)} |
|
|
|
</Td> |
|
|
|
<Td>{user.email}</Td> |
|
|
|
<Td>{user.is_superuser ? "Superuser" : "User"}</Td> |
|
|
|
<Td> |
|
|
|
<Flex gap={2}> |
|
|
|
<Box |
|
|
|
w="2" |
|
|
|
h="2" |
|
|
|
borderRadius="50%" |
|
|
|
bg={user.is_active ? "ui.success" : "ui.danger"} |
|
|
|
alignSelf="center" |
|
|
|
/> |
|
|
|
{user.is_active ? "Active" : "Inactive"} |
|
|
|
</Flex> |
|
|
|
</Td> |
|
|
|
<Td> |
|
|
|
<ActionsMenu |
|
|
|
type="User" |
|
|
|
value={user} |
|
|
|
disabled={currentUser?.id === user.id ? true : false} |
|
|
|
/> |
|
|
|
</Td> |
|
|
|
</Tr> |
|
|
|
))} |
|
|
|
</Tbody> |
|
|
|
) |
|
|
|
} |
|
|
|
const hasNextPage = !isPlaceholderData && users?.data.length === PER_PAGE |
|
|
|
const hasPreviousPage = page > 1 |
|
|
|
|
|
|
|
const MembersBodySkeleton = () => { |
|
|
|
return ( |
|
|
|
<Tbody> |
|
|
|
<Tr> |
|
|
|
{new Array(5).fill(null).map((_, index) => ( |
|
|
|
<Td key={index}> |
|
|
|
<SkeletonText noOfLines={1} paddingBlock="16px" /> |
|
|
|
</Td> |
|
|
|
))} |
|
|
|
</Tr> |
|
|
|
</Tbody> |
|
|
|
) |
|
|
|
} |
|
|
|
useEffect(() => { |
|
|
|
if (hasNextPage) { |
|
|
|
queryClient.prefetchQuery(getUsersQueryOptions({ page: page + 1 })) |
|
|
|
} |
|
|
|
}, [page, queryClient, hasNextPage]) |
|
|
|
|
|
|
|
function Admin() { |
|
|
|
return ( |
|
|
|
<Container maxW="full"> |
|
|
|
<Heading size="lg" textAlign={{ base: "center", md: "left" }} pt={12}> |
|
|
|
User Management |
|
|
|
</Heading> |
|
|
|
<Navbar type={"User"} addModalAs={AddUser} /> |
|
|
|
<> |
|
|
|
<TableContainer> |
|
|
|
<Table fontSize="md" size={{ base: "sm", md: "md" }}> |
|
|
|
<Table size={{ base: "sm", md: "md" }}> |
|
|
|
<Thead> |
|
|
|
<Tr> |
|
|
|
<Th width="20%">Full name</Th> |
|
|
@ -106,11 +82,89 @@ function Admin() { |
|
|
|
<Th width="10%">Actions</Th> |
|
|
|
</Tr> |
|
|
|
</Thead> |
|
|
|
<Suspense fallback={<MembersBodySkeleton />}> |
|
|
|
<MembersTableBody /> |
|
|
|
</Suspense> |
|
|
|
{isPending ? ( |
|
|
|
<Tbody> |
|
|
|
<Tr> |
|
|
|
{new Array(4).fill(null).map((_, index) => ( |
|
|
|
<Td key={index}> |
|
|
|
<SkeletonText noOfLines={1} paddingBlock="16px" /> |
|
|
|
</Td> |
|
|
|
))} |
|
|
|
</Tr> |
|
|
|
</Tbody> |
|
|
|
) : ( |
|
|
|
<Tbody> |
|
|
|
{users?.data.map((user) => ( |
|
|
|
<Tr key={user.id}> |
|
|
|
<Td |
|
|
|
color={!user.full_name ? "ui.dim" : "inherit"} |
|
|
|
isTruncated |
|
|
|
maxWidth="150px" |
|
|
|
> |
|
|
|
{user.full_name || "N/A"} |
|
|
|
{currentUser?.id === user.id && ( |
|
|
|
<Badge ml="1" colorScheme="teal"> |
|
|
|
You |
|
|
|
</Badge> |
|
|
|
)} |
|
|
|
</Td> |
|
|
|
<Td isTruncated maxWidth="150px"> |
|
|
|
{user.email} |
|
|
|
</Td> |
|
|
|
<Td>{user.is_superuser ? "Superuser" : "User"}</Td> |
|
|
|
<Td> |
|
|
|
<Flex gap={2}> |
|
|
|
<Box |
|
|
|
w="2" |
|
|
|
h="2" |
|
|
|
borderRadius="50%" |
|
|
|
bg={user.is_active ? "ui.success" : "ui.danger"} |
|
|
|
alignSelf="center" |
|
|
|
/> |
|
|
|
{user.is_active ? "Active" : "Inactive"} |
|
|
|
</Flex> |
|
|
|
</Td> |
|
|
|
<Td> |
|
|
|
<ActionsMenu |
|
|
|
type="User" |
|
|
|
value={user} |
|
|
|
disabled={currentUser?.id === user.id ? true : false} |
|
|
|
/> |
|
|
|
</Td> |
|
|
|
</Tr> |
|
|
|
))} |
|
|
|
</Tbody> |
|
|
|
)} |
|
|
|
</Table> |
|
|
|
</TableContainer> |
|
|
|
<Flex |
|
|
|
gap={4} |
|
|
|
alignItems="center" |
|
|
|
mt={4} |
|
|
|
direction="row" |
|
|
|
justifyContent="flex-end" |
|
|
|
> |
|
|
|
<Button onClick={() => setPage(page - 1)} isDisabled={!hasPreviousPage}> |
|
|
|
Previous |
|
|
|
</Button> |
|
|
|
<span>Page {page}</span> |
|
|
|
<Button isDisabled={!hasNextPage} onClick={() => setPage(page + 1)}> |
|
|
|
Next |
|
|
|
</Button> |
|
|
|
</Flex> |
|
|
|
</> |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
function Admin() { |
|
|
|
return ( |
|
|
|
<Container maxW="full"> |
|
|
|
<Heading size="lg" textAlign={{ base: "center", md: "left" }} pt={12}> |
|
|
|
Users Management |
|
|
|
</Heading> |
|
|
|
|
|
|
|
<Navbar type={"User"} addModalAs={AddUser} /> |
|
|
|
<UsersTable /> |
|
|
|
</Container> |
|
|
|
) |
|
|
|
} |
|
|
|