mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-app.git
synced 2025-12-14 12:06:46 +00:00
🚀 Add courses and lecturers management functionality
This commit is contained in:
@@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Spatie\Image\Manipulations;
|
use Spatie\Image\Enums\Fit;
|
||||||
use Spatie\MediaLibrary\HasMedia;
|
use Spatie\MediaLibrary\HasMedia;
|
||||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||||
@@ -49,10 +49,10 @@ class Course extends Model implements HasMedia
|
|||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->addMediaConversion('preview')
|
->addMediaConversion('preview')
|
||||||
->fit(Manipulations::FIT_CROP, 300, 300)
|
->fit(Fit::Crop, 300, 300)
|
||||||
->nonQueued();
|
->nonQueued();
|
||||||
$this->addMediaConversion('thumb')
|
$this->addMediaConversion('thumb')
|
||||||
->fit(Manipulations::FIT_CROP, 130, 130)
|
->fit(Fit::Crop, 130, 130)
|
||||||
->width(130)
|
->width(130)
|
||||||
->height(130);
|
->height(130);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||||
use Illuminate\Support\Facades\Cookie;
|
use Illuminate\Support\Facades\Cookie;
|
||||||
use Spatie\Image\Manipulations;
|
use Spatie\Image\Enums\Fit;
|
||||||
use Spatie\MediaLibrary\HasMedia;
|
use Spatie\MediaLibrary\HasMedia;
|
||||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||||
@@ -49,10 +49,10 @@ class Lecturer extends Model implements HasMedia
|
|||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->addMediaConversion('preview')
|
->addMediaConversion('preview')
|
||||||
->fit(Manipulations::FIT_CROP, 300, 300)
|
->fit(Fit::Crop, 300, 300)
|
||||||
->nonQueued();
|
->nonQueued();
|
||||||
$this->addMediaConversion('thumb')
|
$this->addMediaConversion('thumb')
|
||||||
->fit(Manipulations::FIT_CROP, 130, 130)
|
->fit(Fit::Crop, 130, 130)
|
||||||
->width(130)
|
->width(130)
|
||||||
->height(130);
|
->height(130);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"spatie/laravel-permission": "^6.20",
|
"spatie/laravel-permission": "^6.20",
|
||||||
"spatie/laravel-sluggable": "^3.7",
|
"spatie/laravel-sluggable": "^3.7",
|
||||||
"spatie/laravel-tags": "^4.10",
|
"spatie/laravel-tags": "^4.10",
|
||||||
|
"staudenmeir/eloquent-has-many-deep": "^1.21",
|
||||||
"woodsandwalker/laravel-countries": "^1.5"
|
"woodsandwalker/laravel-countries": "^1.5"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
|||||||
110
composer.lock
generated
110
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "9b98697732c4305356365667814db4be",
|
"content-hash": "e2d34fa17a2f68c2fc315cab9b402b42",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "akuechler/laravel-geoly",
|
"name": "akuechler/laravel-geoly",
|
||||||
@@ -5329,6 +5329,114 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-01-13T13:04:43+00:00"
|
"time": "2025-01-13T13:04:43+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "staudenmeir/eloquent-has-many-deep",
|
||||||
|
"version": "v1.21.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/staudenmeir/eloquent-has-many-deep.git",
|
||||||
|
"reference": "b0a3041c44237ebcd0d1005e475a4c736cba482c"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/staudenmeir/eloquent-has-many-deep/zipball/b0a3041c44237ebcd0d1005e475a4c736cba482c",
|
||||||
|
"reference": "b0a3041c44237ebcd0d1005e475a4c736cba482c",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"illuminate/database": "^12.0",
|
||||||
|
"php": "^8.2",
|
||||||
|
"staudenmeir/eloquent-has-many-deep-contracts": "^1.3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"awobaz/compoships": "^2.3",
|
||||||
|
"barryvdh/laravel-ide-helper": "^3.0",
|
||||||
|
"korridor/laravel-has-many-merged": "^1.2",
|
||||||
|
"larastan/larastan": "^3.0",
|
||||||
|
"laravel/framework": "^12.0",
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"orchestra/testbench-core": "^10.0",
|
||||||
|
"phpunit/phpunit": "^11.0",
|
||||||
|
"staudenmeir/eloquent-json-relations": "^1.14",
|
||||||
|
"staudenmeir/laravel-adjacency-list": "^1.24"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Staudenmeir\\EloquentHasManyDeep\\IdeHelperServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Staudenmeir\\EloquentHasManyDeep\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jonas Staudenmeir",
|
||||||
|
"email": "mail@jonas-staudenmeir.de"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Laravel Eloquent HasManyThrough relationships with unlimited levels",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/staudenmeir/eloquent-has-many-deep/issues",
|
||||||
|
"source": "https://github.com/staudenmeir/eloquent-has-many-deep/tree/v1.21.2"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://paypal.me/JonasStaudenmeir",
|
||||||
|
"type": "custom"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-11-08T08:44:24+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "staudenmeir/eloquent-has-many-deep-contracts",
|
||||||
|
"version": "v1.3",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/staudenmeir/eloquent-has-many-deep-contracts.git",
|
||||||
|
"reference": "37ce351e4db919b3af606bc8ca0e62e2e4939cde"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/staudenmeir/eloquent-has-many-deep-contracts/zipball/37ce351e4db919b3af606bc8ca0e62e2e4939cde",
|
||||||
|
"reference": "37ce351e4db919b3af606bc8ca0e62e2e4939cde",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"illuminate/database": "^12.0",
|
||||||
|
"php": "^8.2"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Staudenmeir\\EloquentHasManyDeepContracts\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jonas Staudenmeir",
|
||||||
|
"email": "mail@jonas-staudenmeir.de"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Contracts for staudenmeir/eloquent-has-many-deep",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/staudenmeir/eloquent-has-many-deep-contracts/issues",
|
||||||
|
"source": "https://github.com/staudenmeir/eloquent-has-many-deep-contracts/tree/v1.3"
|
||||||
|
},
|
||||||
|
"time": "2025-02-15T17:11:01+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/clock",
|
"name": "symfony/clock",
|
||||||
"version": "v7.3.0",
|
"version": "v7.3.0",
|
||||||
|
|||||||
@@ -32,6 +32,21 @@
|
|||||||
:current="request()->routeIs('meetups.map')"
|
:current="request()->routeIs('meetups.map')"
|
||||||
wire:navigate>{{ __('Karte') }}</flux:navlist.item>
|
wire:navigate>{{ __('Karte') }}</flux:navlist.item>
|
||||||
</flux:navlist.group>
|
</flux:navlist.group>
|
||||||
|
|
||||||
|
<flux:navlist.group :heading="__('Kurse')" class="grid">
|
||||||
|
<flux:navlist.item icon="academic-cap" :href="route_with_country('courses.index')"
|
||||||
|
:current="request()->routeIs('courses.index')"
|
||||||
|
wire:navigate
|
||||||
|
badge="{{ \App\Models\Course::query()->count() }}">
|
||||||
|
{{ __('Kurse') }}
|
||||||
|
</flux:navlist.item>
|
||||||
|
<flux:navlist.item icon="user" :href="route_with_country('lecturers.index')"
|
||||||
|
:current="request()->routeIs('lecturers.index')"
|
||||||
|
wire:navigate
|
||||||
|
badge="{{ \App\Models\Lecturer::query()->count() }}">
|
||||||
|
{{ __('Dozenten') }}
|
||||||
|
</flux:navlist.item>
|
||||||
|
</flux:navlist.group>
|
||||||
{{--<flux:navlist.group :heading="__('Wallpaper')" class="grid">
|
{{--<flux:navlist.group :heading="__('Wallpaper')" class="grid">
|
||||||
|
|
||||||
</flux:navlist.group>--}}
|
</flux:navlist.group>--}}
|
||||||
|
|||||||
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>
|
||||||
202
resources/views/livewire/lecturers/create.blade.php
Normal file
202
resources/views/livewire/lecturers/create.blade.php
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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 $avatar;
|
||||||
|
|
||||||
|
public string $name = '';
|
||||||
|
public ?string $subtitle = null;
|
||||||
|
public ?string $intro = null;
|
||||||
|
public ?string $description = null;
|
||||||
|
public bool $active = true;
|
||||||
|
|
||||||
|
// Social & Payment Links
|
||||||
|
public ?string $website = null;
|
||||||
|
public ?string $twitter_username = null;
|
||||||
|
public ?string $nostr = null;
|
||||||
|
public ?string $lightning_address = null;
|
||||||
|
public ?string $lnurl = null;
|
||||||
|
public ?string $node_id = null;
|
||||||
|
public ?string $paynym = null;
|
||||||
|
|
||||||
|
public function createLecturer(): void
|
||||||
|
{
|
||||||
|
$validated = $this->validate([
|
||||||
|
'name' => ['required', 'string', 'max:255', 'unique:lecturers,name'],
|
||||||
|
'subtitle' => ['nullable', 'string'],
|
||||||
|
'intro' => ['nullable', 'string'],
|
||||||
|
'description' => ['nullable', 'string'],
|
||||||
|
'active' => ['boolean'],
|
||||||
|
'website' => ['nullable', 'url', 'max:255'],
|
||||||
|
'twitter_username' => ['nullable', 'string', 'max:255'],
|
||||||
|
'nostr' => ['nullable', 'string', 'max:255'],
|
||||||
|
'lightning_address' => ['nullable', 'string'],
|
||||||
|
'lnurl' => ['nullable', 'string'],
|
||||||
|
'node_id' => ['nullable', 'string', 'max:255'],
|
||||||
|
'paynym' => ['nullable', 'string'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$lecturer = Lecturer::create($validated);
|
||||||
|
|
||||||
|
if ($this->avatar) {
|
||||||
|
$lecturer
|
||||||
|
->addMedia($this->avatar->getRealPath())
|
||||||
|
->usingName($lecturer->name)
|
||||||
|
->toMediaCollection('avatar');
|
||||||
|
}
|
||||||
|
|
||||||
|
session()->flash('status', __('Dozent erfolgreich erstellt!'));
|
||||||
|
|
||||||
|
$this->redirect(route_with_country('lecturers.edit', ['lecturer' => $lecturer]), navigate: true);
|
||||||
|
}
|
||||||
|
}; ?>
|
||||||
|
|
||||||
|
<div class="max-w-4xl mx-auto p-6">
|
||||||
|
<flux:heading size="xl" class="mb-8">{{ __('Neuen Dozenten erstellen') }}</flux:heading>
|
||||||
|
|
||||||
|
<form wire:submit="createLecturer" 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="avatar">
|
||||||
|
<!-- Custom avatar 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
|
||||||
|
">
|
||||||
|
@if($avatar)
|
||||||
|
<img src="{{ $avatar?->temporaryUrl() }}" alt="Avatar"
|
||||||
|
class="size-full object-cover rounded-full"/>
|
||||||
|
@else
|
||||||
|
<flux:icon name="user" variant="solid" class="text-zinc-500 dark:text-zinc-400"/>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="absolute bottom-0 right-0 bg-white dark:bg-zinc-800 rounded-full">
|
||||||
|
<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>{{ __('Vollständiger Name des Dozenten') }}</flux:description>
|
||||||
|
<flux:error name="name"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Untertitel') }}</flux:label>
|
||||||
|
<flux:input wire:model="subtitle"/>
|
||||||
|
<flux:description>{{ __('Kurze Berufsbezeichnung oder Rolle') }}</flux:description>
|
||||||
|
<flux:error name="subtitle"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Status') }}</flux:label>
|
||||||
|
<flux:switch wire:model="active"/>
|
||||||
|
<flux:description>{{ __('Ist dieser Dozent aktiv?') }}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Einführung') }}</flux:label>
|
||||||
|
<flux:textarea wire:model="intro" rows="3"/>
|
||||||
|
<flux:description>{{ __('Kurze Vorstellung (wird auf Kurs-Seiten angezeigt)') }}</flux:description>
|
||||||
|
<flux:error name="intro"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Beschreibung') }}</flux:label>
|
||||||
|
<flux:textarea wire:model="description" rows="6"/>
|
||||||
|
<flux:description>{{ __('Ausführliche Beschreibung und Biografie') }}</flux:description>
|
||||||
|
<flux:error name="description"/>
|
||||||
|
</flux:field>
|
||||||
|
</flux:fieldset>
|
||||||
|
|
||||||
|
<!-- Social Links -->
|
||||||
|
<flux:fieldset class="space-y-6">
|
||||||
|
<flux:legend>{{ __('Links & Soziale Medien') }}</flux:legend>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Webseite') }}</flux:label>
|
||||||
|
<flux:input wire:model="website" type="url" placeholder="https://example.com"/>
|
||||||
|
<flux:description>{{ __('Persönliche Webseite oder Portfolio') }}</flux:description>
|
||||||
|
<flux:error name="website"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Twitter Benutzername') }}</flux:label>
|
||||||
|
<flux:input wire:model="twitter_username" placeholder="benutzername"/>
|
||||||
|
<flux:description>{{ __('Twitter-Handle ohne @ Symbol') }}</flux:description>
|
||||||
|
<flux:error name="twitter_username"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Nostr') }}</flux:label>
|
||||||
|
<flux:input wire:model="nostr" placeholder="npub..."/>
|
||||||
|
<flux:description>{{ __('Nostr öffentlicher Schlüssel') }}</flux:description>
|
||||||
|
<flux:error name="nostr"/>
|
||||||
|
</flux:field>
|
||||||
|
</div>
|
||||||
|
</flux:fieldset>
|
||||||
|
|
||||||
|
<!-- Payment Information -->
|
||||||
|
<flux:fieldset class="space-y-6">
|
||||||
|
<flux:legend>{{ __('Zahlungsinformationen') }}</flux:legend>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Lightning Adresse') }}</flux:label>
|
||||||
|
<flux:input wire:model="lightning_address" placeholder="name@getalby.com"/>
|
||||||
|
<flux:description>{{ __('Lightning-Adresse für Zahlungen') }}</flux:description>
|
||||||
|
<flux:error name="lightning_address"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('LNURL') }}</flux:label>
|
||||||
|
<flux:input wire:model="lnurl"/>
|
||||||
|
<flux:description>{{ __('LNURL für Lightning-Zahlungen') }}</flux:description>
|
||||||
|
<flux:error name="lnurl"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Node ID') }}</flux:label>
|
||||||
|
<flux:input wire:model="node_id"/>
|
||||||
|
<flux:description>{{ __('Lightning Node ID') }}</flux:description>
|
||||||
|
<flux:error name="node_id"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('PayNym') }}</flux:label>
|
||||||
|
<flux:input wire:model="paynym"/>
|
||||||
|
<flux:description>{{ __('PayNym für Bitcoin-Zahlungen') }}</flux:description>
|
||||||
|
<flux:error name="paynym"/>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<flux:button class="cursor-pointer" variant="primary" type="submit">
|
||||||
|
{{ __('Dozenten erstellen') }}
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
278
resources/views/livewire/lecturers/edit.blade.php
Normal file
278
resources/views/livewire/lecturers/edit.blade.php
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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 $avatar;
|
||||||
|
|
||||||
|
public Lecturer $lecturer;
|
||||||
|
|
||||||
|
public string $name = '';
|
||||||
|
public ?string $subtitle = null;
|
||||||
|
public ?string $intro = null;
|
||||||
|
public ?string $description = null;
|
||||||
|
public bool $active = true;
|
||||||
|
|
||||||
|
// Social & Payment Links
|
||||||
|
public ?string $website = null;
|
||||||
|
public ?string $twitter_username = null;
|
||||||
|
public ?string $nostr = null;
|
||||||
|
public ?string $lightning_address = null;
|
||||||
|
public ?string $lnurl = null;
|
||||||
|
public ?string $node_id = null;
|
||||||
|
public ?string $paynym = 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->lecturer->load('media');
|
||||||
|
|
||||||
|
$this->name = $this->lecturer->name ?? '';
|
||||||
|
$this->subtitle = $this->lecturer->subtitle;
|
||||||
|
$this->intro = $this->lecturer->intro;
|
||||||
|
$this->description = $this->lecturer->description;
|
||||||
|
$this->active = (bool) $this->lecturer->active;
|
||||||
|
|
||||||
|
$this->website = $this->lecturer->website;
|
||||||
|
$this->twitter_username = $this->lecturer->twitter_username;
|
||||||
|
$this->nostr = $this->lecturer->nostr;
|
||||||
|
$this->lightning_address = $this->lecturer->lightning_address;
|
||||||
|
$this->lnurl = $this->lecturer->lnurl;
|
||||||
|
$this->node_id = $this->lecturer->node_id;
|
||||||
|
$this->paynym = $this->lecturer->paynym;
|
||||||
|
|
||||||
|
$this->created_by = $this->lecturer->created_by;
|
||||||
|
$this->created_at = $this->lecturer->created_at?->format('Y-m-d H:i:s');
|
||||||
|
$this->updated_at = $this->lecturer->updated_at?->format('Y-m-d H:i:s');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateLecturer(): void
|
||||||
|
{
|
||||||
|
$validated = $this->validate([
|
||||||
|
'name' => ['required', 'string', 'max:255', Rule::unique('lecturers')->ignore($this->lecturer->id)],
|
||||||
|
'subtitle' => ['nullable', 'string'],
|
||||||
|
'intro' => ['nullable', 'string'],
|
||||||
|
'description' => ['nullable', 'string'],
|
||||||
|
'active' => ['boolean'],
|
||||||
|
'website' => ['nullable', 'url', 'max:255'],
|
||||||
|
'twitter_username' => ['nullable', 'string', 'max:255'],
|
||||||
|
'nostr' => ['nullable', 'string', 'max:255'],
|
||||||
|
'lightning_address' => ['nullable', 'string'],
|
||||||
|
'lnurl' => ['nullable', 'string'],
|
||||||
|
'node_id' => ['nullable', 'string', 'max:255'],
|
||||||
|
'paynym' => ['nullable', 'string'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->lecturer->update($validated);
|
||||||
|
|
||||||
|
if ($this->avatar) {
|
||||||
|
$this->lecturer->clearMediaCollection('avatar');
|
||||||
|
$this->lecturer
|
||||||
|
->addMedia($this->avatar->getRealPath())
|
||||||
|
->usingName($this->lecturer->name)
|
||||||
|
->toMediaCollection('avatar');
|
||||||
|
$this->avatar = null;
|
||||||
|
$this->lecturer->load('media');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->dispatch('lecturer-updated', name: $this->lecturer->name);
|
||||||
|
|
||||||
|
session()->flash('status', __('Dozent erfolgreich aktualisiert!'));
|
||||||
|
}
|
||||||
|
}; ?>
|
||||||
|
|
||||||
|
<div class="max-w-4xl mx-auto p-6">
|
||||||
|
<flux:heading size="xl" class="mb-8">{{ __('Dozent bearbeiten') }}: {{ $lecturer->name }}</flux:heading>
|
||||||
|
|
||||||
|
<form wire:submit="updateLecturer" 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="avatar">
|
||||||
|
<!-- Custom avatar 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
|
||||||
|
">
|
||||||
|
@if (!$avatar && $lecturer->getFirstMedia('avatar'))
|
||||||
|
<img src="{{ $lecturer->getFirstMediaUrl('avatar') }}" alt="Avatar"
|
||||||
|
class="size-full object-cover rounded-full"/>
|
||||||
|
@elseif($avatar)
|
||||||
|
<img src="{{ $avatar?->temporaryUrl() }}" alt="Avatar"
|
||||||
|
class="size-full object-cover rounded-full"/>
|
||||||
|
@else
|
||||||
|
<flux:icon name="user" variant="solid" class="text-zinc-500 dark:text-zinc-400"/>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="absolute bottom-0 right-0 bg-white dark:bg-zinc-800 rounded-full">
|
||||||
|
<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="{{ $lecturer->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>{{ __('Vollständiger Name des Dozenten') }}</flux:description>
|
||||||
|
<flux:error name="name"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Untertitel') }}</flux:label>
|
||||||
|
<flux:input wire:model="subtitle"/>
|
||||||
|
<flux:description>{{ __('Kurze Berufsbezeichnung oder Rolle') }}</flux:description>
|
||||||
|
<flux:error name="subtitle"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Status') }}</flux:label>
|
||||||
|
<flux:switch wire:model="active"/>
|
||||||
|
<flux:description>{{ __('Ist dieser Dozent aktiv?') }}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Einführung') }}</flux:label>
|
||||||
|
<flux:textarea wire:model="intro" rows="3"/>
|
||||||
|
<flux:description>{{ __('Kurze Vorstellung (wird auf Kurs-Seiten angezeigt)') }}</flux:description>
|
||||||
|
<flux:error name="intro"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Beschreibung') }}</flux:label>
|
||||||
|
<flux:textarea wire:model="description" rows="6"/>
|
||||||
|
<flux:description>{{ __('Ausführliche Beschreibung und Biografie') }}</flux:description>
|
||||||
|
<flux:error name="description"/>
|
||||||
|
</flux:field>
|
||||||
|
</flux:fieldset>
|
||||||
|
|
||||||
|
<!-- Social Links -->
|
||||||
|
<flux:fieldset class="space-y-6">
|
||||||
|
<flux:legend>{{ __('Links & Soziale Medien') }}</flux:legend>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Webseite') }}</flux:label>
|
||||||
|
<flux:input wire:model="website" type="url" placeholder="https://example.com"/>
|
||||||
|
<flux:description>{{ __('Persönliche Webseite oder Portfolio') }}</flux:description>
|
||||||
|
<flux:error name="website"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Twitter Benutzername') }}</flux:label>
|
||||||
|
<flux:input wire:model="twitter_username" placeholder="benutzername"/>
|
||||||
|
<flux:description>{{ __('Twitter-Handle ohne @ Symbol') }}</flux:description>
|
||||||
|
<flux:error name="twitter_username"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Nostr') }}</flux:label>
|
||||||
|
<flux:input wire:model="nostr" placeholder="npub..."/>
|
||||||
|
<flux:description>{{ __('Nostr öffentlicher Schlüssel') }}</flux:description>
|
||||||
|
<flux:error name="nostr"/>
|
||||||
|
</flux:field>
|
||||||
|
</div>
|
||||||
|
</flux:fieldset>
|
||||||
|
|
||||||
|
<!-- Payment Information -->
|
||||||
|
<flux:fieldset class="space-y-6">
|
||||||
|
<flux:legend>{{ __('Zahlungsinformationen') }}</flux:legend>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Lightning Adresse') }}</flux:label>
|
||||||
|
<flux:input wire:model="lightning_address" placeholder="name@getalby.com"/>
|
||||||
|
<flux:description>{{ __('Lightning-Adresse für Zahlungen') }}</flux:description>
|
||||||
|
<flux:error name="lightning_address"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('LNURL') }}</flux:label>
|
||||||
|
<flux:input wire:model="lnurl"/>
|
||||||
|
<flux:description>{{ __('LNURL für Lightning-Zahlungen') }}</flux:description>
|
||||||
|
<flux:error name="lnurl"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Node ID') }}</flux:label>
|
||||||
|
<flux:input wire:model="node_id"/>
|
||||||
|
<flux:description>{{ __('Lightning Node ID') }}</flux:description>
|
||||||
|
<flux:error name="node_id"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('PayNym') }}</flux:label>
|
||||||
|
<flux:input wire:model="paynym"/>
|
||||||
|
<flux:description>{{ __('PayNym für Bitcoin-Zahlungen') }}</flux:description>
|
||||||
|
<flux:error name="paynym"/>
|
||||||
|
</flux:field>
|
||||||
|
</div>
|
||||||
|
</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="{{ $lecturer->createdBy?->name ?? __('Unbekannt') }}" disabled/>
|
||||||
|
<flux:description>{{ __('Ersteller des Dozenten') }}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Erstellt am') }}</flux:label>
|
||||||
|
<flux:input value="{{ $created_at }}" disabled/>
|
||||||
|
<flux:description>{{ __('Wann dieser Dozent 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">
|
||||||
|
{{ __('Dozent aktualisieren') }}
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
136
resources/views/livewire/lecturers/index.blade.php
Normal file
136
resources/views/livewire/lecturers/index.blade.php
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Lecturer;
|
||||||
|
use Livewire\Volt\Component;
|
||||||
|
use Livewire\WithPagination;
|
||||||
|
|
||||||
|
new class extends Component {
|
||||||
|
use WithPagination;
|
||||||
|
|
||||||
|
public $country = 'de';
|
||||||
|
public $sortBy = 'name';
|
||||||
|
public $sortDirection = 'asc';
|
||||||
|
public $search = '';
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
$this->country = request()->route('country');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sort($column)
|
||||||
|
{
|
||||||
|
if ($this->sortBy === $column) {
|
||||||
|
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||||
|
} else {
|
||||||
|
$this->sortBy = $column;
|
||||||
|
$this->sortDirection = 'asc';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function with(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'lecturers' => Lecturer::with(['createdBy'])
|
||||||
|
->when($this->search, fn($query)
|
||||||
|
=> $query->where('name', 'ilike', '%'.$this->search.'%')
|
||||||
|
->orWhere('description', 'ilike', '%'.$this->search.'%')
|
||||||
|
->orWhere('subtitle', 'ilike', '%'.$this->search.'%'),
|
||||||
|
)
|
||||||
|
->orderBy($this->sortBy, $this->sortDirection)
|
||||||
|
->paginate(15),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}; ?>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<flux:heading size="xl">{{ __('Dozenten') }}</flux:heading>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<flux:input
|
||||||
|
wire:model.live="search"
|
||||||
|
:placeholder="__('Suche nach Dozenten...')"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
@auth
|
||||||
|
<flux:button class="cursor-pointer" :href="route_with_country('lecturers.create')" icon="plus" variant="primary">
|
||||||
|
{{ __('Dozenten anlegen') }}
|
||||||
|
</flux:button>
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<flux:table :paginate="$lecturers" class="mt-6">
|
||||||
|
<flux:table.columns>
|
||||||
|
<flux:table.column sortable :sorted="$sortBy === 'name'" :direction="$sortDirection"
|
||||||
|
wire:click="sort('name')">{{ __('Name') }}
|
||||||
|
</flux:table.column>
|
||||||
|
<flux:table.column>{{ __('Untertitel') }}</flux:table.column>
|
||||||
|
<flux:table.column>{{ __('Kurse') }}</flux:table.column>
|
||||||
|
<flux:table.column>{{ __('Links') }}</flux:table.column>
|
||||||
|
<flux:table.column>{{ __('Aktionen') }}</flux:table.column>
|
||||||
|
</flux:table.columns>
|
||||||
|
|
||||||
|
<flux:table.rows>
|
||||||
|
@foreach ($lecturers as $lecturer)
|
||||||
|
<flux:table.row :key="$lecturer->id">
|
||||||
|
<flux:table.cell variant="strong" class="flex items-center gap-3">
|
||||||
|
<flux:avatar size="lg" src="{{ $lecturer->getFirstMedia('avatar') ? $lecturer->getFirstMediaUrl('avatar', 'thumb') : asset('img/einundzwanzig.png') }}"/>
|
||||||
|
<div>
|
||||||
|
<div class="font-semibold">{{ $lecturer->name }}</div>
|
||||||
|
@if($lecturer->active)
|
||||||
|
<flux:badge size="sm" color="green">{{ __('Aktiv') }}</flux:badge>
|
||||||
|
@else
|
||||||
|
<flux:badge size="sm" color="zinc">{{ __('Inaktiv') }}</flux:badge>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</flux:table.cell>
|
||||||
|
|
||||||
|
<flux:table.cell>
|
||||||
|
@if($lecturer->subtitle)
|
||||||
|
<div class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
|
{{ Str::limit($lecturer->subtitle, 50) }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</flux:table.cell>
|
||||||
|
|
||||||
|
<flux:table.cell>
|
||||||
|
<flux:badge size="sm">{{ $lecturer->courses()->count() }} {{ __('Kurse') }}</flux:badge>
|
||||||
|
</flux:table.cell>
|
||||||
|
|
||||||
|
<flux:table.cell>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
@if($lecturer->website)
|
||||||
|
<flux:link :href="$lecturer->website" external variant="subtle" title="{{ __('Website') }}">
|
||||||
|
<flux:icon.globe-alt variant="mini"/>
|
||||||
|
</flux:link>
|
||||||
|
@endif
|
||||||
|
@if($lecturer->twitter_username)
|
||||||
|
<flux:link :href="'https://twitter.com/' . $lecturer->twitter_username" external
|
||||||
|
variant="subtle" title="{{ __('Twitter') }}">
|
||||||
|
<flux:icon.x-mark variant="mini"/>
|
||||||
|
</flux:link>
|
||||||
|
@endif
|
||||||
|
@if($lecturer->nostr)
|
||||||
|
<flux:link :href="'https://njump.me/'.$lecturer->nostr" external variant="subtle"
|
||||||
|
title="{{ __('Nostr') }}">
|
||||||
|
<flux:icon.bolt variant="mini"/>
|
||||||
|
</flux:link>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</flux:table.cell>
|
||||||
|
|
||||||
|
<flux:table.cell>
|
||||||
|
<flux:button
|
||||||
|
:disabled="$lecturer->created_by !== auth()->id()"
|
||||||
|
:href="$lecturer->created_by === auth()->id() ? route_with_country('lecturers.edit', ['lecturer' => $lecturer]) : null"
|
||||||
|
size="xs"
|
||||||
|
variant="filled"
|
||||||
|
icon="pencil">
|
||||||
|
{{ __('Bearbeiten') }}
|
||||||
|
</flux:button>
|
||||||
|
</flux:table.cell>
|
||||||
|
</flux:table.row>
|
||||||
|
@endforeach
|
||||||
|
</flux:table.rows>
|
||||||
|
</flux:table>
|
||||||
|
</div>
|
||||||
@@ -64,7 +64,7 @@ new class extends Component {
|
|||||||
if ($this->event) {
|
if ($this->event) {
|
||||||
$this->event->delete();
|
$this->event->delete();
|
||||||
session()->flash('status', __('Event erfolgreich gelöscht!'));
|
session()->flash('status', __('Event erfolgreich gelöscht!'));
|
||||||
$this->redirect(route_with_country('meetups.edit', ['meetup' => $this->meetup]), navigate: true);
|
$this->redirect(route('meetups.landingpage', ['meetup' => $this->meetup, 'country' => $this->country]), navigate: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}; ?>
|
}; ?>
|
||||||
|
|||||||
337
resources/views/livewire/meetups/create.blade.php
Normal file
337
resources/views/livewire/meetups/create.blade.php
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\City;
|
||||||
|
use App\Models\Country;
|
||||||
|
use App\Models\Meetup;
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Basic Information
|
||||||
|
public string $name = '';
|
||||||
|
public ?int $city_id = null;
|
||||||
|
public ?string $intro = null;
|
||||||
|
|
||||||
|
// Links and Social Media
|
||||||
|
public ?string $telegram_link = null;
|
||||||
|
public ?string $webpage = null;
|
||||||
|
public ?string $twitter_username = null;
|
||||||
|
public ?string $matrix_group = null;
|
||||||
|
public ?string $nostr = null;
|
||||||
|
public ?string $simplex = null;
|
||||||
|
public ?string $signal = null;
|
||||||
|
|
||||||
|
// Additional Information
|
||||||
|
public ?string $community = null;
|
||||||
|
public bool $visible_on_map = true;
|
||||||
|
|
||||||
|
// New City Modal
|
||||||
|
public string $newCityName = '';
|
||||||
|
public ?int $newCityCountryId = null;
|
||||||
|
public float $newCityLatitude = 0;
|
||||||
|
public float $newCityLongitude = 0;
|
||||||
|
|
||||||
|
public function createCity(): void
|
||||||
|
{
|
||||||
|
$validated = $this->validate([
|
||||||
|
'newCityName' => ['required', 'string', 'max:255', 'unique:cities,name'],
|
||||||
|
'newCityCountryId' => ['required', 'exists:countries,id'],
|
||||||
|
'newCityLatitude' => ['required', 'numeric'],
|
||||||
|
'newCityLongitude' => ['required', 'numeric'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$city = City::create([
|
||||||
|
'name' => $validated['newCityName'],
|
||||||
|
'country_id' => $validated['newCityCountryId'],
|
||||||
|
'latitude' => $validated['newCityLatitude'],
|
||||||
|
'longitude' => $validated['newCityLongitude'],
|
||||||
|
'slug' => str($validated['newCityName'])->slug(),
|
||||||
|
'created_by' => auth()->id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->city_id = $city->id;
|
||||||
|
$this->reset(['newCityName', 'newCityCountryId', 'newCityLatitude', 'newCityLongitude']);
|
||||||
|
|
||||||
|
\Flux\Flux::modal('add-city')->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createMeetup(): void
|
||||||
|
{
|
||||||
|
$validated = $this->validate([
|
||||||
|
'name' => ['required', 'string', 'max:255', 'unique:meetups,name'],
|
||||||
|
'city_id' => ['required', 'exists:cities,id'],
|
||||||
|
'intro' => ['nullable', 'string'],
|
||||||
|
'telegram_link' => ['nullable', 'url', 'max:255'],
|
||||||
|
'webpage' => ['nullable', 'url', 'max:255'],
|
||||||
|
'twitter_username' => ['nullable', 'string', 'max:255'],
|
||||||
|
'matrix_group' => ['nullable', 'string', 'max:255'],
|
||||||
|
'nostr' => ['nullable', 'string', 'max:255'],
|
||||||
|
'simplex' => ['nullable', 'string', 'max:255'],
|
||||||
|
'signal' => ['nullable', 'string', 'max:510'],
|
||||||
|
'community' => ['nullable', 'string', 'max:255'],
|
||||||
|
'visible_on_map' => ['boolean'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$meetup = Meetup::create($validated);
|
||||||
|
|
||||||
|
if ($this->logo) {
|
||||||
|
$meetup
|
||||||
|
->addMedia($this->logo->getRealPath())
|
||||||
|
->usingName($meetup->name)
|
||||||
|
->toMediaCollection('logo');
|
||||||
|
}
|
||||||
|
|
||||||
|
session()->flash('status', __('Meetup erfolgreich erstellt!'));
|
||||||
|
|
||||||
|
$this->redirect(route_with_country('meetups.edit', ['meetup' => $meetup]), navigate: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function with(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'cities' => City::query()->orderBy('name')->get(),
|
||||||
|
'countries' => Country::query()->orderBy('countries.name')->get(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}; ?>
|
||||||
|
|
||||||
|
<div class="max-w-4xl mx-auto p-6">
|
||||||
|
<flux:heading size="xl" class="mb-8">{{ __('Neues Meetup erstellen') }}</flux:heading>
|
||||||
|
|
||||||
|
<form wire:submit="createMeetup" 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
|
||||||
|
">
|
||||||
|
@if($logo)
|
||||||
|
<img src="{{ $logo?->temporaryUrl() }}" alt="Logo"
|
||||||
|
class="size-full object-cover rounded-full"/>
|
||||||
|
@else
|
||||||
|
<!-- Show the default icon if no file is uploaded -->
|
||||||
|
<flux:icon name="user-group" 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-full">
|
||||||
|
<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 dieses Meetup') }}</flux:description>
|
||||||
|
<flux:error name="name"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<flux:label>{{ __('Stadt') }} <span class="text-red-500">*</span></flux:label>
|
||||||
|
<flux:modal.trigger name="add-city">
|
||||||
|
<flux:button class="cursor-pointer" size="xs" variant="ghost" icon="plus">
|
||||||
|
{{ __('Stadt hinzufügen') }}
|
||||||
|
</flux:button>
|
||||||
|
</flux:modal.trigger>
|
||||||
|
</div>
|
||||||
|
<flux:select variant="listbox" searchable wire:model="city_id"
|
||||||
|
placeholder="{{ __('Stadt auswählen') }}" required>
|
||||||
|
<x-slot name="search">
|
||||||
|
<flux:select.search class="px-4" placeholder="{{ __('Suche passende Stadt...') }}"/>
|
||||||
|
</x-slot>
|
||||||
|
@foreach($cities as $city)
|
||||||
|
<flux:select.option value="{{ $city->id }}">{{ $city->name }} ({{ $city->country->name }})
|
||||||
|
</flux:select.option>
|
||||||
|
@endforeach
|
||||||
|
</flux:select>
|
||||||
|
<flux:description>{{ __('Die nächstgrößte Stadt oder Ort') }}</flux:description>
|
||||||
|
<flux:error name="city_id"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Auf Karte sichtbar') }}</flux:label>
|
||||||
|
<flux:switch wire:model="visible_on_map"/>
|
||||||
|
<flux:description>{{ __('Soll dieses Meetup auf der Karte angezeigt werden?') }}</flux:description>
|
||||||
|
</flux:field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Einführung') }}</flux:label>
|
||||||
|
<flux:textarea wire:model="intro" rows="4"/>
|
||||||
|
<flux:description>{{ __('Kurze Beschreibung des Meetups') }}</flux:description>
|
||||||
|
<flux:error name="intro"/>
|
||||||
|
</flux:field>
|
||||||
|
</flux:fieldset>
|
||||||
|
|
||||||
|
<!-- Links and Social Media -->
|
||||||
|
<flux:fieldset class="space-y-6">
|
||||||
|
<flux:legend>{{ __('Links & Soziale Medien') }}</flux:legend>
|
||||||
|
|
||||||
|
<!-- Primary Links -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Webseite') }}</flux:label>
|
||||||
|
<flux:input wire:model="webpage" type="url" placeholder="https://example.com"/>
|
||||||
|
<flux:description>{{ __('Offizielle Webseite oder Landingpage') }}</flux:description>
|
||||||
|
<flux:error name="webpage"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Telegram Link') }}</flux:label>
|
||||||
|
<flux:input wire:model="telegram_link" type="url" placeholder="https://t.me/gruppenname"/>
|
||||||
|
<flux:description>{{ __('Link zur Telegram-Gruppe oder zum Kanal') }}</flux:description>
|
||||||
|
<flux:error name="telegram_link"/>
|
||||||
|
</flux:field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Social Media -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Twitter Benutzername') }}</flux:label>
|
||||||
|
<flux:input wire:model="twitter_username" placeholder="benutzername"/>
|
||||||
|
<flux:description>{{ __('Twitter-Handle ohne @ Symbol') }}</flux:description>
|
||||||
|
<flux:error name="twitter_username"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Matrix Gruppe') }}</flux:label>
|
||||||
|
<flux:input wire:model="matrix_group" placeholder="#gruppe:matrix.org"/>
|
||||||
|
<flux:description>{{ __('Matrix-Raum Bezeichner oder Link') }}</flux:description>
|
||||||
|
<flux:error name="matrix_group"/>
|
||||||
|
</flux:field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Decentralized Platforms -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Nostr') }}</flux:label>
|
||||||
|
<flux:input wire:model="nostr" placeholder="npub..."/>
|
||||||
|
<flux:description>{{ __('Nostr öffentlicher Schlüssel oder Bezeichner') }}</flux:description>
|
||||||
|
<flux:error name="nostr"/>
|
||||||
|
</flux:field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Messaging Apps -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('SimpleX') }}</flux:label>
|
||||||
|
<flux:input wire:model="simplex"/>
|
||||||
|
<flux:description>{{ __('SimpleX Chat Kontaktinformationen') }}</flux:description>
|
||||||
|
<flux:error name="simplex"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Signal') }}</flux:label>
|
||||||
|
<flux:input wire:model="signal"/>
|
||||||
|
<flux:description>{{ __('Signal Kontakt- oder Gruppeninformationen') }}</flux:description>
|
||||||
|
<flux:error name="signal"/>
|
||||||
|
</flux:field>
|
||||||
|
</div>
|
||||||
|
</flux:fieldset>
|
||||||
|
|
||||||
|
<!-- Additional Information -->
|
||||||
|
<flux:fieldset class="space-y-6">
|
||||||
|
<flux:legend>{{ __('Zusätzliche Informationen') }}</flux:legend>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Gemeinschaft') }}</flux:label>
|
||||||
|
<flux:select wire:model="community">
|
||||||
|
<flux:select.option value="">{{ __('Keine') }}</flux:select.option>
|
||||||
|
<flux:select.option value="einundzwanzig">einundzwanzig</flux:select.option>
|
||||||
|
<flux:select.option value="bitcoin">bitcoin</flux:select.option>
|
||||||
|
</flux:select>
|
||||||
|
<flux:description>{{ __('Gemeinschafts- oder Organisationsname') }}</flux:description>
|
||||||
|
<flux:error name="community"/>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<flux:button class="cursor-pointer" variant="primary" type="submit">
|
||||||
|
{{ __('Meetup erstellen') }}
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Add City Modal -->
|
||||||
|
<flux:modal name="add-city" variant="flyout" wire:key="add-city-modal">
|
||||||
|
<form wire:submit="createCity" class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<flux:heading size="lg">{{ __('Stadt hinzufügen') }}</flux:heading>
|
||||||
|
<flux:text class="mt-2">{{ __('Füge eine neue Stadt zur Datenbank hinzu.') }}</flux:text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Stadtname') }} <span class="text-red-500">*</span></flux:label>
|
||||||
|
<flux:input wire:model="newCityName" placeholder="{{ __('z.B. Berlin') }}" required/>
|
||||||
|
<flux:error name="newCityName"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Land') }} <span class="text-red-500">*</span></flux:label>
|
||||||
|
<flux:select variant="listbox" searchable wire:model="newCityCountryId"
|
||||||
|
placeholder="{{ __('Land auswählen') }}">
|
||||||
|
@foreach($countries as $country)
|
||||||
|
<flux:select.option value="{{ $country->id }}">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<img alt="{{ str($country->code)->lower() }}"
|
||||||
|
src="{{ asset('vendor/blade-flags/country-'.str($country->code)->lower().'.svg') }}"
|
||||||
|
width="24" height="12"/>
|
||||||
|
<span>{{ $country->name }}</span>
|
||||||
|
</div>
|
||||||
|
</flux:select.option>
|
||||||
|
@endforeach
|
||||||
|
</flux:select>
|
||||||
|
<flux:error name="newCityCountryId"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Breitengrad') }} <span class="text-red-500">*</span></flux:label>
|
||||||
|
<flux:input wire:model="newCityLatitude" type="number" step="0.000001" placeholder="52.520008"
|
||||||
|
required/>
|
||||||
|
<flux:error name="newCityLatitude"/>
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Längengrad') }} <span class="text-red-500">*</span></flux:label>
|
||||||
|
<flux:input wire:model="newCityLongitude" type="number" step="0.000001" placeholder="13.404954"
|
||||||
|
required/>
|
||||||
|
<flux:error name="newCityLongitude"/>
|
||||||
|
</flux:field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<flux:spacer/>
|
||||||
|
|
||||||
|
<flux:modal.close>
|
||||||
|
<flux:button class="cursor-pointer" type="button" variant="ghost">{{ __('Abbrechen') }}</flux:button>
|
||||||
|
</flux:modal.close>
|
||||||
|
|
||||||
|
<flux:button class="cursor-pointer" type="submit" variant="primary">{{ __('Stadt erstellen') }}</flux:button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</flux:modal>
|
||||||
|
</div>
|
||||||
@@ -212,7 +212,7 @@ new class extends Component {
|
|||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<flux:label>{{ __('Stadt') }}</flux:label>
|
<flux:label>{{ __('Stadt') }}</flux:label>
|
||||||
<flux:modal.trigger name="add-city">
|
<flux:modal.trigger name="add-city">
|
||||||
<flux:button size="xs" variant="ghost" icon="plus">
|
<flux:button class="cursor-pointer" size="xs" variant="ghost" icon="plus">
|
||||||
{{ __('Stadt hinzufügen') }}
|
{{ __('Stadt hinzufügen') }}
|
||||||
</flux:button>
|
</flux:button>
|
||||||
</flux:modal.trigger>
|
</flux:modal.trigger>
|
||||||
@@ -350,7 +350,7 @@ new class extends Component {
|
|||||||
|
|
||||||
<!-- Form Actions -->
|
<!-- Form Actions -->
|
||||||
<div class="flex items-center justify-between pt-8 border-t border-gray-200 dark:border-gray-700">
|
<div class="flex items-center justify-between pt-8 border-t border-gray-200 dark:border-gray-700">
|
||||||
<flux:button variant="ghost" type="button" onclick="history.back()">
|
<flux:button class="cursor-pointer" variant="ghost" type="button" onclick="history.back()">
|
||||||
{{ __('Abbrechen') }}
|
{{ __('Abbrechen') }}
|
||||||
</flux:button>
|
</flux:button>
|
||||||
|
|
||||||
@@ -361,7 +361,7 @@ new class extends Component {
|
|||||||
</flux:text>
|
</flux:text>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<flux:button variant="primary" type="submit">
|
<flux:button class="cursor-pointer" variant="primary" type="submit">
|
||||||
{{ __('Meetup aktualisieren') }}
|
{{ __('Meetup aktualisieren') }}
|
||||||
</flux:button>
|
</flux:button>
|
||||||
</div>
|
</div>
|
||||||
@@ -420,10 +420,10 @@ new class extends Component {
|
|||||||
<flux:spacer/>
|
<flux:spacer/>
|
||||||
|
|
||||||
<flux:modal.close>
|
<flux:modal.close>
|
||||||
<flux:button type="button" variant="ghost">{{ __('Abbrechen') }}</flux:button>
|
<flux:button class="cursor-pointer" type="button" variant="ghost">{{ __('Abbrechen') }}</flux:button>
|
||||||
</flux:modal.close>
|
</flux:modal.close>
|
||||||
|
|
||||||
<flux:button type="submit" variant="primary">{{ __('Stadt erstellen') }}</flux:button>
|
<flux:button class="cursor-pointer" type="submit" variant="primary">{{ __('Stadt erstellen') }}</flux:button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</flux:modal>
|
</flux:modal>
|
||||||
|
|||||||
@@ -48,15 +48,20 @@ new class extends Component {
|
|||||||
}; ?>
|
}; ?>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<flux:heading size="xl">{{ __('Meetups') }}</flux:heading>
|
<flux:heading size="xl">{{ __('Meetups') }}</flux:heading>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
<flux:button class="cursor-pointer" x-copy-to-clipboard="'{{ route('ics') }}'" icon="calendar-date-range">{{ __('Kalender-Stream-URL kopieren') }}</flux:button>
|
<flux:button class="cursor-pointer" x-copy-to-clipboard="'{{ route('ics') }}'" icon="calendar-date-range">{{ __('Kalender-Stream-URL kopieren') }}</flux:button>
|
||||||
<div class="mt-4">
|
|
||||||
<flux:input
|
<flux:input
|
||||||
wire:model.live="search"
|
wire:model.live="search"
|
||||||
:placeholder="__('Suche nach Meetups...')"
|
:placeholder="__('Suche nach Meetups...')"
|
||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
|
@auth
|
||||||
|
<flux:button class="cursor-pointer" :href="route_with_country('meetups.create')" icon="plus" variant="primary">
|
||||||
|
{{ __('Meetup erstellen') }}
|
||||||
|
</flux:button>
|
||||||
|
@endauth
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -193,6 +193,11 @@ new class extends Component {
|
|||||||
<div class="mt-16">
|
<div class="mt-16">
|
||||||
<div class="flex items-center space-x-4 mb-6">
|
<div class="flex items-center space-x-4 mb-6">
|
||||||
<flux:heading size="xl">{{ __('Kommende Veranstaltungen') }}</flux:heading>
|
<flux:heading size="xl">{{ __('Kommende Veranstaltungen') }}</flux:heading>
|
||||||
|
@if(auth()->user()->meetups()->find($meetup->id)?->exists)
|
||||||
|
<flux:button :href="route_with_country('meetups.events.create', ['meetup' => $meetup])" variant="primary" icon="calendar">
|
||||||
|
{{ __('Neues Event erstellen') }}
|
||||||
|
</flux:button>
|
||||||
|
@endif
|
||||||
<flux:button class="cursor-pointer" x-copy-to-clipboard="'{{ route('ics', ['meetup' => $meetup]) }}'" icon="calendar-date-range">{{ __('Kalender-Stream-URL kopieren') }}</flux:button>
|
<flux:button class="cursor-pointer" x-copy-to-clipboard="'{{ route('ics', ['meetup' => $meetup]) }}'" icon="calendar-date-range">{{ __('Kalender-Stream-URL kopieren') }}</flux:button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -251,5 +256,15 @@ new class extends Component {
|
|||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="mt-16">
|
||||||
|
<div class="flex items-center space-x-4 mb-6">
|
||||||
|
@if(auth()->user()->meetups()->find($meetup->id)?->exists)
|
||||||
|
<flux:button :href="route_with_country('meetups.events.create', ['meetup' => $meetup])" variant="primary" icon="calendar">
|
||||||
|
{{ __('Neues Event erstellen') }}
|
||||||
|
</flux:button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,20 +13,34 @@ Route::get('stream-calendar', \App\Http\Controllers\DownloadMeetupCalendar::clas
|
|||||||
Route::middleware([])
|
Route::middleware([])
|
||||||
->prefix('/{country:code}')
|
->prefix('/{country:code}')
|
||||||
->group(function () {
|
->group(function () {
|
||||||
|
|
||||||
Volt::route('meetups', 'meetups.index')->name('meetups.index');
|
Volt::route('meetups', 'meetups.index')->name('meetups.index');
|
||||||
Volt::route('map', 'meetups.map')->name('meetups.map');
|
Volt::route('map', 'meetups.map')->name('meetups.map');
|
||||||
Volt::route('meetup/{meetup:slug}', 'meetups.landingpage')->name('meetups.landingpage');
|
Volt::route('meetup/{meetup:slug}', 'meetups.landingpage')->name('meetups.landingpage');
|
||||||
Volt::route('meetup/{meetup:slug}/event/{event}', 'meetups.landingpage-event')->name('meetups.landingpage-event');
|
Volt::route('meetup/{meetup:slug}/event/{event}', 'meetups.landingpage-event')->name('meetups.landingpage-event');
|
||||||
|
|
||||||
|
Volt::route('courses', 'courses.index')->name('courses.index');
|
||||||
|
Volt::route('course/{course}', 'courses.landingpage')->name('courses.landingpage');
|
||||||
|
Volt::route('course/{course}/event/{event}', 'courses.landingpage-event')->name('courses.landingpage-event');
|
||||||
|
|
||||||
|
Volt::route('lecturers', 'lecturers.index')->name('lecturers.index');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::middleware(['auth'])
|
Route::middleware(['auth'])
|
||||||
->prefix('/{country:code}')
|
->prefix('/{country:code}')
|
||||||
->group(function () {
|
->group(function () {
|
||||||
Volt::route('dashboard', 'dashboard')->name('dashboard');
|
Volt::route('dashboard', 'dashboard')->name('dashboard');
|
||||||
|
Volt::route('meetup-create', 'meetups.create')->name('meetups.create');
|
||||||
Volt::route('meetup-edit/{meetup}', 'meetups.edit')->name('meetups.edit');
|
Volt::route('meetup-edit/{meetup}', 'meetups.edit')->name('meetups.edit');
|
||||||
Volt::route('meetup/{meetup}/events/create', 'meetups.create-edit-events')->name('meetups.events.create');
|
Volt::route('meetup/{meetup}/events/create', 'meetups.create-edit-events')->name('meetups.events.create');
|
||||||
Volt::route('meetup/{meetup}/events/{event}/edit', 'meetups.create-edit-events')->name('meetups.events.edit');
|
Volt::route('meetup/{meetup}/events/{event}/edit', 'meetups.create-edit-events')->name('meetups.events.edit');
|
||||||
|
|
||||||
|
Volt::route('course-create', 'courses.create')->name('courses.create');
|
||||||
|
Volt::route('course-edit/{course}', 'courses.edit')->name('courses.edit');
|
||||||
|
Volt::route('course/{course}/events/create', 'courses.create-edit-events')->name('courses.events.create');
|
||||||
|
Volt::route('course/{course}/events/{event}/edit', 'courses.create-edit-events')->name('courses.events.edit');
|
||||||
|
|
||||||
|
Volt::route('lecturer-create', 'lecturers.create')->name('lecturers.create');
|
||||||
|
Volt::route('lecturer-edit/{lecturer}', 'lecturers.edit')->name('lecturers.edit');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::middleware(['auth'])
|
Route::middleware(['auth'])
|
||||||
|
|||||||
Reference in New Issue
Block a user