diff --git a/app/rankings/page.tsx b/app/rankings/page.tsx index 47377a0..c77f2ea 100644 --- a/app/rankings/page.tsx +++ b/app/rankings/page.tsx @@ -1,5 +1,5 @@ // 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 { TrophyIcon } from '@/components/ui/icons'; @@ -9,18 +9,97 @@ export const dynamic = "force-dynamic"; interface RankingDriver { driver_guid: string; driver_name: string; + driver_team: string | null; + car_model: string | null; user_rank: number; laps_completed: number; + cuts_alltime: number; + contacts_alltime: number; created_at: Date; } +interface FilterOptions { + teams: string[]; + carModels: string[]; +} + 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 { + 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; + // 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 - 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; // Get paginated results @@ -28,10 +107,15 @@ async function getRankings(page: number = 1, pageSize: number = 50): Promise<{ d SELECT driver_guid, driver_name, + driver_team, + car_model, user_rank, laps_completed, + cuts_alltime, + contacts_alltime, created_at FROM users + ${whereClause} ORDER BY user_rank DESC LIMIT ${pageSize} OFFSET ${offset} `; @@ -43,27 +127,55 @@ async function getRankings(page: number = 1, pageSize: number = 50): Promise<{ d }; } -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' }; +function getDriverClass(rank: number): { name: string; color: string; code: string } { + if (rank >= 5000) return { name: 'S CLASS', color: 'text-white', code: 'S' }; + if (rank >= 2500) return { name: 'A CLASS', color: 'text-white', code: 'A' }; + if (rank >= 1750) return { name: 'B CLASS', color: 'text-white/80', code: 'B' }; + if (rank >= 1250) return { name: 'C CLASS', color: 'text-white/60', code: 'C' }; + return { name: 'D CLASS', color: 'text-white/40', code: 'D' }; +} + +function buildQueryString(params: Record): 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({ 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 currentPage = parseInt(params.page || '1'); 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 startRank = (currentPage - 1) * pageSize + 1; + + const hasActiveFilters = searchQuery || classFilter || teamFilter || carModelFilter; return ( <> @@ -82,7 +194,7 @@ export default async function RankingsPage({ Global driver rankings based on performance, consistency, and clean racing

- {totalCount.toLocaleString()} total drivers + {totalCount.toLocaleString()} {hasActiveFilters ? 'filtered' : 'total'} drivers

@@ -112,100 +224,256 @@ export default async function RankingsPage({ - {/* Rankings Table */} -
+ {/* Filters & Controls */} +
+
+ {/* Single Filter Form */} +
+ + + + {/* Search Bar */} +
+ +
+ +
+
+ + {/* Filter Grid */} +
+ {/* Class Filter */} +
+ + +
+ + {/* Team Filter */} +
+ + +
+ + {/* Car Model Filter */} +
+ + +
+
+ + {/* Submit Button */} +
+ +
+
+ + {/* Active Filters & Clear */} + {hasActiveFilters && ( +
+
+ ACTIVE FILTERS: + {searchQuery && ( + + Search: {searchQuery} + + )} + {classFilter && ( + + Class: {classFilter} + + )} + {teamFilter && ( + + Team: {teamFilter} + + )} + {carModelFilter && ( + + Car: {carModelFilter} + + )} +
+ + CLEAR ALL + +
+ )} +
+ {/* Page Size Selector */} -
+
Items per page:
- {PAGE_SIZE_OPTIONS.map((size) => ( - - {size} - - ))} + {PAGE_SIZE_OPTIONS.map((size) => { + const newParams = buildQueryString({ + page: '1', + pageSize: size.toString(), + search: searchQuery, + class: classFilter, + team: teamFilter, + carModel: carModelFilter + }); + return ( + + {size} + + ); + })}
-
+ {/* Rankings Table */} +
+ + - + - {drivers.map((driver, index) => { - const driverClass = getDriverClass(driver.user_rank); - const globalRank = startRank + index; - const isPodium = globalRank <= 3; - - return ( - - + + + ) : ( + drivers.map((driver, index) => { + const driverClass = getDriverClass(driver.user_rank); + const globalRank = startRank + index; + const isPodium = globalRank <= 3 && !hasActiveFilters; + + return ( + + - - - - - - - ); - })} + {isPodium && ( + + {globalRank === 1 ? '█' : globalRank === 2 ? '▓' : '▒'} + + )} + + + + + + + + + + ); + }) + )}
RANK DRIVERTEAMCAR RATING CLASS LAPSSTEAM IDCUTS
- 3 ? 'text-white/60' : ''} - `}> - {String(globalRank).padStart(2, '0')} - - {isPodium && ( - - {globalRank === 1 ? '█' : globalRank === 2 ? '▓' : '▒'} + {drivers.length === 0 ? ( +
+ No drivers found matching your filters +
+ 3 || hasActiveFilters) ? 'text-white/60' : ''} + `}> + {String(globalRank).padStart(2, '0')} - )} - - - {driver.driver_name} - - - - {driver.user_rank.toLocaleString()} - - - - {driverClass.name} - - - - {driver.laps_completed.toLocaleString()} - - - - {driver.driver_guid} - -
+ + {driver.driver_name} + + + + {driver.driver_team || '—'} + + + + {driver.car_model || '—'} + + + + {driver.user_rank.toLocaleString()} + + + + {driverClass.name} + + + + {driver.laps_completed.toLocaleString()} + + + + {driver.cuts_alltime.toLocaleString()} + +
@@ -220,7 +488,14 @@ export default async function RankingsPage({ {/* First Page */} {currentPage > 1 && ( «« @@ -230,7 +505,14 @@ export default async function RankingsPage({ {/* Previous Page */} {currentPage > 1 && ( ‹ PREV @@ -253,7 +535,14 @@ export default async function RankingsPage({ return ( NEXT › @@ -278,7 +574,14 @@ export default async function RankingsPage({ {/* Last Page */} {currentPage < totalPages && ( »»