mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-app.git
synced 2025-12-14 12:06:46 +00:00
🚀 initial commit
This commit is contained in:
58
resources/views/livewire/auth/confirm-password.blade.php
Normal file
58
resources/views/livewire/auth/confirm-password.blade.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new #[Layout('components.layouts.auth')] class extends Component {
|
||||
public string $password = '';
|
||||
|
||||
/**
|
||||
* Confirm the current user's password.
|
||||
*/
|
||||
public function confirmPassword(): void
|
||||
{
|
||||
$this->validate([
|
||||
'password' => ['required', 'string'],
|
||||
]);
|
||||
|
||||
if (! Auth::guard('web')->validate([
|
||||
'email' => Auth::user()->email,
|
||||
'password' => $this->password,
|
||||
])) {
|
||||
throw ValidationException::withMessages([
|
||||
'password' => __('auth.password'),
|
||||
]);
|
||||
}
|
||||
|
||||
session(['auth.password_confirmed_at' => time()]);
|
||||
|
||||
$this->redirectIntended(default: route_with_country('dashboard', absolute: false), navigate: true);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-auth-header
|
||||
:title="__('Confirm password')"
|
||||
:description="__('This is a secure area of the application. Please confirm your password before continuing.')"
|
||||
/>
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="text-center" :status="session('status')" />
|
||||
|
||||
<form wire:submit="confirmPassword" class="flex flex-col gap-6">
|
||||
<!-- Password -->
|
||||
<flux:input
|
||||
wire:model="password"
|
||||
:label="__('Password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
:placeholder="__('Password')"
|
||||
viewable
|
||||
/>
|
||||
|
||||
<flux:button variant="primary" type="submit" class="w-full">{{ __('Confirm') }}</flux:button>
|
||||
</form>
|
||||
</div>
|
||||
49
resources/views/livewire/auth/forgot-password.blade.php
Normal file
49
resources/views/livewire/auth/forgot-password.blade.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new #[Layout('components.layouts.auth')] class extends Component {
|
||||
public string $email = '';
|
||||
|
||||
/**
|
||||
* Send a password reset link to the provided email address.
|
||||
*/
|
||||
public function sendPasswordResetLink(): void
|
||||
{
|
||||
$this->validate([
|
||||
'email' => ['required', 'string', 'email'],
|
||||
]);
|
||||
|
||||
Password::sendResetLink($this->only('email'));
|
||||
|
||||
session()->flash('status', __('A reset link will be sent if the account exists.'));
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-auth-header :title="__('Forgot password')" :description="__('Enter your email to receive a password reset link')" />
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="text-center" :status="session('status')" />
|
||||
|
||||
<form wire:submit="sendPasswordResetLink" class="flex flex-col gap-6">
|
||||
<!-- Email Address -->
|
||||
<flux:input
|
||||
wire:model="email"
|
||||
:label="__('Email Address')"
|
||||
type="email"
|
||||
required
|
||||
autofocus
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
|
||||
<flux:button variant="primary" type="submit" class="w-full">{{ __('Email password reset link') }}</flux:button>
|
||||
</form>
|
||||
|
||||
<div class="space-x-1 rtl:space-x-reverse text-center text-sm text-zinc-400">
|
||||
<span>{{ __('Or, return to') }}</span>
|
||||
<flux:link :href="route('login')" wire:navigate>{{ __('log in') }}</flux:link>
|
||||
</div>
|
||||
</div>
|
||||
207
resources/views/livewire/auth/login.blade.php
Normal file
207
resources/views/livewire/auth/login.blade.php
Normal file
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Auth\Events\Lockout;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Volt\Component;
|
||||
use Flux\Flux;
|
||||
|
||||
new #[Layout('components.layouts.auth')]
|
||||
class extends Component {
|
||||
#[Validate('required|string|email')]
|
||||
public string $email = '';
|
||||
|
||||
#[Validate('required|string')]
|
||||
public string $password = '';
|
||||
|
||||
public bool $remember = false;
|
||||
|
||||
/**
|
||||
* Handle an incoming authentication request.
|
||||
*/
|
||||
public function login(): void
|
||||
{
|
||||
if (app()->environment('production')) {
|
||||
Flux::toast(text: 'Login work in progress', variant: 'danger');
|
||||
return;
|
||||
}
|
||||
Auth::loginUsingId(1, true);
|
||||
Session::regenerate();
|
||||
$this->redirectIntended(default: route_with_country('dashboard', absolute: false), navigate: true);
|
||||
return;
|
||||
|
||||
$this->validate();
|
||||
|
||||
$this->ensureIsNotRateLimited();
|
||||
|
||||
if (!Auth::attempt(['email' => $this->email, 'password' => $this->password], $this->remember)) {
|
||||
RateLimiter::hit($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => __('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
RateLimiter::clear($this->throttleKey());
|
||||
Session::regenerate();
|
||||
|
||||
$this->redirectIntended(default: route_with_country('dashboard', absolute: false), navigate: true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the authentication request is not rate limited.
|
||||
*/
|
||||
protected function ensureIsNotRateLimited(): void
|
||||
{
|
||||
if (!RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event(new Lockout(request()));
|
||||
|
||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => __('auth.throttle', [
|
||||
'seconds' => $seconds,
|
||||
'minutes' => ceil($seconds / 60),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authentication rate limiting throttle key.
|
||||
*/
|
||||
protected function throttleKey(): string
|
||||
{
|
||||
return Str::transliterate(Str::lower($this->email).'|'.request()->ip());
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="flex min-h-screen">
|
||||
<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>
|
||||
<img class="h-32 bg-white border-4 border-white"
|
||||
src="{{ asset('img/einundzwanzig-square.svg') }}" alt="Logo">
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Welcome Heading -->
|
||||
<flux:heading class="text-center" size="xl">{{ __('Willkommen zurück') }}</flux:heading>
|
||||
|
||||
<!-- OAuth Buttons -->
|
||||
{{--<div class="space-y-4">
|
||||
<flux:button class="w-full">
|
||||
<x-slot name="icon">
|
||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23.06 12.25C23.06 11.47 22.99 10.72 22.86 10H12.5V14.26H18.42C18.16 15.63 17.38 16.79 16.21 17.57V20.34H19.78C21.86 18.42 23.06 15.6 23.06 12.25Z" fill="#4285F4"/>
|
||||
<path d="M12.4997 23C15.4697 23 17.9597 22.02 19.7797 20.34L16.2097 17.57C15.2297 18.23 13.9797 18.63 12.4997 18.63C9.63969 18.63 7.20969 16.7 6.33969 14.1H2.67969V16.94C4.48969 20.53 8.19969 23 12.4997 23Z" fill="#34A853"/>
|
||||
<path d="M6.34 14.0899C6.12 13.4299 5.99 12.7299 5.99 11.9999C5.99 11.2699 6.12 10.5699 6.34 9.90995V7.06995H2.68C1.93 8.54995 1.5 10.2199 1.5 11.9999C1.5 13.7799 1.93 15.4499 2.68 16.9299L5.53 14.7099L6.34 14.0899Z" fill="#FBBC05"/>
|
||||
<path d="M12.4997 5.38C14.1197 5.38 15.5597 5.94 16.7097 7.02L19.8597 3.87C17.9497 2.09 15.4697 1 12.4997 1C8.19969 1 4.48969 3.47 2.67969 7.07L6.33969 9.91C7.20969 7.31 9.63969 5.38 12.4997 5.38Z" fill="#EA4335"/>
|
||||
</svg>
|
||||
</x-slot>
|
||||
Continue with Google
|
||||
</flux:button>
|
||||
|
||||
<flux:button class="w-full">
|
||||
<x-slot name="icon">
|
||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_614_12799)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4642 0C5.84833 0 0.5 5.5 0.5 12.3043C0.5 17.7433 3.92686 22.3473 8.68082 23.9768C9.27518 24.0993 9.4929 23.712 9.4929 23.3863C9.4929 23.101 9.47331 22.1233 9.47331 21.1045C6.14514 21.838 5.45208 19.6378 5.45208 19.6378C4.91723 18.2118 4.12474 17.8453 4.12474 17.8453C3.03543 17.0915 4.20408 17.0915 4.20408 17.0915C5.41241 17.173 6.04645 18.3545 6.04645 18.3545C7.11592 20.2285 8.83927 19.699 9.53257 19.373C9.63151 18.5785 9.94865 18.0285 10.2854 17.723C7.63094 17.4378 4.83812 16.3785 4.83812 11.6523C4.83812 10.3078 5.31323 9.20775 6.06604 8.35225C5.94727 8.04675 5.53118 6.7835 6.18506 5.09275C6.18506 5.09275 7.19527 4.76675 9.47306 6.35575C10.4483 6.08642 11.454 5.9494 12.4642 5.94825C13.4745 5.94825 14.5042 6.091 15.4552 6.35575C17.7332 4.76675 18.7434 5.09275 18.7434 5.09275C19.3973 6.7835 18.981 8.04675 18.8622 8.35225C19.6349 9.20775 20.0904 10.3078 20.0904 11.6523C20.0904 16.3785 17.2976 17.4173 14.6233 17.723C15.0592 18.11 15.4353 18.8433 15.4353 20.0045C15.4353 21.6545 15.4158 22.9788 15.4158 23.386C15.4158 23.712 15.6337 24.0993 16.2278 23.977C20.9818 22.347 24.4087 17.7433 24.4087 12.3043C24.4282 5.5 19.0603 0 12.4642 0Z" fill="currentColor"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_614_12799">
|
||||
<rect width="24" height="24" fill="white" transform="translate(0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</x-slot>
|
||||
Continue with GitHub
|
||||
</flux:button>
|
||||
</div>--}}
|
||||
|
||||
<!-- Separator -->
|
||||
{{--<flux:separator text="or" />--}}
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="text-center" :status="session('status')" />
|
||||
|
||||
<!-- Login Form -->
|
||||
<form wire:submit="login" class="flex flex-col gap-6">
|
||||
<!-- Email Input -->
|
||||
{{--<flux:input
|
||||
wire:model="email"
|
||||
label="Email"
|
||||
type="email"
|
||||
placeholder="email@example.com"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="email"
|
||||
/>--}}
|
||||
|
||||
<!-- Password Input -->
|
||||
{{--<flux:field>
|
||||
<div class="mb-3 flex justify-between">
|
||||
<flux:label>Password</flux:label>
|
||||
@if (Route::has('password.request'))
|
||||
<flux:link href="{{ route('password.request') }}" variant="subtle" class="text-sm" wire:navigate>
|
||||
Forgot password?
|
||||
</flux:link>
|
||||
@endif
|
||||
</div>
|
||||
<flux:input
|
||||
wire:model="password"
|
||||
type="password"
|
||||
placeholder="Your password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
</flux:field>--}}
|
||||
|
||||
<!-- Remember Me -->
|
||||
{{--<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>
|
||||
|
||||
<!-- Sign up Link -->
|
||||
{{--@if (Route::has('register'))
|
||||
<flux:subheading class="text-center">
|
||||
First time around here? <flux:link href="{{ route('register') }}" wire:navigate>Sign up for free</flux:link>
|
||||
</flux:subheading>
|
||||
@endif--}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
|
||||
<!-- Testimonial -->
|
||||
<div class="mb-6 italic font-base text-3xl xl:text-4xl">
|
||||
Bitcoin, not blockchain. Bitcoin, not crypto.
|
||||
</div>
|
||||
|
||||
<!-- Author Info -->
|
||||
<div class="flex gap-4">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
99
resources/views/livewire/auth/register.blade.php
Normal file
99
resources/views/livewire/auth/register.blade.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new #[Layout('components.layouts.auth')] class extends Component {
|
||||
public string $name = '';
|
||||
public string $email = '';
|
||||
public string $password = '';
|
||||
public string $password_confirmation = '';
|
||||
|
||||
/**
|
||||
* Handle an incoming registration request.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$validated = $this->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:' . User::class],
|
||||
'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
$validated['password'] = Hash::make($validated['password']);
|
||||
|
||||
event(new Registered(($user = User::create($validated))));
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
$this->redirectIntended(route_with_country('dashboard', absolute: false), navigate: true);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-auth-header :title="__('Create an account')" :description="__('Enter your details below to create your account')" />
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="text-center" :status="session('status')" />
|
||||
|
||||
<form wire:submit="register" class="flex flex-col gap-6">
|
||||
<!-- Name -->
|
||||
<flux:input
|
||||
wire:model="name"
|
||||
:label="__('Name')"
|
||||
type="text"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="name"
|
||||
:placeholder="__('Full name')"
|
||||
/>
|
||||
|
||||
<!-- Email Address -->
|
||||
<flux:input
|
||||
wire:model="email"
|
||||
:label="__('Email address')"
|
||||
type="email"
|
||||
required
|
||||
autocomplete="email"
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
|
||||
<!-- Password -->
|
||||
<flux:input
|
||||
wire:model="password"
|
||||
:label="__('Password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
:placeholder="__('Password')"
|
||||
viewable
|
||||
/>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<flux:input
|
||||
wire:model="password_confirmation"
|
||||
:label="__('Confirm password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
:placeholder="__('Confirm password')"
|
||||
viewable
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<flux:button type="submit" variant="primary" class="w-full">
|
||||
{{ __('Create account') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="space-x-1 rtl:space-x-reverse text-center text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<span>{{ __('Already have an account?') }}</span>
|
||||
<flux:link :href="route('login')" wire:navigate>{{ __('Log in') }}</flux:link>
|
||||
</div>
|
||||
</div>
|
||||
115
resources/views/livewire/auth/reset-password.blade.php
Normal file
115
resources/views/livewire/auth/reset-password.blade.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new #[Layout('components.layouts.auth')] class extends Component {
|
||||
#[Locked]
|
||||
public string $token = '';
|
||||
public string $email = '';
|
||||
public string $password = '';
|
||||
public string $password_confirmation = '';
|
||||
|
||||
/**
|
||||
* Mount the component.
|
||||
*/
|
||||
public function mount(string $token): void
|
||||
{
|
||||
$this->token = $token;
|
||||
|
||||
$this->email = request()->string('email');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the password for the given user.
|
||||
*/
|
||||
public function resetPassword(): void
|
||||
{
|
||||
$this->validate([
|
||||
'token' => ['required'],
|
||||
'email' => ['required', 'string', 'email'],
|
||||
'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
// Here we will attempt to reset the user's password. If it is successful we
|
||||
// will update the password on an actual user model and persist it to the
|
||||
// database. Otherwise we will parse the error and return the response.
|
||||
$status = Password::reset(
|
||||
$this->only('email', 'password', 'password_confirmation', 'token'),
|
||||
function ($user) {
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($this->password),
|
||||
'remember_token' => Str::random(60),
|
||||
])->save();
|
||||
|
||||
event(new PasswordReset($user));
|
||||
}
|
||||
);
|
||||
|
||||
// If the password was successfully reset, we will redirect the user back to
|
||||
// the application's home authenticated view. If there is an error we can
|
||||
// redirect them back to where they came from with their error message.
|
||||
if ($status != Password::PasswordReset) {
|
||||
$this->addError('email', __($status));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Session::flash('status', __($status));
|
||||
|
||||
$this->redirectRoute('login', navigate: true);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-auth-header :title="__('Reset password')" :description="__('Please enter your new password below')" />
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="text-center" :status="session('status')" />
|
||||
|
||||
<form wire:submit="resetPassword" class="flex flex-col gap-6">
|
||||
<!-- Email Address -->
|
||||
<flux:input
|
||||
wire:model="email"
|
||||
:label="__('Email')"
|
||||
type="email"
|
||||
required
|
||||
autocomplete="email"
|
||||
/>
|
||||
|
||||
<!-- Password -->
|
||||
<flux:input
|
||||
wire:model="password"
|
||||
:label="__('Password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
:placeholder="__('Password')"
|
||||
viewable
|
||||
/>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<flux:input
|
||||
wire:model="password_confirmation"
|
||||
:label="__('Confirm password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
:placeholder="__('Confirm password')"
|
||||
viewable
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<flux:button type="submit" variant="primary" class="w-full">
|
||||
{{ __('Reset password') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
57
resources/views/livewire/auth/verify-email.blade.php
Normal file
57
resources/views/livewire/auth/verify-email.blade.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Actions\Logout;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new #[Layout('components.layouts.auth')] class extends Component {
|
||||
/**
|
||||
* Send an email verification notification to the user.
|
||||
*/
|
||||
public function sendVerification(): void
|
||||
{
|
||||
if (Auth::user()->hasVerifiedEmail()) {
|
||||
$this->redirectIntended(default: route_with_country('dashboard', absolute: false), navigate: true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Auth::user()->sendEmailVerificationNotification();
|
||||
|
||||
Session::flash('status', 'verification-link-sent');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the current user out of the application.
|
||||
*/
|
||||
public function logout(Logout $logout): void
|
||||
{
|
||||
$logout();
|
||||
|
||||
$this->redirect('/', navigate: true);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-6">
|
||||
<flux:text class="text-center">
|
||||
{{ __('Please verify your email address by clicking on the link we just emailed to you.') }}
|
||||
</flux:text>
|
||||
|
||||
@if (session('status') == 'verification-link-sent')
|
||||
<flux:text class="text-center font-medium !dark:text-green-400 !text-green-600">
|
||||
{{ __('A new verification link has been sent to the email address you provided during registration.') }}
|
||||
</flux:text>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-col items-center justify-between space-y-3">
|
||||
<flux:button wire:click="sendVerification" variant="primary" class="w-full">
|
||||
{{ __('Resend verification email') }}
|
||||
</flux:button>
|
||||
|
||||
<flux:link class="text-sm cursor-pointer" wire:click="logout">
|
||||
{{ __('Log out') }}
|
||||
</flux:link>
|
||||
</div>
|
||||
</div>
|
||||
35
resources/views/livewire/country/chooser.blade.php
Normal file
35
resources/views/livewire/country/chooser.blade.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
public $currentRoute = '';
|
||||
public $country = 'de';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->currentRoute = request()->route()->getName();
|
||||
$this->country = request()->route('country');
|
||||
}
|
||||
|
||||
public function updatedCountry()
|
||||
{
|
||||
$this->redirectRoute($this->currentRoute, ['country' => $this->country]);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div>
|
||||
<flux:select variant="listbox" searchable placeholder="{{ __('Wähle dein Land...') }}" wire:model.live.debounce="country">
|
||||
<x-slot name="search">
|
||||
<flux:select.search class="px-4" placeholder="{{ __('Suche dein Land...') }}"/>
|
||||
</x-slot>
|
||||
@foreach(\WW\Countries\Models\Country::all() as $country)
|
||||
<flux:select.option value="{{ str($country->iso_code)->lower() }}">
|
||||
<div class="flex items-center space-x-2">
|
||||
<img alt="{{ str($country->iso_code)->lower() }}" src="{{ asset('vendor/blade-flags/country-'.str($country->iso_code)->lower().'.svg') }}" width="24" height="12"/>
|
||||
<span>{{ $country->name }}</span>
|
||||
</div>
|
||||
</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
</div>
|
||||
284
resources/views/livewire/meetups/edit.blade.php
Normal file
284
resources/views/livewire/meetups/edit.blade.php
Normal file
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
|
||||
use Livewire\Volt\Component;
|
||||
use App\Models\Meetup;
|
||||
use App\Models\City;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
new class extends Component {
|
||||
public Meetup $meetup;
|
||||
|
||||
// Basic Information
|
||||
public string $name = '';
|
||||
public ?int $city_id = null;
|
||||
public string $slug = '';
|
||||
public ?string $intro = null;
|
||||
|
||||
// Links and Social Media
|
||||
public ?string $telegram_link = null;
|
||||
public ?string $webpage = null;
|
||||
public ?string $twitter_username = null;
|
||||
public ?string $matrix_group = null;
|
||||
public ?string $nostr = null;
|
||||
public ?string $nostr_status = null;
|
||||
public ?string $simplex = null;
|
||||
public ?string $signal = null;
|
||||
|
||||
// Additional Information
|
||||
public ?string $community = null;
|
||||
public ?string $github_data = null;
|
||||
public bool $visible_on_map = false;
|
||||
|
||||
// System fields (read-only)
|
||||
public ?int $created_by = null;
|
||||
public ?string $created_at = null;
|
||||
public ?string $updated_at = null;
|
||||
|
||||
public function mount(Meetup $meetup): void
|
||||
{
|
||||
$this->meetup = $meetup;
|
||||
|
||||
// Basic Information
|
||||
$this->name = $meetup->name ?? '';
|
||||
$this->city_id = $meetup->city_id;
|
||||
$this->slug = $meetup->slug ?? '';
|
||||
$this->intro = $meetup->intro;
|
||||
|
||||
// Links and Social Media
|
||||
$this->telegram_link = $meetup->telegram_link;
|
||||
$this->webpage = $meetup->webpage;
|
||||
$this->twitter_username = $meetup->twitter_username;
|
||||
$this->matrix_group = $meetup->matrix_group;
|
||||
$this->nostr = $meetup->nostr;
|
||||
$this->nostr_status = $meetup->nostr_status;
|
||||
$this->simplex = $meetup->simplex;
|
||||
$this->signal = $meetup->signal;
|
||||
|
||||
// Additional Information
|
||||
$this->community = $meetup->community;
|
||||
$this->github_data = $meetup->github_data ? json_encode($meetup->github_data, JSON_PRETTY_PRINT) : null;
|
||||
$this->visible_on_map = (bool) $meetup->visible_on_map;
|
||||
|
||||
// System fields
|
||||
$this->created_by = $meetup->created_by;
|
||||
$this->created_at = $meetup->created_at?->format('Y-m-d H:i:s');
|
||||
$this->updated_at = $meetup->updated_at?->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
public function updateMeetup(): void
|
||||
{
|
||||
$validated = $this->validate([
|
||||
'name' => ['required', 'string', 'max:255', Rule::unique('meetups')->ignore($this->meetup->id)],
|
||||
'city_id' => ['nullable', 'exists:cities,id'],
|
||||
'intro' => ['nullable', 'string'],
|
||||
'telegram_link' => ['nullable', 'url', 'max:255'],
|
||||
'webpage' => ['nullable', 'url', 'max:255'],
|
||||
'twitter_username' => ['nullable', 'string', 'max:255'],
|
||||
'matrix_group' => ['nullable', 'string', 'max:255'],
|
||||
'nostr' => ['nullable', 'string', 'max:255'],
|
||||
'simplex' => ['nullable', 'string', 'max:255'],
|
||||
'signal' => ['nullable', 'string', 'max:255'],
|
||||
'community' => ['nullable', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
// Convert github_data string back to array if provided
|
||||
if (!empty($validated['github_data'])) {
|
||||
$decoded = json_decode($validated['github_data'], true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$validated['github_data'] = $decoded;
|
||||
} else {
|
||||
$validated['github_data'] = null;
|
||||
}
|
||||
} else {
|
||||
$validated['github_data'] = null;
|
||||
}
|
||||
|
||||
$this->meetup->update($validated);
|
||||
|
||||
$this->dispatch('meetup-updated', name: $this->meetup->name);
|
||||
|
||||
session()->flash('status', __('Meetup erfolgreich aktualisiert!'));
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
return [
|
||||
'cities' => City::orderBy('name')->get(),
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="max-w-4xl mx-auto p-6">
|
||||
<flux:heading size="xl" class="mb-8">{{ __('Meetup bearbeiten') }}: {{ $meetup->name }}</flux:heading>
|
||||
|
||||
<form wire:submit="updateMeetup" class="space-y-10">
|
||||
|
||||
<!-- Basic Information -->
|
||||
<flux:fieldset class="space-y-6">
|
||||
<flux:legend>{{ __('Grundlegende Informationen') }}</flux:legend>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('ID') }}</flux:label>
|
||||
<flux:input value="{{ $meetup->id }}" disabled/>
|
||||
<flux:description>{{ __('System-generierte ID (nur lesbar)') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Name') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="name" required/>
|
||||
<flux:description>{{ __('Der Anzeigename für dieses Meetup') }}</flux:description>
|
||||
<flux:error name="name"/>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Stadt') }}</flux:label>
|
||||
<flux:select variant="listbox" searchable wire:model="city_id" placeholder="{{ __('Stadt auswählen') }}">
|
||||
<x-slot name="search">
|
||||
<flux:select.search class="px-4" placeholder="{{ __('Suche passende Stadt...') }}"/>
|
||||
</x-slot>
|
||||
@foreach($cities as $city)
|
||||
<flux:select.option value="{{ $city->id }}">{{ $city->name }}</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:description>{{ __('Die nächstgrößte Stadt oder Ort') }}</flux:description>
|
||||
<flux:error name="city_id"/>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Einführung') }}</flux:label>
|
||||
<flux:textarea wire:model="intro" rows="4"/>
|
||||
<flux:description>{{ __('Kurze Beschreibung des Meetups') }}</flux:description>
|
||||
<flux:error name="intro"/>
|
||||
</flux:field>
|
||||
</flux:fieldset>
|
||||
|
||||
<!-- Links and Social Media -->
|
||||
<flux:fieldset class="space-y-6">
|
||||
<flux:legend>{{ __('Links & Soziale Medien') }}</flux:legend>
|
||||
|
||||
<!-- Primary Links -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Webseite') }}</flux:label>
|
||||
<flux:input wire:model="webpage" type="url" placeholder="https://example.com"/>
|
||||
<flux:description>{{ __('Offizielle Webseite oder Landingpage') }}</flux:description>
|
||||
<flux:error name="webpage"/>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Telegram Link') }}</flux:label>
|
||||
<flux:input wire:model="telegram_link" type="url" placeholder="https://t.me/gruppenname"/>
|
||||
<flux:description>{{ __('Link zur Telegram-Gruppe oder zum Kanal') }}</flux:description>
|
||||
<flux:error name="telegram_link"/>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<!-- Social Media -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Twitter Benutzername') }}</flux:label>
|
||||
<flux:input wire:model="twitter_username" placeholder="benutzername"/>
|
||||
<flux:description>{{ __('Twitter-Handle ohne @ Symbol') }}</flux:description>
|
||||
<flux:error name="twitter_username"/>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Matrix Gruppe') }}</flux:label>
|
||||
<flux:input wire:model="matrix_group" placeholder="#gruppe:matrix.org"/>
|
||||
<flux:description>{{ __('Matrix-Raum Bezeichner oder Link') }}</flux:description>
|
||||
<flux:error name="matrix_group"/>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<!-- Decentralized Platforms -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Nostr') }}</flux:label>
|
||||
<flux:input wire:model="nostr" placeholder="npub..."/>
|
||||
<flux:description>{{ __('Nostr öffentlicher Schlüssel oder Bezeichner') }}</flux:description>
|
||||
<flux:error name="nostr"/>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<!-- Messaging Apps -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('SimpleX') }}</flux:label>
|
||||
<flux:input wire:model="simplex"/>
|
||||
<flux:description>{{ __('SimpleX Chat Kontaktinformationen') }}</flux:description>
|
||||
<flux:error name="simplex"/>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Signal') }}</flux:label>
|
||||
<flux:input wire:model="signal"/>
|
||||
<flux:description>{{ __('Signal Kontakt- oder Gruppeninformationen') }}</flux:description>
|
||||
<flux:error name="signal"/>
|
||||
</flux:field>
|
||||
</div>
|
||||
</flux:fieldset>
|
||||
|
||||
<!-- Additional Information -->
|
||||
<flux:fieldset class="space-y-6">
|
||||
<flux:legend>{{ __('Zusätzliche Informationen') }}</flux:legend>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Gemeinschaft') }}</flux:label>
|
||||
<flux:select wire:model="community">
|
||||
<flux:select.option value="einundzwanzig">einundzwanzig</flux:select.option>
|
||||
<flux:select.option value="bitcoin">bitcoin</flux:select.option>
|
||||
</flux:select>
|
||||
<flux:description>{{ __('Gemeinschafts- oder Organisationsname') }}</flux:description>
|
||||
<flux:error name="community"/>
|
||||
</flux:field>
|
||||
</div>
|
||||
</flux:fieldset>
|
||||
|
||||
<!-- System Information -->
|
||||
<flux:fieldset class="space-y-6">
|
||||
<flux:legend>{{ __('Systeminformationen') }}</flux:legend>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Erstellt von') }}</flux:label>
|
||||
<flux:input value="{{ $meetup->createdBy?->name ?? __('Unbekannt') }}" disabled/>
|
||||
<flux:description>{{ __('Ersteller des Meetups') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Erstellt am') }}</flux:label>
|
||||
<flux:input value="{{ $created_at }}" disabled/>
|
||||
<flux:description>{{ __('Wann dieses Meetup erstellt wurde') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Aktualisiert am') }}</flux:label>
|
||||
<flux:input value="{{ $updated_at }}" disabled/>
|
||||
<flux:description>{{ __('Letzte Änderungszeit') }}</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
</flux:fieldset>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="flex items-center justify-between pt-8 border-t border-gray-200 dark:border-gray-700">
|
||||
<flux:button variant="ghost" type="button" onclick="history.back()">
|
||||
{{ __('Abbrechen') }}
|
||||
</flux:button>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
@if (session('status'))
|
||||
<flux:text class="text-green-600 dark:text-green-400 font-medium">
|
||||
{{ session('status') }}
|
||||
</flux:text>
|
||||
@endif
|
||||
|
||||
<flux:button variant="primary" type="submit">
|
||||
{{ __('Meetup aktualisieren') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
130
resources/views/livewire/meetups/index.blade.php
Normal file
130
resources/views/livewire/meetups/index.blade.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Meetup;
|
||||
use Livewire\Volt\Component;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
new class extends Component {
|
||||
use WithPagination;
|
||||
|
||||
public $sortBy = 'name';
|
||||
public $sortDirection = 'asc';
|
||||
public $search = '';
|
||||
|
||||
public function sort($column)
|
||||
{
|
||||
if ($this->sortBy === $column) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortBy = $column;
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
return [
|
||||
'meetups' => Meetup::with(['city.country', 'createdBy'])
|
||||
->whereHas('city.country', fn($query) => $query->where('countries.code', request()->route('country')))
|
||||
->when($this->search, fn($query)
|
||||
=> $query->where('name', 'ilike', '%'.$this->search.'%'),
|
||||
)
|
||||
->when($this->sortBy === 'city',
|
||||
fn($query)
|
||||
=> $query
|
||||
->orderBy('cities.name', $this->sortDirection)
|
||||
->join('cities', 'meetups.city_id', '=', 'cities.id'),
|
||||
fn($query) => $query->orderBy($this->sortBy, $this->sortDirection),
|
||||
)
|
||||
->paginate(15),
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div>
|
||||
<flux:heading size="xl">{{ __('Meetups') }}</flux:heading>
|
||||
<div class="mt-4">
|
||||
<flux:input
|
||||
wire:model.live="search"
|
||||
:placeholder="__('Suche nach Meetups...')"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
|
||||
<flux:table :paginate="$meetups" class="mt-6">
|
||||
<flux:table.columns>
|
||||
<flux:table.column sortable :sorted="$sortBy === 'name'" :direction="$sortDirection"
|
||||
wire:click="sort('name')">{{ __('Name') }}
|
||||
</flux:table.column>
|
||||
<flux:table.column>{{ __('Links') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Aktionen') }}</flux:table.column>
|
||||
</flux:table.columns>
|
||||
|
||||
<flux:table.rows>
|
||||
@foreach ($meetups as $meetup)
|
||||
<flux:table.row :key="$meetup->id">
|
||||
<flux:table.cell variant="strong">
|
||||
{{ $meetup->name }}
|
||||
@if($meetup->city)
|
||||
<div class="text-xs text-zinc-500 flex items-center space-x-2">
|
||||
<div>{{ $meetup->city->name }}</div>
|
||||
@if($meetup->city->country)
|
||||
<flux:separator vertical/>
|
||||
<div>{{ $meetup->city->country->name }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
<div class="flex gap-2">
|
||||
@if($meetup->telegram_link)
|
||||
<flux:link :href="$meetup->telegram_link" external variant="subtle" title="Telegram">
|
||||
<flux:icon.paper-airplane variant="mini"/>
|
||||
</flux:link>
|
||||
@endif
|
||||
@if($meetup->webpage)
|
||||
<flux:link :href="$meetup->webpage" external variant="subtle" title="Website">
|
||||
<flux:icon.globe-alt variant="mini"/>
|
||||
</flux:link>
|
||||
@endif
|
||||
@if($meetup->twitter_username)
|
||||
<flux:link :href="'https://twitter.com/' . $meetup->twitter_username" external
|
||||
variant="subtle" title="Twitter">
|
||||
<flux:icon.x-mark variant="mini"/>
|
||||
</flux:link>
|
||||
@endif
|
||||
@if($meetup->matrix_group)
|
||||
<flux:link :href="$meetup->matrix_group" external variant="subtle" title="Matrix">
|
||||
<flux:icon.chat-bubble-left variant="mini"/>
|
||||
</flux:link>
|
||||
@endif
|
||||
@if($meetup->nostr)
|
||||
<flux:link :href="'https://njump.me/'.$meetup->nostr" external variant="subtle" title="Nostr">
|
||||
<flux:icon.bolt variant="mini"/>
|
||||
</flux:link>
|
||||
@endif
|
||||
@if($meetup->simplex)
|
||||
<flux:link :href="$meetup->simplex" external variant="subtle" title="Simplex">
|
||||
<flux:icon.chat-bubble-bottom-center-text variant="mini"/>
|
||||
</flux:link>
|
||||
@endif
|
||||
@if($meetup->signal)
|
||||
<flux:link :href="$meetup->signal" external variant="subtle" title="Signal">
|
||||
<flux:icon.shield-check variant="mini"/>
|
||||
</flux:link>
|
||||
@endif
|
||||
</div>
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
<flux:button :href="route_with_country('meetups.edit', ['meetup' => $meetup])" size="xs"
|
||||
variant="filled" icon="pencil">
|
||||
{{ __('Bearbeiten') }}
|
||||
</flux:button>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@endforeach
|
||||
</flux:table.rows>
|
||||
</flux:table>
|
||||
</div>
|
||||
92
resources/views/livewire/meetups/map.blade.php
Normal file
92
resources/views/livewire/meetups/map.blade.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Meetup;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
public function with(): array
|
||||
{
|
||||
return [
|
||||
'meetups' => Meetup::with(['city:id,longitude,latitude'])
|
||||
->select([
|
||||
'meetups.id',
|
||||
'meetups.city_id',
|
||||
'meetups.name',
|
||||
])
|
||||
->get(),
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div>
|
||||
<style>
|
||||
#map {
|
||||
height: 90vh;
|
||||
z-index: 0!important;
|
||||
}
|
||||
|
||||
#map:focus {
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
@php
|
||||
$attribution = '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
|
||||
@endphp
|
||||
<div>
|
||||
<flux:heading>Zoom = STRG+Scroll</flux:heading>
|
||||
</div>
|
||||
<div x-data="{
|
||||
markers: @js($meetups),
|
||||
initializeMap() {
|
||||
const map = L.map($refs.map, {
|
||||
scrollWheelZoom: false
|
||||
}).setView([51.1657, 10.4515], 6);
|
||||
|
||||
L.tileLayer('https://tile.openstreetmap.de/{z}/{x}/{y}.png', {
|
||||
minZoom: 0,
|
||||
maxZoom: 18,
|
||||
attribution: '{{ $attribution }}'
|
||||
}).addTo(map);
|
||||
|
||||
// Custom BTC icon
|
||||
const btcIcon = L.icon({
|
||||
iconUrl: '/img/btc_marker.png',
|
||||
iconSize: [32, 32], // Full size of the image
|
||||
iconAnchor: [16, 32], // Bottom-center of icon (adjust if needed)
|
||||
popupAnchor: [0, -32], // Popup opens above the icon
|
||||
shadowUrl: null // No shadow for simplicity
|
||||
});
|
||||
|
||||
this.markers.forEach(marker => {
|
||||
L.marker([marker.city.latitude, marker.city.longitude], {
|
||||
icon: btcIcon
|
||||
})
|
||||
.bindPopup(marker.name)
|
||||
.addTo(map);
|
||||
});
|
||||
|
||||
// CTRL + scroll wheel zoom
|
||||
const container = map.getContainer();
|
||||
container.addEventListener('wheel', function (e) {
|
||||
e.preventDefault();
|
||||
if (e.ctrlKey) {
|
||||
const delta = e.deltaY > 0 ? -1 : 1;
|
||||
map.setZoom(map.getZoom() + delta, { animate: true });
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
// Optional hint (removable)
|
||||
const hint = L.control({ position: 'topright' });
|
||||
hint.onAdd = function () {
|
||||
const div = L.DomUtil.create('div', 'leaflet-control-zoom-control leaflet-bar');
|
||||
L.DomEvent.disableClickPropagation(div);
|
||||
return div;
|
||||
};
|
||||
hint.addTo(map);
|
||||
}
|
||||
}"
|
||||
x-init="initializeMap()"
|
||||
>
|
||||
<div id="map" x-ref="map"></div>
|
||||
</div>
|
||||
</div>
|
||||
19
resources/views/livewire/settings/appearance.blade.php
Normal file
19
resources/views/livewire/settings/appearance.blade.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
//
|
||||
}; ?>
|
||||
|
||||
<section class="w-full">
|
||||
@include('partials.settings-heading')
|
||||
|
||||
<x-settings.layout :heading="__('Appearance')" :subheading=" __('Update the appearance settings for your account')">
|
||||
<flux:radio.group x-data variant="segmented" x-model="$flux.appearance">
|
||||
<flux:radio value="light" icon="sun">{{ __('Light') }}</flux:radio>
|
||||
<flux:radio value="dark" icon="moon">{{ __('Dark') }}</flux:radio>
|
||||
<flux:radio value="system" icon="computer-desktop">{{ __('System') }}</flux:radio>
|
||||
</flux:radio.group>
|
||||
</x-settings.layout>
|
||||
</section>
|
||||
58
resources/views/livewire/settings/delete-user-form.blade.php
Normal file
58
resources/views/livewire/settings/delete-user-form.blade.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Actions\Logout;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
public string $password = '';
|
||||
|
||||
/**
|
||||
* Delete the currently authenticated user.
|
||||
*/
|
||||
public function deleteUser(Logout $logout): void
|
||||
{
|
||||
$this->validate([
|
||||
'password' => ['required', 'string', 'current_password'],
|
||||
]);
|
||||
|
||||
tap(Auth::user(), $logout(...))->delete();
|
||||
|
||||
$this->redirect('/', navigate: true);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<section class="mt-10 space-y-6">
|
||||
<div class="relative mb-5">
|
||||
<flux:heading>{{ __('Delete account') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Delete your account and all of its resources') }}</flux:subheading>
|
||||
</div>
|
||||
|
||||
<flux:modal.trigger name="confirm-user-deletion">
|
||||
<flux:button variant="danger" x-data="" x-on:click.prevent="$dispatch('open-modal', 'confirm-user-deletion')">
|
||||
{{ __('Delete account') }}
|
||||
</flux:button>
|
||||
</flux:modal.trigger>
|
||||
|
||||
<flux:modal name="confirm-user-deletion" :show="$errors->isNotEmpty()" focusable class="max-w-lg">
|
||||
<form wire:submit="deleteUser" class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Are you sure you want to delete your account?') }}</flux:heading>
|
||||
|
||||
<flux:subheading>
|
||||
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }}
|
||||
</flux:subheading>
|
||||
</div>
|
||||
|
||||
<flux:input wire:model="password" :label="__('Password')" type="password" />
|
||||
|
||||
<div class="flex justify-end space-x-2 rtl:space-x-reverse">
|
||||
<flux:modal.close>
|
||||
<flux:button variant="filled">{{ __('Cancel') }}</flux:button>
|
||||
</flux:modal.close>
|
||||
|
||||
<flux:button variant="danger" type="submit">{{ __('Delete account') }}</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
</flux:modal>
|
||||
</section>
|
||||
78
resources/views/livewire/settings/password.blade.php
Normal file
78
resources/views/livewire/settings/password.blade.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
public string $current_password = '';
|
||||
public string $password = '';
|
||||
public string $password_confirmation = '';
|
||||
|
||||
/**
|
||||
* Update the password for the currently authenticated user.
|
||||
*/
|
||||
public function updatePassword(): void
|
||||
{
|
||||
try {
|
||||
$validated = $this->validate([
|
||||
'current_password' => ['required', 'string', 'current_password'],
|
||||
'password' => ['required', 'string', Password::defaults(), 'confirmed'],
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
$this->reset('current_password', 'password', 'password_confirmation');
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
Auth::user()->update([
|
||||
'password' => Hash::make($validated['password']),
|
||||
]);
|
||||
|
||||
$this->reset('current_password', 'password', 'password_confirmation');
|
||||
|
||||
$this->dispatch('password-updated');
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<section class="w-full">
|
||||
@include('partials.settings-heading')
|
||||
|
||||
<x-settings.layout :heading="__('Update password')" :subheading="__('Ensure your account is using a long, random password to stay secure')">
|
||||
<form wire:submit="updatePassword" class="mt-6 space-y-6">
|
||||
<flux:input
|
||||
wire:model="current_password"
|
||||
:label="__('Current password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
<flux:input
|
||||
wire:model="password"
|
||||
:label="__('New password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<flux:input
|
||||
wire:model="password_confirmation"
|
||||
:label="__('Confirm Password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center justify-end">
|
||||
<flux:button variant="primary" type="submit" class="w-full">{{ __('Save') }}</flux:button>
|
||||
</div>
|
||||
|
||||
<x-action-message class="me-3" on="password-updated">
|
||||
{{ __('Saved.') }}
|
||||
</x-action-message>
|
||||
</div>
|
||||
</form>
|
||||
</x-settings.layout>
|
||||
</section>
|
||||
114
resources/views/livewire/settings/profile.blade.php
Normal file
114
resources/views/livewire/settings/profile.blade.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
public string $name = '';
|
||||
public string $email = '';
|
||||
|
||||
/**
|
||||
* Mount the component.
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
$this->name = Auth::user()->name;
|
||||
$this->email = Auth::user()->email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the profile information for the currently authenticated user.
|
||||
*/
|
||||
public function updateProfileInformation(): void
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$validated = $this->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'lowercase',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique(User::class)->ignore($user->id)
|
||||
],
|
||||
]);
|
||||
|
||||
$user->fill($validated);
|
||||
|
||||
if ($user->isDirty('email')) {
|
||||
$user->email_verified_at = null;
|
||||
}
|
||||
|
||||
$user->save();
|
||||
|
||||
$this->dispatch('profile-updated', name: $user->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an email verification notification to the current user.
|
||||
*/
|
||||
public function resendVerificationNotification(): void
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if ($user->hasVerifiedEmail()) {
|
||||
$this->redirectIntended(default: route_with_country('dashboard', absolute: false));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$user->sendEmailVerificationNotification();
|
||||
|
||||
Session::flash('status', 'verification-link-sent');
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<section class="w-full">
|
||||
@include('partials.settings-heading')
|
||||
|
||||
<x-settings.layout :heading="__('Profile')" :subheading="__('Update your name and email address')">
|
||||
<form wire:submit="updateProfileInformation" class="my-6 w-full space-y-6">
|
||||
<flux:input wire:model="name" :label="__('Name')" type="text" required autofocus autocomplete="name" />
|
||||
|
||||
{{--<div>
|
||||
<flux:input wire:model="email" :label="__('Email')" type="email" required autocomplete="email" />
|
||||
|
||||
@if (auth()->user() instanceof \Illuminate\Contracts\Auth\MustVerifyEmail &&! auth()->user()->hasVerifiedEmail())
|
||||
<div>
|
||||
<flux:text class="mt-4">
|
||||
{{ __('Your email address is unverified.') }}
|
||||
|
||||
<flux:link class="text-sm cursor-pointer" wire:click.prevent="resendVerificationNotification">
|
||||
{{ __('Click here to re-send the verification email.') }}
|
||||
</flux:link>
|
||||
</flux:text>
|
||||
|
||||
@if (session('status') === 'verification-link-sent')
|
||||
<flux:text class="mt-2 font-medium !dark:text-green-400 !text-green-600">
|
||||
{{ __('A new verification link has been sent to your email address.') }}
|
||||
</flux:text>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>--}}
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center justify-end">
|
||||
<flux:button variant="primary" type="submit" class="w-full">{{ __('Save') }}</flux:button>
|
||||
</div>
|
||||
|
||||
<x-action-message class="me-3" on="profile-updated">
|
||||
{{ __('Saved.') }}
|
||||
</x-action-message>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<livewire:settings.delete-user-form />
|
||||
</x-settings.layout>
|
||||
</section>
|
||||
78
resources/views/livewire/welcome.blade.php
Normal file
78
resources/views/livewire/welcome.blade.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
use Livewire\Volt\Component;
|
||||
use Livewire\Attributes\Layout;
|
||||
|
||||
new #[Layout('components.layouts.auth')]
|
||||
class extends Component {
|
||||
public function goToMeetups(): void
|
||||
{
|
||||
$this->redirect(route_with_country('meetups.index'), navigate: true);
|
||||
}
|
||||
|
||||
public function goToMap(): void
|
||||
{
|
||||
$this->redirect(route_with_country('meetups.map'), navigate: true);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="flex min-h-screen">
|
||||
<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>
|
||||
<img class="h-32 bg-white border-4 border-white"
|
||||
src="{{ asset('img/einundzwanzig-square.svg') }}" alt="Logo">
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Welcome Heading -->
|
||||
<flux:heading class="text-center" size="xl">{{ __('Bitcoin Meetups') }}</flux:heading>
|
||||
|
||||
<!-- Navigation Buttons -->
|
||||
<div class="space-y-4">
|
||||
<flux:button wire:click="goToMeetups" class="cursor-pointer w-full">
|
||||
<x-slot name="icon">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
|
||||
</svg>
|
||||
</x-slot>
|
||||
{{ __('Alle Meetups anzeigen') }}
|
||||
</flux:button>
|
||||
|
||||
<flux:button wire:click="goToMap" class="cursor-pointer w-full">
|
||||
<x-slot name="icon">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/>
|
||||
</svg>
|
||||
</x-slot>
|
||||
{{ __('Kartenansicht öffnen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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://images.unsplash.com/photo-1526778548025-fa2f459cd5c1?ixlib=rb-1.2.1&auto=format&fit=crop&w=1333&q=80'); background-size: cover">
|
||||
<!-- Testimonial -->
|
||||
<div class="mb-6 italic font-base text-3xl xl:text-4xl">
|
||||
{{ __('Verbinde dich mit Bitcoinern in deiner Nähe') }}
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="flex gap-4">
|
||||
<div class="flex flex-col justify-center font-medium">
|
||||
<div class="text-lg">{{ __('Bitcoin Meetups') }}</div>
|
||||
<div class="text-zinc-300">{{ __('Finde deine lokale Community') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user