328 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// app/rankings/page.tsx
// Rankings with pagination and adjustable page size
import { query } from '@/lib/db';
import { TrophyIcon } from '@/components/ui/icons';
import Link from 'next/link';
export const dynamic = "force-dynamic";
interface RankingDriver {
driver_guid: string;
driver_name: string;
user_rank: number;
laps_completed: number;
created_at: Date;
}
const PAGE_SIZE_OPTIONS = [25, 50, 100];
async function getRankings(page: number = 1, pageSize: number = 50): Promise<{ drivers: RankingDriver[]; totalCount: number }> {
const offset = (page - 1) * pageSize;
// Get total count
const countResult = await query(`SELECT COUNT(*) as count FROM users`);
const totalCount = countResult[0]?.count || 0;
// Get paginated results
const sql = `
SELECT
driver_guid,
driver_name,
user_rank,
laps_completed,
created_at
FROM users
ORDER BY user_rank DESC
LIMIT ${pageSize} OFFSET ${offset}
`;
const rows = await query(sql);
return {
drivers: rows as RankingDriver[],
totalCount: totalCount
};
}
function getDriverClass(rank: number): { name: string; color: string } {
if (rank >= 5000) return { name: 'S CLASS', color: 'text-white' };
if (rank >= 2500) return { name: 'A CLASS', color: 'text-white' };
if (rank >= 1750) return { name: 'B CLASS', color: 'text-white/80' };
if (rank >= 1250) return { name: 'C CLASS', color: 'text-white/60' };
return { name: 'D CLASS', color: 'text-white/40' };
}
export default async function RankingsPage({
searchParams,
}: {
searchParams: Promise<{ page?: string; pageSize?: string }>;
}) {
// Await searchParams in Next.js 15+
const params = await searchParams;
const currentPage = parseInt(params.page || '1');
const pageSize = parseInt(params.pageSize || '50');
const { drivers, totalCount } = await getRankings(currentPage, pageSize);
const totalPages = Math.ceil(totalCount / pageSize);
const startRank = (currentPage - 1) * pageSize + 1;
return (
<>
{/* Hero */}
<div className="relative border-b border-white/10 grid-overlay">
<div className="max-w-7xl mx-auto px-6 py-16">
<div className="space-y-4">
<div className="inline-flex items-center space-x-2 px-3 py-1 border border-white/20 bg-black">
<TrophyIcon className="w-4 h-4" />
<span className="text-xs font-medium tracking-wider">DRIVER RANKINGS</span>
</div>
<h1 className="text-6xl font-bold tracking-tight">
LEADERBOARD
</h1>
<p className="text-white/60 text-lg max-w-3xl">
Global driver rankings based on performance, consistency, and clean racing
</p>
<p className="text-white/40 text-sm">
{totalCount.toLocaleString()} total drivers
</p>
</div>
{/* Class Explanation */}
<div className="mt-12 grid grid-cols-1 md:grid-cols-4 gap-4">
<ClassCard
name="D CLASS"
range="0 - 1250"
description="Entry level — learning the fundamentals"
/>
<ClassCard
name="C CLASS"
range="1250 - 1750"
description="Intermediate — consistent performance"
/>
<ClassCard
name="B CLASS"
range="1750 - 2500"
description="Advanced — competitive racing"
/>
<ClassCard
name="A CLASS"
range="2500+"
description="Elite — top-tier drivers"
/>
</div>
</div>
</div>
{/* Rankings Table */}
<div className="max-w-7xl mx-auto px-6 py-12">
{/* Page Size Selector */}
<div className="mb-6 flex items-center justify-between border border-white/10 p-4 bg-black">
<div className="text-sm text-white/60">
Items per page:
</div>
<div className="flex items-center space-x-2">
{PAGE_SIZE_OPTIONS.map((size) => (
<Link
key={size}
href={`/rankings?page=1&pageSize=${size}`}
className={`px-4 py-2 border text-sm transition-colors bg-black ${
pageSize === size
? 'border-white text-white'
: 'border-white/20 text-white/60 hover:border-white/40 hover:text-white'
}`}
>
{size}
</Link>
))}
</div>
</div>
<div className="border border-white/10 bg-black">
<table className="w-full">
<thead>
<tr className="border-b border-white/10 bg-white/5">
<th className="px-6 py-4 text-left text-xs font-bold tracking-wider text-white/60">RANK</th>
<th className="px-6 py-4 text-left text-xs font-bold tracking-wider text-white/60">DRIVER</th>
<th className="px-6 py-4 text-left text-xs font-bold tracking-wider text-white/60">RATING</th>
<th className="px-6 py-4 text-left text-xs font-bold tracking-wider text-white/60">CLASS</th>
<th className="px-6 py-4 text-left text-xs font-bold tracking-wider text-white/60">LAPS</th>
<th className="px-6 py-4 text-left text-xs font-bold tracking-wider text-white/60">STEAM ID</th>
</tr>
</thead>
<tbody>
{drivers.map((driver, index) => {
const driverClass = getDriverClass(driver.user_rank);
const globalRank = startRank + index;
const isPodium = globalRank <= 3;
return (
<tr
key={driver.driver_guid}
className={`
border-b border-white/5 hover:bg-white/5 transition-colors
${isPodium ? 'bg-white/[0.02]' : ''}
`}
>
<td className="px-6 py-4">
<span className={`
text-base font-bold tracking-tight
${globalRank === 1 ? 'text-white' : ''}
${globalRank === 2 ? 'text-white/90' : ''}
${globalRank === 3 ? 'text-white/80' : ''}
${globalRank > 3 ? 'text-white/60' : ''}
`}>
{String(globalRank).padStart(2, '0')}
</span>
{isPodium && (
<span className="ml-2 text-xs text-white/40">
{globalRank === 1 ? '█' : globalRank === 2 ? '▓' : '▒'}
</span>
)}
</td>
<td className="px-6 py-4">
<span className="tracking-tight text-base" style={{ letterSpacing: "0.04em" }}>
{driver.driver_name}
</span>
</td>
<td className="px-6 py-4">
<span className="font-mono text-base">
{driver.user_rank.toLocaleString()}
</span>
</td>
<td className="px-6 py-4">
<span className={`inline-block px-3 py-1 border border-white/20 text-xs font-bold tracking-wider bg-black ${driverClass.color}`}>
{driverClass.name}
</span>
</td>
<td className="px-6 py-4">
<span className="text-white/70 font-mono text-sm">
{driver.laps_completed.toLocaleString()}
</span>
</td>
<td className="px-6 py-4">
<span className="text-white/40 font-mono text-xs">
{driver.driver_guid}
</span>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="mt-8 flex items-center justify-between border border-white/10 p-4 bg-black">
<div className="text-sm text-white/60">
Page {currentPage} of {totalPages} Showing {startRank}-{Math.min(startRank + pageSize - 1, totalCount)} of {totalCount.toLocaleString()}
</div>
<div className="flex items-center space-x-2">
{/* First Page */}
{currentPage > 1 && (
<Link
href={`/rankings?page=1&pageSize=${pageSize}`}
className="px-3 py-2 border border-white/20 hover:border-white/40 transition-colors text-sm bg-black"
>
««
</Link>
)}
{/* Previous Page */}
{currentPage > 1 && (
<Link
href={`/rankings?page=${currentPage - 1}&pageSize=${pageSize}`}
className="px-3 py-2 border border-white/20 hover:border-white/40 transition-colors text-sm bg-black"
>
PREV
</Link>
)}
{/* Page Numbers */}
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
let pageNum;
if (totalPages <= 5) {
pageNum = i + 1;
} else if (currentPage <= 3) {
pageNum = i + 1;
} else if (currentPage >= totalPages - 2) {
pageNum = totalPages - 4 + i;
} else {
pageNum = currentPage - 2 + i;
}
return (
<Link
key={pageNum}
href={`/rankings?page=${pageNum}&pageSize=${pageSize}`}
className={`px-4 py-2 border text-sm transition-colors bg-black ${
currentPage === pageNum
? 'border-white text-white'
: 'border-white/20 text-white/60 hover:border-white/40 hover:text-white'
}`}
>
{pageNum}
</Link>
);
})}
{/* Next Page */}
{currentPage < totalPages && (
<Link
href={`/rankings?page=${currentPage + 1}&pageSize=${pageSize}`}
className="px-3 py-2 border border-white/20 hover:border-white/40 transition-colors text-sm bg-black"
>
NEXT
</Link>
)}
{/* Last Page */}
{currentPage < totalPages && (
<Link
href={`/rankings?page=${totalPages}&pageSize=${pageSize}`}
className="px-3 py-2 border border-white/20 hover:border-white/40 transition-colors text-sm bg-black"
>
»»
</Link>
)}
</div>
</div>
)}
{/* Info Box */}
<div className="mt-8 border border-white/10 p-6 topo-lines-dense bg-black">
<h3 className="text-sm font-bold tracking-wider text-white/60 mb-4">RANKING SYSTEM</h3>
<div className="space-y-2 text-sm text-white/60">
<p> Rating increases with <span className="text-white">clean racing</span> and <span className="text-white">fast lap times</span></p>
<p> Penalties applied for <span className="text-white/40">track cuts</span> and <span className="text-white/40">collisions</span></p>
<p> Class promotion based on consistent performance over multiple sessions</p>
</div>
</div>
</div>
</>
);
}
function ClassCard({
name,
range,
description
}: {
name: string;
range: string;
description: string;
}) {
return (
<div className="border border-white/10 p-4 sharp-border">
<div className="text-xs font-bold tracking-wider text-white/60 mb-2">
{name}
</div>
<div className="text-2xl font-bold tracking-tight mb-1">
{range}
</div>
<div className="text-sm text-white/50">
{description}
</div>
</div>
);
}