FODASSE QUE BORRAÇO

This commit is contained in:
Afonso Clerigo Mendes de Sousa 2025-10-25 23:16:38 +01:00
parent 2f641217ce
commit ede5003530
21 changed files with 2745 additions and 143 deletions

322
app/api/drivers/route.ts Normal file
View File

@ -0,0 +1,322 @@
// app/api/drivers/route.ts
// API Route: GET /api/drivers
// Returns all connected drivers with their server info
import { NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { DriverWithServer } from '@/types/racing';
export async function GET(request: Request) {
try {
// Parse query params (e.g., ?connected=true)
const { searchParams } = new URL(request.url);
const connectedOnly = searchParams.get('connected') === 'true';
// SQL query - joins drivers with servers
const sql = `
SELECT
u.driver_guid,
u.driver_name,
u.driver_team,
u.car_model,
u.car_skin,
u.cuts_alltime,
u.contacts_alltime,
u.laps_completed,
u.user_rank,
u.is_connect,
u.is_loading,
u.current_server,
u.created_at,
s.server_id,
s.server_name,
s.server_track,
s.session_type,
s.session_flag,
s.connected_players
FROM users u
LEFT JOIN servers s ON u.current_server = s.server_id
${connectedOnly ? 'WHERE u.is_connect = true' : ''}
ORDER BY u.user_rank ASC
`;
const rows = await query(sql);
// Transform to proper structure
const drivers: DriverWithServer[] = rows.map((row: any) => ({
driver_guid: row.driver_guid,
driver_name: row.driver_name,
driver_team: row.driver_team,
car_model: row.car_model,
car_skin: row.car_skin,
cuts_alltime: row.cuts_alltime,
contacts_alltime: row.contacts_alltime,
laps_completed: row.laps_completed,
user_rank: row.user_rank,
is_connect: row.is_connect,
is_loading: row.is_loading,
current_server: row.current_server,
created_at: row.created_at,
// Include server info if they're connected
server: row.server_id ? {
server_id: row.server_id,
server_name: row.server_name,
server_track: row.server_track,
session_type: row.session_type,
session_flag: row.session_flag,
connected_players: row.connected_players,
} : undefined,
}));
return NextResponse.json({
success: true,
data: drivers,
count: drivers.length,
});
} catch (error) {
console.error('Error fetching drivers:', error);
return NextResponse.json(
{
success: false,
error: 'Failed to fetch drivers'
},
{ status: 500 }
);
}
}
// POST endpoint for updating driver data (optional, for future use)
export async function POST(request: Request) {
try {
const body = await request.json();
// Add your update logic here
// Example: Update driver rank, stats, etc.
return NextResponse.json({
success: true,
message: 'Driver updated',
});
} catch (error) {
console.error('Error updating driver:', error);
return NextResponse.json(
{ success: false, error: 'Failed to update driver' },
{ status: 500 }
);
}
}// app/api/drivers/route.ts
// API Route: GET /api/drivers
// Returns all connected drivers with their server info
import { NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { DriverWithServer } from '@/types/racing';
export async function GET(request: Request) {
try {
// Parse query params (e.g., ?connected=true)
const { searchParams } = new URL(request.url);
const connectedOnly = searchParams.get('connected') === 'true';
// SQL query - joins drivers with servers
const sql = `
SELECT
u.driver_guid,
u.driver_name,
u.driver_team,
u.car_model,
u.car_skin,
u.cuts_alltime,
u.contacts_alltime,
u.laps_completed,
u.user_rank,
u.is_connect,
u.is_loading,
u.current_server,
u.created_at,
s.server_id,
s.server_name,
s.server_track,
s.session_type,
s.session_flag,
s.connected_players
FROM users u
LEFT JOIN servers s ON u.current_server = s.server_id
${connectedOnly ? 'WHERE u.is_connect = true' : ''}
ORDER BY u.user_rank ASC
`;
const rows = await query(sql);
// Transform to proper structure
const drivers: DriverWithServer[] = rows.map((row: any) => ({
driver_guid: row.driver_guid,
driver_name: row.driver_name,
driver_team: row.driver_team,
car_model: row.car_model,
car_skin: row.car_skin,
cuts_alltime: row.cuts_alltime,
contacts_alltime: row.contacts_alltime,
laps_completed: row.laps_completed,
user_rank: row.user_rank,
is_connect: row.is_connect,
is_loading: row.is_loading,
current_server: row.current_server,
created_at: row.created_at,
// Include server info if they're connected
server: row.server_id ? {
server_id: row.server_id,
server_name: row.server_name,
server_track: row.server_track,
session_type: row.session_type,
session_flag: row.session_flag,
connected_players: row.connected_players,
} : undefined,
}));
return NextResponse.json({
success: true,
data: drivers,
count: drivers.length,
});
} catch (error) {
console.error('Error fetching drivers:', error);
return NextResponse.json(
{
success: false,
error: 'Failed to fetch drivers'
},
{ status: 500 }
);
}
}
// POST endpoint for updating driver data (optional, for future use)
export async function POST(request: Request) {
try {
const body = await request.json();
// Add your update logic here
// Example: Update driver rank, stats, etc.
return NextResponse.json({
success: true,
message: 'Driver updated',
});
} catch (error) {
console.error('Error updating driver:', error);
return NextResponse.json(
{ success: false, error: 'Failed to update driver' },
{ status: 500 }
);
}
}// app/api/drivers/route.ts
// API Route: GET /api/drivers
// Returns all connected drivers with their server info
import { NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { DriverWithServer } from '@/types/racing';
export async function GET(request: Request) {
try {
// Parse query params (e.g., ?connected=true)
const { searchParams } = new URL(request.url);
const connectedOnly = searchParams.get('connected') === 'true';
// SQL query - joins drivers with servers
const sql = `
SELECT
u.driver_guid,
u.driver_name,
u.driver_team,
u.car_model,
u.car_skin,
u.cuts_alltime,
u.contacts_alltime,
u.laps_completed,
u.user_rank,
u.is_connect,
u.is_loading,
u.current_server,
u.created_at,
s.server_id,
s.server_name,
s.server_track,
s.session_type,
s.session_flag,
s.connected_players
FROM users u
LEFT JOIN servers s ON u.current_server = s.server_id
${connectedOnly ? 'WHERE u.is_connect = true' : ''}
ORDER BY u.user_rank ASC
`;
const rows = await query(sql);
// Transform to proper structure
const drivers: DriverWithServer[] = rows.map((row: any) => ({
driver_guid: row.driver_guid,
driver_name: row.driver_name,
driver_team: row.driver_team,
car_model: row.car_model,
car_skin: row.car_skin,
cuts_alltime: row.cuts_alltime,
contacts_alltime: row.contacts_alltime,
laps_completed: row.laps_completed,
user_rank: row.user_rank,
is_connect: row.is_connect,
is_loading: row.is_loading,
current_server: row.current_server,
created_at: row.created_at,
// Include server info if they're connected
server: row.server_id ? {
server_id: row.server_id,
server_name: row.server_name,
server_track: row.server_track,
session_type: row.session_type,
session_flag: row.session_flag,
connected_players: row.connected_players,
} : undefined,
}));
return NextResponse.json({
success: true,
data: drivers,
count: drivers.length,
});
} catch (error) {
console.error('Error fetching drivers:', error);
return NextResponse.json(
{
success: false,
error: 'Failed to fetch drivers'
},
{ status: 500 }
);
}
}
// POST endpoint for updating driver data (optional, for future use)
export async function POST(request: Request) {
try {
const body = await request.json();
// Add your update logic here
// Example: Update driver rank, stats, etc.
return NextResponse.json({
success: true,
message: 'Driver updated',
});
} catch (error) {
console.error('Error updating driver:', error);
return NextResponse.json(
{ success: false, error: 'Failed to update driver' },
{ status: 500 }
);
}
}

View File

@ -0,0 +1,105 @@
// app/api/events/register/route.ts
// API endpoint for event registration
import { NextResponse } from 'next/server';
import { query, queryOne } from '@/lib/db';
export async function POST(request: Request) {
try {
const body = await request.json();
const { eventId, steamId, carModel, carSkin, teamName } = body;
// Validate required fields
if (!eventId || !steamId || !carModel) {
return NextResponse.json(
{ success: false, error: 'Missing required fields' },
{ status: 400 }
);
}
// Convert steamId to number (BIGINT)
const driverGuid = parseInt(steamId);
if (isNaN(driverGuid)) {
return NextResponse.json(
{ success: false, error: 'Invalid Steam ID format' },
{ status: 400 }
);
}
// Check if user exists in database
const userCheck = await queryOne(
'SELECT driver_guid FROM users WHERE driver_guid = $1',
[driverGuid]
);
if (!userCheck) {
return NextResponse.json(
{ success: false, error: 'Steam ID not found in database. Please join a server first.' },
{ status: 404 }
);
}
// Check if event exists and is open
const eventCheck: any = await queryOne(
`SELECT event_id, event_status, max_participants,
(SELECT COUNT(*) FROM event_registrations
WHERE event_id = $1 AND status = 'REGISTERED') as current_registrations
FROM events WHERE event_id = $1`,
[eventId]
);
if (!eventCheck) {
return NextResponse.json(
{ success: false, error: 'Event not found' },
{ status: 404 }
);
}
if (eventCheck.event_status !== 'OPEN') {
return NextResponse.json(
{ success: false, error: 'Event registration is closed' },
{ status: 400 }
);
}
if (eventCheck.current_registrations >= eventCheck.max_participants) {
return NextResponse.json(
{ success: false, error: 'Event is full' },
{ status: 400 }
);
}
// Check if already registered
const existingReg = await queryOne(
'SELECT registration_id FROM event_registrations WHERE event_id = $1 AND driver_guid = $2',
[eventId, driverGuid]
);
if (existingReg) {
return NextResponse.json(
{ success: false, error: 'You are already registered for this event' },
{ status: 400 }
);
}
// Insert registration
await query(
`INSERT INTO event_registrations
(event_id, driver_guid, car_model, car_skin, team_name, status)
VALUES ($1, $2, $3, $4, $5, 'REGISTERED')`,
[eventId, driverGuid, carModel, carSkin || null, teamName || null]
);
return NextResponse.json({
success: true,
message: 'Registration successful',
});
} catch (error) {
console.error('Registration error:', error);
return NextResponse.json(
{ success: false, error: 'Registration failed. Please try again.' },
{ status: 500 }
);
}
}

235
app/dashboard/page.tsx Normal file
View File

@ -0,0 +1,235 @@
// app/dashboard/page.tsx
// Dashboard - navbar/footer now in layout.tsx
import { query } from '@/lib/db';
import { DriverWithServer } from '@/types/racing';
import { UsersIcon, ServerIcon, ActivityIcon, MapPinIcon, FlagIcon, LiveDotIcon } from '@/components/ui/icons';
async function getConnectedDrivers(): Promise<DriverWithServer[]> {
const sql = `
SELECT
u.driver_guid,
u.driver_name,
u.driver_team,
u.car_model,
u.car_skin,
u.user_rank,
u.laps_completed,
s.server_id,
s.server_name,
s.server_track,
s.session_flag,
s.connected_players
FROM users u
INNER JOIN servers s ON u.current_server = s.server_id
WHERE u.is_connect = true
ORDER BY s.server_id, u.user_rank ASC
`;
const rows = await query(sql);
return rows.map((row: any) => ({
driver_guid: row.driver_guid,
driver_name: row.driver_name,
driver_team: row.driver_team,
car_model: row.car_model,
car_skin: row.car_skin,
cuts_alltime: 0,
contacts_alltime: 0,
laps_completed: row.laps_completed,
user_rank: row.user_rank,
is_connect: true,
is_loading: false,
current_server: row.server_id,
created_at: new Date(),
server: {
server_id: row.server_id,
server_name: row.server_name,
server_track: row.server_track,
session_type: 0,
session_flag: row.session_flag,
connected_players: row.connected_players,
},
}));
}
export default async function DashboardPage() {
const drivers = await getConnectedDrivers();
const serverGroups = drivers.reduce((acc, driver) => {
const serverId = driver.server?.server_id ?? 0;
if (!acc[serverId]) {
acc[serverId] = [];
}
acc[serverId].push(driver);
return acc;
}, {} as Record<number, DriverWithServer[]>);
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">
<LiveDotIcon className="w-2 h-2 text-white animate-pulse" />
<span className="text-xs font-medium tracking-wider">LIVE SYSTEM</span>
</div>
<h1 className="text-6xl font-bold tracking-tight">
LIVE DASHBOARD
</h1>
<p className="text-white/60 text-lg max-w-2xl">
Real-time telemetry and session monitoring across all OpenWheels Racing infrastructure
</p>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-12">
<StatCard
title="DRIVERS ONLINE"
value={drivers.length}
icon={<UsersIcon className="w-8 h-8" />}
/>
<StatCard
title="ACTIVE SERVERS"
value={Object.keys(serverGroups).length}
icon={<ServerIcon className="w-8 h-8" />}
/>
<StatCard
title="TOTAL LAPS"
value={drivers.reduce((sum, d) => sum + d.laps_completed, 0)}
icon={<ActivityIcon className="w-8 h-8" />}
/>
</div>
</div>
</div>
{/* Server Listings */}
<div className="max-w-7xl mx-auto px-6 py-12 space-y-6">
{Object.keys(serverGroups).length === 0 ? (
<div className="border border-white/10 p-16 text-center bg-black">
<div className="w-16 h-16 border-2 border-white/20 mx-auto mb-6"></div>
<p className="text-white/40 text-base tracking-wider">NO ACTIVE SESSIONS</p>
<p className="text-white/20 text-sm mt-2">System idle waiting for connections</p>
</div>
) : (
Object.entries(serverGroups).map(([serverId, serverDrivers]) => {
const server = serverDrivers[0].server;
return (
<div key={serverId} className="border border-white/10 sharp-border bg-black">
{/* Server Header */}
<div className="border-b border-white/10 p-6 topo-lines-dense">
<div className="flex items-start justify-between">
<div className="flex-1 space-y-3">
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-2 px-2 py-1 border border-white/30 text-xs">
<LiveDotIcon className="w-2 h-2 animate-pulse" />
<span className="font-medium tracking-wider">LIVE</span>
</div>
<span className="text-xs text-white/40 tracking-wider">
ID: {server?.server_id}
</span>
</div>
<h2 className="text-2xl font-bold tracking-tight">
{server?.server_name}
</h2>
<div className="flex flex-wrap gap-x-6 gap-y-2">
<div className="flex items-center space-x-2">
<MapPinIcon className="w-4 h-4 text-white/60" />
<span className="text-white/80 text-sm">{server?.server_track}</span>
</div>
<div className="flex items-center space-x-2">
<FlagIcon className="w-4 h-4 text-white/60" />
<span className="text-white/80 text-sm">{server?.session_flag}</span>
</div>
<div className="flex items-center space-x-2">
<UsersIcon className="w-4 h-4 text-white/60" />
<span className="text-white/80 text-sm">{server?.connected_players} CONNECTED</span>
</div>
</div>
</div>
</div>
</div>
{/* Driver Table */}
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-white/10">
<th className="px-6 py-4 text-left text-xs font-bold tracking-wider text-white/60">POS</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">RANK</th>
<th className="px-6 py-4 text-left text-xs font-bold tracking-wider text-white/60">LAPS</th>
</tr>
</thead>
<tbody>
{serverDrivers.map((driver, index) => (
<tr
key={driver.driver_guid}
className="border-b border-white/5 hover:bg-white/5 transition-colors"
>
<td className="px-6 py-4">
<span className="text-base font-bold tracking-tight">
{String(index + 1).padStart(2, '0')}
</span>
</td>
<td className="px-6 py-4">
<span className="font-semibold tracking-tight text-base">{driver.driver_name}</span>
</td>
<td className="px-6 py-4">
<span className="text-white/50 text-sm">
{driver.driver_team || '—'}
</span>
</td>
<td className="px-6 py-4">
<span className="text-white/70 text-sm font-mono tracking-tight">
{driver.car_model}
</span>
</td>
<td className="px-6 py-4">
<span className="inline-block px-3 py-1 border border-white/20 text-sm font-mono">
{driver.user_rank}
</span>
</td>
<td className="px-6 py-4">
<span className="font-mono text-sm">{driver.laps_completed}</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
})
)}
</div>
</>
);
}
function StatCard({
title,
value,
icon
}: {
title: string;
value: number;
icon: React.ReactNode;
}) {
return (
<div className="border border-white/10 p-6 sharp-border">
<div className="flex items-center justify-between mb-4">
<span className="text-xs font-bold tracking-wider text-white/60">{title}</span>
<div className="text-white/40">
{icon}
</div>
</div>
<div className="text-5xl font-bold tracking-tight">
{value.toLocaleString()}
</div>
</div>
);
}

View File

@ -0,0 +1,193 @@
// app/events/[id]/page.tsx
// Event detail page with registration form
import { query } from '@/lib/db';
import { Event, EventRegistration } from '@/types/racing';
import { notFound } from 'next/navigation';
import { TrophyIcon, MapPinIcon, UsersIcon } from '@/components/ui/icons';
import EventRegistrationForm from '@/components/events/EventRegistrationForm';
async function getEvent(eventId: number): Promise<Event | null> {
const sql = `
SELECT e.*, COUNT(er.registration_id) as registrations_count
FROM events e
LEFT JOIN event_registrations er ON e.event_id = er.event_id AND er.status = 'REGISTERED'
WHERE e.event_id = $1
GROUP BY e.event_id
`;
const rows = await query(sql, [eventId]);
return rows.length > 0 ? rows[0] as Event : null;
}
async function getEventRegistrations(eventId: number): Promise<EventRegistration[]> {
const sql = `
SELECT
er.*,
u.driver_name
FROM event_registrations er
JOIN users u ON er.driver_guid = u.driver_guid
WHERE er.event_id = $1 AND er.status = 'REGISTERED'
ORDER BY er.registration_date ASC
`;
const rows = await query(sql, [eventId]);
return rows as EventRegistration[];
}
function formatDate(date: Date): string {
return new Date(date).toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
export default async function EventDetailPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const eventId = parseInt(id);
const event: any = await getEvent(eventId);
if (!event) {
notFound();
}
const registrations = await getEventRegistrations(eventId);
const isOpen = event.event_status === 'OPEN';
const isFull = event.registrations_count >= event.max_participants;
const deadlinePassed = event.registration_deadline && new Date(event.registration_deadline) < new Date();
const canRegister = isOpen && !isFull && !deadlinePassed;
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">EVENT DETAILS</span>
</div>
<h1 className="text-5xl font-bold tracking-tight">
{event.event_name}
</h1>
<p className="text-white/70 text-lg max-w-3xl">
{event.event_description}
</p>
</div>
{/* Event Info Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-12">
<InfoCard label="TRACK" value={event.event_track} icon={<MapPinIcon className="w-6 h-6" />} />
<InfoCard label="DATE" value={formatDate(event.event_date)} icon="📅" />
<InfoCard label="DURATION" value={`${event.event_duration || 'TBD'} min`} icon="⏱️" />
<InfoCard
label="PARTICIPANTS"
value={`${event.registrations_count}/${event.max_participants}`}
icon={<UsersIcon className="w-6 h-6" />}
/>
</div>
</div>
</div>
{/* Main Content */}
<div className="max-w-7xl mx-auto px-6 py-12">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column - Registration */}
<div className="lg:col-span-2 space-y-6">
{/* Registration Form */}
{canRegister ? (
<div className="border border-white/10 bg-black p-6">
<h2 className="text-2xl font-bold tracking-tight mb-6">REGISTER FOR EVENT</h2>
<EventRegistrationForm eventId={event.event_id} />
</div>
) : (
<div className="border border-white/10 bg-black p-6">
<h2 className="text-2xl font-bold tracking-tight mb-4">REGISTRATION CLOSED</h2>
<p className="text-white/60">
{isFull && 'This event is full.'}
{deadlinePassed && !isFull && 'Registration deadline has passed.'}
{event.event_status === 'CLOSED' && !isFull && !deadlinePassed && 'Registration is closed for this event.'}
</p>
</div>
)}
{/* Event Rules */}
{event.event_rules && (
<div className="border border-white/10 bg-black p-6 topo-lines-dense">
<h3 className="text-xl font-bold tracking-tight mb-4">EVENT RULES</h3>
<div className="text-white/70 text-sm space-y-2 whitespace-pre-line">
{event.event_rules}
</div>
</div>
)}
</div>
{/* Right Column - Registered Participants */}
<div className="lg:col-span-1">
<div className="border border-white/10 bg-black p-6 sticky top-20">
<h3 className="text-xl font-bold tracking-tight mb-4">
REGISTERED DRIVERS ({event.registrations_count})
</h3>
<div className="space-y-3 max-h-[600px] overflow-y-auto">
{registrations.length === 0 ? (
<p className="text-white/40 text-sm">No registrations yet</p>
) : (
registrations.map((reg: any, index: number) => (
<div key={reg.registration_id} className="border border-white/10 p-3">
<div className="flex items-start justify-between mb-2">
<div className="flex items-center space-x-2">
<span className="text-xs font-bold text-white/40">
#{String(index + 1).padStart(2, '0')}
</span>
<span className="font-semibold text-sm">{reg.driver_name}</span>
</div>
</div>
<div className="space-y-1 text-xs text-white/60">
<div>Car: <span className="text-white/80 font-mono">{reg.car_model}</span></div>
{reg.car_skin && <div>Skin: <span className="text-white/80">{reg.car_skin}</span></div>}
{reg.team_name && <div>Team: <span className="text-white/80">{reg.team_name}</span></div>}
</div>
</div>
))
)}
</div>
</div>
</div>
</div>
</div>
</>
);
}
function InfoCard({
label,
value,
icon
}: {
label: string;
value: string;
icon: React.ReactNode;
}) {
return (
<div className="border border-white/10 p-4 sharp-border">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-bold tracking-wider text-white/60">{label}</span>
<div className="text-white/40 text-sm">
{icon}
</div>
</div>
<div className="text-lg font-bold tracking-tight">
{value}
</div>
</div>
);
}

160
app/events/page.tsx Normal file
View File

@ -0,0 +1,160 @@
// app/events/page.tsx
// Events listing page
import { query } from '@/lib/db';
import { Event } from '@/types/racing';
import Link from 'next/link';
import { TrophyIcon, UsersIcon, MapPinIcon, ClockIcon, CalendarIcon } from '@/components/ui/icons';
async function getEvents(): Promise<Event[]> {
const sql = `
SELECT
e.*,
COUNT(er.registration_id) as registrations_count
FROM events e
LEFT JOIN event_registrations er ON e.event_id = er.event_id AND er.status = 'REGISTERED'
WHERE e.event_status IN ('OPEN', 'CLOSED')
GROUP BY e.event_id
ORDER BY e.event_date ASC
`;
const rows = await query(sql);
return rows as Event[];
}
function formatDate(date: Date): string {
return new Date(date).toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
export default async function EventsPage() {
const events = await getEvents();
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">RACING EVENTS</span>
</div>
<h1 className="text-6xl font-bold tracking-tight">
EVENTS
</h1>
<p className="text-white/60 text-lg max-w-3xl">
Join competitive racing events, championships, and special races. Register now to secure your spot.
</p>
</div>
</div>
</div>
{/* Events List */}
<div className="max-w-7xl mx-auto px-6 py-12 space-y-6">
{events.length === 0 ? (
<div className="border border-white/10 p-16 text-center bg-black">
<TrophyIcon className="w-16 h-16 mx-auto mb-6 text-white/20" />
<p className="text-white/40 text-base tracking-wider">NO UPCOMING EVENTS</p>
<p className="text-white/20 text-sm mt-2">Check back soon for new racing events</p>
</div>
) : (
events.map((event: any) => {
const isOpen = event.event_status === 'OPEN';
const isFull = event.registrations_count >= event.max_participants;
const deadlinePassed = event.registration_deadline && new Date(event.registration_deadline) < new Date();
const canRegister = isOpen && !isFull && !deadlinePassed;
return (
<div key={event.event_id} className="border border-white/10 sharp-border bg-black">
<img
src="https://openwheels.racing/files/img/EnduranceEvents1-1.png"
alt={event.event_name}
className="w-full h-48 object-cover object-[50%_0%] border-b border-white/10 grayscale hover:grayscale-0 transition duration-300"
/>
{/* Event Header */}
<div className="border-b border-white/10 p-6 topo-lines-dense">
<div className="flex items-start justify-between">
<div className="flex-1 space-y-3">
<div className="flex items-center space-x-3">
<div className={`flex items-center space-x-2 px-2 py-1 border text-xs ${
canRegister
? 'border-white/30 text-white'
: 'border-white/10 text-white/40'
}`}>
<span className="font-medium tracking-wider">{event.event_status}</span>
</div>
<span className="text-xs text-white/40 tracking-wider">
ID: {event.event_id}
</span>
</div>
<h2 className="text-2xl font-bold tracking-tight">
{event.event_name}
</h2>
<p className="text-white/70 text-sm max-w-3xl">
{event.event_description}
</p>
<div className="flex flex-wrap gap-x-6 gap-y-2">
<div className="flex items-center space-x-2">
<MapPinIcon className="w-4 h-4 text-white/60" />
<span className="text-white/80 text-sm">{event.event_track}</span>
</div>
<div className="flex items-center space-x-2">
<UsersIcon className="w-4 h-4 text-white/60" />
<span className="text-white/80 text-sm">
{event.registrations_count}/{event.max_participants} registered
</span>
</div>
<div className="flex items-center space-x-2">
<CalendarIcon className="w-4 h-4 text-white/60" />
<span className="text-white/80 text-sm">
{formatDate(event.event_date)}
</span>
</div>
{event.event_duration && (
<div className="flex items-center space-x-2">
<ClockIcon className="w-4 h-4 text-white/60" />
<span className="text-white/80 text-sm">
{event.event_duration} minutes
</span>
</div>
)}
</div>
</div>
</div>
</div>
{/* Event Actions */}
<div className="p-6 flex items-center justify-between">
<div className="text-sm text-white/60">
{deadlinePassed && <span>Registration deadline passed</span>}
{isFull && !deadlinePassed && <span>Event is full</span>}
{!canRegister && !deadlinePassed && !isFull && event.event_status === 'CLOSED' && <span>Registration closed</span>}
</div>
<Link
href={`/events/${event.event_id}`}
className={`px-6 py-3 border text-sm font-medium transition-all ${
canRegister
? 'border-white hover:bg-white hover:text-black'
: 'border-white/20 text-white/60'
}`}
>
{canRegister ? 'REGISTER NOW' : 'VIEW DETAILS'}
</Link>
</div>
</div>
);
})
)}
</div>
</>
);
}

View File

@ -1,26 +1,71 @@
@import url('https://fonts.googleapis.com/css2?family=BBH+Sans+Bartle&family=BBH+Sans+Bogle&display=swap');
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
/* Base styles */
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
background-color: #0a0a0a;
color: #fff;
font-family: 'BBH Sans Bogle', sans-serif;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'BBH Sans Bartle', sans-serif;
letter-spacing: -0.02em;
}
/* Topographic line pattern */
.topo-lines {
background-image:
repeating-linear-gradient(
0deg,
transparent,
transparent 50px,
rgba(255, 255, 255, 0.03) 50px,
rgba(255, 255, 255, 0.03) 51px
),
repeating-linear-gradient(
90deg,
transparent,
transparent 50px,
rgba(255, 255, 255, 0.03) 50px,
rgba(255, 255, 255, 0.03) 51px
);
}
.topo-lines-dense {
background-image:
repeating-linear-gradient(
45deg,
transparent,
transparent 20px,
rgba(255, 255, 255, 0.02) 20px,
rgba(255, 255, 255, 0.02) 21px
),
repeating-linear-gradient(
-45deg,
transparent,
transparent 20px,
rgba(255, 255, 255, 0.02) 20px,
rgba(255, 255, 255, 0.02) 21px
);
}
/* Sharp borders and hover effects */
.sharp-border {
border: 1px solid rgba(255, 255, 255, 0.1);
background: #000;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.sharp-border:hover {
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
/* Grid overlay effect */
.grid-overlay {
background-image:
linear-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
background-size: 30px 30px;
}

View File

@ -1,34 +1,75 @@
// app/layout.tsx
// Root layout with navbar and footer (like Laravel layouts!)
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import Navbar from "@/components/navbar";
import InteractiveTopo from "@/components/interactiveTopo";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "OpenWheels Racing",
description: "Free racing community - Live dashboard and rankings",
};
export default function RootLayout({
children,
}: Readonly<{
}: {
children: React.ReactNode;
}>) {
}) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<body>
<div className="min-h-screen bg-[#0a0a0a] flex flex-col relative">
{/* Interactive topology effect */}
<InteractiveTopo />
{/* Static topology background */}
<div className="fixed inset-0 topo-lines pointer-events-none z-0" />
{/* Content wrapper */}
<div className="relative z-10 flex flex-col min-h-screen">
{/* Navbar - appears on all pages */}
<Navbar />
{/* Page content */}
<main className="flex-1">
{children}
</main>
{/* Footer - appears on all pages */}
<Footer />
</div>
</div>
</body>
</html>
);
}
function Footer() {
return (
<footer className="border-t border-white/10 mt-20">
<div className="max-w-7xl mx-auto px-6 py-8">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center space-x-3">
<img
src="https://openwheels.racing/files/img/Openwheels_small.svg"
alt="OW"
className="h-6 w-6 brightness-0 invert opacity-50"
/>
<span className="text-white/40 tracking-wider">
© 2025 OPENWHEELS.RACING
</span>
</div>
<div className="flex items-center space-x-6 text-white/40">
<a href="https://discord.gg/nvuB8EvT9P" className="hover:text-white transition-colors">
DISCORD
</a>
<a href="https://openwheels.racing" className="hover:text-white transition-colors">
WEBSITE
</a>
</div>
</div>
</div>
</footer>
);
}

View File

@ -1,65 +1,214 @@
import Image from "next/image";
// app/page.tsx
// Homepage - navbar/footer now in layout.tsx
import Link from 'next/link';
import { query } from '@/lib/db';
import { TrophyIcon, ChartIcon, CircleIcon, UsersIcon, ServerIcon, ActivityIcon } from '@/components/ui/icons';
async function getStats() {
const driversOnline = await query(`
SELECT COUNT(*) as count
FROM users
WHERE is_connect = true
`);
const totalDrivers = await query(`
SELECT COUNT(*) as count
FROM users
`);
const activeServers = await query(`
SELECT COUNT(DISTINCT server_id) as count
FROM servers
WHERE connected_players > 0
`);
return {
driversOnline: driversOnline[0]?.count || 0,
totalDrivers: totalDrivers[0]?.count || 0,
activeServers: activeServers[0]?.count || 0,
};
}
export default async function HomePage() {
const stats = await getStats();
export default function Home() {
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
<>
{/* Hero */}
<div className="relative border-b border-white/10">
<div className="absolute inset-0 grid-overlay opacity-50"></div>
<div className="max-w-7xl mx-auto px-6 py-32 relative">
<div className="text-center space-y-8">
{/* Logo */}
<div className="flex justify-center mb-8">
<img
src="https://openwheels.racing/files/img/Openwheels_landscape.svg"
alt="OpenWheels"
className="h-24 brightness-0 invert"
/>
</div>
{/* Tagline */}
<div className="space-y-4">
<h1 className="text-7xl font-bold tracking-tight leading-none">
FREE RACING.<br/>
NO BARRIERS.
</h1>
<p className="text-xl text-white/60 max-w-2xl mx-auto leading-relaxed">
Join our community of passionate racers. Compete in multiple leagues,
track your progress, and race for free on dedicated infrastructure.
</p>
</div>
{/* CTA */}
<div className="flex items-center justify-center space-x-4 pt-8">
<Link
href="/dashboard"
className="px-8 py-4 border-2 border-white hover:bg-white hover:text-[#0a0a0a] transition-all text-base font-medium"
>
VIEW DASHBOARD
</Link>
<a
href="https://discord.gg/nvuB8EvT9P"
target="_blank"
rel="noopener noreferrer"
className="px-8 py-4 border border-white/30 hover:border-white transition-all text-base"
>
JOIN COMMUNITY
</a>
</div>
</div>
{/* Live Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-24">
<LiveStatCard
value={stats.driversOnline}
label="ONLINE NOW"
icon={<UsersIcon />}
/>
<LiveStatCard
value={stats.activeServers}
label="ACTIVE SERVERS"
icon={<ServerIcon />}
/>
<LiveStatCard
value={stats.totalDrivers}
label="TOTAL DRIVERS"
icon={<ActivityIcon />}
/>
</div>
</div>
</div>
{/* Features Grid */}
<div className="max-w-7xl mx-auto px-6 py-24">
<div className="text-center mb-20">
<h2 className="text-5xl font-bold tracking-tight mb-4">
INFRASTRUCTURE
</h2>
<p className="text-white/60 text-lg">
Everything required for competitive sim racing
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<FeatureCard
icon={<TrophyIcon className="w-10 h-10" />}
title="MULTIPLE LEAGUES"
description="Progress through skill-based divisions. C-Class to Pro level competition across all disciplines."
/>
<FeatureCard
icon={<ChartIcon className="w-10 h-10" />}
title="LIVE RANKINGS"
description="Real-time performance tracking. Transparent ELO system with historical data and statistics."
/>
<FeatureCard
icon={<CircleIcon className="w-10 h-10" />}
title="FREE ACCESS"
description="No subscriptions. No hidden costs. Open infrastructure for the racing community."
/>
<FeatureCard
icon={<TrophyIcon className="w-10 h-10" />}
title="WEATHER SYSTEMS"
description="Dynamic conditions. Rain, dry, variable grip. Test your adaptability in all scenarios."
/>
<FeatureCard
icon={<ActivityIcon className="w-10 h-10" />}
title="MULTIPLE CLASSES"
description="MX5, GT4, GT3 categories. Different cars for different racing philosophies and styles."
/>
<FeatureCard
icon={<UsersIcon className="w-10 h-10" />}
title="ACTIVE COMMUNITY"
description="Discord integration. Event organization, race coordination, and community engagement tools."
/>
</div>
</main>
</div>
{/* System Status */}
<div className="border-t border-white/10">
<div className="max-w-7xl mx-auto px-6 py-12">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-2 h-2 bg-white rounded-full animate-pulse"></div>
<span className="text-xs tracking-wider text-white/60">SYSTEM OPERATIONAL</span>
</div>
<div className="text-xs text-white/40 font-mono">
v2.0.0 | UPTIME: 99.9%
</div>
</div>
</div>
</div>
</>
);
}
function LiveStatCard({
value,
label,
icon
}: {
value: number;
label: string;
icon: React.ReactNode;
}) {
return (
<div className="border border-white/10 p-8 sharp-border">
<div className="flex items-center justify-between mb-4">
<div className="text-white/40">
{icon}
</div>
<div className="w-2 h-2 bg-white rounded-full animate-pulse"></div>
</div>
<div className="text-5xl font-bold tracking-tight mb-2">
{value.toLocaleString()}
</div>
<div className="text-sm text-white/60 tracking-wider font-medium">
{label}
</div>
</div>
);
}
function FeatureCard({
icon,
title,
description
}: {
icon: React.ReactNode;
title: string;
description: string;
}) {
return (
<div className="border border-white/10 p-8 sharp-border topo-lines-dense bg-black">
<div className="mb-6 text-white/80">
{icon}
</div>
<h3 className="text-xl font-bold tracking-tight mb-3">{title}</h3>
<p className="text-white/60 leading-relaxed">{description}</p>
</div>
);
}

326
app/rankings/page.tsx Normal file
View File

@ -0,0 +1,326 @@
// 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';
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="font-semibold tracking-tight text-base">
{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>
);
}

View File

View File

View File

@ -0,0 +1,146 @@
// components/events/EventRegistrationForm.tsx
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export default function EventRegistrationForm({ eventId }: { eventId: number }) {
const router = useRouter();
const [formData, setFormData] = useState({
steamId: '',
carModel: '',
carSkin: '',
teamName: '',
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
setSuccess(false);
try {
const response = await fetch('/api/events/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
eventId,
...formData,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Registration failed');
}
setSuccess(true);
setTimeout(() => {
router.refresh();
}, 1500);
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Steam ID */}
<div>
<label htmlFor="steamId" className="block text-sm font-bold tracking-wider text-white/60 mb-2">
STEAM ID *
</label>
<input
type="text"
id="steamId"
required
value={formData.steamId}
onChange={(e) => setFormData({ ...formData, steamId: e.target.value })}
className="w-full px-4 py-3 bg-black border border-white/20 text-white focus:border-white focus:outline-none transition-colors"
placeholder="76561198XXXXXXXXX"
/>
<p className="text-xs text-white/40 mt-1">Your Steam ID from the database</p>
</div>
{/* Car Model */}
<div>
<label htmlFor="carModel" className="block text-sm font-bold tracking-wider text-white/60 mb-2">
CAR MODEL *
</label>
<input
type="text"
id="carModel"
required
value={formData.carModel}
onChange={(e) => setFormData({ ...formData, carModel: e.target.value })}
className="w-full px-4 py-3 bg-black border border-white/20 text-white focus:border-white focus:outline-none transition-colors font-mono"
placeholder="ks_ferrari_488_gt3"
/>
<p className="text-xs text-white/40 mt-1">Assetto Corsa car folder name</p>
</div>
{/* Car Skin */}
<div>
<label htmlFor="carSkin" className="block text-sm font-bold tracking-wider text-white/60 mb-2">
CAR SKIN
</label>
<input
type="text"
id="carSkin"
value={formData.carSkin}
onChange={(e) => setFormData({ ...formData, carSkin: e.target.value })}
className="w-full px-4 py-3 bg-black border border-white/20 text-white focus:border-white focus:outline-none transition-colors"
placeholder="01_red_white (optional)"
/>
</div>
{/* Team Name */}
<div>
<label htmlFor="teamName" className="block text-sm font-bold tracking-wider text-white/60 mb-2">
TEAM NAME
</label>
<input
type="text"
id="teamName"
value={formData.teamName}
onChange={(e) => setFormData({ ...formData, teamName: e.target.value })}
className="w-full px-4 py-3 bg-black border border-white/20 text-white focus:border-white focus:outline-none transition-colors"
placeholder="Enter team name (optional)"
/>
</div>
{/* Error Message */}
{error && (
<div className="border border-red-500/20 bg-red-500/10 p-4 text-red-400 text-sm">
{error}
</div>
)}
{/* Success Message */}
{success && (
<div className="border border-green-500/20 bg-green-500/10 p-4 text-green-400 text-sm">
Registration successful! Redirecting...
</div>
)}
{/* Submit Button */}
<button
type="submit"
disabled={loading || success}
className="w-full px-6 py-4 border border-white hover:bg-white hover:text-black transition-all text-sm font-bold tracking-wider disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'REGISTERING...' : success ? 'REGISTERED!' : 'REGISTER NOW'}
</button>
<p className="text-xs text-white/40 text-center">
* Required fields
</p>
</form>
);
}

View File

@ -0,0 +1,61 @@
// components/InteractiveTopo.tsx
'use client';
import { useEffect, useState } from 'react';
export default function InteractiveTopo() {
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
// Normalize mouse position to percentage
const x = (e.clientX / window.innerWidth) * 100;
const y = (e.clientY / window.innerHeight) * 100;
setMousePosition({ x, y });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return (
<div
className="fixed inset-0 pointer-events-none z-0 opacity-30"
style={{
background: `
radial-gradient(
circle at ${mousePosition.x}% ${mousePosition.y}%,
rgba(255, 255, 255, 0.03) 0%,
transparent 50%
)
`,
transition: 'background 0.3s ease-out',
}}
>
{/* Animated grid that warps around mouse */}
<div
className="absolute inset-0"
style={{
backgroundImage: `
repeating-linear-gradient(
0deg,
transparent,
transparent 50px,
rgba(255, 255, 255, 0.02) 50px,
rgba(255, 255, 255, 0.02) 51px
),
repeating-linear-gradient(
90deg,
transparent,
transparent 50px,
rgba(255, 255, 255, 0.02) 50px,
rgba(255, 255, 255, 0.02) 51px
)
`,
transform: `translate(${(mousePosition.x - 50) * 0.02}px, ${(mousePosition.y - 50) * 0.02}px)`,
transition: 'transform 0.3s ease-out',
}}
/>
</div>
);
}

95
components/navbar.tsx Normal file
View File

@ -0,0 +1,95 @@
// components/Navbar.tsx
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
export default function Navbar() {
const pathname = usePathname();
const isActive = (path: string) => {
if (path === '/') {
return pathname === path;
}
return pathname.startsWith(path);
};
return (
<nav className="border-b border-white/10 bg-[#0a0a0a]/80 backdrop-blur-xl sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-6">
<div className="flex items-center justify-between h-16">
{/* Logo - links to home */}
<Link href="/" className="flex items-center space-x-3 hover:opacity-80 transition-opacity">
<img
src="https://openwheels.racing/files/img/Openwheels_landscape.svg"
alt="OpenWheels"
className="h-6 hidden sm:block brightness-0 invert"
/>
</Link>
{/* Nav Links */}
<div className="flex items-center space-x-1">
<Link
href="/"
className={`px-4 py-2 text-sm font-medium transition-colors ${
isActive('/') && pathname === '/'
? 'text-white border-b-2 border-white'
: 'text-white/50 hover:text-white'
}`}
>
HOME
</Link>
<Link
href="/dashboard"
className={`px-4 py-2 text-sm font-medium transition-colors ${
isActive('/dashboard')
? 'text-white border-b-2 border-white'
: 'text-white/50 hover:text-white'
}`}
>
DASHBOARD
</Link>
<Link
href="/rankings"
className={`px-4 py-2 text-sm font-medium transition-colors ${
isActive('/rankings')
? 'text-white border-b-2 border-white'
: 'text-white/50 hover:text-white'
}`}
>
RANKINGS
</Link>
<Link
href="/events"
className={`px-4 py-2 text-sm font-medium transition-colors ${
isActive('/events')
? 'text-white border-b-2 border-white'
: 'text-white/50 hover:text-white'
}`}
>
EVENTS
</Link>
<Link
href="/live"
className={`px-4 py-2 text-sm font-medium transition-colors ${
isActive('/live')
? 'text-white border-b-2 border-white'
: 'text-white/50 hover:text-white'
}`}
>
LIVE
</Link>
<a
href="https://discord.gg/nvuB8EvT9P"
target="_blank"
rel="noopener noreferrer"
className="ml-4 px-5 py-2 text-sm border border-white hover:bg-white hover:text-[#0a0a0a] transition-all"
>
DISCORD
</a>
</div>
</div>
</div>
</nav>
);
}

136
components/ui/icons.tsx Normal file
View File

@ -0,0 +1,136 @@
// components/ui/icons.tsx
// Sharp, technical SVG icons for racing dashboard
export function UsersIcon({ className = "w-6 h-6" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" strokeLinecap="square" strokeLinejoin="miter"/>
<circle cx="9" cy="7" r="4" strokeLinecap="square" strokeLinejoin="miter"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87" strokeLinecap="square" strokeLinejoin="miter"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75" strokeLinecap="square" strokeLinejoin="miter"/>
</svg>
);
}
export function ServerIcon({ className = "w-6 h-6" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<rect x="2" y="2" width="20" height="8" rx="0" strokeLinecap="square" strokeLinejoin="miter"/>
<rect x="2" y="14" width="20" height="8" rx="0" strokeLinecap="square" strokeLinejoin="miter"/>
<line x1="6" y1="6" x2="6.01" y2="6" strokeLinecap="square" strokeLinejoin="miter"/>
<line x1="6" y1="18" x2="6.01" y2="18" strokeLinecap="square" strokeLinejoin="miter"/>
</svg>
);
}
export function ActivityIcon({ className = "w-6 h-6" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" strokeLinecap="square" strokeLinejoin="miter"/>
</svg>
);
}
export function MapPinIcon({ className = "w-6 h-6" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" strokeLinecap="square" strokeLinejoin="miter"/>
<circle cx="12" cy="10" r="3" strokeLinecap="square" strokeLinejoin="miter"/>
</svg>
);
}
export function FlagIcon({ className = "w-6 h-6" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z" strokeLinecap="square" strokeLinejoin="miter"/>
<line x1="4" y1="22" x2="4" y2="15" strokeLinecap="square" strokeLinejoin="miter"/>
</svg>
);
}
export function TrophyIcon({ className = "w-6 h-6" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6" strokeLinecap="square" strokeLinejoin="miter"/>
<path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18" strokeLinecap="square" strokeLinejoin="miter"/>
<path d="M4 22h16" strokeLinecap="square" strokeLinejoin="miter"/>
<path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22" strokeLinecap="square" strokeLinejoin="miter"/>
<path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22" strokeLinecap="square" strokeLinejoin="miter"/>
<path d="M18 2H6v7a6 6 0 0 0 12 0V2z" strokeLinecap="square" strokeLinejoin="miter"/>
</svg>
);
}
export function ChartIcon({ className = "w-6 h-6" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<line x1="12" y1="20" x2="12" y2="10" strokeLinecap="square" strokeLinejoin="miter"/>
<line x1="18" y1="20" x2="18" y2="4" strokeLinecap="square" strokeLinejoin="miter"/>
<line x1="6" y1="20" x2="6" y2="16" strokeLinecap="square" strokeLinejoin="miter"/>
</svg>
);
}
export function CircleIcon({ className = "w-6 h-6" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<circle cx="12" cy="12" r="10" strokeLinecap="square" strokeLinejoin="miter"/>
</svg>
);
}
export function LiveDotIcon({ className = "w-3 h-3" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 12 12" fill="currentColor">
<circle cx="6" cy="6" r="6"/>
</svg>
);
}
export function CalendarIcon({ className = "w-6 h-6" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<rect x="3" y="4" width="18" height="18" rx="2" strokeLinecap="square" strokeLinejoin="miter"/>
<line x1="16" y1="2" x2="16" y2="6" strokeLinecap="square" strokeLinejoin="miter"/>
<line x1="8" y1="2" x2="8" y2="6" strokeLinecap="square" strokeLinejoin="miter"/>
<line x1="3" y1="10" x2="21" y2="10" strokeLinecap="square" strokeLinejoin="miter"/>
</svg>
);
}
export function ChevronRightIcon({ className = "w-6 h-6" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<polyline points="9 18 15 12 9 6" strokeLinecap="square" strokeLinejoin="miter"/>
</svg>
);
}
export function ChevronLeftIcon({ className = "w-6 h-6" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<polyline points="15 18 9 12 15 6" strokeLinecap="square" strokeLinejoin="miter"/>
</svg>
);
}
export function ExternalLinkIcon({ className = "w-6 h-6" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" strokeLinecap="square" strokeLinejoin="miter"/>
<polyline points="15 3 21 3 21 9" strokeLinecap="square" strokeLinejoin="miter"/>
<line x1="10" y1="14" x2="21" y2="3" strokeLinecap="square" strokeLinejoin="miter"/>
</svg>
);
}
export function ClockIcon({ className = "w-6 h-6" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<circle cx="12" cy="12" r="10" strokeLinecap="square" strokeLinejoin="miter"/>
<polyline points="12 6 12 12 16 14" strokeLinecap="square" strokeLinejoin="miter"/>
</svg>
);
}

0
lib/api-client.ts Normal file
View File

61
lib/db.ts Normal file
View File

@ -0,0 +1,61 @@
// lib/db.ts
// Database connection pool - reuses connections efficiently
import { Pool } from 'pg';
// Singleton pattern - only create one pool for the entire app
let pool: Pool | null = null;
export function getPool(): Pool {
if (!pool) {
pool = new Pool({
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME || 'openwheels',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD,
max: 20, // Maximum number of connections
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
// Log pool errors
pool.on('error', (err) => {
console.error('Unexpected database error:', err);
});
}
return pool;
}
// Helper function for queries - handles errors nicely
export async function query<T = any>(
text: string,
params?: any[]
): Promise<T[]> {
const pool = getPool();
try {
const result = await pool.query(text, params);
return result.rows;
} catch (error) {
console.error('Database query error:', error);
throw error;
}
}
// Helper for single row queries
export async function queryOne<T = any>(
text: string,
params?: any[]
): Promise<T | null> {
const rows = await query<T>(text, params);
return rows.length > 0 ? rows[0] : null;
}
// Graceful shutdown
export async function closePool(): Promise<void> {
if (pool) {
await pool.end();
pool = null;
}
}

View File

@ -1,7 +1,24 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
/** @type {import('next').NextConfig} */
const nextConfig = {
// Use standalone output for server deployment
output: 'standalone',
// Image optimization
images: {
unoptimized: true,
remotePatterns: [
{
protocol: 'https',
hostname: 'openwheels.racing',
},
],
},
// Type checking during build
typescript: {
ignoreBuildErrors: false,
},
};
export default nextConfig;
module.exports = nextConfig;

429
package-lock.json generated
View File

@ -8,13 +8,18 @@
"name": "openwheels-frontend",
"version": "0.1.0",
"dependencies": {
"axios": "^1.12.2",
"date-fns": "^4.1.0",
"next": "16.0.0",
"pg": "^8.16.3",
"react": "19.2.0",
"react-dom": "19.2.0"
"react-dom": "19.2.0",
"socket.io-client": "^4.8.1"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/node": "^20.19.23",
"@types/pg": "^8.15.5",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
@ -1195,6 +1200,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@ -1517,6 +1528,18 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@types/pg": {
"version": "8.15.5",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz",
"integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"pg-protocol": "*",
"pg-types": "^2.2.0"
}
},
"node_modules/@types/react": {
"version": "19.2.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
@ -2357,6 +2380,12 @@
"node": ">= 0.4"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@ -2383,6 +2412,17 @@
"node": ">=4"
}
},
"node_modules/axios": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@ -2491,7 +2531,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@ -2591,6 +2630,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -2688,6 +2739,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@ -2749,6 +2810,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@ -2776,7 +2846,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@ -2801,6 +2870,45 @@
"dev": true,
"license": "MIT"
},
"node_modules/engine.io-client": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/enhanced-resolve": {
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
@ -2888,7 +2996,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -2898,7 +3005,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -2936,7 +3042,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@ -2949,7 +3054,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@ -3564,6 +3668,26 @@
"dev": true,
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@ -3580,11 +3704,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -3645,7 +3784,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@ -3670,7 +3808,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
@ -3758,7 +3895,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -3837,7 +3973,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -3850,7 +3985,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@ -3866,7 +4000,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@ -4849,7 +4982,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -4879,6 +5011,27 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -4906,7 +5059,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
@ -5268,6 +5420,95 @@
"dev": true,
"license": "MIT"
},
"node_modules/pg": {
"version": "8.16.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1",
"pg-protocol": "^1.10.3",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
"engines": {
"node": ">= 16.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.2.7"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz",
"integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==",
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
"integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
"license": "MIT"
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"license": "ISC",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
"integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
"integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==",
"license": "MIT"
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"license": "MIT",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -5326,6 +5567,45 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -5348,6 +5628,12 @@
"react-is": "^16.13.1"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -5802,6 +6088,68 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/socket.io-client": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -5811,6 +6159,15 @@
"node": ">=0.10.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/stable-hash": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
@ -6490,6 +6847,44 @@
"node": ">=0.10.0"
}
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@ -1,26 +1,31 @@
{
"name": "openwheels-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"react": "19.2.0",
"react-dom": "19.2.0",
"next": "16.0.0"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4",
"eslint": "^9",
"eslint-config-next": "16.0.0"
}
"name": "openwheels-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --hostname 0.0.0.0",
"build": "next build",
"start": "next start --hostname 0.0.0.0 --port 3001",
"lint": "next lint"
},
"dependencies": {
"axios": "^1.12.2",
"date-fns": "^4.1.0",
"next": "16.0.0",
"pg": "^8.16.3",
"react": "19.2.0",
"react-dom": "19.2.0",
"socket.io-client": "^4.8.1"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20.19.23",
"@types/pg": "^8.15.5",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.0.0",
"tailwindcss": "^4",
"typescript": "^5"
}
}

110
types/racing.ts Normal file
View File

@ -0,0 +1,110 @@
// types/racing.ts
// These match your PostgreSQL schema exactly
export type FlagType = 'NONE' | 'YELLOW' | 'BLUE' | 'BLACK' | 'CHECKERED';
export interface Server {
server_id: number;
session_type: number;
session_flag: FlagType;
session_count: number;
server_name: string;
server_track: string;
server_config: string;
server_weather_graphics: string | null;
typ: number | null;
session_time: number;
session_laps: number;
session_wait_time: number;
session_ambient_temp: number;
session_road_temp: number;
session_elapsed_time: number;
connected_players: number;
created_at: Date;
updated_at: Date;
}
export interface Driver {
driver_guid: number;
driver_name: string;
driver_team: string | null;
car_model: string | null;
car_skin: string | null;
cuts_alltime: number;
contacts_alltime: number;
laps_completed: number;
user_rank: number;
is_connect: boolean;
is_loading: boolean;
current_server: number | null;
created_at: Date;
}
// Helpful joined type for displaying drivers with their server info
export interface DriverWithServer extends Driver {
server?: Partial<Server>; // Make it partial since we don't always need all fields
}
// For live tracking (you mentioned getting positions from parser)
export interface LivePosition {
driver_guid: number;
driver_name: string;
position: number;
lap: number;
last_lap_time?: number;
best_lap_time?: number;
car_model: string;
// Add whatever else your parser sends
}
// API Response wrappers
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
}
export interface DashboardStats {
total_drivers_online: number;
active_servers: number;
total_laps_today: number;
fastest_lap_today?: {
driver_name: string;
lap_time: number;
track: string;
};
}
// Events types
export interface Event {
event_id: number;
event_name: string;
event_description: string | null;
event_track: string;
event_date: Date;
event_duration: number | null;
max_participants: number;
registration_deadline: Date | null;
event_status: 'OPEN' | 'CLOSED' | 'COMPLETED' | 'CANCELLED';
event_banner_url: string | null;
event_rules: string | null;
created_at: Date;
updated_at: Date;
}
export interface EventRegistration {
registration_id: number;
event_id: number;
driver_guid: number; // Changed from string to number (BIGINT)
car_model: string;
car_skin: string | null;
team_name: string | null;
registration_date: Date;
status: 'REGISTERED' | 'CONFIRMED' | 'CANCELLED';
notes: string | null;
}
export interface EventWithRegistrations extends Event {
registrations_count: number;
user_registered?: boolean;
}