feat(ui): Whitemode auth views + design tokens + vite server block + touch.js entry
- app.css: add accent/success/danger/surface tokens - vite.config.js: server block (HMR wss), touch.js in input array - Breeze components: text-input, primary-button, input-label, input-error, auth-session-status → oklch token system - layouts/guest.blade.php: Lernschiff logo, white card, no CDN fonts - All 5 auth pages (de): Willkommen zurück, password show/hide toggle, wire:loading spinners - dev-quick-login: redesigned with accent hover, monospace email, heroicons Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>main
parent
d9a957b892
commit
d522f0d376
|
|
@ -6,6 +6,12 @@
|
|||
--color-text-secondary: oklch(0.42 0.012 270);
|
||||
--color-text-muted: oklch(0.58 0.01 270);
|
||||
--color-border-subtle: oklch(0.92 0.006 85);
|
||||
--color-surface: #ffffff;
|
||||
--color-accent: oklch(0.42 0.22 255);
|
||||
--color-accent-hover: oklch(0.36 0.24 255);
|
||||
--color-accent-muted: oklch(0.96 0.04 255);
|
||||
--color-success: oklch(0.52 0.18 155);
|
||||
--color-danger: oklch(0.50 0.22 25);
|
||||
--font-display: 'Plus Jakarta Sans', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
@props(['status'])
|
||||
|
||||
@if ($status)
|
||||
<div {{ $attributes->merge(['class' => 'font-medium text-sm text-green-600']) }}>
|
||||
<div {{ $attributes->merge([
|
||||
'class' => 'flex items-center gap-2 text-sm font-medium text-[--color-success] bg-[oklch(0.96_0.05_155)] border border-[oklch(0.88_0.10_155)] rounded-lg px-4 py-3'
|
||||
]) }}>
|
||||
<x-heroicon-s-check-circle class="w-4 h-4 shrink-0" />
|
||||
{{ $status }}
|
||||
</div>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
@props(['messages'])
|
||||
|
||||
@if ($messages)
|
||||
<ul {{ $attributes->merge(['class' => 'text-sm text-red-600 space-y-1']) }}>
|
||||
<ul {{ $attributes->merge(['class' => 'text-xs text-[--color-danger] space-y-0.5 mt-1.5']) }}>
|
||||
@foreach ((array) $messages as $message)
|
||||
<li>{{ $message }}</li>
|
||||
<li class="flex items-center gap-1.5">
|
||||
<x-heroicon-s-exclamation-circle class="w-3.5 h-3.5 shrink-0" />
|
||||
{{ $message }}
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
@props(['value'])
|
||||
|
||||
<label {{ $attributes->merge(['class' => 'block font-medium text-sm text-gray-700']) }}>
|
||||
<label {{ $attributes->merge(['class' => 'block text-sm font-medium text-[--color-text-secondary] mb-1']) }}>
|
||||
{{ $value ?? $slot }}
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150']) }}>
|
||||
<button {{ $attributes->merge([
|
||||
'type' => 'submit',
|
||||
'class' => 'inline-flex items-center justify-center gap-2 px-5 py-2.5 bg-[--color-accent] hover:bg-[--color-accent-hover] text-white font-semibold text-sm rounded-lg focus:outline-none focus:ring-2 focus:ring-[--color-accent] focus:ring-offset-2 focus:ring-offset-white transition-all duration-150 cursor-pointer disabled:opacity-60 disabled:cursor-not-allowed'
|
||||
]) }}>
|
||||
{{ $slot }}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
@props(['disabled' => false])
|
||||
|
||||
<input @disabled($disabled) {{ $attributes->merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm']) }}>
|
||||
<input
|
||||
@disabled($disabled)
|
||||
{{ $attributes->merge([
|
||||
'class' => 'w-full px-3.5 py-2.5 bg-white border border-[--color-border-subtle] rounded-lg text-sm text-[--color-text-primary] placeholder-[--color-text-muted] focus:outline-none focus:ring-2 focus:ring-[--color-accent] focus:border-transparent transition-all duration-150 disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
]) }}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,30 +1,37 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<html lang="de" class="h-full">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
||||
<title>{{ config('app.name', 'Lernschiff') }}</title>
|
||||
@vite(['resources/css/app.css', 'resources/js/auth.js'])
|
||||
@livewireStyles
|
||||
</head>
|
||||
<body class="min-h-screen bg-[--color-bg-base] flex items-center justify-center px-4 py-12">
|
||||
|
||||
<title>{{ config('app.name', 'Laravel') }}</title>
|
||||
<div class="w-full max-w-[420px]">
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Scripts -->
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="font-sans text-gray-900 antialiased">
|
||||
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
|
||||
<div>
|
||||
<a href="/" wire:navigate>
|
||||
<x-application-logo class="w-20 h-20 fill-current text-gray-500" />
|
||||
</a>
|
||||
{{-- Logo --}}
|
||||
<div class="flex items-center justify-center gap-2.5 mb-8">
|
||||
<div class="w-9 h-9 rounded-xl bg-[--color-accent] flex items-center justify-center shadow-sm">
|
||||
<x-heroicon-o-academic-cap class="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<span class="font-bold text-xl tracking-tight text-[--color-text-primary]">Lernschiff</span>
|
||||
</div>
|
||||
|
||||
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
|
||||
{{-- Card --}}
|
||||
<div class="bg-white rounded-2xl border border-[--color-border-subtle] shadow-sm px-8 py-8">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
|
||||
{{-- Footer --}}
|
||||
<p class="text-center text-xs text-[--color-text-muted] mt-6">
|
||||
© {{ date('Y') }} Lernschiff · DACH · Alle Rechte vorbehalten
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,50 @@
|
|||
<div class="p-8 max-w-lg mx-auto">
|
||||
<h1 class="text-2xl font-bold mb-6 text-[--color-text-primary]">Quick Login (Dev Only)</h1>
|
||||
<div class="min-h-screen bg-[--color-bg-base] flex items-center justify-center px-4 py-12">
|
||||
<div class="w-full max-w-[480px]">
|
||||
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center gap-2.5 mb-8">
|
||||
<div class="w-9 h-9 rounded-xl bg-[--color-accent] flex items-center justify-center shadow-sm">
|
||||
<x-heroicon-o-academic-cap class="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-bold text-xl tracking-tight text-[--color-text-primary]">Lernschiff</span>
|
||||
<span class="ml-2 text-xs font-semibold uppercase tracking-widest text-[--color-text-muted]">Dev</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl border border-[--color-border-subtle] shadow-sm px-8 py-8">
|
||||
|
||||
<div class="flex items-center gap-2 mb-6">
|
||||
<x-heroicon-o-bolt class="w-5 h-5 text-[--color-accent]" />
|
||||
<div>
|
||||
<h1 class="text-lg font-bold text-[--color-text-primary]">Quick Login</h1>
|
||||
<p class="text-xs text-[--color-text-muted]">Nur lokal sichtbar · ALLOW_QUICK_LOGIN=true</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
@foreach($users as $user)
|
||||
<button
|
||||
wire:click="loginAs('{{ $user->email }}')"
|
||||
class="w-full text-left px-4 py-3 rounded-lg border border-[--color-border-subtle] hover:bg-white transition-colors"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="loginAs('{{ $user->email }}')"
|
||||
class="w-full flex items-center justify-between px-4 py-3 rounded-xl border border-[--color-border-subtle] hover:border-[--color-accent] hover:bg-[--color-accent-muted] transition-all duration-150 group cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
<span class="font-semibold">{{ $user->name }}</span>
|
||||
<span class="text-sm text-[--color-text-muted] ml-2">{{ $user->email }}</span>
|
||||
<div class="text-left">
|
||||
<p class="text-sm font-semibold text-[--color-text-primary] group-hover:text-[--color-accent]">
|
||||
{{ $user->name }}
|
||||
</p>
|
||||
<p class="text-xs text-[--color-text-muted] font-mono">{{ $user->email }}</p>
|
||||
</div>
|
||||
<x-heroicon-o-arrow-right class="w-4 h-4 text-[--color-text-muted] group-hover:text-[--color-accent] transition-colors" />
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<p class="text-center text-xs text-[--color-text-muted] mt-6">
|
||||
© {{ date('Y') }} Lernschiff · DACH
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,10 +8,8 @@ use Livewire\Volt\Component;
|
|||
new #[Layout('layouts.guest')] class extends Component
|
||||
{
|
||||
public string $password = '';
|
||||
public bool $showPassword = false;
|
||||
|
||||
/**
|
||||
* Confirm the current user's password.
|
||||
*/
|
||||
public function confirmPassword(): void
|
||||
{
|
||||
$this->validate([
|
||||
|
|
@ -28,35 +26,58 @@ new #[Layout('layouts.guest')] class extends Component
|
|||
}
|
||||
|
||||
session(['auth.password_confirmed_at' => time()]);
|
||||
|
||||
$this->redirectIntended(default: route('dashboard', absolute: false), navigate: true);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div>
|
||||
<div class="mb-4 text-sm text-gray-600">
|
||||
{{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold tracking-tight text-[--color-text-primary]">Sicherheitsbereich</h1>
|
||||
<p class="text-sm text-[--color-text-muted] mt-1">
|
||||
Bitte bestätige dein Passwort, um fortzufahren.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form wire:submit="confirmPassword">
|
||||
<!-- Password -->
|
||||
<form wire:submit="confirmPassword" class="space-y-4">
|
||||
|
||||
<div>
|
||||
<x-input-label for="password" :value="__('Password')" />
|
||||
|
||||
<x-text-input wire:model="password"
|
||||
<x-input-label for="password" value="Aktuelles Passwort" />
|
||||
<div class="relative">
|
||||
<x-text-input
|
||||
wire:model="password"
|
||||
id="password"
|
||||
class="block mt-1 w-full"
|
||||
type="password"
|
||||
:type="$showPassword ? 'text' : 'password'"
|
||||
name="password"
|
||||
required autocomplete="current-password" />
|
||||
|
||||
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||
required
|
||||
autocomplete="current-password"
|
||||
class="pr-10"
|
||||
/>
|
||||
<button type="button" wire:click="$set('showPassword', !$showPassword)"
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 text-[--color-text-muted] hover:text-[--color-text-secondary] transition-colors duration-150 cursor-pointer"
|
||||
aria-label="Passwort anzeigen">
|
||||
@if($showPassword)
|
||||
<x-heroicon-o-eye-slash class="w-4 h-4" />
|
||||
@else
|
||||
<x-heroicon-o-eye class="w-4 h-4" />
|
||||
@endif
|
||||
</button>
|
||||
</div>
|
||||
<x-input-error :messages="$errors->get('password')" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end mt-4">
|
||||
<x-primary-button>
|
||||
{{ __('Confirm') }}
|
||||
<x-primary-button class="w-full" wire:loading.attr="disabled">
|
||||
<span wire:loading.remove wire:target="confirmPassword">
|
||||
<x-heroicon-o-shield-check class="w-4 h-4" />
|
||||
Bestätigen
|
||||
</span>
|
||||
<span wire:loading wire:target="confirmPassword" class="flex items-center gap-2">
|
||||
<svg class="animate-spin w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
Wird geprüft…
|
||||
</span>
|
||||
</x-primary-button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,54 +8,70 @@ new #[Layout('layouts.guest')] 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'],
|
||||
]);
|
||||
|
||||
// We will send the password reset link to this user. Once we have attempted
|
||||
// to send the link, we will examine the response then see the message we
|
||||
// need to show to the user. Finally, we'll send out a proper response.
|
||||
$status = Password::sendResetLink(
|
||||
$this->only('email')
|
||||
);
|
||||
$status = Password::sendResetLink($this->only('email'));
|
||||
|
||||
if ($status != Password::RESET_LINK_SENT) {
|
||||
$this->addError('email', __($status));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->reset('email');
|
||||
|
||||
session()->flash('status', __($status));
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div>
|
||||
<div class="mb-4 text-sm text-gray-600">
|
||||
{{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }}
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold tracking-tight text-[--color-text-primary]">Passwort zurücksetzen</h1>
|
||||
<p class="text-sm text-[--color-text-muted] mt-1">
|
||||
Gib deine E-Mail-Adresse ein — wir senden dir einen Reset-Link.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="mb-4" :status="session('status')" />
|
||||
<x-auth-session-status class="mb-5" :status="session('status')" />
|
||||
|
||||
<form wire:submit="sendPasswordResetLink" class="space-y-4">
|
||||
|
||||
<form wire:submit="sendPasswordResetLink">
|
||||
<!-- Email Address -->
|
||||
<div>
|
||||
<x-input-label for="email" :value="__('Email')" />
|
||||
<x-text-input wire:model="email" id="email" class="block mt-1 w-full" type="email" name="email" required autofocus />
|
||||
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||
<x-input-label for="email" value="E-Mail-Adresse" />
|
||||
<x-text-input
|
||||
wire:model="email"
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="username"
|
||||
placeholder="name@schule.de"
|
||||
/>
|
||||
<x-input-error :messages="$errors->get('email')" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<x-primary-button>
|
||||
{{ __('Email Password Reset Link') }}
|
||||
<x-primary-button class="w-full" wire:loading.attr="disabled">
|
||||
<span wire:loading.remove wire:target="sendPasswordResetLink">
|
||||
<x-heroicon-o-envelope class="w-4 h-4" />
|
||||
Reset-Link senden
|
||||
</span>
|
||||
<span wire:loading wire:target="sendPasswordResetLink" class="flex items-center gap-2">
|
||||
<svg class="animate-spin w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
Wird gesendet…
|
||||
</span>
|
||||
</x-primary-button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<div class="mt-5 text-center">
|
||||
<a href="{{ route('login') }}" wire:navigate class="text-sm text-[--color-accent] hover:underline font-medium">
|
||||
← Zurück zur Anmeldung
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,63 +9,120 @@ new #[Layout('layouts.guest')] class extends Component
|
|||
{
|
||||
public LoginForm $form;
|
||||
|
||||
/**
|
||||
* Handle an incoming authentication request.
|
||||
*/
|
||||
public bool $showPassword = false;
|
||||
|
||||
public function login(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$this->form->authenticate();
|
||||
|
||||
Session::regenerate();
|
||||
|
||||
$this->redirectIntended(default: route('dashboard', absolute: false), navigate: true);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div>
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="mb-4" :status="session('status')" />
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold tracking-tight text-[--color-text-primary]">Willkommen zurück</h1>
|
||||
<p class="text-sm text-[--color-text-muted] mt-1">Melde dich mit deinem Konto an</p>
|
||||
</div>
|
||||
|
||||
<x-auth-session-status class="mb-5" :status="session('status')" />
|
||||
|
||||
<form wire:submit="login" class="space-y-4">
|
||||
|
||||
<form wire:submit="login">
|
||||
<!-- Email Address -->
|
||||
<div>
|
||||
<x-input-label for="email" :value="__('Email')" />
|
||||
<x-text-input wire:model="form.email" id="email" class="block mt-1 w-full" type="email" name="email" required autofocus autocomplete="username" />
|
||||
<x-input-error :messages="$errors->get('form.email')" class="mt-2" />
|
||||
<x-input-label for="email" value="E-Mail-Adresse" />
|
||||
<x-text-input
|
||||
wire:model="form.email"
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="username"
|
||||
placeholder="name@schule.de"
|
||||
/>
|
||||
<x-input-error :messages="$errors->get('form.email')" />
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="password" :value="__('Password')" />
|
||||
|
||||
<x-text-input wire:model="form.password" id="password" class="block mt-1 w-full"
|
||||
type="password"
|
||||
<div>
|
||||
<x-input-label for="password" value="Passwort" />
|
||||
<div class="relative">
|
||||
<x-text-input
|
||||
wire:model="form.password"
|
||||
id="password"
|
||||
:type="$showPassword ? 'text' : 'password'"
|
||||
name="password"
|
||||
required autocomplete="current-password" />
|
||||
|
||||
<x-input-error :messages="$errors->get('form.password')" class="mt-2" />
|
||||
required
|
||||
autocomplete="current-password"
|
||||
class="pr-10"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
wire:click="$set('showPassword', !$showPassword)"
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 text-[--color-text-muted] hover:text-[--color-text-secondary] transition-colors duration-150 cursor-pointer"
|
||||
aria-label="Passwort anzeigen / verbergen"
|
||||
>
|
||||
@if($showPassword)
|
||||
<x-heroicon-o-eye-slash class="w-4 h-4" />
|
||||
@else
|
||||
<x-heroicon-o-eye class="w-4 h-4" />
|
||||
@endif
|
||||
</button>
|
||||
</div>
|
||||
<x-input-error :messages="$errors->get('form.password')" />
|
||||
</div>
|
||||
|
||||
<!-- Remember Me -->
|
||||
<div class="block mt-4">
|
||||
<label for="remember" class="inline-flex items-center">
|
||||
<input wire:model="form.remember" id="remember" type="checkbox" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500" name="remember">
|
||||
<span class="ms-2 text-sm text-gray-600">{{ __('Remember me') }}</span>
|
||||
<div class="flex items-center justify-between pt-1">
|
||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input
|
||||
wire:model="form.remember"
|
||||
id="remember"
|
||||
type="checkbox"
|
||||
class="w-4 h-4 rounded border-[--color-border-subtle] accent-[--color-accent] cursor-pointer"
|
||||
>
|
||||
<span class="text-sm text-[--color-text-secondary]">Angemeldet bleiben</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
@if (Route::has('password.request'))
|
||||
<a class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route('password.request') }}" wire:navigate>
|
||||
{{ __('Forgot your password?') }}
|
||||
<a
|
||||
href="{{ route('password.request') }}"
|
||||
wire:navigate
|
||||
class="text-sm text-[--color-accent] hover:underline font-medium"
|
||||
>
|
||||
Passwort vergessen?
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<x-primary-button class="ms-3">
|
||||
{{ __('Log in') }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
|
||||
<x-primary-button class="w-full mt-2" wire:loading.attr="disabled">
|
||||
<span wire:loading.remove wire:target="login">
|
||||
<x-heroicon-o-arrow-right-on-rectangle class="w-4 h-4" />
|
||||
Anmelden
|
||||
</span>
|
||||
<span wire:loading wire:target="login" class="flex items-center gap-2">
|
||||
<svg class="animate-spin w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
Wird angemeldet…
|
||||
</span>
|
||||
</x-primary-button>
|
||||
|
||||
</form>
|
||||
|
||||
@if(app()->isLocal() && env('ALLOW_QUICK_LOGIN', false))
|
||||
<div class="mt-6 pt-5 border-t border-[--color-border-subtle]">
|
||||
<p class="text-xs font-medium text-[--color-text-muted] uppercase tracking-widest mb-3">
|
||||
Dev · Quick Login
|
||||
</p>
|
||||
<a
|
||||
href="{{ route('dev.quick-login') }}"
|
||||
class="inline-flex items-center gap-1.5 text-xs text-[--color-accent] hover:underline"
|
||||
>
|
||||
<x-heroicon-o-bolt class="w-3.5 h-3.5" />
|
||||
Alle Seed-User anzeigen
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,19 +18,15 @@ new #[Layout('layouts.guest')] class extends Component
|
|||
public string $password = '';
|
||||
public string $password_confirmation = '';
|
||||
|
||||
/**
|
||||
* Mount the component.
|
||||
*/
|
||||
public bool $showPassword = false;
|
||||
public bool $showPasswordConfirmation = false;
|
||||
|
||||
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([
|
||||
|
|
@ -39,9 +35,6 @@ new #[Layout('layouts.guest')] class extends Component
|
|||
'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) {
|
||||
|
|
@ -49,57 +42,105 @@ new #[Layout('layouts.guest')] class extends Component
|
|||
'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::PASSWORD_RESET) {
|
||||
$this->addError('email', __($status));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Session::flash('status', __($status));
|
||||
|
||||
$this->redirectRoute('login', navigate: true);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div>
|
||||
<form wire:submit="resetPassword">
|
||||
<!-- Email Address -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold tracking-tight text-[--color-text-primary]">Neues Passwort</h1>
|
||||
<p class="text-sm text-[--color-text-muted] mt-1">Wähle ein sicheres neues Passwort für dein Konto.</p>
|
||||
</div>
|
||||
|
||||
<form wire:submit="resetPassword" class="space-y-4">
|
||||
|
||||
<div>
|
||||
<x-input-label for="email" :value="__('Email')" />
|
||||
<x-text-input wire:model="email" id="email" class="block mt-1 w-full" type="email" name="email" required autofocus autocomplete="username" />
|
||||
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||
<x-input-label for="email" value="E-Mail-Adresse" />
|
||||
<x-text-input
|
||||
wire:model="email"
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="username"
|
||||
/>
|
||||
<x-input-error :messages="$errors->get('email')" />
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="password" :value="__('Password')" />
|
||||
<x-text-input wire:model="password" id="password" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
|
||||
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||
<div>
|
||||
<x-input-label for="password" value="Neues Passwort" />
|
||||
<div class="relative">
|
||||
<x-text-input
|
||||
wire:model="password"
|
||||
id="password"
|
||||
:type="$showPassword ? 'text' : 'password'"
|
||||
name="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
class="pr-10"
|
||||
/>
|
||||
<button type="button" wire:click="$set('showPassword', !$showPassword)"
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 text-[--color-text-muted] hover:text-[--color-text-secondary] transition-colors duration-150 cursor-pointer"
|
||||
aria-label="Passwort anzeigen">
|
||||
@if($showPassword)
|
||||
<x-heroicon-o-eye-slash class="w-4 h-4" />
|
||||
@else
|
||||
<x-heroicon-o-eye class="w-4 h-4" />
|
||||
@endif
|
||||
</button>
|
||||
</div>
|
||||
<x-input-error :messages="$errors->get('password')" />
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
|
||||
|
||||
<x-text-input wire:model="password_confirmation" id="password_confirmation" class="block mt-1 w-full"
|
||||
type="password"
|
||||
name="password_confirmation" required autocomplete="new-password" />
|
||||
|
||||
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
|
||||
<div>
|
||||
<x-input-label for="password_confirmation" value="Passwort bestätigen" />
|
||||
<div class="relative">
|
||||
<x-text-input
|
||||
wire:model="password_confirmation"
|
||||
id="password_confirmation"
|
||||
:type="$showPasswordConfirmation ? 'text' : 'password'"
|
||||
name="password_confirmation"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
class="pr-10"
|
||||
/>
|
||||
<button type="button" wire:click="$set('showPasswordConfirmation', !$showPasswordConfirmation)"
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 text-[--color-text-muted] hover:text-[--color-text-secondary] transition-colors duration-150 cursor-pointer"
|
||||
aria-label="Passwort anzeigen">
|
||||
@if($showPasswordConfirmation)
|
||||
<x-heroicon-o-eye-slash class="w-4 h-4" />
|
||||
@else
|
||||
<x-heroicon-o-eye class="w-4 h-4" />
|
||||
@endif
|
||||
</button>
|
||||
</div>
|
||||
<x-input-error :messages="$errors->get('password_confirmation')" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<x-primary-button>
|
||||
{{ __('Reset Password') }}
|
||||
<x-primary-button class="w-full" wire:loading.attr="disabled">
|
||||
<span wire:loading.remove wire:target="resetPassword">
|
||||
<x-heroicon-o-lock-closed class="w-4 h-4" />
|
||||
Passwort speichern
|
||||
</span>
|
||||
<span wire:loading wire:target="resetPassword" class="flex items-center gap-2">
|
||||
<svg class="animate-spin w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
Wird gespeichert…
|
||||
</span>
|
||||
</x-primary-button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,51 +8,53 @@ use Livewire\Volt\Component;
|
|||
|
||||
new #[Layout('layouts.guest')] class extends Component
|
||||
{
|
||||
/**
|
||||
* Send an email verification notification to the user.
|
||||
*/
|
||||
public function sendVerification(): void
|
||||
{
|
||||
if (Auth::user()->hasVerifiedEmail()) {
|
||||
$this->redirectIntended(default: route('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>
|
||||
<div class="mb-4 text-sm text-gray-600">
|
||||
{{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }}
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold tracking-tight text-[--color-text-primary]">E-Mail bestätigen</h1>
|
||||
<p class="text-sm text-[--color-text-muted] mt-1">
|
||||
Bitte klicke auf den Link in der E-Mail, die wir dir gesendet haben.
|
||||
Kein Link erhalten? Wir schicken dir einen neuen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if (session('status') == 'verification-link-sent')
|
||||
<div class="mb-4 font-medium text-sm text-green-600">
|
||||
{{ __('A new verification link has been sent to the email address you provided during registration.') }}
|
||||
<div class="flex items-center gap-2 text-sm font-medium text-[--color-success] bg-[oklch(0.96_0.05_155)] border border-[oklch(0.88_0.10_155)] rounded-lg px-4 py-3 mb-5">
|
||||
<x-heroicon-s-check-circle class="w-4 h-4 shrink-0" />
|
||||
Neuer Bestätigungslink wurde gesendet.
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<x-primary-button wire:click="sendVerification">
|
||||
{{ __('Resend Verification Email') }}
|
||||
<div class="space-y-3">
|
||||
<x-primary-button class="w-full" wire:click="sendVerification" wire:loading.attr="disabled">
|
||||
<x-heroicon-o-envelope class="w-4 h-4" />
|
||||
Bestätigungslink erneut senden
|
||||
</x-primary-button>
|
||||
|
||||
<button wire:click="logout" type="submit" class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
{{ __('Log Out') }}
|
||||
<button
|
||||
wire:click="logout"
|
||||
type="button"
|
||||
class="w-full flex items-center justify-center gap-2 px-5 py-2.5 text-sm font-medium text-[--color-text-secondary] hover:text-[--color-text-primary] border border-[--color-border-subtle] rounded-lg hover:bg-[--color-bg-base] transition-all duration-150 cursor-pointer"
|
||||
>
|
||||
<x-heroicon-o-arrow-left-on-rectangle class="w-4 h-4" />
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export default defineConfig({
|
|||
plugins: [
|
||||
tailwindcss(),
|
||||
laravel({
|
||||
input: ['resources/css/app.css', 'resources/js/app.js', 'resources/js/auth.js'],
|
||||
input: ['resources/css/app.css', 'resources/js/auth.js', 'resources/js/app.js', 'resources/js/touch.js'],
|
||||
refresh: true,
|
||||
}),
|
||||
VitePWA({
|
||||
|
|
@ -34,4 +34,15 @@ export default defineConfig({
|
|||
},
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
watch: { ignored: ['**/storage/framework/views/**'] },
|
||||
hmr: {
|
||||
host: 'app.dev.lernschiff.com',
|
||||
protocol: 'wss',
|
||||
clientPort: 443,
|
||||
},
|
||||
cors: true,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue