Port to Next.js with static export for Cloudflare Pages

- Create Next.js app with TypeScript, Tailwind, and Postcss
- Extract API logic, types, and utilities to lib/
- Build React components for Race Results, Driver Standings, Points Key, and D3 Chart
- Use environment variables for API credentials (NEXT_PUBLIC_*)
- Static export (output: 'export') for Cloudflare Pages deployment
- D3 chart rendered client-side with dynamic import
- Update deploy workflow to build and deploy ./out directory
- Remove original public/index.html
This commit is contained in:
2026-03-01 14:21:02 +01:00
parent 04552e1eee
commit 6c9a5ea7a6
21 changed files with 3733 additions and 645 deletions

View File

@@ -0,0 +1,98 @@
import { AppData } from '@/lib/types';
import { parsePoints, findStyleByPoints } from '@/lib/leaderboard';
interface RaceResultsTableProps {
appData: AppData;
}
export default function RaceResultsTable({ appData }: RaceResultsTableProps) {
return (
<div id="table-container" className="bg-gray-800/70 p-4 rounded-2xl shadow-2xl backdrop-blur-sm border border-gray-700/50 overflow-x-auto">
<table id="race-results-table" className="w-full min-w-[1200px] border-collapse text-center">
<thead id="race-table-head">
<tr>
<th scope="col" className="py-3 px-4 min-w-[200px]">Track</th>
<th scope="col" className="py-3 px-4 min-w-[100px]">FL</th>
{appData.drivers.map((driver) => (
<th key={driver.initials} scope="col" className="py-3 px-4 min-w-[170px]" style={{ textAlign: 'center' }}>
<span>{driver.name}</span>
</th>
))}
</tr>
</thead>
<tbody id="race-table-body" className="divide-y divide-gray-700">
{appData.tracks.map((track, trackIndex) => (
<tr key={trackIndex}>
<td className="font-semibold py-3 px-4 whitespace-nowrap min-w-[200px]">
<span>{track.name}</span>
</td>
<td className="py-3 px-4 font-mono min-w-[100px]">{track.fastestLapInitials || ''}</td>
{appData.drivers.map((driver, driverIndex) => {
const driverInitials = driver.initials;
let result = track.results[driverIndex];
// DNS fallback
if (result === undefined || result === null || result === '') {
result = 'DNS';
}
const pointsNum = parsePoints(result);
const style = findStyleByPoints(appData, pointsNum);
let cellStyles: React.CSSProperties = {
backgroundColor: style.color,
color: style.textColor,
};
let hasFL = false;
if (track.fastestLapInitials && track.fastestLapInitials === driverInitials) {
hasFL = true;
const flColor = appData.flPoints.color;
if (pointsNum === 0) {
cellStyles = {
backgroundColor: flColor,
color: appData.flPoints.textColor,
};
} else {
cellStyles = {
background: `linear-gradient(135deg, ${style.color} 50%, ${flColor} 50%)`,
color: style.textColor,
};
}
}
if (hasFL) {
cellStyles = {
...cellStyles,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexDirection: 'row',
paddingLeft: '0.75rem',
paddingRight: '0.75rem',
};
} else {
cellStyles = {
...cellStyles,
textAlign: 'center',
};
}
return (
<td key={driverIndex} className="py-3 px-4 font-mono" style={cellStyles}>
{hasFL ? (
<>
<span style={{ flexGrow: 1, textAlign: 'left' }}>{result}</span>
<span className="fl-text-only" style={{ color: appData.flPoints.textColor }}>(+1)</span>
</>
) : (
<span>{result}</span>
)}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
);
}