|
@ -1,4 +1,6 @@ |
|
|
|
|
|
import { z } from "zod" |
|
|
import { |
|
|
import { |
|
|
|
|
|
Button, |
|
|
Container, |
|
|
Container, |
|
|
Flex, |
|
|
Flex, |
|
|
Heading, |
|
|
Heading, |
|
@ -11,85 +13,118 @@ import { |
|
|
Thead, |
|
|
Thead, |
|
|
Tr, |
|
|
Tr, |
|
|
} from "@chakra-ui/react" |
|
|
} from "@chakra-ui/react" |
|
|
import { useSuspenseQuery } from "@tanstack/react-query" |
|
|
import { useQuery, useQueryClient } from "@tanstack/react-query" |
|
|
import { createFileRoute } from "@tanstack/react-router" |
|
|
import { createFileRoute, useNavigate } from "@tanstack/react-router" |
|
|
|
|
|
|
|
|
import { Suspense } from "react" |
|
|
import { useEffect } from "react" |
|
|
import { ErrorBoundary } from "react-error-boundary" |
|
|
|
|
|
import { ItemsService } from "../../client" |
|
|
import { ItemsService } from "../../client" |
|
|
import ActionsMenu from "../../components/Common/ActionsMenu" |
|
|
import ActionsMenu from "../../components/Common/ActionsMenu" |
|
|
import Navbar from "../../components/Common/Navbar" |
|
|
import Navbar from "../../components/Common/Navbar" |
|
|
|
|
|
|
|
|
|
|
|
const itemsSearchSchema = z.object({ |
|
|
|
|
|
page: z.number().catch(1), |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
export const Route = createFileRoute("/_layout/items")({ |
|
|
export const Route = createFileRoute("/_layout/items")({ |
|
|
component: Items, |
|
|
component: Items, |
|
|
|
|
|
validateSearch: (search) => itemsSearchSchema.parse(search), |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
function ItemsTableBody() { |
|
|
const PER_PAGE = 5 |
|
|
const { data: items } = useSuspenseQuery({ |
|
|
|
|
|
queryKey: ["items"], |
|
|
|
|
|
queryFn: () => ItemsService.readItems({}), |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
function getItemsQueryOptions({ page }: { page: number }) { |
|
|
<Tbody> |
|
|
return { |
|
|
{items.data.map((item) => ( |
|
|
queryFn: () => |
|
|
<Tr key={item.id}> |
|
|
ItemsService.readItems({ skip: (page - 1) * PER_PAGE, limit: PER_PAGE }), |
|
|
<Td>{item.id}</Td> |
|
|
queryKey: ["items", { page }], |
|
|
<Td>{item.title}</Td> |
|
|
} |
|
|
<Td color={!item.description ? "ui.dim" : "inherit"}> |
|
|
|
|
|
{item.description || "N/A"} |
|
|
|
|
|
</Td> |
|
|
|
|
|
<Td> |
|
|
|
|
|
<ActionsMenu type={"Item"} value={item} /> |
|
|
|
|
|
</Td> |
|
|
|
|
|
</Tr> |
|
|
|
|
|
))} |
|
|
|
|
|
</Tbody> |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function ItemsTable() { |
|
|
function ItemsTable() { |
|
|
|
|
|
const queryClient = useQueryClient() |
|
|
|
|
|
const { page } = Route.useSearch() |
|
|
|
|
|
const navigate = useNavigate({ from: Route.fullPath }) |
|
|
|
|
|
const setPage = (page: number) => |
|
|
|
|
|
navigate({ search: (prev) => ({ ...prev, page }) }) |
|
|
|
|
|
|
|
|
|
|
|
const { |
|
|
|
|
|
data: items, |
|
|
|
|
|
isPending, |
|
|
|
|
|
isPlaceholderData, |
|
|
|
|
|
} = useQuery({ |
|
|
|
|
|
...getItemsQueryOptions({ page }), |
|
|
|
|
|
placeholderData: (prevData) => prevData, |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
const hasNextPage = !isPlaceholderData && items?.data.length === PER_PAGE |
|
|
|
|
|
const hasPreviousPage = page > 1 |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
if (hasNextPage) { |
|
|
|
|
|
queryClient.prefetchQuery(getItemsQueryOptions({ page: page + 1 })) |
|
|
|
|
|
} |
|
|
|
|
|
}, [page, queryClient]) |
|
|
|
|
|
|
|
|
return ( |
|
|
return ( |
|
|
<TableContainer> |
|
|
<> |
|
|
<Table size={{ base: "sm", md: "md" }}> |
|
|
<TableContainer> |
|
|
<Thead> |
|
|
<Table size={{ base: "sm", md: "md" }}> |
|
|
<Tr> |
|
|
<Thead> |
|
|
<Th>ID</Th> |
|
|
<Tr> |
|
|
<Th>Title</Th> |
|
|
<Th>ID</Th> |
|
|
<Th>Description</Th> |
|
|
<Th>Title</Th> |
|
|
<Th>Actions</Th> |
|
|
<Th>Description</Th> |
|
|
</Tr> |
|
|
<Th>Actions</Th> |
|
|
</Thead> |
|
|
</Tr> |
|
|
<ErrorBoundary |
|
|
</Thead> |
|
|
fallbackRender={({ error }) => ( |
|
|
{isPending ? ( |
|
|
|
|
|
<Tbody> |
|
|
|
|
|
{new Array(5).fill(null).map((_, index) => ( |
|
|
|
|
|
<Tr key={index}> |
|
|
|
|
|
{new Array(4).fill(null).map((_, index) => ( |
|
|
|
|
|
<Td key={index}> |
|
|
|
|
|
<Flex> |
|
|
|
|
|
<Skeleton height="20px" width="20px" /> |
|
|
|
|
|
</Flex> |
|
|
|
|
|
</Td> |
|
|
|
|
|
))} |
|
|
|
|
|
</Tr> |
|
|
|
|
|
))} |
|
|
|
|
|
</Tbody> |
|
|
|
|
|
) : ( |
|
|
<Tbody> |
|
|
<Tbody> |
|
|
<Tr> |
|
|
{items?.data.map((item) => ( |
|
|
<Td colSpan={4}>Something went wrong: {error.message}</Td> |
|
|
<Tr key={item.id} opacity={isPlaceholderData ? 0.5 : 1}> |
|
|
</Tr> |
|
|
<Td>{item.id}</Td> |
|
|
|
|
|
<Td>{item.title}</Td> |
|
|
|
|
|
<Td color={!item.description ? "ui.dim" : "inherit"}> |
|
|
|
|
|
{item.description || "N/A"} |
|
|
|
|
|
</Td> |
|
|
|
|
|
<Td> |
|
|
|
|
|
<ActionsMenu type={"Item"} value={item} /> |
|
|
|
|
|
</Td> |
|
|
|
|
|
</Tr> |
|
|
|
|
|
))} |
|
|
</Tbody> |
|
|
</Tbody> |
|
|
)} |
|
|
)} |
|
|
> |
|
|
</Table> |
|
|
<Suspense |
|
|
</TableContainer> |
|
|
fallback={ |
|
|
<Flex |
|
|
<Tbody> |
|
|
gap={4} |
|
|
{new Array(5).fill(null).map((_, index) => ( |
|
|
alignItems="center" |
|
|
<Tr key={index}> |
|
|
mt={4} |
|
|
{new Array(4).fill(null).map((_, index) => ( |
|
|
direction="row" |
|
|
<Td key={index}> |
|
|
justifyContent="flex-end" |
|
|
<Flex> |
|
|
> |
|
|
<Skeleton height="20px" width="20px" /> |
|
|
<Button onClick={() => setPage(page - 1)} isDisabled={!hasPreviousPage}> |
|
|
</Flex> |
|
|
Previous |
|
|
</Td> |
|
|
</Button> |
|
|
))} |
|
|
<span>Page {page}</span> |
|
|
</Tr> |
|
|
<Button isDisabled={!hasNextPage} onClick={() => setPage(page + 1)}> |
|
|
))} |
|
|
Next |
|
|
</Tbody> |
|
|
</Button> |
|
|
} |
|
|
</Flex> |
|
|
> |
|
|
</> |
|
|
<ItemsTableBody /> |
|
|
|
|
|
</Suspense> |
|
|
|
|
|
</ErrorBoundary> |
|
|
|
|
|
</Table> |
|
|
|
|
|
</TableContainer> |
|
|
|
|
|
) |
|
|
) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|