Add CRUD support for Cities and Venues

This commit is contained in:
HolgerHatGarKeineNode
2025-11-21 17:04:56 +01:00
parent d05485a406
commit 3481301720
11 changed files with 705 additions and 7 deletions

View File

@@ -322,5 +322,57 @@
"Öffnen/RSVP": "", "Öffnen/RSVP": "",
"Über den Dozenten": "", "Über den Dozenten": "",
"Über den Kurs": "", "Über den Kurs": "",
"Über uns": "" "Über uns": "",
"Diverses": "",
"Orte/Gebiete": "",
"Städte/Gebiete": "",
"Veranstaltungsorte": "",
"Copy": "",
"Click to connect": "",
"City successfully created!": "",
"Create City": "",
"Country": "",
"Select a country": "",
"Latitude": "",
"Longitude": "",
"Population": "",
"Population Date": "",
"City successfully updated!": "",
"Edit City": "",
"Update City": "",
"Cities": "",
"Search cities...": "",
"Created By": "",
"Actions": "",
"Edit": "",
"Wann beginnt das Event?": "",
"Endzeit": "",
"Wann endet das Event?": "",
"Veranstaltungsort": "",
"Ort hinzufügen": "",
"Veranstaltungsort auswählen": "",
"Suche nach Ort...": "",
"Link zu weiteren Informationen oder zur Anmeldung": "",
"Veranstaltungsort hinzufügen": "",
"Füge einen neuen Veranstaltungsort zur Datenbank hinzu.": "",
"z.B. Bitcoin Zentrum München": "",
"Straße": "",
"z.B. Hauptstraße 1": "",
"Ort erstellen": "",
"Anmeldungen": "",
"weitere Termine": "",
"Venue successfully created!": "",
"Create Venue": "",
"City": "",
"Select a city": "",
"Street": "",
"Venue successfully updated!": "",
"Edit Venue": "",
"Update Venue": "",
"Venues": "",
"Search venues...": "",
"Basic Information": "",
"Coordinates": "",
"Demographics": "",
"Venue Information": ""
} }

View File

@@ -322,5 +322,57 @@
"Öffnen/RSVP": "Open/RSVP", "Öffnen/RSVP": "Open/RSVP",
"Über den Dozenten": "About the lecturer", "Über den Dozenten": "About the lecturer",
"Über den Kurs": "About the course", "Über den Kurs": "About the course",
"Über uns": "About us" "Über uns": "About us",
"Diverses": "Miscellaneous",
"Orte/Gebiete": "Places/Areas",
"Städte/Gebiete": "Cities/Areas",
"Veranstaltungsorte": "Venues",
"Copy": "Copy",
"Click to connect": "Click to connect",
"City successfully created!": "City successfully created!",
"Create City": "Create City",
"Country": "Country",
"Select a country": "Select a country",
"Latitude": "Latitude",
"Longitude": "Longitude",
"Population": "Population",
"Population Date": "Population Date",
"City successfully updated!": "City successfully updated!",
"Edit City": "Edit City",
"Update City": "Update City",
"Cities": "Cities",
"Search cities...": "Search cities...",
"Created By": "Created By",
"Actions": "Actions",
"Edit": "Edit",
"Wann beginnt das Event?": "When does the event start?",
"Endzeit": "End time",
"Wann endet das Event?": "When does the event end?",
"Veranstaltungsort": "Venue",
"Ort hinzufügen": "Add location",
"Veranstaltungsort auswählen": "Select venue",
"Suche nach Ort...": "Search for location...",
"Link zu weiteren Informationen oder zur Anmeldung": "Link to further information or registration",
"Veranstaltungsort hinzufügen": "Add venue",
"Füge einen neuen Veranstaltungsort zur Datenbank hinzu.": "Add a new venue to the database.",
"z.B. Bitcoin Zentrum München": "e.g. Bitcoin Center Munich",
"Straße": "Street",
"z.B. Hauptstraße 1": "e.g. Main Street 1",
"Ort erstellen": "Create location",
"Anmeldungen": "Registrations",
"weitere Termine": "more dates",
"Venue successfully created!": "Venue successfully created!",
"Create Venue": "Create Venue",
"City": "City",
"Select a city": "Select a city",
"Street": "Street",
"Venue successfully updated!": "Venue successfully updated!",
"Edit Venue": "Edit Venue",
"Update Venue": "Update Venue",
"Venues": "Venues",
"Search venues...": "Search venues...",
"Basic Information": "Basic Information",
"Coordinates": "Coordinates",
"Demographics": "Demographics",
"Venue Information": "Venue Information"
} }

View File

@@ -321,5 +321,57 @@
"Öffnen/RSVP": "Abrir/RSVP", "Öffnen/RSVP": "Abrir/RSVP",
"Über den Dozenten": "Sobre el profesor", "Über den Dozenten": "Sobre el profesor",
"Über den Kurs": "Sobre el curso", "Über den Kurs": "Sobre el curso",
"Über uns": "Sobre nosotros" "Über uns": "Sobre nosotros",
"Diverses": "Varios",
"Orte/Gebiete": "Lugares/Áreas",
"Städte/Gebiete": "Ciudades/Áreas",
"Veranstaltungsorte": "Lugares de eventos",
"Copy": "Copiar",
"Click to connect": "Haz clic para conectar",
"City successfully created!": "¡Ciudad creada exitosamente!",
"Create City": "Crear ciudad",
"Country": "País",
"Select a country": "Seleccionar país",
"Latitude": "Latitud",
"Longitude": "Longitud",
"Population": "Población",
"Population Date": "Fecha de población",
"City successfully updated!": "¡Ciudad actualizada exitosamente!",
"Edit City": "Editar ciudad",
"Update City": "Actualizar ciudad",
"Cities": "Ciudades",
"Search cities...": "Buscar ciudades...",
"Created By": "Creado por",
"Actions": "Acciones",
"Edit": "Editar",
"Wann beginnt das Event?": "¿Cuándo comienza el evento?",
"Endzeit": "Hora de finalización",
"Wann endet das Event?": "¿Cuándo termina el evento?",
"Veranstaltungsort": "Lugar del evento",
"Ort hinzufügen": "Agregar lugar",
"Veranstaltungsort auswählen": "Seleccionar lugar del evento",
"Suche nach Ort...": "Buscar lugar...",
"Link zu weiteren Informationen oder zur Anmeldung": "Enlace para más información o registro",
"Veranstaltungsort hinzufügen": "Agregar lugar de evento",
"Füge einen neuen Veranstaltungsort zur Datenbank hinzu.": "Agrega un nuevo lugar de evento a la base de datos.",
"z.B. Bitcoin Zentrum München": "ej. Centro Bitcoin Múnich",
"Straße": "Calle",
"z.B. Hauptstraße 1": "ej. Calle Principal 1",
"Ort erstellen": "Crear lugar",
"Anmeldungen": "Registros",
"weitere Termine": "más fechas",
"Venue successfully created!": "¡Lugar creado exitosamente!",
"Create Venue": "Crear lugar",
"City": "Ciudad",
"Select a city": "Seleccionar ciudad",
"Street": "Calle",
"Venue successfully updated!": "¡Lugar actualizado exitosamente!",
"Edit Venue": "Editar lugar",
"Update Venue": "Actualizar lugar",
"Venues": "Lugares",
"Search venues...": "Buscar lugares...",
"Basic Information": "Información básica",
"Coordinates": "Coordenadas",
"Demographics": "Demografía",
"Venue Information": "Información del lugar"
} }

View File

@@ -47,9 +47,23 @@
{{ __('Dozenten') }} {{ __('Dozenten') }}
</flux:navlist.item> </flux:navlist.item>
</flux:navlist.group> </flux:navlist.group>
{{--<flux:navlist.group :heading="__('Wallpaper')" class="grid">
</flux:navlist.group>--}} <flux:navlist.group :heading="__('Diverses')" class="grid">
<flux:navlist.group :heading="__('Orte/Gebiete')" expandable :expanded="request()->routeIs('cities.*') || request()->routeIs('venues.*')">
<flux:navlist.item icon="building-office-2" :href="route_with_country('cities.index')"
:current="request()->routeIs('cities.index')"
wire:navigate
badge="{{ \App\Models\City::query()->count() }}">
{{ __('Städte/Gebiete') }}
</flux:navlist.item>
<flux:navlist.item icon="map-pin" :href="route_with_country('venues.index')"
:current="request()->routeIs('venues.index')"
wire:navigate
badge="{{ \App\Models\Venue::query()->count() }}">
{{ __('Veranstaltungsorte') }}
</flux:navlist.item>
</flux:navlist.group>
</flux:navlist.group>
</flux:navlist> </flux:navlist>
<flux:spacer/> <flux:spacer/>

View File

@@ -0,0 +1,88 @@
<?php
use App\Models\City;
use App\Models\Country;
use Livewire\Volt\Component;
new class extends Component {
public string $name = '';
public ?int $country_id = null;
public float $latitude = 0;
public float $longitude = 0;
public ?int $population = null;
public ?string $population_date = null;
public function createCity(): void
{
$validated = $this->validate([
'name' => ['required', 'string', 'max:255', 'unique:cities,name'],
'country_id' => ['required', 'exists:countries,id'],
'latitude' => ['required', 'numeric', 'between:-90,90'],
'longitude' => ['required', 'numeric', 'between:-180,180'],
'population' => ['nullable', 'integer', 'min:0'],
'population_date' => ['nullable', 'string', 'max:255'],
]);
$validated['slug'] = str($validated['name'])->slug();
$validated['created_by'] = auth()->id();
$city = City::create($validated);
session()->flash('status', __('City successfully created!'));
$this->redirect(route_with_country('cities.index'), navigate: true);
}
public function with(): array
{
return [
'countries' => Country::query()->orderBy('name')->get(),
];
}
}; ?>
<div>
<div class="mb-6">
<flux:heading size="xl">{{ __('Create City') }}</flux:heading>
</div>
<form wire:submit="createCity" class="space-y-8">
<flux:fieldset>
<flux:legend>{{ __('Basic Information') }}</flux:legend>
<div class="space-y-6">
<flux:input label="{{ __('Name') }}" wire:model="name" required />
<flux:select label="{{ __('Country') }}" wire:model="country_id" required>
<option value="">{{ __('Select a country') }}</option>
@foreach($countries as $country)
<option value="{{ $country->id }}">{{ $country->name }}</option>
@endforeach
</flux:select>
</div>
</flux:fieldset>
<flux:fieldset>
<flux:legend>{{ __('Coordinates') }}</flux:legend>
<div class="grid grid-cols-2 gap-x-4 gap-y-6">
<flux:input label="{{ __('Latitude') }}" type="number" step="any" wire:model="latitude" required />
<flux:input label="{{ __('Longitude') }}" type="number" step="any" wire:model="longitude" required />
</div>
</flux:fieldset>
<flux:fieldset>
<flux:legend>{{ __('Demographics') }}</flux:legend>
<div class="grid grid-cols-2 gap-x-4 gap-y-6">
<flux:input label="{{ __('Population') }}" type="number" wire:model="population" />
<flux:input label="{{ __('Population Date') }}" wire:model="population_date" placeholder="e.g. 2024" />
</div>
</flux:fieldset>
<div class="flex gap-4">
<flux:button type="submit" variant="primary">{{ __('Create City') }}</flux:button>
<flux:button :href="route_with_country('cities.index')" variant="ghost">{{ __('Cancel') }}</flux:button>
</div>
</form>
</div>

View File

@@ -0,0 +1,99 @@
<?php
use App\Models\City;
use App\Models\Country;
use Livewire\Volt\Component;
new class extends Component {
public City $city;
public string $name = '';
public ?int $country_id = null;
public float $latitude = 0;
public float $longitude = 0;
public ?int $population = null;
public ?string $population_date = null;
public function mount(City $city): void
{
$this->city = $city;
$this->name = $city->name;
$this->country_id = $city->country_id;
$this->latitude = $city->latitude;
$this->longitude = $city->longitude;
$this->population = $city->population;
$this->population_date = $city->population_date;
}
public function updateCity(): void
{
$validated = $this->validate([
'name' => ['required', 'string', 'max:255', 'unique:cities,name,'.$this->city->id],
'country_id' => ['required', 'exists:countries,id'],
'latitude' => ['required', 'numeric', 'between:-90,90'],
'longitude' => ['required', 'numeric', 'between:-180,180'],
'population' => ['nullable', 'integer', 'min:0'],
'population_date' => ['nullable', 'string', 'max:255'],
]);
$validated['slug'] = str($validated['name'])->slug();
$this->city->update($validated);
session()->flash('status', __('City successfully updated!'));
$this->redirect(route_with_country('cities.index'), navigate: true);
}
public function with(): array
{
return [
'countries' => Country::query()->orderBy('name')->get(),
];
}
}; ?>
<div>
<div class="mb-6">
<flux:heading size="xl">{{ __('Edit City') }}: {{ $city->name }}</flux:heading>
</div>
<form wire:submit="updateCity" class="space-y-8">
<flux:fieldset>
<flux:legend>{{ __('Basic Information') }}</flux:legend>
<div class="space-y-6">
<flux:input label="{{ __('Name') }}" wire:model="name" required />
<flux:select label="{{ __('Country') }}" wire:model="country_id" required>
<option value="">{{ __('Select a country') }}</option>
@foreach($countries as $country)
<option value="{{ $country->id }}">{{ $country->name }}</option>
@endforeach
</flux:select>
</div>
</flux:fieldset>
<flux:fieldset>
<flux:legend>{{ __('Coordinates') }}</flux:legend>
<div class="grid grid-cols-2 gap-x-4 gap-y-6">
<flux:input label="{{ __('Latitude') }}" type="number" step="any" wire:model="latitude" required />
<flux:input label="{{ __('Longitude') }}" type="number" step="any" wire:model="longitude" required />
</div>
</flux:fieldset>
<flux:fieldset>
<flux:legend>{{ __('Demographics') }}</flux:legend>
<div class="grid grid-cols-2 gap-x-4 gap-y-6">
<flux:input label="{{ __('Population') }}" type="number" wire:model="population" />
<flux:input label="{{ __('Population Date') }}" wire:model="population_date" placeholder="e.g. 2024" />
</div>
</flux:fieldset>
<div class="flex gap-4">
<flux:button type="submit" variant="primary">{{ __('Update City') }}</flux:button>
<flux:button :href="route_with_country('cities.index')" variant="ghost">{{ __('Cancel') }}</flux:button>
</div>
</form>
</div>

View File

@@ -0,0 +1,94 @@
<?php
use App\Models\City;
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 [
'cities' => City::with(['country', 'createdBy'])
->when($this->search, fn($query)
=> $query->where('name', 'ilike', '%'.$this->search.'%'),
)
->orderBy('name')
->paginate(15),
];
}
}; ?>
<div>
<div class="flex items-center justify-between mb-6">
<flux:heading size="xl">{{ __('Cities') }}</flux:heading>
<div class="flex items-center gap-4">
<flux:input
wire:model.live="search"
:placeholder="__('Search cities...')"
clearable
/>
@auth
<flux:button class="cursor-pointer" :href="route_with_country('cities.create')" icon="plus" variant="primary">
{{ __('Create City') }}
</flux:button>
@endauth
</div>
</div>
<flux:table :paginate="$cities" class="mt-6">
<flux:table.columns>
<flux:table.column>{{ __('Name') }}</flux:table.column>
<flux:table.column>{{ __('Country') }}</flux:table.column>
<flux:table.column>{{ __('Population') }}</flux:table.column>
<flux:table.column>{{ __('Created By') }}</flux:table.column>
<flux:table.column>{{ __('Actions') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@foreach ($cities as $city)
<flux:table.row :key="$city->id">
<flux:table.cell variant="strong">
{{ $city->name }}
</flux:table.cell>
<flux:table.cell>
@if($city->country)
{{ $city->country->name }}
@endif
</flux:table.cell>
<flux:table.cell>
@if($city->population)
{{ number_format($city->population) }}
@if($city->population_date)
<span class="text-xs text-zinc-500">({{ $city->population_date }})</span>
@endif
@endif
</flux:table.cell>
<flux:table.cell>
@if($city->createdBy)
{{ $city->createdBy->name }}
@endif
</flux:table.cell>
<flux:table.cell>
<div class="flex gap-2">
@auth
<flux:button size="sm" :href="route('cities.edit',['city' => $city, 'country' => $country])" icon="pencil">
{{ __('Edit') }}
</flux:button>
@endauth
</div>
</flux:table.cell>
</flux:table.row>
@endforeach
</flux:table.rows>
</flux:table>
</div>

View File

@@ -0,0 +1,71 @@
<?php
use App\Models\Venue;
use App\Models\City;
use Livewire\Volt\Component;
new class extends Component {
public string $name = '';
public ?int $city_id = null;
public string $street = '';
public function createVenue(): void
{
$validated = $this->validate([
'name' => ['required', 'string', 'max:255', 'unique:venues,name'],
'city_id' => ['required', 'exists:cities,id'],
'street' => ['required', 'string', 'max:255'],
]);
$validated['slug'] = str($validated['name'])->slug();
$validated['created_by'] = auth()->id();
$venue = Venue::create($validated);
session()->flash('status', __('Venue successfully created!'));
$this->redirect(route_with_country('venues.index'), navigate: true);
}
public function with(): array
{
return [
'cities' => City::query()->with('country')->orderBy('name')->get(),
];
}
}; ?>
<div>
<div class="mb-6">
<flux:heading size="xl">{{ __('Create Venue') }}</flux:heading>
</div>
<form wire:submit="createVenue" class="space-y-8">
<flux:fieldset>
<flux:legend>{{ __('Venue Information') }}</flux:legend>
<div class="space-y-6">
<flux:input label="{{ __('Name') }}" wire:model="name" required />
<flux:select label="{{ __('City') }}" wire:model="city_id" required>
<option value="">{{ __('Select a city') }}</option>
@foreach($cities as $city)
<option value="{{ $city->id }}">
{{ $city->name }}
@if($city->country)
({{ $city->country->name }})
@endif
</option>
@endforeach
</flux:select>
<flux:input label="{{ __('Street') }}" wire:model="street" required />
</div>
</flux:fieldset>
<div class="flex gap-4">
<flux:button type="submit" variant="primary">{{ __('Create Venue') }}</flux:button>
<flux:button :href="route_with_country('venues.index')" variant="ghost">{{ __('Cancel') }}</flux:button>
</div>
</form>
</div>

View File

@@ -0,0 +1,79 @@
<?php
use App\Models\Venue;
use App\Models\City;
use Livewire\Volt\Component;
new class extends Component {
public Venue $venue;
public string $name = '';
public ?int $city_id = null;
public string $street = '';
public function mount(Venue $venue): void
{
$this->venue = $venue;
$this->name = $venue->name;
$this->city_id = $venue->city_id;
$this->street = $venue->street;
}
public function updateVenue(): void
{
$validated = $this->validate([
'name' => ['required', 'string', 'max:255', 'unique:venues,name,'.$this->venue->id],
'city_id' => ['required', 'exists:cities,id'],
'street' => ['required', 'string', 'max:255'],
]);
$validated['slug'] = str($validated['name'])->slug();
$this->venue->update($validated);
session()->flash('status', __('Venue successfully updated!'));
$this->redirect(route_with_country('venues.index'), navigate: true);
}
public function with(): array
{
return [
'cities' => City::query()->with('country')->orderBy('name')->get(),
];
}
}; ?>
<div>
<div class="mb-6">
<flux:heading size="xl">{{ __('Edit Venue') }}: {{ $venue->name }}</flux:heading>
</div>
<form wire:submit="updateVenue" class="space-y-8">
<flux:fieldset>
<flux:legend>{{ __('Venue Information') }}</flux:legend>
<div class="space-y-6">
<flux:input label="{{ __('Name') }}" wire:model="name" required />
<flux:select label="{{ __('City') }}" wire:model="city_id" required>
<option value="">{{ __('Select a city') }}</option>
@foreach($cities as $city)
<option value="{{ $city->id }}">
{{ $city->name }}
@if($city->country)
({{ $city->country->name }})
@endif
</option>
@endforeach
</flux:select>
<flux:input label="{{ __('Street') }}" wire:model="street" required />
</div>
</flux:fieldset>
<div class="flex gap-4">
<flux:button type="submit" variant="primary">{{ __('Update Venue') }}</flux:button>
<flux:button :href="route_with_country('venues.index')" variant="ghost">{{ __('Cancel') }}</flux:button>
</div>
</form>
</div>

View File

@@ -0,0 +1,88 @@
<?php
use App\Models\Venue;
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 [
'venues' => Venue::with(['city.country', 'createdBy'])
->when($this->search, fn($query)
=> $query->where('name', 'ilike', '%'.$this->search.'%'),
)
->orderBy('name')
->paginate(15),
];
}
}; ?>
<div>
<div class="flex items-center justify-between mb-6">
<flux:heading size="xl">{{ __('Venues') }}</flux:heading>
<div class="flex items-center gap-4">
<flux:input
wire:model.live="search"
:placeholder="__('Search venues...')"
clearable
/>
@auth
<flux:button class="cursor-pointer" :href="route_with_country('venues.create')" icon="plus" variant="primary">
{{ __('Create Venue') }}
</flux:button>
@endauth
</div>
</div>
<flux:table :paginate="$venues" class="mt-6">
<flux:table.columns>
<flux:table.column>{{ __('Name') }}</flux:table.column>
<flux:table.column>{{ __('City') }}</flux:table.column>
<flux:table.column>{{ __('Created By') }}</flux:table.column>
<flux:table.column>{{ __('Actions') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@foreach ($venues as $venue)
<flux:table.row :key="$venue->id">
<flux:table.cell variant="strong">
{{ $venue->name }}
</flux:table.cell>
<flux:table.cell>
@if($venue->city)
{{ $venue->city->name }}
@if($venue->city->country)
<span class="text-xs text-zinc-500">({{ $venue->city->country->name }})</span>
@endif
@endif
</flux:table.cell>
<flux:table.cell>
@if($venue->createdBy)
{{ $venue->createdBy->name }}
@endif
</flux:table.cell>
<flux:table.cell>
<div class="flex gap-2">
@auth
<flux:button size="sm" :href="route('venues.edit', ['venue' => $venue, 'country' => $country])" icon="pencil">
{{ __('Edit') }}
</flux:button>
@endauth
</div>
</flux:table.cell>
</flux:table.row>
@endforeach
</flux:table.rows>
</flux:table>
</div>

View File

@@ -42,6 +42,9 @@ Route::middleware([])
Volt::route('course/{course}/event/{event}', 'courses.landingpage-event')->name('courses.landingpage-event'); Volt::route('course/{course}/event/{event}', 'courses.landingpage-event')->name('courses.landingpage-event');
Volt::route('lecturers', 'lecturers.index')->name('lecturers.index'); Volt::route('lecturers', 'lecturers.index')->name('lecturers.index');
Volt::route('cities', 'cities.index')->name('cities.index');
Volt::route('venues', 'venues.index')->name('venues.index');
}); });
Route::middleware(['auth']) Route::middleware(['auth'])
@@ -60,6 +63,12 @@ Route::middleware(['auth'])
Volt::route('lecturer-create', 'lecturers.create')->name('lecturers.create'); Volt::route('lecturer-create', 'lecturers.create')->name('lecturers.create');
Volt::route('lecturer-edit/{lecturer}', 'lecturers.edit')->name('lecturers.edit'); Volt::route('lecturer-edit/{lecturer}', 'lecturers.edit')->name('lecturers.edit');
Volt::route('city-create', 'cities.create')->name('cities.create');
Volt::route('city-edit/{city}', 'cities.edit')->name('cities.edit');
Volt::route('venue-create', 'venues.create')->name('venues.create');
Volt::route('venue-edit/{venue}', 'venues.edit')->name('venues.edit');
}); });
Route::middleware(['auth']) Route::middleware(['auth'])