"use client"; import { useRef, useState, useEffect, useMemo, ReactNode } from "react"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { cn } from "@/utils/cn"; import { useIsMobile, useIsTablet } from "@/hooks/useMediaQuery"; interface PagedGridCarouselProps { items: T[]; renderItem: (item: T, index: number) => ReactNode; keyExtractor: (item: T) => string; itemsPerPage?: number; columns?: number; rows?: number; gap?: string; className?: string; } export function PagedGridCarousel({ items, renderItem, keyExtractor, itemsPerPage = 6, columns = 3, rows = 2, gap = "gap-2", className, }: PagedGridCarouselProps) { const scrollRef = useRef(null); const [canScrollLeft, setCanScrollLeft] = useState(false); const [canScrollRight, setCanScrollRight] = useState(false); const [currentPage, setCurrentPage] = useState(0); const isMobile = useIsMobile(); const isTablet = useIsTablet(); const isMobileOrTablet = isMobile || isTablet; // Group items into pages const pages = useMemo(() => { const result: T[][] = []; for (let i = 0; i < items.length; i += itemsPerPage) { result.push(items.slice(i, i + itemsPerPage)); } return result; }, [items, itemsPerPage]); // Check scroll state const checkScroll = () => { const el = scrollRef.current; if (!el) return; setCanScrollLeft(el.scrollLeft > 0); setCanScrollRight(el.scrollLeft < el.scrollWidth - el.clientWidth - 1); // Update current page based on scroll position const pageWidth = el.clientWidth; const newPage = Math.round(el.scrollLeft / pageWidth); setCurrentPage(newPage); }; useEffect(() => { checkScroll(); const el = scrollRef.current; if (el) { el.addEventListener("scroll", checkScroll); window.addEventListener("resize", checkScroll); } return () => { if (el) el.removeEventListener("scroll", checkScroll); window.removeEventListener("resize", checkScroll); }; }, [pages]); const scroll = (direction: "left" | "right") => { const el = scrollRef.current; if (!el) return; const scrollAmount = el.clientWidth; el.scrollBy({ left: direction === "left" ? -scrollAmount : scrollAmount, behavior: "smooth", }); }; const goToPage = (pageIndex: number) => { const el = scrollRef.current; if (el) { el.scrollTo({ left: pageIndex * el.clientWidth, behavior: "smooth", }); } }; if (items.length === 0) return null; return (
{/* Left Arrow (desktop only) */} {!isMobileOrTablet && canScrollLeft && ( )} {/* Scrollable Container */}
{pages.map((page, pageIndex) => (
{page.map((item, itemIndex) => (
{renderItem( item, pageIndex * itemsPerPage + itemIndex )}
))} {/* Fill empty slots */} {page.length < itemsPerPage && Array.from({ length: itemsPerPage - page.length, }).map((_, i) =>
)}
))}
{/* Right Arrow (desktop only) */} {!isMobileOrTablet && canScrollRight && ( )} {/* Page indicators */} {pages.length > 1 && (
{pages.map((_, index) => (
)}
); }