From adc85b2b88f19fd5156f6436d5a98af5a3bbd6ca Mon Sep 17 00:00:00 2001 From: ut-masekela Date: Wed, 25 Mar 2026 03:32:33 +0200 Subject: [PATCH] Rename car_number to number, time_ms to best_time_ms, update participant statuses, and redesign leaderboard with broadcast-style UI --- .../Resources/ParticipantResource.php | 95 +++--- .../Pages/CreateParticipant.php | 12 +- .../Pages/EditParticipant.php | 12 +- src/app/Models/Participant.php | 48 ++-- ...03_25_000001_create_participants_table.php | 6 +- src/resources/views/leaderboard.blade.php | 271 ++++++++++-------- 6 files changed, 232 insertions(+), 212 deletions(-) diff --git a/src/app/Filament/Resources/ParticipantResource.php b/src/app/Filament/Resources/ParticipantResource.php index 30631f1..3f613d9 100644 --- a/src/app/Filament/Resources/ParticipantResource.php +++ b/src/app/Filament/Resources/ParticipantResource.php @@ -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; diff --git a/src/app/Filament/Resources/ParticipantResource/Pages/CreateParticipant.php b/src/app/Filament/Resources/ParticipantResource/Pages/CreateParticipant.php index a8aef90..dfd308a 100644 --- a/src/app/Filament/Resources/ParticipantResource/Pages/CreateParticipant.php +++ b/src/app/Filament/Resources/ParticipantResource/Pages/CreateParticipant.php @@ -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']); diff --git a/src/app/Filament/Resources/ParticipantResource/Pages/EditParticipant.php b/src/app/Filament/Resources/ParticipantResource/Pages/EditParticipant.php index 4b4dbaa..b37d689 100644 --- a/src/app/Filament/Resources/ParticipantResource/Pages/EditParticipant.php +++ b/src/app/Filament/Resources/ParticipantResource/Pages/EditParticipant.php @@ -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']); diff --git a/src/app/Models/Participant.php b/src/app/Models/Participant.php index 2cab5d6..3004703 100644 --- a/src/app/Models/Participant.php +++ b/src/app/Models/Participant.php @@ -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'); } } diff --git a/src/database/migrations/2026_03_25_000001_create_participants_table.php b/src/database/migrations/2026_03_25_000001_create_participants_table.php index 512294b..4d99be3 100644 --- a/src/database/migrations/2026_03_25_000001_create_participants_table.php +++ b/src/database/migrations/2026_03_25_000001_create_participants_table.php @@ -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(); }); } diff --git a/src/resources/views/leaderboard.blade.php b/src/resources/views/leaderboard.blade.php index c33dcca..7bf2d26 100644 --- a/src/resources/views/leaderboard.blade.php +++ b/src/resources/views/leaderboard.blade.php @@ -4,9 +4,9 @@ - Shell Leaderboard + Shell Race Standings - + - - -
-
-
-
- - - -
-
-

SHELL LEADERBOARD

-

Race Results

+ + + +
+
+ +
+
+ S
+

+ Race Standings +

-
-
+
+ @if($participants->isEmpty()) -
-
🏁
-

No Participants Yet

-

Waiting for race data...

+
+
🏁
+

WAITING FOR DRIVERS

+

Race data will appear here

@else - -
-
-
POS
-
#
-
DRIVER
-
TIME
-
GAP
-
-
- - -
- @php $leaderTime = $participants->first()?->time_ms; @endphp + +
+ @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 -
-
- -
- - {{ $position }} - -
- - -
- - {{ $participant->car_number ?? '-' }} - -
- - -
- + +
+ + +
+ + {{ $position }} + +
+ + +
+ + +
+ @if($participant->number) + + #{{ $participant->number }} + + @endif + {{ $participant->name }} - @if($participant->status === 'dnf') - DNF - @elseif($participant->status === 'pending') - In Progress + + @if($statusBadge) + + {{ $statusBadge[1] }} + @endif
- + -
- - @if($participant->formatted_time) - {{ $participant->formatted_time }} - @else - --:--.--- - @endif - -
- - -
- - @if($isLeader && $participant->time_ms) - LEADER - @elseif($gap) - {{ $gap }} - @else - - - @endif - +
+ @if($timeDisplay) + + {{ $timeDisplay }} + + @else + + @endif
+
@endforeach
@endif +
-