first commit
This commit is contained in:
commit
405df0a122
3083 changed files with 69203 additions and 0 deletions
189
resources/views/livewire/admin/users.blade.php
Normal file
189
resources/views/livewire/admin/users.blade.php
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Volt\Component;
|
||||
use Livewire\WithPagination; // Wichtig für Paginierung
|
||||
|
||||
|
||||
new class extends Component {
|
||||
use WithPagination;
|
||||
|
||||
// Optional: Such- und Filter-Properties
|
||||
public string $search = '';
|
||||
public string $statusFilter = '';
|
||||
public string $roleFilter = '';
|
||||
|
||||
// Optional: Sortierung
|
||||
public string $sortField = 'name';
|
||||
public string $sortDirection = 'asc';
|
||||
|
||||
|
||||
/**
|
||||
* Mount the component.
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Standardwerte für Sortierung setzen
|
||||
$this->sortField = 'name';
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
|
||||
public function sortBy(string $field): void
|
||||
{
|
||||
if ($this->sortField === $field) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
$this->sortField = $field;
|
||||
}
|
||||
|
||||
// Die Hauptmethode, um Daten zu laden
|
||||
public function users()
|
||||
{
|
||||
return User::query()
|
||||
->when($this->search, fn($query, $search) =>
|
||||
$query->where('name', 'like', '%' . $search . '%')
|
||||
->orWhere('email', 'like', '%' . $search . '%')
|
||||
)
|
||||
->when($this->statusFilter, fn($query, $status) =>
|
||||
$query->where('status', $status)
|
||||
)
|
||||
->when($this->roleFilter, fn($query, $role) =>
|
||||
$query->where('role', $role) // oder 'role_id' wenn du IDs verwendest
|
||||
)
|
||||
->orderBy($this->sortField, $this->sortDirection)
|
||||
->paginate(10); // 10 Einträge pro Seite
|
||||
}
|
||||
|
||||
// Optional: Lifecycle hook für das Zurücksetzen der Paginierung bei Suche/Filterung
|
||||
public function updatedSearch() { $this->resetPage(); }
|
||||
public function updatedStatusFilter() { $this->resetPage(); }
|
||||
public function updatedRoleFilter() { $this->resetPage(); }
|
||||
|
||||
// Wird für die Paginierung mit Tailwind benötigt (Standard in Livewire 3)
|
||||
// Wenn FluxUI Bootstrap-basierte Paginierung braucht, musst du das anpassen
|
||||
// public function paginationView()
|
||||
// {
|
||||
// return 'vendor.livewire.tailwind'; // oder 'livewire::bootstrap'
|
||||
// }
|
||||
|
||||
// Wenn du mit Relationen arbeitest (z.B. user->role->name)
|
||||
// public function with(): array
|
||||
// {
|
||||
// return [
|
||||
// 'users' => User::with(['role', 'group']) // Eager loading
|
||||
// // ... deine Query-Logik von oben ...
|
||||
// ->paginate(10),
|
||||
// ];
|
||||
// }
|
||||
}; ?>
|
||||
|
||||
<div>
|
||||
{{-- Filter und Suchleiste (Beispiel, FluxUI Klassen anpassen) --}}
|
||||
<div class="mb-4 grid grid-cols-1 md:grid-cols-3 gap-4 p-4 bg-gray-100 rounded">
|
||||
<div>
|
||||
<label for="search" class="block text-sm font-medium text-gray-700">Suche</label>
|
||||
<input wire:model.live.debounce.300ms="search" id="search" type="text" placeholder="Name oder E-Mail..."
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
|
||||
</div>
|
||||
<div>
|
||||
<label for="statusFilter" class="block text-sm font-medium text-gray-700">Status</label>
|
||||
<select wire:model.live="statusFilter" id="statusFilter"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
|
||||
<option value="">Alle</option>
|
||||
<option value="active">Aktiv</option>
|
||||
<option value="inactive">Inaktiv</option>
|
||||
<option value="pending">Ausstehend</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="roleFilter" class="block text-sm font-medium text-gray-700">Rolle</label>
|
||||
<input wire:model.live.debounce.300ms="roleFilter" id="roleFilter" type="text" placeholder="Rolle..."
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Tabelle (FluxUI Klassen anpassen!) --}}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 shadow">
|
||||
<thead class="bg-gray-50"> {{-- FluxUI Klasse für Thead --}}
|
||||
<tr>
|
||||
<th scope="col" wire:click="sortBy('name')"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer">
|
||||
Name @if($sortField === 'name')<span>{{ $sortDirection === 'asc' ? '▲' : '▼' }}</span>@endif
|
||||
</th>
|
||||
<th scope="col" wire:click="sortBy('email')"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer">
|
||||
E-Mail @if($sortField === 'email')<span>{{ $sortDirection === 'asc' ? '▲' : '▼' }}</span>@endif
|
||||
</th>
|
||||
<th scope="col" wire:click="sortBy('status')"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer">
|
||||
Status @if($sortField === 'status')<span>{{ $sortDirection === 'asc' ? '▲' : '▼' }}</span>@endif
|
||||
</th>
|
||||
<th scope="col" wire:click="sortBy('role')"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer">
|
||||
Rolle @if($sortField === 'role')<span>{{ $sortDirection === 'asc' ? '▲' : '▼' }}</span>@endif
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Gruppe
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">
|
||||
Rechte
|
||||
</th>
|
||||
<th scope="col" class="relative px-6 py-3">
|
||||
<span class="sr-only">Aktionen</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200"> {{-- FluxUI Klasse für Tbody --}}
|
||||
@forelse ($this->users() as $user)
|
||||
<tr wire:key="{{ $user->id }}">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ $user->name }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ $user->email }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
|
||||
{{ $user->status === 'active' ? 'bg-green-100 text-green-800' : ($user->status === 'inactive' ? 'bg-red-100 text-red-800' : 'bg-yellow-100 text-yellow-800') }}">
|
||||
{{ ucfirst($user->status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $user->role }} {{-- Oder $user->role->name, wenn es eine Relation ist --}}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $user->group }} {{-- Oder $user->group->name --}}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{-- Darstellung der Rechte. Wenn es eine Many-to-Many Relation ist: --}}
|
||||
{{-- @foreach($user->permissions as $permission)
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800">
|
||||
{{ $permission->name }}
|
||||
</span>
|
||||
@endforeach --}}
|
||||
{{-- Oder wenn es ein JSON-Feld ist, musst du es parsen und anzeigen --}}
|
||||
Einfache Rechte-Anzeige (Todo)
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<a href="#" class="text-indigo-600 hover:text-indigo-900">Bearbeiten</a> {{-- FluxUI Button-Klassen --}}
|
||||
<button wire:click="$dispatch('openModal', { component: 'admin.delete-user-modal', arguments: { userId: {{ $user->id }} }})"
|
||||
class="text-red-600 hover:text-red-900 ml-2">Löschen</button> {{-- FluxUI Button-Klassen --}}
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-center">
|
||||
Keine Benutzer gefunden.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
{{ $this->users()->links() }} {{-- Stellt sicher, dass die Paginierungs-Views für dein UI-Kit konfiguriert sind --}}
|
||||
</div>
|
||||
</div>
|
||||
144
resources/views/livewire/admin/users/table.blade.php
Normal file
144
resources/views/livewire/admin/users/table.blade.php
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Livewire\Volt\Component;
|
||||
use Livewire\WithPagination; // Wichtig für Paginierung
|
||||
|
||||
|
||||
new class extends Component {
|
||||
use WithPagination;
|
||||
|
||||
// Optional: Such- und Filter-Properties
|
||||
public string $search = '';
|
||||
public string $statusFilter = '';
|
||||
public string $roleFilter = '';
|
||||
|
||||
// Optional: Sortierung
|
||||
public $sortBy = 'name';
|
||||
public $sortDirection = 'asc';
|
||||
|
||||
|
||||
/**
|
||||
* Mount the component.
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Standardwerte für Sortierung setzen
|
||||
$this->sortBy = 'name';
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
|
||||
|
||||
public function sort($column) {
|
||||
if ($this->sortBy === $column) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortBy = $column;
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*public function orders()
|
||||
{
|
||||
return \App\Models\Order::query()
|
||||
->tap(fn ($query) => $this->sortBy ? $query->orderBy($this->sortBy, $this->sortDirection) : $query)
|
||||
->paginate(5);
|
||||
}*/
|
||||
|
||||
|
||||
|
||||
// Die Hauptmethode, um Daten zu laden
|
||||
#[\Livewire\Attributes\Computed]
|
||||
public function users()
|
||||
{
|
||||
return User::query()
|
||||
->when($this->search, fn($query, $search) =>
|
||||
$query->where('name', 'like', '%' . $search . '%')
|
||||
->orWhere('email', 'like', '%' . $search . '%')
|
||||
)
|
||||
->when($this->statusFilter, fn($query, $status) =>
|
||||
$query->where('status', $status)
|
||||
)
|
||||
->when($this->roleFilter, fn($query, $role) =>
|
||||
$query->where('role', $role) // oder 'role_id' wenn du IDs verwendest
|
||||
)
|
||||
->orderBy($this->sortBy, $this->sortDirection)
|
||||
->paginate(10); // 10 Einträge pro Seite
|
||||
}
|
||||
|
||||
// Optional: Lifecycle hook für das Zurücksetzen der Paginierung bei Suche/Filterung
|
||||
public function updatedSearch() { $this->resetPage(); }
|
||||
public function updatedStatusFilter() { $this->resetPage(); }
|
||||
public function updatedRoleFilter() { $this->resetPage(); }
|
||||
|
||||
// Wird für die Paginierung mit Tailwind benötigt (Standard in Livewire 3)
|
||||
// Wenn FluxUI Bootstrap-basierte Paginierung braucht, musst du das anpassen
|
||||
// public function paginationView()
|
||||
// {
|
||||
// return 'vendor.livewire.tailwind'; // oder 'livewire::bootstrap'
|
||||
// }
|
||||
|
||||
// Wenn du mit Relationen arbeitest (z.B. user->role->name)
|
||||
// public function with(): array
|
||||
// {
|
||||
// return [
|
||||
// 'users' => User::with(['role', 'group']) // Eager loading
|
||||
// // ... deine Query-Logik von oben ...
|
||||
// ->paginate(10),
|
||||
// ];
|
||||
// }
|
||||
}; ?>
|
||||
<flux:table :paginate="$this->users">
|
||||
<flux:table.columns>
|
||||
<flux:table.column>Customer</flux:table.column>
|
||||
<flux:table.column sortable :sorted="$sortBy === 'date'" :direction="$sortDirection" wire:click="sort('date')">Date</flux:table.column>
|
||||
<flux:table.column sortable :sorted="$sortBy === 'status'" :direction="$sortDirection" wire:click="sort('status')">Status</flux:table.column>
|
||||
<flux:table.column sortable :sorted="$sortBy === 'amount'" :direction="$sortDirection" wire:click="sort('amount')">Amount</flux:table.column>
|
||||
</flux:table.columns>
|
||||
|
||||
<flux:table.rows>
|
||||
@foreach ($this->users as $user)
|
||||
<flux:table.row :key="$user->id">
|
||||
<flux:table.cell class="flex items-center gap-3">
|
||||
<flux:avatar size="xs" src="{{ $user->avatar }}" />
|
||||
|
||||
{{ $user->name }}
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell class="whitespace-nowrap">{{ $user->email }}</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
<flux:badge size="sm" color="lime" inset="top bottom">RTest</flux:badge>
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell variant="strong">{{ $user->role }}</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
<flux:dropdown>
|
||||
<flux:button variant="ghost" size="sm" icon="ellipsis-horizontal" inset="top bottom"></flux:button>
|
||||
<flux:menu>
|
||||
<flux:menu.item icon="plus">New post</flux:menu.item>
|
||||
<flux:menu.separator />
|
||||
<flux:menu.submenu heading="Sort by">
|
||||
<flux:menu.radio.group>
|
||||
<flux:menu.radio checked>Name</flux:menu.radio>
|
||||
<flux:menu.radio>Date</flux:menu.radio>
|
||||
<flux:menu.radio>Popularity</flux:menu.radio>
|
||||
</flux:menu.radio.group>
|
||||
</flux:menu.submenu>
|
||||
<flux:menu.submenu heading="Filter">
|
||||
<flux:menu.checkbox checked>Draft</flux:menu.checkbox>
|
||||
<flux:menu.checkbox checked>Published</flux:menu.checkbox>
|
||||
<flux:menu.checkbox>Archived</flux:menu.checkbox>
|
||||
</flux:menu.submenu>
|
||||
<flux:menu.separator />
|
||||
<flux:menu.item variant="danger" icon="trash">Delete</flux:menu.item>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@endforeach
|
||||
</flux:table.rows>
|
||||
</flux:table>
|
||||
|
||||
57
resources/views/livewire/auth/confirm-password.blade.php
Normal file
57
resources/views/livewire/auth/confirm-password.blade.php
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?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('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')"
|
||||
/>
|
||||
|
||||
<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 text-center text-sm text-zinc-400">
|
||||
{{ __('Or, return to') }}
|
||||
<flux:link :href="route('login')" wire:navigate>{{ __('log in') }}</flux:link>
|
||||
</div>
|
||||
</div>
|
||||
153
resources/views/livewire/auth/login-simple.blade.php
Normal file
153
resources/views/livewire/auth/login-simple.blade.php
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
<?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;
|
||||
|
||||
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
|
||||
{
|
||||
$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('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 flex-col gap-6 p-8 bg-white rounded-lg shadow-lg">
|
||||
<div class="text-center">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Log in to your account</h1>
|
||||
<p class="text-gray-600 mt-2">Enter your email and password below to log in</p>
|
||||
</div>
|
||||
|
||||
<!-- Session Status -->
|
||||
@if (session('status'))
|
||||
<div class="text-center text-sm text-green-600">
|
||||
{{ session('status') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form wire:submit="login" class="flex flex-col gap-6">
|
||||
<!-- Email Address -->
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700">Email address</label>
|
||||
<input
|
||||
wire:model="email"
|
||||
id="email"
|
||||
type="email"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="email"
|
||||
placeholder="email@example.com"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
@error('email')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="relative">
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">Password</label>
|
||||
<input
|
||||
wire:model="password"
|
||||
id="password"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
placeholder="Password"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
@error('password')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
|
||||
@if (Route::has('password.request'))
|
||||
<a href="{{ route('password.request') }}" class="absolute right-0 top-0 text-sm text-blue-600 hover:text-blue-500">
|
||||
Forgot your password?
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Remember Me -->
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
wire:model="remember"
|
||||
id="remember"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label for="remember" class="ml-2 block text-sm text-gray-900">Remember me</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
||||
Log in
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@if (Route::has('register'))
|
||||
<div class="text-center text-sm text-gray-600">
|
||||
Don't have an account?
|
||||
<a href="{{ route('register') }}" class="text-blue-600 hover:text-blue-500">Sign up</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
126
resources/views/livewire/auth/login.blade.php
Normal file
126
resources/views/livewire/auth/login.blade.php
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<?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;
|
||||
|
||||
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
|
||||
{
|
||||
$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('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 flex-col gap-6">
|
||||
<x-auth-header :title="__('Log in to your account')" :description="__('Enter your email and password below to log in')" />
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="text-center" :status="session('status')" />
|
||||
|
||||
<form wire:submit="login" class="flex flex-col gap-6">
|
||||
<!-- Email Address -->
|
||||
<flux:input
|
||||
wire:model="email"
|
||||
:label="__('Email address')"
|
||||
type="email"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="email"
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="relative">
|
||||
<flux:input
|
||||
wire:model="password"
|
||||
:label="__('Password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
:placeholder="__('Password')"
|
||||
/>
|
||||
|
||||
@if (Route::has('password.request'))
|
||||
<flux:link class="absolute right-0 top-0 text-sm" :href="route('password.request')" wire:navigate>
|
||||
{{ __('Forgot your password?') }}
|
||||
</flux:link>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Remember Me -->
|
||||
<flux:checkbox wire:model="remember" :label="__('Remember me')" />
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<flux:button variant="primary" type="submit" class="w-full">{{ __('Log in') }}</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@if (Route::has('register'))
|
||||
<div class="space-x-1 text-center text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Don\'t have an account?') }}
|
||||
<flux:link :href="route('register')" wire:navigate>{{ __('Sign up') }}</flux:link>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
97
resources/views/livewire/auth/register.blade.php
Normal file
97
resources/views/livewire/auth/register.blade.php
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<?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('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')"
|
||||
/>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<flux:input
|
||||
wire:model="password_confirmation"
|
||||
:label="__('Confirm password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
:placeholder="__('Confirm password')"
|
||||
/>
|
||||
|
||||
<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 text-center text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Already have an account?') }}
|
||||
<flux:link :href="route('login')" wire:navigate>{{ __('Log in') }}</flux:link>
|
||||
</div>
|
||||
</div>
|
||||
113
resources/views/livewire/auth/reset-password.blade.php
Normal file
113
resources/views/livewire/auth/reset-password.blade.php
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<?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')"
|
||||
/>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<flux:input
|
||||
wire:model="password_confirmation"
|
||||
:label="__('Confirm password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
:placeholder="__('Confirm password')"
|
||||
/>
|
||||
|
||||
<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('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>
|
||||
11
resources/views/livewire/notifications.blade.php
Normal file
11
resources/views/livewire/notifications.blade.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
public bool $notifications = false;
|
||||
}; ?>
|
||||
|
||||
<div>
|
||||
<flux:switch wire:model.live="notifications" label="Enable notifications" />
|
||||
</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('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>
|
||||
290
resources/views/livewire/web/burger-menu.blade.php
Normal file
290
resources/views/livewire/web/burger-menu.blade.php
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
<?php
|
||||
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
public $isOpen = false;
|
||||
public $openSections = [];
|
||||
|
||||
public function toggleMenu()
|
||||
{
|
||||
$this->isOpen = !$this->isOpen;
|
||||
}
|
||||
|
||||
public function toggleSection($section)
|
||||
{
|
||||
if (in_array($section, $this->openSections)) {
|
||||
$this->openSections = array_diff($this->openSections, [$section]);
|
||||
} else {
|
||||
$this->openSections[] = $section;
|
||||
}
|
||||
}
|
||||
|
||||
public function isSectionOpen($section)
|
||||
{
|
||||
return in_array($section, $this->openSections);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div
|
||||
x-data="{ isOpen: $wire.entangle('isOpen') }"
|
||||
@toggle-mobile-menu.window="$wire.toggleMenu()"
|
||||
>
|
||||
<!-- Overlay -->
|
||||
<div
|
||||
x-show="isOpen"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
@click="$wire.toggleMenu()"
|
||||
class="fixed inset-0 bg-black/50 backdrop-blur-sm z-[60]"
|
||||
x-cloak
|
||||
></div>
|
||||
|
||||
<!-- Slide-out Menu -->
|
||||
<div
|
||||
x-show="isOpen"
|
||||
x-transition:enter="transition ease-out duration-300 transform"
|
||||
x-transition:enter-start="-translate-x-full"
|
||||
x-transition:enter-end="translate-x-0"
|
||||
x-transition:leave="transition ease-in duration-200 transform"
|
||||
x-transition:leave-start="translate-x-0"
|
||||
x-transition:leave-end="-translate-x-full"
|
||||
class="fixed left-0 top-0 h-full w-80 sm:w-96 bg-white dark:bg-gray-900 shadow-2xl z-[70] overflow-y-auto transition-colors duration-200"
|
||||
x-cloak
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="sticky top-0 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 p-4 flex items-center justify-between transition-colors duration-200">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-1 h-6 bg-gradient-to-b from-[var(--color-primary)] to-[var(--color-secondary)] rounded-full"></span>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Navigation</h2>
|
||||
</div>
|
||||
<button
|
||||
@click="$wire.toggleMenu()"
|
||||
class="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||
aria-label="Menü schließen"
|
||||
>
|
||||
<svg class="h-6 w-6 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="p-6 space-y-6">
|
||||
<!-- Portal Section -->
|
||||
<div>
|
||||
<div class="py-2 text-sm font-semibold text-gray-900 dark:text-gray-100">Portal</div>
|
||||
<div class="space-y-1">
|
||||
<a href="/" class="block py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:translate-x-1 transition-all">
|
||||
Startseite
|
||||
</a>
|
||||
<a href="/kategorien" class="block py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:translate-x-1 transition-all">
|
||||
Kategorien
|
||||
</a>
|
||||
<a href="/suche" class="block py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:translate-x-1 transition-all">
|
||||
Suche
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Services Section -->
|
||||
<div>
|
||||
<button
|
||||
wire:click="toggleSection('services')"
|
||||
class="flex items-center justify-between w-full py-2 text-sm font-semibold text-gray-900 dark:text-gray-100 hover:text-[var(--color-primary)] transition-colors"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path>
|
||||
</svg>
|
||||
Services
|
||||
</span>
|
||||
<svg
|
||||
class="h-4 w-4 transition-transform {{ $this->isSectionOpen('services') ? 'rotate-180' : '' }}"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
@if($this->isSectionOpen('services'))
|
||||
<div class="mt-2 space-y-1 pl-6">
|
||||
<a href="/veroeffentlichen" class="block py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:translate-x-1 transition-all">
|
||||
Pressemitteilung veröffentlichen
|
||||
</a>
|
||||
<a href="/newsrooms" class="block py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:translate-x-1 transition-all">
|
||||
Newsrooms
|
||||
</a>
|
||||
<a href="/preise" class="block py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:translate-x-1 transition-all">
|
||||
Preise & Leistungen
|
||||
</a>
|
||||
<a href="/api" class="block py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:translate-x-1 transition-all">
|
||||
API & Integrationen
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Über uns Section -->
|
||||
<div>
|
||||
<button
|
||||
wire:click="toggleSection('about')"
|
||||
class="flex items-center justify-between w-full py-2 text-sm font-semibold text-gray-900 dark:text-gray-100 hover:text-[var(--color-primary)] transition-colors"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>
|
||||
</svg>
|
||||
Über uns
|
||||
</span>
|
||||
<svg
|
||||
class="h-4 w-4 transition-transform {{ $this->isSectionOpen('about') ? 'rotate-180' : '' }}"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
@if($this->isSectionOpen('about'))
|
||||
<div class="mt-2 space-y-1 pl-6">
|
||||
<a href="/ueber-uns" class="block py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:translate-x-1 transition-all">
|
||||
Über Business Portal
|
||||
</a>
|
||||
<a href="/team" class="block py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:translate-x-1 transition-all">
|
||||
Team
|
||||
</a>
|
||||
<a href="/partner" class="block py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:translate-x-1 transition-all">
|
||||
Partner
|
||||
</a>
|
||||
<a href="/karriere" class="block py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:translate-x-1 transition-all">
|
||||
Karriere
|
||||
</a>
|
||||
<a href="/presse" class="block py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:translate-x-1 transition-all">
|
||||
Presse
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Hilfe & Support Section -->
|
||||
<div>
|
||||
<button
|
||||
wire:click="toggleSection('support')"
|
||||
class="flex items-center justify-between w-full py-2 text-sm font-semibold text-gray-900 dark:text-gray-100 hover:text-[var(--color-primary)] transition-colors"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path>
|
||||
</svg>
|
||||
Hilfe & Support
|
||||
</span>
|
||||
<svg
|
||||
class="h-4 w-4 transition-transform {{ $this->isSectionOpen('support') ? 'rotate-180' : '' }}"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
@if($this->isSectionOpen('support'))
|
||||
<div class="mt-2 space-y-1 pl-6">
|
||||
<a href="/faq" class="block py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:translate-x-1 transition-all">
|
||||
FAQ
|
||||
</a>
|
||||
<a href="/hilfe" class="block py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:translate-x-1 transition-all">
|
||||
Hilfe-Center
|
||||
</a>
|
||||
<a href="/kontakt" class="block py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:translate-x-1 transition-all">
|
||||
Kontakt
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Rechtliches Section -->
|
||||
<div>
|
||||
<button
|
||||
wire:click="toggleSection('legal')"
|
||||
class="flex items-center justify-between w-full py-2 text-sm font-semibold text-gray-900 dark:text-gray-100 hover:text-[var(--color-primary)] transition-colors"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
|
||||
</svg>
|
||||
Rechtliches
|
||||
</span>
|
||||
<svg
|
||||
class="h-4 w-4 transition-transform {{ $this->isSectionOpen('legal') ? 'rotate-180' : '' }}"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
@if($this->isSectionOpen('legal'))
|
||||
<div class="mt-2 space-y-1 pl-6">
|
||||
<a href="/impressum" class="block py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:translate-x-1 transition-all">
|
||||
Impressum
|
||||
</a>
|
||||
<a href="/datenschutz" class="block py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:translate-x-1 transition-all">
|
||||
Datenschutz
|
||||
</a>
|
||||
<a href="/agb" class="block py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:translate-x-1 transition-all">
|
||||
AGB
|
||||
</a>
|
||||
<a href="/cookies" class="block py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:translate-x-1 transition-all">
|
||||
Cookie-Richtlinien
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Footer Actions -->
|
||||
<div class="sticky bottom-0 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-800 p-6 space-y-3 transition-colors duration-200">
|
||||
<a
|
||||
href="/veroeffentlichen"
|
||||
class="block w-full text-center px-4 py-3 text-sm font-medium text-white bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-secondary)] hover:from-[var(--color-primary)]/90 hover:to-[var(--color-secondary)]/90 rounded-lg shadow-md hover:shadow-lg transition-all"
|
||||
>
|
||||
Pressemitteilung veröffentlichen
|
||||
</a>
|
||||
<div class="flex gap-2">
|
||||
<a
|
||||
href="/login"
|
||||
class="flex-1 text-center px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg transition-all"
|
||||
>
|
||||
Anmelden
|
||||
</a>
|
||||
<a
|
||||
href="/register"
|
||||
class="flex-1 text-center px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg transition-all"
|
||||
>
|
||||
Registrieren
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Info -->
|
||||
<div class="px-6 pb-6">
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg transition-colors duration-200">
|
||||
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
<span>Kontakt</span>
|
||||
</div>
|
||||
<a href="mailto:info@businessportal.de" class="text-sm text-gray-900 dark:text-gray-100 hover:text-[var(--color-primary)] transition-colors">
|
||||
info@businessportal.de
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
101
resources/views/livewire/web/featured-releases.blade.php
Normal file
101
resources/views/livewire/web/featured-releases.blade.php
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
public function with(): array
|
||||
{
|
||||
// Mock-Daten für Featured Releases
|
||||
return [
|
||||
'releases' => [
|
||||
[
|
||||
'slug' => 'ki-revolution-deutsche-unternehmen',
|
||||
'title' => 'KI-Revolution: Deutsche Unternehmen investieren Milliarden in Automatisierung',
|
||||
'teaser' => 'Eine neue Studie zeigt: Unternehmen im DACH-Raum planen für 2025 Investitionen in Höhe von über 15 Milliarden Euro in KI-gestützte Automatisierungslösungen.',
|
||||
'company' => 'TechVision Analytics',
|
||||
'industry' => 'IT & Software',
|
||||
'region' => 'Deutschland',
|
||||
'date' => 'Heute, 14:30',
|
||||
'hasImage' => true,
|
||||
'hasPdf' => true,
|
||||
'imageUrl' => 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800&h=600&fit=crop',
|
||||
'companyLogo' => 'logo',
|
||||
],
|
||||
[
|
||||
'slug' => 'energiewende-beschleunigt',
|
||||
'title' => 'Energiewende beschleunigt sich: Neue Rekorde bei erneuerbaren Energien',
|
||||
'teaser' => 'Im ersten Quartal 2025 erreicht der Anteil erneuerbarer Energien am Strommix einen historischen Höchststand von 58%. Experten sprechen von einem Wendepunkt.',
|
||||
'company' => 'GreenPower Deutschland',
|
||||
'industry' => 'Energie',
|
||||
'region' => 'Deutschland',
|
||||
'date' => 'Heute, 11:15',
|
||||
'hasImage' => true,
|
||||
'imageUrl' => 'https://images.unsplash.com/photo-1466611653911-95081537e5b7?w=800&h=600&fit=crop',
|
||||
],
|
||||
[
|
||||
'slug' => 'fintech-startup-series-b',
|
||||
'title' => 'FinTech-Startup sichert sich 45 Millionen Euro in Series-B-Runde',
|
||||
'teaser' => 'Das Berliner FinTech-Startup PaymentFlow konnte in einer Series-B-Finanzierungsrunde 45 Millionen Euro einsammeln.',
|
||||
'company' => 'PaymentFlow GmbH',
|
||||
'industry' => 'Finanzen',
|
||||
'region' => 'Berlin',
|
||||
'date' => 'Gestern, 16:45',
|
||||
'hasPdf' => true,
|
||||
'imageUrl' => 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&h=600&fit=crop',
|
||||
],
|
||||
]
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Large Featured Card - Takes 2 columns -->
|
||||
<div class="lg:col-span-2">
|
||||
<x-web.press-release-card
|
||||
:title="$releases[0]['title']"
|
||||
:teaser="$releases[0]['teaser']"
|
||||
:company="$releases[0]['company']"
|
||||
:industry="$releases[0]['industry']"
|
||||
:region="$releases[0]['region']"
|
||||
:date="$releases[0]['date']"
|
||||
:hasImage="$releases[0]['hasImage']"
|
||||
:hasPdf="$releases[0]['hasPdf']"
|
||||
:slug="$releases[0]['slug']"
|
||||
:imageUrl="$releases[0]['imageUrl']"
|
||||
:companyLogo="$releases[0]['companyLogo']"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Two Smaller Featured Cards - Stack in right column -->
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-web.press-release-card
|
||||
:title="$releases[1]['title']"
|
||||
:teaser="$releases[1]['teaser']"
|
||||
:company="$releases[1]['company']"
|
||||
:industry="$releases[1]['industry']"
|
||||
:region="$releases[1]['region']"
|
||||
:date="$releases[1]['date']"
|
||||
:hasImage="$releases[1]['hasImage']"
|
||||
:hasPdf="$releases[1]['hasPdf'] ?? false"
|
||||
:slug="$releases[1]['slug']"
|
||||
:imageUrl="$releases[1]['imageUrl']"
|
||||
:companyLogo="null"
|
||||
/>
|
||||
|
||||
<x-web.press-release-card
|
||||
:title="$releases[2]['title']"
|
||||
:teaser="$releases[2]['teaser']"
|
||||
:company="$releases[2]['company']"
|
||||
:industry="$releases[2]['industry']"
|
||||
:region="$releases[2]['region']"
|
||||
:date="$releases[2]['date']"
|
||||
:hasImage="$releases[2]['hasImage'] ?? false"
|
||||
:hasPdf="$releases[2]['hasPdf']"
|
||||
:slug="$releases[2]['slug']"
|
||||
:imageUrl="$releases[2]['imageUrl']"
|
||||
:companyLogo="null"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
204
resources/views/livewire/web/filter-bar.blade.php
Normal file
204
resources/views/livewire/web/filter-bar.blade.php
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
<?php
|
||||
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
public $timeframe = '7';
|
||||
public $industry = 'all';
|
||||
public $region = 'all';
|
||||
public $sortBy = 'newest';
|
||||
public $activeFilters = [];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->updateActiveFilters();
|
||||
}
|
||||
|
||||
public function updated($property)
|
||||
{
|
||||
$this->updateActiveFilters();
|
||||
$this->dispatch('filters-updated', [
|
||||
'timeframe' => $this->timeframe,
|
||||
'industry' => $this->industry,
|
||||
'region' => $this->region,
|
||||
'sortBy' => $this->sortBy,
|
||||
]);
|
||||
}
|
||||
|
||||
public function removeFilter($filter)
|
||||
{
|
||||
$this->$filter = 'all';
|
||||
if ($filter === 'timeframe') {
|
||||
$this->timeframe = '7';
|
||||
}
|
||||
$this->updateActiveFilters();
|
||||
}
|
||||
|
||||
public function resetFilters()
|
||||
{
|
||||
$this->timeframe = '7';
|
||||
$this->industry = 'all';
|
||||
$this->region = 'all';
|
||||
$this->sortBy = 'newest';
|
||||
$this->updateActiveFilters();
|
||||
}
|
||||
|
||||
private function updateActiveFilters()
|
||||
{
|
||||
$this->activeFilters = [];
|
||||
|
||||
if ($this->timeframe !== '7') {
|
||||
$this->activeFilters[] = ['key' => 'timeframe', 'label' => $this->getTimeframeLabel()];
|
||||
}
|
||||
if ($this->industry !== 'all') {
|
||||
$this->activeFilters[] = ['key' => 'industry', 'label' => $this->getIndustryLabel()];
|
||||
}
|
||||
if ($this->region !== 'all') {
|
||||
$this->activeFilters[] = ['key' => 'region', 'label' => $this->getRegionLabel()];
|
||||
}
|
||||
}
|
||||
|
||||
private function getTimeframeLabel()
|
||||
{
|
||||
return match($this->timeframe) {
|
||||
'today' => 'Heute',
|
||||
'7' => '7 Tage',
|
||||
'30' => '30 Tage',
|
||||
default => 'Zeitraum'
|
||||
};
|
||||
}
|
||||
|
||||
private function getIndustryLabel()
|
||||
{
|
||||
return match($this->industry) {
|
||||
'it' => 'IT & Software',
|
||||
'finance' => 'Finanzen',
|
||||
'health' => 'Gesundheit',
|
||||
'auto' => 'Automobil',
|
||||
'energy' => 'Energie',
|
||||
default => 'Alle Branchen'
|
||||
};
|
||||
}
|
||||
|
||||
private function getRegionLabel()
|
||||
{
|
||||
return match($this->region) {
|
||||
'de' => 'Deutschland',
|
||||
'at' => 'Österreich',
|
||||
'ch' => 'Schweiz',
|
||||
default => 'Alle Regionen'
|
||||
};
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div>
|
||||
<div class="sticky top-16 z-40 bg-white/95 dark:bg-gray-900/95 backdrop-blur-sm border-b border-gray-200 dark:border-gray-800 shadow-sm transition-colors duration-200">
|
||||
<div class="container mx-auto px-4 py-4">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<!-- Zeitraum -->
|
||||
<div class="relative">
|
||||
<select
|
||||
wire:model.live="timeframe"
|
||||
class="appearance-none pl-10 pr-8 py-2 text-sm border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent transition-all hover:border-gray-400 dark:hover:border-gray-600 cursor-pointer"
|
||||
>
|
||||
<option value="today">Heute</option>
|
||||
<option value="7">7 Tage</option>
|
||||
<option value="30">30 Tage</option>
|
||||
<option value="custom">Zeitraum</option>
|
||||
</select>
|
||||
<svg class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500 dark:text-gray-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
<svg class="absolute right-2 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500 dark:text-gray-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Branche -->
|
||||
<div class="relative">
|
||||
<select
|
||||
wire:model.live="industry"
|
||||
class="appearance-none pl-10 pr-8 py-2 text-sm border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent transition-all hover:border-gray-400 dark:hover:border-gray-600 cursor-pointer"
|
||||
>
|
||||
<option value="all">Alle Branchen</option>
|
||||
<option value="it">IT & Software</option>
|
||||
<option value="finance">Finanzen</option>
|
||||
<option value="health">Gesundheit</option>
|
||||
<option value="auto">Automobil</option>
|
||||
<option value="energy">Energie</option>
|
||||
</select>
|
||||
<svg class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500 dark:text-gray-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path>
|
||||
</svg>
|
||||
<svg class="absolute right-2 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500 dark:text-gray-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Region -->
|
||||
<div class="relative">
|
||||
<select
|
||||
wire:model.live="region"
|
||||
class="appearance-none pl-10 pr-8 py-2 text-sm border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent transition-all hover:border-gray-400 dark:hover:border-gray-600 cursor-pointer"
|
||||
>
|
||||
<option value="all">Alle Regionen</option>
|
||||
<option value="de">Deutschland</option>
|
||||
<option value="at">Österreich</option>
|
||||
<option value="ch">Schweiz</option>
|
||||
</select>
|
||||
<svg class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500 dark:text-gray-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
<svg class="absolute right-2 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500 dark:text-gray-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Sortierung -->
|
||||
<div class="relative">
|
||||
<select
|
||||
wire:model.live="sortBy"
|
||||
class="appearance-none pl-10 pr-8 py-2 text-sm border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent transition-all hover:border-gray-400 dark:hover:border-gray-600 cursor-pointer"
|
||||
>
|
||||
<option value="newest">Neueste</option>
|
||||
<option value="relevance">Relevanz</option>
|
||||
</select>
|
||||
<svg class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500 dark:text-gray-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"></path>
|
||||
</svg>
|
||||
<svg class="absolute right-2 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500 dark:text-gray-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Active Filters Display -->
|
||||
@if(count($activeFilters) > 0)
|
||||
<div class="flex items-center gap-2 flex-wrap ml-auto">
|
||||
@foreach($activeFilters as $filter)
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium bg-[var(--color-primary)]/10 dark:bg-[var(--color-primary)]/20 text-[var(--color-primary)] border border-[var(--color-primary)]/20 dark:border-[var(--color-primary)]/30 transition-all hover:bg-[var(--color-primary)]/20 dark:hover:bg-[var(--color-primary)]/30">
|
||||
{{ $filter['label'] }}
|
||||
<button
|
||||
wire:click="removeFilter('{{ $filter['key'] }}')"
|
||||
class="hover:text-[var(--color-primary)]/70 transition-colors"
|
||||
>
|
||||
<svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
@endforeach
|
||||
|
||||
<!-- Reset Button -->
|
||||
<button
|
||||
wire:click="resetFilters"
|
||||
class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
|
||||
>
|
||||
Alle zurücksetzen
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
127
resources/views/livewire/web/footer.blade.php
Normal file
127
resources/views/livewire/web/footer.blade.php
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<?php
|
||||
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
public $darkMode = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
// Check if user has a preference stored
|
||||
$this->darkMode = false;
|
||||
}
|
||||
|
||||
public function toggleTheme()
|
||||
{
|
||||
$this->darkMode = !$this->darkMode;
|
||||
$this->dispatch('theme-changed', darkMode: $this->darkMode);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div x-data="{ darkMode: $wire.entangle('darkMode') }" x-init="
|
||||
darkMode = localStorage.getItem('theme') === 'dark';
|
||||
$watch('darkMode', value => {
|
||||
localStorage.setItem('theme', value ? 'dark' : 'light');
|
||||
if (value) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
});
|
||||
// Initialize theme on load
|
||||
if (darkMode) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
">
|
||||
<footer class="bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-800 py-8 mt-auto transition-colors duration-200">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-8 mb-8">
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-3">Unternehmen</h3>
|
||||
<ul class="space-y-2">
|
||||
<li><a href="/ueber-uns" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors">Über uns</a></li>
|
||||
<li><a href="/kontakt" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors">Kontakt</a></li>
|
||||
<li><a href="/presse" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors">Presse</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-3">Services</h3>
|
||||
<ul class="space-y-2">
|
||||
<li><a href="/preise" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors">Preise</a></li>
|
||||
<li><a href="/newsrooms" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors">Newsrooms</a></li>
|
||||
<li><a href="/api" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors">API</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-3">Rechtliches</h3>
|
||||
<ul class="space-y-2">
|
||||
<li><a href="/impressum" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors">Impressum</a></li>
|
||||
<li><a href="/datenschutz" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors">Datenschutz</a></li>
|
||||
<li><a href="/agb" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors">AGB</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-3">Folgen Sie uns</h3>
|
||||
<ul class="space-y-2">
|
||||
<li><a href="https://linkedin.com" target="_blank" rel="noopener" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors">LinkedIn</a></li>
|
||||
<li><a href="https://twitter.com" target="_blank" rel="noopener" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors">Twitter</a></li>
|
||||
<li><a href="/rss" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors">RSS</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$theme = config('app.theme', 'businessportal24');
|
||||
$isPresseecho = $theme === 'presseecho';
|
||||
$currentYear = date('Y');
|
||||
$siteName = $isPresseecho ? 'Presseecho' : 'Business Portal';
|
||||
@endphp
|
||||
|
||||
<!-- Cross-Link für Presseecho -->
|
||||
@if($isPresseecho)
|
||||
<div class="mb-6 p-5 bg-gradient-to-r from-[var(--color-primary)]/5 to-[var(--color-secondary)]/5 dark:from-[var(--color-primary)]/10 dark:to-[var(--color-secondary)]/10 rounded-lg border border-[var(--color-primary)]/10">
|
||||
<div class="flex items-center justify-between gap-4 flex-wrap">
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1">
|
||||
Für maximale Reichweite?
|
||||
</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Besuchen Sie unser reichweitenstarkes Portal <strong class="text-gray-900 dark:text-gray-100">Businessportal24</strong>
|
||||
</p>
|
||||
</div>
|
||||
<a href="https://businessportal24.test"
|
||||
class="inline-flex items-center gap-2 px-5 py-2.5 text-sm font-medium text-white bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-secondary)] hover:from-[var(--color-primary)]/90 hover:to-[var(--color-secondary)]/90 rounded-lg shadow-md hover:shadow-lg transition-all whitespace-nowrap">
|
||||
Zu Businessportal24
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<div class="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">© {{ $currentYear }} {{ $siteName }}. Alle Rechte vorbehalten.</p>
|
||||
|
||||
<!-- Theme Toggle -->
|
||||
<button
|
||||
@click="darkMode = !darkMode"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-all"
|
||||
aria-label="Theme wechseln"
|
||||
>
|
||||
<!-- Sun Icon (zeigt in Dark Mode) -->
|
||||
<svg x-show="darkMode" class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" x-cloak>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path>
|
||||
</svg>
|
||||
<!-- Moon Icon (zeigt in Light Mode) -->
|
||||
<svg x-show="!darkMode" class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
|
||||
</svg>
|
||||
<span x-text="darkMode ? 'Hell' : 'Dunkel'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
142
resources/views/livewire/web/header.blade.php
Normal file
142
resources/views/livewire/web/header.blade.php
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
<?php
|
||||
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
public $searchQuery = '';
|
||||
public $showMobileSearch = false;
|
||||
|
||||
public function search()
|
||||
{
|
||||
// Implementierung der Suchlogik
|
||||
$this->dispatch('search-pressed', query: $this->searchQuery);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div>
|
||||
<header
|
||||
class="sticky top-0 z-50 bg-white dark:bg-gray-900 border-b border-gray-200/50 dark:border-gray-800/50 shadow-sm backdrop-blur-sm transition-colors duration-200">
|
||||
<!-- Brand Accent Bar -->
|
||||
<div
|
||||
class="h-1 bg-gradient-to-r from-[var(--color-primary)] via-[var(--color-secondary)] to-[var(--color-primary)]">
|
||||
</div>
|
||||
|
||||
<div class="container mx-auto px-4 h-16 flex items-center justify-between gap-4">
|
||||
<!-- Burger Menu + Logo -->
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Burger Menu Button (immer sichtbar) -->
|
||||
<button @click="$dispatch('toggle-mobile-menu')"
|
||||
class="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||
aria-label="Menü öffnen">
|
||||
<svg class="h-6 w-6 text-gray-900 dark:text-gray-100" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<a href="/" class="flex-shrink-0 flex items-center gap-3">
|
||||
@php
|
||||
$theme = config('app.theme', 'businessportal24');
|
||||
|
||||
// Versuche verschiedene Logo-Namenskonventionen
|
||||
$logoVariants = [
|
||||
"images/{$theme}-logo.svg",
|
||||
"images/" . str_replace('24', '', $theme) . "-logo.svg", // businessportal-logo.svg
|
||||
];
|
||||
|
||||
$logoPath = null;
|
||||
foreach ($logoVariants as $variant) {
|
||||
if (file_exists(public_path($variant))) {
|
||||
$logoPath = $variant;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$altText = ucfirst(str_replace(['24', '-'], [' ', ' '], $theme));
|
||||
@endphp
|
||||
|
||||
@if ($logoPath)
|
||||
<img src="{{ asset($logoPath) }}" alt="{{ $altText }}"
|
||||
class="h-12 w-auto dark:brightness-0 dark:invert">
|
||||
@else
|
||||
<div
|
||||
class="h-10 w-10 rounded-lg bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-secondary)] flex items-center justify-center shadow-md">
|
||||
<span class="text-white font-bold text-lg">{{ strtoupper(substr($theme, 0, 2)) }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Search - Desktop -->
|
||||
<div class="hidden md:flex flex-1 max-w-xl">
|
||||
<form wire:submit.prevent="search" class="relative w-full">
|
||||
<svg class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
<input type="search" wire:model="searchQuery" placeholder="Pressemitteilungen durchsuchen..."
|
||||
class="pl-10 w-full rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent transition-all" />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Search Icon - Mobile -->
|
||||
<button @click="$wire.showMobileSearch = !$wire.showMobileSearch"
|
||||
class="md:hidden p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors">
|
||||
<svg class="h-5 w-5 text-gray-900 dark:text-gray-100" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- CTA Buttons - Domain-specific -->
|
||||
<div class="flex items-center gap-2">
|
||||
@php
|
||||
$theme = config('app.theme', 'businessportal24');
|
||||
$isPresseecho = $theme === 'presseecho';
|
||||
@endphp
|
||||
|
||||
@if ($isPresseecho)
|
||||
<!-- Presseecho: Dezente Navigation -->
|
||||
<a href="/login"
|
||||
class="hidden sm:inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors">
|
||||
Anmelden
|
||||
</a>
|
||||
<a href="/beitrag-einreichen"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors border border-gray-300 dark:border-gray-700">
|
||||
Beitrag einreichen
|
||||
</a>
|
||||
@else
|
||||
<!-- Businessportal24: Prominenter CTA -->
|
||||
<a href="/login"
|
||||
class="hidden sm:inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors">
|
||||
Anmelden
|
||||
</a>
|
||||
<a href="/veroeffentlichen"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-secondary)] hover:from-[var(--color-primary)]/90 hover:to-[var(--color-secondary)]/90 rounded-lg shadow-md hover:shadow-lg transition-all">
|
||||
Veröffentlichen
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Search Bar -->
|
||||
<div x-show="$wire.showMobileSearch" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2" x-transition:enter-end="opacity-100 translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100 translate-y-0"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2"
|
||||
class="md:hidden border-t border-gray-200 dark:border-gray-800 px-4 py-3 bg-white dark:bg-gray-900" x-cloak>
|
||||
<form wire:submit.prevent="search" class="relative">
|
||||
<svg class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400 dark:text-gray-500"
|
||||
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
<input type="search" wire:model="searchQuery" placeholder="Pressemitteilungen durchsuchen..."
|
||||
class="pl-10 w-full rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent transition-all" />
|
||||
</form>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
130
resources/views/livewire/web/press-releases-grid.blade.php
Normal file
130
resources/views/livewire/web/press-releases-grid.blade.php
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
<?php
|
||||
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
public function with(): array
|
||||
{
|
||||
// Mock-Daten für Press Releases Grid
|
||||
return [
|
||||
'releases' => [
|
||||
[
|
||||
'slug' => 'ki-revolution-deutsche-unternehmen',
|
||||
'title' => 'KI-Revolution: Deutsche Unternehmen investieren Milliarden in Automatisierung',
|
||||
'teaser' => 'Eine neue Studie zeigt: Unternehmen im DACH-Raum planen für 2025 Investitionen in Höhe von über 15 Milliarden Euro in KI-gestützte Automatisierungslösungen.',
|
||||
'company' => 'TechVision Analytics',
|
||||
'industry' => 'IT & Software',
|
||||
'region' => 'Deutschland',
|
||||
'date' => '17. Okt 2024',
|
||||
'contentType' => 'ANALYSE',
|
||||
'hasImage' => true,
|
||||
'hasPdf' => true,
|
||||
'imageUrl' => 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800&h=600&fit=crop',
|
||||
'companyLogo' => 'logo',
|
||||
],
|
||||
[
|
||||
'slug' => 'energiewende-beschleunigt',
|
||||
'title' => 'Energiewende beschleunigt sich: Neue Rekorde bei erneuerbaren Energien',
|
||||
'teaser' => 'Im ersten Quartal 2025 erreicht der Anteil erneuerbarer Energien am Strommix einen historischen Höchststand von 58%.',
|
||||
'company' => 'GreenPower Deutschland',
|
||||
'industry' => 'Energie',
|
||||
'region' => 'Deutschland',
|
||||
'date' => '16. Okt 2024',
|
||||
'contentType' => 'FACHMELDUNG',
|
||||
'hasImage' => true,
|
||||
'imageUrl' => 'https://images.unsplash.com/photo-1466611653911-95081537e5b7?w=800&h=600&fit=crop',
|
||||
],
|
||||
[
|
||||
'slug' => 'fintech-startup-series-b',
|
||||
'title' => 'FinTech-Startup sichert sich 45 Millionen Euro in Series-B-Runde',
|
||||
'teaser' => 'Das Berliner FinTech-Startup PaymentFlow konnte in einer Series-B-Finanzierungsrunde 45 Millionen Euro einsammeln.',
|
||||
'company' => 'PaymentFlow GmbH',
|
||||
'industry' => 'Finanzen',
|
||||
'region' => 'Berlin',
|
||||
'date' => '15. Okt 2024',
|
||||
'contentType' => 'FACHMELDUNG',
|
||||
'hasPdf' => true,
|
||||
'imageUrl' => null,
|
||||
],
|
||||
[
|
||||
'slug' => 'gesundheitsbranche-digital',
|
||||
'title' => 'Gesundheitsbranche setzt verstärkt auf digitale Lösungen',
|
||||
'teaser' => 'Telemedizin und KI-gestützte Diagnostik werden zum Standard: 78% der Krankenhäuser in Deutschland planen Investitionen.',
|
||||
'company' => 'MediTech Solutions',
|
||||
'industry' => 'Gesundheit',
|
||||
'region' => 'München',
|
||||
'date' => '14. Okt 2024',
|
||||
'contentType' => 'INTERVIEW',
|
||||
'hasImage' => true,
|
||||
'imageUrl' => 'https://images.unsplash.com/photo-1576091160399-112ba8d25d1d?w=800&h=600&fit=crop',
|
||||
],
|
||||
[
|
||||
'slug' => 'automobilindustrie-transformation',
|
||||
'title' => 'Automobilindustrie: Transformation zur E-Mobilität nimmt Fahrt auf',
|
||||
'teaser' => 'Führende Automobilhersteller kündigen massive Investitionen in E-Mobilität an. Bis 2030 sollen 80% der Neufahrzeuge elektrisch sein.',
|
||||
'company' => 'Auto Industry Report',
|
||||
'industry' => 'Automobil',
|
||||
'region' => 'Stuttgart',
|
||||
'date' => '13. Okt 2024',
|
||||
'contentType' => 'ANALYSE',
|
||||
'hasImage' => true,
|
||||
'hasPdf' => true,
|
||||
'imageUrl' => 'https://images.unsplash.com/photo-1593941707882-a5bba14938c7?w=800&h=600&fit=crop',
|
||||
],
|
||||
[
|
||||
'slug' => 'cybersecurity-massnahmen',
|
||||
'title' => 'Cybersecurity: Unternehmen verstärken Schutzmaßnahmen gegen Hackerangriffe',
|
||||
'teaser' => 'Nach einer Serie von Cyberattacken erhöhen deutsche Unternehmen ihre Investitionen in IT-Sicherheit um durchschnittlich 35%.',
|
||||
'company' => 'CyberSafe Europe',
|
||||
'industry' => 'IT & Software',
|
||||
'region' => 'Frankfurt',
|
||||
'date' => '12. Okt 2024',
|
||||
'contentType' => 'FACHMELDUNG',
|
||||
'hasImage' => true,
|
||||
'imageUrl' => 'https://images.unsplash.com/photo-1550751827-4bd374c3f58b?w=800&h=600&fit=crop',
|
||||
],
|
||||
]
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div>
|
||||
@php
|
||||
$theme = config('app.theme', 'businessportal24');
|
||||
$isPresseecho = $theme === 'presseecho';
|
||||
@endphp
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
@foreach($releases as $release)
|
||||
@if($isPresseecho)
|
||||
{{-- Presseecho: Hochwertige Magazine-Card --}}
|
||||
<x-web.presseecho-release-card
|
||||
:title="$release['title']"
|
||||
:teaser="$release['teaser']"
|
||||
:company="$release['company']"
|
||||
:industry="$release['industry']"
|
||||
:region="$release['region']"
|
||||
:date="$release['date']"
|
||||
:contentType="$release['contentType'] ?? 'FACHMELDUNG'"
|
||||
:slug="$release['slug']"
|
||||
:imageUrl="$release['imageUrl'] ?? null"
|
||||
/>
|
||||
@else
|
||||
{{-- Businessportal24: Standard-Card --}}
|
||||
<x-web.press-release-card
|
||||
:title="$release['title']"
|
||||
:teaser="$release['teaser']"
|
||||
:company="$release['company']"
|
||||
:industry="$release['industry']"
|
||||
:region="$release['region']"
|
||||
:date="$release['date']"
|
||||
:hasImage="$release['hasImage'] ?? false"
|
||||
:hasPdf="$release['hasPdf'] ?? false"
|
||||
:slug="$release['slug']"
|
||||
:imageUrl="$release['imageUrl'] ?? null"
|
||||
:companyLogo="$release['companyLogo'] ?? null"
|
||||
/>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
Loading…
Add table
Add a link
Reference in a new issue