diff --git a/peerprep-fe/src/app/(main)/components/filter/FilterBar.tsx b/peerprep-fe/src/app/(main)/components/filter/FilterBar.tsx index a8396b536f..53199fa659 100644 --- a/peerprep-fe/src/app/(main)/components/filter/FilterBar.tsx +++ b/peerprep-fe/src/app/(main)/components/filter/FilterBar.tsx @@ -49,6 +49,7 @@ export default function FilterBar({ updateFilter('topics', value)} + multiselect={true} />
void; isAdmin?: boolean; + multiselect: boolean; } export function TopicsPopover({ selectedTopics, onChange, isAdmin, + multiselect, }: TopicsPopoverProps) { const [open, setOpen] = useState(false); const [topics, setTopics] = useState([]); @@ -44,6 +46,27 @@ export function TopicsPopover({ topic.toLowerCase().includes(searchTerm.toLowerCase()), ); + const handleTopicSelection = (selectedTopic: string) => { + if (multiselect) { + const newSelectedTopics = selectedTopics.includes(selectedTopic) + ? selectedTopics.filter((t) => t !== selectedTopic) + : [...selectedTopics, selectedTopic]; + onChange(newSelectedTopics); + } else { + const newSelectedTopics = selectedTopics.includes(selectedTopic) + ? [] + : [selectedTopic]; + onChange(newSelectedTopics); + setOpen(false); + } + }; + + const getButtonText = () => { + if (selectedTopics.length === 0) return 'Select topics'; + if (!multiselect) return selectedTopics[0]; + return `${selectedTopics.length} topics selected`; + }; + return ( @@ -53,9 +76,7 @@ export function TopicsPopover({ aria-expanded={open} className="w-[200px] justify-between border-gray-700 bg-gray-800" > - {selectedTopics.length > 0 - ? `${selectedTopics.length} topics selected` - : 'Select topics'} + {getButtonText()} @@ -73,9 +94,7 @@ export function TopicsPopover({ {isAdmin && ( ))}
diff --git a/peerprep-fe/src/app/signin/page.tsx b/peerprep-fe/src/app/signin/page.tsx index 1326bc87bb..9af4a3c924 100644 --- a/peerprep-fe/src/app/signin/page.tsx +++ b/peerprep-fe/src/app/signin/page.tsx @@ -94,11 +94,6 @@ export default function LoginForm({ searchParams }: Props) { className="w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-sm text-white placeholder-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500" /> -
- - Forgot your password? - -
diff --git a/peerprep-fe/src/components/dialogs/PreMatch.tsx b/peerprep-fe/src/components/dialogs/PreMatch.tsx index 2d30d6fe16..fde2da4e63 100644 --- a/peerprep-fe/src/components/dialogs/PreMatch.tsx +++ b/peerprep-fe/src/components/dialogs/PreMatch.tsx @@ -79,6 +79,7 @@ export function PreMatch() { diff --git a/peerprep-fe/src/hooks/useFilteredProblems.ts b/peerprep-fe/src/hooks/useFilteredProblems.ts index 8046a6d3d3..6946c727f9 100644 --- a/peerprep-fe/src/hooks/useFilteredProblems.ts +++ b/peerprep-fe/src/hooks/useFilteredProblems.ts @@ -9,10 +9,19 @@ export interface FilterState { search: string | null; } +interface PaginatedResponse { + items: Problem[]; + pagination: { + page: number; + limit: number; + total: number; + totalPages: number; + }; +} + const PAGE_SIZE = 20; export function useFilteredProblems() { - // States for both filtering and pagination const [problems, setProblems] = useState([]); const [filters, setFilters] = useState({ difficulty: null, @@ -21,59 +30,60 @@ export function useFilteredProblems() { search: null, }); const [isLoading, setIsLoading] = useState(true); - const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); + const [isEmpty, setIsEmpty] = useState(false); const seenIds = useRef(new Set()); + const currentPage = useRef(1); const fetchProblems = useCallback( - async (pageNum: number, isLoadingMore = false) => { + async (isLoadingMore = false) => { if (!isLoadingMore) { seenIds.current.clear(); + currentPage.current = 1; + setIsEmpty(false); } setIsLoading(true); try { const params = new URLSearchParams(); - params.append('page', pageNum.toString()); + params.append('page', currentPage.current.toString()); params.append('limit', PAGE_SIZE.toString()); - // Apply filters to query if (filters.difficulty) params.append('difficulty', filters.difficulty); if (filters.status) params.append('status', filters.status); if (filters.topics?.length) { - filters.topics.forEach((topic) => params.append('topics', topic)); + params.append('topics', filters.topics.join(',')); } if (filters.search) params.append('search', filters.search); const url = `/questions?${params.toString()}`; - const response = await axiosClient.get(url); - const newProblems = response.data; + const response = await axiosClient.get(url); + const { items: newProblems } = response.data; - if (newProblems.length === 0) { + if (!isLoadingMore && newProblems.length === 0) { + setIsEmpty(true); + setProblems([]); setHasMore(false); return; } if (isLoadingMore) { - console.log('Fetching a page of 20 items'); - const uniqueNewProblems: Problem[] = []; - let foundDuplicate = false; - - for (const problem of newProblems) { + const uniqueNewProblems = newProblems.filter((problem) => { if (seenIds.current.has(problem._id)) { - foundDuplicate = true; - break; + return false; } seenIds.current.add(problem._id); - uniqueNewProblems.push(problem); - } + return true; + }); - if (foundDuplicate || uniqueNewProblems.length === 0) { + if (uniqueNewProblems.length === 0) { setHasMore(false); + return; } setProblems((prev) => [...prev, ...uniqueNewProblems]); + setHasMore(newProblems.length === PAGE_SIZE); } else { newProblems.forEach((problem) => seenIds.current.add(problem._id)); setProblems(newProblems); @@ -82,14 +92,17 @@ export function useFilteredProblems() { } catch (error) { console.error('Error fetching problems:', error); setHasMore(false); + if (!isLoadingMore) { + setIsEmpty(true); + setProblems([]); + } } finally { setIsLoading(false); } }, [filters], - ); // Note filters dependency + ); - // Filter functions const updateFilter = useCallback( (key: keyof FilterState, value: string | string[] | null) => { setFilters((prev) => ({ @@ -113,20 +126,16 @@ export function useFilteredProblems() { })); }, []); - // Reset and fetch when filters change useEffect(() => { - setPage(1); - fetchProblems(1, false); + fetchProblems(false); }, [filters, fetchProblems]); - // Load more function for infinite scroll const loadMore = useCallback(() => { if (!isLoading && hasMore) { - const nextPage = page + 1; - setPage(nextPage); - fetchProblems(nextPage, true); + currentPage.current += 1; + fetchProblems(true); } - }, [isLoading, hasMore, page, fetchProblems]); + }, [isLoading, hasMore, fetchProblems]); return { problems, @@ -135,7 +144,7 @@ export function useFilteredProblems() { removeFilter, isLoading, hasMore, + isEmpty, loadMore, - fetchProblems, }; } diff --git a/question-service/src/routes/questionsController.ts b/question-service/src/routes/questionsController.ts index 9590c31107..5d50b87d39 100644 --- a/question-service/src/routes/questionsController.ts +++ b/question-service/src/routes/questionsController.ts @@ -56,30 +56,76 @@ router.get('/', async (req: Request, res: Response) => { const limitNumber = parseInt(limit as string); const skip = (pageNumber - 1) * limitNumber; - let query: any = {}; + // Build the query object + const query: Record = {}; + const conditions: Record[] = []; + + // Add difficulty filter if (difficulty) { - query.difficulty = parseInt(difficulty as string); + conditions.push({ + difficulty: parseInt(difficulty as string), + }); } + + // Add status filter if (status) { - query.status = status as string; + conditions.push({ + status: status as string, + }); } + + // Add topics filter if (topics && typeof topics === 'string') { - const topicsArray = topics.split(','); - query.tags = { $in: topicsArray }; + const topicsArray = topics + .split(',') + .map((t) => t.trim()) + .filter(Boolean); + if (topicsArray.length > 0) { + conditions.push({ + tags: { $all: topicsArray }, // Changed from $in to $all to match all topics + }); + } } + + // Add search filter if (search && typeof search === 'string') { - query.$or = [{ title: { $regex: search, $options: 'i' } }]; + conditions.push({ + title: { + $regex: search.trim(), + $options: 'i', + }, + }); + } + + // Combine all conditions with $and if there are any + if (conditions.length > 0) { + query.$and = conditions; } - // Add pagination to MongoDB query + // console.log('MongoDB Query:', JSON.stringify(query, null, 2)); // Debug log + + // Execute the query with pagination const items = await questionsCollection .find(query) + .sort({ _id: -1 }) // Optional: Add sorting .skip(skip) .limit(limitNumber) .toArray(); - res.status(200).json(items); + // Get total count for pagination + const total = await questionsCollection.countDocuments(query); + + res.status(200).json({ + items, + pagination: { + page: pageNumber, + limit: limitNumber, + total, + totalPages: Math.ceil(total / limitNumber), + }, + }); } catch (error) { + console.error('Error fetching items:', error); res.status(500).json({ error: 'Failed to fetch items' }); } });