feat: filters in Ranking page
This commit is contained in:
parent
81b497517e
commit
8e8d59c335
@ -1,5 +1,5 @@
|
|||||||
// app/rankings/page.tsx
|
// app/rankings/page.tsx
|
||||||
// Rankings with pagination and adjustable page size
|
// Rankings with pagination, adjustable page size, and filters
|
||||||
|
|
||||||
import { query } from '@/lib/db';
|
import { query } from '@/lib/db';
|
||||||
import { TrophyIcon } from '@/components/ui/icons';
|
import { TrophyIcon } from '@/components/ui/icons';
|
||||||
@ -9,18 +9,97 @@ export const dynamic = "force-dynamic";
|
|||||||
interface RankingDriver {
|
interface RankingDriver {
|
||||||
driver_guid: string;
|
driver_guid: string;
|
||||||
driver_name: string;
|
driver_name: string;
|
||||||
|
driver_team: string | null;
|
||||||
|
car_model: string | null;
|
||||||
user_rank: number;
|
user_rank: number;
|
||||||
laps_completed: number;
|
laps_completed: number;
|
||||||
|
cuts_alltime: number;
|
||||||
|
contacts_alltime: number;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FilterOptions {
|
||||||
|
teams: string[];
|
||||||
|
carModels: string[];
|
||||||
|
}
|
||||||
|
|
||||||
const PAGE_SIZE_OPTIONS = [25, 50, 100];
|
const PAGE_SIZE_OPTIONS = [25, 50, 100];
|
||||||
|
|
||||||
async function getRankings(page: number = 1, pageSize: number = 50): Promise<{ drivers: RankingDriver[]; totalCount: number }> {
|
async function getFilterOptions(): Promise<FilterOptions> {
|
||||||
|
const teamsQuery = `
|
||||||
|
SELECT DISTINCT driver_team
|
||||||
|
FROM users
|
||||||
|
WHERE driver_team IS NOT NULL AND driver_team != ''
|
||||||
|
ORDER BY driver_team
|
||||||
|
`;
|
||||||
|
|
||||||
|
const carsQuery = `
|
||||||
|
SELECT DISTINCT car_model
|
||||||
|
FROM users
|
||||||
|
WHERE car_model IS NOT NULL AND car_model != ''
|
||||||
|
ORDER BY car_model
|
||||||
|
`;
|
||||||
|
|
||||||
|
const teams = await query(teamsQuery);
|
||||||
|
const cars = await query(carsQuery);
|
||||||
|
|
||||||
|
return {
|
||||||
|
teams: teams.map((t: any) => t.driver_team),
|
||||||
|
carModels: cars.map((c: any) => c.car_model)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRankings(
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 50,
|
||||||
|
filters: {
|
||||||
|
search?: string;
|
||||||
|
class?: string;
|
||||||
|
team?: string;
|
||||||
|
carModel?: string;
|
||||||
|
} = {}
|
||||||
|
): Promise<{ drivers: RankingDriver[]; totalCount: number }> {
|
||||||
const offset = (page - 1) * pageSize;
|
const offset = (page - 1) * pageSize;
|
||||||
|
|
||||||
|
// Build WHERE clause
|
||||||
|
const conditions: string[] = [];
|
||||||
|
|
||||||
|
if (filters.search) {
|
||||||
|
conditions.push(`LOWER(driver_name) LIKE LOWER('%${filters.search}%')`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.class) {
|
||||||
|
switch (filters.class) {
|
||||||
|
case 'S':
|
||||||
|
conditions.push('user_rank >= 5000');
|
||||||
|
break;
|
||||||
|
case 'A':
|
||||||
|
conditions.push('user_rank >= 2500 AND user_rank < 5000');
|
||||||
|
break;
|
||||||
|
case 'B':
|
||||||
|
conditions.push('user_rank >= 1750 AND user_rank < 2500');
|
||||||
|
break;
|
||||||
|
case 'C':
|
||||||
|
conditions.push('user_rank >= 1250 AND user_rank < 1750');
|
||||||
|
break;
|
||||||
|
case 'D':
|
||||||
|
conditions.push('user_rank < 1250');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.team) {
|
||||||
|
conditions.push(`driver_team = '${filters.team}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.carModel) {
|
||||||
|
conditions.push(`car_model = '${filters.carModel}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||||
|
|
||||||
// Get total count
|
// Get total count
|
||||||
const countResult = await query(`SELECT COUNT(*) as count FROM users`);
|
const countResult = await query(`SELECT COUNT(*) as count FROM users ${whereClause}`);
|
||||||
const totalCount = countResult[0]?.count || 0;
|
const totalCount = countResult[0]?.count || 0;
|
||||||
|
|
||||||
// Get paginated results
|
// Get paginated results
|
||||||
@ -28,10 +107,15 @@ async function getRankings(page: number = 1, pageSize: number = 50): Promise<{ d
|
|||||||
SELECT
|
SELECT
|
||||||
driver_guid,
|
driver_guid,
|
||||||
driver_name,
|
driver_name,
|
||||||
|
driver_team,
|
||||||
|
car_model,
|
||||||
user_rank,
|
user_rank,
|
||||||
laps_completed,
|
laps_completed,
|
||||||
|
cuts_alltime,
|
||||||
|
contacts_alltime,
|
||||||
created_at
|
created_at
|
||||||
FROM users
|
FROM users
|
||||||
|
${whereClause}
|
||||||
ORDER BY user_rank DESC
|
ORDER BY user_rank DESC
|
||||||
LIMIT ${pageSize} OFFSET ${offset}
|
LIMIT ${pageSize} OFFSET ${offset}
|
||||||
`;
|
`;
|
||||||
@ -43,28 +127,56 @@ async function getRankings(page: number = 1, pageSize: number = 50): Promise<{ d
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDriverClass(rank: number): { name: string; color: string } {
|
function getDriverClass(rank: number): { name: string; color: string; code: string } {
|
||||||
if (rank >= 5000) return { name: 'S CLASS', color: 'text-white' };
|
if (rank >= 5000) return { name: 'S CLASS', color: 'text-white', code: 'S' };
|
||||||
if (rank >= 2500) return { name: 'A CLASS', color: 'text-white' };
|
if (rank >= 2500) return { name: 'A CLASS', color: 'text-white', code: 'A' };
|
||||||
if (rank >= 1750) return { name: 'B CLASS', color: 'text-white/80' };
|
if (rank >= 1750) return { name: 'B CLASS', color: 'text-white/80', code: 'B' };
|
||||||
if (rank >= 1250) return { name: 'C CLASS', color: 'text-white/60' };
|
if (rank >= 1250) return { name: 'C CLASS', color: 'text-white/60', code: 'C' };
|
||||||
return { name: 'D CLASS', color: 'text-white/40' };
|
return { name: 'D CLASS', color: 'text-white/40', code: 'D' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQueryString(params: Record<string, string | undefined>): string {
|
||||||
|
const filtered = Object.entries(params)
|
||||||
|
.filter(([_, value]) => value !== undefined && value !== '')
|
||||||
|
.map(([key, value]) => `${key}=${encodeURIComponent(value!)}`)
|
||||||
|
.join('&');
|
||||||
|
return filtered ? `?${filtered}` : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function RankingsPage({
|
export default async function RankingsPage({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{ page?: string; pageSize?: string }>;
|
searchParams: Promise<{
|
||||||
|
page?: string;
|
||||||
|
pageSize?: string;
|
||||||
|
search?: string;
|
||||||
|
class?: string;
|
||||||
|
team?: string;
|
||||||
|
carModel?: string;
|
||||||
|
}>;
|
||||||
}) {
|
}) {
|
||||||
// Await searchParams in Next.js 15+
|
|
||||||
const params = await searchParams;
|
const params = await searchParams;
|
||||||
const currentPage = parseInt(params.page || '1');
|
const currentPage = parseInt(params.page || '1');
|
||||||
const pageSize = parseInt(params.pageSize || '50');
|
const pageSize = parseInt(params.pageSize || '50');
|
||||||
|
const searchQuery = params.search || '';
|
||||||
|
const classFilter = params.class || '';
|
||||||
|
const teamFilter = params.team || '';
|
||||||
|
const carModelFilter = params.carModel || '';
|
||||||
|
|
||||||
|
const { drivers, totalCount } = await getRankings(currentPage, pageSize, {
|
||||||
|
search: searchQuery,
|
||||||
|
class: classFilter,
|
||||||
|
team: teamFilter,
|
||||||
|
carModel: carModelFilter
|
||||||
|
});
|
||||||
|
|
||||||
|
const filterOptions = await getFilterOptions();
|
||||||
|
|
||||||
const { drivers, totalCount } = await getRankings(currentPage, pageSize);
|
|
||||||
const totalPages = Math.ceil(totalCount / pageSize);
|
const totalPages = Math.ceil(totalCount / pageSize);
|
||||||
const startRank = (currentPage - 1) * pageSize + 1;
|
const startRank = (currentPage - 1) * pageSize + 1;
|
||||||
|
|
||||||
|
const hasActiveFilters = searchQuery || classFilter || teamFilter || carModelFilter;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Hero */}
|
{/* Hero */}
|
||||||
@ -82,7 +194,7 @@ export default async function RankingsPage({
|
|||||||
Global driver rankings based on performance, consistency, and clean racing
|
Global driver rankings based on performance, consistency, and clean racing
|
||||||
</p>
|
</p>
|
||||||
<p className="text-white/40 text-sm">
|
<p className="text-white/40 text-sm">
|
||||||
{totalCount.toLocaleString()} total drivers
|
{totalCount.toLocaleString()} {hasActiveFilters ? 'filtered' : 'total'} drivers
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -112,100 +224,256 @@ export default async function RankingsPage({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Rankings Table */}
|
{/* Filters & Controls */}
|
||||||
<div className="max-w-7xl mx-auto px-6 py-12">
|
<div className="max-w-7xl mx-auto px-6 py-8">
|
||||||
|
<div className="border border-white/10 bg-black p-6 space-y-6">
|
||||||
|
{/* Single Filter Form */}
|
||||||
|
<form action="/rankings" method="get" className="space-y-6">
|
||||||
|
<input type="hidden" name="page" value="1" />
|
||||||
|
<input type="hidden" name="pageSize" value={pageSize} />
|
||||||
|
|
||||||
|
{/* Search Bar */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold tracking-wider text-white/60 mb-2">
|
||||||
|
SEARCH DRIVER
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="search"
|
||||||
|
defaultValue={searchQuery}
|
||||||
|
placeholder="Enter driver name..."
|
||||||
|
className="flex-1 px-4 py-3 bg-black border border-white/20 text-white placeholder-white/40 focus:border-white focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter Grid */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
{/* Class Filter */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold tracking-wider text-white/60 mb-2">
|
||||||
|
CLASS
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
name="class"
|
||||||
|
className="w-full px-4 py-3 bg-black border border-white/20 text-white focus:border-white focus:outline-none"
|
||||||
|
defaultValue={classFilter}
|
||||||
|
>
|
||||||
|
<option value="">All Classes</option>
|
||||||
|
<option value="S">S CLASS (5000+)</option>
|
||||||
|
<option value="A">A CLASS (2500-4999)</option>
|
||||||
|
<option value="B">B CLASS (1750-2499)</option>
|
||||||
|
<option value="C">C CLASS (1250-1749)</option>
|
||||||
|
<option value="D">D CLASS (0-1249)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Team Filter */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold tracking-wider text-white/60 mb-2">
|
||||||
|
TEAM
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
name="team"
|
||||||
|
className="w-full px-4 py-3 bg-black border border-white/20 text-white focus:border-white focus:outline-none"
|
||||||
|
defaultValue={teamFilter}
|
||||||
|
>
|
||||||
|
<option value="">All Teams</option>
|
||||||
|
{filterOptions.teams.map(team => (
|
||||||
|
<option key={team} value={team}>{team}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Car Model Filter */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold tracking-wider text-white/60 mb-2">
|
||||||
|
CAR MODEL
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
name="carModel"
|
||||||
|
className="w-full px-4 py-3 bg-black border border-white/20 text-white focus:border-white focus:outline-none"
|
||||||
|
defaultValue={carModelFilter}
|
||||||
|
>
|
||||||
|
<option value="">All Cars</option>
|
||||||
|
{filterOptions.carModels.map(car => (
|
||||||
|
<option key={car} value={car}>{car}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-8 py-3 border border-white/20 hover:border-white bg-black text-white transition-colors"
|
||||||
|
>
|
||||||
|
APPLY FILTERS
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Active Filters & Clear */}
|
||||||
|
{hasActiveFilters && (
|
||||||
|
<div className="flex items-center justify-between pt-4 border-t border-white/10">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span className="text-xs text-white/40">ACTIVE FILTERS:</span>
|
||||||
|
{searchQuery && (
|
||||||
|
<span className="px-2 py-1 bg-white/10 text-xs border border-white/20">
|
||||||
|
Search: {searchQuery}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{classFilter && (
|
||||||
|
<span className="px-2 py-1 bg-white/10 text-xs border border-white/20">
|
||||||
|
Class: {classFilter}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{teamFilter && (
|
||||||
|
<span className="px-2 py-1 bg-white/10 text-xs border border-white/20">
|
||||||
|
Team: {teamFilter}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{carModelFilter && (
|
||||||
|
<span className="px-2 py-1 bg-white/10 text-xs border border-white/20">
|
||||||
|
Car: {carModelFilter}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href={`/rankings?pageSize=${pageSize}`}
|
||||||
|
className="px-4 py-2 border border-white/20 hover:border-white text-xs transition-colors"
|
||||||
|
>
|
||||||
|
CLEAR ALL
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Page Size Selector */}
|
{/* Page Size Selector */}
|
||||||
<div className="mb-6 flex items-center justify-between border border-white/10 p-4 bg-black">
|
<div className="mt-6 flex items-center justify-between border border-white/10 p-4 bg-black">
|
||||||
<div className="text-sm text-white/60">
|
<div className="text-sm text-white/60">
|
||||||
Items per page:
|
Items per page:
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{PAGE_SIZE_OPTIONS.map((size) => (
|
{PAGE_SIZE_OPTIONS.map((size) => {
|
||||||
<Link
|
const newParams = buildQueryString({
|
||||||
key={size}
|
page: '1',
|
||||||
href={`/rankings?page=1&pageSize=${size}`}
|
pageSize: size.toString(),
|
||||||
className={`px-4 py-2 border text-sm transition-colors bg-black ${
|
search: searchQuery,
|
||||||
pageSize === size
|
class: classFilter,
|
||||||
? 'border-white text-white'
|
team: teamFilter,
|
||||||
: 'border-white/20 text-white/60 hover:border-white/40 hover:text-white'
|
carModel: carModelFilter
|
||||||
}`}
|
});
|
||||||
>
|
return (
|
||||||
{size}
|
<Link
|
||||||
</Link>
|
key={size}
|
||||||
))}
|
href={`/rankings${newParams}`}
|
||||||
|
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>
|
</div>
|
||||||
|
|
||||||
<div className="border border-white/10 bg-black">
|
{/* Rankings Table */}
|
||||||
|
<div className="mt-6 border border-white/10 bg-black overflow-x-auto">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-white/10 bg-white/5">
|
<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">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">DRIVER</th>
|
||||||
|
<th className="px-6 py-4 text-left text-xs font-bold tracking-wider text-white/60">TEAM</th>
|
||||||
|
<th className="px-6 py-4 text-left text-xs font-bold tracking-wider text-white/60">CAR</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">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">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">LAPS</th>
|
||||||
<th className="px-6 py-4 text-left text-xs font-bold tracking-wider text-white/60">STEAM ID</th>
|
<th className="px-6 py-4 text-left text-xs font-bold tracking-wider text-white/60">CUTS</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{drivers.map((driver, index) => {
|
{drivers.length === 0 ? (
|
||||||
const driverClass = getDriverClass(driver.user_rank);
|
<tr>
|
||||||
const globalRank = startRank + index;
|
<td colSpan={8} className="px-6 py-12 text-center text-white/40">
|
||||||
const isPodium = globalRank <= 3;
|
No drivers found matching your filters
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
drivers.map((driver, index) => {
|
||||||
|
const driverClass = getDriverClass(driver.user_rank);
|
||||||
|
const globalRank = startRank + index;
|
||||||
|
const isPodium = globalRank <= 3 && !hasActiveFilters;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={driver.driver_guid}
|
key={driver.driver_guid}
|
||||||
className={`
|
className={`
|
||||||
border-b border-white/5 hover:bg-white/5 transition-colors
|
border-b border-white/5 hover:bg-white/5 transition-colors
|
||||||
${isPodium ? 'bg-white/[0.02]' : ''}
|
${isPodium ? 'bg-white/[0.02]' : ''}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<span className={`
|
<span className={`
|
||||||
text-base font-bold tracking-tight
|
text-base font-bold tracking-tight
|
||||||
${globalRank === 1 ? 'text-white' : ''}
|
${globalRank === 1 && !hasActiveFilters ? 'text-white' : ''}
|
||||||
${globalRank === 2 ? 'text-white/90' : ''}
|
${globalRank === 2 && !hasActiveFilters ? 'text-white/90' : ''}
|
||||||
${globalRank === 3 ? 'text-white/80' : ''}
|
${globalRank === 3 && !hasActiveFilters ? 'text-white/80' : ''}
|
||||||
${globalRank > 3 ? 'text-white/60' : ''}
|
${(globalRank > 3 || hasActiveFilters) ? 'text-white/60' : ''}
|
||||||
`}>
|
`}>
|
||||||
{String(globalRank).padStart(2, '0')}
|
{String(globalRank).padStart(2, '0')}
|
||||||
</span>
|
|
||||||
{isPodium && (
|
|
||||||
<span className="ml-2 text-xs text-white/40">
|
|
||||||
{globalRank === 1 ? '█' : globalRank === 2 ? '▓' : '▒'}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
{isPodium && (
|
||||||
</td>
|
<span className="ml-2 text-xs text-white/40">
|
||||||
<td className="px-6 py-4">
|
{globalRank === 1 ? '█' : globalRank === 2 ? '▓' : '▒'}
|
||||||
<span className="tracking-tight text-base" style={{ letterSpacing: "0.04em" }}>
|
</span>
|
||||||
{driver.driver_name}
|
)}
|
||||||
</span>
|
</td>
|
||||||
</td>
|
<td className="px-6 py-4">
|
||||||
<td className="px-6 py-4">
|
<span className="tracking-tight text-base" style={{ letterSpacing: "0.04em" }}>
|
||||||
<span className="font-mono text-base">
|
{driver.driver_name}
|
||||||
{driver.user_rank.toLocaleString()}
|
</span>
|
||||||
</span>
|
</td>
|
||||||
</td>
|
<td className="px-6 py-4">
|
||||||
<td className="px-6 py-4">
|
<span className="text-white/60 text-sm">
|
||||||
<span className={`inline-block px-3 py-1 border border-white/20 text-xs font-bold tracking-wider bg-black ${driverClass.color}`}>
|
{driver.driver_team || '—'}
|
||||||
{driverClass.name}
|
</span>
|
||||||
</span>
|
</td>
|
||||||
</td>
|
<td className="px-6 py-4">
|
||||||
<td className="px-6 py-4">
|
<span className="text-white/60 text-sm">
|
||||||
<span className="text-white/70 font-mono text-sm">
|
{driver.car_model || '—'}
|
||||||
{driver.laps_completed.toLocaleString()}
|
</span>
|
||||||
</span>
|
</td>
|
||||||
</td>
|
<td className="px-6 py-4">
|
||||||
<td className="px-6 py-4">
|
<span className="font-mono text-base">
|
||||||
<span className="text-white/40 font-mono text-xs">
|
{driver.user_rank.toLocaleString()}
|
||||||
{driver.driver_guid}
|
</span>
|
||||||
</span>
|
</td>
|
||||||
</td>
|
<td className="px-6 py-4">
|
||||||
</tr>
|
<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/50 font-mono text-sm">
|
||||||
|
{driver.cuts_alltime.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -220,7 +488,14 @@ export default async function RankingsPage({
|
|||||||
{/* First Page */}
|
{/* First Page */}
|
||||||
{currentPage > 1 && (
|
{currentPage > 1 && (
|
||||||
<Link
|
<Link
|
||||||
href={`/rankings?page=1&pageSize=${pageSize}`}
|
href={`/rankings${buildQueryString({
|
||||||
|
page: '1',
|
||||||
|
pageSize: pageSize.toString(),
|
||||||
|
search: searchQuery,
|
||||||
|
class: classFilter,
|
||||||
|
team: teamFilter,
|
||||||
|
carModel: carModelFilter
|
||||||
|
})}`}
|
||||||
className="px-3 py-2 border border-white/20 hover:border-white/40 transition-colors text-sm bg-black"
|
className="px-3 py-2 border border-white/20 hover:border-white/40 transition-colors text-sm bg-black"
|
||||||
>
|
>
|
||||||
««
|
««
|
||||||
@ -230,7 +505,14 @@ export default async function RankingsPage({
|
|||||||
{/* Previous Page */}
|
{/* Previous Page */}
|
||||||
{currentPage > 1 && (
|
{currentPage > 1 && (
|
||||||
<Link
|
<Link
|
||||||
href={`/rankings?page=${currentPage - 1}&pageSize=${pageSize}`}
|
href={`/rankings${buildQueryString({
|
||||||
|
page: (currentPage - 1).toString(),
|
||||||
|
pageSize: pageSize.toString(),
|
||||||
|
search: searchQuery,
|
||||||
|
class: classFilter,
|
||||||
|
team: teamFilter,
|
||||||
|
carModel: carModelFilter
|
||||||
|
})}`}
|
||||||
className="px-3 py-2 border border-white/20 hover:border-white/40 transition-colors text-sm bg-black"
|
className="px-3 py-2 border border-white/20 hover:border-white/40 transition-colors text-sm bg-black"
|
||||||
>
|
>
|
||||||
‹ PREV
|
‹ PREV
|
||||||
@ -253,7 +535,14 @@ export default async function RankingsPage({
|
|||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={pageNum}
|
key={pageNum}
|
||||||
href={`/rankings?page=${pageNum}&pageSize=${pageSize}`}
|
href={`/rankings${buildQueryString({
|
||||||
|
page: pageNum.toString(),
|
||||||
|
pageSize: pageSize.toString(),
|
||||||
|
search: searchQuery,
|
||||||
|
class: classFilter,
|
||||||
|
team: teamFilter,
|
||||||
|
carModel: carModelFilter
|
||||||
|
})}`}
|
||||||
className={`px-4 py-2 border text-sm transition-colors bg-black ${
|
className={`px-4 py-2 border text-sm transition-colors bg-black ${
|
||||||
currentPage === pageNum
|
currentPage === pageNum
|
||||||
? 'border-white text-white'
|
? 'border-white text-white'
|
||||||
@ -268,7 +557,14 @@ export default async function RankingsPage({
|
|||||||
{/* Next Page */}
|
{/* Next Page */}
|
||||||
{currentPage < totalPages && (
|
{currentPage < totalPages && (
|
||||||
<Link
|
<Link
|
||||||
href={`/rankings?page=${currentPage + 1}&pageSize=${pageSize}`}
|
href={`/rankings${buildQueryString({
|
||||||
|
page: (currentPage + 1).toString(),
|
||||||
|
pageSize: pageSize.toString(),
|
||||||
|
search: searchQuery,
|
||||||
|
class: classFilter,
|
||||||
|
team: teamFilter,
|
||||||
|
carModel: carModelFilter
|
||||||
|
})}`}
|
||||||
className="px-3 py-2 border border-white/20 hover:border-white/40 transition-colors text-sm bg-black"
|
className="px-3 py-2 border border-white/20 hover:border-white/40 transition-colors text-sm bg-black"
|
||||||
>
|
>
|
||||||
NEXT ›
|
NEXT ›
|
||||||
@ -278,7 +574,14 @@ export default async function RankingsPage({
|
|||||||
{/* Last Page */}
|
{/* Last Page */}
|
||||||
{currentPage < totalPages && (
|
{currentPage < totalPages && (
|
||||||
<Link
|
<Link
|
||||||
href={`/rankings?page=${totalPages}&pageSize=${pageSize}`}
|
href={`/rankings${buildQueryString({
|
||||||
|
page: totalPages.toString(),
|
||||||
|
pageSize: pageSize.toString(),
|
||||||
|
search: searchQuery,
|
||||||
|
class: classFilter,
|
||||||
|
team: teamFilter,
|
||||||
|
carModel: carModelFilter
|
||||||
|
})}`}
|
||||||
className="px-3 py-2 border border-white/20 hover:border-white/40 transition-colors text-sm bg-black"
|
className="px-3 py-2 border border-white/20 hover:border-white/40 transition-colors text-sm bg-black"
|
||||||
>
|
>
|
||||||
»»
|
»»
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user