feat: isStudentLed filter
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user