generated from theradcoza/Laravel-Docker-Dev-Template
Rename car_number to number, time_ms to best_time_ms, update participant statuses, and redesign leaderboard with broadcast-style UI
This commit is contained in:
@@ -29,30 +29,34 @@ public static function form(Form $form): Form
|
||||
Forms\Components\TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->placeholder('Enter participant name'),
|
||||
Forms\Components\TextInput::make('car_number')
|
||||
->label('Car Number')
|
||||
->placeholder('Driver name'),
|
||||
Forms\Components\TextInput::make('number')
|
||||
->label('Number')
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(999)
|
||||
->placeholder('e.g., 44'),
|
||||
])->columns(2),
|
||||
Forms\Components\Select::make('status')
|
||||
->options(Participant::statuses())
|
||||
->default('ready')
|
||||
->required(),
|
||||
])->columns(3),
|
||||
|
||||
Forms\Components\Section::make('Timing')
|
||||
Forms\Components\Section::make('Best Time')
|
||||
->schema([
|
||||
Forms\Components\Grid::make(4)
|
||||
Forms\Components\Grid::make(3)
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('time_minutes')
|
||||
->label('Minutes')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(59)
|
||||
->default(0)
|
||||
->placeholder('00')
|
||||
->default(null)
|
||||
->placeholder('MM')
|
||||
->dehydrated(false)
|
||||
->afterStateHydrated(function ($component, $state, $record) {
|
||||
if ($record && $record->time_ms) {
|
||||
$component->state(floor($record->time_ms / 60000));
|
||||
if ($record && $record->best_time_ms) {
|
||||
$component->state(floor($record->best_time_ms / 60000));
|
||||
}
|
||||
}),
|
||||
Forms\Components\TextInput::make('time_seconds')
|
||||
@@ -60,12 +64,12 @@ public static function form(Form $form): Form
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(59)
|
||||
->default(0)
|
||||
->placeholder('00')
|
||||
->default(null)
|
||||
->placeholder('SS')
|
||||
->dehydrated(false)
|
||||
->afterStateHydrated(function ($component, $state, $record) {
|
||||
if ($record && $record->time_ms) {
|
||||
$component->state(floor(($record->time_ms % 60000) / 1000));
|
||||
if ($record && $record->best_time_ms) {
|
||||
$component->state(floor(($record->best_time_ms % 60000) / 1000));
|
||||
}
|
||||
}),
|
||||
Forms\Components\TextInput::make('time_milliseconds')
|
||||
@@ -73,26 +77,18 @@ public static function form(Form $form): Form
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(999)
|
||||
->default(0)
|
||||
->placeholder('000')
|
||||
->default(null)
|
||||
->placeholder('ms')
|
||||
->dehydrated(false)
|
||||
->afterStateHydrated(function ($component, $state, $record) {
|
||||
if ($record && $record->time_ms) {
|
||||
$component->state($record->time_ms % 1000);
|
||||
if ($record && $record->best_time_ms) {
|
||||
$component->state($record->best_time_ms % 1000);
|
||||
}
|
||||
}),
|
||||
Forms\Components\Select::make('status')
|
||||
->options([
|
||||
'pending' => 'Pending',
|
||||
'completed' => 'Completed',
|
||||
'dnf' => 'DNF',
|
||||
])
|
||||
->default('pending')
|
||||
->required(),
|
||||
]),
|
||||
Forms\Components\Hidden::make('time_ms'),
|
||||
Forms\Components\Hidden::make('best_time_ms'),
|
||||
])
|
||||
->description('Enter time as MM:SS.ms (e.g., 01:23.456)'),
|
||||
->description('Enter best lap time as MM:SS.ms (leave blank if no time set)'),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -107,7 +103,7 @@ public static function table(Table $table): Table
|
||||
})
|
||||
->alignCenter()
|
||||
->weight('bold'),
|
||||
Tables\Columns\TextColumn::make('car_number')
|
||||
Tables\Columns\TextColumn::make('number')
|
||||
->label('#')
|
||||
->alignCenter()
|
||||
->placeholder('-')
|
||||
@@ -117,29 +113,28 @@ public static function table(Table $table): Table
|
||||
->sortable()
|
||||
->weight('bold'),
|
||||
Tables\Columns\TextColumn::make('formatted_time')
|
||||
->label('Time')
|
||||
->placeholder('No time')
|
||||
->label('Best Time')
|
||||
->placeholder('—')
|
||||
->fontFamily('mono')
|
||||
->alignCenter(),
|
||||
Tables\Columns\BadgeColumn::make('status')
|
||||
->formatStateUsing(fn (string $state): string => Participant::statuses()[$state] ?? $state)
|
||||
->colors([
|
||||
'warning' => 'pending',
|
||||
'success' => 'completed',
|
||||
'gray' => 'ready',
|
||||
'info' => 'running',
|
||||
'success' => 'finished',
|
||||
'warning' => 'pit',
|
||||
'danger' => 'dnf',
|
||||
]),
|
||||
Tables\Columns\TextColumn::make('updated_at')
|
||||
->label('Last Update')
|
||||
->dateTime('M j, H:i')
|
||||
->label('Updated')
|
||||
->dateTime('H:i:s')
|
||||
->sortable(),
|
||||
])
|
||||
->defaultSort('time_ms', 'asc')
|
||||
->defaultSort('best_time_ms', 'asc')
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('status')
|
||||
->options([
|
||||
'pending' => 'Pending',
|
||||
'completed' => 'Completed',
|
||||
'dnf' => 'DNF',
|
||||
]),
|
||||
->options(Participant::statuses()),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
@@ -168,24 +163,24 @@ public static function getPages(): array
|
||||
|
||||
public static function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
return static::calculateTimeMs($data);
|
||||
return static::calculateBestTimeMs($data);
|
||||
}
|
||||
|
||||
public static function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
return static::calculateTimeMs($data);
|
||||
return static::calculateBestTimeMs($data);
|
||||
}
|
||||
|
||||
protected static function calculateTimeMs(array $data): array
|
||||
protected static function calculateBestTimeMs(array $data): array
|
||||
{
|
||||
$minutes = (int) ($data['time_minutes'] ?? 0);
|
||||
$seconds = (int) ($data['time_seconds'] ?? 0);
|
||||
$milliseconds = (int) ($data['time_milliseconds'] ?? 0);
|
||||
$minutes = $data['time_minutes'] ?? null;
|
||||
$seconds = $data['time_seconds'] ?? null;
|
||||
$milliseconds = $data['time_milliseconds'] ?? null;
|
||||
|
||||
if ($minutes > 0 || $seconds > 0 || $milliseconds > 0) {
|
||||
$data['time_ms'] = ($minutes * 60000) + ($seconds * 1000) + $milliseconds;
|
||||
if ($minutes !== null || $seconds !== null || $milliseconds !== null) {
|
||||
$data['best_time_ms'] = ((int)$minutes * 60000) + ((int)$seconds * 1000) + (int)$milliseconds;
|
||||
} else {
|
||||
$data['time_ms'] = null;
|
||||
$data['best_time_ms'] = null;
|
||||
}
|
||||
|
||||
return $data;
|
||||
|
||||
@@ -11,14 +11,14 @@ class CreateParticipant extends CreateRecord
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$minutes = (int) ($data['time_minutes'] ?? 0);
|
||||
$seconds = (int) ($data['time_seconds'] ?? 0);
|
||||
$milliseconds = (int) ($data['time_milliseconds'] ?? 0);
|
||||
$minutes = $data['time_minutes'] ?? null;
|
||||
$seconds = $data['time_seconds'] ?? null;
|
||||
$milliseconds = $data['time_milliseconds'] ?? null;
|
||||
|
||||
if ($minutes > 0 || $seconds > 0 || $milliseconds > 0) {
|
||||
$data['time_ms'] = ($minutes * 60000) + ($seconds * 1000) + $milliseconds;
|
||||
if ($minutes !== null || $seconds !== null || $milliseconds !== null) {
|
||||
$data['best_time_ms'] = ((int)$minutes * 60000) + ((int)$seconds * 1000) + (int)$milliseconds;
|
||||
} else {
|
||||
$data['time_ms'] = null;
|
||||
$data['best_time_ms'] = null;
|
||||
}
|
||||
|
||||
unset($data['time_minutes'], $data['time_seconds'], $data['time_milliseconds']);
|
||||
|
||||
@@ -19,14 +19,14 @@ protected function getHeaderActions(): array
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
$minutes = (int) ($data['time_minutes'] ?? 0);
|
||||
$seconds = (int) ($data['time_seconds'] ?? 0);
|
||||
$milliseconds = (int) ($data['time_milliseconds'] ?? 0);
|
||||
$minutes = $data['time_minutes'] ?? null;
|
||||
$seconds = $data['time_seconds'] ?? null;
|
||||
$milliseconds = $data['time_milliseconds'] ?? null;
|
||||
|
||||
if ($minutes > 0 || $seconds > 0 || $milliseconds > 0) {
|
||||
$data['time_ms'] = ($minutes * 60000) + ($seconds * 1000) + $milliseconds;
|
||||
if ($minutes !== null || $seconds !== null || $milliseconds !== null) {
|
||||
$data['best_time_ms'] = ((int)$minutes * 60000) + ((int)$seconds * 1000) + (int)$milliseconds;
|
||||
} else {
|
||||
$data['time_ms'] = null;
|
||||
$data['best_time_ms'] = null;
|
||||
}
|
||||
|
||||
unset($data['time_minutes'], $data['time_seconds'], $data['time_milliseconds']);
|
||||
|
||||
@@ -11,26 +11,40 @@ class Participant extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'car_number',
|
||||
'time_ms',
|
||||
'number',
|
||||
'best_time_ms',
|
||||
'status',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'car_number' => 'integer',
|
||||
'time_ms' => 'integer',
|
||||
'number' => 'integer',
|
||||
'best_time_ms' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Format time_ms as mm:ss.ms (e.g., 01:23.456)
|
||||
*/
|
||||
const STATUS_READY = 'ready';
|
||||
const STATUS_RUNNING = 'running';
|
||||
const STATUS_FINISHED = 'finished';
|
||||
const STATUS_PIT = 'pit';
|
||||
const STATUS_DNF = 'dnf';
|
||||
|
||||
public static function statuses(): array
|
||||
{
|
||||
return [
|
||||
self::STATUS_READY => 'READY',
|
||||
self::STATUS_RUNNING => 'RUNNING',
|
||||
self::STATUS_FINISHED => 'FINISHED',
|
||||
self::STATUS_PIT => 'IN PIT',
|
||||
self::STATUS_DNF => 'DNF',
|
||||
];
|
||||
}
|
||||
|
||||
public function getFormattedTimeAttribute(): ?string
|
||||
{
|
||||
if ($this->time_ms === null) {
|
||||
if ($this->best_time_ms === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$totalMs = $this->time_ms;
|
||||
$totalMs = $this->best_time_ms;
|
||||
$minutes = floor($totalMs / 60000);
|
||||
$seconds = floor(($totalMs % 60000) / 1000);
|
||||
$milliseconds = $totalMs % 1000;
|
||||
@@ -38,20 +52,14 @@ public function getFormattedTimeAttribute(): ?string
|
||||
return sprintf('%02d:%02d.%03d', $minutes, $seconds, $milliseconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get ranked participants (fastest first, null times last)
|
||||
*/
|
||||
public function scopeRanked($query)
|
||||
public function getStatusLabelAttribute(): string
|
||||
{
|
||||
return $query->orderByRaw('CASE WHEN time_ms IS NULL THEN 1 ELSE 0 END')
|
||||
->orderBy('time_ms', 'asc');
|
||||
return self::statuses()[$this->status] ?? strtoupper($this->status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get only completed participants
|
||||
*/
|
||||
public function scopeCompleted($query)
|
||||
public function scopeRanked($query)
|
||||
{
|
||||
return $query->where('status', 'completed');
|
||||
return $query->orderByRaw('CASE WHEN best_time_ms IS NULL THEN 1 ELSE 0 END')
|
||||
->orderBy('best_time_ms', 'asc');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ public function up(): void
|
||||
Schema::create('participants', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->integer('car_number')->nullable();
|
||||
$table->integer('time_ms')->nullable(); // Time in milliseconds for accurate sorting
|
||||
$table->string('status')->default('pending'); // pending, completed, dnf
|
||||
$table->integer('number')->nullable();
|
||||
$table->integer('best_time_ms')->nullable(); // Best lap time in milliseconds
|
||||
$table->string('status')->default('ready'); // ready, running, finished, pit, dnf
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="refresh" content="10">
|
||||
<title>Shell Leaderboard</title>
|
||||
<title>Shell Race Standings</title>
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700,800,900&display=swap" rel="stylesheet" />
|
||||
<link href="https://fonts.bunny.net/css?family=russo-one:400&display=swap" rel="stylesheet" />
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
@@ -16,176 +16,193 @@
|
||||
shell: {
|
||||
red: '#E60000',
|
||||
yellow: '#FFD100',
|
||||
dark: '#1A1A1A',
|
||||
},
|
||||
broadcast: {
|
||||
bg: '#0E0E0E',
|
||||
bar: '#F5F5F5',
|
||||
}
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||
}
|
||||
display: ['Russo One', 'sans-serif'],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% { box-shadow: 0 0 20px rgba(230, 0, 0, 0.3); }
|
||||
50% { box-shadow: 0 0 40px rgba(230, 0, 0, 0.6); }
|
||||
body {
|
||||
background: #0E0E0E;
|
||||
font-family: 'Russo One', sans-serif;
|
||||
}
|
||||
.leader-glow {
|
||||
animation: pulse-glow 2s ease-in-out infinite;
|
||||
|
||||
.bar-light {
|
||||
background: #F5F5F5;
|
||||
}
|
||||
@keyframes slide-in {
|
||||
from { opacity: 0; transform: translateX(-20px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
|
||||
.pos-gold {
|
||||
background: linear-gradient(180deg, #FFD700 0%, #B8860B 100%);
|
||||
}
|
||||
.animate-row {
|
||||
animation: slide-in 0.3s ease-out forwards;
|
||||
|
||||
.pos-silver {
|
||||
background: linear-gradient(180deg, #C0C0C0 0%, #808080 100%);
|
||||
}
|
||||
|
||||
.pos-bronze {
|
||||
background: linear-gradient(180deg, #CD7F32 0%, #8B4513 100%);
|
||||
}
|
||||
|
||||
.pos-default {
|
||||
background: linear-gradient(180deg, #E60000 0%, #990000 100%);
|
||||
}
|
||||
|
||||
.tabular {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.live-pulse {
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.6; transform: scale(0.95); }
|
||||
}
|
||||
|
||||
.row-shadow {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-shell-dark min-h-screen font-sans antialiased">
|
||||
<!-- Header -->
|
||||
<header class="bg-gradient-to-r from-shell-red to-red-700 py-6 px-8 shadow-2xl">
|
||||
<div class="max-w-7xl mx-auto flex items-center justify-between">
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="w-16 h-16 bg-shell-yellow rounded-lg flex items-center justify-center shadow-lg">
|
||||
<svg class="w-10 h-10 text-shell-dark" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2L1 12h3v9h6v-6h4v6h6v-9h3L12 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-4xl md:text-5xl lg:text-6xl font-black text-white tracking-tight">SHELL LEADERBOARD</h1>
|
||||
<p class="text-shell-yellow text-lg md:text-xl font-semibold tracking-widest uppercase mt-1">Race Results</p>
|
||||
<body class="min-h-screen antialiased text-white">
|
||||
|
||||
<!-- Header Bar -->
|
||||
<header class="bg-shell-red">
|
||||
<div class="w-full px-4 sm:px-6 lg:px-8 py-3 lg:py-4 flex items-center justify-between">
|
||||
<!-- Left: Logo + Title -->
|
||||
<div class="flex items-center gap-3 lg:gap-5">
|
||||
<div class="w-10 h-10 lg:w-12 lg:h-12 bg-shell-yellow rounded flex items-center justify-center flex-shrink-0">
|
||||
<span class="text-black font-bold text-lg lg:text-xl">S</span>
|
||||
</div>
|
||||
<h1 class="text-2xl sm:text-3xl lg:text-4xl font-bold text-white tracking-wide uppercase">
|
||||
Race Standings
|
||||
</h1>
|
||||
</div>
|
||||
<div class="text-right hidden md:block">
|
||||
<div class="text-white/80 text-sm uppercase tracking-wider">Live Standings</div>
|
||||
<div class="text-shell-yellow text-2xl font-bold tabular-nums" id="clock">{{ now()->format('H:i:s') }}</div>
|
||||
|
||||
<!-- Right: Live + Clock -->
|
||||
<div class="flex items-center gap-4 lg:gap-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="live-pulse w-3 h-3 lg:w-4 lg:h-4 bg-white rounded-full"></span>
|
||||
<span class="text-white text-base lg:text-xl font-semibold uppercase tracking-wider">Live</span>
|
||||
</div>
|
||||
<span class="text-white text-xl lg:text-2xl font-medium tabular" id="clock">{{ now()->format('H:i') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Leaderboard -->
|
||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<main class="w-full px-3 sm:px-4 lg:px-6 py-4 lg:py-6">
|
||||
|
||||
@if($participants->isEmpty())
|
||||
<div class="text-center py-20">
|
||||
<div class="text-shell-yellow text-6xl mb-4">🏁</div>
|
||||
<h2 class="text-3xl font-bold text-white mb-2">No Participants Yet</h2>
|
||||
<p class="text-gray-400 text-lg">Waiting for race data...</p>
|
||||
<div class="text-center py-20 lg:py-32">
|
||||
<div class="text-6xl lg:text-8xl mb-6">🏁</div>
|
||||
<h2 class="text-3xl lg:text-5xl font-bold text-white mb-4">WAITING FOR DRIVERS</h2>
|
||||
<p class="text-gray-500 text-xl lg:text-2xl">Race data will appear here</p>
|
||||
</div>
|
||||
@else
|
||||
<!-- Table Header -->
|
||||
<div class="bg-shell-dark/50 border-b-2 border-shell-yellow mb-2 rounded-t-xl overflow-hidden">
|
||||
<div class="grid grid-cols-12 gap-4 px-6 py-4 text-shell-yellow font-bold uppercase tracking-wider text-sm md:text-base">
|
||||
<div class="col-span-1 text-center">POS</div>
|
||||
<div class="col-span-1 text-center">#</div>
|
||||
<div class="col-span-6">DRIVER</div>
|
||||
<div class="col-span-2 text-center">TIME</div>
|
||||
<div class="col-span-2 text-center">GAP</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Participants -->
|
||||
<div class="space-y-2">
|
||||
@php $leaderTime = $participants->first()?->time_ms; @endphp
|
||||
<!-- Standings -->
|
||||
<div class="space-y-1.5 lg:space-y-2">
|
||||
@php
|
||||
$leaderTime = $participants->first()?->best_time_ms;
|
||||
@endphp
|
||||
|
||||
@foreach($participants as $index => $participant)
|
||||
@php
|
||||
$position = $index + 1;
|
||||
$isLeader = $position === 1;
|
||||
$isPodium = $position <= 3;
|
||||
$gap = null;
|
||||
if ($leaderTime && $participant->time_ms && !$isLeader) {
|
||||
$gapMs = $participant->time_ms - $leaderTime;
|
||||
$gapSec = $gapMs / 1000;
|
||||
$gap = sprintf('+%.3f', $gapSec);
|
||||
$timeDisplay = null;
|
||||
|
||||
if ($isLeader && $participant->best_time_ms) {
|
||||
$timeDisplay = $participant->formatted_time;
|
||||
} elseif ($leaderTime && $participant->best_time_ms) {
|
||||
$gapMs = $participant->best_time_ms - $leaderTime;
|
||||
$timeDisplay = '+' . number_format($gapMs / 1000, 2);
|
||||
}
|
||||
|
||||
$posClass = match($position) {
|
||||
1 => 'pos-gold',
|
||||
2 => 'pos-silver',
|
||||
3 => 'pos-bronze',
|
||||
default => 'pos-default'
|
||||
};
|
||||
|
||||
$statusBadge = match($participant->status) {
|
||||
'running' => ['bg-blue-500 text-white', 'RUNNING'],
|
||||
'pit' => ['bg-shell-yellow text-black', 'IN PIT'],
|
||||
'dnf' => ['bg-red-600 text-white', 'DNF'],
|
||||
'finished' => ['bg-green-600 text-white', 'FINISHED'],
|
||||
default => null
|
||||
};
|
||||
@endphp
|
||||
<div class="animate-row {{ $isLeader ? 'leader-glow' : '' }}" style="animation-delay: {{ $index * 0.05 }}s">
|
||||
<div class="grid grid-cols-12 gap-4 px-6 py-4 md:py-5 rounded-xl items-center transition-all duration-300 hover:scale-[1.01]
|
||||
@if($isLeader)
|
||||
bg-gradient-to-r from-shell-red to-red-800 text-white
|
||||
@elseif($position === 2)
|
||||
bg-gradient-to-r from-gray-600 to-gray-700 text-white
|
||||
@elseif($position === 3)
|
||||
bg-gradient-to-r from-amber-700 to-amber-800 text-white
|
||||
@else
|
||||
bg-gray-800/80 text-white hover:bg-gray-700/80
|
||||
@endif
|
||||
">
|
||||
<!-- Position -->
|
||||
<div class="col-span-1 text-center">
|
||||
<span class="inline-flex items-center justify-center w-10 h-10 md:w-12 md:h-12 rounded-full font-black text-xl md:text-2xl
|
||||
@if($isLeader) bg-shell-yellow text-shell-dark
|
||||
@elseif($position === 2) bg-gray-300 text-gray-800
|
||||
@elseif($position === 3) bg-amber-500 text-amber-900
|
||||
@else bg-gray-600 text-white
|
||||
@endif
|
||||
">
|
||||
{{ $position }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Car Number -->
|
||||
<div class="col-span-1 text-center">
|
||||
<span class="text-xl md:text-2xl font-bold {{ $isLeader ? 'text-shell-yellow' : 'text-gray-300' }}">
|
||||
{{ $participant->car_number ?? '-' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Name -->
|
||||
<div class="col-span-6">
|
||||
<span class="text-xl md:text-2xl lg:text-3xl font-bold tracking-wide uppercase">
|
||||
|
||||
<div class="row-shadow flex items-stretch h-14 sm:h-16 lg:h-20">
|
||||
|
||||
<!-- Position Cube -->
|
||||
<div class="{{ $posClass }} w-14 sm:w-16 lg:w-20 flex-shrink-0 flex items-center justify-center">
|
||||
<span class="tabular text-2xl sm:text-3xl lg:text-4xl text-white drop-shadow-md">
|
||||
{{ $position }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Main Bar (Light) -->
|
||||
<div class="bar-light flex-1 flex items-center px-4 lg:px-6">
|
||||
|
||||
<!-- Number + Name -->
|
||||
<div class="flex-1 min-w-0 flex items-center gap-2 lg:gap-4">
|
||||
@if($participant->number)
|
||||
<span class="tabular text-sm sm:text-base lg:text-xl text-gray-500">
|
||||
#{{ $participant->number }}
|
||||
</span>
|
||||
@endif
|
||||
<span class="text-lg sm:text-xl lg:text-3xl text-gray-900 uppercase tracking-wide truncate">
|
||||
{{ $participant->name }}
|
||||
</span>
|
||||
@if($participant->status === 'dnf')
|
||||
<span class="ml-3 px-2 py-1 bg-red-600 text-white text-xs font-bold rounded uppercase">DNF</span>
|
||||
@elseif($participant->status === 'pending')
|
||||
<span class="ml-3 px-2 py-1 bg-yellow-600 text-white text-xs font-bold rounded uppercase">In Progress</span>
|
||||
|
||||
@if($statusBadge)
|
||||
<span class="px-2 py-0.5 lg:px-2.5 lg:py-1 {{ $statusBadge[0] }} text-xs lg:text-sm rounded flex-shrink-0">
|
||||
{{ $statusBadge[1] }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Time -->
|
||||
<div class="col-span-2 text-center">
|
||||
<span class="text-xl md:text-2xl lg:text-3xl font-mono font-bold {{ $isLeader ? 'text-shell-yellow' : '' }}">
|
||||
@if($participant->formatted_time)
|
||||
{{ $participant->formatted_time }}
|
||||
@else
|
||||
<span class="text-gray-500">--:--.---</span>
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Gap -->
|
||||
<div class="col-span-2 text-center">
|
||||
<span class="text-lg md:text-xl font-mono {{ $isLeader ? 'text-shell-yellow font-bold' : 'text-gray-400' }}">
|
||||
@if($isLeader && $participant->time_ms)
|
||||
LEADER
|
||||
@elseif($gap)
|
||||
{{ $gap }}
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</span>
|
||||
<div class="flex-shrink-0 text-right">
|
||||
@if($timeDisplay)
|
||||
<span class="tabular text-lg sm:text-xl lg:text-3xl text-gray-900">
|
||||
{{ $timeDisplay }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-lg sm:text-xl lg:text-3xl text-gray-400">—</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="fixed bottom-0 left-0 right-0 bg-gradient-to-r from-shell-dark via-gray-900 to-shell-dark py-4 border-t border-gray-800">
|
||||
<div class="max-w-7xl mx-auto px-8 flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-8 h-8 bg-shell-yellow rounded flex items-center justify-center">
|
||||
<span class="text-shell-dark font-black text-sm">S</span>
|
||||
</div>
|
||||
<span class="text-gray-400 text-sm">Shell Racing Event</span>
|
||||
<footer class="fixed bottom-0 left-0 right-0 bg-broadcast-bg border-t border-gray-800">
|
||||
<div class="w-full px-4 sm:px-6 lg:px-8 py-2 lg:py-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 lg:gap-3">
|
||||
<span class="text-shell-red font-bold text-sm lg:text-lg uppercase">Shell</span>
|
||||
<span class="text-gray-600 text-xs lg:text-sm">Racing Event</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-block w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
|
||||
<span class="text-gray-400 text-sm">Auto-refresh every 10s</span>
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
|
||||
<span class="text-gray-600 text-xs lg:text-sm">Auto-refresh</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -195,7 +212,7 @@ function updateClock() {
|
||||
const clock = document.getElementById('clock');
|
||||
if (clock) {
|
||||
const now = new Date();
|
||||
clock.textContent = now.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
clock.textContent = now.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
}
|
||||
setInterval(updateClock, 1000);
|
||||
|
||||
Reference in New Issue
Block a user