No Participants Yet
+Waiting for race data...
+diff --git a/docker-compose.yml b/docker-compose.yml index cd94aee..487a74c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,7 +43,7 @@ services: image: mysql:8.0 restart: unless-stopped ports: - - "${DB_PORT:-3306}:3306" + - "${DB_PORT:-3307}:3306" environment: MYSQL_DATABASE: ${DB_DATABASE:-laravel} MYSQL_USER: ${DB_USERNAME:-laravel} diff --git a/src/app/Filament/Resources/ParticipantResource.php b/src/app/Filament/Resources/ParticipantResource.php new file mode 100644 index 0000000..30631f1 --- /dev/null +++ b/src/app/Filament/Resources/ParticipantResource.php @@ -0,0 +1,193 @@ +schema([ + Forms\Components\Section::make('Participant Information') + ->schema([ + Forms\Components\TextInput::make('name') + ->required() + ->maxLength(255) + ->placeholder('Enter participant name'), + Forms\Components\TextInput::make('car_number') + ->label('Car Number') + ->numeric() + ->minValue(1) + ->maxValue(999) + ->placeholder('e.g., 44'), + ])->columns(2), + + Forms\Components\Section::make('Timing') + ->schema([ + Forms\Components\Grid::make(4) + ->schema([ + Forms\Components\TextInput::make('time_minutes') + ->label('Minutes') + ->numeric() + ->minValue(0) + ->maxValue(59) + ->default(0) + ->placeholder('00') + ->dehydrated(false) + ->afterStateHydrated(function ($component, $state, $record) { + if ($record && $record->time_ms) { + $component->state(floor($record->time_ms / 60000)); + } + }), + Forms\Components\TextInput::make('time_seconds') + ->label('Seconds') + ->numeric() + ->minValue(0) + ->maxValue(59) + ->default(0) + ->placeholder('00') + ->dehydrated(false) + ->afterStateHydrated(function ($component, $state, $record) { + if ($record && $record->time_ms) { + $component->state(floor(($record->time_ms % 60000) / 1000)); + } + }), + Forms\Components\TextInput::make('time_milliseconds') + ->label('Milliseconds') + ->numeric() + ->minValue(0) + ->maxValue(999) + ->default(0) + ->placeholder('000') + ->dehydrated(false) + ->afterStateHydrated(function ($component, $state, $record) { + if ($record && $record->time_ms) { + $component->state($record->time_ms % 1000); + } + }), + Forms\Components\Select::make('status') + ->options([ + 'pending' => 'Pending', + 'completed' => 'Completed', + 'dnf' => 'DNF', + ]) + ->default('pending') + ->required(), + ]), + Forms\Components\Hidden::make('time_ms'), + ]) + ->description('Enter time as MM:SS.ms (e.g., 01:23.456)'), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('position') + ->label('P') + ->state(function ($record, $rowLoop) { + return $rowLoop->iteration; + }) + ->alignCenter() + ->weight('bold'), + Tables\Columns\TextColumn::make('car_number') + ->label('#') + ->alignCenter() + ->placeholder('-') + ->sortable(), + Tables\Columns\TextColumn::make('name') + ->searchable() + ->sortable() + ->weight('bold'), + Tables\Columns\TextColumn::make('formatted_time') + ->label('Time') + ->placeholder('No time') + ->fontFamily('mono') + ->alignCenter(), + Tables\Columns\BadgeColumn::make('status') + ->colors([ + 'warning' => 'pending', + 'success' => 'completed', + 'danger' => 'dnf', + ]), + Tables\Columns\TextColumn::make('updated_at') + ->label('Last Update') + ->dateTime('M j, H:i') + ->sortable(), + ]) + ->defaultSort('time_ms', 'asc') + ->filters([ + Tables\Filters\SelectFilter::make('status') + ->options([ + 'pending' => 'Pending', + 'completed' => 'Completed', + 'dnf' => 'DNF', + ]), + ]) + ->actions([ + Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return []; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListParticipants::route('/'), + 'create' => Pages\CreateParticipant::route('/create'), + 'edit' => Pages\EditParticipant::route('/{record}/edit'), + ]; + } + + public static function mutateFormDataBeforeCreate(array $data): array + { + return static::calculateTimeMs($data); + } + + public static function mutateFormDataBeforeSave(array $data): array + { + return static::calculateTimeMs($data); + } + + protected static function calculateTimeMs(array $data): array + { + $minutes = (int) ($data['time_minutes'] ?? 0); + $seconds = (int) ($data['time_seconds'] ?? 0); + $milliseconds = (int) ($data['time_milliseconds'] ?? 0); + + if ($minutes > 0 || $seconds > 0 || $milliseconds > 0) { + $data['time_ms'] = ($minutes * 60000) + ($seconds * 1000) + $milliseconds; + } else { + $data['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 new file mode 100644 index 0000000..a8aef90 --- /dev/null +++ b/src/app/Filament/Resources/ParticipantResource/Pages/CreateParticipant.php @@ -0,0 +1,33 @@ + 0 || $seconds > 0 || $milliseconds > 0) { + $data['time_ms'] = ($minutes * 60000) + ($seconds * 1000) + $milliseconds; + } else { + $data['time_ms'] = null; + } + + unset($data['time_minutes'], $data['time_seconds'], $data['time_milliseconds']); + + return $data; + } + + protected function getRedirectUrl(): string + { + return $this->getResource()::getUrl('index'); + } +} diff --git a/src/app/Filament/Resources/ParticipantResource/Pages/EditParticipant.php b/src/app/Filament/Resources/ParticipantResource/Pages/EditParticipant.php new file mode 100644 index 0000000..4b4dbaa --- /dev/null +++ b/src/app/Filament/Resources/ParticipantResource/Pages/EditParticipant.php @@ -0,0 +1,41 @@ + 0 || $seconds > 0 || $milliseconds > 0) { + $data['time_ms'] = ($minutes * 60000) + ($seconds * 1000) + $milliseconds; + } else { + $data['time_ms'] = null; + } + + unset($data['time_minutes'], $data['time_seconds'], $data['time_milliseconds']); + + return $data; + } + + protected function getRedirectUrl(): string + { + return $this->getResource()::getUrl('index'); + } +} diff --git a/src/app/Filament/Resources/ParticipantResource/Pages/ListParticipants.php b/src/app/Filament/Resources/ParticipantResource/Pages/ListParticipants.php new file mode 100644 index 0000000..cf4d4fe --- /dev/null +++ b/src/app/Filament/Resources/ParticipantResource/Pages/ListParticipants.php @@ -0,0 +1,19 @@ + 'integer', + 'time_ms' => 'integer', + ]; + + /** + * Format time_ms as mm:ss.ms (e.g., 01:23.456) + */ + public function getFormattedTimeAttribute(): ?string + { + if ($this->time_ms === null) { + return null; + } + + $totalMs = $this->time_ms; + $minutes = floor($totalMs / 60000); + $seconds = floor(($totalMs % 60000) / 1000); + $milliseconds = $totalMs % 1000; + + return sprintf('%02d:%02d.%03d', $minutes, $seconds, $milliseconds); + } + + /** + * Scope to get ranked participants (fastest first, null times last) + */ + public function scopeRanked($query) + { + return $query->orderByRaw('CASE WHEN time_ms IS NULL THEN 1 ELSE 0 END') + ->orderBy('time_ms', 'asc'); + } + + /** + * Scope to get only completed participants + */ + public function scopeCompleted($query) + { + return $query->where('status', 'completed'); + } +} 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 new file mode 100644 index 0000000..512294b --- /dev/null +++ b/src/database/migrations/2026_03_25_000001_create_participants_table.php @@ -0,0 +1,25 @@ +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->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('participants'); + } +}; diff --git a/src/resources/views/leaderboard.blade.php b/src/resources/views/leaderboard.blade.php new file mode 100644 index 0000000..c33dcca --- /dev/null +++ b/src/resources/views/leaderboard.blade.php @@ -0,0 +1,204 @@ + + +
+ + + +Race Results
+Waiting for race data...
+