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:
@@ -99,6 +99,7 @@ class EventController extends Controller
|
|||||||
$event->updated_by = $request->user()->id;
|
$event->updated_by = $request->user()->id;
|
||||||
$event->save();
|
$event->save();
|
||||||
|
|
||||||
|
$this->handleThumbnail($request, $event);
|
||||||
$this->createParticipantsForTeam($event);
|
$this->createParticipantsForTeam($event);
|
||||||
$this->syncAssignments($event, $request);
|
$this->syncAssignments($event, $request);
|
||||||
$this->saveKnownLocation($validated, $request->input('location_name'));
|
$this->saveKnownLocation($validated, $request->input('location_name'));
|
||||||
@@ -172,6 +173,7 @@ class EventController extends Controller
|
|||||||
$event->syncParticipants($request->user()->id);
|
$event->syncParticipants($request->user()->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->handleThumbnail($request, $event);
|
||||||
$this->syncAssignments($event, $request);
|
$this->syncAssignments($event, $request);
|
||||||
$this->saveKnownLocation($validated, $request->input('location_name'));
|
$this->saveKnownLocation($validated, $request->input('location_name'));
|
||||||
$this->syncEventFiles($event, $request);
|
$this->syncEventFiles($event, $request);
|
||||||
@@ -349,6 +351,7 @@ class EventController extends Controller
|
|||||||
'new_files.*' => ['file', 'max:10240', 'mimes:pdf,docx,xlsx,jpg,jpeg,png,gif,webp'],
|
'new_files.*' => ['file', 'max:10240', 'mimes:pdf,docx,xlsx,jpg,jpeg,png,gif,webp'],
|
||||||
'new_file_categories' => ['nullable', 'array'],
|
'new_file_categories' => ['nullable', 'array'],
|
||||||
'new_file_categories.*' => ['integer', 'exists:file_categories,id'],
|
'new_file_categories.*' => ['integer', 'exists:file_categories,id'],
|
||||||
|
'thumbnail' => ['nullable', 'image', 'max:10240', 'mimes:jpg,jpeg,png,gif,webp'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
@@ -666,4 +669,68 @@ class EventController extends Controller
|
|||||||
|
|
||||||
return count($dates);
|
return count($dates);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thumbnail hochladen, auf max. 1920x1080 skalieren und als JPEG speichern.
|
||||||
|
*/
|
||||||
|
private function handleThumbnail(Request $request, Event $event): void
|
||||||
|
{
|
||||||
|
// Thumbnail entfernen
|
||||||
|
if ($request->boolean('remove_thumbnail') && $event->thumbnail) {
|
||||||
|
Storage::disk('public')->delete('thumbnails/' . $event->thumbnail);
|
||||||
|
$event->thumbnail = null;
|
||||||
|
$event->save();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$request->hasFile('thumbnail')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Altes Thumbnail löschen
|
||||||
|
if ($event->thumbnail) {
|
||||||
|
Storage::disk('public')->delete('thumbnails/' . $event->thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
|
$uploadedFile = $request->file('thumbnail');
|
||||||
|
$storedName = Str::uuid() . '.jpg';
|
||||||
|
|
||||||
|
// Bild laden und auf max. FullHD skalieren
|
||||||
|
$imageData = file_get_contents($uploadedFile->getRealPath());
|
||||||
|
$source = @imagecreatefromstring($imageData);
|
||||||
|
|
||||||
|
if (!$source) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$origWidth = imagesx($source);
|
||||||
|
$origHeight = imagesy($source);
|
||||||
|
$maxWidth = 1920;
|
||||||
|
$maxHeight = 1080;
|
||||||
|
|
||||||
|
// Nur verkleinern, nicht vergrößern
|
||||||
|
if ($origWidth > $maxWidth || $origHeight > $maxHeight) {
|
||||||
|
$ratio = min($maxWidth / $origWidth, $maxHeight / $origHeight);
|
||||||
|
$newWidth = (int) round($origWidth * $ratio);
|
||||||
|
$newHeight = (int) round($origHeight * $ratio);
|
||||||
|
|
||||||
|
$resized = imagecreatetruecolor($newWidth, $newHeight);
|
||||||
|
imagecopyresampled($resized, $source, 0, 0, 0, 0, $newWidth, $newHeight, $origWidth, $origHeight);
|
||||||
|
imagedestroy($source);
|
||||||
|
$source = $resized;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verzeichnis sicherstellen
|
||||||
|
$dir = storage_path('app/public/thumbnails');
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
mkdir($dir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Als JPEG speichern (Qualität 85)
|
||||||
|
imagejpeg($source, $dir . '/' . $storedName, 85);
|
||||||
|
imagedestroy($source);
|
||||||
|
|
||||||
|
$event->thumbnail = $storedName;
|
||||||
|
$event->save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class Event extends Model
|
|||||||
'opponent',
|
'opponent',
|
||||||
'score_home',
|
'score_home',
|
||||||
'score_away',
|
'score_away',
|
||||||
|
'thumbnail',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
@@ -300,6 +301,27 @@ class Event extends Model
|
|||||||
return $query->where('team_id', $teamId);
|
return $query->where('team_id', $teamId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL zum Event-Bild: Custom-Thumbnail oder Standard-Logo nach Event-Typ.
|
||||||
|
*/
|
||||||
|
public function imageUrl(): string
|
||||||
|
{
|
||||||
|
if ($this->thumbnail) {
|
||||||
|
return asset('storage/thumbnails/' . $this->thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
|
$map = [
|
||||||
|
'home_game' => 'Logo_Heimspiel.png',
|
||||||
|
'away_game' => 'Logo_Auswärtsspiel.png',
|
||||||
|
'training' => 'Logo_Training.png',
|
||||||
|
'tournament' => 'Logo_Turnier.png',
|
||||||
|
'meeting' => 'Logo_Besprechung.png',
|
||||||
|
'other' => 'Logo_Sonstiges.png',
|
||||||
|
];
|
||||||
|
|
||||||
|
return asset('images/' . ($map[$this->type->value] ?? 'Logo_Sonstiges.png'));
|
||||||
|
}
|
||||||
|
|
||||||
public function isPartOfSeries(): bool
|
public function isPartOfSeries(): bool
|
||||||
{
|
{
|
||||||
return $this->event_series_id !== null;
|
return $this->event_series_id !== null;
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('events', function (Blueprint $table) {
|
||||||
|
$table->string('thumbnail', 255)->nullable()->after('event_series_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('events', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('thumbnail');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -221,6 +221,8 @@ return [
|
|||||||
'category_not_empty' => 'لا يمكن حذف الفئة لأنها تحتوي على ملفات.',
|
'category_not_empty' => 'لا يمكن حذف الفئة لأنها تحتوي على ملفات.',
|
||||||
'confirm_delete_category' => 'هل تريد حذف هذه الفئة حقاً؟',
|
'confirm_delete_category' => 'هل تريد حذف هذه الفئة حقاً؟',
|
||||||
'event_files' => 'الملفات',
|
'event_files' => 'الملفات',
|
||||||
|
'event_thumbnail' => 'صورة مصغرة',
|
||||||
|
'upload_thumbnail' => 'اختر صورة',
|
||||||
'attach_from_library' => 'إرفاق من المكتبة',
|
'attach_from_library' => 'إرفاق من المكتبة',
|
||||||
'upload_new_file' => 'رفع ملف جديد',
|
'upload_new_file' => 'رفع ملف جديد',
|
||||||
'attached_files' => 'الملفات المرفقة',
|
'attached_files' => 'الملفات المرفقة',
|
||||||
|
|||||||
@@ -252,6 +252,8 @@ return [
|
|||||||
'category_not_empty' => 'Kategorie kann nicht gelöscht werden, da sie noch Dateien enthält.',
|
'category_not_empty' => 'Kategorie kann nicht gelöscht werden, da sie noch Dateien enthält.',
|
||||||
'confirm_delete_category' => 'Kategorie wirklich löschen?',
|
'confirm_delete_category' => 'Kategorie wirklich löschen?',
|
||||||
'event_files' => 'Dateien',
|
'event_files' => 'Dateien',
|
||||||
|
'event_thumbnail' => 'Vorschaubild',
|
||||||
|
'upload_thumbnail' => 'Bild wählen',
|
||||||
'attach_from_library' => 'Aus Bibliothek anhängen',
|
'attach_from_library' => 'Aus Bibliothek anhängen',
|
||||||
'upload_new_file' => 'Neue Datei hochladen',
|
'upload_new_file' => 'Neue Datei hochladen',
|
||||||
'attached_files' => 'Angehängte Dateien',
|
'attached_files' => 'Angehängte Dateien',
|
||||||
|
|||||||
@@ -220,6 +220,8 @@ return [
|
|||||||
'category_not_empty' => 'Category cannot be deleted because it still contains files.',
|
'category_not_empty' => 'Category cannot be deleted because it still contains files.',
|
||||||
'confirm_delete_category' => 'Really delete this category?',
|
'confirm_delete_category' => 'Really delete this category?',
|
||||||
'event_files' => 'Files',
|
'event_files' => 'Files',
|
||||||
|
'event_thumbnail' => 'Thumbnail',
|
||||||
|
'upload_thumbnail' => 'Choose image',
|
||||||
'attach_from_library' => 'Attach from library',
|
'attach_from_library' => 'Attach from library',
|
||||||
'upload_new_file' => 'Upload new file',
|
'upload_new_file' => 'Upload new file',
|
||||||
'attached_files' => 'Attached Files',
|
'attached_files' => 'Attached Files',
|
||||||
|
|||||||
@@ -221,6 +221,8 @@ return [
|
|||||||
'category_not_empty' => 'Nie można usunąć kategorii, ponieważ zawiera pliki.',
|
'category_not_empty' => 'Nie można usunąć kategorii, ponieważ zawiera pliki.',
|
||||||
'confirm_delete_category' => 'Naprawdę usunąć tę kategorię?',
|
'confirm_delete_category' => 'Naprawdę usunąć tę kategorię?',
|
||||||
'event_files' => 'Pliki',
|
'event_files' => 'Pliki',
|
||||||
|
'event_thumbnail' => 'Miniatura',
|
||||||
|
'upload_thumbnail' => 'Wybierz obraz',
|
||||||
'attach_from_library' => 'Dołącz z biblioteki',
|
'attach_from_library' => 'Dołącz z biblioteki',
|
||||||
'upload_new_file' => 'Prześlij nowy plik',
|
'upload_new_file' => 'Prześlij nowy plik',
|
||||||
'attached_files' => 'Dołączone pliki',
|
'attached_files' => 'Dołączone pliki',
|
||||||
|
|||||||
@@ -239,6 +239,8 @@ return [
|
|||||||
'category_not_empty' => 'Категория не может быть удалена, так как содержит файлы.',
|
'category_not_empty' => 'Категория не может быть удалена, так как содержит файлы.',
|
||||||
'confirm_delete_category' => 'Действительно удалить эту категорию?',
|
'confirm_delete_category' => 'Действительно удалить эту категорию?',
|
||||||
'event_files' => 'Файлы',
|
'event_files' => 'Файлы',
|
||||||
|
'event_thumbnail' => 'Миниатюра',
|
||||||
|
'upload_thumbnail' => 'Выбрать фото',
|
||||||
'attach_from_library' => 'Прикрепить из библиотеки',
|
'attach_from_library' => 'Прикрепить из библиотеки',
|
||||||
'upload_new_file' => 'Загрузить новый файл',
|
'upload_new_file' => 'Загрузить новый файл',
|
||||||
'attached_files' => 'Прикреплённые файлы',
|
'attached_files' => 'Прикреплённые файлы',
|
||||||
|
|||||||
@@ -239,6 +239,8 @@ return [
|
|||||||
'category_not_empty' => 'Kategori dosya içerdiği için silinemez.',
|
'category_not_empty' => 'Kategori dosya içerdiği için silinemez.',
|
||||||
'confirm_delete_category' => 'Bu kategoriyi gerçekten silmek istiyor musunuz?',
|
'confirm_delete_category' => 'Bu kategoriyi gerçekten silmek istiyor musunuz?',
|
||||||
'event_files' => 'Dosyalar',
|
'event_files' => 'Dosyalar',
|
||||||
|
'event_thumbnail' => 'Önizleme resmi',
|
||||||
|
'upload_thumbnail' => 'Resim seç',
|
||||||
'attach_from_library' => 'Kütüphaneden ekle',
|
'attach_from_library' => 'Kütüphaneden ekle',
|
||||||
'upload_new_file' => 'Yeni dosya yükle',
|
'upload_new_file' => 'Yeni dosya yükle',
|
||||||
'attached_files' => 'Ekli Dosyalar',
|
'attached_files' => 'Ekli Dosyalar',
|
||||||
|
|||||||
@@ -12,28 +12,53 @@
|
|||||||
|
|
||||||
<div class="bg-white rounded-lg shadow p-6 max-w-2xl">
|
<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"
|
<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
|
@csrf
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
<div class="flex gap-4 mb-4">
|
||||||
<div>
|
<div class="flex-1 grid grid-cols-2 gap-4">
|
||||||
<label for="team_id" class="block text-sm font-medium text-gray-700 mb-1">{{ __('ui.team') }} *</label>
|
<div>
|
||||||
<select name="team_id" id="team_id" required class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
<label for="team_id" class="block text-sm font-medium text-gray-700 mb-1">{{ __('ui.team') }} *</label>
|
||||||
<option value="">{{ __('admin.please_select') }}</option>
|
<select name="team_id" id="team_id" required class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||||
@foreach ($teams as $team)
|
<option value="">{{ __('admin.please_select') }}</option>
|
||||||
<option value="{{ $team->id }}" {{ old('team_id') == $team->id ? 'selected' : '' }}>{{ $team->name }}</option>
|
@foreach ($teams as $team)
|
||||||
@endforeach
|
<option value="{{ $team->id }}" {{ old('team_id') == $team->id ? 'selected' : '' }}>{{ $team->name }}</option>
|
||||||
</select>
|
@endforeach
|
||||||
@error('team_id')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
</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>
|
||||||
<div>
|
{{-- Thumbnail --}}
|
||||||
<label for="type" class="block text-sm font-medium text-gray-700 mb-1">{{ __('ui.type') }} *</label>
|
<div class="shrink-0">
|
||||||
<select name="type" id="type" required class="w-full px-3 py-2 border border-gray-300 rounded-md" x-model="currentType">
|
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.event_thumbnail') }}</label>
|
||||||
@foreach ($types as $t)
|
<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"
|
||||||
<option value="{{ $t->value }}" {{ old('type') === $t->value ? 'selected' : '' }}>{{ $t->label() }}</option>
|
@click="$refs.thumbnailInput.click()">
|
||||||
@endforeach
|
<template x-if="thumbnailPreview">
|
||||||
</select>
|
<img :src="thumbnailPreview" class="w-full h-full object-cover">
|
||||||
@error('type')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<div class="bg-white rounded-lg shadow p-6 max-w-2xl">
|
<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"
|
<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
|
@csrf
|
||||||
@method('PUT')
|
@method('PUT')
|
||||||
|
|
||||||
@@ -22,25 +22,51 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
<div class="flex gap-4 mb-4">
|
||||||
<div>
|
<div class="flex-1 grid grid-cols-2 gap-4">
|
||||||
<label for="team_id" class="block text-sm font-medium text-gray-700 mb-1">{{ __('ui.team') }} *</label>
|
<div>
|
||||||
<select name="team_id" id="team_id" required class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
<label for="team_id" class="block text-sm font-medium text-gray-700 mb-1">{{ __('ui.team') }} *</label>
|
||||||
<option value="">{{ __('admin.please_select') }}</option>
|
<select name="team_id" id="team_id" required class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||||
@foreach ($teams as $team)
|
<option value="">{{ __('admin.please_select') }}</option>
|
||||||
<option value="{{ $team->id }}" {{ old('team_id', $event->team_id) == $team->id ? 'selected' : '' }}>{{ $team->name }}</option>
|
@foreach ($teams as $team)
|
||||||
@endforeach
|
<option value="{{ $team->id }}" {{ old('team_id', $event->team_id) == $team->id ? 'selected' : '' }}>{{ $team->name }}</option>
|
||||||
</select>
|
@endforeach
|
||||||
@error('team_id')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
</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>
|
||||||
<div>
|
{{-- Thumbnail --}}
|
||||||
<label for="type" class="block text-sm font-medium text-gray-700 mb-1">{{ __('ui.type') }} *</label>
|
<div class="shrink-0">
|
||||||
<select name="type" id="type" required class="w-full px-3 py-2 border border-gray-300 rounded-md" x-model="currentType">
|
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.event_thumbnail') }}</label>
|
||||||
@foreach ($types as $t)
|
<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"
|
||||||
<option value="{{ $t->value }}" {{ old('type', $event->type->value) === $t->value ? 'selected' : '' }}>{{ $t->label() }}</option>
|
@click="$refs.thumbnailInput.click()">
|
||||||
@endforeach
|
<template x-if="thumbnailPreview">
|
||||||
</select>
|
<img :src="thumbnailPreview" class="w-full h-full object-cover">
|
||||||
@error('type')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,8 @@
|
|||||||
@endphp
|
@endphp
|
||||||
<div class="{{ $bgClass }} border rounded-lg shadow-sm p-4 mb-3">
|
<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">
|
<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 --}}
|
{{-- Links: Typ, Team, Titel, Datum, Ort, Status --}}
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center gap-2 mb-1 flex-wrap">
|
<div class="flex items-center gap-2 mb-1 flex-wrap">
|
||||||
|
|||||||
@@ -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">
|
<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">
|
<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-1 min-w-0">
|
||||||
<div class="flex items-center gap-2 mb-1 flex-wrap">
|
<div class="flex items-center gap-2 mb-1 flex-wrap">
|
||||||
<x-event-type-badge :type="$event->type" />
|
<x-event-type-badge :type="$event->type" />
|
||||||
|
|||||||
Reference in New Issue
Block a user