Skip to content

Commit 7ac2498

Browse files
committed
PEER-219: Add Question Attempts Table
Signed-off-by: SeeuSim <[email protected]>
1 parent 4d28d5b commit 7ac2498

File tree

15 files changed

+505
-23
lines changed

15 files changed

+505
-23
lines changed

backend/question/src/services/get/get-attempts.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { and, asc, eq, or } from 'drizzle-orm';
1+
import { and, desc, eq, or } from 'drizzle-orm';
22

33
import { db, questionAttempts as QUESTION_ATTEMPTS_TABLE } from '@/lib/db';
44

@@ -23,7 +23,7 @@ export const getQuestionAttempts = async ({ questionId, userId, limit = 10, offs
2323
.select()
2424
.from(QUESTION_ATTEMPTS_TABLE)
2525
.where(and(...filterClauses))
26-
.orderBy(asc(QUESTION_ATTEMPTS_TABLE.timestamp))
26+
.orderBy(desc(QUESTION_ATTEMPTS_TABLE.timestamp))
2727
.offset(offset ?? 0)
2828
.limit(limit);
2929
};

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"@radix-ui/react-icons": "^1.3.0",
1919
"@radix-ui/react-label": "^2.1.0",
2020
"@radix-ui/react-navigation-menu": "^1.2.0",
21+
"@radix-ui/react-popover": "^1.1.2",
2122
"@radix-ui/react-scroll-area": "^1.1.0",
2223
"@radix-ui/react-select": "^2.1.1",
2324
"@radix-ui/react-separator": "^1.1.0",

frontend/src/components/blocks/interview/editor.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ export const Editor = ({ questionId, room, onAIClick, onPartnerClick }: EditorPr
133133
userId: userId as string,
134134
questionId,
135135
code,
136+
setCode,
136137
members,
137138
language,
138139
}}

frontend/src/components/blocks/interview/room/complete-dialog.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useMutation } from '@tanstack/react-query';
22
import { Loader2 } from 'lucide-react';
3-
import { FC, PropsWithChildren, useCallback, useState } from 'react';
3+
import { Dispatch, FC, PropsWithChildren, SetStateAction, useCallback, useState } from 'react';
44
import { useNavigate } from 'react-router-dom';
55

66
import { Button } from '@/components/ui/button';
@@ -22,6 +22,7 @@ type CompleteDialogProps = {
2222
code: string;
2323
language: string;
2424
members: Array<IYjsUserState['user']>;
25+
setCode: Dispatch<SetStateAction<string>>;
2526
setCompleting: (state: string, resetId?: boolean) => void;
2627
};
2728

@@ -31,6 +32,7 @@ export const CompleteDialog: FC<PropsWithChildren<CompleteDialogProps>> = ({
3132
questionId,
3233
userId,
3334
code,
35+
setCode,
3436
language,
3537
members,
3638
}) => {
@@ -62,6 +64,7 @@ export const CompleteDialog: FC<PropsWithChildren<CompleteDialogProps>> = ({
6264
});
6365
},
6466
onSuccess: () => {
67+
setCode('');
6568
setCompleting(COMPLETION_STATES.SUCCESS);
6669
// Navigate to home page
6770
setTimeout(() => {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { ColumnDef } from '@tanstack/react-table';
2+
3+
import { Badge } from '@/components/ui/badge';
4+
import { DataTableSortableHeader } from '@/components/ui/data-table';
5+
import { IQuestionAttempt } from '@/types/question-types';
6+
7+
export const columns: Array<ColumnDef<IQuestionAttempt>> = [
8+
{
9+
accessorKey: 'timestamp',
10+
header: ({ column }) => <DataTableSortableHeader column={column} title='Attempted' />,
11+
cell({ row }) {
12+
const attemptedTime = row.getValue('timestamp') as string;
13+
return <div>{new Date(attemptedTime).toLocaleString()}</div>;
14+
},
15+
},
16+
{
17+
accessorKey: 'language',
18+
header: ({ column }) => (
19+
<DataTableSortableHeader column={column} title='Language' className='ml-auto mr-3' />
20+
),
21+
cell({ row }) {
22+
return (
23+
<div>
24+
<Badge className='rounded-full' variant='secondary'>
25+
{row.getValue('language')}
26+
</Badge>
27+
</div>
28+
);
29+
},
30+
},
31+
];
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import {
2+
ArrowLeftIcon,
3+
ArrowRightIcon,
4+
DoubleArrowLeftIcon,
5+
DoubleArrowRightIcon,
6+
} from '@radix-ui/react-icons';
7+
import {
8+
ColumnDef,
9+
ColumnFiltersState,
10+
flexRender,
11+
getCoreRowModel,
12+
getFilteredRowModel,
13+
getPaginationRowModel,
14+
getSortedRowModel,
15+
SortingState,
16+
useReactTable,
17+
} from '@tanstack/react-table';
18+
import { useState } from 'react';
19+
20+
import { Button } from '@/components/ui/button';
21+
import { ComboboxMulti } from '@/components/ui/combobox';
22+
import { Pagination, PaginationContent, PaginationItem } from '@/components/ui/pagination';
23+
import { ScrollArea } from '@/components/ui/scroll-area';
24+
import {
25+
Table,
26+
TableBody,
27+
TableCell,
28+
TableFooter,
29+
TableHead,
30+
TableHeader,
31+
TableRow,
32+
} from '@/components/ui/table';
33+
import { IQuestionAttempt } from '@/types/question-types';
34+
35+
interface QuestionTableProps<TData, TValue> {
36+
columns: Array<ColumnDef<TData, TValue>>;
37+
data: Array<TData>;
38+
isError: boolean;
39+
}
40+
41+
export function QuestionAttemptsTable<TValue>({
42+
columns,
43+
data,
44+
isError,
45+
}: QuestionTableProps<IQuestionAttempt, TValue>) {
46+
const [pagination, setPagination] = useState({
47+
pageIndex: 0,
48+
pageSize: 10,
49+
});
50+
51+
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
52+
const [sorting, setSorting] = useState<SortingState>([]);
53+
54+
const table = useReactTable({
55+
data,
56+
columns,
57+
state: { pagination, columnFilters, sorting },
58+
filterFns: {},
59+
getPaginationRowModel: getPaginationRowModel(),
60+
getFilteredRowModel: getFilteredRowModel(),
61+
getCoreRowModel: getCoreRowModel(),
62+
onColumnFiltersChange: setColumnFilters,
63+
onPaginationChange: setPagination,
64+
onSortingChange: setSorting,
65+
getSortedRowModel: getSortedRowModel(),
66+
});
67+
68+
const setLanguages = (languages: Array<string>) => {
69+
setColumnFilters((columnFilters) => [
70+
...columnFilters.filter((v) => v.id !== 'language'),
71+
...languages.map((v) => ({ id: 'language', value: v })),
72+
]);
73+
};
74+
75+
return (
76+
<div className='relative flex max-h-full w-full grow flex-col'>
77+
<div className='flex items-center py-4'>
78+
<div className='flex flex-col gap-1'>
79+
<label className='text-sm font-medium'>Filter by Language</label>
80+
<ComboboxMulti
81+
setValuesCallback={setLanguages}
82+
options={Array.from(new Set(data.map((v) => v.language))).map((v) => ({
83+
value: v,
84+
label: v,
85+
}))}
86+
placeholderText='Select a language filter'
87+
noOptionsText='None of the available languages match your search'
88+
/>
89+
</div>
90+
{/* <Input
91+
placeholder='Search questions...'
92+
value={(table.getColumn('title')?.getFilterValue() as string) ?? ''}
93+
onChange={(event) => table.getColumn('title')?.setFilterValue(event.target.value)}
94+
className='max-w-sm'
95+
/> */}
96+
</div>
97+
<div className='border-border sticky top-0 rounded-t-md border'>
98+
<Table>
99+
<TableHeader>
100+
{table.getHeaderGroups().map((headerGroup) => (
101+
<TableRow
102+
className='border-border/60 bg-primary-foreground text-primary'
103+
key={headerGroup.id}
104+
>
105+
{headerGroup.headers.map((header) => {
106+
return (
107+
<TableHead key={header.id}>
108+
{header.isPlaceholder
109+
? null
110+
: flexRender(header.column.columnDef.header, header.getContext())}
111+
</TableHead>
112+
);
113+
})}
114+
</TableRow>
115+
))}
116+
</TableHeader>
117+
</Table>
118+
</div>
119+
<ScrollArea className='size-full overflow-x-auto border-x'>
120+
<Table>
121+
<TableBody>
122+
{!isError && table.getRowModel().rows?.length ? (
123+
table.getRowModel().rows.map((row) => (
124+
<TableRow key={row.id} className='border-border/60 even:bg-secondary/10'>
125+
{row.getVisibleCells().map((cell) => (
126+
<TableCell key={cell.id}>
127+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
128+
</TableCell>
129+
))}
130+
</TableRow>
131+
))
132+
) : (
133+
<TableRow>
134+
<TableCell colSpan={columns.length} className='h-24 text-center'>
135+
No results.
136+
</TableCell>
137+
</TableRow>
138+
)}
139+
</TableBody>
140+
</Table>
141+
</ScrollArea>
142+
<div className='sticky bottom-0 rounded-b-md border'>
143+
<Table>
144+
<TableFooter>
145+
<TableRow>
146+
<TableCell colSpan={columns.length}>
147+
<Pagination className='flex items-center justify-end space-x-2 p-2'>
148+
<PaginationContent>
149+
<PaginationItem>
150+
<Button
151+
variant='outline'
152+
size='sm'
153+
className='px-2'
154+
onClick={() => table.firstPage()}
155+
disabled={!table.getCanPreviousPage()}
156+
>
157+
<DoubleArrowLeftIcon />
158+
</Button>
159+
</PaginationItem>
160+
<PaginationItem className='mr-1'>
161+
<Button
162+
variant='outline'
163+
size='sm'
164+
className='px-2'
165+
onClick={() => table.previousPage()}
166+
disabled={!table.getCanPreviousPage()}
167+
>
168+
<ArrowLeftIcon />
169+
</Button>
170+
</PaginationItem>
171+
<PaginationItem className='text-sm'>
172+
{table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
173+
</PaginationItem>
174+
<PaginationItem className='ml-1'>
175+
<Button
176+
variant='outline'
177+
size='sm'
178+
className='px-2'
179+
onClick={() => table.nextPage()}
180+
disabled={!table.getCanNextPage()}
181+
>
182+
<ArrowRightIcon />
183+
</Button>
184+
</PaginationItem>
185+
<PaginationItem>
186+
<Button
187+
variant='outline'
188+
size='sm'
189+
className='px-2'
190+
onClick={() => table.lastPage()}
191+
disabled={!table.getCanNextPage()}
192+
>
193+
<DoubleArrowRightIcon />
194+
</Button>
195+
</PaginationItem>
196+
</PaginationContent>
197+
</Pagination>
198+
</TableCell>
199+
</TableRow>
200+
</TableFooter>
201+
</Table>
202+
</div>
203+
</div>
204+
);
205+
}
Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,40 @@
1-
import { useQuery } from '@tanstack/react-query';
2-
import React from 'react';
1+
import { useInfiniteQuery } from '@tanstack/react-query';
2+
import React, { useEffect, useMemo } from 'react';
33

4+
import { CardContent } from '@/components/ui/card';
5+
import { getQuestionAttempts } from '@/services/question-service';
46
import { useAuthedRoute } from '@/stores/auth-store';
7+
import { IPostGetQuestionAttemptsResponse } from '@/types/question-types';
8+
9+
import { columns } from './question-attempts-columns';
10+
import { QuestionAttemptsTable } from './question-attempts-table';
511

612
type QuestionAttemptsProps = {
713
questionId: number;
814
};
915

10-
export const QuestionAttemptsPane: React.FC<QuestionAttemptsProps> = ({ questionId: _q }) => {
16+
export const QuestionAttemptsPane: React.FC<QuestionAttemptsProps> = ({ questionId }) => {
1117
const { userId } = useAuthedRoute();
12-
const { data: _ } = useQuery({
13-
queryKey: ['question', 'attempts', userId],
14-
enabled: false,
15-
queryFn: async () => {},
18+
const { data, hasNextPage, isFetchingNextPage, fetchNextPage, isError } = useInfiniteQuery({
19+
queryKey: ['question', 'attempts', questionId, userId],
20+
queryFn: ({ pageParam }) =>
21+
getQuestionAttempts({ questionId, userId, ...(pageParam ? { offset: pageParam } : {}) }),
22+
getNextPageParam: (lastPage, pages) => {
23+
return lastPage.length > 0 ? pages.length * 10 : undefined;
24+
},
25+
initialPageParam: 0,
1626
});
17-
return <div />;
27+
useEffect(() => {
28+
if (hasNextPage && !isFetchingNextPage) {
29+
fetchNextPage();
30+
}
31+
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
32+
const attempts = useMemo(() => {
33+
return data?.pages.flatMap((v) => v as IPostGetQuestionAttemptsResponse) ?? [];
34+
}, [data]);
35+
return (
36+
<CardContent className='flex size-full p-0'>
37+
<QuestionAttemptsTable columns={columns} data={attempts} isError={isError} />
38+
</CardContent>
39+
);
1840
};

frontend/src/components/providers/query-provider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { FC, PropsWithChildren } from 'react';
44

55
import { queryClient } from '@/lib/query-client';
66

7-
const IS_SHOW_DEVTOOLS = false;
7+
const IS_SHOW_DEVTOOLS = true;
88

99
export const QueryProvider: FC<PropsWithChildren> = ({ children }) => {
1010
return (

0 commit comments

Comments
 (0)