feat: isStudentLed filter

This commit is contained in:
JamesFlare1212
2025-05-14 01:28:10 -04:00
parent b491de731b
commit a43f60770c
4 changed files with 65 additions and 36 deletions

View File

@@ -11,7 +11,7 @@ import {
import { debounce } from '@utils/debounce';
import ClubCard from './ClubCard';
import SearchBar from './SearchBar';
import ClubDetailModal from './ClubDetailModal'; // Import the modal
import ClubDetailModal from './ClubDetailModal';
const ClubList: React.FC = () => {
const [allClubsData, setAllClubsData] = useState<ClubBasic[]>([]);
@@ -22,6 +22,7 @@ const ClubList: React.FC = () => {
const [selectedCategory, setSelectedCategory] = useState<string>('');
const [selectedAcademicYear, setSelectedAcademicYear] = useState<string>('');
const [selectedGrade, setSelectedGrade] = useState<string>('');
const [selectedStudentLed, setSelectedStudentLed] = useState<string>('');
const [availableCategories, setAvailableCategories] = useState<CategoryCount>({});
const [availableAcademicYears, setAvailableAcademicYears] = useState<AcademicYearCount>({});
@@ -79,15 +80,19 @@ const ClubList: React.FC = () => {
return;
}
const studentLedParam = selectedStudentLed === "" ? undefined : selectedStudentLed === "true";
let clubs;
if (
selectedCategory || selectedAcademicYear ||
(gradeNumber && gradeNumber >= 1 && gradeNumber <= 12)
(gradeNumber && gradeNumber >= 1 && gradeNumber <= 12) ||
selectedStudentLed !== ""
) {
clubs = await filterClubs({
category: selectedCategory || undefined,
academicYear: selectedAcademicYear || undefined,
grade: gradeNumber,
isStudentLed: studentLedParam,
});
} else {
clubs = await listAllClubs();
@@ -101,7 +106,7 @@ const ClubList: React.FC = () => {
} finally {
setIsLoading(false);
}
}, [selectedCategory, selectedAcademicYear, selectedGrade, sortClubsDesc]);
}, [selectedCategory, selectedAcademicYear, selectedGrade, selectedStudentLed, sortClubsDesc]);
useEffect(() => {
fetchFilteredClubsFromAPI();
@@ -159,6 +164,8 @@ const ClubList: React.FC = () => {
availableAcademicYears={availableAcademicYears}
selectedGrade={selectedGrade}
onGradeChange={setSelectedGrade}
selectedStudentLed={selectedStudentLed}
onStudentLedChange={setSelectedStudentLed}
/>
{isLoading && <p className="loading-message">Filtering clubs...</p>}
{!isLoading && displayedClubs.length === 0 && (
@@ -169,9 +176,9 @@ const ClubList: React.FC = () => {
<ClubCard
key={club.id}
club={club}
initialDetails={clubDetailsCache[club.id]} // Pass cached details if available
initialDetails={clubDetailsCache[club.id]}
onDetailsLoaded={handleDetailsLoadedForCache}
onViewMore={handleOpenModal} // Pass the modal opener function
onViewMore={handleOpenModal}
/>
))}
</div>

View File

@@ -1,9 +1,9 @@
import React, { useState } from 'react'; // Removed useEffect as it's not needed here anymore
// src/components/SearchBar.tsx
import React, { useState } from 'react';
import type { CategoryCount, AcademicYearCount } from '@types';
interface SearchBarProps {
// initialSearchTerm?: string; // Could be used if you want to pre-fill from URL params etc.
onSearchTermChange: (term: string) => void; // This is the debounced function from parent
onSearchTermChange: (term: string) => void;
selectedCategory: string;
onCategoryChange: (category: string) => void;
availableCategories: CategoryCount;
@@ -12,10 +12,12 @@ interface SearchBarProps {
availableAcademicYears: AcademicYearCount;
selectedGrade: string;
onGradeChange: (grade: string) => void;
selectedStudentLed: string;
onStudentLedChange: (value: string) => void;
}
const SearchBar: React.FC<SearchBarProps> = ({
onSearchTermChange, // This is the debounced function from ClubList
onSearchTermChange,
selectedCategory,
onCategoryChange,
availableCategories,
@@ -24,13 +26,15 @@ const SearchBar: React.FC<SearchBarProps> = ({
availableAcademicYears,
selectedGrade,
onGradeChange,
selectedStudentLed,
onStudentLedChange,
}) => {
const [inputValue, setInputValue] = useState<string>(''); // Local state for immediate input update
const [inputValue, setInputValue] = useState<string>('');
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
setInputValue(newValue); // Update local state immediately for responsive input field
onSearchTermChange(newValue); // Call the debounced function passed from the parent
setInputValue(newValue);
onSearchTermChange(newValue);
};
return (
@@ -38,10 +42,31 @@ const SearchBar: React.FC<SearchBarProps> = ({
<input
type="text"
placeholder="Search clubs by name or ID..."
value={inputValue} // Bind to local state
value={inputValue}
onChange={handleInputChange}
aria-label="Search clubs"
/>
<input
type="number"
placeholder="Grade (1-12)"
value={selectedGrade}
onChange={(e) => onGradeChange(e.target.value)}
min="1"
max="12"
aria-label="Filter by grade"
/>
<select
value={selectedAcademicYear}
onChange={(e) => onAcademicYearChange(e.target.value)}
aria-label="Filter by academic year"
>
<option value="">All Academic Years</option>
{Object.entries(availableAcademicYears).map(([year, count]) => (
<option key={year} value={year}>
{year} ({count})
</option>
))}
</select>
<select
value={selectedCategory}
onChange={(e) => onCategoryChange(e.target.value)}
@@ -55,26 +80,14 @@ const SearchBar: React.FC<SearchBarProps> = ({
))}
</select>
<select
value={selectedAcademicYear}
onChange={(e) => onAcademicYearChange(e.target.value)}
aria-label="Filter by academic year"
value={selectedStudentLed}
onChange={(e) => onStudentLedChange(e.target.value)}
aria-label="Filter by student-led status"
>
<option value="">All Academic Years</option>
{Object.entries(availableAcademicYears).map(([year, count]) => (
<option key={year} value={year}>
{year} ({count})
</option>
))}
<option value="">All Types</option>
<option value="true">Student-Led Only</option>
<option value="false">Teacher-Led Only</option>
</select>
<input
type="number"
placeholder="Grade (1-12)"
value={selectedGrade} // Grade filter updates directly, not usually typed fast
onChange={(e) => onGradeChange(e.target.value)}
min="1"
max="12"
aria-label="Filter by grade"
/>
</div>
);
};

View File

@@ -162,6 +162,8 @@ footer {
line-height: 1.4;
flex-grow: 1;
flex-shrink: 1;
height: 2.75rem;
vertical-align: middle;
}
/* Handle select dropdown arrow color for dark mode (can be tricky cross-browser) */
/* For modern browsers, accent-color can influence form controls */
@@ -179,10 +181,14 @@ footer {
min-width: 250px;
}
.search-bar-container select,
.search-bar-container select {
flex-basis: 160px;
min-width: 120px;
}
.search-bar-container input[type="number"] {
flex-basis: 200px;
min-width: 150px;
flex-basis: 120px;
min-width: 80px;
}
@@ -228,6 +234,7 @@ footer {
min-width: 0;
flex-grow: 0;
flex-shrink: 0;
height: 2.5rem;
}
}

View File

@@ -15,8 +15,8 @@ const BASE_URL = 'https://dsas-cca.jamesflare.com/v1';
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
async function fetchWithErrorHandling<T>(url: string, isRetryable: boolean = false, attempt: number = 1): Promise<T> {
const MAX_RETRY_ATTEMPTS_FOR_NON_PERSISTENT = 3; // Max retries for general errors if not persistent
const RETRY_DELAY_MS = 1000; // 1 second for persistent retry
const MAX_RETRY_ATTEMPTS_FOR_NON_PERSISTENT = 3;
const RETRY_DELAY_MS = 1000;
try {
const response = await fetch(url);
@@ -59,11 +59,13 @@ export async function filterClubs(params: {
category?: string;
academicYear?: string;
grade?: number;
isStudentLed?: boolean | undefined;
}): Promise<ClubBasic[]> {
const queryParams = new URLSearchParams();
if (params.category) queryParams.append('category', params.category);
if (params.academicYear) queryParams.append('academicYear', params.academicYear);
if (params.grade !== undefined) queryParams.append('grade', String(params.grade));
if (params.isStudentLed !== undefined) queryParams.append('isStudentLed', String(params.isStudentLed));
const url = `${BASE_URL}/activity/list?${queryParams.toString()}`;
const data = await fetchWithErrorHandling<ApiClubListResponse>(url);