From ede50035303a020a47888e5ef227150611df3763 Mon Sep 17 00:00:00 2001 From: Server Ubunto - HOME Date: Sat, 25 Oct 2025 23:16:38 +0100 Subject: [PATCH] =?UTF-8?q?FODASSE=20QUE=20BORRA=C3=87O?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/drivers/route.ts | 322 +++++++++++++++ app/api/events/register/route.ts | 105 +++++ app/dashboard/page.tsx | 235 +++++++++++ app/events/[event_id]/page.tsx | 193 +++++++++ app/events/page.tsx | 160 ++++++++ app/globals.css | 89 +++- app/layout.tsx | 79 +++- app/page.tsx | 263 +++++++++--- app/rankings/page.tsx | 326 +++++++++++++++ components/dashboard/LiveDrivers.tsx | 0 components/dashboard/ServerList.tsx | 0 components/events/EventRegistrationForm.tsx | 146 +++++++ components/interactiveTopo.tsx | 61 +++ components/navbar.tsx | 95 +++++ components/ui/icons.tsx | 136 +++++++ lib/api-client.ts | 0 lib/db.ts | 61 +++ next.config.ts | 25 +- package-lock.json | 429 +++++++++++++++++++- package.json | 53 +-- types/racing.ts | 110 +++++ 21 files changed, 2745 insertions(+), 143 deletions(-) create mode 100644 app/api/drivers/route.ts create mode 100644 app/api/events/register/route.ts create mode 100644 app/dashboard/page.tsx create mode 100644 app/events/[event_id]/page.tsx create mode 100644 app/events/page.tsx create mode 100644 app/rankings/page.tsx create mode 100644 components/dashboard/LiveDrivers.tsx create mode 100644 components/dashboard/ServerList.tsx create mode 100644 components/events/EventRegistrationForm.tsx create mode 100644 components/interactiveTopo.tsx create mode 100644 components/navbar.tsx create mode 100644 components/ui/icons.tsx create mode 100644 lib/api-client.ts create mode 100644 lib/db.ts create mode 100644 types/racing.ts diff --git a/app/api/drivers/route.ts b/app/api/drivers/route.ts new file mode 100644 index 0000000..6507b61 --- /dev/null +++ b/app/api/drivers/route.ts @@ -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 } + ); + } +} diff --git a/app/api/events/register/route.ts b/app/api/events/register/route.ts new file mode 100644 index 0000000..7d773d0 --- /dev/null +++ b/app/api/events/register/route.ts @@ -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 } + ); + } +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx new file mode 100644 index 0000000..8e101ed --- /dev/null +++ b/app/dashboard/page.tsx @@ -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 { + 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); + + return ( + <> + {/* Hero */} +
+
+
+
+ + LIVE SYSTEM +
+

+ LIVE DASHBOARD +

+

+ Real-time telemetry and session monitoring across all OpenWheels Racing infrastructure +

+
+ + {/* Stats Grid */} +
+ } + /> + } + /> + sum + d.laps_completed, 0)} + icon={} + /> +
+
+
+ + {/* Server Listings */} +
+ {Object.keys(serverGroups).length === 0 ? ( +
+
+

NO ACTIVE SESSIONS

+

System idle — waiting for connections

+
+ ) : ( + Object.entries(serverGroups).map(([serverId, serverDrivers]) => { + const server = serverDrivers[0].server; + return ( +
+ {/* Server Header */} +
+
+
+
+
+ + LIVE +
+ + ID: {server?.server_id} + +
+

+ {server?.server_name} +

+
+
+ + {server?.server_track} +
+
+ + {server?.session_flag} +
+
+ + {server?.connected_players} CONNECTED +
+
+
+
+
+ + {/* Driver Table */} +
+ + + + + + + + + + + + + {serverDrivers.map((driver, index) => ( + + + + + + + + + ))} + +
POSDRIVERTEAMCARRANKLAPS
+ + {String(index + 1).padStart(2, '0')} + + + {driver.driver_name} + + + {driver.driver_team || '—'} + + + + {driver.car_model} + + + + {driver.user_rank} + + + {driver.laps_completed} +
+
+
+ ); + }) + )} +
+ + ); +} + +function StatCard({ + title, + value, + icon +}: { + title: string; + value: number; + icon: React.ReactNode; +}) { + return ( +
+
+ {title} +
+ {icon} +
+
+
+ {value.toLocaleString()} +
+
+ ); +} diff --git a/app/events/[event_id]/page.tsx b/app/events/[event_id]/page.tsx new file mode 100644 index 0000000..c2e524c --- /dev/null +++ b/app/events/[event_id]/page.tsx @@ -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 { + 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 { + 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 */} +
+
+
+
+ + EVENT DETAILS +
+

+ {event.event_name} +

+

+ {event.event_description} +

+
+ + {/* Event Info Cards */} +
+ } /> + + + } + /> +
+
+
+ + {/* Main Content */} +
+
+ {/* Left Column - Registration */} +
+ {/* Registration Form */} + {canRegister ? ( +
+

REGISTER FOR EVENT

+ +
+ ) : ( +
+

REGISTRATION CLOSED

+

+ {isFull && 'This event is full.'} + {deadlinePassed && !isFull && 'Registration deadline has passed.'} + {event.event_status === 'CLOSED' && !isFull && !deadlinePassed && 'Registration is closed for this event.'} +

+
+ )} + + {/* Event Rules */} + {event.event_rules && ( +
+

EVENT RULES

+
+ {event.event_rules} +
+
+ )} +
+ + {/* Right Column - Registered Participants */} +
+
+

+ REGISTERED DRIVERS ({event.registrations_count}) +

+
+ {registrations.length === 0 ? ( +

No registrations yet

+ ) : ( + registrations.map((reg: any, index: number) => ( +
+
+
+ + #{String(index + 1).padStart(2, '0')} + + {reg.driver_name} +
+
+
+
Car: {reg.car_model}
+ {reg.car_skin &&
Skin: {reg.car_skin}
} + {reg.team_name &&
Team: {reg.team_name}
} +
+
+ )) + )} +
+
+
+
+
+ + ); +} + +function InfoCard({ + label, + value, + icon +}: { + label: string; + value: string; + icon: React.ReactNode; +}) { + return ( +
+
+ {label} +
+ {icon} +
+
+
+ {value} +
+
+ ); +} diff --git a/app/events/page.tsx b/app/events/page.tsx new file mode 100644 index 0000000..102b8ce --- /dev/null +++ b/app/events/page.tsx @@ -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 { + 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 */} +
+
+
+
+ + RACING EVENTS +
+

+ EVENTS +

+

+ Join competitive racing events, championships, and special races. Register now to secure your spot. +

+
+
+
+ + {/* Events List */} +
+ {events.length === 0 ? ( +
+ +

NO UPCOMING EVENTS

+

Check back soon for new racing events

+
+ ) : ( + 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 ( +
+ + {event.event_name} + + {/* Event Header */} +
+
+
+
+
+ {event.event_status} +
+ + ID: {event.event_id} + +
+

+ {event.event_name} +

+

+ {event.event_description} +

+
+
+ + {event.event_track} +
+
+ + + {event.registrations_count}/{event.max_participants} registered + +
+
+ + + {formatDate(event.event_date)} + +
+ {event.event_duration && ( +
+ + + {event.event_duration} minutes + +
+ )} +
+
+
+
+ + {/* Event Actions */} +
+
+ {deadlinePassed && Registration deadline passed} + {isFull && !deadlinePassed && Event is full} + {!canRegister && !deadlinePassed && !isFull && event.event_status === 'CLOSED' && Registration closed} +
+ + {canRegister ? 'REGISTER NOW' : 'VIEW DETAILS'} + +
+
+ ); + }) + )} +
+ + ); +} diff --git a/app/globals.css b/app/globals.css index a2dc41e..bda5463 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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; } diff --git a/app/layout.tsx b/app/layout.tsx index f7fa87e..a4adb85 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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 ( - - {children} + +
+ {/* Interactive topology effect */} + + + {/* Static topology background */} +
+ + {/* Content wrapper */} +
+ {/* Navbar - appears on all pages */} + + + {/* Page content */} +
+ {children} +
+ + {/* Footer - appears on all pages */} +
+
+
); } + +function Footer() { + return ( + + ); +} diff --git a/app/page.tsx b/app/page.tsx index 295f8fd..70fe3e1 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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 ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. + <> + {/* Hero */} +

+
+ +
+
+ {/* Logo */} +
+ OpenWheels +
+ + {/* Tagline */} +
+

+ FREE RACING.
+ NO BARRIERS. +

+ +

+ Join our community of passionate racers. Compete in multiple leagues, + track your progress, and race for free on dedicated infrastructure. +

+
+ + {/* CTA */} +
+ + VIEW DASHBOARD + + + JOIN COMMUNITY + +
+
+ + {/* Live Stats */} +
+ } + /> + } + /> + } + /> +
+
+
+ + {/* Features Grid */} +
+
+

+ INFRASTRUCTURE +

+

+ Everything required for competitive sim racing

-
- - Vercel logomark - Deploy Now - - - Documentation - + +
+ } + title="MULTIPLE LEAGUES" + description="Progress through skill-based divisions. C-Class to Pro level competition across all disciplines." + /> + } + title="LIVE RANKINGS" + description="Real-time performance tracking. Transparent ELO system with historical data and statistics." + /> + } + title="FREE ACCESS" + description="No subscriptions. No hidden costs. Open infrastructure for the racing community." + /> + } + title="WEATHER SYSTEMS" + description="Dynamic conditions. Rain, dry, variable grip. Test your adaptability in all scenarios." + /> + } + title="MULTIPLE CLASSES" + description="MX5, GT4, GT3 categories. Different cars for different racing philosophies and styles." + /> + } + title="ACTIVE COMMUNITY" + description="Discord integration. Event organization, race coordination, and community engagement tools." + />
-
+
+ + {/* System Status */} +
+
+
+
+
+ SYSTEM OPERATIONAL +
+
+ v2.0.0 | UPTIME: 99.9% +
+
+
+
+ + ); +} + +function LiveStatCard({ + value, + label, + icon +}: { + value: number; + label: string; + icon: React.ReactNode; +}) { + return ( +
+
+
+ {icon} +
+
+
+
+ {value.toLocaleString()} +
+
+ {label} +
+
+ ); +} + +function FeatureCard({ + icon, + title, + description +}: { + icon: React.ReactNode; + title: string; + description: string; +}) { + return ( +
+
+ {icon} +
+

{title}

+

{description}

); } diff --git a/app/rankings/page.tsx b/app/rankings/page.tsx new file mode 100644 index 0000000..c39d03b --- /dev/null +++ b/app/rankings/page.tsx @@ -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 */} +
+
+
+
+ + DRIVER RANKINGS +
+

+ LEADERBOARD +

+

+ Global driver rankings based on performance, consistency, and clean racing +

+

+ {totalCount.toLocaleString()} total drivers +

+
+ + {/* Class Explanation */} +
+ + + + +
+
+
+ + {/* Rankings Table */} +
+ {/* Page Size Selector */} +
+
+ Items per page: +
+
+ {PAGE_SIZE_OPTIONS.map((size) => ( + + {size} + + ))} +
+
+ +
+ + + + + + + + + + + + + {drivers.map((driver, index) => { + const driverClass = getDriverClass(driver.user_rank); + const globalRank = startRank + index; + const isPodium = globalRank <= 3; + + return ( + + + + + + + + + ); + })} + +
RANKDRIVERRATINGCLASSLAPSSTEAM ID
+ 3 ? 'text-white/60' : ''} + `}> + {String(globalRank).padStart(2, '0')} + + {isPodium && ( + + {globalRank === 1 ? '█' : globalRank === 2 ? '▓' : '▒'} + + )} + + + {driver.driver_name} + + + + {driver.user_rank.toLocaleString()} + + + + {driverClass.name} + + + + {driver.laps_completed.toLocaleString()} + + + + {driver.driver_guid} + +
+
+ + {/* Pagination */} + {totalPages > 1 && ( +
+
+ Page {currentPage} of {totalPages} • Showing {startRank}-{Math.min(startRank + pageSize - 1, totalCount)} of {totalCount.toLocaleString()} +
+
+ {/* First Page */} + {currentPage > 1 && ( + + «« + + )} + + {/* Previous Page */} + {currentPage > 1 && ( + + ‹ PREV + + )} + + {/* 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 ( + + {pageNum} + + ); + })} + + {/* Next Page */} + {currentPage < totalPages && ( + + NEXT › + + )} + + {/* Last Page */} + {currentPage < totalPages && ( + + »» + + )} +
+
+ )} + + {/* Info Box */} +
+

RANKING SYSTEM

+
+

• Rating increases with clean racing and fast lap times

+

• Penalties applied for track cuts and collisions

+

• Class promotion based on consistent performance over multiple sessions

+
+
+
+ + ); +} + +function ClassCard({ + name, + range, + description +}: { + name: string; + range: string; + description: string; +}) { + return ( +
+
+ {name} +
+
+ {range} +
+
+ {description} +
+
+ ); +} diff --git a/components/dashboard/LiveDrivers.tsx b/components/dashboard/LiveDrivers.tsx new file mode 100644 index 0000000..e69de29 diff --git a/components/dashboard/ServerList.tsx b/components/dashboard/ServerList.tsx new file mode 100644 index 0000000..e69de29 diff --git a/components/events/EventRegistrationForm.tsx b/components/events/EventRegistrationForm.tsx new file mode 100644 index 0000000..c15a9dc --- /dev/null +++ b/components/events/EventRegistrationForm.tsx @@ -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 ( +
+ {/* Steam ID */} +
+ + 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" + /> +

Your Steam ID from the database

+
+ + {/* Car Model */} +
+ + 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" + /> +

Assetto Corsa car folder name

+
+ + {/* Car Skin */} +
+ + 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)" + /> +
+ + {/* Team Name */} +
+ + 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)" + /> +
+ + {/* Error Message */} + {error && ( +
+ {error} +
+ )} + + {/* Success Message */} + {success && ( +
+ Registration successful! Redirecting... +
+ )} + + {/* Submit Button */} + + +

+ * Required fields +

+
+ ); +} diff --git a/components/interactiveTopo.tsx b/components/interactiveTopo.tsx new file mode 100644 index 0000000..43ee46f --- /dev/null +++ b/components/interactiveTopo.tsx @@ -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 ( +
+ {/* Animated grid that warps around mouse */} +
+
+ ); +} diff --git a/components/navbar.tsx b/components/navbar.tsx new file mode 100644 index 0000000..de65497 --- /dev/null +++ b/components/navbar.tsx @@ -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 ( + + ); +} diff --git a/components/ui/icons.tsx b/components/ui/icons.tsx new file mode 100644 index 0000000..feba003 --- /dev/null +++ b/components/ui/icons.tsx @@ -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 ( + + + + + + + ); +} + +export function ServerIcon({ className = "w-6 h-6" }: { className?: string }) { + return ( + + + + + + + ); +} + +export function ActivityIcon({ className = "w-6 h-6" }: { className?: string }) { + return ( + + + + ); +} + +export function MapPinIcon({ className = "w-6 h-6" }: { className?: string }) { + return ( + + + + + ); +} + +export function FlagIcon({ className = "w-6 h-6" }: { className?: string }) { + return ( + + + + + ); +} + +export function TrophyIcon({ className = "w-6 h-6" }: { className?: string }) { + return ( + + + + + + + + + ); +} + +export function ChartIcon({ className = "w-6 h-6" }: { className?: string }) { + return ( + + + + + + ); +} + +export function CircleIcon({ className = "w-6 h-6" }: { className?: string }) { + return ( + + + + ); +} + +export function LiveDotIcon({ className = "w-3 h-3" }: { className?: string }) { + return ( + + + + ); +} + +export function CalendarIcon({ className = "w-6 h-6" }: { className?: string }) { + return ( + + + + + + + ); +} + +export function ChevronRightIcon({ className = "w-6 h-6" }: { className?: string }) { + return ( + + + + ); +} + +export function ChevronLeftIcon({ className = "w-6 h-6" }: { className?: string }) { + return ( + + + + ); +} + +export function ExternalLinkIcon({ className = "w-6 h-6" }: { className?: string }) { + return ( + + + + + + ); +} + +export function ClockIcon({ className = "w-6 h-6" }: { className?: string }) { + return ( + + + + + ); +} + diff --git a/lib/api-client.ts b/lib/api-client.ts new file mode 100644 index 0000000..e69de29 diff --git a/lib/db.ts b/lib/db.ts new file mode 100644 index 0000000..04e5ee2 --- /dev/null +++ b/lib/db.ts @@ -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( + text: string, + params?: any[] +): Promise { + 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( + text: string, + params?: any[] +): Promise { + const rows = await query(text, params); + return rows.length > 0 ? rows[0] : null; +} + +// Graceful shutdown +export async function closePool(): Promise { + if (pool) { + await pool.end(); + pool = null; + } +} diff --git a/next.config.ts b/next.config.ts index e9ffa30..49e29df 100644 --- a/next.config.ts +++ b/next.config.ts @@ -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; diff --git a/package-lock.json b/package-lock.json index 2f0a6c4..33441ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index ac1ea2c..2309d76 100644 --- a/package.json +++ b/package.json @@ -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" + } } diff --git a/types/racing.ts b/types/racing.ts new file mode 100644 index 0000000..56b774b --- /dev/null +++ b/types/racing.ts @@ -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; // 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 { + 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; +}