- Migrate from vanilla JavaScript SPA to Next.js 16 with App Router - Add server-side rendering for all pages (Home, Compare, Rankings) - Create individual school pages with dynamic routing (/school/[urn]) - Implement Chart.js and Leaflet map integrations - Add comprehensive SEO with sitemap, robots.txt, and JSON-LD - Set up Docker multi-service architecture (PostgreSQL, FastAPI, Next.js) - Update CI/CD pipeline to build both backend and frontend images - Fix Dockerfile to include devDependencies for TypeScript compilation - Add Jest testing configuration - Implement performance optimizations (code splitting, caching) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
127 lines
3.0 KiB
TypeScript
127 lines
3.0 KiB
TypeScript
/**
|
|
* Pagination Component
|
|
* Navigate through pages of results
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
|
|
import styles from './Pagination.module.css';
|
|
|
|
interface PaginationProps {
|
|
currentPage: number;
|
|
totalPages: number;
|
|
total: number;
|
|
}
|
|
|
|
export function Pagination({ currentPage, totalPages, total }: PaginationProps) {
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const searchParams = useSearchParams();
|
|
|
|
if (totalPages <= 1) return null;
|
|
|
|
const goToPage = (page: number) => {
|
|
const params = new URLSearchParams(searchParams);
|
|
params.set('page', page.toString());
|
|
router.push(`${pathname}?${params.toString()}`);
|
|
};
|
|
|
|
const handlePrevious = () => {
|
|
if (currentPage > 1) {
|
|
goToPage(currentPage - 1);
|
|
}
|
|
};
|
|
|
|
const handleNext = () => {
|
|
if (currentPage < totalPages) {
|
|
goToPage(currentPage + 1);
|
|
}
|
|
};
|
|
|
|
// Generate page numbers to show
|
|
const getPageNumbers = () => {
|
|
const pages: (number | string)[] = [];
|
|
const maxVisible = 7;
|
|
|
|
if (totalPages <= maxVisible) {
|
|
// Show all pages
|
|
for (let i = 1; i <= totalPages; i++) {
|
|
pages.push(i);
|
|
}
|
|
} else {
|
|
// Show first, last, and pages around current
|
|
pages.push(1);
|
|
|
|
if (currentPage > 3) {
|
|
pages.push('...');
|
|
}
|
|
|
|
const start = Math.max(2, currentPage - 1);
|
|
const end = Math.min(totalPages - 1, currentPage + 1);
|
|
|
|
for (let i = start; i <= end; i++) {
|
|
pages.push(i);
|
|
}
|
|
|
|
if (currentPage < totalPages - 2) {
|
|
pages.push('...');
|
|
}
|
|
|
|
pages.push(totalPages);
|
|
}
|
|
|
|
return pages;
|
|
};
|
|
|
|
const pageNumbers = getPageNumbers();
|
|
|
|
return (
|
|
<div className={styles.pagination}>
|
|
<div className={styles.info}>
|
|
Showing page {currentPage} of {totalPages} ({total.toLocaleString()} total schools)
|
|
</div>
|
|
|
|
<div className={styles.controls}>
|
|
<button
|
|
onClick={handlePrevious}
|
|
disabled={currentPage === 1}
|
|
className={styles.navButton}
|
|
aria-label="Previous page"
|
|
>
|
|
← Previous
|
|
</button>
|
|
|
|
<div className={styles.pages}>
|
|
{pageNumbers.map((page, index) => (
|
|
typeof page === 'number' ? (
|
|
<button
|
|
key={index}
|
|
onClick={() => goToPage(page)}
|
|
className={page === currentPage ? styles.pageButtonActive : styles.pageButton}
|
|
aria-label={`Go to page ${page}`}
|
|
aria-current={page === currentPage ? 'page' : undefined}
|
|
>
|
|
{page}
|
|
</button>
|
|
) : (
|
|
<span key={index} className={styles.ellipsis}>
|
|
{page}
|
|
</span>
|
|
)
|
|
))}
|
|
</div>
|
|
|
|
<button
|
|
onClick={handleNext}
|
|
disabled={currentPage === totalPages}
|
|
className={styles.navButton}
|
|
aria-label="Next page"
|
|
>
|
|
Next →
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|