Skip to content

Commit

Permalink
Merge pull request #178 from CS3219-AY2425S1/fix/cleanup_features
Browse files Browse the repository at this point in the history
Fix/cleanup features
  • Loading branch information
tyouwei authored Nov 13, 2024
2 parents bfc5632 + 49e6c5c commit 1f7e28c
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 58 deletions.
1 change: 1 addition & 0 deletions peerprep-fe/src/app/(main)/components/filter/FilterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export default function FilterBar({
<TopicsPopover
selectedTopics={filters.topics || []}
onChange={(value) => updateFilter('topics', value)}
multiselect={true}
/>
<div className="flex-grow">
<Input
Expand Down
44 changes: 29 additions & 15 deletions peerprep-fe/src/app/(main)/components/filter/TopicsPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ interface TopicsPopoverProps {
selectedTopics: string[];
onChange: (value: string[]) => void;
isAdmin?: boolean;
multiselect: boolean;
}

export function TopicsPopover({
selectedTopics,
onChange,
isAdmin,
multiselect,
}: TopicsPopoverProps) {
const [open, setOpen] = useState(false);
const [topics, setTopics] = useState<string[]>([]);
Expand All @@ -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 (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
Expand All @@ -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()}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
Expand All @@ -73,9 +94,7 @@ export function TopicsPopover({
{isAdmin && (
<Button
onClick={async () => {
if (!searchTerm.trim()) {
return;
}
if (!searchTerm.trim()) return;
setTopics((prev) => [...prev, searchTerm]);
setSearchTerm('');
}}
Expand All @@ -95,23 +114,18 @@ export function TopicsPopover({
<Button
key={topic}
variant="ghost"
className="justify-start"
onClick={() => {
const newSelectedTopics = selectedTopics.includes(topic)
? selectedTopics.filter((t) => t !== topic)
: [...selectedTopics, topic];
onChange(newSelectedTopics);
}}
className="h-auto min-h-[2.5rem] justify-start whitespace-normal text-left"
onClick={() => handleTopicSelection(topic)}
>
<Check
className={cn(
'mr-2 h-4 w-4',
'mr-2 h-4 w-4 shrink-0',
selectedTopics.includes(topic)
? 'opacity-100'
: 'opacity-0',
)}
/>
{topic}
<span className="break-words">{topic}</span>
</Button>
))}
</div>
Expand Down
5 changes: 0 additions & 5 deletions peerprep-fe/src/app/signin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
/>
</div>
<div className="flex items-center justify-start">
<a href="#" className="text-sm text-blue-500 hover:underline">
Forgot your password?
</a>
</div>
<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">
Sign in
</Button>
Expand Down
1 change: 1 addition & 0 deletions peerprep-fe/src/components/dialogs/PreMatch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export function PreMatch() {
<TopicsPopover
selectedTopics={selectedTopics}
onChange={setSelectedTopics}
multiselect={false}
/>
</div>
</div>
Expand Down
69 changes: 39 additions & 30 deletions peerprep-fe/src/hooks/useFilteredProblems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Problem[]>([]);
const [filters, setFilters] = useState<FilterState>({
difficulty: null,
Expand All @@ -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<number>());
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<Problem[]>(url);
const newProblems = response.data;
const response = await axiosClient.get<PaginatedResponse>(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);
Expand All @@ -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) => ({
Expand All @@ -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,
Expand All @@ -135,7 +144,7 @@ export function useFilteredProblems() {
removeFilter,
isLoading,
hasMore,
isEmpty,
loadMore,
fetchProblems,
};
}
62 changes: 54 additions & 8 deletions question-service/src/routes/questionsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any> = {};
const conditions: Record<string, any>[] = [];

// 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' });
}
});
Expand Down

0 comments on commit 1f7e28c

Please sign in to comment.