mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-app.git
synced 2025-12-14 12:06:46 +00:00
✨ Enhance RSVP and attendee management for meetup events
This commit is contained in:
@@ -172,7 +172,7 @@ class extends Component {
|
|||||||
{{--<flux:checkbox wire:model="remember" label="Remember me for 30 days" />--}}
|
{{--<flux:checkbox wire:model="remember" label="Remember me for 30 days" />--}}
|
||||||
|
|
||||||
<!-- Submit Button -->
|
<!-- Submit Button -->
|
||||||
<flux:button class="cursor-pointer" variant="primary" @click="openNostrLogin" class="w-full">{{ __('Log in mit Nostr') }}</flux:button>
|
<flux:button variant="primary" @click="openNostrLogin" class="w-full cursor-pointer">{{ __('Log in mit Nostr') }}</flux:button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sign up Link -->
|
<!-- Sign up Link -->
|
||||||
|
|||||||
@@ -73,17 +73,19 @@ new class extends Component {
|
|||||||
<flux:separator class="my-4"/>
|
<flux:separator class="my-4"/>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
@foreach($myUpcomingEvents as $event)
|
@foreach($myUpcomingEvents as $event)
|
||||||
<div class="flex items-start justify-between gap-3">
|
<a href="{{ route('meetups.landingpage-event', ['meetup' => $event->meetup->slug, 'event' => $event->id, 'country' => $event->meetup->city->country->code]) }}" class="block hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-lg p-3 -m-3 transition-colors">
|
||||||
<div class="flex-1">
|
<div class="flex items-start justify-between gap-3">
|
||||||
<div class="font-medium">{{ $event->meetup->name }}</div>
|
<div class="flex-1">
|
||||||
<div class="text-sm text-zinc-500">
|
<div class="font-medium">{{ $event->meetup->name }}</div>
|
||||||
{{ $event->meetup->city->name }}, {{ $event->meetup->city->country->name }}
|
<div class="text-sm text-zinc-500">
|
||||||
|
{{ $event->meetup->city->name }}, {{ $event->meetup->city->country->name }}
|
||||||
|
</div>
|
||||||
|
<flux:badge color="green" size="sm" class="mt-1">
|
||||||
|
{{ $event->start->format('d.m.Y H:i') }}
|
||||||
|
</flux:badge>
|
||||||
</div>
|
</div>
|
||||||
<flux:badge color="green" size="sm" class="mt-1">
|
|
||||||
{{ $event->start->format('d.m.Y H:i') }}
|
|
||||||
</flux:badge>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
|
|||||||
@@ -1,15 +1,27 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Models\MeetupEvent;
|
use App\Models\MeetupEvent;
|
||||||
|
use App\Models\User;
|
||||||
|
use Livewire\Attributes\Validate;
|
||||||
use Livewire\Volt\Component;
|
use Livewire\Volt\Component;
|
||||||
|
|
||||||
new class extends Component {
|
new class extends Component {
|
||||||
public MeetupEvent $event;
|
public MeetupEvent $event;
|
||||||
public $country = 'de';
|
public $country = 'de';
|
||||||
|
|
||||||
|
#[Validate('required|min:2')]
|
||||||
|
public string $name = '';
|
||||||
|
|
||||||
|
public bool $willShowUp = false;
|
||||||
|
public bool $perhapsShowUp = false;
|
||||||
|
public array $attendees = [];
|
||||||
|
public array $mightAttendees = [];
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
{
|
{
|
||||||
$this->country = request()->route('country');
|
$this->country = request()->route('country');
|
||||||
|
$this->name = auth()->user()->name ?? '';
|
||||||
|
$this->loadAttendees();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function with(): array
|
public function with(): array
|
||||||
@@ -18,13 +30,120 @@ new class extends Component {
|
|||||||
'event' => $this->event->load('meetup'),
|
'event' => $this->event->load('meetup'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getUserIdentifier(): string
|
||||||
|
{
|
||||||
|
return auth()->check()
|
||||||
|
? 'id_'.auth()->id()
|
||||||
|
: 'anon_'.session()->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadAttendees(): void
|
||||||
|
{
|
||||||
|
$identifier = $this->getUserIdentifier();
|
||||||
|
$attendees = collect($this->event->attendees ?? []);
|
||||||
|
$mightAttendees = collect($this->event->might_attendees ?? []);
|
||||||
|
|
||||||
|
// Check if user is in attendees
|
||||||
|
$attendeeEntry = $attendees->first(fn($v) => str($v)->startsWith($identifier));
|
||||||
|
if ($attendeeEntry) {
|
||||||
|
$this->name = str($attendeeEntry)->after('|')->toString();
|
||||||
|
$this->willShowUp = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is in might_attendees
|
||||||
|
$mightAttendeeEntry = $mightAttendees->first(fn($v) => str($v)->startsWith($identifier));
|
||||||
|
if ($mightAttendeeEntry) {
|
||||||
|
$this->name = str($mightAttendeeEntry)->after('|')->toString();
|
||||||
|
$this->perhapsShowUp = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->attendees = $this->mapAttendees($attendees);
|
||||||
|
$this->mightAttendees = $this->mapAttendees($mightAttendees);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mapAttendees($collection): array
|
||||||
|
{
|
||||||
|
return $collection->map(function ($value) {
|
||||||
|
$isAnon = str($value)->contains('anon_');
|
||||||
|
$id = $isAnon ? -1 : str($value)->before('|')->after('id_')->toInteger();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $id,
|
||||||
|
'user' => $id > 0 ? User::query()
|
||||||
|
->select(['id', 'name', 'profile_photo_path'])
|
||||||
|
->find($id)
|
||||||
|
?->append('profile_photo_url')
|
||||||
|
->toArray() : null,
|
||||||
|
'name' => str($value)->after('|')->toString(),
|
||||||
|
];
|
||||||
|
})->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attend(): void
|
||||||
|
{
|
||||||
|
$this->validate();
|
||||||
|
$this->removeFromLists();
|
||||||
|
|
||||||
|
$attendees = collect($this->event->attendees ?? []);
|
||||||
|
$entry = $this->getUserIdentifier().'|'.$this->name;
|
||||||
|
|
||||||
|
if (!$attendees->contains($entry)) {
|
||||||
|
$attendees->push($entry);
|
||||||
|
$this->event->update(['attendees' => $attendees->toArray()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->loadAttendees();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mightAttend(): void
|
||||||
|
{
|
||||||
|
$this->validate();
|
||||||
|
$this->removeFromLists();
|
||||||
|
|
||||||
|
$mightAttendees = collect($this->event->might_attendees ?? []);
|
||||||
|
$entry = $this->getUserIdentifier().'|'.$this->name;
|
||||||
|
|
||||||
|
if (!$mightAttendees->contains($entry)) {
|
||||||
|
$mightAttendees->push($entry);
|
||||||
|
$this->event->update(['might_attendees' => $mightAttendees->toArray()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->loadAttendees();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cannotCome(): void
|
||||||
|
{
|
||||||
|
$this->removeFromLists();
|
||||||
|
$this->loadAttendees();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function removeFromLists(): void
|
||||||
|
{
|
||||||
|
$identifier = $this->getUserIdentifier();
|
||||||
|
|
||||||
|
$attendees = collect($this->event->attendees ?? [])
|
||||||
|
->reject(fn($v) => str($v)->startsWith($identifier));
|
||||||
|
|
||||||
|
$mightAttendees = collect($this->event->might_attendees ?? [])
|
||||||
|
->reject(fn($v) => str($v)->startsWith($identifier));
|
||||||
|
|
||||||
|
$this->event->update([
|
||||||
|
'attendees' => $attendees->toArray(),
|
||||||
|
'might_attendees' => $mightAttendees->toArray(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->willShowUp = false;
|
||||||
|
$this->perhapsShowUp = false;
|
||||||
|
}
|
||||||
}; ?>
|
}; ?>
|
||||||
|
|
||||||
<div class="container mx-auto px-4 py-8">
|
<div class="container mx-auto px-4 py-8">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<flux:text class="text-sm text-zinc-600 dark:text-zinc-400">
|
<flux:text class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
<a href="{{ route('meetups.landingpage', ['meetup' => $event->meetup->slug, 'country' => $country]) }}" class="hover:underline">
|
<a href="{{ route('meetups.landingpage', ['meetup' => $event->meetup->slug, 'country' => $country]) }}"
|
||||||
|
class="hover:underline">
|
||||||
{{ $event->meetup->name }}
|
{{ $event->meetup->name }}
|
||||||
</a>
|
</a>
|
||||||
<span class="mx-2">/</span>
|
<span class="mx-2">/</span>
|
||||||
@@ -35,24 +154,25 @@ new class extends Component {
|
|||||||
<!-- Event Details -->
|
<!-- Event Details -->
|
||||||
<flux:card class="max-w-3xl">
|
<flux:card class="max-w-3xl">
|
||||||
<flux:heading size="xl" class="mb-4">
|
<flux:heading size="xl" class="mb-4">
|
||||||
<flux:icon.calendar class="inline w-6 h-6 mr-2" />
|
<flux:icon.calendar class="inline w-6 h-6 mr-2"/>
|
||||||
{{ $event->start->format('d.m.Y') }}
|
{{ $event->start->format('d.m.Y') }}
|
||||||
</flux:heading>
|
</flux:heading>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<!-- Date and Time -->
|
<!-- Date and Time -->
|
||||||
<div class="flex items-center text-zinc-700 dark:text-zinc-300">
|
<div class="flex items-center text-zinc-700 dark:text-zinc-300">
|
||||||
<flux:icon.clock class="w-5 h-5 mr-3" />
|
<flux:icon.clock class="w-5 h-5 mr-3"/>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-semibold">{{ $event->start->format('H:i') }} Uhr</div>
|
<div class="font-semibold">{{ $event->start->format('H:i') }} Uhr</div>
|
||||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">{{ $event->start->isoFormat('dddd, D. MMMM YYYY') }}</div>
|
<div
|
||||||
|
class="text-sm text-zinc-600 dark:text-zinc-400">{{ $event->start->isoFormat('dddd, D. MMMM YYYY') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Location -->
|
<!-- Location -->
|
||||||
@if($event->location)
|
@if($event->location)
|
||||||
<div class="flex items-center text-zinc-700 dark:text-zinc-300">
|
<div class="flex items-center text-zinc-700 dark:text-zinc-300">
|
||||||
<flux:icon.map-pin class="w-5 h-5 mr-3" />
|
<flux:icon.map-pin class="w-5 h-5 mr-3"/>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-semibold">{{ __('Ort') }}</div>
|
<div class="font-semibold">{{ __('Ort') }}</div>
|
||||||
<div class="text-sm">{{ $event->location }}</div>
|
<div class="text-sm">{{ $event->location }}</div>
|
||||||
@@ -72,35 +192,109 @@ new class extends Component {
|
|||||||
@if($event->link)
|
@if($event->link)
|
||||||
<div class="pt-4">
|
<div class="pt-4">
|
||||||
<flux:button href="{{ $event->link }}" target="_blank" variant="primary">
|
<flux:button href="{{ $event->link }}" target="_blank" variant="primary">
|
||||||
<flux:icon.arrow-top-right-on-square class="w-5 h-5 mr-2" />
|
<flux:icon.arrow-top-right-on-square class="w-5 h-5 mr-2"/>
|
||||||
{{ __('Mehr Informationen') }}
|
{{ __('Mehr Informationen') }}
|
||||||
</flux:button>
|
</flux:button>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
<!-- RSVP Section -->
|
||||||
|
<div class="pt-4 border-t border-zinc-200 dark:border-zinc-700">
|
||||||
|
<flux:heading size="lg" class="mb-4">{{ __('Teilnahme') }}</flux:heading>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
|
||||||
|
@if(!auth()->check())
|
||||||
|
<flux:callout variant="warning" icon="exclamation-triangle" inline>
|
||||||
|
<flux:callout.heading>{{ __('Du bist nicht eingloggt und musst deshalb den Namen selbst eintippen.') }}</flux:callout.heading>
|
||||||
|
<x-slot name="actions">
|
||||||
|
<flux:button :href="route('login')">{{ __('Log in') }}</flux:button>
|
||||||
|
</x-slot>
|
||||||
|
</flux:callout>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<!-- Name Input -->
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>{{ __('Dein Name') }}</flux:label>
|
||||||
|
<flux:input wire:model="name" type="text" placeholder="{{ __('Name eingeben') }}"/>
|
||||||
|
@error('name')
|
||||||
|
<flux:error>{{ $message }}</flux:error>
|
||||||
|
@enderror
|
||||||
|
</flux:field>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<flux:button
|
||||||
|
class="cursor-pointer"
|
||||||
|
icon="check"
|
||||||
|
wire:click="attend"
|
||||||
|
variant="{{ $willShowUp ? 'primary' : 'outline' }}"
|
||||||
|
>
|
||||||
|
{{ __('Ich komme') }}
|
||||||
|
</flux:button>
|
||||||
|
|
||||||
|
<flux:button
|
||||||
|
class="cursor-pointer"
|
||||||
|
icon="question-mark-circle"
|
||||||
|
wire:click="mightAttend"
|
||||||
|
variant="{{ $perhapsShowUp ? 'primary' : 'outline' }}"
|
||||||
|
>
|
||||||
|
{{ __('Vielleicht') }}
|
||||||
|
</flux:button>
|
||||||
|
|
||||||
|
@if($willShowUp || $perhapsShowUp)
|
||||||
|
<flux:button
|
||||||
|
class="cursor-pointer"
|
||||||
|
icon="x-mark"
|
||||||
|
wire:click="cannotCome"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
{{ __('Absagen') }}
|
||||||
|
</flux:button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Attendees -->
|
<!-- Attendees -->
|
||||||
@if($event->attendees && count($event->attendees) > 0)
|
@if(count($attendees) > 0)
|
||||||
<div class="pt-4 border-t border-zinc-200 dark:border-zinc-700">
|
<div class="pt-4 border-t border-zinc-200 dark:border-zinc-700">
|
||||||
<flux:heading size="lg" class="mb-2">
|
<flux:heading size="lg" class="mb-2">
|
||||||
{{ __('Zusagen') }} ({{ count($event->attendees) }})
|
{{ __('Zusagen') }} ({{ count($attendees) }})
|
||||||
</flux:heading>
|
</flux:heading>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
@foreach($event->attendees as $attendee)
|
@foreach($attendees as $attendee)
|
||||||
<flux:badge>{{ $attendee }}</flux:badge>
|
@if($attendee['user'])
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2 px-3 py-1.5 bg-zinc-100 dark:bg-zinc-800 rounded-full">
|
||||||
|
<flux:avatar size="xs" :src="$attendee['user']['profile_photo_url']"/>
|
||||||
|
<span class="text-sm">{{ $attendee['name'] }}</span>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<flux:badge>{{ $attendee['name'] }}</flux:badge>
|
||||||
|
@endif
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<!-- Might Attend -->
|
<!-- Might Attend -->
|
||||||
@if($event->might_attendees && count($event->might_attendees) > 0)
|
@if(count($mightAttendees) > 0)
|
||||||
<div class="pt-4 border-t border-zinc-200 dark:border-zinc-700">
|
<div class="pt-4 border-t border-zinc-200 dark:border-zinc-700">
|
||||||
<flux:heading size="lg" class="mb-2">
|
<flux:heading size="lg" class="mb-2">
|
||||||
{{ __('Vielleicht') }} ({{ count($event->might_attendees) }})
|
{{ __('Vielleicht') }} ({{ count($mightAttendees) }})
|
||||||
</flux:heading>
|
</flux:heading>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
@foreach($event->might_attendees as $attendee)
|
@foreach($mightAttendees as $attendee)
|
||||||
<flux:badge variant="outline">{{ $attendee }}</flux:badge>
|
@if($attendee['user'])
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2 px-3 py-1.5 bg-zinc-100 dark:bg-zinc-800 rounded-full">
|
||||||
|
<flux:avatar size="xs" :src="$attendee['user']['profile_photo_url']"/>
|
||||||
|
<span class="text-sm">{{ $attendee['name'] }}</span>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<flux:badge variant="outline">{{ $attendee['name'] }}</flux:badge>
|
||||||
|
@endif
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,8 +304,10 @@ new class extends Component {
|
|||||||
|
|
||||||
<!-- Back Button -->
|
<!-- Back Button -->
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<flux:button href="{{ route('meetups.landingpage', ['meetup' => $event->meetup->slug, 'country' => $country]) }}" variant="ghost">
|
<flux:button
|
||||||
<flux:icon.arrow-left class="w-5 h-5 mr-2" />
|
href="{{ route('meetups.landingpage', ['meetup' => $event->meetup->slug, 'country' => $country]) }}"
|
||||||
|
variant="ghost">
|
||||||
|
<flux:icon.arrow-left class="w-5 h-5 mr-2"/>
|
||||||
{{ __('Zurück zum Meetup') }}
|
{{ __('Zurück zum Meetup') }}
|
||||||
</flux:button>
|
</flux:button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user