/** * Component: Sticky Pagination with Progress Bar * Documentation: documentation/frontend/components.md */ 'use client'; import React, { useState, useEffect, useRef } from 'react'; import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline'; interface StickyPaginationProps { currentPage: number; totalPages: number; onPageChange: (page: number) => void; sectionRef: React.RefObject; label: string; // e.g., "Popular Audiobooks" footerRef?: React.RefObject; // Optional footer ref to avoid overlap } export function StickyPagination({ currentPage, totalPages, onPageChange, sectionRef, label, footerRef, }: StickyPaginationProps) { const [isVisible, setIsVisible] = useState(false); const [isFooterVisible, setIsFooterVisible] = useState(false); const [jumpPage, setJumpPage] = useState(currentPage.toString()); // Update jump page input when current page changes externally useEffect(() => { setJumpPage(currentPage.toString()); }, [currentPage]); // Intersection Observer to show/hide pagination based on section visibility useEffect(() => { if (!sectionRef.current) return; const observer = new IntersectionObserver( ([entry]) => { // Show pagination when section is in viewport setIsVisible(entry.isIntersecting && entry.intersectionRatio > 0.1); }, { threshold: [0, 0.1, 0.5, 1], rootMargin: '-60px 0px -60px 0px', // Account for header/footer } ); observer.observe(sectionRef.current); return () => observer.disconnect(); }, [sectionRef]); // Footer observer to hide pagination when footer is visible useEffect(() => { if (!footerRef?.current) return; const observer = new IntersectionObserver( ([entry]) => { // Hide pagination when footer is in viewport setIsFooterVisible(entry.isIntersecting); }, { threshold: [0, 0.1], rootMargin: '0px', } ); observer.observe(footerRef.current); return () => observer.disconnect(); }, [footerRef]); if (totalPages <= 1) { return null; } const handlePrevious = () => { if (currentPage > 1) { onPageChange(currentPage - 1); } }; const handleNext = () => { if (currentPage < totalPages) { onPageChange(currentPage + 1); } }; const handleJumpSubmit = (e: React.FormEvent) => { e.preventDefault(); const page = parseInt(jumpPage, 10); if (!isNaN(page) && page >= 1 && page <= totalPages) { onPageChange(page); } else { // Reset to current page if invalid setJumpPage(currentPage.toString()); } }; // Final visibility: show when section is visible AND footer is not visible const shouldShow = isVisible && !isFooterVisible; return (
{/* Section Label - Hidden on small screens */}
{label}
{/* Previous Button */} {/* Page Info & Jump */}
Page
setJumpPage(e.target.value)} onBlur={handleJumpSubmit} className="w-10 px-1.5 py-0.5 text-center text-sm font-medium rounded bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-transparent" aria-label="Current page" />
of {totalPages}
{/* Next Button */}
); }