Skip to content

Commit 0d7886e

Browse files
committed
PEER-248: Add questions table and pagination
1 parent b50b1f0 commit 0d7886e

File tree

12 files changed

+528
-1
lines changed

12 files changed

+528
-1
lines changed

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@radix-ui/react-tabs": "^1.1.0",
2222
"@tanstack/react-query": "^5.56.2",
2323
"@tanstack/react-query-devtools": "^5.56.2",
24+
"@tanstack/react-table": "^8.20.5",
2425
"axios": "^1.7.7",
2526
"class-variance-authority": "^0.7.0",
2627
"clsx": "^2.1.1",
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import * as React from 'react';
2+
import { ChevronLeftIcon, ChevronRightIcon, DotsHorizontalIcon } from '@radix-ui/react-icons';
3+
4+
import { cn } from '@/lib/utils';
5+
import { ButtonProps, buttonVariants } from '@/components/ui/button';
6+
7+
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
8+
<nav
9+
role='navigation'
10+
aria-label='pagination'
11+
className={cn('mx-auto flex w-full justify-center', className)}
12+
{...props}
13+
/>
14+
);
15+
Pagination.displayName = 'Pagination';
16+
17+
const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<'ul'>>(
18+
({ className, ...props }, ref) => (
19+
<ul ref={ref} className={cn('flex flex-row items-center gap-1', className)} {...props} />
20+
)
21+
);
22+
PaginationContent.displayName = 'PaginationContent';
23+
24+
const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<'li'>>(
25+
({ className, ...props }, ref) => <li ref={ref} className={cn('', className)} {...props} />
26+
);
27+
PaginationItem.displayName = 'PaginationItem';
28+
29+
type PaginationLinkProps = {
30+
isActive?: boolean;
31+
} & Pick<ButtonProps, 'size'> &
32+
React.ComponentProps<'a'>;
33+
34+
const PaginationLink = ({ className, isActive, size = 'icon', ...props }: PaginationLinkProps) => (
35+
<a
36+
aria-current={isActive ? 'page' : undefined}
37+
className={cn(
38+
buttonVariants({
39+
variant: isActive ? 'outline' : 'ghost',
40+
size,
41+
}),
42+
className
43+
)}
44+
{...props}
45+
/>
46+
);
47+
PaginationLink.displayName = 'PaginationLink';
48+
49+
const PaginationPrevious = ({
50+
className,
51+
...props
52+
}: React.ComponentProps<typeof PaginationLink>) => (
53+
<PaginationLink
54+
aria-label='Go to previous page'
55+
size='default'
56+
className={cn('gap-1 pl-2.5', className)}
57+
{...props}
58+
>
59+
<ChevronLeftIcon className='size-4' />
60+
<span>Previous</span>
61+
</PaginationLink>
62+
);
63+
PaginationPrevious.displayName = 'PaginationPrevious';
64+
65+
const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
66+
<PaginationLink
67+
aria-label='Go to next page'
68+
size='default'
69+
className={cn('gap-1 pr-2.5', className)}
70+
{...props}
71+
>
72+
<span>Next</span>
73+
<ChevronRightIcon className='size-4' />
74+
</PaginationLink>
75+
);
76+
PaginationNext.displayName = 'PaginationNext';
77+
78+
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => (
79+
<span
80+
aria-hidden
81+
className={cn('flex h-9 w-9 items-center justify-center', className)}
82+
{...props}
83+
>
84+
<DotsHorizontalIcon className='size-4' />
85+
<span className='sr-only'>More pages</span>
86+
</span>
87+
);
88+
PaginationEllipsis.displayName = 'PaginationEllipsis';
89+
90+
export {
91+
Pagination,
92+
PaginationContent,
93+
PaginationLink,
94+
PaginationItem,
95+
PaginationPrevious,
96+
PaginationNext,
97+
PaginationEllipsis,
98+
};
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import * as React from 'react';
2+
3+
import { cn } from '@/lib/utils';
4+
5+
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
6+
({ className, ...props }, ref) => (
7+
<div className='relative w-full overflow-auto'>
8+
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
9+
</div>
10+
)
11+
);
12+
Table.displayName = 'Table';
13+
14+
const TableHeader = React.forwardRef<
15+
HTMLTableSectionElement,
16+
React.HTMLAttributes<HTMLTableSectionElement>
17+
>(({ className, ...props }, ref) => (
18+
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
19+
));
20+
TableHeader.displayName = 'TableHeader';
21+
22+
const TableBody = React.forwardRef<
23+
HTMLTableSectionElement,
24+
React.HTMLAttributes<HTMLTableSectionElement>
25+
>(({ className, ...props }, ref) => (
26+
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
27+
));
28+
TableBody.displayName = 'TableBody';
29+
30+
const TableFooter = React.forwardRef<
31+
HTMLTableSectionElement,
32+
React.HTMLAttributes<HTMLTableSectionElement>
33+
>(({ className, ...props }, ref) => (
34+
<tfoot
35+
ref={ref}
36+
className={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)}
37+
{...props}
38+
/>
39+
));
40+
TableFooter.displayName = 'TableFooter';
41+
42+
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
43+
({ className, ...props }, ref) => (
44+
<tr
45+
ref={ref}
46+
className={cn(
47+
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
48+
className
49+
)}
50+
{...props}
51+
/>
52+
)
53+
);
54+
TableRow.displayName = 'TableRow';
55+
56+
const TableHead = React.forwardRef<
57+
HTMLTableCellElement,
58+
React.ThHTMLAttributes<HTMLTableCellElement>
59+
>(({ className, ...props }, ref) => (
60+
<th
61+
ref={ref}
62+
className={cn(
63+
'h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
64+
className
65+
)}
66+
{...props}
67+
/>
68+
));
69+
TableHead.displayName = 'TableHead';
70+
71+
const TableCell = React.forwardRef<
72+
HTMLTableCellElement,
73+
React.TdHTMLAttributes<HTMLTableCellElement>
74+
>(({ className, ...props }, ref) => (
75+
<td
76+
ref={ref}
77+
className={cn(
78+
'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
79+
className
80+
)}
81+
{...props}
82+
/>
83+
));
84+
TableCell.displayName = 'TableCell';
85+
86+
const TableCaption = React.forwardRef<
87+
HTMLTableCaptionElement,
88+
React.HTMLAttributes<HTMLTableCaptionElement>
89+
>(({ className, ...props }, ref) => (
90+
<caption ref={ref} className={cn('mt-4 text-sm text-muted-foreground', className)} {...props} />
91+
));
92+
TableCaption.displayName = 'TableCaption';
93+
94+
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };

frontend/src/lib/router.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Login } from '@/routes/login';
77
import { Root } from '@/routes/root';
88
import { SignUp } from '@/routes/signup';
99
import { ROUTES } from './routes';
10+
import Questions from '@/routes/question/main';
1011

1112
export const router = createBrowserRouter([
1213
{
@@ -20,6 +21,14 @@ export const router = createBrowserRouter([
2021
</PrivateRoute>
2122
),
2223
},
24+
{
25+
path: ROUTES.QUESTIONS,
26+
element: (
27+
<PrivateRoute>
28+
<Questions />
29+
</PrivateRoute>
30+
),
31+
},
2332
{
2433
path: ROUTES.LOGIN,
2534
element: <Login />,

frontend/src/lib/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export const ROUTES = {
33
LOGIN: '/login',
44
SIGNUP: '/signup',
55
FORGOT_PASSWORD: '/forgot-password',
6+
QUESTIONS: '/questions',
67
};
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Badge } from '@/components/ui/badge';
2+
import { CheckCircledIcon } from '@radix-ui/react-icons';
3+
import { ColumnDef } from '@tanstack/react-table';
4+
import { Question } from './logic';
5+
6+
export const columns: ColumnDef<Question>[] = [
7+
{
8+
accessorKey: 'attempted',
9+
header: 'Attempted',
10+
cell: ({ row }) => {
11+
const attempted = row.getValue<boolean>('attempted');
12+
return <div className='ml-3'>{attempted && <CheckCircledIcon />}</div>;
13+
},
14+
},
15+
{
16+
accessorKey: 'name',
17+
header: 'Name',
18+
},
19+
{
20+
accessorKey: 'difficulty',
21+
header: 'Difficulty',
22+
},
23+
{
24+
accessorKey: 'topic',
25+
header: 'Topics',
26+
cell: ({ row }) => {
27+
const topics: string[] = row.getValue('topic');
28+
return (
29+
<div>
30+
{topics.map((topic) => (
31+
<Badge className='mr-1 text-xs' variant='outline'>
32+
{topic}
33+
</Badge>
34+
))}
35+
</div>
36+
);
37+
},
38+
},
39+
];

frontend/src/routes/question/index.ts

Whitespace-only changes.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { IGetQuestionsResponse } from '../../types/question-types';
2+
3+
export type Question = {
4+
number: number;
5+
name: string;
6+
difficulty: 'Easy' | 'Medium' | 'Hard';
7+
topic: Array<string>;
8+
attempted: boolean;
9+
};
10+
11+
export async function fetchQuestions(): Promise<IGetQuestionsResponse> {
12+
const dummyData: Question[] = [
13+
{
14+
number: 1,
15+
attempted: true,
16+
name: 'Basic Arrays',
17+
difficulty: 'Easy',
18+
topic: ['Arrays', 'Basic'],
19+
},
20+
{
21+
number: 2,
22+
attempted: false,
23+
name: 'Sorting Algorithms',
24+
difficulty: 'Medium',
25+
topic: ['Algorithms', 'Sorting'],
26+
},
27+
{
28+
number: 3,
29+
attempted: true,
30+
name: 'Graph Traversal',
31+
difficulty: 'Hard',
32+
topic: ['Graphs', 'DFS', 'BFS'],
33+
},
34+
{
35+
number: 4,
36+
attempted: false,
37+
name: 'Dynamic Programming',
38+
difficulty: 'Hard',
39+
topic: ['Dynamic Programming'],
40+
},
41+
{
42+
number: 5,
43+
attempted: true,
44+
name: 'Binary Search Trees',
45+
difficulty: 'Medium',
46+
topic: ['Trees', 'Binary Search'],
47+
},
48+
{
49+
number: 6,
50+
attempted: false,
51+
name: 'Recursion Basics',
52+
difficulty: 'Easy',
53+
topic: ['Recursion'],
54+
},
55+
{
56+
number: 7,
57+
attempted: true,
58+
name: 'Hash Tables',
59+
difficulty: 'Easy',
60+
topic: ['Data Structures', 'Hashing'],
61+
},
62+
{
63+
number: 8,
64+
attempted: false,
65+
name: 'Graph Algorithms',
66+
difficulty: 'Medium',
67+
topic: ['Graphs', 'Shortest Path'],
68+
},
69+
{
70+
number: 9,
71+
attempted: true,
72+
name: 'Object-Oriented Programming',
73+
difficulty: 'Medium',
74+
topic: ['OOP', 'Classes', 'Inheritance'],
75+
},
76+
{
77+
number: 10,
78+
attempted: false,
79+
name: 'Concurrency Basics',
80+
difficulty: 'Hard',
81+
topic: ['Concurrency', 'Multithreading'],
82+
},
83+
{
84+
number: 11,
85+
attempted: true,
86+
name: 'Machine Learning Introduction',
87+
difficulty: 'Medium',
88+
topic: ['Machine Learning', 'Algorithms'],
89+
},
90+
{
91+
number: 12,
92+
attempted: false,
93+
name: 'Database Normalization',
94+
difficulty: 'Medium',
95+
topic: ['Databases', 'Normalization'],
96+
},
97+
];
98+
99+
return {
100+
questions: dummyData,
101+
totalQuestions: 20,
102+
};
103+
}

0 commit comments

Comments
 (0)