Event-Thumbnails: Vorschaubilder mit Auto-Resize und Typ-Logos

- Migration: thumbnail-Spalte in events-Tabelle
- Event-Model: imageUrl() liefert Custom-Thumbnail oder Standard-Logo
  je Event-Typ (Logo_Training.png, Logo_Heimspiel.png, etc.)
- Thumbnail-Upload neben Typ-Auswahl bei Erstellen/Bearbeiten
  mit Live-Vorschau und Entfernen-Button
- Automatische Skalierung auf max. FullHD (1920x1080) via GD
  und Speicherung als JPEG (Qualität 85)
- Event-Listen (App + Admin): Logo/Thumbnail links im Terminblock
- Übersetzungen in allen 6 Sprachen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Rhino
2026-03-03 10:45:35 +01:00
parent 5942bdf6c3
commit c0287367c0
13 changed files with 215 additions and 38 deletions

View File

@@ -12,28 +12,53 @@
<div class="bg-white rounded-lg shadow p-6 max-w-2xl">
<form method="POST" action="{{ route('admin.events.store') }}" id="eventForm" enctype="multipart/form-data"
x-data="{ currentType: @js(old('type', '')) }" x-init="$watch('currentType', () => {}); document.getElementById('type').addEventListener('change', (e) => currentType = e.target.value); currentType = document.getElementById('type').value;">
x-data="{ currentType: @js(old('type', '')), thumbnailPreview: null }" x-init="$watch('currentType', () => {}); document.getElementById('type').addEventListener('change', (e) => currentType = e.target.value); currentType = document.getElementById('type').value;">
@csrf
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label for="team_id" class="block text-sm font-medium text-gray-700 mb-1">{{ __('ui.team') }} *</label>
<select name="team_id" id="team_id" required class="w-full px-3 py-2 border border-gray-300 rounded-md">
<option value="">{{ __('admin.please_select') }}</option>
@foreach ($teams as $team)
<option value="{{ $team->id }}" {{ old('team_id') == $team->id ? 'selected' : '' }}>{{ $team->name }}</option>
@endforeach
</select>
@error('team_id')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
<div class="flex gap-4 mb-4">
<div class="flex-1 grid grid-cols-2 gap-4">
<div>
<label for="team_id" class="block text-sm font-medium text-gray-700 mb-1">{{ __('ui.team') }} *</label>
<select name="team_id" id="team_id" required class="w-full px-3 py-2 border border-gray-300 rounded-md">
<option value="">{{ __('admin.please_select') }}</option>
@foreach ($teams as $team)
<option value="{{ $team->id }}" {{ old('team_id') == $team->id ? 'selected' : '' }}>{{ $team->name }}</option>
@endforeach
</select>
@error('team_id')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
</div>
<div>
<label for="type" class="block text-sm font-medium text-gray-700 mb-1">{{ __('ui.type') }} *</label>
<select name="type" id="type" required class="w-full px-3 py-2 border border-gray-300 rounded-md" x-model="currentType">
@foreach ($types as $t)
<option value="{{ $t->value }}" {{ old('type') === $t->value ? 'selected' : '' }}>{{ $t->label() }}</option>
@endforeach
</select>
@error('type')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
</div>
</div>
<div>
<label for="type" class="block text-sm font-medium text-gray-700 mb-1">{{ __('ui.type') }} *</label>
<select name="type" id="type" required class="w-full px-3 py-2 border border-gray-300 rounded-md" x-model="currentType">
@foreach ($types as $t)
<option value="{{ $t->value }}" {{ old('type') === $t->value ? 'selected' : '' }}>{{ $t->label() }}</option>
@endforeach
</select>
@error('type')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
{{-- Thumbnail --}}
<div class="shrink-0">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.event_thumbnail') }}</label>
<div class="relative w-24 h-24 border-2 border-dashed border-gray-300 rounded-lg overflow-hidden cursor-pointer hover:border-blue-400 transition-colors"
@click="$refs.thumbnailInput.click()">
<template x-if="thumbnailPreview">
<img :src="thumbnailPreview" class="w-full h-full object-cover">
</template>
<template x-if="!thumbnailPreview">
<div class="flex flex-col items-center justify-center h-full text-gray-400">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
<span class="text-[10px] mt-1">{{ __('admin.upload_thumbnail') }}</span>
</div>
</template>
<template x-if="thumbnailPreview">
<button type="button" @click.stop="thumbnailPreview = null; $refs.thumbnailInput.value = ''"
class="absolute top-0.5 right-0.5 bg-red-500 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs hover:bg-red-600">×</button>
</template>
</div>
<input type="file" name="thumbnail" x-ref="thumbnailInput" class="hidden" accept="image/jpeg,image/png,image/gif,image/webp"
@change="if ($refs.thumbnailInput.files[0]) { const r = new FileReader(); r.onload = (e) => thumbnailPreview = e.target.result; r.readAsDataURL($refs.thumbnailInput.files[0]); }">
@error('thumbnail')<p class="mt-1 text-sm text-red-600 text-xs">{{ $message }}</p>@enderror
</div>
</div>

View File

@@ -12,7 +12,7 @@
<div class="bg-white rounded-lg shadow p-6 max-w-2xl">
<form method="POST" action="{{ route('admin.events.update', $event) }}" id="eventForm" enctype="multipart/form-data"
x-data="{ currentType: @js(old('type', $event->type->value)) }" x-init="document.getElementById('type').addEventListener('change', (e) => currentType = e.target.value);">
x-data="{ currentType: @js(old('type', $event->type->value)), thumbnailPreview: @js($event->thumbnail ? $event->imageUrl() : null) }" x-init="document.getElementById('type').addEventListener('change', (e) => currentType = e.target.value);">
@csrf
@method('PUT')
@@ -22,25 +22,51 @@
</div>
@endif
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label for="team_id" class="block text-sm font-medium text-gray-700 mb-1">{{ __('ui.team') }} *</label>
<select name="team_id" id="team_id" required class="w-full px-3 py-2 border border-gray-300 rounded-md">
<option value="">{{ __('admin.please_select') }}</option>
@foreach ($teams as $team)
<option value="{{ $team->id }}" {{ old('team_id', $event->team_id) == $team->id ? 'selected' : '' }}>{{ $team->name }}</option>
@endforeach
</select>
@error('team_id')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
<div class="flex gap-4 mb-4">
<div class="flex-1 grid grid-cols-2 gap-4">
<div>
<label for="team_id" class="block text-sm font-medium text-gray-700 mb-1">{{ __('ui.team') }} *</label>
<select name="team_id" id="team_id" required class="w-full px-3 py-2 border border-gray-300 rounded-md">
<option value="">{{ __('admin.please_select') }}</option>
@foreach ($teams as $team)
<option value="{{ $team->id }}" {{ old('team_id', $event->team_id) == $team->id ? 'selected' : '' }}>{{ $team->name }}</option>
@endforeach
</select>
@error('team_id')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
</div>
<div>
<label for="type" class="block text-sm font-medium text-gray-700 mb-1">{{ __('ui.type') }} *</label>
<select name="type" id="type" required class="w-full px-3 py-2 border border-gray-300 rounded-md" x-model="currentType">
@foreach ($types as $t)
<option value="{{ $t->value }}" {{ old('type', $event->type->value) === $t->value ? 'selected' : '' }}>{{ $t->label() }}</option>
@endforeach
</select>
@error('type')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
</div>
</div>
<div>
<label for="type" class="block text-sm font-medium text-gray-700 mb-1">{{ __('ui.type') }} *</label>
<select name="type" id="type" required class="w-full px-3 py-2 border border-gray-300 rounded-md" x-model="currentType">
@foreach ($types as $t)
<option value="{{ $t->value }}" {{ old('type', $event->type->value) === $t->value ? 'selected' : '' }}>{{ $t->label() }}</option>
@endforeach
</select>
@error('type')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
{{-- Thumbnail --}}
<div class="shrink-0">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.event_thumbnail') }}</label>
<div class="relative w-24 h-24 border-2 border-dashed border-gray-300 rounded-lg overflow-hidden cursor-pointer hover:border-blue-400 transition-colors"
@click="$refs.thumbnailInput.click()">
<template x-if="thumbnailPreview">
<img :src="thumbnailPreview" class="w-full h-full object-cover">
</template>
<template x-if="!thumbnailPreview">
<div class="flex flex-col items-center justify-center h-full text-gray-400">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
<span class="text-[10px] mt-1">{{ __('admin.upload_thumbnail') }}</span>
</div>
</template>
<template x-if="thumbnailPreview">
<button type="button" @click.stop="thumbnailPreview = null; $refs.thumbnailInput.value = ''; $refs.removeThumbnail.value = '1'"
class="absolute top-0.5 right-0.5 bg-red-500 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs hover:bg-red-600">×</button>
</template>
</div>
<input type="file" name="thumbnail" x-ref="thumbnailInput" class="hidden" accept="image/jpeg,image/png,image/gif,image/webp"
@change="if ($refs.thumbnailInput.files[0]) { $refs.removeThumbnail.value = ''; const r = new FileReader(); r.onload = (e) => thumbnailPreview = e.target.result; r.readAsDataURL($refs.thumbnailInput.files[0]); }">
<input type="hidden" name="remove_thumbnail" x-ref="removeThumbnail" value="">
@error('thumbnail')<p class="mt-1 text-sm text-red-600 text-xs">{{ $message }}</p>@enderror
</div>
</div>

View File

@@ -38,6 +38,8 @@
@endphp
<div class="{{ $bgClass }} border rounded-lg shadow-sm p-4 mb-3">
<div class="flex flex-col lg:flex-row lg:items-center gap-4">
{{-- Bild --}}
<img src="{{ $event->imageUrl() }}" alt="" class="hidden lg:block w-14 h-14 rounded-lg object-cover shrink-0">
{{-- Links: Typ, Team, Titel, Datum, Ort, Status --}}
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1 flex-wrap">

View File

@@ -61,6 +61,7 @@
<a href="{{ route('events.show', $event) }}" class="block {{ $bgClass }} rounded-lg shadow p-4 mb-3 hover:shadow-md transition-shadow">
<div class="flex flex-col sm:flex-row sm:items-center gap-3">
<img src="{{ $event->imageUrl() }}" alt="" class="hidden sm:block w-14 h-14 rounded-lg object-cover shrink-0">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1 flex-wrap">
<x-event-type-badge :type="$event->type" />