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:
107
nextjs-app/components/SchoolCard.tsx
Normal file
107
nextjs-app/components/SchoolCard.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* SchoolCard Component
|
||||
* Displays school information with metrics and actions
|
||||
*/
|
||||
|
||||
import Link from 'next/link';
|
||||
import type { School } from '@/lib/types';
|
||||
import { formatPercentage, formatProgress, calculateTrend, getTrendColor } from '@/lib/utils';
|
||||
import styles from './SchoolCard.module.css';
|
||||
|
||||
interface SchoolCardProps {
|
||||
school: School;
|
||||
onAddToCompare?: (school: School) => void;
|
||||
showDistance?: boolean;
|
||||
distance?: number;
|
||||
}
|
||||
|
||||
export function SchoolCard({ school, onAddToCompare, showDistance, distance }: SchoolCardProps) {
|
||||
const trend = calculateTrend(school.rwm_expected_pct, school.prev_rwm_expected_pct);
|
||||
const trendColor = getTrendColor(trend);
|
||||
|
||||
return (
|
||||
<div className={styles.card}>
|
||||
<div className={styles.header}>
|
||||
<h3 className={styles.title}>
|
||||
<Link href={`/school/${school.urn}`}>
|
||||
{school.school_name}
|
||||
</Link>
|
||||
</h3>
|
||||
{showDistance && distance !== undefined && (
|
||||
<span className={styles.distance}>
|
||||
{distance.toFixed(1)} km away
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.meta}>
|
||||
{school.local_authority && (
|
||||
<span className={styles.metaItem}>{school.local_authority}</span>
|
||||
)}
|
||||
{school.school_type && (
|
||||
<span className={styles.metaItem}>{school.school_type}</span>
|
||||
)}
|
||||
{school.religious_denomination && school.religious_denomination !== 'Does not apply' && (
|
||||
<span className={styles.metaItem}>{school.religious_denomination}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(school.rwm_expected_pct !== null || school.reading_progress !== null) && (
|
||||
<div className={styles.metrics}>
|
||||
{school.rwm_expected_pct !== null && (
|
||||
<div className={styles.metric}>
|
||||
<span className={styles.metricLabel}>RWM Expected</span>
|
||||
<div className={styles.metricValue}>
|
||||
<strong>{formatPercentage(school.rwm_expected_pct)}</strong>
|
||||
{school.prev_rwm_expected_pct !== null && (
|
||||
<span
|
||||
className={styles.trend}
|
||||
style={{ color: trendColor }}
|
||||
title={`Previous: ${formatPercentage(school.prev_rwm_expected_pct)}`}
|
||||
>
|
||||
{trend === 'up' ? '↑' : trend === 'down' ? '↓' : '→'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{school.reading_progress !== null && (
|
||||
<div className={styles.metric}>
|
||||
<span className={styles.metricLabel}>Reading Progress</span>
|
||||
<strong>{formatProgress(school.reading_progress)}</strong>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{school.writing_progress !== null && (
|
||||
<div className={styles.metric}>
|
||||
<span className={styles.metricLabel}>Writing Progress</span>
|
||||
<strong>{formatProgress(school.writing_progress)}</strong>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{school.maths_progress !== null && (
|
||||
<div className={styles.metric}>
|
||||
<span className={styles.metricLabel}>Maths Progress</span>
|
||||
<strong>{formatProgress(school.maths_progress)}</strong>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.actions}>
|
||||
<Link href={`/school/${school.urn}`} className={styles.btnSecondary}>
|
||||
View Details
|
||||
</Link>
|
||||
{onAddToCompare && (
|
||||
<button
|
||||
onClick={() => onAddToCompare(school)}
|
||||
className={styles.btnPrimary}
|
||||
>
|
||||
Add to Compare
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user