Skip to content

Commit 29e8d1b

Browse files
committed
feat: filtering and sorting for questions repo
1 parent 118bba9 commit 29e8d1b

File tree

3 files changed

+457
-68
lines changed

3 files changed

+457
-68
lines changed

project/apps/web/app/questions/page.tsx

Lines changed: 179 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,65 @@
1-
"use client";
1+
"use client"
22

3-
import { Suspense, useState } from "react";
4-
import { Plus } from "lucide-react";
5-
import { QuestionDto, CreateQuestionDto } from "@repo/dtos/questions";
3+
import { Suspense, useState, useMemo } from 'react';
4+
import { Plus } from 'lucide-react';
5+
import { QuestionDto, CreateQuestionDto } from '@repo/dtos/questions';
66
import {
77
Table,
88
TableBody,
99
TableCell,
1010
TableHead,
1111
TableHeader,
1212
TableRow,
13-
} from "@/components/ui/table";
14-
import { QUERY_KEYS } from "@/constants/queryKeys";
13+
} from '@/components/ui/table';
14+
import { QUERY_KEYS } from '@/constants/queryKeys';
1515
import {
1616
useMutation,
1717
useQueryClient,
1818
useSuspenseQuery,
19-
} from "@tanstack/react-query";
20-
import { Badge } from "@/components/ui/badge";
21-
import { Button } from "@/components/ui/button";
22-
import CreateModal from "./components/CreateModal";
23-
import { useToast } from "@/hooks/use-toast";
24-
import { createQuestion, fetchQuestions } from "@/lib/api/question";
25-
import Link from "next/link";
26-
import DifficultyBadge from "@/components/DifficultyBadge";
27-
import QuestionsSkeleton from "./components/QuestionsSkeleton";
28-
import EmptyPlaceholder from "./components/EmptyPlaceholder";
19+
} from '@tanstack/react-query';
20+
import { Badge } from '@/components/ui/badge';
21+
import { Button } from '@/components/ui/button';
22+
import Select from 'react-select';
23+
import CreateModal from './components/CreateModal';
24+
import { toast } from '@/hooks/use-toast';
25+
import { createQuestion, fetchQuestions } from '@/lib/api/question';
26+
import Link from 'next/link';
27+
import DifficultyBadge from '@/components/DifficultyBadge';
28+
import QuestionsSkeleton from './components/QuestionsSkeleton';
29+
import EmptyPlaceholder from './components/EmptyPlaceholder';
30+
import { CATEGORY, COMPLEXITY } from '@/constants/question';
31+
32+
type SortField = 'q_title' | 'q_complexity' | 'q_category';
2933

3034
const QuestionRepositoryContent = () => {
3135
const queryClient = useQueryClient();
3236
const [isCreateModalOpen, setCreateModalOpen] = useState(false);
3337
const [confirmLoading, setConfirmLoading] = useState(false);
34-
const { toast } = useToast();
38+
3539
const { data } = useSuspenseQuery<QuestionDto[]>({
3640
queryKey: [QUERY_KEYS.Question],
3741
queryFn: fetchQuestions,
3842
});
43+
3944
const createMutation = useMutation({
4045
mutationFn: (newQuestion: CreateQuestionDto) => createQuestion(newQuestion),
4146
onMutate: () => setConfirmLoading(true),
4247
onSuccess: async () => {
4348
await queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.Question] });
4449
setCreateModalOpen(false);
4550
toast({
46-
variant: "success",
47-
title: "Success",
48-
description: "Question created successfully",
51+
variant: 'success',
52+
title: 'Success',
53+
description: 'Question created successfully',
4954
});
5055
},
5156
onSettled: () => setConfirmLoading(false),
5257
onError: (error) => {
58+
console.error('Error creating question:', error);
5359
toast({
54-
variant: "destructive",
55-
title: "Error",
56-
description: "Error creating question: " + error.message,
60+
variant: 'destructive',
61+
title: 'Error',
62+
description: 'Error creating question: ' + error,
5763
});
5864
},
5965
});
@@ -62,48 +68,186 @@ const QuestionRepositoryContent = () => {
6268
createMutation.mutate(newQuestion);
6369
};
6470

71+
const [sortField, setSortField] = useState<SortField>('q_title');
72+
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
73+
74+
const [filterDifficulty, setFilterDifficulty] = useState<Array<{ value: COMPLEXITY; label: string }>>([]);
75+
const [filterCategories, setFilterCategories] = useState<Array<{ value: CATEGORY; label: string }>>([]);
76+
77+
const complexityOrder: { [key in COMPLEXITY]: number } = {
78+
[COMPLEXITY.Easy]: 1,
79+
[COMPLEXITY.Medium]: 2,
80+
[COMPLEXITY.Hard]: 3,
81+
};
82+
83+
const categoryOptions = [
84+
{ value: CATEGORY.DataStructures, label: 'Data Structures' },
85+
{ value: CATEGORY.Algorithms, label: 'Algorithms' },
86+
{ value: CATEGORY.BrainTeaser, label: 'Brain Teaser' },
87+
{ value: CATEGORY.Strings, label: 'Strings' },
88+
{ value: CATEGORY.Databases, label: 'Databases' },
89+
{ value: CATEGORY.BitManipulation, label: 'Bit Manipulation' },
90+
{ value: CATEGORY.Arrays, label: 'Arrays' },
91+
{ value: CATEGORY.Recursion, label: 'Recursion' },
92+
];
93+
94+
const handleSort = (field: SortField) => {
95+
if (sortField === field) {
96+
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
97+
} else {
98+
setSortField(field);
99+
setSortOrder('asc');
100+
}
101+
};
102+
103+
const filteredData = useMemo(() => {
104+
return data.filter((question) => {
105+
const difficultyMatch =
106+
filterDifficulty.length === 0 ||
107+
filterDifficulty.some((option) => option.value === question.q_complexity);
108+
109+
const categoryMatch =
110+
filterCategories.length === 0 ||
111+
question.q_category.some((cat) =>
112+
filterCategories.some((option) => option.value === cat)
113+
);
114+
115+
return difficultyMatch && categoryMatch;
116+
});
117+
}, [data, filterDifficulty, filterCategories]);
118+
119+
const sortedData = useMemo(() => {
120+
return [...filteredData].sort((a, b) => {
121+
let aValue: string | number;
122+
let bValue: string | number;
123+
124+
if (sortField === 'q_title') {
125+
aValue = a.q_title.toLowerCase();
126+
bValue = b.q_title.toLowerCase();
127+
} else if (sortField === 'q_complexity') {
128+
aValue = complexityOrder[a.q_complexity as COMPLEXITY];
129+
bValue = complexityOrder[b.q_complexity as COMPLEXITY];
130+
} else if (sortField === 'q_category') {
131+
aValue = a.q_category.join(', ').toLowerCase();
132+
bValue = b.q_category.join(', ').toLowerCase();
133+
} else {
134+
return 0;
135+
}
136+
137+
if (aValue < bValue) {
138+
return sortOrder === 'asc' ? -1 : 1;
139+
}
140+
if (aValue > bValue) {
141+
return sortOrder === 'asc' ? 1 : -1;
142+
}
143+
return 0;
144+
});
145+
}, [filteredData, sortField, sortOrder]);
146+
65147
return (
66-
<div className="container p-4 mx-auto">
67-
<div className="flex items-center justify-between my-4">
148+
<div className="container mx-auto p-4">
149+
<div className="flex justify-between items-center my-4">
68150
<h1 className="text-xl font-semibold">Question Repository</h1>
69151
<Button
70152
variant="outline"
71153
disabled={confirmLoading}
72154
onClick={() => setCreateModalOpen(true)}
73155
>
74-
<Plus className="w-4 h-4" />
156+
<Plus className="h-4 w-4" />
75157
</Button>
76158
</div>
77159

160+
{/* Filters */}
161+
<div className="flex gap-4 my-4">
162+
{/* Difficulty Filter */}
163+
<div className="w-64">
164+
<h2 className="font-semibold mb-2">Filter by Difficulty</h2>
165+
<Select
166+
isMulti
167+
options={[
168+
{ value: COMPLEXITY.Easy, label: 'Easy' },
169+
{ value: COMPLEXITY.Medium, label: 'Medium' },
170+
{ value: COMPLEXITY.Hard, label: 'Hard' },
171+
]}
172+
value={filterDifficulty}
173+
onChange={(selectedOptions) => {
174+
setFilterDifficulty(selectedOptions as { value: COMPLEXITY; label: string }[] || []);
175+
}}
176+
placeholder="Select Difficulty"
177+
className="react-select-container"
178+
classNamePrefix="react-select"
179+
/>
180+
</div>
181+
182+
{/* Topic Filter */}
183+
<div className="w-200">
184+
<h2 className="font-semibold mb-2">Filter by Topics</h2>
185+
<Select
186+
isMulti
187+
options={categoryOptions}
188+
value={filterCategories}
189+
onChange={(selectedOptions) => {
190+
setFilterCategories(selectedOptions as { value: CATEGORY; label: string }[] || []);
191+
}}
192+
placeholder="Select Topic(s)"
193+
className="react-select-container"
194+
classNamePrefix="react-select"
195+
/>
196+
</div>
197+
</div>
198+
199+
{/* Table */}
78200
{data?.length === 0 ? (
79201
<EmptyPlaceholder />
80202
) : (
81203
<Table>
82204
<TableHeader>
83205
<TableRow>
84-
<TableHead>Title</TableHead>
85-
<TableHead>Difficulty</TableHead>
86-
<TableHead>Categories</TableHead>
206+
<TableHead
207+
onClick={() => handleSort('q_title')}
208+
className="cursor-pointer"
209+
style={{ width: '40%' }}
210+
>
211+
Title{' '}
212+
{sortField === 'q_title' && (sortOrder === 'asc' ? '↑' : '↓')}
213+
</TableHead>
214+
<TableHead
215+
onClick={() => handleSort('q_complexity')}
216+
className="cursor-pointer"
217+
style={{ width: '10%' }}
218+
>
219+
Difficulty{' '}
220+
{sortField === 'q_complexity' &&
221+
(sortOrder === 'asc' ? '↑' : '↓')}
222+
</TableHead>
223+
<TableHead
224+
onClick={() => handleSort('q_category')}
225+
className="cursor-pointer"
226+
style={{ width: '50%' }}
227+
>
228+
Categories{' '}
229+
{sortField === 'q_category' && (sortOrder === 'asc' ? '↑' : '↓')}
230+
</TableHead>
87231
</TableRow>
88232
</TableHeader>
89233
<TableBody
90-
className={`${confirmLoading ? "opacity-50" : "opacity-100"}`}
234+
className={`${confirmLoading ? 'opacity-50' : 'opacity-100'}`}
91235
>
92-
{data?.map((question) => (
236+
{sortedData.map((question) => (
93237
<TableRow key={question.id}>
94-
<TableCell style={{ width: "40%" }}>
238+
<TableCell style={{ width: '40%' }}>
95239
<Link
96240
href={`/question/${question.id}`}
97241
className="text-blue-500 hover:text-blue-700"
98242
>
99243
{question.q_title}
100244
</Link>
101245
</TableCell>
102-
<TableCell style={{ width: "10%" }}>
246+
<TableCell style={{ width: '10%' }}>
103247
<DifficultyBadge complexity={question.q_complexity} />
104248
</TableCell>
105-
<TableCell style={{ width: "50%" }}>
106-
<div className="flex flex-wrap max-w-md gap-2">
249+
<TableCell style={{ width: '50%' }}>
250+
<div className="flex flex-wrap gap-2 max-w-md">
107251
{question.q_category.map((category) => (
108252
<Badge
109253
key={category}

project/apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@radix-ui/react-slot": "^1.1.0",
2222
"@radix-ui/react-toast": "^1.2.1",
2323
"@repo/dtos": "workspace:*",
24+
"react-select": "5.8.1",
2425
"@tanstack/react-query": "^5.56.2",
2526
"axios": "^1.7.7",
2627
"class-variance-authority": "^0.7.0",

0 commit comments

Comments
 (0)