@@ -12,11 +12,17 @@ import {
1212
1313export default function ChallengeManagement ( ) {
1414 const [ challenges , setChallenges ] = useState ( [ ] ) ;
15+ const [ filteredChallenges , setFilteredChallenges ] = useState ( [ ] ) ;
1516 const [ selectedChallenge , setSelectedChallenge ] = useState ( null ) ;
1617 const [ showModal , setShowModal ] = useState ( false ) ;
1718 const [ showDeleteModal , setShowDeleteModal ] = useState ( false ) ;
1819 const [ error , setError ] = useState ( null ) ;
1920 const [ isLoading , setIsLoading ] = useState ( true ) ;
21+
22+ // Filter states
23+ const [ searchQuery , setSearchQuery ] = useState ( "" ) ;
24+ const [ tagFilter , setTagFilter ] = useState ( "all" ) ;
25+ const [ bonusFilter , setBonusFilter ] = useState ( "all" ) ;
2026 const [ formData , setFormData ] = useState ( {
2127 title : "" ,
2228 description : "" ,
@@ -34,6 +40,11 @@ export default function ChallengeManagement() {
3440 useEffect ( ( ) => {
3541 fetchChallenges ( ) ;
3642 } , [ ] ) ;
43+
44+ // Apply filters whenever challenges array or filter settings change
45+ useEffect ( ( ) => {
46+ applyFilters ( ) ;
47+ } , [ challenges , searchQuery , tagFilter , bonusFilter ] ) ;
3748
3849 const fetchChallenges = async ( ) => {
3950 try {
@@ -173,6 +184,41 @@ export default function ChallengeManagement() {
173184 setSelectedChallenge ( null ) ;
174185 } ;
175186
187+ // Function to apply all filters to the challenges array
188+ const applyFilters = ( ) => {
189+ let result = [ ...challenges ] ;
190+
191+ // Apply tag filter
192+ if ( tagFilter !== "all" ) {
193+ result = result . filter ( challenge => challenge . tag ?. toLowerCase ( ) === tagFilter . toLowerCase ( ) ) ;
194+ }
195+
196+ // Apply bonus filter
197+ if ( bonusFilter === "with-bonus" ) {
198+ result = result . filter ( challenge => challenge . bonusPoints > 0 && challenge . bonusLimit > 0 ) ;
199+ } else if ( bonusFilter === "no-bonus" ) {
200+ result = result . filter ( challenge => ! ( challenge . bonusPoints > 0 && challenge . bonusLimit > 0 ) ) ;
201+ }
202+
203+ // Apply search query
204+ if ( searchQuery . trim ( ) ) {
205+ const query = searchQuery . toLowerCase ( ) . trim ( ) ;
206+ result = result . filter (
207+ challenge =>
208+ challenge . title ?. toLowerCase ( ) . includes ( query ) ||
209+ challenge . description ?. toLowerCase ( ) . includes ( query )
210+ ) ;
211+ }
212+
213+ setFilteredChallenges ( result ) ;
214+ } ;
215+
216+ // Extract unique tags from challenges for the tag filter dropdown
217+ const getUniqueTags = ( ) => {
218+ const tags = challenges . map ( challenge => challenge . tag ) . filter ( Boolean ) ;
219+ return [ ...new Set ( tags ) ] ;
220+ } ;
221+
176222 const openEditModal = challenge => {
177223 setSelectedChallenge ( challenge ) ;
178224 setFormData ( {
@@ -206,6 +252,93 @@ export default function ChallengeManagement() {
206252 </ button >
207253 </ div >
208254
255+ { /* Filter Controls */ }
256+ < div className = "mb-6 bg-gray-800/30 border border-gray-800 rounded-xl p-4" >
257+ < div className = "flex flex-col md:flex-row gap-4" >
258+ { /* Search Input */ }
259+ < div className = "flex-1" >
260+ < label htmlFor = "search" className = "block text-sm font-medium text-gray-400 mb-1" >
261+ Search
262+ </ label >
263+ < div className = "relative" >
264+ < div className = "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none" >
265+ < svg className = "w-4 h-4 text-gray-500" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" xmlns = "http://www.w3.org/2000/svg" >
266+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = "2" d = "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" > </ path >
267+ </ svg >
268+ </ div >
269+ < input
270+ type = "text"
271+ id = "search"
272+ value = { searchQuery }
273+ onChange = { ( e ) => setSearchQuery ( e . target . value ) }
274+ placeholder = "Search by title or description..."
275+ className = "w-full pl-10 pr-3 py-2 bg-gray-900/50 border border-gray-800 text-gray-200 text-sm rounded-lg focus:ring-1 focus:ring-purple-500/50 focus:border-purple-500/50 block focus:outline-none transition-colors"
276+ />
277+ </ div >
278+ </ div >
279+
280+ { /* Tag Filter */ }
281+ < div className = "w-full md:w-64" >
282+ < label htmlFor = "tagFilter" className = "block text-sm font-medium text-gray-400 mb-1" >
283+ Filter by Tag
284+ </ label >
285+ < select
286+ id = "tagFilter"
287+ value = { tagFilter }
288+ onChange = { ( e ) => setTagFilter ( e . target . value ) }
289+ className = "w-full px-3 py-2 bg-gray-900/50 border border-gray-800 text-gray-200 text-sm rounded-lg focus:ring-1 focus:ring-purple-500/50 focus:border-purple-500/50 block focus:outline-none transition-colors appearance-none"
290+ style = { { backgroundImage : `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")` , backgroundPosition : 'right 0.5rem center' , backgroundRepeat : 'no-repeat' , backgroundSize : '1.5em 1.5em' , paddingRight : '2.5rem' } }
291+ >
292+ < option value = "all" > All Tags</ option >
293+ { getUniqueTags ( ) . map ( ( tag ) => (
294+ < option key = { tag } value = { tag } >
295+ { tag }
296+ </ option >
297+ ) ) }
298+ </ select >
299+ </ div >
300+
301+ { /* Bonus Points Filter */ }
302+ < div className = "w-full md:w-64" >
303+ < label htmlFor = "bonusFilter" className = "block text-sm font-medium text-gray-400 mb-1" >
304+ Bonus Points
305+ </ label >
306+ < select
307+ id = "bonusFilter"
308+ value = { bonusFilter }
309+ onChange = { ( e ) => setBonusFilter ( e . target . value ) }
310+ className = "w-full px-3 py-2 bg-gray-900/50 border border-gray-800 text-gray-200 text-sm rounded-lg focus:ring-1 focus:ring-purple-500/50 focus:border-purple-500/50 block focus:outline-none transition-colors appearance-none"
311+ style = { { backgroundImage : `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")` , backgroundPosition : 'right 0.5rem center' , backgroundRepeat : 'no-repeat' , backgroundSize : '1.5em 1.5em' , paddingRight : '2.5rem' } }
312+ >
313+ < option value = "all" > All Challenges</ option >
314+ < option value = "with-bonus" > With Bonus Points</ option >
315+ < option value = "no-bonus" > No Bonus Points</ option >
316+ </ select >
317+ </ div >
318+ </ div >
319+
320+ { /* Filter Status and Reset */ }
321+ < div className = "mt-4 flex justify-between items-center" >
322+ < div className = "text-sm text-gray-400" >
323+ Showing { filteredChallenges . length } of { challenges . length } challenges
324+ </ div >
325+
326+ { ( searchQuery || tagFilter !== "all" || bonusFilter !== "all" ) && (
327+ < button
328+ onClick = { ( ) => {
329+ setSearchQuery ( "" ) ;
330+ setTagFilter ( "all" ) ;
331+ setBonusFilter ( "all" ) ;
332+ } }
333+ className = "text-xs px-2 py-1 bg-gray-800 hover:bg-gray-700 text-gray-400 hover:text-white rounded-md transition-colors flex items-center"
334+ >
335+ < X className = "w-3.5 h-3.5 mr-1" />
336+ Clear Filters
337+ </ button >
338+ ) }
339+ </ div >
340+ </ div >
341+
209342 { /* Challenges Table */ }
210343 < div className = "border border-gray-800 rounded-xl overflow-hidden" >
211344 < table className = "w-full" >
@@ -219,7 +352,7 @@ export default function ChallengeManagement() {
219352 </ tr >
220353 </ thead >
221354 < tbody >
222- { challenges . map ( challenge => (
355+ { filteredChallenges . length > 0 ? filteredChallenges . map ( challenge => (
223356 < tr key = { challenge . _id } className = "border-t border-gray-800 hover:bg-gray-800/50 transition-colors" >
224357 < td className = "py-4 px-6" >
225358 < span className = { `px-3 py-1 rounded-full text-sm ${ getTagStyle ( challenge . tag ) } ` } >
@@ -271,7 +404,22 @@ export default function ChallengeManagement() {
271404 </ div >
272405 </ td >
273406 </ tr >
274- ) ) }
407+ ) ) : (
408+ < tr >
409+ < td colSpan = "5" className = "py-8 px-6 text-center text-gray-400" >
410+ { isLoading ? (
411+ < div className = "flex justify-center" >
412+ < div className = "animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-purple-500" > </ div >
413+ </ div >
414+ ) : (
415+ < div >
416+ < div className = "text-xl mb-2" > No challenges found</ div >
417+ < div className = "text-sm" > Try adjusting your filters or add a new challenge</ div >
418+ </ div >
419+ ) }
420+ </ td >
421+ </ tr >
422+ ) }
275423 </ tbody >
276424 </ table >
277425 </ div >
0 commit comments