feat(teams): add member count filter to browse teams

- Add min_members and max_members params to useTeams hook
- Add member filter dropdown with options: All, Looking for Members (1-4), 1-5 members
- Filter persists in URL query params

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Maulana Sodiqin
2025-11-29 23:12:42 +07:00
co-authored by Claude
parent a7196dd453
commit d67e3e7244
2 changed files with 54 additions and 1 deletions
+50 -1
View File
@@ -17,6 +17,17 @@ import { Icon } from '@iconify/react';
const DEFAULT_PER_PAGE = 12; const DEFAULT_PER_PAGE = 12;
const PER_PAGE_OPTIONS = [6, 12, 24, 48]; const PER_PAGE_OPTIONS = [6, 12, 24, 48];
// Member filter options
const MEMBER_FILTER_OPTIONS = [
{ label: 'All Teams', value: '', minMembers: undefined, maxMembers: undefined },
{ label: 'Looking for Members (1-4)', value: 'looking', minMembers: 1, maxMembers: 4 },
{ label: '1 Member', value: '1', minMembers: 1, maxMembers: 1 },
{ label: '2 Members', value: '2', minMembers: 2, maxMembers: 2 },
{ label: '3 Members', value: '3', minMembers: 3, maxMembers: 3 },
{ label: '4 Members', value: '4', minMembers: 4, maxMembers: 4 },
{ label: '5 Members (Full)', value: '5', minMembers: 5, maxMembers: 5 },
];
// Skeleton card component for loading state // Skeleton card component for loading state
const TeamCardSkeleton: FC = () => ( const TeamCardSkeleton: FC = () => (
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-md overflow-hidden flex flex-col border dark:border-gray-800 animate-pulse"> <div className="bg-white dark:bg-gray-900 rounded-lg shadow-md overflow-hidden flex flex-col border dark:border-gray-800 animate-pulse">
@@ -54,10 +65,12 @@ const BrowseTeamsPage: FC = (): ReactElement => {
); );
const initialSearch = searchParams.get('search') || ''; const initialSearch = searchParams.get('search') || '';
const initialCity = searchParams.get('city') || ''; const initialCity = searchParams.get('city') || '';
const initialMembers = searchParams.get('members') || '';
const [searchTerm, setSearchTerm] = useState(initialSearch); const [searchTerm, setSearchTerm] = useState(initialSearch);
const [debouncedSearch, setDebouncedSearch] = useState(initialSearch); const [debouncedSearch, setDebouncedSearch] = useState(initialSearch);
const [selectedCity, setSelectedCity] = useState(initialCity); const [selectedCity, setSelectedCity] = useState(initialCity);
const [selectedMembers, setSelectedMembers] = useState(initialMembers);
const [selectedTeamId, setSelectedTeamId] = useState<string | null>(null); const [selectedTeamId, setSelectedTeamId] = useState<string | null>(null);
const [showJoinModal, setShowJoinModal] = useState(false); const [showJoinModal, setShowJoinModal] = useState(false);
const [currentPage, setCurrentPage] = useState( const [currentPage, setCurrentPage] = useState(
@@ -74,6 +87,7 @@ const BrowseTeamsPage: FC = (): ReactElement => {
per_page?: number; per_page?: number;
search?: string; search?: string;
city?: string; city?: string;
members?: string;
}) => { }) => {
const newParams = new URLSearchParams(searchParams); const newParams = new URLSearchParams(searchParams);
@@ -109,6 +123,14 @@ const BrowseTeamsPage: FC = (): ReactElement => {
} }
} }
if (params.members !== undefined) {
if (params.members === '') {
newParams.delete('members');
} else {
newParams.set('members', params.members);
}
}
setSearchParams(newParams, { replace: true }); setSearchParams(newParams, { replace: true });
}, },
[searchParams, setSearchParams] [searchParams, setSearchParams]
@@ -134,6 +156,11 @@ const BrowseTeamsPage: FC = (): ReactElement => {
} }
}, [selectedCity]); }, [selectedCity]);
// Get member filter values
const memberFilter = MEMBER_FILTER_OPTIONS.find(
(opt) => opt.value === selectedMembers
);
const { const {
data: teamsData, data: teamsData,
isLoading, isLoading,
@@ -144,6 +171,8 @@ const BrowseTeamsPage: FC = (): ReactElement => {
search: debouncedSearch, search: debouncedSearch,
city: selectedCity || undefined, city: selectedCity || undefined,
visibility: ETeamVisibility.PUBLIC, visibility: ETeamVisibility.PUBLIC,
minMembers: memberFilter?.minMembers,
maxMembers: memberFilter?.maxMembers,
}); });
const { data: myTeamsData } = useMyTeams(); const { data: myTeamsData } = useMyTeams();
@@ -243,7 +272,7 @@ const BrowseTeamsPage: FC = (): ReactElement => {
{/* Filters */} {/* Filters */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="bg-white dark:bg-gray-900 p-6 rounded-lg shadow-sm mb-6 border dark:border-gray-800"> <div className="bg-white dark:bg-gray-900 p-6 rounded-lg shadow-sm mb-6 border dark:border-gray-800">
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-3">
<div> <div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Search Teams Search Teams
@@ -266,6 +295,26 @@ const BrowseTeamsPage: FC = (): ReactElement => {
placeholder="All Cities (search to filter...)" placeholder="All Cities (search to filter...)"
/> />
</div> </div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Filter by Members
</label>
<select
value={selectedMembers}
onChange={(e) => {
setSelectedMembers(e.target.value);
setCurrentPage(1);
updateUrlParams({ members: e.target.value, page: 1 });
}}
className="w-full h-[42px] px-3 border border-gray-300 dark:border-gray-700 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
>
{MEMBER_FILTER_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
</div> </div>
</div> </div>
+4
View File
@@ -125,6 +125,8 @@ export const useTeams = (params?: {
city?: string; city?: string;
visibility?: string; visibility?: string;
search?: string; search?: string;
minMembers?: number;
maxMembers?: number;
}) => { }) => {
return useQuery({ return useQuery({
queryKey: teamKeys.list(params), queryKey: teamKeys.list(params),
@@ -135,6 +137,8 @@ export const useTeams = (params?: {
if (params?.search) queryParams.append('search', params.search); if (params?.search) queryParams.append('search', params.search);
if (params?.city) queryParams.append('city', params.city); if (params?.city) queryParams.append('city', params.city);
if (params?.visibility) queryParams.append('visibility', params.visibility); if (params?.visibility) queryParams.append('visibility', params.visibility);
if (params?.minMembers) queryParams.append('min_members', String(params.minMembers));
if (params?.maxMembers) queryParams.append('max_members', String(params.maxMembers));
const response = await hackathonApi.get<ListResponseWithMeta<Team>>( const response = await hackathonApi.get<ListResponseWithMeta<Team>>(
`/teams/browse${queryParams.toString() ? `?${queryParams.toString()}` : ''}` `/teams/browse${queryParams.toString() ? `?${queryParams.toString()}` : ''}`