feat(teams): add skeleton loading and per_page selector

- Show skeleton cards during pagination loading
- Add per_page dropdown (6, 12, 24, 48 options)
- Reset to page 1 when changing per_page

🤖 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 17:11:56 +07:00
co-authored by Claude
parent 35a5761a7c
commit c83e54fae8
+62 -10
View File
@@ -14,7 +14,33 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { CitySelect } from '../../../components/city-select'; import { CitySelect } from '../../../components/city-select';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
const TEAMS_PER_PAGE = 12; const DEFAULT_PER_PAGE = 12;
const PER_PAGE_OPTIONS = [6, 12, 24, 48];
// Skeleton card component for loading state
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="w-full aspect-3/1 bg-gray-200 dark:bg-gray-700" />
<div className="p-6 flex flex-col flex-1">
<div className="flex items-center space-x-3 mb-3">
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-700" />
<div className="flex-1">
<div className="h-5 w-32 bg-gray-200 dark:bg-gray-700 rounded mb-2" />
<div className="h-4 w-48 bg-gray-200 dark:bg-gray-700 rounded" />
</div>
</div>
<div className="space-y-2 mb-4 flex-1">
<div className="h-4 w-full bg-gray-200 dark:bg-gray-700 rounded" />
<div className="h-4 w-5/6 bg-gray-200 dark:bg-gray-700 rounded" />
<div className="h-4 w-4/6 bg-gray-200 dark:bg-gray-700 rounded" />
</div>
<div className="space-y-3 mt-auto">
<div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded" />
<div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded" />
</div>
</div>
</div>
);
const BrowseTeamsPage: FC = (): ReactElement => { const BrowseTeamsPage: FC = (): ReactElement => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -24,6 +50,7 @@ const BrowseTeamsPage: FC = (): ReactElement => {
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(1); const [currentPage, setCurrentPage] = useState(1);
const [perPage, setPerPage] = useState(DEFAULT_PER_PAGE);
// Debounce search term and reset page // Debounce search term and reset page
useEffect(() => { useEffect(() => {
@@ -45,7 +72,7 @@ const BrowseTeamsPage: FC = (): ReactElement => {
isFetching, isFetching,
} = useTeams({ } = useTeams({
page: currentPage, page: currentPage,
limit: TEAMS_PER_PAGE, limit: perPage,
search: debouncedSearch, search: debouncedSearch,
city: selectedCity || undefined, city: selectedCity || undefined,
visibility: ETeamVisibility.PUBLIC, visibility: ETeamVisibility.PUBLIC,
@@ -177,16 +204,17 @@ const BrowseTeamsPage: FC = (): ReactElement => {
{/* Teams Count */} {/* Teams Count */}
{!isLoading && total > 0 && ( {!isLoading && total > 0 && (
<div className="mb-4 text-sm text-gray-600 dark:text-gray-400"> <div className="mb-4 text-sm text-gray-600 dark:text-gray-400">
Showing {(currentPage - 1) * TEAMS_PER_PAGE + 1}- Showing {(currentPage - 1) * perPage + 1}-
{Math.min(currentPage * TEAMS_PER_PAGE, total)} of {total} teams {Math.min(currentPage * perPage, total)} of {total} teams
</div> </div>
)} )}
{/* Teams List */} {/* Teams List */}
{isLoading ? ( {isLoading || isFetching ? (
<div className="text-center py-12"> <div className="grid gap-6 md:grid-cols-2 xl:grid-cols-3">
<Icon icon="mdi:loading" className="animate-spin text-4xl text-gray-400 mx-auto mb-2" /> {Array.from({ length: perPage }).map((_, index) => (
<p className="text-gray-600 dark:text-gray-400">Loading teams...</p> <TeamCardSkeleton key={index} />
))}
</div> </div>
) : teams.length === 0 ? ( ) : teams.length === 0 ? (
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-sm p-12 text-center"> <div className="bg-white dark:bg-gray-900 rounded-lg shadow-sm p-12 text-center">
@@ -199,7 +227,7 @@ const BrowseTeamsPage: FC = (): ReactElement => {
</div> </div>
) : ( ) : (
<> <>
<div className={`grid gap-6 md:grid-cols-2 xl:grid-cols-3 ${isFetching ? 'opacity-50' : ''}`}> <div className="grid gap-6 md:grid-cols-2 xl:grid-cols-3">
{teams.map((team: any) => ( {teams.map((team: any) => (
<div <div
key={team.id} key={team.id}
@@ -295,8 +323,9 @@ const BrowseTeamsPage: FC = (): ReactElement => {
</div> </div>
{/* Pagination */} {/* Pagination */}
{totalPages > 1 && ( {(totalPages > 1 || total > 6) && (
<div className="mt-8 flex flex-col sm:flex-row items-center justify-center gap-4"> <div className="mt-8 flex flex-col sm:flex-row items-center justify-center gap-4">
{totalPages > 1 && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button
variant="secondary" variant="secondary"
@@ -342,10 +371,33 @@ const BrowseTeamsPage: FC = (): ReactElement => {
<Icon icon="mdi:chevron-right" className="text-xl" /> <Icon icon="mdi:chevron-right" className="text-xl" />
</Button> </Button>
</div> </div>
)}
<div className="flex items-center gap-3">
{totalPages > 1 && (
<span className="text-sm text-gray-500 dark:text-gray-400"> <span className="text-sm text-gray-500 dark:text-gray-400">
Page {currentPage} of {totalPages} Page {currentPage} of {totalPages}
</span> </span>
)}
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500 dark:text-gray-400">Show:</span>
<select
value={perPage}
onChange={(e) => {
setPerPage(Number(e.target.value));
setCurrentPage(1);
}}
className="h-10 px-3 pr-8 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
{PER_PAGE_OPTIONS.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
</div>
</div> </div>
)} )}
</> </>