Skip to content

Commit

Permalink
add pagination
Browse files Browse the repository at this point in the history
  • Loading branch information
alan2207 committed Aug 11, 2024
1 parent df2aa6c commit 9a99921
Show file tree
Hide file tree
Showing 25 changed files with 436 additions and 116 deletions.
4 changes: 2 additions & 2 deletions apps/react-vite/src/app/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ export const createAppRouter = (queryClient: QueryClient) =>
);
return { Component: DiscussionsRoute };
},
loader: async () => {
loader: async (args: LoaderFunctionArgs) => {
const { discussionsLoader } = await import(
'./routes/app/discussions/discussions'
);
return discussionsLoader(queryClient)();
return discussionsLoader(queryClient)(args);
},
},
{
Expand Down
12 changes: 7 additions & 5 deletions apps/react-vite/src/app/routes/app/discussions/discussion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useParams, LoaderFunctionArgs } from 'react-router-dom';

import { ContentLayout } from '@/components/layouts';
import { Spinner } from '@/components/ui/spinner';
import { getCommentsQueryOptions } from '@/features/comments/api/get-comments';
import { getInfiniteCommentsQueryOptions } from '@/features/comments/api/get-comments';
import { Comments } from '@/features/comments/components/comments';
import {
useDiscussion,
Expand All @@ -18,13 +18,13 @@ export const discussionLoader =
const discussionId = params.discussionId as string;

const discussionQuery = getDiscussionQueryOptions(discussionId);
const commentsQuery = getCommentsQueryOptions(discussionId);
const commentsQuery = getInfiniteCommentsQueryOptions(discussionId);

const promises = [
queryClient.getQueryData(discussionQuery.queryKey) ??
(await queryClient.fetchQuery(discussionQuery)),
queryClient.getQueryData(commentsQuery.queryKey) ??
(await queryClient.fetchQuery(commentsQuery)),
(await queryClient.fetchInfiniteQuery(commentsQuery)),
] as const;

const [discussion, comments] = await Promise.all(promises);
Expand All @@ -50,11 +50,13 @@ export const DiscussionRoute = () => {
);
}

if (!discussionQuery.data) return null;
const discussion = discussionQuery.data?.data;

if (!discussion) return null;

return (
<>
<ContentLayout title={discussionQuery.data.title}>
<ContentLayout title={discussion.title}>
<DiscussionView discussionId={discussionId} />
<div className="mt-8">
<ErrorBoundary
Expand Down
27 changes: 18 additions & 9 deletions apps/react-vite/src/app/routes/app/discussions/discussions.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import { QueryClient, useQueryClient } from '@tanstack/react-query';
import { LoaderFunctionArgs } from 'react-router-dom';

import { ContentLayout } from '@/components/layouts';
import { getCommentsQueryOptions } from '@/features/comments/api/get-comments';
import { getInfiniteCommentsQueryOptions } from '@/features/comments/api/get-comments';
import { getDiscussionsQueryOptions } from '@/features/discussions/api/get-discussions';
import { CreateDiscussion } from '@/features/discussions/components/create-discussion';
import { DiscussionsList } from '@/features/discussions/components/discussions-list';

export const discussionsLoader = (queryClient: QueryClient) => async () => {
const query = getDiscussionsQueryOptions();
export const discussionsLoader =
(queryClient: QueryClient) =>
async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url);

return (
queryClient.getQueryData(query.queryKey) ??
(await queryClient.fetchQuery(query))
);
};
const page = Number(url.searchParams.get('page') || 1);

const query = getDiscussionsQueryOptions({ page });

return (
queryClient.getQueryData(query.queryKey) ??
(await queryClient.fetchQuery(query))
);
};

export const DiscussionsRoute = () => {
const queryClient = useQueryClient();
Expand All @@ -26,7 +33,9 @@ export const DiscussionsRoute = () => {
<DiscussionsList
onDiscussionPrefetch={(id) => {
// Prefetch the comments data when the user hovers over the link in the list
queryClient.prefetchQuery(getCommentsQueryOptions(id));
queryClient.prefetchInfiniteQuery(
getInfiniteCommentsQueryOptions(id),
);
}}
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/react-vite/src/app/routes/auth/register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const RegisterRoute = () => {
}
chooseTeam={chooseTeam}
setChooseTeam={() => setChooseTeam(!chooseTeam)}
teams={teamsQuery.data}
teams={teamsQuery.data?.data}
/>
</AuthLayout>
);
Expand Down
188 changes: 188 additions & 0 deletions apps/react-vite/src/components/ui/table/pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import {
ChevronLeftIcon,
ChevronRightIcon,
DotsHorizontalIcon,
} from '@radix-ui/react-icons';
import * as React from 'react';

import { ButtonProps, buttonVariants } from '@/components/ui/button';
import { cn } from '@/utils/cn';

import { Link } from '../link';

const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
<nav
role="navigation"
aria-label="pagination"
className={cn('mx-auto flex w-full justify-center', className)}
{...props}
/>
);
Pagination.displayName = 'Pagination';

const PaginationContent = React.forwardRef<
HTMLUListElement,
React.ComponentProps<'ul'>
>(({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn('flex flex-row items-center gap-1', className)}
{...props}
/>
));
PaginationContent.displayName = 'PaginationContent';

const PaginationItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<'li'>
>(({ className, ...props }, ref) => (
<li ref={ref} className={cn('', className)} {...props} />
));
PaginationItem.displayName = 'PaginationItem';

type PaginationLinkProps = {
isActive?: boolean;
} & Pick<ButtonProps, 'size'> &
React.ComponentProps<'a'>;

const PaginationLink = ({
className,
isActive,
size = 'icon',
children,
href,
...props
}: PaginationLinkProps) => (
<Link
to={href as string}
aria-current={isActive ? 'page' : undefined}
className={cn(
buttonVariants({
variant: isActive ? 'outline' : 'ghost',
size,
}),
className,
)}
{...props}
>
{children}
</Link>
);
PaginationLink.displayName = 'PaginationLink';

const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn('gap-1 pl-2.5', className)}
{...props}
>
<ChevronLeftIcon className="size-4" />
<span>Previous</span>
</PaginationLink>
);
PaginationPrevious.displayName = 'PaginationPrevious';

const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn('gap-1 pr-2.5', className)}
{...props}
>
<span>Next</span>
<ChevronRightIcon className="size-4" />
</PaginationLink>
);
PaginationNext.displayName = 'PaginationNext';

const PaginationEllipsis = ({
className,
...props
}: React.ComponentProps<'span'>) => (
<span
aria-hidden
className={cn('flex h-9 w-9 items-center justify-center', className)}
{...props}
>
<DotsHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span>
</span>
);
PaginationEllipsis.displayName = 'PaginationEllipsis';

export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
};

export type TablePaginationProps = {
totalPages: number;
currentPage: number;
rootUrl: string;
};

export const TablePagination = ({
totalPages,
currentPage,
rootUrl,
}: TablePaginationProps) => {
const createHref = (page: number) => `${rootUrl}?page=${page}`;

return (
<Pagination className="justify-end py-8">
<PaginationContent>
{currentPage > 1 && (
<PaginationItem>
<PaginationPrevious href={createHref(currentPage - 1)} />
</PaginationItem>
)}
{currentPage > 2 && (
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
)}
{currentPage > 1 && (
<PaginationItem>
<PaginationLink href={createHref(currentPage - 1)}>
{currentPage - 1}
</PaginationLink>
</PaginationItem>
)}
<PaginationItem className="rounded-sm bg-gray-200">
<PaginationLink href={createHref(currentPage)}>
{currentPage}
</PaginationLink>
</PaginationItem>
{totalPages > currentPage && (
<PaginationItem>
<PaginationLink href={createHref(currentPage + 1)}>
{currentPage + 1}
</PaginationLink>
</PaginationItem>
)}
{totalPages > currentPage + 1 && (
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
)}
{currentPage < totalPages && (
<PaginationItem>
<PaginationNext href={createHref(totalPages)} />
</PaginationItem>
)}
</PaginationContent>
</Pagination>
);
};
44 changes: 26 additions & 18 deletions apps/react-vite/src/components/ui/table/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import * as React from 'react';
import { BaseEntity } from '@/types/api';
import { cn } from '@/utils/cn';

import { TablePagination, TablePaginationProps } from './pagination';

const TableElement = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
Expand Down Expand Up @@ -130,11 +132,13 @@ type TableColumn<Entry> = {
export type TableProps<Entry> = {
data: Entry[];
columns: TableColumn<Entry>[];
pagination?: TablePaginationProps;
};

export const Table = <Entry extends BaseEntity>({
data,
columns,
pagination,
}: TableProps<Entry>) => {
if (!data?.length) {
return (
Expand All @@ -145,25 +149,29 @@ export const Table = <Entry extends BaseEntity>({
);
}
return (
<TableElement>
<TableHeader>
<TableRow>
{columns.map((column, index) => (
<TableHead key={column.title + index}>{column.title}</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{data.map((entry, entryIndex) => (
<TableRow key={entry?.id || entryIndex}>
{columns.map(({ Cell, field, title }, columnIndex) => (
<TableCell key={title + columnIndex}>
{Cell ? <Cell entry={entry} /> : `${entry[field]}`}
</TableCell>
<>
<TableElement>
<TableHeader>
<TableRow>
{columns.map((column, index) => (
<TableHead key={column.title + index}>{column.title}</TableHead>
))}
</TableRow>
))}
</TableBody>
</TableElement>
</TableHeader>
<TableBody>
{data.map((entry, entryIndex) => (
<TableRow key={entry?.id || entryIndex}>
{columns.map(({ Cell, field, title }, columnIndex) => (
<TableCell key={title + columnIndex}>
{Cell ? <Cell entry={entry} /> : `${entry[field]}`}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</TableElement>

{pagination && <TablePagination {...pagination} />}
</>
);
};
4 changes: 2 additions & 2 deletions apps/react-vite/src/features/comments/api/create-comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { api } from '@/lib/api-client';
import { MutationConfig } from '@/lib/react-query';
import { Comment } from '@/types/api';

import { getCommentsQueryOptions } from './get-comments';
import { getInfiniteCommentsQueryOptions } from './get-comments';

export const createCommentInputSchema = z.object({
discussionId: z.string().min(1, 'Required'),
Expand Down Expand Up @@ -38,7 +38,7 @@ export const useCreateComment = ({
return useMutation({
onSuccess: (...args) => {
queryClient.invalidateQueries({
queryKey: getCommentsQueryOptions(discussionId).queryKey,
queryKey: getInfiniteCommentsQueryOptions(discussionId).queryKey,
});
onSuccess?.(...args);
},
Expand Down
Loading

0 comments on commit 9a99921

Please sign in to comment.