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" />--}}
|
||||
|
||||
<!-- 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 -->
|
||||
|
||||
@@ -73,6 +73,7 @@ new class extends Component {
|
||||
<flux:separator class="my-4"/>
|
||||
<div class="space-y-3">
|
||||
@foreach($myUpcomingEvents as $event)
|
||||
<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>
|
||||
@@ -84,6 +85,7 @@ new class extends Component {
|
||||
</flux:badge>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
|
||||
@@ -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>
|
||||
@@ -45,7 +164,8 @@ new class extends Component {
|
||||
<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>
|
||||
|
||||
@@ -78,29 +198,103 @@ new class extends Component {
|
||||
</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,7 +304,9 @@ 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: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>
|
||||
|
||||
Reference in New Issue
Block a user