1
- "use client" ;
1
+ "use client"
2
2
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' ;
6
6
import {
7
7
Table ,
8
8
TableBody ,
9
9
TableCell ,
10
10
TableHead ,
11
11
TableHeader ,
12
12
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' ;
15
15
import {
16
16
useMutation ,
17
17
useQueryClient ,
18
18
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' ;
29
33
30
34
const QuestionRepositoryContent = ( ) => {
31
35
const queryClient = useQueryClient ( ) ;
32
36
const [ isCreateModalOpen , setCreateModalOpen ] = useState ( false ) ;
33
37
const [ confirmLoading , setConfirmLoading ] = useState ( false ) ;
34
- const { toast } = useToast ( ) ;
38
+
35
39
const { data } = useSuspenseQuery < QuestionDto [ ] > ( {
36
40
queryKey : [ QUERY_KEYS . Question ] ,
37
41
queryFn : fetchQuestions ,
38
42
} ) ;
43
+
39
44
const createMutation = useMutation ( {
40
45
mutationFn : ( newQuestion : CreateQuestionDto ) => createQuestion ( newQuestion ) ,
41
46
onMutate : ( ) => setConfirmLoading ( true ) ,
42
47
onSuccess : async ( ) => {
43
48
await queryClient . invalidateQueries ( { queryKey : [ QUERY_KEYS . Question ] } ) ;
44
49
setCreateModalOpen ( false ) ;
45
50
toast ( {
46
- variant : " success" ,
47
- title : " Success" ,
48
- description : " Question created successfully" ,
51
+ variant : ' success' ,
52
+ title : ' Success' ,
53
+ description : ' Question created successfully' ,
49
54
} ) ;
50
55
} ,
51
56
onSettled : ( ) => setConfirmLoading ( false ) ,
52
57
onError : ( error ) => {
58
+ console . error ( 'Error creating question:' , error ) ;
53
59
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 ,
57
63
} ) ;
58
64
} ,
59
65
} ) ;
@@ -62,48 +68,186 @@ const QuestionRepositoryContent = () => {
62
68
createMutation . mutate ( newQuestion ) ;
63
69
} ;
64
70
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
+
65
147
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" >
68
150
< h1 className = "text-xl font-semibold" > Question Repository</ h1 >
69
151
< Button
70
152
variant = "outline"
71
153
disabled = { confirmLoading }
72
154
onClick = { ( ) => setCreateModalOpen ( true ) }
73
155
>
74
- < Plus className = "w -4 h -4" />
156
+ < Plus className = "h -4 w -4" />
75
157
</ Button >
76
158
</ div >
77
159
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 */ }
78
200
{ data ?. length === 0 ? (
79
201
< EmptyPlaceholder />
80
202
) : (
81
203
< Table >
82
204
< TableHeader >
83
205
< 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 >
87
231
</ TableRow >
88
232
</ TableHeader >
89
233
< TableBody
90
- className = { `${ confirmLoading ? " opacity-50" : " opacity-100" } ` }
234
+ className = { `${ confirmLoading ? ' opacity-50' : ' opacity-100' } ` }
91
235
>
92
- { data ? .map ( ( question ) => (
236
+ { sortedData . map ( ( question ) => (
93
237
< TableRow key = { question . id } >
94
- < TableCell style = { { width : " 40%" } } >
238
+ < TableCell style = { { width : ' 40%' } } >
95
239
< Link
96
240
href = { `/question/${ question . id } ` }
97
241
className = "text-blue-500 hover:text-blue-700"
98
242
>
99
243
{ question . q_title }
100
244
</ Link >
101
245
</ TableCell >
102
- < TableCell style = { { width : " 10%" } } >
246
+ < TableCell style = { { width : ' 10%' } } >
103
247
< DifficultyBadge complexity = { question . q_complexity } />
104
248
</ 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" >
107
251
{ question . q_category . map ( ( category ) => (
108
252
< Badge
109
253
key = { category }
0 commit comments