Skip to content

Commit 2de99fe

Browse files
committed
d7/ui: Add interviews tables and crumbs
Signed-off-by: SeeuSim <[email protected]>
1 parent 55564fe commit 2de99fe

File tree

5 files changed

+277
-3
lines changed

5 files changed

+277
-3
lines changed

frontend/src/components/blocks/nav-bar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const NavBar = observer(() => {
2727
<Link to={ROUTES.QUESTIONS}>Questions</Link>
2828
</Button>
2929
<Button variant='ghost' asChild>
30-
<Link to={ROUTES.INTERVIEWS}>Interviews</Link>
30+
<Link to={ROUTES.INTERVIEWS}>Ongoing Interviews</Link>
3131
</Button>
3232
</>
3333
)}

frontend/src/lib/routes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ const TOP_LEVEL_AUTHED_ROUTES = {
2626
},
2727
],
2828
[ROUTES.INTERVIEW.replace(':roomId', '')]: [
29+
{
30+
path: ROUTES.INTERVIEWS,
31+
title: 'Interviews',
32+
},
2933
{
3034
path: ROUTES.INTERVIEW,
3135
title: 'Interview',
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { DotsVerticalIcon, TrashIcon } from '@radix-ui/react-icons';
2+
import { ColumnDef } from '@tanstack/react-table';
3+
import { Link } from 'react-router-dom';
4+
5+
import { Badge } from '@/components/ui/badge';
6+
import { Button } from '@/components/ui/button';
7+
import { DataTableSortableHeader } from '@/components/ui/data-table';
8+
import {
9+
DropdownMenu,
10+
DropdownMenuContent,
11+
DropdownMenuItem,
12+
DropdownMenuTrigger,
13+
} from '@/components/ui/dropdown-menu';
14+
import { ROUTES } from '@/lib/routes';
15+
import { useAuthedRoute } from '@/stores/auth-store';
16+
import { IInterviewRoom } from '@/types/collab-types';
17+
18+
export const columns: Array<ColumnDef<IInterviewRoom>> = [
19+
{
20+
id: 'roomId',
21+
accessorKey: 'roomId',
22+
header: 'Room Link',
23+
cell: ({ row }) => (
24+
<Button variant='link'>
25+
<Link to={ROUTES.INTERVIEW.replace(':roomId', row.getValue('roomId'))}>
26+
{((row.getValue('roomId') as string) ?? '').slice(0, 6)}
27+
</Link>
28+
</Button>
29+
),
30+
},
31+
{
32+
id: 'createdAt',
33+
accessorKey: 'createdAt',
34+
header: ({ column }) => (
35+
<DataTableSortableHeader column={column} title='Interview Started At' />
36+
),
37+
cell: ({ row }) => {
38+
return <span>{new Date(row.getValue('createdAt')).toLocaleString()}</span>;
39+
},
40+
},
41+
{
42+
id: 'question',
43+
accessorKey: 'questionId',
44+
header: ({ column }) => <DataTableSortableHeader column={column} title='Question Id' />,
45+
},
46+
{
47+
id: 'users',
48+
header: 'Users',
49+
cell: ({ row }) => {
50+
const { userId } = useAuthedRoute();
51+
const { userId1, userId2 } = row.original;
52+
const isMe = (value?: string) => userId === value;
53+
return (
54+
<div className='flex gap-2'>
55+
{[userId1, userId2].map((value, index) => (
56+
<Badge key={index} variant={isMe(value) ? 'easy' : 'secondary'}>
57+
<span className='uppercase'>{isMe(value) ? 'Me' : value?.slice(0, 6)}</span>
58+
</Badge>
59+
))}
60+
</div>
61+
);
62+
},
63+
},
64+
{
65+
id: 'actions',
66+
cell: ({ row: _ }) => {
67+
// const { isAdmin } = useAuthedRoute();
68+
return (
69+
<DropdownMenu>
70+
{/* <DropdownMenuTrigger disabled={!isAdmin}> */}
71+
<DropdownMenuTrigger disabled>
72+
<DotsVerticalIcon />
73+
</DropdownMenuTrigger>
74+
<DropdownMenuContent>
75+
<DropdownMenuItem className='flex justify-between gap-2'>
76+
<span>Delete Room</span>
77+
<TrashIcon />
78+
</DropdownMenuItem>
79+
</DropdownMenuContent>
80+
</DropdownMenu>
81+
);
82+
},
83+
},
84+
];
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
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+
useReactTable,
15+
} from '@tanstack/react-table';
16+
import { type FC, useState } from 'react';
17+
18+
import { Button } from '@/components/ui/button';
19+
import { Pagination, PaginationContent, PaginationItem } from '@/components/ui/pagination';
20+
import {
21+
Table,
22+
TableBody,
23+
TableCell,
24+
TableHead,
25+
TableHeader,
26+
TableRow,
27+
} from '@/components/ui/table';
28+
import type { IInterviewRoom } from '@/types/collab-types';
29+
30+
type InterviewsTableProps = {
31+
columns: Array<ColumnDef<IInterviewRoom>>;
32+
data: Array<IInterviewRoom>;
33+
isError: boolean;
34+
};
35+
36+
export const InterviewsTable: FC<InterviewsTableProps> = ({ columns, data, isError }) => {
37+
const [pagination, setPagination] = useState({
38+
pageIndex: 0,
39+
pageSize: 10,
40+
});
41+
42+
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
43+
44+
const table = useReactTable({
45+
data,
46+
columns,
47+
state: { pagination, columnFilters },
48+
filterFns: {},
49+
getPaginationRowModel: getPaginationRowModel(),
50+
getFilteredRowModel: getFilteredRowModel(),
51+
getCoreRowModel: getCoreRowModel(),
52+
onColumnFiltersChange: setColumnFilters,
53+
onPaginationChange: setPagination,
54+
});
55+
56+
return (
57+
<div className='flex size-full flex-col'>
58+
{/* <div className='flex items-center py-4'>
59+
<div className='mr-2'>
60+
<Select onValueChange={handleStatusFilterChange}>
61+
<SelectTrigger className='w-[140px]'>
62+
<SelectValue placeholder='Status' />
63+
</SelectTrigger>
64+
<SelectContent>
65+
<SelectItem value='all'>All</SelectItem>
66+
<SelectItem value='attempted'>Attempted</SelectItem>
67+
<SelectItem value='not-attempted'>Not attempted</SelectItem>
68+
</SelectContent>
69+
</Select>
70+
</div>
71+
<div className='mr-2'>
72+
<Select onValueChange={handleDifficultyFilterChange}>
73+
<SelectTrigger className='w-[140px]'>
74+
<SelectValue placeholder='Difficulty' />
75+
</SelectTrigger>
76+
<SelectContent>
77+
<SelectItem value='all'>All</SelectItem>
78+
<SelectItem value='Easy'>Easy</SelectItem>
79+
<SelectItem value='Medium'>Medium</SelectItem>
80+
<SelectItem value='Hard'>Hard</SelectItem>
81+
</SelectContent>
82+
</Select>
83+
</div>
84+
<Input
85+
placeholder='Search questions...'
86+
value={(table.getColumn('title')?.getFilterValue() as string) ?? ''}
87+
onChange={(event) => table.getColumn('title')?.setFilterValue(event.target.value)}
88+
className='max-w-sm'
89+
/>
90+
</div> */}
91+
<div className='border-border rounded-md border'>
92+
<Table>
93+
<TableHeader>
94+
{table.getHeaderGroups().map((headerGroup) => (
95+
<TableRow
96+
className='border-border/60 bg-primary-foreground text-primary'
97+
key={headerGroup.id}
98+
>
99+
{headerGroup.headers.map((header) => {
100+
return (
101+
<TableHead key={header.id}>
102+
{header.isPlaceholder
103+
? null
104+
: flexRender(header.column.columnDef.header, header.getContext())}
105+
</TableHead>
106+
);
107+
})}
108+
</TableRow>
109+
))}
110+
</TableHeader>
111+
<TableBody>
112+
{!isError && table.getRowModel().rows?.length ? (
113+
table.getRowModel().rows.map((row) => (
114+
<TableRow key={row.id} className='border-border/60 even:bg-secondary/10'>
115+
{row.getVisibleCells().map((cell) => (
116+
<TableCell key={cell.id}>
117+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
118+
</TableCell>
119+
))}
120+
</TableRow>
121+
))
122+
) : (
123+
<TableRow>
124+
<TableCell colSpan={columns.length} className='h-24 text-center'>
125+
No results.
126+
</TableCell>
127+
</TableRow>
128+
)}
129+
</TableBody>
130+
</Table>
131+
</div>
132+
<Pagination className='flex items-center justify-end space-x-2 py-4'>
133+
<PaginationContent>
134+
<PaginationItem>
135+
<Button
136+
variant='outline'
137+
size='sm'
138+
onClick={() => table.firstPage()}
139+
disabled={!table.getCanPreviousPage()}
140+
>
141+
<DoubleArrowLeftIcon />
142+
</Button>
143+
</PaginationItem>
144+
<PaginationItem className='mr-1'>
145+
<Button
146+
variant='outline'
147+
size='sm'
148+
onClick={() => table.previousPage()}
149+
disabled={!table.getCanPreviousPage()}
150+
>
151+
<ArrowLeftIcon />
152+
</Button>
153+
</PaginationItem>
154+
<PaginationItem className='text-sm'>
155+
{table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
156+
</PaginationItem>
157+
<PaginationItem className='ml-1'>
158+
<Button
159+
variant='outline'
160+
size='sm'
161+
onClick={() => table.nextPage()}
162+
disabled={!table.getCanNextPage()}
163+
>
164+
<ArrowRightIcon />
165+
</Button>
166+
</PaginationItem>
167+
<PaginationItem>
168+
<Button
169+
variant='outline'
170+
size='sm'
171+
onClick={() => table.lastPage()}
172+
disabled={!table.getCanNextPage()}
173+
>
174+
<DoubleArrowRightIcon />
175+
</Button>
176+
</PaginationItem>
177+
</PaginationContent>
178+
</Pagination>
179+
</div>
180+
);
181+
};

frontend/src/routes/interview/main.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ import { getRooms } from '@/services/collab-service';
77
import { useAuthedRoute } from '@/stores/auth-store';
88
import { IInterviewRoom } from '@/types/collab-types';
99

10+
import { columns } from './interviews-columns';
11+
import { InterviewsTable } from './interviews-table';
12+
1013
export const InterviewsPage = () => {
1114
const { userId } = useAuthedRoute();
1215
const { crumbs } = useCrumbs();
13-
const { data, isFetchingNextPage, hasNextPage, fetchNextPage } = useInfiniteQuery<
16+
const { data, isFetchingNextPage, hasNextPage, fetchNextPage, isError } = useInfiniteQuery<
1417
Array<IInterviewRoom>
1518
>({
1619
queryKey: ['interviews', userId],
@@ -37,7 +40,9 @@ export const InterviewsPage = () => {
3740

3841
return (
3942
<WithNavBanner crumbs={crumbs}>
40-
<div />
43+
<div className='max-w-[calc(min(650px,100dvw))] p-8'>
44+
<InterviewsTable columns={columns} data={_rooms} isError={isError} />
45+
</div>
4146
</WithNavBanner>
4247
);
4348
};

0 commit comments

Comments
 (0)