first commit
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled

This commit is contained in:
Kevin Adametz 2025-10-20 17:53:02 +02:00
commit 405df0a122
3083 changed files with 69203 additions and 0 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>