Complete Next.js migration with SSR and Docker deployment
Some checks failed
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 1m26s
Build and Push Docker Images / Build Frontend (Next.js) (push) Failing after 1m48s
Build and Push Docker Images / Trigger Portainer Update (push) Has been skipped

- 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>
This commit is contained in:
Tudor
2026-02-02 20:34:35 +00:00
parent f4919db3b9
commit ff7f5487e6
72 changed files with 18636 additions and 20 deletions

View File

@@ -0,0 +1,126 @@
/**
* 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>
);
}