feat: isStudentLed filter
This commit is contained in:
@@ -11,7 +11,7 @@ import {
|
|||||||
import { debounce } from '@utils/debounce';
|
import { debounce } from '@utils/debounce';
|
||||||
import ClubCard from './ClubCard';
|
import ClubCard from './ClubCard';
|
||||||
import SearchBar from './SearchBar';
|
import SearchBar from './SearchBar';
|
||||||
import ClubDetailModal from './ClubDetailModal'; // Import the modal
|
import ClubDetailModal from './ClubDetailModal';
|
||||||
|
|
||||||
const ClubList: React.FC = () => {
|
const ClubList: React.FC = () => {
|
||||||
const [allClubsData, setAllClubsData] = useState<ClubBasic[]>([]);
|
const [allClubsData, setAllClubsData] = useState<ClubBasic[]>([]);
|
||||||
@@ -22,6 +22,7 @@ const ClubList: React.FC = () => {
|
|||||||
const [selectedCategory, setSelectedCategory] = useState<string>('');
|
const [selectedCategory, setSelectedCategory] = useState<string>('');
|
||||||
const [selectedAcademicYear, setSelectedAcademicYear] = useState<string>('');
|
const [selectedAcademicYear, setSelectedAcademicYear] = useState<string>('');
|
||||||
const [selectedGrade, setSelectedGrade] = useState<string>('');
|
const [selectedGrade, setSelectedGrade] = useState<string>('');
|
||||||
|
const [selectedStudentLed, setSelectedStudentLed] = useState<string>('');
|
||||||
|
|
||||||
const [availableCategories, setAvailableCategories] = useState<CategoryCount>({});
|
const [availableCategories, setAvailableCategories] = useState<CategoryCount>({});
|
||||||
const [availableAcademicYears, setAvailableAcademicYears] = useState<AcademicYearCount>({});
|
const [availableAcademicYears, setAvailableAcademicYears] = useState<AcademicYearCount>({});
|
||||||
@@ -79,15 +80,19 @@ const ClubList: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const studentLedParam = selectedStudentLed === "" ? undefined : selectedStudentLed === "true";
|
||||||
|
|
||||||
let clubs;
|
let clubs;
|
||||||
if (
|
if (
|
||||||
selectedCategory || selectedAcademicYear ||
|
selectedCategory || selectedAcademicYear ||
|
||||||
(gradeNumber && gradeNumber >= 1 && gradeNumber <= 12)
|
(gradeNumber && gradeNumber >= 1 && gradeNumber <= 12) ||
|
||||||
|
selectedStudentLed !== ""
|
||||||
) {
|
) {
|
||||||
clubs = await filterClubs({
|
clubs = await filterClubs({
|
||||||
category: selectedCategory || undefined,
|
category: selectedCategory || undefined,
|
||||||
academicYear: selectedAcademicYear || undefined,
|
academicYear: selectedAcademicYear || undefined,
|
||||||
grade: gradeNumber,
|
grade: gradeNumber,
|
||||||
|
isStudentLed: studentLedParam,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
clubs = await listAllClubs();
|
clubs = await listAllClubs();
|
||||||
@@ -101,7 +106,7 @@ const ClubList: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [selectedCategory, selectedAcademicYear, selectedGrade, sortClubsDesc]);
|
}, [selectedCategory, selectedAcademicYear, selectedGrade, selectedStudentLed, sortClubsDesc]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchFilteredClubsFromAPI();
|
fetchFilteredClubsFromAPI();
|
||||||
@@ -159,6 +164,8 @@ const ClubList: React.FC = () => {
|
|||||||
availableAcademicYears={availableAcademicYears}
|
availableAcademicYears={availableAcademicYears}
|
||||||
selectedGrade={selectedGrade}
|
selectedGrade={selectedGrade}
|
||||||
onGradeChange={setSelectedGrade}
|
onGradeChange={setSelectedGrade}
|
||||||
|
selectedStudentLed={selectedStudentLed}
|
||||||
|
onStudentLedChange={setSelectedStudentLed}
|
||||||
/>
|
/>
|
||||||
{isLoading && <p className="loading-message">Filtering clubs...</p>}
|
{isLoading && <p className="loading-message">Filtering clubs...</p>}
|
||||||
{!isLoading && displayedClubs.length === 0 && (
|
{!isLoading && displayedClubs.length === 0 && (
|
||||||
@@ -169,9 +176,9 @@ const ClubList: React.FC = () => {
|
|||||||
<ClubCard
|
<ClubCard
|
||||||
key={club.id}
|
key={club.id}
|
||||||
club={club}
|
club={club}
|
||||||
initialDetails={clubDetailsCache[club.id]} // Pass cached details if available
|
initialDetails={clubDetailsCache[club.id]}
|
||||||
onDetailsLoaded={handleDetailsLoadedForCache}
|
onDetailsLoaded={handleDetailsLoadedForCache}
|
||||||
onViewMore={handleOpenModal} // Pass the modal opener function
|
onViewMore={handleOpenModal}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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';
|
import type { CategoryCount, AcademicYearCount } from '@types';
|
||||||
|
|
||||||
interface SearchBarProps {
|
interface SearchBarProps {
|
||||||
// initialSearchTerm?: string; // Could be used if you want to pre-fill from URL params etc.
|
onSearchTermChange: (term: string) => void;
|
||||||
onSearchTermChange: (term: string) => void; // This is the debounced function from parent
|
|
||||||
selectedCategory: string;
|
selectedCategory: string;
|
||||||
onCategoryChange: (category: string) => void;
|
onCategoryChange: (category: string) => void;
|
||||||
availableCategories: CategoryCount;
|
availableCategories: CategoryCount;
|
||||||
@@ -12,10 +12,12 @@ interface SearchBarProps {
|
|||||||
availableAcademicYears: AcademicYearCount;
|
availableAcademicYears: AcademicYearCount;
|
||||||
selectedGrade: string;
|
selectedGrade: string;
|
||||||
onGradeChange: (grade: string) => void;
|
onGradeChange: (grade: string) => void;
|
||||||
|
selectedStudentLed: string;
|
||||||
|
onStudentLedChange: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SearchBar: React.FC<SearchBarProps> = ({
|
const SearchBar: React.FC<SearchBarProps> = ({
|
||||||
onSearchTermChange, // This is the debounced function from ClubList
|
onSearchTermChange,
|
||||||
selectedCategory,
|
selectedCategory,
|
||||||
onCategoryChange,
|
onCategoryChange,
|
||||||
availableCategories,
|
availableCategories,
|
||||||
@@ -24,13 +26,15 @@ const SearchBar: React.FC<SearchBarProps> = ({
|
|||||||
availableAcademicYears,
|
availableAcademicYears,
|
||||||
selectedGrade,
|
selectedGrade,
|
||||||
onGradeChange,
|
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 handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
setInputValue(newValue); // Update local state immediately for responsive input field
|
setInputValue(newValue);
|
||||||
onSearchTermChange(newValue); // Call the debounced function passed from the parent
|
onSearchTermChange(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -38,10 +42,31 @@ const SearchBar: React.FC<SearchBarProps> = ({
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search clubs by name or ID..."
|
placeholder="Search clubs by name or ID..."
|
||||||
value={inputValue} // Bind to local state
|
value={inputValue}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
aria-label="Search clubs"
|
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
|
<select
|
||||||
value={selectedCategory}
|
value={selectedCategory}
|
||||||
onChange={(e) => onCategoryChange(e.target.value)}
|
onChange={(e) => onCategoryChange(e.target.value)}
|
||||||
@@ -55,26 +80,14 @@ const SearchBar: React.FC<SearchBarProps> = ({
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<select
|
<select
|
||||||
value={selectedAcademicYear}
|
value={selectedStudentLed}
|
||||||
onChange={(e) => onAcademicYearChange(e.target.value)}
|
onChange={(e) => onStudentLedChange(e.target.value)}
|
||||||
aria-label="Filter by academic year"
|
aria-label="Filter by student-led status"
|
||||||
>
|
>
|
||||||
<option value="">All Academic Years</option>
|
<option value="">All Types</option>
|
||||||
{Object.entries(availableAcademicYears).map(([year, count]) => (
|
<option value="true">Student-Led Only</option>
|
||||||
<option key={year} value={year}>
|
<option value="false">Teacher-Led Only</option>
|
||||||
{year} ({count})
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -162,6 +162,8 @@ footer {
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
|
height: 2.75rem;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
/* Handle select dropdown arrow color for dark mode (can be tricky cross-browser) */
|
/* Handle select dropdown arrow color for dark mode (can be tricky cross-browser) */
|
||||||
/* For modern browsers, accent-color can influence form controls */
|
/* For modern browsers, accent-color can influence form controls */
|
||||||
@@ -179,10 +181,14 @@ footer {
|
|||||||
min-width: 250px;
|
min-width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar-container select,
|
.search-bar-container select {
|
||||||
|
flex-basis: 160px;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
.search-bar-container input[type="number"] {
|
.search-bar-container input[type="number"] {
|
||||||
flex-basis: 200px;
|
flex-basis: 120px;
|
||||||
min-width: 150px;
|
min-width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -228,6 +234,7 @@ footer {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
flex-shrink: 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));
|
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
|
||||||
async function fetchWithErrorHandling<T>(url: string, isRetryable: boolean = false, attempt: number = 1): Promise<T> {
|
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 MAX_RETRY_ATTEMPTS_FOR_NON_PERSISTENT = 3;
|
||||||
const RETRY_DELAY_MS = 1000; // 1 second for persistent retry
|
const RETRY_DELAY_MS = 1000;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
@@ -59,11 +59,13 @@ export async function filterClubs(params: {
|
|||||||
category?: string;
|
category?: string;
|
||||||
academicYear?: string;
|
academicYear?: string;
|
||||||
grade?: number;
|
grade?: number;
|
||||||
|
isStudentLed?: boolean | undefined;
|
||||||
}): Promise<ClubBasic[]> {
|
}): Promise<ClubBasic[]> {
|
||||||
const queryParams = new URLSearchParams();
|
const queryParams = new URLSearchParams();
|
||||||
if (params.category) queryParams.append('category', params.category);
|
if (params.category) queryParams.append('category', params.category);
|
||||||
if (params.academicYear) queryParams.append('academicYear', params.academicYear);
|
if (params.academicYear) queryParams.append('academicYear', params.academicYear);
|
||||||
if (params.grade !== undefined) queryParams.append('grade', String(params.grade));
|
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 url = `${BASE_URL}/activity/list?${queryParams.toString()}`;
|
||||||
const data = await fetchWithErrorHandling<ApiClubListResponse>(url);
|
const data = await fetchWithErrorHandling<ApiClubListResponse>(url);
|
||||||
|
|||||||
Reference in New Issue
Block a user