mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-app.git
synced 2025-12-15 12:16:47 +00:00
- 🛠️ Replaced inline dashboard layout with Livewire component for better reusability and management.
- 🔒 Introduced Nostr-based login functionality with `nostr-tools` integration. - 🖼️ Added user profile photo handling (upload, delete, and URL retrieval) in the `User` model. - 💻 Updated views to use `flux:avatar` for consistent user avatars. - ✂️ Removed unused routes and adjusted dashboard routing logic. - 📦 Updated dependencies in `package.json` and `yarn.lock`.
This commit is contained in:
@@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Session;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Volt\Component;
|
||||
use Flux\Flux;
|
||||
@@ -22,18 +23,16 @@ class extends Component {
|
||||
|
||||
public bool $remember = false;
|
||||
|
||||
/**
|
||||
* Handle an incoming authentication request.
|
||||
*/
|
||||
public function login(): void
|
||||
#[On('nostrLoggedIn')]
|
||||
public function loginListener($pubkey): void
|
||||
{
|
||||
if (app()->environment('production')) {
|
||||
Flux::toast(text: 'Login work in progress', variant: 'danger');
|
||||
$user = \App\Models\User::query()->where('nostr', $pubkey)->first();
|
||||
if ($user) {
|
||||
Auth::loginUsingId($user->id);
|
||||
Session::regenerate();
|
||||
$this->redirectIntended(default: route_with_country('dashboard', absolute: false), navigate: true);
|
||||
return;
|
||||
}
|
||||
Auth::loginUsingId(1, true);
|
||||
Session::regenerate();
|
||||
$this->redirectIntended(default: route_with_country('dashboard', absolute: false), navigate: true);
|
||||
return;
|
||||
|
||||
$this->validate();
|
||||
@@ -84,14 +83,15 @@ class extends Component {
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="flex min-h-screen">
|
||||
<div class="flex min-h-screen" x-data="nostrLogin">
|
||||
<div class="flex-1 flex justify-center items-center">
|
||||
<div class="w-80 max-w-80 space-y-6">
|
||||
<!-- Logo -->
|
||||
<div class="flex justify-center">
|
||||
<a href="/" class="group flex items-center gap-3">
|
||||
<div>
|
||||
<flux:avatar class="[:where(&)]:size-32 [:where(&)]:text-base" size="xl" src="{{ asset('img/einundzwanzig-square.svg') }}" />
|
||||
<flux:avatar class="[:where(&)]:size-32 [:where(&)]:text-base" size="xl"
|
||||
src="{{ asset('img/einundzwanzig-square.svg') }}"/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@@ -134,10 +134,10 @@ class extends Component {
|
||||
{{--<flux:separator text="or" />--}}
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="text-center" :status="session('status')" />
|
||||
<x-auth-session-status class="text-center" :status="session('status')"/>
|
||||
|
||||
<!-- Login Form -->
|
||||
<form wire:submit="login" class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-6">
|
||||
<!-- Email Input -->
|
||||
{{--<flux:input
|
||||
wire:model="email"
|
||||
@@ -172,8 +172,8 @@ class extends Component {
|
||||
{{--<flux:checkbox wire:model="remember" label="Remember me for 30 days" />--}}
|
||||
|
||||
<!-- Submit Button -->
|
||||
<flux:button variant="primary" type="submit" class="w-full">Log in</flux:button>
|
||||
</form>
|
||||
<flux:button class="cursor-pointer" variant="primary" @click="openNostrLogin" class="w-full">{{ __('Log in mit Nostr') }}</flux:button>
|
||||
</div>
|
||||
|
||||
<!-- Sign up Link -->
|
||||
{{--@if (Route::has('register'))
|
||||
@@ -186,7 +186,8 @@ class extends Component {
|
||||
|
||||
<!-- Right Side Panel -->
|
||||
<div class="flex-1 p-4 max-lg:hidden">
|
||||
<div class="text-white relative rounded-lg h-full w-full bg-zinc-900 flex flex-col items-start justify-end p-16" style="background-image: url('https://dergigi.com/assets/images/bitcoin-is-time.jpg'); background-size: cover">
|
||||
<div class="text-white relative rounded-lg h-full w-full bg-zinc-900 flex flex-col items-start justify-end p-16"
|
||||
style="background-image: url('https://dergigi.com/assets/images/bitcoin-is-time.jpg'); background-size: cover">
|
||||
|
||||
<!-- Testimonial -->
|
||||
<div class="mb-6 italic font-base text-3xl xl:text-4xl">
|
||||
@@ -195,7 +196,7 @@ class extends Component {
|
||||
|
||||
<!-- Author Info -->
|
||||
<div class="flex gap-4">
|
||||
<flux:avatar src="https://dergigi.com/assets/images/avatar.jpg" size="xl" />
|
||||
<flux:avatar src="https://dergigi.com/assets/images/avatar.jpg" size="xl"/>
|
||||
<div class="flex flex-col justify-center font-medium">
|
||||
<div class="text-lg">Gigi</div>
|
||||
<div class="text-zinc-300">bitcoiner and software engineer</div>
|
||||
|
||||
174
resources/views/livewire/dashboard.blade.php
Normal file
174
resources/views/livewire/dashboard.blade.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Meetup;
|
||||
use App\Models\MeetupEvent;
|
||||
use Livewire\Volt\Component;
|
||||
use Flux\Flux;
|
||||
|
||||
new class extends Component {
|
||||
public $selectedMeetupId = null;
|
||||
|
||||
public function addMeetup()
|
||||
{
|
||||
if ($this->selectedMeetupId) {
|
||||
$user = auth()->user();
|
||||
|
||||
// Prüfen ob bereits zugeordnet
|
||||
if (!$user->meetups()->where('meetup_id', $this->selectedMeetupId)->exists()) {
|
||||
$user->meetups()->attach($this->selectedMeetupId);
|
||||
}
|
||||
|
||||
$this->selectedMeetupId = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function removeMeetup($meetupId)
|
||||
{
|
||||
auth()->user()->meetups()->detach($meetupId);
|
||||
Flux::modals()->close();
|
||||
$this->reset('selectedMeetupId');
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
// Meine Meetups
|
||||
$myMeetups = $user->meetups()
|
||||
->with(['city.country'])
|
||||
->get();
|
||||
|
||||
// Alle verfügbaren Meetups (außer die bereits zugeordneten)
|
||||
$availableMeetups = Meetup::with(['city.country'])
|
||||
->whereNotIn('id', $myMeetups->pluck('id'))
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
// Meine nächsten Meetup Termine
|
||||
$myUpcomingEvents = MeetupEvent::whereHas('meetup', function($query) use ($user) {
|
||||
$query->whereHas('users', function($q) use ($user) {
|
||||
$q->where('users.id', $user->id);
|
||||
});
|
||||
})
|
||||
->where('start', '>=', now())
|
||||
->with(['meetup.city.country'])
|
||||
->orderBy('start')
|
||||
->limit(5)
|
||||
->get();
|
||||
|
||||
return [
|
||||
'myMeetups' => $myMeetups,
|
||||
'availableMeetups' => $availableMeetups,
|
||||
'myUpcomingEvents' => $myUpcomingEvents,
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="flex h-full w-full flex-1 flex-col gap-4 rounded-xl">
|
||||
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||
<div class="relative overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
|
||||
<div class="p-6">
|
||||
<flux:heading size="lg" class="mb-4">{{ __('Meine nächsten Meetup Termine') }}</flux:heading>
|
||||
@if($myUpcomingEvents->count() > 0)
|
||||
<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 }}
|
||||
</div>
|
||||
<flux:badge color="green" size="sm" class="mt-1">
|
||||
{{ $event->start->format('d.m.Y H:i') }}
|
||||
</flux:badge>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="text-sm text-zinc-500">{{ __('Keine bevorstehenden Termine') }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
|
||||
<div class="p-6">
|
||||
<flux:heading size="lg" class="mb-4">{{ __('Meine Meetups') }}</flux:heading>
|
||||
|
||||
<flux:select variant="listbox" searchable placeholder="{{ __('Meetup hinzufügen...') }}" wire:model="selectedMeetupId" wire:change="addMeetup">
|
||||
<x-slot name="search">
|
||||
<flux:select.search class="px-4" placeholder="{{ __('Meetup suchen...') }}"/>
|
||||
</x-slot>
|
||||
@foreach($availableMeetups as $meetup)
|
||||
<flux:select.option value="{{ $meetup->id }}">
|
||||
<div class="flex items-center space-x-2">
|
||||
<img alt="{{ $meetup->name }}" src="{{ $meetup->getFirstMedia('logo') ? $meetup->getFirstMediaUrl('logo', 'thumb') : asset('android-chrome-512x512.png') }}" width="24" height="24" class="rounded"/>
|
||||
<div>
|
||||
<span class="font-medium">{{ $meetup->name }}</span>
|
||||
@if($meetup->city)
|
||||
<span class="text-xs text-zinc-500">- {{ $meetup->city->name }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
|
||||
@if($myMeetups->count() > 0)
|
||||
<flux:separator class="my-4"/>
|
||||
<div class="space-y-3">
|
||||
@foreach($myMeetups as $meetup)
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3 flex-1">
|
||||
<flux:avatar size="sm" src="{{ $meetup->getFirstMedia('logo') ? $meetup->getFirstMediaUrl('logo', 'thumb') : asset('android-chrome-512x512.png') }}"/>
|
||||
<div>
|
||||
<div class="font-medium">{{ $meetup->name }}</div>
|
||||
<div class="text-xs text-zinc-500">
|
||||
{{ $meetup->city->name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:button :href="route_with_country('meetups.edit', ['meetup' => $meetup])" size="xs" variant="ghost" icon="pencil">
|
||||
{{ __('Bearbeiten') }}
|
||||
</flux:button>
|
||||
<flux:modal.trigger :name="'remove-meetup-' . $meetup->id">
|
||||
<flux:button size="xs" variant="danger" icon="trash"></flux:button>
|
||||
</flux:modal.trigger>
|
||||
</div>
|
||||
|
||||
<flux:modal wire:key="remove-meetup-{{ $meetup->id }}" :name="'remove-meetup-' . $meetup->id" class="min-w-[22rem]">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Meetup entfernen?') }}</flux:heading>
|
||||
|
||||
<flux:text class="mt-2">
|
||||
{{ __('Möchtest du') }} "{{ $meetup->name }}" {{ __('aus deinen Meetups entfernen?') }}<br>
|
||||
{{ __('Du kannst es jederzeit wieder hinzufügen.') }}
|
||||
</flux:text>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<flux:spacer />
|
||||
|
||||
<flux:modal.close>
|
||||
<flux:button variant="ghost">{{ __('Abbrechen') }}</flux:button>
|
||||
</flux:modal.close>
|
||||
|
||||
<flux:button wire:click="removeMeetup({{ $meetup->id }})" variant="danger">{{ __('Entfernen') }}</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="text-sm text-zinc-500 mt-4">{{ __('Keine Meetups zugeordnet') }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{--<div class="relative h-full flex-1 overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
|
||||
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
|
||||
</div>--}}
|
||||
</div>
|
||||
@@ -146,7 +146,9 @@ new class extends Component {
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
<flux:button :href="route_with_country('meetups.edit', ['meetup' => $meetup])" size="xs"
|
||||
<flux:button
|
||||
:disabled="!$meetup->belongsToMe"
|
||||
:href="$meetup->belongsToMe ? route_with_country('meetups.edit', ['meetup' => $meetup]) : null" size="xs"
|
||||
variant="filled" icon="pencil">
|
||||
{{ __('Bearbeiten') }}
|
||||
</flux:button>
|
||||
|
||||
Reference in New Issue
Block a user