mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-app.git
synced 2025-12-24 02:40:16 +00:00
🚀 Add courses and lecturers management functionality
This commit is contained in:
126
resources/views/livewire/courses/create.blade.php
Normal file
126
resources/views/livewire/courses/create.blade.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Course;
|
||||
use App\Models\Lecturer;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Volt\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
new class extends Component {
|
||||
use WithFileUploads;
|
||||
|
||||
#[Validate('image|max:10240')] // 10MB Max
|
||||
public $logo;
|
||||
|
||||
public string $name = '';
|
||||
public ?int $lecturer_id = null;
|
||||
public ?string $description = null;
|
||||
|
||||
public function createCourse(): void
|
||||
{
|
||||
$validated = $this->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'lecturer_id' => ['required', 'exists:lecturers,id'],
|
||||
'description' => ['nullable', 'string'],
|
||||
]);
|
||||
|
||||
$course = Course::create($validated);
|
||||
|
||||
if ($this->logo) {
|
||||
$course
|
||||
->addMedia($this->logo->getRealPath())
|
||||
->usingName($course->name)
|
||||
->toMediaCollection('logo');
|
||||
}
|
||||
|
||||
session()->flash('status', __('Kurs erfolgreich erstellt!'));
|
||||
|
||||
$this->redirect(route_with_country('courses.edit', ['course' => $course]), navigate: true);
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
return [
|
||||
'lecturers' => Lecturer::query()->orderBy('name')->get(),
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="max-w-4xl mx-auto p-6">
|
||||
<flux:heading size="xl" class="mb-8">{{ __('Neuen Kurs erstellen') }}</flux:heading>
|
||||
|
||||
<form wire:submit="createCourse" class="space-y-10">
|
||||
|
||||
<!-- Basic Information -->
|
||||
<flux:fieldset class="space-y-6">
|
||||
<flux:legend>{{ __('Grundlegende Informationen') }}</flux:legend>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
|
||||
<flux:file-upload wire:model="logo">
|
||||
<!-- Custom logo uploader -->
|
||||
<div class="
|
||||
relative flex items-center justify-center size-20 rounded transition-colors cursor-pointer
|
||||
border border-zinc-200 dark:border-white/10 hover:border-zinc-300 dark:hover:border-white/10
|
||||
bg-zinc-100 hover:bg-zinc-200 dark:bg-white/10 hover:dark:bg-white/15 in-data-dragging:dark:bg-white/15
|
||||
">
|
||||
<!-- Show the uploaded file if it exists -->
|
||||
@if($logo)
|
||||
<img src="{{ $logo?->temporaryUrl() }}" alt="Logo"
|
||||
class="size-full object-cover rounded"/>
|
||||
@else
|
||||
<!-- Show the default icon if no file is uploaded -->
|
||||
<flux:icon name="academic-cap" variant="solid" class="text-zinc-500 dark:text-zinc-400"/>
|
||||
@endif
|
||||
|
||||
<!-- Corner upload icon -->
|
||||
<div class="absolute bottom-0 right-0 bg-white dark:bg-zinc-800 rounded">
|
||||
<flux:icon name="arrow-up-circle" variant="solid" class="text-zinc-500 dark:text-zinc-400"/>
|
||||
</div>
|
||||
</div>
|
||||
</flux:file-upload>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Name') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="name" required/>
|
||||
<flux:description>{{ __('Der Anzeigename für diesen Kurs') }}</flux:description>
|
||||
<flux:error name="name"/>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Dozent') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:select variant="listbox" searchable wire:model="lecturer_id"
|
||||
placeholder="{{ __('Dozent auswählen') }}">
|
||||
<x-slot name="search">
|
||||
<flux:select.search class="px-4" placeholder="{{ __('Suche passenden Dozenten...') }}"/>
|
||||
</x-slot>
|
||||
@foreach($lecturers as $lecturer)
|
||||
<flux:select.option value="{{ $lecturer->id }}">{{ $lecturer->name }}
|
||||
</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:description>{{ __('Der Dozent, der diesen Kurs leitet') }}</flux:description>
|
||||
<flux:error name="lecturer_id"/>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Beschreibung') }}</flux:label>
|
||||
<flux:textarea wire:model="description" rows="6"/>
|
||||
<flux:description>{{ __('Ausführliche Beschreibung des Kurses') }}</flux:description>
|
||||
<flux:error name="description"/>
|
||||
</flux:field>
|
||||
</flux:fieldset>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="flex items-center justify-between pt-8 border-t border-gray-200 dark:border-gray-700">
|
||||
<flux:button class="cursor-pointer" variant="ghost" type="button" onclick="history.back()">
|
||||
{{ __('Abbrechen') }}
|
||||
</flux:button>
|
||||
|
||||
<flux:button class="cursor-pointer" variant="primary" type="submit">
|
||||
{{ __('Kurs erstellen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
195
resources/views/livewire/courses/edit.blade.php
Normal file
195
resources/views/livewire/courses/edit.blade.php
Normal file
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Course;
|
||||
use App\Models\Lecturer;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Volt\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
new class extends Component {
|
||||
use WithFileUploads;
|
||||
|
||||
#[Validate('image|max:10240')] // 10MB Max
|
||||
public $logo;
|
||||
|
||||
public Course $course;
|
||||
|
||||
// Basic Information
|
||||
public string $name = '';
|
||||
public ?int $lecturer_id = null;
|
||||
public ?string $description = null;
|
||||
|
||||
// System fields (read-only)
|
||||
public ?int $created_by = null;
|
||||
public ?string $created_at = null;
|
||||
public ?string $updated_at = null;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->course->load('media');
|
||||
|
||||
// Basic Information
|
||||
$this->name = $this->course->name ?? '';
|
||||
$this->lecturer_id = $this->course->lecturer_id;
|
||||
$this->description = $this->course->description;
|
||||
|
||||
// System fields
|
||||
$this->created_by = $this->course->created_by;
|
||||
$this->created_at = $this->course->created_at?->format('Y-m-d H:i:s');
|
||||
$this->updated_at = $this->course->updated_at?->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
public function updateCourse(): void
|
||||
{
|
||||
$validated = $this->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'lecturer_id' => ['required', 'exists:lecturers,id'],
|
||||
'description' => ['nullable', 'string'],
|
||||
]);
|
||||
|
||||
$this->course->update($validated);
|
||||
|
||||
if ($this->logo) {
|
||||
$this->course->clearMediaCollection('logo');
|
||||
$this->course
|
||||
->addMedia($this->logo->getRealPath())
|
||||
->usingName($this->course->name)
|
||||
->toMediaCollection('logo');
|
||||
$this->logo = null;
|
||||
$this->course->load('media');
|
||||
}
|
||||
|
||||
$this->dispatch('course-updated', name: $this->course->name);
|
||||
|
||||
session()->flash('status', __('Kurs erfolgreich aktualisiert!'));
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
return [
|
||||
'lecturers' => Lecturer::query()->orderBy('name')->get(),
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="max-w-4xl mx-auto p-6">
|
||||
<flux:heading size="xl" class="mb-8">{{ __('Kurs bearbeiten') }}: {{ $course->name }}</flux:heading>
|
||||
|
||||
<form wire:submit="updateCourse" class="space-y-10">
|
||||
|
||||
<!-- Basic Information -->
|
||||
<flux:fieldset class="space-y-6">
|
||||
<flux:legend>{{ __('Grundlegende Informationen') }}</flux:legend>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
|
||||
<flux:file-upload wire:model="logo">
|
||||
<!-- Custom logo uploader -->
|
||||
<div class="
|
||||
relative flex items-center justify-center size-20 rounded-full transition-colors cursor-pointer
|
||||
border border-zinc-200 dark:border-white/10 hover:border-zinc-300 dark:hover:border-white/10
|
||||
bg-zinc-100 hover:bg-zinc-200 dark:bg-white/10 hover:dark:bg-white/15 in-data-dragging:dark:bg-white/15
|
||||
">
|
||||
<!-- Show the uploaded file if it exists -->
|
||||
@if (!$logo && $course->getFirstMedia('logo'))
|
||||
<img src="{{ $course->getFirstMediaUrl('logo') }}" alt="Logo"
|
||||
class="size-full object-cover rounded"/>
|
||||
@elseif($logo)
|
||||
<img src="{{ $logo?->temporaryUrl() }}" alt="Logo"
|
||||
class="size-full object-cover rounded"/>
|
||||
@else
|
||||
<!-- Show the default icon if no file is uploaded -->
|
||||
<flux:icon name="academic-cap" variant="solid" class="text-zinc-500 dark:text-zinc-400"/>
|
||||
@endif
|
||||
|
||||
<!-- Corner upload icon -->
|
||||
<div class="absolute bottom-0 right-0 bg-white dark:bg-zinc-800 rounded">
|
||||
<flux:icon name="arrow-up-circle" variant="solid" class="text-zinc-500 dark:text-zinc-400"/>
|
||||
</div>
|
||||
</div>
|
||||
</flux:file-upload>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('ID') }}</flux:label>
|
||||
<flux:input value="{{ $course->id }}" disabled/>
|
||||
<flux:description>{{ __('System-generierte ID (nur lesbar)') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Name') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="name" required/>
|
||||
<flux:description>{{ __('Der Anzeigename für diesen Kurs') }}</flux:description>
|
||||
<flux:error name="name"/>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Dozent') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:select variant="listbox" searchable wire:model="lecturer_id"
|
||||
placeholder="{{ __('Dozent auswählen') }}">
|
||||
<x-slot name="search">
|
||||
<flux:select.search class="px-4" placeholder="{{ __('Suche passenden Dozenten...') }}"/>
|
||||
</x-slot>
|
||||
@foreach($lecturers as $lecturer)
|
||||
<flux:select.option value="{{ $lecturer->id }}">{{ $lecturer->name }}
|
||||
</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:description>{{ __('Der Dozent, der diesen Kurs leitet') }}</flux:description>
|
||||
<flux:error name="lecturer_id"/>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Beschreibung') }}</flux:label>
|
||||
<flux:textarea wire:model="description" rows="6"/>
|
||||
<flux:description>{{ __('Ausführliche Beschreibung des Kurses') }}</flux:description>
|
||||
<flux:error name="description"/>
|
||||
</flux:field>
|
||||
</flux:fieldset>
|
||||
|
||||
<!-- System Information -->
|
||||
<flux:fieldset class="space-y-6">
|
||||
<flux:legend>{{ __('Systeminformationen') }}</flux:legend>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Erstellt von') }}</flux:label>
|
||||
<flux:input value="{{ $course->createdBy?->name ?? __('Unbekannt') }}" disabled/>
|
||||
<flux:description>{{ __('Ersteller des Kurses') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Erstellt am') }}</flux:label>
|
||||
<flux:input value="{{ $created_at }}" disabled/>
|
||||
<flux:description>{{ __('Wann dieser Kurs erstellt wurde') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Aktualisiert am') }}</flux:label>
|
||||
<flux:input value="{{ $updated_at }}" disabled/>
|
||||
<flux:description>{{ __('Letzte Änderungszeit') }}</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
</flux:fieldset>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="flex items-center justify-between pt-8 border-t border-gray-200 dark:border-gray-700">
|
||||
<flux:button class="cursor-pointer" variant="ghost" type="button" onclick="history.back()">
|
||||
{{ __('Abbrechen') }}
|
||||
</flux:button>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
@if (session('status'))
|
||||
<flux:text class="text-green-600 dark:text-green-400 font-medium">
|
||||
{{ session('status') }}
|
||||
</flux:text>
|
||||
@endif
|
||||
|
||||
<flux:button class="cursor-pointer" variant="primary" type="submit">
|
||||
{{ __('Kurs aktualisieren') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
127
resources/views/livewire/courses/index.blade.php
Normal file
127
resources/views/livewire/courses/index.blade.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Course;
|
||||
use Livewire\Volt\Component;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
new class extends Component {
|
||||
use WithPagination;
|
||||
|
||||
public $country = 'de';
|
||||
public $search = '';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->country = request()->route('country');
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
return [
|
||||
'courses' => Course::with(['lecturer', 'createdBy'])
|
||||
->withExists([
|
||||
'courseEvents as has_future_events' => fn($query) => $query->where('from', '>=', now())
|
||||
])
|
||||
->when($this->search, fn($query)
|
||||
=> $query
|
||||
->where('name', 'ilike', '%'.$this->search.'%')
|
||||
->orWhere('description', 'ilike', '%'.$this->search.'%'),
|
||||
)
|
||||
->orderByDesc('has_future_events')
|
||||
->paginate(15),
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<flux:heading size="xl">{{ __('Kurse') }}</flux:heading>
|
||||
<div class="flex items-center gap-4">
|
||||
<div>
|
||||
<flux:input
|
||||
wire:model.live="search"
|
||||
:placeholder="__('Suche nach Kursen...')"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<flux:button variant="primary" icon="plus-circle" :href="route_with_country('courses.create')"
|
||||
wire:navigate>{{ __('Neuer Kurs') }}</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<flux:table :paginate="$courses" class="mt-6">
|
||||
<flux:table.columns>
|
||||
<flux:table.column>
|
||||
{{ __('Name') }}
|
||||
</flux:table.column>
|
||||
<flux:table.column>
|
||||
{{ __('Dozent') }}
|
||||
</flux:table.column>
|
||||
<flux:table.column>{{ __('Nächster Termin') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Aktionen') }}</flux:table.column>
|
||||
</flux:table.columns>
|
||||
|
||||
<flux:table.rows>
|
||||
@foreach ($courses as $course)
|
||||
<flux:table.row :key="$course->id">
|
||||
<flux:table.cell variant="strong" class="flex items-center gap-3">
|
||||
<flux:avatar :href="route('courses.landingpage', ['course' => $course, 'country' => $country])"
|
||||
src="{{ $course->getFirstMedia('logo') ? $course->getFirstMediaUrl('logo', 'thumb') : asset('android-chrome-512x512.png') }}"/>
|
||||
<div>
|
||||
<a href="{{ route('courses.landingpage', ['course' => $course, 'country' => $country]) }}">
|
||||
<span>{{ $course->name }}</span>
|
||||
@if($course->description)
|
||||
<div class="text-xs text-zinc-500">
|
||||
{{ Str::limit($course->description, 60) }}
|
||||
</div>
|
||||
@endif
|
||||
</a>
|
||||
</div>
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
@if($course->lecturer)
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:avatar size="xs"
|
||||
src="{{ $course->lecturer->getFirstMedia('avatar') ? $course->lecturer->getFirstMediaUrl('avatar', 'thumb') : asset('img/einundzwanzig.png') }}"/>
|
||||
<span>{{ $course->lecturer->name }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
@php
|
||||
$nextEvent = $course->courseEvents()
|
||||
->where('from', '>=', now())
|
||||
->orderBy('from', 'asc')
|
||||
->first();
|
||||
@endphp
|
||||
@if($nextEvent)
|
||||
<flux:badge color="green" size="sm">
|
||||
{{ $nextEvent->from->format('d.m.Y H:i') }}
|
||||
</flux:badge>
|
||||
@endif
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
<flux:button
|
||||
:disabled="$course->created_by !== auth()->id()"
|
||||
:href="$course->created_by === auth()->id() ? route_with_country('courses.edit', ['course' => $course]) : null"
|
||||
size="xs"
|
||||
variant="filled"
|
||||
icon="pencil">
|
||||
{{ __('Bearbeiten') }}
|
||||
</flux:button>
|
||||
<flux:button
|
||||
:href="route_with_country('courses.events.create', ['course' => $course])"
|
||||
size="xs"
|
||||
variant="filled"
|
||||
icon="calendar">
|
||||
{{ __('Neues Event erstellen') }}
|
||||
</flux:button>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@endforeach
|
||||
</flux:table.rows>
|
||||
</flux:table>
|
||||
</div>
|
||||
164
resources/views/livewire/courses/landingpage.blade.php
Normal file
164
resources/views/livewire/courses/landingpage.blade.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Course;
|
||||
use App\Models\CourseEvent;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
public Course $course;
|
||||
|
||||
public $country = 'de';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->country = request()->route('country');
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
return [
|
||||
'course' => $this->course->load('lecturer'),
|
||||
'events' => $this->course
|
||||
->courseEvents()
|
||||
->where('from', '>=', now())
|
||||
->orderBy('from', 'asc')
|
||||
->get(),
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Left Column: Course Details -->
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center space-x-4">
|
||||
<flux:avatar class="[:where(&)]:size-32 [:where(&)]:text-base" size="xl"
|
||||
src="{{ $course->getFirstMedia('logo') ? $course->getFirstMediaUrl('logo') : asset('android-chrome-512x512.png') }}"/>
|
||||
<div class="space-y-2">
|
||||
<flux:heading size="xl" class="mb-4">{{ $course->name }}</flux:heading>
|
||||
@if($course->lecturer)
|
||||
<flux:subheading class="text-gray-600 dark:text-gray-400 flex items-center gap-2">
|
||||
<flux:avatar size="xs" src="{{ $course->lecturer->getFirstMedia('avatar') ? $course->lecturer->getFirstMediaUrl('avatar', 'thumb') : asset('img/einundzwanzig.png') }}"/>
|
||||
{{ $course->lecturer->name }}
|
||||
</flux:subheading>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($course->description)
|
||||
<div>
|
||||
<flux:heading size="lg" class="mb-2">{{ __('Über den Kurs') }}</flux:heading>
|
||||
<x-markdown class="prose whitespace-pre-wrap">{!! $course->description !!}</x-markdown>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($course->lecturer)
|
||||
<div class="space-y-4">
|
||||
<flux:heading size="lg">{{ __('Über den Dozenten') }}</flux:heading>
|
||||
|
||||
<div class="flex items-start gap-4 p-4 bg-zinc-50 dark:bg-zinc-900 rounded-lg">
|
||||
<flux:avatar size="lg" src="{{ $course->lecturer->getFirstMedia('avatar') ? $course->lecturer->getFirstMediaUrl('avatar', 'preview') : asset('img/einundzwanzig.png') }}"/>
|
||||
<div class="flex-1">
|
||||
<flux:heading size="md" class="mb-1">{{ $course->lecturer->name }}</flux:heading>
|
||||
@if($course->lecturer->subtitle)
|
||||
<flux:text class="text-sm text-zinc-600 dark:text-zinc-400 mb-2">{{ $course->lecturer->subtitle }}</flux:text>
|
||||
@endif
|
||||
@if($course->lecturer->intro)
|
||||
<x-markdown class="prose prose-sm whitespace-pre-wrap">{!! $course->lecturer->intro !!}</x-markdown>
|
||||
@endif
|
||||
|
||||
<!-- Lecturer Social Links -->
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
@if($course->lecturer->website)
|
||||
<flux:button href="{{ $course->lecturer->website }}" target="_blank" variant="ghost" size="xs">
|
||||
<flux:icon.globe-alt class="w-4 h-4 mr-1"/>
|
||||
Website
|
||||
</flux:button>
|
||||
@endif
|
||||
|
||||
@if($course->lecturer->twitter_username)
|
||||
<flux:button href="https://twitter.com/{{ $course->lecturer->twitter_username }}" target="_blank" variant="ghost" size="xs">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
|
||||
</svg>
|
||||
Twitter
|
||||
</flux:button>
|
||||
@endif
|
||||
|
||||
@if($course->lecturer->nostr)
|
||||
<flux:button href="https://njump.me/{{ $course->lecturer->nostr }}" target="_blank" variant="ghost" size="xs">
|
||||
<flux:icon.bolt class="w-4 h-4 mr-1"/>
|
||||
Nostr
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Lecturer Avatar/Info -->
|
||||
<div>
|
||||
@if($course->lecturer && $course->lecturer->getFirstMedia('avatar'))
|
||||
<div class="sticky top-8">
|
||||
<flux:heading size="lg" class="mb-4">{{ __('Dozent') }}</flux:heading>
|
||||
<img src="{{ $course->lecturer->getFirstMediaUrl('avatar') }}"
|
||||
alt="{{ $course->lecturer->name }}"
|
||||
class="w-full rounded-lg shadow-lg"/>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Events Section -->
|
||||
@if($events->isNotEmpty())
|
||||
<div class="mt-16">
|
||||
<flux:heading size="xl" class="mb-6">{{ __('Kommende Veranstaltungen') }}</flux:heading>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
@foreach($events as $event)
|
||||
<flux:card size="sm" class="h-full flex flex-col">
|
||||
<flux:heading class="flex items-center gap-2">
|
||||
{{ $event->from->format('d.m.Y') }}
|
||||
</flux:heading>
|
||||
|
||||
<flux:text class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<flux:icon.clock class="inline w-4 h-4"/>
|
||||
{{ $event->from->format('H:i') }} - {{ $event->to->format('H:i') }} Uhr
|
||||
</flux:text>
|
||||
|
||||
@if($event->venue)
|
||||
<flux:text class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<flux:icon.map-pin class="inline w-4 h-4"/>
|
||||
{{ $event->venue->name }}
|
||||
</flux:text>
|
||||
@endif
|
||||
|
||||
<div class="mt-auto pt-4 flex gap-2">
|
||||
<flux:button
|
||||
target="_blank"
|
||||
:href="$event->link"
|
||||
size="xs"
|
||||
variant="primary"
|
||||
class="flex-1"
|
||||
>
|
||||
{{ __('Details/Anmelden') }}
|
||||
</flux:button>
|
||||
@if($course->created_by === auth()->id())
|
||||
<flux:button
|
||||
:href="route_with_country('courses.events.edit', ['course' => $course, 'event' => $event])"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
icon="pencil"
|
||||
>
|
||||
{{ __('Bearbeiten') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
</flux:card>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
Reference in New Issue
Block a user