mirror of
				https://github.com/walkxcode/dashboard-icons.git
				synced 2025-10-27 13:39:03 +08:00 
			
		
		
		
	Compare commits
	
		
			8 Commits
		
	
	
		
			renovate/m
			...
			feat/pagin
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | f20de48e1a | ||
|   | f32b62009b | ||
|   | e9fe6d3842 | ||
|   | eb799c3637 | ||
|   | 3e2709e7a8 | ||
|   | 245033befc | ||
|   | 9949f663eb | ||
|   | a579d41f45 | 
| @@ -19,7 +19,8 @@ import { Input } from "@/components/ui/input" | |||||||
| import { Separator } from "@/components/ui/separator" | import { Separator } from "@/components/ui/separator" | ||||||
| import { BASE_URL } from "@/constants" | import { BASE_URL } from "@/constants" | ||||||
| import type { Icon, IconSearchProps } from "@/types/icons" | import type { Icon, IconSearchProps } from "@/types/icons" | ||||||
| import { ArrowDownAZ, ArrowUpZA, Calendar, Filter, Search, SortAsc, X } from "lucide-react" | import { AnimatePresence, motion } from "framer-motion" | ||||||
|  | import { ArrowDownAZ, ArrowUpZA, Calendar, ChevronLeft, ChevronRight, Filter, Search, SortAsc, X } from "lucide-react" | ||||||
| import { useTheme } from "next-themes" | import { useTheme } from "next-themes" | ||||||
| import Image from "next/image" | import Image from "next/image" | ||||||
| import Link from "next/link" | import Link from "next/link" | ||||||
| @@ -30,21 +31,84 @@ import { toast } from "sonner" | |||||||
|  |  | ||||||
| type SortOption = "relevance" | "alphabetical-asc" | "alphabetical-desc" | "newest" | type SortOption = "relevance" | "alphabetical-asc" | "alphabetical-desc" | "newest" | ||||||
|  |  | ||||||
|  | // Get the display rows count based on viewport size | ||||||
|  | function getDefaultRowsPerPage() { | ||||||
|  | 	if (typeof window === "undefined") return 3 // Default for SSR | ||||||
|  |  | ||||||
|  | 	// Calculate based on viewport height and width | ||||||
|  | 	const vh = window.innerHeight | ||||||
|  | 	const vw = window.innerWidth | ||||||
|  |  | ||||||
|  | 	// Determine number of columns based on viewport width | ||||||
|  | 	let columns = 2 // Default for small screens (sm) | ||||||
|  | 	if (vw >= 1280) | ||||||
|  | 		columns = 8 // xl breakpoint | ||||||
|  | 	else if (vw >= 1024) | ||||||
|  | 		columns = 6 // lg breakpoint | ||||||
|  | 	else if (vw >= 768) | ||||||
|  | 		columns = 4 // md breakpoint | ||||||
|  | 	else if (vw >= 640) columns = 3 // sm breakpoint | ||||||
|  |  | ||||||
|  | 	// Calculate rows (accounting for pagination UI space) | ||||||
|  | 	const rowHeight = 130 // Approximate height of each row in pixels | ||||||
|  | 	const availableHeight = vh * 0.6 // 60% of viewport height | ||||||
|  |  | ||||||
|  | 	// Ensure at least 1 row, maximum 5 rows | ||||||
|  | 	return Math.max(1, Math.min(5, Math.floor(availableHeight / rowHeight))) | ||||||
|  | } | ||||||
|  |  | ||||||
| export function IconSearch({ icons }: IconSearchProps) { | export function IconSearch({ icons }: IconSearchProps) { | ||||||
| 	const searchParams = useSearchParams() | 	const searchParams = useSearchParams() | ||||||
| 	const initialQuery = searchParams.get("q") | 	const initialQuery = searchParams.get("q") | ||||||
| 	const initialCategories = searchParams.getAll("category") | 	const initialCategories = searchParams.getAll("category") | ||||||
| 	const initialSort = (searchParams.get("sort") as SortOption) || "relevance" | 	const initialSort = (searchParams.get("sort") as SortOption) || "relevance" | ||||||
|  | 	const initialPage = Number(searchParams.get("page") || "1") | ||||||
| 	const router = useRouter() | 	const router = useRouter() | ||||||
| 	const pathname = usePathname() | 	const pathname = usePathname() | ||||||
| 	const [searchQuery, setSearchQuery] = useState(initialQuery ?? "") | 	const [searchQuery, setSearchQuery] = useState(initialQuery ?? "") | ||||||
| 	const [debouncedQuery, setDebouncedQuery] = useState(initialQuery ?? "") | 	const [debouncedQuery, setDebouncedQuery] = useState(initialQuery ?? "") | ||||||
| 	const [selectedCategories, setSelectedCategories] = useState<string[]>(initialCategories ?? []) | 	const [selectedCategories, setSelectedCategories] = useState<string[]>(initialCategories ?? []) | ||||||
| 	const [sortOption, setSortOption] = useState<SortOption>(initialSort) | 	const [sortOption, setSortOption] = useState<SortOption>(initialSort) | ||||||
|  | 	const [currentPage, setCurrentPage] = useState(initialPage) | ||||||
|  | 	const [iconsPerPage, setIconsPerPage] = useState(getDefaultRowsPerPage() * 8) // Default cols is 8 for xl screens | ||||||
| 	const timeoutRef = useRef<NodeJS.Timeout | null>(null) | 	const timeoutRef = useRef<NodeJS.Timeout | null>(null) | ||||||
| 	const { resolvedTheme } = useTheme() | 	const { resolvedTheme } = useTheme() | ||||||
| 	const [isLazyRequestSubmitted, setIsLazyRequestSubmitted] = useState(false) | 	const [isLazyRequestSubmitted, setIsLazyRequestSubmitted] = useState(false) | ||||||
|  |  | ||||||
|  | 	// Add resize observer to update iconsPerPage when window size changes | ||||||
|  | 	useEffect(() => { | ||||||
|  | 		const updateIconsPerPage = () => { | ||||||
|  | 			const rows = getDefaultRowsPerPage() | ||||||
|  |  | ||||||
|  | 			// Determine columns based on current viewport | ||||||
|  | 			const vw = window.innerWidth | ||||||
|  | 			let columns = 2 // Default for small screens | ||||||
|  | 			if (vw >= 1280) | ||||||
|  | 				columns = 8 // xl breakpoint | ||||||
|  | 			else if (vw >= 1024) | ||||||
|  | 				columns = 6 // lg breakpoint | ||||||
|  | 			else if (vw >= 768) | ||||||
|  | 				columns = 4 // md breakpoint | ||||||
|  | 			else if (vw >= 640) columns = 3 // sm breakpoint | ||||||
|  |  | ||||||
|  | 			setIconsPerPage(rows * columns) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Initial setup | ||||||
|  | 		updateIconsPerPage() | ||||||
|  |  | ||||||
|  | 		// Add resize listener | ||||||
|  | 		window.addEventListener("resize", updateIconsPerPage) | ||||||
|  |  | ||||||
|  | 		// Cleanup | ||||||
|  | 		return () => window.removeEventListener("resize", updateIconsPerPage) | ||||||
|  | 	}, []) | ||||||
|  |  | ||||||
|  | 	// Reset page when search parameters change | ||||||
|  | 	useEffect(() => { | ||||||
|  | 		setCurrentPage(1) | ||||||
|  | 	}, []) | ||||||
|  |  | ||||||
| 	useEffect(() => { | 	useEffect(() => { | ||||||
| 		const timer = setTimeout(() => { | 		const timer = setTimeout(() => { | ||||||
| 			setDebouncedQuery(searchQuery) | 			setDebouncedQuery(searchQuery) | ||||||
| @@ -138,7 +202,7 @@ export function IconSearch({ icons }: IconSearchProps) { | |||||||
| 	}, [filterIcons, debouncedQuery, selectedCategories, sortOption]) | 	}, [filterIcons, debouncedQuery, selectedCategories, sortOption]) | ||||||
|  |  | ||||||
| 	const updateResults = useCallback( | 	const updateResults = useCallback( | ||||||
| 		(query: string, categories: string[], sort: SortOption) => { | 		(query: string, categories: string[], sort: SortOption, page = 1) => { | ||||||
| 			const params = new URLSearchParams() | 			const params = new URLSearchParams() | ||||||
| 			if (query) params.set("q", query) | 			if (query) params.set("q", query) | ||||||
|  |  | ||||||
| @@ -152,12 +216,32 @@ export function IconSearch({ icons }: IconSearchProps) { | |||||||
| 				params.set("sort", sort) | 				params.set("sort", sort) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  | 			// Add page parameter if not the first page | ||||||
|  | 			if (page > 1) { | ||||||
|  | 				params.set("page", page.toString()) | ||||||
|  | 			} | ||||||
|  |  | ||||||
| 			const newUrl = params.toString() ? `${pathname}?${params.toString()}` : pathname | 			const newUrl = params.toString() ? `${pathname}?${params.toString()}` : pathname | ||||||
| 			router.push(newUrl, { scroll: false }) | 			router.push(newUrl, { scroll: false }) | ||||||
| 		}, | 		}, | ||||||
| 		[pathname, router, initialSort], | 		[pathname, router, initialSort], | ||||||
| 	) | 	) | ||||||
|  |  | ||||||
|  | 	// Validate currentPage when iconsPerPage or filteredIcons change | ||||||
|  | 	useEffect(() => { | ||||||
|  | 		// Calculate new total pages | ||||||
|  | 		const totalPages = Math.ceil(filteredIcons.length / iconsPerPage) | ||||||
|  |  | ||||||
|  | 		// If current page is out of bounds, adjust it | ||||||
|  | 		if (currentPage > totalPages && totalPages > 0) { | ||||||
|  | 			// Update current page state | ||||||
|  | 			setCurrentPage(totalPages) | ||||||
|  |  | ||||||
|  | 			// Update URL to reflect the adjusted page | ||||||
|  | 			updateResults(searchQuery, selectedCategories, sortOption, totalPages) | ||||||
|  | 		} | ||||||
|  | 	}, [iconsPerPage, filteredIcons.length, currentPage, searchQuery, selectedCategories, sortOption, updateResults]) | ||||||
|  |  | ||||||
| 	const handleSearch = useCallback( | 	const handleSearch = useCallback( | ||||||
| 		(query: string) => { | 		(query: string) => { | ||||||
| 			setSearchQuery(query) | 			setSearchQuery(query) | ||||||
| @@ -197,11 +281,20 @@ export function IconSearch({ icons }: IconSearchProps) { | |||||||
| 		[updateResults, searchQuery, selectedCategories], | 		[updateResults, searchQuery, selectedCategories], | ||||||
| 	) | 	) | ||||||
|  |  | ||||||
|  | 	const handlePageChange = useCallback( | ||||||
|  | 		(page: number) => { | ||||||
|  | 			setCurrentPage(page) | ||||||
|  | 			updateResults(searchQuery, selectedCategories, sortOption, page) | ||||||
|  | 		}, | ||||||
|  | 		[updateResults, searchQuery, selectedCategories, sortOption], | ||||||
|  | 	) | ||||||
|  |  | ||||||
| 	const clearFilters = useCallback(() => { | 	const clearFilters = useCallback(() => { | ||||||
| 		setSearchQuery("") | 		setSearchQuery("") | ||||||
| 		setSelectedCategories([]) | 		setSelectedCategories([]) | ||||||
| 		setSortOption("relevance") | 		setSortOption("relevance") | ||||||
| 		updateResults("", [], "relevance") | 		setCurrentPage(1) | ||||||
|  | 		updateResults("", [], "relevance", 1) | ||||||
| 	}, [updateResults]) | 	}, [updateResults]) | ||||||
|  |  | ||||||
| 	useEffect(() => { | 	useEffect(() => { | ||||||
| @@ -435,7 +528,14 @@ export function IconSearch({ icons }: IconSearchProps) { | |||||||
| 						</div> | 						</div> | ||||||
| 					</div> | 					</div> | ||||||
|  |  | ||||||
| 					<IconsGrid filteredIcons={filteredIcons} matchedAliases={matchedAliases} /> | 					<IconsGrid | ||||||
|  | 						filteredIcons={filteredIcons} | ||||||
|  | 						matchedAliases={matchedAliases} | ||||||
|  | 						currentPage={currentPage} | ||||||
|  | 						iconsPerPage={iconsPerPage} | ||||||
|  | 						onPageChange={handlePageChange} | ||||||
|  | 						totalIcons={filteredIcons.length} | ||||||
|  | 					/> | ||||||
| 				</> | 				</> | ||||||
| 			)} | 			)} | ||||||
| 		</> | 		</> | ||||||
| @@ -445,15 +545,13 @@ export function IconSearch({ icons }: IconSearchProps) { | |||||||
| function IconCard({ | function IconCard({ | ||||||
| 	name, | 	name, | ||||||
| 	data: iconData, | 	data: iconData, | ||||||
| 	matchedAlias, |  | ||||||
| }: { | }: { | ||||||
| 	name: string | 	name: string | ||||||
| 	data: Icon | 	data: Icon | ||||||
| 	matchedAlias?: string | null |  | ||||||
| }) { | }) { | ||||||
| 	return ( | 	return ( | ||||||
| 		<MagicCard className="rounded-md shadow-md"> | 		<MagicCard className="rounded-md shadow-md cursor-pointer"> | ||||||
| 			<Link prefetch={false} href={`/icons/${name}`} className="group flex flex-col items-center p-3 sm:p-4 cursor-pointer"> | 			<Link prefetch={false} href={`/icons/${name}`} className="group flex flex-col items-center p-3 sm:p-4"> | ||||||
| 				<div className="relative h-12 w-12 sm:h-16 sm:w-16 mb-2"> | 				<div className="relative h-12 w-12 sm:h-16 sm:w-16 mb-2"> | ||||||
| 					<Image | 					<Image | ||||||
| 						src={`${BASE_URL}/${iconData.base}/${name}.${iconData.base}`} | 						src={`${BASE_URL}/${iconData.base}/${name}.${iconData.base}`} | ||||||
| @@ -465,8 +563,6 @@ function IconCard({ | |||||||
| 				<span className="text-xs sm:text-sm text-center truncate w-full capitalize group- dark:group-hover:text-rose-400 transition-colors duration-200 font-medium"> | 				<span className="text-xs sm:text-sm text-center truncate w-full capitalize group- dark:group-hover:text-rose-400 transition-colors duration-200 font-medium"> | ||||||
| 					{name.replace(/-/g, " ")} | 					{name.replace(/-/g, " ")} | ||||||
| 				</span> | 				</span> | ||||||
|  |  | ||||||
| 				{matchedAlias && <span className="text-[10px] text-center truncate w-full mt-1">Alias: {matchedAlias}</span>} |  | ||||||
| 			</Link> | 			</Link> | ||||||
| 		</MagicCard> | 		</MagicCard> | ||||||
| 	) | 	) | ||||||
| @@ -475,17 +571,251 @@ function IconCard({ | |||||||
| interface IconsGridProps { | interface IconsGridProps { | ||||||
| 	filteredIcons: { name: string; data: Icon }[] | 	filteredIcons: { name: string; data: Icon }[] | ||||||
| 	matchedAliases: Record<string, string> | 	matchedAliases: Record<string, string> | ||||||
|  | 	currentPage: number | ||||||
|  | 	iconsPerPage: number | ||||||
|  | 	onPageChange: (page: number) => void | ||||||
|  | 	totalIcons: number | ||||||
| } | } | ||||||
|  |  | ||||||
| function IconsGrid({ filteredIcons, matchedAliases }: IconsGridProps) { | function IconsGrid({ filteredIcons, matchedAliases, currentPage, iconsPerPage, onPageChange, totalIcons }: IconsGridProps) { | ||||||
|  | 	// Calculate pagination values | ||||||
|  | 	const totalPages = Math.ceil(totalIcons / iconsPerPage) | ||||||
|  | 	const indexOfLastIcon = currentPage * iconsPerPage | ||||||
|  | 	const indexOfFirstIcon = indexOfLastIcon - iconsPerPage | ||||||
|  | 	const currentIcons = filteredIcons.slice(indexOfFirstIcon, indexOfLastIcon) | ||||||
|  |  | ||||||
|  | 	// Calculate letter ranges for each page | ||||||
|  | 	const getLetterRange = (pageNum: number) => { | ||||||
|  | 		if (filteredIcons.length === 0) return "" | ||||||
|  | 		const start = (pageNum - 1) * iconsPerPage | ||||||
|  | 		const end = Math.min(start + iconsPerPage - 1, filteredIcons.length - 1) | ||||||
|  |  | ||||||
|  | 		if (start >= filteredIcons.length) return "" | ||||||
|  |  | ||||||
|  | 		const firstLetter = filteredIcons[start].name.charAt(0).toUpperCase() | ||||||
|  | 		const lastLetter = filteredIcons[end].name.charAt(0).toUpperCase() | ||||||
|  |  | ||||||
|  | 		return firstLetter === lastLetter ? firstLetter : `${firstLetter} - ${lastLetter}` | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Get current page letter range | ||||||
|  | 	const currentLetterRange = getLetterRange(currentPage) | ||||||
|  |  | ||||||
|  | 	// Handle direct page input | ||||||
|  | 	const [pageInput, setPageInput] = useState(currentPage.toString()) | ||||||
|  |  | ||||||
|  | 	useEffect(() => { | ||||||
|  | 		setPageInput(currentPage.toString()) | ||||||
|  | 	}, [currentPage]) | ||||||
|  |  | ||||||
|  | 	const handlePageInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||||||
|  | 		setPageInput(e.target.value) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	const handlePageInputSubmit = (e: React.FormEvent) => { | ||||||
|  | 		e.preventDefault() | ||||||
|  | 		const pageNumber = Number.parseInt(pageInput) | ||||||
|  | 		if (!Number.isNaN(pageNumber) && pageNumber >= 1 && pageNumber <= totalPages) { | ||||||
|  | 			onPageChange(pageNumber) | ||||||
|  | 		} else { | ||||||
|  | 			// Reset to current page if invalid | ||||||
|  | 			setPageInput(currentPage.toString()) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return ( | 	return ( | ||||||
| 		<> | 		<> | ||||||
| 			<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4 mt-2"> | 			<AnimatePresence mode="wait"> | ||||||
| 				{filteredIcons.slice(0, 120).map(({ name, data }) => ( | 				<motion.div | ||||||
| 					<IconCard key={name} name={name} data={data} matchedAlias={matchedAliases[name] || null} /> | 					key={currentPage} | ||||||
| 				))} | 					initial={{ opacity: 0, y: 20 }} | ||||||
| 			</div> | 					animate={{ opacity: 1, y: 0 }} | ||||||
| 			{filteredIcons.length > 120 && <p className="text-sm text-muted-foreground">And {filteredIcons.length - 120} more...</p>} | 					exit={{ opacity: 0, y: -20 }} | ||||||
|  | 					transition={{ duration: 0.3 }} | ||||||
|  | 					className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4 mt-2" | ||||||
|  | 				> | ||||||
|  | 					{currentIcons.map(({ name, data }) => ( | ||||||
|  | 						<IconCard key={name} name={name} data={data} /> | ||||||
|  | 					))} | ||||||
|  | 				</motion.div> | ||||||
|  | 			</AnimatePresence> | ||||||
|  |  | ||||||
|  | 			{totalPages > 1 && ( | ||||||
|  | 				<div className="flex flex-col gap-4 mt-8"> | ||||||
|  | 					{/* Mobile view: centered content */} | ||||||
|  | 					<div className="text-sm text-muted-foreground text-center md:text-left md:hidden"> | ||||||
|  | 						Showing {indexOfFirstIcon + 1}-{Math.min(indexOfLastIcon, totalIcons)} of {totalIcons} icons | ||||||
|  | 						{currentLetterRange && <span className="ml-2 font-medium">({currentLetterRange})</span>} | ||||||
|  | 					</div> | ||||||
|  |  | ||||||
|  | 					{/* Desktop view layout */} | ||||||
|  | 					<div className="hidden md:flex justify-between items-center"> | ||||||
|  | 						<div className="text-sm text-muted-foreground"> | ||||||
|  | 							Showing {indexOfFirstIcon + 1}-{Math.min(indexOfLastIcon, totalIcons)} of {totalIcons} icons | ||||||
|  | 							{currentLetterRange && <span className="ml-2 font-medium">({currentLetterRange})</span>} | ||||||
|  | 						</div> | ||||||
|  |  | ||||||
|  | 						<div className="flex items-center gap-4"> | ||||||
|  | 							{/* Page input and total count */} | ||||||
|  | 							<form onSubmit={handlePageInputSubmit} className="flex items-center gap-2"> | ||||||
|  | 								<Input | ||||||
|  | 									type="number" | ||||||
|  | 									min={1} | ||||||
|  | 									max={totalPages} | ||||||
|  | 									value={pageInput} | ||||||
|  | 									onChange={handlePageInputChange} | ||||||
|  | 									className="w-16 h-8 text-center cursor-text" | ||||||
|  | 									aria-label="Go to page" | ||||||
|  | 								/> | ||||||
|  | 								<span className="text-sm whitespace-nowrap">of {totalPages}</span> | ||||||
|  | 								<Button type="submit" size="sm" variant="outline" className="h-8 cursor-pointer"> | ||||||
|  | 									Go | ||||||
|  | 								</Button> | ||||||
|  | 							</form> | ||||||
|  |  | ||||||
|  | 							{/* Pagination controls */} | ||||||
|  | 							<div className="flex items-center"> | ||||||
|  | 								<Button | ||||||
|  | 									onClick={() => onPageChange(currentPage - 1)} | ||||||
|  | 									disabled={currentPage === 1} | ||||||
|  | 									size="sm" | ||||||
|  | 									variant="outline" | ||||||
|  | 									className="h-8 rounded-r-none cursor-pointer" | ||||||
|  | 									aria-label="Previous page" | ||||||
|  | 								> | ||||||
|  | 									<ChevronLeft className="h-4 w-4" /> | ||||||
|  | 								</Button> | ||||||
|  |  | ||||||
|  | 								<div className="flex items-center overflow-hidden"> | ||||||
|  | 									{Array.from({ length: Math.min(5, totalPages) }, (_, i) => { | ||||||
|  | 										// Show pages around current page | ||||||
|  | 										let pageNum: number | ||||||
|  | 										if (totalPages <= 5) { | ||||||
|  | 											pageNum = i + 1 | ||||||
|  | 										} else if (currentPage <= 3) { | ||||||
|  | 											pageNum = i + 1 | ||||||
|  | 										} else if (currentPage >= totalPages - 2) { | ||||||
|  | 											pageNum = totalPages - 4 + i | ||||||
|  | 										} else { | ||||||
|  | 											pageNum = currentPage - 2 + i | ||||||
|  | 										} | ||||||
|  |  | ||||||
|  | 										// Calculate letter range for this page | ||||||
|  | 										const letterRange = getLetterRange(pageNum) | ||||||
|  |  | ||||||
|  | 										return ( | ||||||
|  | 											<Button | ||||||
|  | 												key={pageNum} | ||||||
|  | 												onClick={() => onPageChange(pageNum)} | ||||||
|  | 												variant={pageNum === currentPage ? "default" : "outline"} | ||||||
|  | 												size="sm" | ||||||
|  | 												className={`h-8 w-8 p-0 rounded-none relative group cursor-pointer transition-colors duration-200 ${ | ||||||
|  | 													pageNum === currentPage ? "font-medium" : "" | ||||||
|  | 												}`} | ||||||
|  | 												aria-label={`Page ${pageNum}`} | ||||||
|  | 												aria-current={pageNum === currentPage ? "page" : undefined} | ||||||
|  | 											> | ||||||
|  | 												{pageNum} | ||||||
|  | 												{letterRange && ( | ||||||
|  | 													<span className="absolute -top-8 left-1/2 transform -translate-x-1/2 bg-popover text-popover-foreground px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity shadow-md whitespace-nowrap"> | ||||||
|  | 														{letterRange} | ||||||
|  | 													</span> | ||||||
|  | 												)} | ||||||
|  | 											</Button> | ||||||
|  | 										) | ||||||
|  | 									})} | ||||||
|  | 								</div> | ||||||
|  |  | ||||||
|  | 								<Button | ||||||
|  | 									onClick={() => onPageChange(currentPage + 1)} | ||||||
|  | 									disabled={currentPage === totalPages} | ||||||
|  | 									size="sm" | ||||||
|  | 									variant="outline" | ||||||
|  | 									className="h-8 rounded-l-none cursor-pointer" | ||||||
|  | 									aria-label="Next page" | ||||||
|  | 								> | ||||||
|  | 									<ChevronRight className="h-4 w-4" /> | ||||||
|  | 								</Button> | ||||||
|  | 							</div> | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  |  | ||||||
|  | 					{/* Mobile-only pagination layout - centered */} | ||||||
|  | 					<div className="flex flex-col items-center gap-4 md:hidden"> | ||||||
|  | 						{/* Mobile pagination controls */} | ||||||
|  | 						<div className="flex items-center"> | ||||||
|  | 							<Button | ||||||
|  | 								onClick={() => onPageChange(currentPage - 1)} | ||||||
|  | 								disabled={currentPage === 1} | ||||||
|  | 								size="sm" | ||||||
|  | 								variant="outline" | ||||||
|  | 								className="h-8 rounded-r-none cursor-pointer" | ||||||
|  | 								aria-label="Previous page" | ||||||
|  | 							> | ||||||
|  | 								<ChevronLeft className="h-4 w-4" /> | ||||||
|  | 							</Button> | ||||||
|  |  | ||||||
|  | 							<div className="flex items-center overflow-hidden"> | ||||||
|  | 								{Array.from({ length: Math.min(5, totalPages) }, (_, i) => { | ||||||
|  | 									// Show pages around current page - same logic as desktop | ||||||
|  | 									let pageNum: number | ||||||
|  | 									if (totalPages <= 5) { | ||||||
|  | 										pageNum = i + 1 | ||||||
|  | 									} else if (currentPage <= 3) { | ||||||
|  | 										pageNum = i + 1 | ||||||
|  | 									} else if (currentPage >= totalPages - 2) { | ||||||
|  | 										pageNum = totalPages - 4 + i | ||||||
|  | 									} else { | ||||||
|  | 										pageNum = currentPage - 2 + i | ||||||
|  | 									} | ||||||
|  |  | ||||||
|  | 									return ( | ||||||
|  | 										<Button | ||||||
|  | 											key={pageNum} | ||||||
|  | 											onClick={() => onPageChange(pageNum)} | ||||||
|  | 											variant={pageNum === currentPage ? "default" : "outline"} | ||||||
|  | 											size="sm" | ||||||
|  | 											className={`h-8 w-8 p-0 rounded-none cursor-pointer ${pageNum === currentPage ? "font-medium" : ""}`} | ||||||
|  | 											aria-label={`Page ${pageNum}`} | ||||||
|  | 											aria-current={pageNum === currentPage ? "page" : undefined} | ||||||
|  | 										> | ||||||
|  | 											{pageNum} | ||||||
|  | 										</Button> | ||||||
|  | 									) | ||||||
|  | 								})} | ||||||
|  | 							</div> | ||||||
|  |  | ||||||
|  | 							<Button | ||||||
|  | 								onClick={() => onPageChange(currentPage + 1)} | ||||||
|  | 								disabled={currentPage === totalPages} | ||||||
|  | 								size="sm" | ||||||
|  | 								variant="outline" | ||||||
|  | 								className="h-8 rounded-l-none cursor-pointer" | ||||||
|  | 								aria-label="Next page" | ||||||
|  | 							> | ||||||
|  | 								<ChevronRight className="h-4 w-4" /> | ||||||
|  | 							</Button> | ||||||
|  | 						</div> | ||||||
|  |  | ||||||
|  | 						{/* Mobile page input */} | ||||||
|  | 						<form onSubmit={handlePageInputSubmit} className="flex items-center gap-2"> | ||||||
|  | 							<Input | ||||||
|  | 								type="number" | ||||||
|  | 								min={1} | ||||||
|  | 								max={totalPages} | ||||||
|  | 								value={pageInput} | ||||||
|  | 								onChange={handlePageInputChange} | ||||||
|  | 								className="w-16 h-8 text-center cursor-text" | ||||||
|  | 								aria-label="Go to page" | ||||||
|  | 							/> | ||||||
|  | 							<span className="text-sm whitespace-nowrap">of {totalPages}</span> | ||||||
|  | 							<Button type="submit" size="sm" variant="outline" className="h-8 cursor-pointer"> | ||||||
|  | 								Go | ||||||
|  | 							</Button> | ||||||
|  | 						</form> | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 			)} | ||||||
| 		</> | 		</> | ||||||
| 	) | 	) | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user