Skip to content

Commit 1f7e28c

Browse files
authored
Merge pull request #178 from CS3219-AY2425S1/fix/cleanup_features
Fix/cleanup features
2 parents bfc5632 + 49e6c5c commit 1f7e28c

File tree

6 files changed

+124
-58
lines changed

6 files changed

+124
-58
lines changed

peerprep-fe/src/app/(main)/components/filter/FilterBar.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export default function FilterBar({
4949
<TopicsPopover
5050
selectedTopics={filters.topics || []}
5151
onChange={(value) => updateFilter('topics', value)}
52+
multiselect={true}
5253
/>
5354
<div className="flex-grow">
5455
<Input

peerprep-fe/src/app/(main)/components/filter/TopicsPopover.tsx

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ interface TopicsPopoverProps {
1616
selectedTopics: string[];
1717
onChange: (value: string[]) => void;
1818
isAdmin?: boolean;
19+
multiselect: boolean;
1920
}
2021

2122
export function TopicsPopover({
2223
selectedTopics,
2324
onChange,
2425
isAdmin,
26+
multiselect,
2527
}: TopicsPopoverProps) {
2628
const [open, setOpen] = useState(false);
2729
const [topics, setTopics] = useState<string[]>([]);
@@ -44,6 +46,27 @@ export function TopicsPopover({
4446
topic.toLowerCase().includes(searchTerm.toLowerCase()),
4547
);
4648

49+
const handleTopicSelection = (selectedTopic: string) => {
50+
if (multiselect) {
51+
const newSelectedTopics = selectedTopics.includes(selectedTopic)
52+
? selectedTopics.filter((t) => t !== selectedTopic)
53+
: [...selectedTopics, selectedTopic];
54+
onChange(newSelectedTopics);
55+
} else {
56+
const newSelectedTopics = selectedTopics.includes(selectedTopic)
57+
? []
58+
: [selectedTopic];
59+
onChange(newSelectedTopics);
60+
setOpen(false);
61+
}
62+
};
63+
64+
const getButtonText = () => {
65+
if (selectedTopics.length === 0) return 'Select topics';
66+
if (!multiselect) return selectedTopics[0];
67+
return `${selectedTopics.length} topics selected`;
68+
};
69+
4770
return (
4871
<Popover open={open} onOpenChange={setOpen}>
4972
<PopoverTrigger asChild>
@@ -53,9 +76,7 @@ export function TopicsPopover({
5376
aria-expanded={open}
5477
className="w-[200px] justify-between border-gray-700 bg-gray-800"
5578
>
56-
{selectedTopics.length > 0
57-
? `${selectedTopics.length} topics selected`
58-
: 'Select topics'}
79+
{getButtonText()}
5980
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
6081
</Button>
6182
</PopoverTrigger>
@@ -73,9 +94,7 @@ export function TopicsPopover({
7394
{isAdmin && (
7495
<Button
7596
onClick={async () => {
76-
if (!searchTerm.trim()) {
77-
return;
78-
}
97+
if (!searchTerm.trim()) return;
7998
setTopics((prev) => [...prev, searchTerm]);
8099
setSearchTerm('');
81100
}}
@@ -95,23 +114,18 @@ export function TopicsPopover({
95114
<Button
96115
key={topic}
97116
variant="ghost"
98-
className="justify-start"
99-
onClick={() => {
100-
const newSelectedTopics = selectedTopics.includes(topic)
101-
? selectedTopics.filter((t) => t !== topic)
102-
: [...selectedTopics, topic];
103-
onChange(newSelectedTopics);
104-
}}
117+
className="h-auto min-h-[2.5rem] justify-start whitespace-normal text-left"
118+
onClick={() => handleTopicSelection(topic)}
105119
>
106120
<Check
107121
className={cn(
108-
'mr-2 h-4 w-4',
122+
'mr-2 h-4 w-4 shrink-0',
109123
selectedTopics.includes(topic)
110124
? 'opacity-100'
111125
: 'opacity-0',
112126
)}
113127
/>
114-
{topic}
128+
<span className="break-words">{topic}</span>
115129
</Button>
116130
))}
117131
</div>

peerprep-fe/src/app/signin/page.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,6 @@ export default function LoginForm({ searchParams }: Props) {
9494
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"
9595
/>
9696
</div>
97-
<div className="flex items-center justify-start">
98-
<a href="#" className="text-sm text-blue-500 hover:underline">
99-
Forgot your password?
100-
</a>
101-
</div>
10297
<Button className="w-full rounded-md bg-blue-600 py-2 text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-800">
10398
Sign in
10499
</Button>

peerprep-fe/src/components/dialogs/PreMatch.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export function PreMatch() {
7979
<TopicsPopover
8080
selectedTopics={selectedTopics}
8181
onChange={setSelectedTopics}
82+
multiselect={false}
8283
/>
8384
</div>
8485
</div>

peerprep-fe/src/hooks/useFilteredProblems.ts

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,19 @@ export interface FilterState {
99
search: string | null;
1010
}
1111

12+
interface PaginatedResponse {
13+
items: Problem[];
14+
pagination: {
15+
page: number;
16+
limit: number;
17+
total: number;
18+
totalPages: number;
19+
};
20+
}
21+
1222
const PAGE_SIZE = 20;
1323

1424
export function useFilteredProblems() {
15-
// States for both filtering and pagination
1625
const [problems, setProblems] = useState<Problem[]>([]);
1726
const [filters, setFilters] = useState<FilterState>({
1827
difficulty: null,
@@ -21,59 +30,60 @@ export function useFilteredProblems() {
2130
search: null,
2231
});
2332
const [isLoading, setIsLoading] = useState(true);
24-
const [page, setPage] = useState(1);
2533
const [hasMore, setHasMore] = useState(true);
34+
const [isEmpty, setIsEmpty] = useState(false);
2635
const seenIds = useRef(new Set<number>());
36+
const currentPage = useRef(1);
2737

2838
const fetchProblems = useCallback(
29-
async (pageNum: number, isLoadingMore = false) => {
39+
async (isLoadingMore = false) => {
3040
if (!isLoadingMore) {
3141
seenIds.current.clear();
42+
currentPage.current = 1;
43+
setIsEmpty(false);
3244
}
3345

3446
setIsLoading(true);
3547

3648
try {
3749
const params = new URLSearchParams();
38-
params.append('page', pageNum.toString());
50+
params.append('page', currentPage.current.toString());
3951
params.append('limit', PAGE_SIZE.toString());
4052

41-
// Apply filters to query
4253
if (filters.difficulty) params.append('difficulty', filters.difficulty);
4354
if (filters.status) params.append('status', filters.status);
4455
if (filters.topics?.length) {
45-
filters.topics.forEach((topic) => params.append('topics', topic));
56+
params.append('topics', filters.topics.join(','));
4657
}
4758
if (filters.search) params.append('search', filters.search);
4859

4960
const url = `/questions?${params.toString()}`;
50-
const response = await axiosClient.get<Problem[]>(url);
51-
const newProblems = response.data;
61+
const response = await axiosClient.get<PaginatedResponse>(url);
62+
const { items: newProblems } = response.data;
5263

53-
if (newProblems.length === 0) {
64+
if (!isLoadingMore && newProblems.length === 0) {
65+
setIsEmpty(true);
66+
setProblems([]);
5467
setHasMore(false);
5568
return;
5669
}
5770

5871
if (isLoadingMore) {
59-
console.log('Fetching a page of 20 items');
60-
const uniqueNewProblems: Problem[] = [];
61-
let foundDuplicate = false;
62-
63-
for (const problem of newProblems) {
72+
const uniqueNewProblems = newProblems.filter((problem) => {
6473
if (seenIds.current.has(problem._id)) {
65-
foundDuplicate = true;
66-
break;
74+
return false;
6775
}
6876
seenIds.current.add(problem._id);
69-
uniqueNewProblems.push(problem);
70-
}
77+
return true;
78+
});
7179

72-
if (foundDuplicate || uniqueNewProblems.length === 0) {
80+
if (uniqueNewProblems.length === 0) {
7381
setHasMore(false);
82+
return;
7483
}
7584

7685
setProblems((prev) => [...prev, ...uniqueNewProblems]);
86+
setHasMore(newProblems.length === PAGE_SIZE);
7787
} else {
7888
newProblems.forEach((problem) => seenIds.current.add(problem._id));
7989
setProblems(newProblems);
@@ -82,14 +92,17 @@ export function useFilteredProblems() {
8292
} catch (error) {
8393
console.error('Error fetching problems:', error);
8494
setHasMore(false);
95+
if (!isLoadingMore) {
96+
setIsEmpty(true);
97+
setProblems([]);
98+
}
8599
} finally {
86100
setIsLoading(false);
87101
}
88102
},
89103
[filters],
90-
); // Note filters dependency
104+
);
91105

92-
// Filter functions
93106
const updateFilter = useCallback(
94107
(key: keyof FilterState, value: string | string[] | null) => {
95108
setFilters((prev) => ({
@@ -113,20 +126,16 @@ export function useFilteredProblems() {
113126
}));
114127
}, []);
115128

116-
// Reset and fetch when filters change
117129
useEffect(() => {
118-
setPage(1);
119-
fetchProblems(1, false);
130+
fetchProblems(false);
120131
}, [filters, fetchProblems]);
121132

122-
// Load more function for infinite scroll
123133
const loadMore = useCallback(() => {
124134
if (!isLoading && hasMore) {
125-
const nextPage = page + 1;
126-
setPage(nextPage);
127-
fetchProblems(nextPage, true);
135+
currentPage.current += 1;
136+
fetchProblems(true);
128137
}
129-
}, [isLoading, hasMore, page, fetchProblems]);
138+
}, [isLoading, hasMore, fetchProblems]);
130139

131140
return {
132141
problems,
@@ -135,7 +144,7 @@ export function useFilteredProblems() {
135144
removeFilter,
136145
isLoading,
137146
hasMore,
147+
isEmpty,
138148
loadMore,
139-
fetchProblems,
140149
};
141150
}

question-service/src/routes/questionsController.ts

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,30 +56,76 @@ router.get('/', async (req: Request, res: Response) => {
5656
const limitNumber = parseInt(limit as string);
5757
const skip = (pageNumber - 1) * limitNumber;
5858

59-
let query: any = {};
59+
// Build the query object
60+
const query: Record<string, any> = {};
61+
const conditions: Record<string, any>[] = [];
62+
63+
// Add difficulty filter
6064
if (difficulty) {
61-
query.difficulty = parseInt(difficulty as string);
65+
conditions.push({
66+
difficulty: parseInt(difficulty as string),
67+
});
6268
}
69+
70+
// Add status filter
6371
if (status) {
64-
query.status = status as string;
72+
conditions.push({
73+
status: status as string,
74+
});
6575
}
76+
77+
// Add topics filter
6678
if (topics && typeof topics === 'string') {
67-
const topicsArray = topics.split(',');
68-
query.tags = { $in: topicsArray };
79+
const topicsArray = topics
80+
.split(',')
81+
.map((t) => t.trim())
82+
.filter(Boolean);
83+
if (topicsArray.length > 0) {
84+
conditions.push({
85+
tags: { $all: topicsArray }, // Changed from $in to $all to match all topics
86+
});
87+
}
6988
}
89+
90+
// Add search filter
7091
if (search && typeof search === 'string') {
71-
query.$or = [{ title: { $regex: search, $options: 'i' } }];
92+
conditions.push({
93+
title: {
94+
$regex: search.trim(),
95+
$options: 'i',
96+
},
97+
});
98+
}
99+
100+
// Combine all conditions with $and if there are any
101+
if (conditions.length > 0) {
102+
query.$and = conditions;
72103
}
73104

74-
// Add pagination to MongoDB query
105+
// console.log('MongoDB Query:', JSON.stringify(query, null, 2)); // Debug log
106+
107+
// Execute the query with pagination
75108
const items = await questionsCollection
76109
.find(query)
110+
.sort({ _id: -1 }) // Optional: Add sorting
77111
.skip(skip)
78112
.limit(limitNumber)
79113
.toArray();
80114

81-
res.status(200).json(items);
115+
// Get total count for pagination
116+
const total = await questionsCollection.countDocuments(query);
117+
118+
res.status(200).json({
119+
items,
120+
pagination: {
121+
page: pageNumber,
122+
limit: limitNumber,
123+
total,
124+
totalPages: Math.ceil(total / limitNumber),
125+
},
126+
});
82127
} catch (error) {
128+
console.error('Error fetching items:', error);
83129
res.status(500).json({ error: 'Failed to fetch items' });
84130
}
85131
});

0 commit comments

Comments
 (0)