1+ import { useEffect , useState } from 'react'
2+ import { useParams , useSearchParams , Link } from 'react-router-dom'
3+ import { Card , CardContent , CardHeader , CardTitle } from '@/components/ui/card'
4+ import { Badge } from '@/components/ui/badge'
5+ import { Button } from '@/components/ui/button'
6+ import { ChevronLeft , ChevronRight , ExternalLink , Loader2 , ArrowLeft } from 'lucide-react'
7+ import SEO from '../SEO/SEO'
8+
9+ interface Problem {
10+ _id : string
11+ title : string
12+ difficulty : 'Easy' | 'Medium' | 'Hard'
13+ platform : 'LeetCode' | 'GFG' | 'CodeChef' | 'Codeforces'
14+ problemLink ?: string
15+ }
16+
17+ interface PaginationInfo {
18+ currentPage : number
19+ totalPages : number
20+ totalProblems : number
21+ hasNext : boolean
22+ hasPrev : boolean
23+ }
24+
25+ export default function CategoryProblems ( ) {
26+ const { categoryName } = useParams ( )
27+ const [ searchParams , setSearchParams ] = useSearchParams ( )
28+ const [ problems , setProblems ] = useState < Problem [ ] > ( [ ] )
29+ const [ loading , setLoading ] = useState ( true )
30+ const [ actualCategoryName , setActualCategoryName ] = useState < string > ( '' )
31+ const [ pagination , setPagination ] = useState < PaginationInfo > ( {
32+ currentPage : 1 ,
33+ totalPages : 1 ,
34+ totalProblems : 0 ,
35+ hasNext : false ,
36+ hasPrev : false
37+ } )
38+
39+ const currentPage = parseInt ( searchParams . get ( 'page' ) || '1' )
40+ const categoryNameFromURL = searchParams . get ( 'name' ) || ''
41+
42+ useEffect ( ( ) => {
43+ const fetchProblems = async ( ) => {
44+ if ( ! categoryName ) return
45+
46+ setLoading ( true )
47+ try {
48+ // Use the category name passed from URL parameter
49+ const actualCategory = categoryNameFromURL || formatCategoryName ( categoryName )
50+ setActualCategoryName ( actualCategory )
51+
52+ const response = await fetch (
53+ `${ import . meta. env . VITE_API_BASE_URL } /api/challenges?page=${ currentPage } &limit=10&category=${ actualCategory } `
54+ )
55+
56+ if ( ! response . ok ) throw new Error ( 'Failed to fetch problems' )
57+
58+ const data = await response . json ( )
59+
60+ setProblems ( data . challenges || [ ] )
61+ setPagination ( {
62+ currentPage : data . currentPage || 1 ,
63+ totalPages : data . totalPages || 1 ,
64+ totalProblems : data . totalChallenges || 0 ,
65+ hasNext : data . hasNextPage || false ,
66+ hasPrev : data . hasPreviousPage || false
67+ } )
68+ } catch ( error ) {
69+ console . error ( 'Error fetching problems:' , error )
70+ setProblems ( [ ] )
71+ } finally {
72+ setLoading ( false )
73+ }
74+ }
75+
76+ fetchProblems ( )
77+ } , [ categoryName , currentPage , categoryNameFromURL ] )
78+
79+ const handlePageChange = ( newPage : number ) => {
80+ if ( newPage >= 1 && newPage <= pagination . totalPages ) {
81+ setSearchParams ( { page : newPage . toString ( ) } )
82+ }
83+ }
84+
85+ const getDifficultyColor = ( difficulty : string ) => {
86+ switch ( difficulty ) {
87+ case 'Easy' : return 'bg-green-100 text-green-800 hover:bg-green-200'
88+ case 'Medium' : return 'bg-yellow-100 text-yellow-800 hover:bg-yellow-200'
89+ case 'Hard' : return 'bg-red-100 text-red-800 hover:bg-red-200'
90+ default : return 'bg-gray-100 text-gray-800 hover:bg-gray-200'
91+ }
92+ }
93+
94+ const getPlatformColor = ( platform : string ) => {
95+ switch ( platform ) {
96+ case 'LeetCode' : return 'bg-orange-100 text-orange-800'
97+ case 'GFG' : return 'bg-green-100 text-green-800'
98+ case 'CodeChef' : return 'bg-brown-100 text-brown-800'
99+ case 'Codeforces' : return 'bg-blue-100 text-blue-800'
100+ default : return 'bg-gray-100 text-gray-800'
101+ }
102+ }
103+
104+ const formatCategoryName = ( slug ?: string ) => {
105+ if ( ! slug ) return ''
106+ return slug . split ( '-' ) . map ( word =>
107+ word . charAt ( 0 ) . toUpperCase ( ) + word . slice ( 1 )
108+ ) . join ( ' ' )
109+ }
110+
111+ if ( loading ) {
112+ return (
113+ < div className = "min-h-screen flex items-center justify-center" >
114+ < Loader2 className = "h-8 w-8 animate-spin" />
115+ </ div >
116+ )
117+ }
118+
119+ return (
120+ < >
121+ < SEO
122+ title = { `${ actualCategoryName || formatCategoryName ( categoryName ) } Problems | CodeQuest` }
123+ description = { `Solve ${ actualCategoryName || formatCategoryName ( categoryName ) } coding problems and challenges. Practice algorithms and data structures with problems from LeetCode, GeeksforGeeks, and more.` }
124+ keywords = { `${ actualCategoryName || categoryName } problems, ${ actualCategoryName || categoryName } algorithms, coding challenges, programming practice` }
125+ />
126+ < div className = "container mx-auto px-4 py-8 min-h-screen" >
127+ { /* Header with back navigation */ }
128+ < div className = "mb-8" >
129+ < Link
130+ to = "/problemset"
131+ className = "inline-flex items-center text-sm text-muted-foreground hover:text-primary mb-4"
132+ >
133+ < ArrowLeft className = "h-4 w-4 mr-2" />
134+ Back to Problem Set
135+ </ Link >
136+
137+ < div className = "flex items-center justify-between" >
138+ < div >
139+ < h1 className = "text-3xl font-bold" > { actualCategoryName || formatCategoryName ( categoryName ) } Problems</ h1 >
140+ < p className = "text-muted-foreground mt-2" >
141+ { pagination . totalProblems > 0
142+ ? `${ pagination . totalProblems } problems available`
143+ : 'No problems found' }
144+ </ p >
145+ </ div >
146+ </ div >
147+ </ div >
148+
149+ { /* Problems List */ }
150+ { problems . length > 0 ? (
151+ < Card >
152+ < CardHeader >
153+ < CardTitle className = "text-lg" > Problems</ CardTitle >
154+ </ CardHeader >
155+ < CardContent className = "p-0" >
156+ < div className = "divide-y" >
157+ { problems . map ( ( problem , index ) => (
158+ < a
159+ key = { problem . _id }
160+ href = { problem . problemLink }
161+ target = "_blank"
162+ rel = "noopener noreferrer"
163+ className = "flex items-center justify-between p-4 hover:bg-muted/50 transition-colors cursor-pointer"
164+ >
165+ < div className = "flex items-center space-x-4 flex-1" >
166+ < span className = "text-sm text-muted-foreground w-8" >
167+ { ( pagination . currentPage - 1 ) * 10 + index + 1 }
168+ </ span >
169+ < div className = "flex-1" >
170+ < h3 className = "font-medium" > { problem . title } </ h3 >
171+ </ div >
172+ </ div >
173+
174+ < div className = "flex items-center space-x-3" >
175+ < Badge
176+ variant = "secondary"
177+ className = { getDifficultyColor ( problem . difficulty ) }
178+ >
179+ { problem . difficulty }
180+ </ Badge >
181+
182+ < Badge
183+ variant = "outline"
184+ className = { getPlatformColor ( problem . platform ) }
185+ >
186+ { problem . platform }
187+ </ Badge >
188+
189+ < ExternalLink className = "h-4 w-4 text-muted-foreground" />
190+ </ div >
191+ </ a >
192+ ) ) }
193+ </ div >
194+ </ CardContent >
195+ </ Card >
196+ ) : (
197+ < Card >
198+ < CardContent className = "text-center py-12" >
199+ < p className = "text-muted-foreground" > No problems found for this category.</ p >
200+ </ CardContent >
201+ </ Card >
202+ ) }
203+
204+ { /* Pagination */ }
205+ { pagination . totalPages > 1 && (
206+ < div className = "flex items-center justify-center space-x-2 mt-8" >
207+ < Button
208+ variant = "outline"
209+ size = "sm"
210+ onClick = { ( ) => handlePageChange ( currentPage - 1 ) }
211+ disabled = { ! pagination . hasPrev }
212+ >
213+ < ChevronLeft className = "h-4 w-4" />
214+ Previous
215+ </ Button >
216+
217+ < div className = "flex items-center space-x-1" >
218+ { Array . from ( { length : pagination . totalPages } , ( _ , i ) => i + 1 )
219+ . filter ( page => {
220+ const distance = Math . abs ( page - currentPage )
221+ return distance <= 2 || page === 1 || page === pagination . totalPages
222+ } )
223+ . map ( ( page , index , array ) => (
224+ < div key = { page } className = "flex items-center" >
225+ { index > 0 && array [ index - 1 ] !== page - 1 && (
226+ < span className = "px-2 text-muted-foreground" > ...</ span >
227+ ) }
228+ < Button
229+ variant = { page === currentPage ? "default" : "outline" }
230+ size = "sm"
231+ onClick = { ( ) => handlePageChange ( page ) }
232+ className = "min-w-[40px]"
233+ >
234+ { page }
235+ </ Button >
236+ </ div >
237+ ) ) }
238+ </ div >
239+
240+ < Button
241+ variant = "outline"
242+ size = "sm"
243+ onClick = { ( ) => handlePageChange ( currentPage + 1 ) }
244+ disabled = { ! pagination . hasNext }
245+ >
246+ Next
247+ < ChevronRight className = "h-4 w-4" />
248+ </ Button >
249+ </ div >
250+ ) }
251+
252+ { /* Results info */ }
253+ { pagination . totalProblems > 0 && (
254+ < div className = "text-center text-sm text-muted-foreground mt-4" >
255+ Showing { ( pagination . currentPage - 1 ) * 10 + 1 } - { Math . min ( pagination . currentPage * 10 , pagination . totalProblems ) } of { pagination . totalProblems } problems
256+ </ div >
257+ ) }
258+ </ div >
259+ </ >
260+ )
261+ }
0 commit comments