Enhance RSVP and attendee management for meetup events

This commit is contained in:
HolgerHatGarKeineNode
2025-11-21 12:31:32 +01:00
parent 7f9c42994c
commit 0800213e80
3 changed files with 224 additions and 26 deletions

View File

@@ -172,7 +172,7 @@ class extends Component {
{{--<flux:checkbox wire:model="remember" label="Remember me for 30 days" />--}}
<!-- 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>
<!-- Sign up Link -->

View File

@@ -73,17 +73,19 @@ new class extends Component {
<flux:separator class="my-4"/>
<div class="space-y-3">
@foreach($myUpcomingEvents as $event)
<div class="flex items-start justify-between gap-3">
<div class="flex-1">
<div class="font-medium">{{ $event->meetup->name }}</div>
<div class="text-sm text-zinc-500">
{{ $event->meetup->city->name }}, {{ $event->meetup->city->country->name }}
<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 items-start justify-between gap-3">
<div class="flex-1">
<div class="font-medium">{{ $event->meetup->name }}</div>
<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>
<flux:badge color="green" size="sm" class="mt-1">
{{ $event->start->format('d.m.Y H:i') }}
</flux:badge>
</div>
</div>
</a>
@endforeach
</div>
@else

View File

@@ -1,15 +1,27 @@
<?php
use App\Models\MeetupEvent;
use App\Models\User;
use Livewire\Attributes\Validate;
use Livewire\Volt\Component;
new class extends Component {
public MeetupEvent $event;
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
{
$this->country = request()->route('country');
$this->name = auth()->user()->name ?? '';
$this->loadAttendees();
}
public function with(): array
@@ -18,13 +30,120 @@ new class extends Component {
'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">
<!-- Breadcrumb -->
<div class="mb-6">
<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 }}
</a>
<span class="mx-2">/</span>
@@ -35,24 +154,25 @@ new class extends Component {
<!-- Event Details -->
<flux:card class="max-w-3xl">
<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') }}
</flux:heading>
<div class="space-y-4">
<!-- Date and Time -->
<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 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>
<!-- Location -->
@if($event->location)
<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 class="font-semibold">{{ __('Ort') }}</div>
<div class="text-sm">{{ $event->location }}</div>
@@ -72,35 +192,109 @@ new class extends Component {
@if($event->link)
<div class="pt-4">
<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') }}
</flux:button>
</div>
@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 -->
@if($event->attendees && count($event->attendees) > 0)
@if(count($attendees) > 0)
<div class="pt-4 border-t border-zinc-200 dark:border-zinc-700">
<flux:heading size="lg" class="mb-2">
{{ __('Zusagen') }} ({{ count($event->attendees) }})
{{ __('Zusagen') }} ({{ count($attendees) }})
</flux:heading>
<div class="flex flex-wrap gap-2">
@foreach($event->attendees as $attendee)
<flux:badge>{{ $attendee }}</flux:badge>
@foreach($attendees as $attendee)
@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
</div>
</div>
@endif
<!-- 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">
<flux:heading size="lg" class="mb-2">
{{ __('Vielleicht') }} ({{ count($event->might_attendees) }})
{{ __('Vielleicht') }} ({{ count($mightAttendees) }})
</flux:heading>
<div class="flex flex-wrap gap-2">
@foreach($event->might_attendees as $attendee)
<flux:badge variant="outline">{{ $attendee }}</flux:badge>
@foreach($mightAttendees as $attendee)
@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
</div>
</div>
@@ -110,8 +304,10 @@ new class extends Component {
<!-- Back Button -->
<div class="mt-6">
<flux:button href="{{ route('meetups.landingpage', ['meetup' => $event->meetup->slug, 'country' => $country]) }}" variant="ghost">
<flux:icon.arrow-left class="w-5 h-5 mr-2" />
<flux:button
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') }}
</flux:button>
</div>