Complete Next.js migration with SSR and Docker deployment
- 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:
57
nextjs-app/components/SchoolMap.tsx
Normal file
57
nextjs-app/components/SchoolMap.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* SchoolMap Component
|
||||
* Client-side Leaflet map wrapper for displaying school locations
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import type { School } from '@/lib/types';
|
||||
import styles from './SchoolMap.module.css';
|
||||
|
||||
// Dynamic import to avoid SSR issues with Leaflet
|
||||
const LeafletMap = dynamic(() => import('./LeafletMapInner'), {
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div className={styles.mapLoading}>
|
||||
<div className={styles.spinner}></div>
|
||||
<p>Loading map...</p>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
interface SchoolMapProps {
|
||||
schools: School[];
|
||||
center?: [number, number];
|
||||
zoom?: number;
|
||||
onMarkerClick?: (school: School) => void;
|
||||
}
|
||||
|
||||
export function SchoolMap({ schools, center, zoom = 13, onMarkerClick }: SchoolMapProps) {
|
||||
// Calculate center if not provided
|
||||
const mapCenter: [number, number] = center || (() => {
|
||||
if (schools.length === 0) return [51.5074, -0.1278]; // Default to London
|
||||
if (schools.length === 1 && schools[0].latitude && schools[0].longitude) {
|
||||
return [schools[0].latitude, schools[0].longitude];
|
||||
}
|
||||
|
||||
// Calculate average position
|
||||
const validSchools = schools.filter(s => s.latitude && s.longitude);
|
||||
if (validSchools.length === 0) return [51.5074, -0.1278];
|
||||
|
||||
const avgLat = validSchools.reduce((sum, s) => sum + (s.latitude || 0), 0) / validSchools.length;
|
||||
const avgLng = validSchools.reduce((sum, s) => sum + (s.longitude || 0), 0) / validSchools.length;
|
||||
return [avgLat, avgLng];
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className={styles.mapWrapper}>
|
||||
<LeafletMap
|
||||
schools={schools}
|
||||
center={mapCenter}
|
||||
zoom={zoom}
|
||||
onMarkerClick={onMarkerClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user