10-04-2026

This commit is contained in:
Kevin Adametz 2026-04-10 17:18:17 +02:00
parent 4d6b4930b2
commit 4bb89aad8c
836 changed files with 52961 additions and 5950 deletions

View file

@ -160,9 +160,47 @@
</flux:navlist.group>
<flux:navlist.group :heading="__('CMS')" class="grid mb-4">
<flux:navlist.item icon="home" :href="route('admin.cms.cabinet')"
:current="request()->routeIs('admin.cms.cabinet')" wire:navigate>{{ __('Cabinet') }}
</flux:navlist.item>
<flux:navlist.group expandable
:expanded="request()->routeIs(['cms.dashboard', 'cms.content.index', 'cms.projects.index', 'cms.articles.index', 'cms.media.index'])"
heading="Website-Inhalte" class="grid">
<flux:navlist.item icon="rectangle-group" :href="route('cms.dashboard')"
:current="request()->routeIs('cms.dashboard')" wire:navigate>{{ __('Übersicht') }}
</flux:navlist.item>
<flux:navlist.item icon="document-text" :href="route('cms.content.index')"
:current="request()->routeIs('cms.content.index')" wire:navigate>{{ __('Inhalte') }}
</flux:navlist.item>
<flux:navlist.item icon="building-office" :href="route('cms.projects.index')"
:current="request()->routeIs('cms.projects.index')" wire:navigate>{{ __('Projekte') }}
</flux:navlist.item>
<flux:navlist.item icon="newspaper" :href="route('cms.articles.index')"
:current="request()->routeIs('cms.articles.index')" wire:navigate>{{ __('Magazin') }}
</flux:navlist.item>
<flux:navlist.item icon="photo" :href="route('cms.media.index')"
:current="request()->routeIs('cms.media.index')" wire:navigate>{{ __('Medien') }}
</flux:navlist.item>
</flux:navlist.group>
</flux:navlist.group>
<flux:navlist.group :heading="__('Cabinet')" class="grid mb-4">
<flux:navlist.group expandable
:expanded="request()->routeIs(['admin.cms.display-dashboard', 'admin.cms.display-media', 'admin.cms.display-versions', 'admin.cms.display-version-edit', 'admin.cms.displays', 'admin.cms.cabinet', 'admin.cms.cabinet-tablet'])"
heading="Store Displays" class="grid">
<flux:navlist.item icon="squares-2x2" :href="route('admin.cms.display-dashboard')"
:current="request()->routeIs('admin.cms.display-dashboard')" wire:navigate>{{ __('Übersicht') }}
</flux:navlist.item>
<flux:navlist.item icon="photo" :href="route('admin.cms.display-media')"
:current="request()->routeIs('admin.cms.display-media')" wire:navigate>{{ __('Mediathek') }}
</flux:navlist.item>
<flux:navlist.item icon="rectangle-group" :href="route('admin.cms.display-versions')"
:current="request()->routeIs(['admin.cms.display-versions', 'admin.cms.display-version-edit'])" wire:navigate>{{ __('Versionen') }}
</flux:navlist.item>
<flux:navlist.item icon="tv" :href="route('admin.cms.displays')"
:current="request()->routeIs('admin.cms.displays')" wire:navigate>{{ __('Displays') }}
</flux:navlist.item>
<flux:navlist.item icon="device-tablet" :href="route('admin.cms.cabinet-tablet')"
:current="request()->routeIs('admin.cms.cabinet-tablet')" wire:navigate>{{ __('Info-Tablet') }}
</flux:navlist.item>
</flux:navlist.group>
</flux:navlist.group>
@endhasrole
@ -342,7 +380,9 @@
<flux:toast />
{{-- Flux vor Livewire: flux.js registriert Alpine.data('fluxModal') im alpine:init-Handler --}}
@fluxScripts
@livewireScripts
</body>
</html>

View file

@ -0,0 +1,18 @@
@if ($hasWebp)
<picture>
<source srcset="{{ $webpSrc }}" type="image/webp">
<img src="{{ $src }}" alt="{{ $alt }}" class="{{ $class }}"
@if($loading) loading="{{ $loading }}" @endif
@if($width) width="{{ $width }}" @endif
@if($height) height="{{ $height }}" @endif
{{ $attributes->except(['src', 'alt', 'class', 'loading', 'width', 'height']) }}
/>
</picture>
@else
<img src="{{ $src }}" alt="{{ $alt }}" class="{{ $class }}"
@if($loading) loading="{{ $loading }}" @endif
@if($width) width="{{ $width }}" @endif
@if($height) height="{{ $height }}" @endif
{{ $attributes->except(['src', 'alt', 'class', 'loading', 'width', 'height']) }}
/>
@endif

View file

@ -0,0 +1,73 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="CABINET Status">
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#0f0f0f">
<link rel="apple-touch-icon" sizes="57x57" href="/favicon/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/favicon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/favicon/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/favicon/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/favicon/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/favicon/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/favicon/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/favicon/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/favicon/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="96x96" href="/favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
<link rel="shortcut icon" href="/favicon/favicon.ico">
<link rel="manifest" href="/favicon/manifest.json">
<meta name="msapplication-TileColor" content="#0f0f0f">
<meta name="msapplication-TileImage" content="/favicon/ms-icon-144x144.png">
<title>CABINET · Status</title>
@livewireStyles
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0f0f0f;
--surface: #1a1a1a;
--surface-2: #242424;
--border: rgba(255,255,255,0.08);
--fg: #f0f0f0;
--muted: rgba(255,255,255,0.45);
--safe-top: env(safe-area-inset-top, 20px);
--safe-bottom: env(safe-area-inset-bottom, 20px);
}
html, body {
height: 100%;
background: var(--bg);
color: var(--fg);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
overscroll-behavior: none;
}
.app {
min-height: 100svh;
display: flex;
flex-direction: column;
max-width: 480px;
margin: 0 auto;
padding: calc(var(--safe-top) + 20px) 20px calc(var(--safe-bottom) + 20px);
gap: 20px;
}
</style>
</head>
<body>
<div class="app">
{{ $slot }}
</div>
@livewireScripts
</body>
</html>

View file

@ -0,0 +1,367 @@
<?php
use App\Models\CmsArticle;
use Flux\Flux;
use function Livewire\Volt\{layout, title, state, computed, on};
layout('components.layouts.app');
title('Magazin verwalten');
state([
'search' => '',
'showForm' => false,
'editingId' => null,
'editLocale' => 'de',
'slug' => '',
'articleTitle' => '',
'subtitle' => '',
'image' => '',
'category' => '',
'date_label' => '',
'read_time' => '',
'authorName' => '',
'authorBio' => '',
'authorAvatar' => '',
'intro' => '',
'sections' => [],
'is_published' => true,
'order' => 0,
]);
on(['media-selected' => function ($mediaId, $url, $field) {
$media = $mediaId ? \FluxCms\Core\Models\CmsMedia::find($mediaId) : null;
if ($field === 'article_image' && $media) {
$this->image = $media->filename;
}
if ($field === 'author_avatar' && $media) {
$this->authorAvatar = $media->filename;
}
}]);
$articles = computed(
fn () => CmsArticle::query()
->when($this->search, fn ($q, $s) => $q->where('slug', 'like', "%{$s}%")
->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(title, '$.de')) LIKE ?", ["%{$s}%"]))
->ordered()
->get(),
);
$openCreate = function () {
$this->editingId = null;
$this->reset(['slug', 'articleTitle', 'subtitle', 'image', 'category', 'date_label', 'read_time', 'authorName', 'authorBio', 'authorAvatar', 'intro', 'sections']);
$this->is_published = true;
$this->order = 0;
$this->sections = [['title' => '', 'content' => '']];
$this->showForm = true;
};
$openEdit = function (int $id) {
$article = CmsArticle::find($id);
if (! $article) {
return;
}
$this->editingId = $id;
$this->slug = $article->slug;
$this->image = $article->image ?? '';
$this->category = $article->category ?? '';
$this->date_label = $article->date_label ?? '';
$this->read_time = $article->read_time ?? '';
$this->is_published = $article->is_published;
$this->order = $article->order ?? 0;
$author = $article->author ?? [];
$this->authorName = $author['name'] ?? '';
$this->authorBio = $author['bio'] ?? '';
$this->authorAvatar = $author['avatar'] ?? '';
$this->loadLocaleFields($article, $this->editLocale);
$this->showForm = true;
};
$loadLocaleFields = function (CmsArticle $article, string $locale) {
$this->articleTitle = $article->getTranslation('title', $locale) ?? '';
$this->subtitle = $article->getTranslation('subtitle', $locale) ?? '';
$content = $article->getTranslation('content', $locale);
$this->intro = $content['intro'] ?? '';
$this->sections = $content['sections'] ?? [['title' => '', 'content' => '']];
};
$switchLocale = function (string $locale) {
$this->editLocale = $locale;
if ($this->editingId) {
$article = CmsArticle::find($this->editingId);
if ($article) {
$this->loadLocaleFields($article, $locale);
}
}
};
$addSection = function () {
$this->sections[] = ['title' => '', 'content' => ''];
};
$removeSection = function (int $index) {
unset($this->sections[$index]);
$this->sections = array_values($this->sections);
};
$save = function () {
$validated = validator([
'slug' => $this->slug,
'articleTitle' => $this->articleTitle,
'subtitle' => $this->subtitle,
'image' => $this->image,
'category' => $this->category,
'date_label' => $this->date_label,
'read_time' => $this->read_time,
'authorName' => $this->authorName,
'intro' => $this->intro,
'is_published' => $this->is_published,
'order' => $this->order,
], [
'slug' => 'required|string|max:255',
'articleTitle' => 'required|string|max:500',
'subtitle' => 'nullable|string|max:1000',
'image' => 'nullable|string|max:500',
'category' => 'nullable|string|max:255',
'date_label' => 'nullable|string|max:100',
'read_time' => 'nullable|string|max:50',
'authorName' => 'nullable|string|max:255',
'intro' => 'nullable|string',
'is_published' => 'boolean',
'order' => 'integer|min:0',
])->validate();
$article = $this->editingId
? CmsArticle::findOrFail($this->editingId)
: CmsArticle::query()->make();
$article->slug = $validated['slug'];
$article->setTranslation('title', $this->editLocale, $validated['articleTitle']);
$article->setTranslation('subtitle', $this->editLocale, $validated['subtitle'] ?? '');
$contentData = [
'intro' => $validated['intro'] ?? '',
'sections' => collect($this->sections)
->filter(fn ($s) => ! empty($s['title']) || ! empty($s['content']))
->values()
->toArray(),
];
$article->setTranslation('content', $this->editLocale, $contentData);
$article->image = $validated['image'] ?? null;
$article->category = $validated['category'] ?? null;
$article->date_label = $validated['date_label'] ?? null;
$article->read_time = $validated['read_time'] ?? null;
$article->author = [
'name' => $this->authorName,
'bio' => $this->authorBio,
'avatar' => $this->authorAvatar,
];
$article->is_published = $validated['is_published'];
$article->order = $validated['order'];
$article->save();
$this->showForm = false;
$this->editingId = null;
Flux::toast(variant: 'success', heading: 'Gespeichert', text: 'Artikel wurde erfolgreich gespeichert.');
};
$togglePublished = function (int $id) {
$article = CmsArticle::findOrFail($id);
$article->update(['is_published' => ! $article->is_published]);
Flux::toast(heading: 'Status geändert', text: $article->is_published ? 'Veröffentlicht' : 'Entwurf');
};
$deleteArticle = function (int $id) {
$article = CmsArticle::findOrFail($id);
$article->delete();
Flux::toast(variant: 'success', heading: 'Gelöscht', text: 'Artikel wurde entfernt.');
};
$cancelForm = function () {
$this->showForm = false;
$this->editingId = null;
};
?>
<div>
<div class="mb-6 flex items-center justify-between">
<flux:heading size="xl">Magazin Beiträge</flux:heading>
<div class="flex items-center gap-2">
@foreach (config('flux-cms.locales', ['de' => 'DE', 'en' => 'EN']) as $code => $label)
<flux:button size="xs" :variant="$editLocale === $code ? 'primary' : 'ghost'"
wire:click="switchLocale('{{ $code }}')">{{ strtoupper($code) }}</flux:button>
@endforeach
<flux:button variant="primary" icon="plus" wire:click="openCreate">Neuer Artikel</flux:button>
</div>
</div>
<div class="mb-4">
<flux:input wire:model.live.debounce.300ms="search" placeholder="Artikel suchen..." icon="magnifying-glass"
size="sm" class="w-64" />
</div>
@if ($showForm)
<flux:card class="mb-6">
<div class="mb-4 flex items-center justify-between">
<flux:heading size="lg">{{ $editingId ? 'Artikel bearbeiten' : 'Neuer Artikel' }}</flux:heading>
<div class="flex gap-1">
@foreach (config('flux-cms.locales', ['de' => 'DE', 'en' => 'EN']) as $code => $label)
<flux:button size="xs" :variant="$editLocale === $code ? 'primary' : 'ghost'"
wire:click="switchLocale('{{ $code }}')">{{ strtoupper($code) }}</flux:button>
@endforeach
</div>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<flux:input wire:model="slug" label="Slug" placeholder="escrow-system-dubai-investoren" />
<flux:input wire:model="articleTitle" label="Titel ({{ strtoupper($editLocale) }})" placeholder="Artikeltitel" />
<div class="md:col-span-2">
<flux:input wire:model="subtitle" label="Untertitel ({{ strtoupper($editLocale) }})" placeholder="Kurze Beschreibung" />
</div>
<flux:input wire:model="category" label="Kategorie" placeholder="z.B. Dubai Investment" />
<flux:input wire:model="date_label" label="Datum (Anzeige)" placeholder="März 10, 2026" />
<flux:input wire:model="read_time" label="Lesezeit" placeholder="6 min read" />
<flux:input wire:model="order" label="Sortierung" type="number" />
</div>
{{-- Bild --}}
<div class="mt-4">
<label class="mb-1 block text-sm font-medium text-zinc-700 dark:text-zinc-300">Artikelbild</label>
<div class="flex items-start gap-3">
@if ($image)
<div class="h-16 w-24 shrink-0 overflow-hidden rounded-lg border border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800">
<img src="{{ media_url($image) }}" class="h-full w-full object-cover" />
</div>
@endif
<div class="flex-1">
<livewire:admin.cms.media-picker
:value="null"
field="article_image"
type="image"
profile="card"
label="Bild wählen"
:key="'article-img-' . ($editingId ?? 'new')" />
<p class="mt-1 text-xs text-zinc-400">{{ $image ?: 'Kein Bild' }}</p>
</div>
</div>
</div>
{{-- Autor --}}
<div class="mt-6">
<flux:heading size="sm" class="mb-3">Autor</flux:heading>
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<flux:input wire:model="authorName" label="Name" placeholder="Marcel Scheibe" />
<flux:input wire:model="authorBio" label="Bio" placeholder="Kurze Biografie" />
<div>
<label class="mb-1 block text-sm font-medium text-zinc-700 dark:text-zinc-300">Avatar</label>
<div class="flex items-center gap-2">
@if ($authorAvatar)
<div class="h-10 w-10 shrink-0 overflow-hidden rounded-full border border-zinc-200 dark:border-zinc-700">
<img src="{{ media_url($authorAvatar) }}" class="h-full w-full object-cover" />
</div>
@endif
<livewire:admin.cms.media-picker
:value="null"
field="author_avatar"
type="image"
label="Avatar wählen"
:key="'author-avatar-' . ($editingId ?? 'new')" />
</div>
</div>
</div>
</div>
{{-- Inhalt --}}
<div class="mt-6">
<flux:heading size="sm" class="mb-3">Inhalt ({{ strtoupper($editLocale) }})</flux:heading>
<flux:textarea wire:model="intro" label="Einleitung" rows="4" placeholder="Einleitungstext des Artikels..." />
<div class="mt-4">
<div class="mb-2 flex items-center justify-between">
<label class="text-sm font-medium text-zinc-700 dark:text-zinc-300">Abschnitte</label>
<flux:button size="xs" variant="ghost" icon="plus" wire:click="addSection">Abschnitt hinzufügen</flux:button>
</div>
<div class="space-y-4">
@foreach ($sections as $i => $section)
<div wire:key="section-{{ $i }}" class="rounded-lg border border-zinc-200 p-4 dark:border-zinc-700">
<div class="mb-2 flex items-center justify-between">
<span class="text-xs font-medium text-zinc-500">Abschnitt {{ $i + 1 }}</span>
@if (count($sections) > 1)
<flux:button size="xs" variant="ghost" icon="trash" wire:click="removeSection({{ $i }})" />
@endif
</div>
<flux:input wire:model="sections.{{ $i }}.title" label="Überschrift" placeholder="Abschnitt-Titel" class="mb-3" />
<flux:textarea wire:model="sections.{{ $i }}.content" label="Text" rows="3" placeholder="Abschnitt-Inhalt..." />
</div>
@endforeach
</div>
</div>
</div>
<div class="mt-4">
<flux:switch wire:model="is_published" label="Veröffentlicht" />
</div>
<div class="mt-4 flex gap-2">
<flux:button variant="primary" wire:click="save">Speichern</flux:button>
<flux:button variant="ghost" wire:click="cancelForm">Abbrechen</flux:button>
</div>
</flux:card>
@endif
<flux:card>
<div class="divide-y dark:divide-zinc-700">
@forelse ($this->articles as $article)
<div wire:key="article-{{ $article->id }}" class="flex items-center justify-between gap-4 py-3">
<div class="flex min-w-0 flex-1 items-center gap-3">
@if ($article->image)
<div class="h-12 w-20 shrink-0 overflow-hidden rounded-lg bg-zinc-100 dark:bg-zinc-700">
<img src="{{ media_url($article->image) }}" alt="" class="h-full w-full object-cover" loading="lazy" />
</div>
@else
<div class="flex h-12 w-20 shrink-0 items-center justify-center rounded-lg bg-zinc-100 dark:bg-zinc-700">
<x-heroicon-o-newspaper class="h-6 w-6 text-zinc-400" />
</div>
@endif
<div class="min-w-0">
<div class="flex items-center gap-2">
<span class="truncate font-medium text-zinc-800 dark:text-zinc-200">
{{ strip_tags($article->getTranslation('title', $editLocale)) }}
</span>
@if ($article->category)
<flux:badge size="sm" color="blue">{{ $article->category }}</flux:badge>
@endif
@unless ($article->is_published)
<flux:badge size="sm" color="amber">Entwurf</flux:badge>
@endunless
</div>
<p class="text-sm text-zinc-500">
{{ $article->author['name'] ?? '' }} · {{ $article->date_label ?? '' }} · {{ $article->read_time ?? '' }}
</p>
</div>
</div>
<div class="flex shrink-0 items-center gap-1">
<flux:button size="sm" variant="ghost" icon="pencil" wire:click="openEdit({{ $article->id }})" />
<flux:button size="sm" variant="ghost" :icon="$article->is_published ? 'eye' : 'eye-slash'"
wire:click="togglePublished({{ $article->id }})" />
<flux:button size="sm" variant="ghost" icon="trash"
wire:click="deleteArticle({{ $article->id }})"
wire:confirm="Artikel wirklich löschen?" />
</div>
</div>
@empty
<div class="py-12 text-center">
<x-heroicon-o-newspaper class="mx-auto mb-4 h-12 w-12 text-zinc-400" />
<flux:heading>Keine Artikel</flux:heading>
<flux:text>Erstelle den ersten Magazin-Beitrag mit dem Button oben.</flux:text>
</div>
@endforelse
</div>
</flux:card>
</div>

View file

@ -0,0 +1,165 @@
<div>
<flux:header class="mb-6">
<flux:heading size="xl">{{ __('Cabinet Info-Tablet') }}</flux:heading>
<flux:subheading>{{ __('Einstellungen für das Schaufenster-Tablet') }}</flux:subheading>
</flux:header>
{{-- Hilfe-Banner --}}
<flux:card class="mb-6 bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800">
<div class="flex items-start gap-4">
<flux:icon.information-circle class="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
<div class="flex-1">
<h3 class="font-semibold text-blue-900 dark:text-blue-100 mb-2">{{ __('Info-Tablet Steuerung') }}</h3>
<div class="text-sm text-blue-800 dark:text-blue-200 space-y-1">
<p>Das Info-Tablet im Schaufenster zeigt Store-Status, Öffnungszeiten und den nächsten freien Termin.</p>
<p> <strong>Automatisch:</strong> Offen/Geschlossen wird automatisch aus den hinterlegten Öffnungszeiten berechnet.</p>
<p> <strong>API-Endpunkt:</strong> <code class="px-2 py-0.5 bg-blue-100 dark:bg-blue-800 rounded">{{ url('/api/cabinet-tablet/status') }}</code></p>
<p> <strong>Website-Endpunkt:</strong> <code class="px-2 py-0.5 bg-blue-100 dark:bg-blue-800 rounded"><a target="_blank" href="https://cabinet.b2in.eu/info">https://cabinet.b2in.eu/info</a></code></p>
<p> <strong>Quick-Status-Endpunkt:</strong> <code class="px-2 py-0.5 bg-blue-100 dark:bg-blue-800 rounded"><a target="_blank" href="https://portal.b2in.eu/info/status?key={{ config('domains.cabinet_status_key') }}">https://portal.b2in.eu/info/status?key={{ config('domains.cabinet_status_key') }}</a></code></p>
<p> <strong>Quick-Status-Hinweis:</strong> Der Quick-Status-Endpunkt ist ein kleines Tool, um den Store-Status schnell zu ändern. Auf dem Mobiltelefon den Link öffen und dem Home-Screen hinzufügen.</p>
</div>
</div>
</div>
</flux:card>
{{-- Success-Meldungen --}}
@if (session()->has('success'))
<x-success-alert>
{{ session('success') }}
</x-success-alert>
@endif
<form wire:submit.prevent="save"
x-data
x-on:submit.prevent="
const hours = {};
document.querySelectorAll('[data-hours-prop]').forEach(el => {
const prop = el.dataset.hoursProp;
const picker = el.querySelector('ui-time-picker');
hours[prop] = picker ? (picker.value ?? '') : '';
});
$wire.save(hours);
">
<div class="space-y-6">
{{-- Store-Status --}}
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Store-Status') }}</flux:heading>
<div class="space-y-4">
<flux:select wire:model.live="storeStatus" label="Modus">
<option value="auto">Automatisch (aus Öffnungszeiten)</option>
<option value="notice">Hinweis (Orange)</option>
<option value="warning">Warnung (Rot)</option>
<option value="closed">Manuell geschlossen (Gelb)</option>
</flux:select>
@if($storeStatus !== 'auto')
@php
$noticeColors = match($storeStatus) {
'notice' => 'bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800',
'warning' => 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800',
default => 'bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800',
};
@endphp
<div class="space-y-4 p-4 rounded-lg {{ $noticeColors }}">
<flux:input wire:model="noticeHeadline" label="Headline" placeholder="z.B. Heute erst ab 11:00 Uhr" maxlength="40" />
@error('noticeHeadline') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
<flux:input wire:model="noticeSubtext" label="Subtext (optional)" placeholder="z.B. Wegen eines Kundentermins öffnen wir heute später." maxlength="80" />
@error('noticeSubtext') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
</div>
@endif
</div>
</flux:card>
{{-- Öffnungszeiten --}}
<flux:card>
<flux:heading size="lg" class="mb-1">{{ __('Öffnungszeiten') }}</flux:heading>
<p class="text-sm text-zinc-500 dark:text-zinc-400 mb-4">Felder leer lassen = Geschlossen. Der Status "Geöffnet/Geschlossen" wird automatisch aus diesen Zeiten berechnet.</p>
<div class="space-y-3">
@foreach([
['label' => 'Montag', 'open' => 'hoursMondayOpen', 'close' => 'hoursMondayClose'],
['label' => 'Dienstag', 'open' => 'hoursTuesdayOpen', 'close' => 'hoursTuesdayClose'],
['label' => 'Mittwoch', 'open' => 'hoursWednesdayOpen', 'close' => 'hoursWednesdayClose'],
['label' => 'Donnerstag', 'open' => 'hoursThursdayOpen', 'close' => 'hoursThursdayClose'],
['label' => 'Freitag', 'open' => 'hoursFridayOpen', 'close' => 'hoursFridayClose'],
['label' => 'Samstag', 'open' => 'hoursSaturdayOpen', 'close' => 'hoursSaturdayClose'],
['label' => 'Sonntag', 'open' => 'hoursSundayOpen', 'close' => 'hoursSundayClose'],
] as $row)
@php
$isClosedDay = empty($this->{$row['open']}) && empty($this->{$row['close']});
@endphp
<div class="grid grid-cols-[120px_1fr_1fr_80px] items-center gap-3 py-2 border-b border-zinc-100 dark:border-zinc-800 last:border-0">
<span class="text-sm font-medium text-zinc-700 dark:text-zinc-300">{{ $row['label'] }}</span>
<div data-hours-prop="{{ $row['open'] }}">
<flux:time-picker wire:model.live="{{ $row['open'] }}" label="Öffnung" time-format="24-hour" interval="15" clearable placeholder="" />
</div>
<div data-hours-prop="{{ $row['close'] }}">
<flux:time-picker wire:model.live="{{ $row['close'] }}" label="Schluss" time-format="24-hour" interval="15" clearable placeholder="" />
</div>
<span class="text-xs text-center {{ $isClosedDay ? 'text-red-500 dark:text-red-400' : 'text-emerald-600 dark:text-emerald-400' }}">
{{ $isClosedDay ? 'Geschlossen' : 'Geöffnet' }}
</span>
</div>
@endforeach
</div>
</flux:card>
{{-- Sonderöffnung heute
<flux:card>
<div class="flex items-center justify-between mb-4">
<flux:heading size="lg">{{ __('Sonderöffnung heute') }}</flux:heading>
@if($overrideOpenToday || $overrideCloseToday)
<flux:button wire:click="clearOverrides" size="sm" variant="ghost" icon="x-mark">
{{ __('Zurücksetzen') }}
</flux:button>
@endif
</div>
<p class="text-sm text-zinc-500 dark:text-zinc-400 mb-4">Überschreibt die reguläre Öffnungszeit für heute. Wird um Mitternacht automatisch zurückgesetzt.</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<flux:time-picker wire:model="overrideOpenToday" label="Öffnung" time-format="24-hour" interval="15" clearable placeholder="" />
@error('overrideOpenToday') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
<flux:time-picker wire:model="overrideCloseToday" label="Schluss" time-format="24-hour" interval="15" clearable placeholder="" />
@error('overrideCloseToday') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
</div>
</flux:card>--}}
{{-- Nächster Termin
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Nächster freier Termin') }}</flux:heading>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<flux:input wire:model="nextAppointmentDate" type="date" label="Datum" />
@error('nextAppointmentDate') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
<flux:time-picker wire:model="nextAppointmentTime" label="Uhrzeit" time-format="24-hour" interval="15" clearable placeholder="" />
@error('nextAppointmentTime') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
</div>
</flux:card>--}}
{{-- Kontakt --}}
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Kontakt') }}</flux:heading>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<flux:input wire:model="contactPhone" label="Telefon" placeholder="z.B. 0521 98620100" />
@error('contactPhone') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
<flux:input wire:model="contactEmail" label="E-Mail" type="email" placeholder="z.B. info@cabinet-bielefeld.de" />
@error('contactEmail') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
</div>
</flux:card>
{{-- Speichern --}}
<div class="flex justify-end">
<flux:button type="submit" variant="primary" icon="check">
{{ __('Einstellungen speichern') }}
</flux:button>
</div>
</div>
</form>
</div>

View file

@ -0,0 +1,782 @@
<?php
use App\Services\CmsFluxEditorHtmlTransformer;
use Flux\Flux;
use FluxCms\Core\Models\CmsContent;
use FluxCms\Core\Models\CmsMedia;
use FluxCms\Core\Services\CmsContentService;
use FluxCms\Core\Services\HeroiconOutlineList;
use function Livewire\Volt\{layout, title, state, computed, on};
if (! function_exists('_cmsParseJsonItems')) {
function _cmsParseJsonItems(array $value): array
{
if (empty($value)) {
return [false, []];
}
$isList = array_is_list($value);
$stringify = fn ($v) => is_array($v) ? json_encode($v, JSON_UNESCAPED_UNICODE) : (string) $v;
if ($isList && ! is_array($value[0])) {
return [true, array_map(fn ($v) => ['_value' => $stringify($v)], $value)];
}
if ($isList) {
return [false, array_map(fn ($item) => is_array($item)
? array_map($stringify, $item)
: ['_value' => (string) $item], $value)];
}
return [false, [array_map($stringify, $value)]];
}
}
if (! function_exists('_cmsFieldLooksLikeImage')) {
function _cmsFieldLooksLikeImage(string $fieldKey, mixed $fieldValue): bool
{
if (! is_string($fieldValue) || trim($fieldValue) === '') {
return false;
}
$key = strtolower($fieldKey);
if (in_array($key, ['image', 'photo', 'avatar', 'picture', 'thumbnail', 'img', 'hero_image', 'background_image', 'cover_image'], true)) {
return true;
}
if (preg_match('/_(image|photo|avatar|picture|thumb)$/i', $fieldKey)) {
return true;
}
return (bool) preg_match('/\.(jpe?g|png|gif|webp|svg)$/i', $fieldValue);
}
}
layout('components.layouts.app');
title('CMS Inhalte');
state([
'selectedGroup' => null,
'search' => '',
'editingId' => null,
'editingField' => null,
'editLocale' => 'de',
'editValue' => '',
'editMediaId' => null,
'showJsonModal' => false,
'jsonItems' => [],
'jsonIsStringArray' => false,
'jsonEditingKey' => '',
'editingFieldType' => 'text',
]);
on(['media-selected' => function (string $field, ?int $mediaId, ?string $url) {
if ($field === 'content_image') {
$media = $mediaId ? CmsMedia::find($mediaId) : null;
if ($media) {
$this->editValue = $media->filename;
$this->editMediaId = $mediaId;
} else {
$this->editValue = '';
$this->editMediaId = null;
}
return;
}
if (str_starts_with($field, 'jsonimg:')) {
$parts = explode(':', $field, 3);
if (count($parts) === 3 && $mediaId) {
$media = CmsMedia::find($mediaId);
if ($media) {
$idx = (int) $parts[1];
$fname = $parts[2];
if (isset($this->jsonItems[$idx]) && is_array($this->jsonItems[$idx])) {
$this->jsonItems[$idx][$fname] = $media->filename;
}
}
}
return;
}
}]);
$groups = computed(fn () => CmsContent::query()
->selectRaw('`group`, count(*) as count')
->groupBy('group')
->orderBy('group')
->pluck('count', 'group')
->toArray());
$flatContents = computed(function () {
if (! $this->selectedGroup) {
return collect();
}
$contents = CmsContent::forGroup($this->selectedGroup)
->orderBy('order')
->get();
$rows = [];
foreach ($contents as $content) {
$value = $content->getTranslation('value', $this->editLocale);
if ($content->type === 'json' && is_array($value) && ! array_is_list($value)) {
foreach ($value as $fieldKey => $fieldValue) {
if ($this->search && ! str_contains(strtolower($content->key . '.' . $fieldKey), strtolower($this->search))) {
continue;
}
$fieldType = 'text';
if ($this->selectedGroup === 'legal' && $fieldKey === 'content') {
$fieldType = 'legal_html';
} elseif (is_array($fieldValue)) {
$fieldType = 'json';
} elseif (is_string($fieldValue) && preg_match('/<[^>]+>/', $fieldValue)) {
$fieldType = 'html';
} elseif (_cmsFieldLooksLikeImage($fieldKey, $fieldValue)) {
$fieldType = 'image';
}
$rows[] = (object) [
'content_id' => $content->id,
'section_key' => $content->key,
'field_key' => $fieldKey,
'display_key' => $content->key . '.' . $fieldKey,
'type' => $fieldType,
'value' => $fieldValue,
'is_subfield' => true,
];
}
} else {
if ($this->search && ! str_contains(strtolower($content->key), strtolower($this->search))) {
continue;
}
$rows[] = (object) [
'content_id' => $content->id,
'section_key' => $content->key,
'field_key' => null,
'display_key' => $content->key,
'type' => $content->type ?? 'text',
'value' => $value,
'is_subfield' => false,
];
}
}
return collect($rows);
});
$availableIcons = computed(fn () => HeroiconOutlineList::names());
$selectGroup = function (string $group) {
$this->showJsonModal = false;
$this->jsonItems = [];
$this->jsonIsStringArray = false;
$this->jsonEditingKey = '';
$this->editingId = null;
$this->editingField = null;
$this->editValue = '';
$this->editMediaId = null;
$this->editingFieldType = 'text';
$this->selectedGroup = $group;
};
$startFieldEdit = function (int $contentId, ?string $fieldKey = null) {
$content = CmsContent::find($contentId);
if (! $content) {
return;
}
$fullValue = $content->getTranslation('value', $this->editLocale);
if ($fieldKey !== null && is_array($fullValue)) {
$fieldValue = $fullValue[$fieldKey] ?? '';
if (is_array($fieldValue)) {
$this->editingId = $contentId;
$this->editingField = $fieldKey;
$this->editingFieldType = 'text';
$this->jsonEditingKey = $content->key . '.' . $fieldKey;
$isList = array_is_list($fieldValue);
if ($isList) {
[$this->jsonIsStringArray, $this->jsonItems] = _cmsParseJsonItems($fieldValue);
} else {
$this->jsonIsStringArray = false;
$this->jsonItems = [array_map(
fn ($v) => is_array($v) ? json_encode($v, JSON_UNESCAPED_UNICODE) : (string) $v,
$fieldValue,
)];
}
$this->jsonItems = CmsFluxEditorHtmlTransformer::toEditorJsonItems($this->jsonItems, $this->jsonIsStringArray);
$this->showJsonModal = true;
return;
}
$this->editingId = $contentId;
$this->editingField = $fieldKey;
$this->editValue = (string) $fieldValue;
if (_cmsFieldLooksLikeImage($fieldKey, $fieldValue)) {
$this->editingFieldType = 'image';
$this->editMediaId = CmsMedia::where('filename', $this->editValue)->first()?->id;
} elseif ($content->group === 'legal' && $fieldKey === 'content') {
$this->editingFieldType = 'legal_html';
$this->editMediaId = null;
} else {
$this->editingFieldType = is_string($fieldValue) && preg_match('/<[^>]+>/', $fieldValue) ? 'html' : 'text';
$this->editMediaId = null;
}
if ($this->editingFieldType === 'html') {
$this->editValue = CmsFluxEditorHtmlTransformer::toEditor($this->editValue);
}
} else {
$this->editingId = $contentId;
$this->editingField = null;
if ($content->type === 'json') {
$this->jsonEditingKey = $content->key;
if (is_array($fullValue)) {
[$this->jsonIsStringArray, $this->jsonItems] = _cmsParseJsonItems($fullValue);
$this->jsonItems = CmsFluxEditorHtmlTransformer::toEditorJsonItems($this->jsonItems, $this->jsonIsStringArray);
} else {
$this->jsonIsStringArray = false;
$this->jsonItems = [];
}
$this->showJsonModal = true;
return;
}
$this->editValue = is_array($fullValue)
? json_encode($fullValue, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
: ((string) ($fullValue ?? ''));
if ($content->type === 'image') {
$this->editingFieldType = 'image';
$this->editMediaId = CmsMedia::where('filename', $this->editValue)->first()?->id;
} elseif ($content->type === 'html') {
$this->editingFieldType = 'html';
$this->editMediaId = null;
$this->editValue = CmsFluxEditorHtmlTransformer::toEditor($this->editValue);
} else {
$this->editingFieldType = 'text';
$this->editMediaId = null;
}
}
};
$switchLocale = function (string $locale) {
$this->editLocale = $locale;
if ($this->editingId && $this->showJsonModal) {
$content = CmsContent::find($this->editingId);
if (! $content) {
return;
}
$fullValue = $content->getTranslation('value', $locale);
if ($this->editingField !== null && is_array($fullValue)) {
$fieldValue = $fullValue[$this->editingField] ?? [];
if (is_array($fieldValue)) {
[$this->jsonIsStringArray, $this->jsonItems] = _cmsParseJsonItems($fieldValue);
$this->jsonItems = CmsFluxEditorHtmlTransformer::toEditorJsonItems($this->jsonItems, $this->jsonIsStringArray);
}
} elseif (is_array($fullValue)) {
[$this->jsonIsStringArray, $this->jsonItems] = _cmsParseJsonItems($fullValue);
$this->jsonItems = CmsFluxEditorHtmlTransformer::toEditorJsonItems($this->jsonItems, $this->jsonIsStringArray);
}
} elseif ($this->editingId) {
$content = CmsContent::find($this->editingId);
if (! $content) {
return;
}
$fullValue = $content->getTranslation('value', $locale);
if ($this->editingField !== null && is_array($fullValue)) {
$fieldValue = $fullValue[$this->editingField] ?? '';
$this->editValue = is_array($fieldValue)
? json_encode($fieldValue, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
: (string) $fieldValue;
if (! is_array($fieldValue)) {
if (_cmsFieldLooksLikeImage((string) $this->editingField, $fieldValue)) {
$this->editingFieldType = 'image';
$this->editMediaId = CmsMedia::where('filename', $this->editValue)->first()?->id;
} elseif ($content->group === 'legal' && $this->editingField === 'content') {
$this->editingFieldType = 'legal_html';
$this->editMediaId = null;
} else {
$this->editingFieldType = is_string($fieldValue) && preg_match('/<[^>]+>/', $fieldValue) ? 'html' : 'text';
$this->editMediaId = null;
}
if ($this->editingFieldType === 'html') {
$this->editValue = CmsFluxEditorHtmlTransformer::toEditor($this->editValue);
}
}
} else {
$this->editValue = is_array($fullValue)
? json_encode($fullValue, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
: (string) ($fullValue ?? '');
if ($content->type === 'image') {
$this->editingFieldType = 'image';
$this->editMediaId = CmsMedia::where('filename', $this->editValue)->first()?->id;
} elseif ($content->type === 'html') {
$this->editingFieldType = 'html';
$this->editMediaId = null;
$this->editValue = CmsFluxEditorHtmlTransformer::toEditor($this->editValue);
} else {
$this->editingFieldType = 'text';
$this->editMediaId = null;
}
}
}
};
$saveEdit = function () {
$content = CmsContent::find($this->editingId);
if (! $content) {
return;
}
$valueToSave = $this->editValue;
if ($this->editingFieldType === 'html') {
$valueToSave = CmsFluxEditorHtmlTransformer::fromEditor($valueToSave);
}
if ($this->editingField !== null) {
$fullValue = $content->getTranslation('value', $this->editLocale);
if (! is_array($fullValue)) {
$fullValue = [];
}
$fullValue[$this->editingField] = $valueToSave;
$content->setTranslation('value', $this->editLocale, $fullValue);
} else {
$content->setTranslation('value', $this->editLocale, $valueToSave);
}
$content->save();
app(CmsContentService::class)->clearCache($this->selectedGroup);
$this->editingId = null;
$this->editingField = null;
$this->editValue = '';
$this->editingFieldType = 'text';
$this->editMediaId = null;
Flux::toast(variant: 'success', heading: 'Gespeichert', text: 'Inhalt wurde erfolgreich aktualisiert.');
};
$addJsonItem = function () {
if ($this->jsonIsStringArray) {
$this->jsonItems[] = ['_value' => ''];
} elseif (! empty($this->jsonItems)) {
$template = array_map(fn () => '', $this->jsonItems[0]);
$this->jsonItems[] = $template;
}
};
$removeJsonItem = function (int $index) {
unset($this->jsonItems[$index]);
$this->jsonItems = array_values($this->jsonItems);
};
$saveJsonModal = function () {
$content = CmsContent::find($this->editingId);
if (! $content) {
return;
}
$itemsForSave = CmsFluxEditorHtmlTransformer::fromEditorJsonItems($this->jsonItems, $this->jsonIsStringArray);
if ($this->jsonIsStringArray) {
$newValue = array_values(array_map(fn ($item) => $item['_value'] ?? '', $itemsForSave));
} else {
$newValue = array_values(
array_map(function ($item) {
$cleaned = [];
foreach ($item as $k => $v) {
if (is_string($v) && (str_starts_with($v, '[') || str_starts_with($v, '{'))) {
$decoded = json_decode($v, true);
$cleaned[$k] = json_last_error() === JSON_ERROR_NONE ? $decoded : $v;
} else {
$cleaned[$k] = $v;
}
}
return $cleaned;
}, $itemsForSave),
);
}
if ($this->editingField !== null) {
$fullValue = $content->getTranslation('value', $this->editLocale);
if (! is_array($fullValue)) {
$fullValue = [];
}
$fullValue[$this->editingField] = $newValue;
$content->setTranslation('value', $this->editLocale, $fullValue);
} else {
$content->setTranslation('value', $this->editLocale, $newValue);
}
$content->save();
app(CmsContentService::class)->clearCache($this->selectedGroup);
$this->showJsonModal = false;
$this->editingId = null;
$this->editingField = null;
$this->jsonItems = [];
Flux::toast(variant: 'success', heading: 'Gespeichert', text: 'Inhalt wurde erfolgreich aktualisiert.');
};
$cancelEdit = function () {
$this->editingId = null;
$this->editingField = null;
$this->editValue = '';
$this->editingFieldType = 'text';
$this->editMediaId = null;
};
$cancelJsonModal = function () {
$this->showJsonModal = false;
$this->editingId = null;
$this->editingField = null;
$this->jsonItems = [];
};
?>
@php
$cmsGroupLabels = [
'legal' => 'Rechtliches',
];
@endphp
<div>
<div class="mb-6 flex items-center justify-between">
<flux:heading size="xl">Inhalte verwalten</flux:heading>
<div class="flex items-center gap-2">
<flux:badge color="blue">{{ array_sum($this->groups) }} Einträge</flux:badge>
</div>
</div>
<div class="grid grid-cols-1 gap-6 lg:grid-cols-4">
<div class="lg:col-span-1">
<flux:card>
<flux:heading size="sm" class="mb-3">Seiten / Gruppen</flux:heading>
<div class="flex flex-col gap-1">
@foreach ($this->groups as $group => $count)
<button wire:click="selectGroup('{{ $group }}')"
class="flex items-center justify-between rounded-lg px-3 py-2 text-sm transition-colors {{ $selectedGroup === $group ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' : 'hover:bg-zinc-100 dark:hover:bg-zinc-700' }}">
<span>{{ $cmsGroupLabels[$group] ?? $group }}</span>
<flux:badge size="sm">{{ $count }}</flux:badge>
</button>
@endforeach
</div>
</flux:card>
</div>
<div class="lg:col-span-3" wire:key="cms-content-panel-{{ $selectedGroup ?? 'empty' }}">
@if ($selectedGroup)
<flux:card>
<div class="mb-4 flex items-center justify-between">
<flux:heading size="lg">{{ $cmsGroupLabels[$selectedGroup] ?? $selectedGroup }}</flux:heading>
<div class="flex items-center gap-3">
<flux:input wire:model.live.debounce.300ms="search" placeholder="Suchen..." size="sm"
icon="magnifying-glass" class="w-48" />
<div class="flex gap-1">
@foreach (config('flux-cms.locales', ['de' => 'DE', 'en' => 'EN']) as $code => $label)
<flux:button size="xs" :variant="$editLocale === $code ? 'primary' : 'ghost'"
wire:click="switchLocale('{{ $code }}')">
{{ strtoupper($code) }}
</flux:button>
@endforeach
</div>
</div>
</div>
@php $lastSection = null; @endphp
<div class="divide-y dark:divide-zinc-700">
@forelse ($this->flatContents as $row)
@if ($row->section_key !== $lastSection)
@php $lastSection = $row->section_key; @endphp
<div class="bg-zinc-50 dark:bg-zinc-800/50 px-3 py-2">
<span class="text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400">{{ $row->section_key }}</span>
</div>
@endif
@php
$isEditing = $editingId === $row->content_id
&& $editingField === $row->field_key
&& ! $showJsonModal;
$editKey = $row->field_key !== null
? $row->content_id . ",'" . $row->field_key . "'"
: $row->content_id . ',null';
@endphp
<div wire:key="row-{{ $row->content_id }}-{{ $row->field_key ?? 'root' }}" class="py-3 px-1">
<div class="flex items-start justify-between gap-4">
<div class="min-w-0 flex-1">
<div class="mb-1 flex items-center gap-2">
<code class="rounded bg-zinc-100 px-1.5 py-0.5 text-xs text-zinc-500 dark:bg-zinc-700 dark:text-zinc-400">{{ $row->field_key ?? $row->section_key }}</code>
@php
$typeColors = ['html' => 'amber', 'legal_html' => 'blue', 'image' => 'green', 'json' => 'violet', 'link' => 'rose'];
$badgeColor = $typeColors[$row->type] ?? 'zinc';
@endphp
<flux:badge size="sm" :color="$badgeColor">{{ $row->type }}</flux:badge>
</div>
<div
wire:key="field-value-{{ $row->content_id }}-{{ $row->field_key ?? 'root' }}-{{ $isEditing ? 'edit' : 'view' }}"
>
@if ($isEditing)
<div class="mt-2">
@if ($editingFieldType === 'image')
<div class="flex flex-col gap-3 sm:flex-row sm:items-start">
@if ($editValue)
<div class="h-24 w-36 shrink-0 overflow-hidden rounded-lg border border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800">
<img src="{{ media_url($editValue) }}" alt="" class="h-full w-full object-cover" />
</div>
@endif
<div class="min-w-0 flex-1 space-y-2">
<livewire:admin.cms.media-picker
:value="$editMediaId"
field="content_image"
type="image"
profile="card"
label="Bild aus Medienbibliothek wählen"
:key="'cms-content-img-' . $selectedGroup . '-' . $editingId . '-' . ($editingField ?? 'root') . '-' . $editLocale"
/>
<flux:input wire:model="editValue" size="sm" label="Dateiname (optional manuell)" placeholder="z. B. b2in/hero.jpg" />
</div>
</div>
@elseif ($editingFieldType === 'html')
<flux:editor wire:model="editValue" toolbar="bold italic highlight"
class="**:data-[slot=content]:min-h-[60px]!" />
@elseif ($editingFieldType === 'legal_html')
{{-- natives textarea, Inhalt nur per Livewire-JS (kein </textarea> im HTML); verhindert DOM-Bruch bei Impressum-HTML --}}
<div class="space-y-2">
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300" for="legal-html-{{ $row->content_id }}-{{ $row->field_key ?? 'root' }}">HTML (Rohtext)</label>
<textarea
id="legal-html-{{ $row->content_id }}-{{ $row->field_key ?? 'root' }}"
wire:model.defer="editValue"
rows="28"
spellcheck="false"
class="block min-h-[24rem] w-full resize-y rounded-lg border border-zinc-200 bg-white p-3 font-mono text-xs text-zinc-800 shadow-xs dark:border-white/10 dark:bg-white/10 dark:text-zinc-200"
></textarea>
</div>
@else
<flux:textarea wire:model="editValue" rows="8" class="font-mono text-sm" />
@endif
<div class="mt-2 flex gap-2">
<flux:button size="sm" variant="primary" wire:click="saveEdit" wire:loading.attr="disabled" wire:target="saveEdit">
<span wire:loading.remove wire:target="saveEdit">Speichern</span>
<span wire:loading wire:target="saveEdit">Speichern…</span>
</flux:button>
<flux:button size="sm" variant="ghost" wire:click="cancelEdit" wire:loading.attr="disabled" wire:target="saveEdit">Abbrechen</flux:button>
</div>
</div>
@else
@if ($row->type === 'json' && is_array($row->value))
@php
$isList = array_is_list($row->value);
$cnt = count($row->value);
@endphp
<div class="flex items-center gap-2 text-sm text-zinc-500">
<flux:badge size="sm" color="violet">{{ $cnt }} {{ $isList ? 'Einträge' : 'Felder' }}</flux:badge>
@if ($isList && $cnt > 0 && is_array($row->value[0] ?? null))
<span class="text-xs text-zinc-400">{{ implode(', ', array_keys($row->value[0])) }}</span>
@elseif (! $isList)
<span class="text-xs text-zinc-400">{{ implode(', ', array_slice(array_keys($row->value), 0, 5)) }}{{ $cnt > 5 ? ', …' : '' }}</span>
@endif
</div>
@elseif ($row->type === 'image' && is_string($row->value) && $row->value !== '')
<div class="flex items-center gap-3">
<div class="h-14 w-24 shrink-0 overflow-hidden rounded-lg border border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800">
<img src="{{ media_url($row->value) }}" alt="" class="h-full w-full object-cover" loading="lazy" />
</div>
<span class="truncate text-xs text-zinc-500">{{ $row->value }}</span>
</div>
@elseif ($row->type === 'legal_html')
<p class="line-clamp-3 text-sm text-zinc-600 dark:text-zinc-400">
{{ \Illuminate\Support\Str::limit(strip_tags((string) $row->value), 200) }}
</p>
@elseif ($row->type === 'html')
<div class="prose prose-sm max-w-none text-zinc-800 dark:prose-invert dark:text-zinc-200 line-clamp-2">
{!! \Illuminate\Support\Str::limit((string) $row->value, 200) !!}
</div>
@else
<p class="truncate text-sm text-zinc-800 dark:text-zinc-200">
{{ \Illuminate\Support\Str::limit(strip_tags(is_array($row->value) ? json_encode($row->value, JSON_UNESCAPED_UNICODE) : (string) ($row->value ?? '')), 120) }}
</p>
@endif
@endif
</div>
</div>
@if (! $isEditing)
<flux:button size="sm" variant="ghost" icon="pencil"
wire:click="startFieldEdit({{ $editKey }})" />
@endif
</div>
</div>
@empty
<flux:text class="py-8 text-center">Keine Einträge gefunden.</flux:text>
@endforelse
</div>
</flux:card>
@else
<flux:card>
<div class="py-12 text-center">
<flux:icon name="document-text" class="mx-auto mb-4 h-12 w-12 text-zinc-400" />
<flux:heading>Seite auswählen</flux:heading>
<flux:text>Wähle links eine Seite/Gruppe aus, um deren Inhalte zu bearbeiten.</flux:text>
</div>
</flux:card>
@endif
</div>
</div>
{{-- JSON-Editor: kein flux:modal vermeidet ReferenceError fluxModal bei jedem Livewire-Render (z. B. bei legal_html). --}}
@if ($showJsonModal)
<div
class="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6"
wire:key="cms-json-editor-overlay-{{ $selectedGroup ?? 'none' }}"
x-data
x-on:keydown.escape.window.prevent="$wire.cancelJsonModal()"
>
<div class="fixed inset-0 bg-zinc-950/50 backdrop-blur-sm" wire:click="cancelJsonModal" aria-hidden="true">
</div>
<div
role="dialog"
aria-modal="true"
class="relative z-10 flex w-full max-w-5xl max-h-[90vh] flex-col overflow-y-auto space-y-6 rounded-xl border border-zinc-200 bg-white p-6 shadow-xl dark:border-zinc-700 dark:bg-zinc-800"
wire:click.stop
>
<div>
<flux:heading size="lg">{{ $jsonEditingKey }}</flux:heading>
<flux:text class="mt-1">
{{ $jsonIsStringArray ? 'Einfache Liste' : 'Strukturierte Einträge' }}
({{ count($jsonItems) }} Einträge) {{ strtoupper($editLocale) }}
</flux:text>
</div>
<div class="flex items-center gap-2">
@foreach (config('flux-cms.locales', ['de' => 'DE', 'en' => 'EN']) as $code => $label)
<flux:button size="xs" :variant="$editLocale === $code ? 'primary' : 'ghost'"
wire:click="switchLocale('{{ $code }}')">{{ strtoupper($code) }}</flux:button>
@endforeach
</div>
<div class="space-y-4">
@foreach ($jsonItems as $idx => $item)
<div wire:key="json-item-{{ $idx }}"
class="rounded-lg border border-zinc-200 dark:border-zinc-700 p-4">
<div class="mb-3 flex items-center justify-between">
<span class="text-xs font-semibold text-zinc-500">Eintrag {{ $idx + 1 }}</span>
<flux:button size="xs" variant="ghost" icon="trash"
wire:click="removeJsonItem({{ $idx }})"
wire:confirm="Eintrag {{ $idx + 1 }} wirklich entfernen?" />
</div>
@if ($jsonIsStringArray)
<flux:input wire:model="jsonItems.{{ $idx }}._value" placeholder="Wert" />
@else
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
@foreach ($item as $field => $fieldValue)
@php
$isIcon = in_array($field, ['icon']);
$isRichText = in_array($field, ['description', 'text', 'content', 'help', 'answer', 'quote']);
$isNestedJson = is_string($fieldValue) && (str_starts_with($fieldValue, '[') || str_starts_with($fieldValue, '{'));
$isImageField = _cmsFieldLooksLikeImage($field, $fieldValue);
$jsonImageMediaId = is_string($fieldValue) && $fieldValue !== '' ? \FluxCms\Core\Models\CmsMedia::where('filename', $fieldValue)->first()?->id : null;
@endphp
@if ($isIcon)
<div class="md:col-span-2">
<div class="flex items-end gap-3">
<div class="flex-1">
<flux:select wire:model="jsonItems.{{ $idx }}.{{ $field }}"
variant="listbox" searchable label="{{ ucfirst($field) }}"
placeholder="Icon auswählen...">
<flux:select.option value=""> Kein Icon </flux:select.option>
@foreach ($this->availableIcons as $iconName)
<flux:select.option value="{{ $iconName }}">{{ $iconName }}</flux:select.option>
@endforeach
</flux:select>
</div>
@if (! empty($fieldValue))
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-zinc-200 dark:border-zinc-700">
<x-dynamic-component :component="'heroicon-o-' . $fieldValue" class="h-5 w-5 text-primary" />
</div>
@endif
</div>
</div>
@elseif ($isImageField)
<div class="md:col-span-2">
<label class="mb-1 block text-sm font-medium text-zinc-700 dark:text-zinc-300">{{ ucfirst($field) }}</label>
<div class="flex flex-col gap-3 sm:flex-row sm:items-start">
@if (is_string($fieldValue) && $fieldValue !== '')
<div class="h-20 w-32 shrink-0 overflow-hidden rounded-lg border border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800">
<img src="{{ media_url($fieldValue) }}" alt="" class="h-full w-full object-cover" />
</div>
@endif
<div class="min-w-0 flex-1 space-y-2">
<livewire:admin.cms.media-picker
:value="$jsonImageMediaId"
:field="'jsonimg:' . $idx . ':' . $field"
type="image"
profile="card"
label="Bild wählen"
:key="'json-img-' . ($selectedGroup ?? '') . '-' . $jsonEditingKey . '-' . $idx . '-' . $field . '-' . $editLocale"
/>
<flux:input wire:model="jsonItems.{{ $idx }}.{{ $field }}" size="sm" placeholder="Dateiname (optional manuell)" />
</div>
</div>
</div>
@elseif ($isRichText)
<div class="md:col-span-2">
<flux:editor wire:model="jsonItems.{{ $idx }}.{{ $field }}"
label="{{ ucfirst($field) }}" toolbar="bold italic highlight"
class="**:data-[slot=content]:min-h-[60px]!" />
</div>
@elseif ($isNestedJson)
<div class="md:col-span-2">
<flux:textarea wire:model="jsonItems.{{ $idx }}.{{ $field }}"
label="{{ ucfirst($field) }} (JSON)" rows="3"
class="font-mono text-xs" />
</div>
@else
<flux:input wire:model="jsonItems.{{ $idx }}.{{ $field }}"
label="{{ ucfirst($field) }}" />
@endif
@endforeach
</div>
@endif
</div>
@endforeach
</div>
<div class="flex items-center justify-between border-t border-zinc-200 dark:border-zinc-700 pt-4">
<flux:button size="sm" variant="ghost" icon="plus" wire:click="addJsonItem">
Eintrag hinzufügen
</flux:button>
<div class="flex gap-2">
<flux:button variant="ghost" wire:click="cancelJsonModal">Abbrechen</flux:button>
<flux:button variant="primary" wire:click="saveJsonModal">Speichern</flux:button>
</div>
</div>
</div>
</div>
@endif
</div>

View file

@ -0,0 +1,156 @@
<?php
use App\Models\CmsArticle;
use App\Models\CmsProject;
use FluxCms\Core\Models\CmsContent;
use FluxCms\Core\Models\CmsMedia;
use function Livewire\Volt\{layout, title, computed};
layout('components.layouts.app');
title('CMS Dashboard');
$stats = computed(fn () => [
'contents' => CmsContent::count(),
'groups' => CmsContent::distinct()->pluck('group')->count(),
'projects' => CmsProject::count(),
'projects_published' => CmsProject::published()->count(),
'articles' => CmsArticle::count(),
'articles_published' => CmsArticle::published()->count(),
'media' => CmsMedia::count(),
]);
?>
<div>
<div class="mb-6">
<flux:heading size="xl">CMS</flux:heading>
<flux:text class="mt-1">Inhalte, Projekte und Medien verwalten.</flux:text>
</div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<a href="{{ route('cms.content.index') }}" wire:navigate>
<flux:card class="hover:border-blue-500 transition-colors">
<div class="flex items-center gap-3">
<flux:icon name="document-text" class="text-blue-500" />
<div>
<flux:heading size="lg">{{ $this->stats['contents'] }}</flux:heading>
<flux:text class="text-sm">Inhalte in {{ $this->stats['groups'] }} Gruppen</flux:text>
</div>
</div>
</flux:card>
</a>
<a href="{{ route('cms.projects.index') }}" wire:navigate>
<flux:card class="hover:border-emerald-500 transition-colors">
<div class="flex items-center gap-3">
<flux:icon name="building-office" class="text-emerald-500" />
<div>
<flux:heading size="lg">{{ $this->stats['projects'] }}</flux:heading>
<flux:text class="text-sm">Projekte ({{ $this->stats['projects_published'] }} veröffentlicht)</flux:text>
</div>
</div>
</flux:card>
</a>
<a href="{{ route('cms.articles.index') }}" wire:navigate>
<flux:card class="hover:border-amber-500 transition-colors">
<div class="flex items-center gap-3">
<flux:icon name="newspaper" class="text-amber-500" />
<div>
<flux:heading size="lg">{{ $this->stats['articles'] }}</flux:heading>
<flux:text class="text-sm">Magazin-Artikel ({{ $this->stats['articles_published'] }} veröffentlicht)</flux:text>
</div>
</div>
</flux:card>
</a>
<a href="{{ route('cms.media.index') }}" wire:navigate>
<flux:card class="hover:border-violet-500 transition-colors">
<div class="flex items-center gap-3">
<flux:icon name="photo" class="text-violet-500" />
<div>
<flux:heading size="lg">{{ $this->stats['media'] }}</flux:heading>
<flux:text class="text-sm">Medien</flux:text>
</div>
</div>
</flux:card>
</a>
</div>
<flux:card class="mt-6 max-w-4xl">
<flux:heading size="lg" class="mb-4">So funktioniert das CMS</flux:heading>
<div class="space-y-5 text-sm text-zinc-600 dark:text-zinc-400">
<div>
<flux:heading size="sm" class="mb-1 flex items-center gap-2">
<flux:icon name="squares-2x2" class="size-4 text-zinc-500" />
Aufbau & Navigation
</flux:heading>
<p>
Das CMS ist die zentrale Stelle, um alle Inhalte der öffentlichen Website zu pflegen.
Es gliedert sich in vier Module, die Sie über die Kacheln oben erreichen:
</p>
<ul class="mt-2 ml-5 list-disc space-y-1">
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Inhalte</strong> Texte, Abschnitte und strukturierte Daten der Website (z.&nbsp;B. Startseite, Netzwerk, rechtliche Texte). Inhalte sind in <em>Gruppen</em> organisiert, sodass zusammengehörige Felder gebündelt bearbeitet werden können.</li>
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Projekte</strong> Immobilien-Referenzobjekte mit Bildern, Preisen und Beschreibungen. Jedes Projekt kann als Entwurf angelegt und später veröffentlicht werden.</li>
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Magazin</strong> Redaktionelle Artikel (z.&nbsp;B. über Dubai-Immobilien, Supply-Chain oder Einrichtung). Artikel unterstützen Kategorien, Autoren und Lesezeiten.</li>
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Medien</strong> Zentrale Bild- und Dateiverwaltung. Alle Uploads landen hier und können in jedem Modul wiederverwendet werden.</li>
</ul>
</div>
<flux:separator />
<div>
<flux:heading size="sm" class="mb-1 flex items-center gap-2">
Medienbibliothek & Bildfelder
</flux:heading>
<p>
Jede Datei, die auf der Website erscheint, wird in der <strong class="font-medium text-zinc-800 dark:text-zinc-200">Medienbibliothek</strong> verwaltet.
Sie haben zwei Wege, Dateien hochzuladen:
</p>
<ol class="mt-2 ml-5 list-decimal space-y-1">
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Vorab</strong> Im Modul „Medien" Dateien hochladen, mit Titel und Alt-Text versehen und organisieren.</li>
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Im Kontext</strong> Beim Bearbeiten eines Inhalts, Projekts oder Artikels auf „Bild auswählen" klicken. Im Auswahldialog können Sie ein bestehendes Medium wählen <em>oder</em> per Drag-and-drop direkt hochladen. Dieser Schnell-Upload wird automatisch in der Medienbibliothek gespeichert.</li>
</ol>
<p class="mt-2">
Egal welchen Weg Sie wählen: Es gibt immer einen zentralen Bibliothekseintrag. So behalten Sie den Überblick und können dasselbe Bild an mehreren Stellen einsetzen.
</p>
</div>
<flux:separator />
<div>
<flux:heading size="sm" class="mb-1 flex items-center gap-2">
<flux:icon name="document-text" class="size-4 text-blue-500" />
Inhalte bearbeiten
</flux:heading>
<p>
Inhalte sind nach <strong class="font-medium text-zinc-800 dark:text-zinc-200">Gruppen</strong> geordnet z.&nbsp;B. <em>homepage</em>, <em>netzwerk</em>, <em>legal</em>.
Innerhalb einer Gruppe sehen Sie die einzelnen Felder (Texte, Rich-Text, Bilder oder JSON-Strukturen).
</p>
<ul class="mt-2 ml-5 list-disc space-y-1">
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Mehrsprachigkeit:</strong> Wo mehrere Sprachen vorgesehen sind, wählen Sie die gewünschte Sprache (DE / EN) im Editor. Jede Sprachversion wird einzeln gespeichert.</li>
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Sofort live:</strong> Gespeicherte Änderungen erscheinen auf der öffentlichen Website, sobald die jeweilige Seite den entsprechenden CMS-Key einbindet ein zusätzliches „Veröffentlichen" ist nicht nötig.</li>
</ul>
</div>
<flux:separator />
<div>
<flux:heading size="sm" class="mb-1 flex items-center gap-2">
<flux:icon name="building-office" class="size-4 text-emerald-500" />
Projekte & Magazin verwalten
</flux:heading>
<p>
Projekte und Magazin-Artikel sind eigenständige Einträge mit eigenem Lebenszyklus:
</p>
<ul class="mt-2 ml-5 list-disc space-y-1">
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Entwurf / Veröffentlicht:</strong> Neue Einträge starten als Entwurf. Erst nach dem Veröffentlichen sind sie auf der Website sichtbar. Sie können Einträge jederzeit wieder auf „Entwurf" setzen, um sie temporär auszublenden.</li>
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Reihenfolge:</strong> Über das Sortierfeld steuern Sie, in welcher Reihenfolge Projekte und Artikel auf der Website erscheinen (z.&nbsp;B. Portfolio-Listen, Magazin-Übersicht).</li>
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Bilder:</strong> Projekt- und Artikelbilder werden über die Medienauswahl zugeordnet (siehe oben).</li>
</ul>
</div>
</div>
</flux:card>
</div>

View file

@ -0,0 +1,294 @@
<?php
use App\Enums\DisplayVersionType;
use App\Models\CabinetTabletSetting;
use App\Models\Display;
use App\Models\DisplayMedia;
use App\Models\DisplayVersion;
use App\Models\DisplayVersionItem;
use function Livewire\Volt\{layout, title, computed};
layout('components.layouts.app');
title('Store Displays');
$stats = computed(fn () => [
'displays' => Display::count(),
'displays_active' => Display::where('is_active', true)->count(),
'versions' => DisplayVersion::count(),
'versions_active' => DisplayVersion::active()->count(),
'items' => DisplayVersionItem::count(),
'items_active' => DisplayVersionItem::where('is_active', true)->count(),
'type_video' => DisplayVersion::ofType(DisplayVersionType::VideoDisplay)->count(),
'type_b2in' => DisplayVersion::ofType(DisplayVersionType::B2in)->count(),
'type_offers' => DisplayVersion::ofType(DisplayVersionType::Offers)->count(),
'media_total' => DisplayMedia::count(),
'media_uploads' => DisplayMedia::uploads()->count(),
'media_externals' => DisplayMedia::externals()->count(),
]);
$tabletStatus = computed(function () {
try {
$settings = CabinetTabletSetting::current();
return $settings->computeStatus()['status'];
} catch (\Throwable) {
return null;
}
});
?>
<div>
<div class="mb-6">
<flux:heading size="xl">Store Displays</flux:heading>
<flux:text class="mt-1">Displays, Inhalts-Versionen und Info-Tablet im Cabinet Showroom verwalten.</flux:text>
</div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-5">
<a href="{{ route('admin.cms.display-media') }}" wire:navigate>
<flux:card class="hover:border-violet-500 transition-colors">
<div class="flex items-center gap-3">
<flux:icon name="photo" class="text-violet-500" />
<div>
<flux:heading size="lg">{{ $this->stats['media_total'] }}</flux:heading>
<flux:text class="text-sm">Medien ({{ $this->stats['media_uploads'] }} Uploads, {{ $this->stats['media_externals'] }} extern)</flux:text>
</div>
</div>
</flux:card>
</a>
<a href="{{ route('admin.cms.display-versions') }}" wire:navigate>
<flux:card class="hover:border-purple-500 transition-colors">
<div class="flex items-center gap-3">
<flux:icon name="rectangle-group" class="text-purple-500" />
<div>
<flux:heading size="lg">{{ $this->stats['versions'] }}</flux:heading>
<flux:text class="text-sm">Versionen ({{ $this->stats['versions_active'] }} aktiv)</flux:text>
</div>
</div>
</flux:card>
</a>
<a href="{{ route('admin.cms.displays') }}" wire:navigate>
<flux:card class="hover:border-blue-500 transition-colors">
<div class="flex items-center gap-3">
<flux:icon name="tv" class="text-blue-500" />
<div>
<flux:heading size="lg">{{ $this->stats['displays'] }}</flux:heading>
<flux:text class="text-sm">Displays ({{ $this->stats['displays_active'] }} aktiv)</flux:text>
</div>
</div>
</flux:card>
</a>
<a href="{{ route('admin.cms.cabinet-tablet') }}" wire:navigate>
<flux:card class="hover:border-teal-500 transition-colors">
<div class="flex items-center gap-3">
<flux:icon name="device-tablet" class="text-teal-500" />
<div>
<flux:heading size="lg">Info-Tablet</flux:heading>
<flux:text class="text-sm">
@if($this->tabletStatus)
Status:
<flux:badge size="sm" :color="match($this->tabletStatus) {
'open' => 'green',
'closed' => 'red',
'notice' => 'amber',
'warning' => 'orange',
default => 'zinc',
}">
{{ match($this->tabletStatus) {
'open' => 'Geöffnet',
'closed' => 'Geschlossen',
'notice' => 'Hinweis',
'warning' => 'Warnung',
default => $this->tabletStatus,
} }}
</flux:badge>
@else
Öffnungszeiten & Status
@endif
</flux:text>
</div>
</div>
</flux:card>
</a>
<flux:card>
<div class="flex items-center gap-3">
<flux:icon name="queue-list" class="text-zinc-400" />
<div>
<flux:heading size="lg">{{ $this->stats['items'] }}</flux:heading>
<flux:text class="text-sm">Inhalte gesamt ({{ $this->stats['items_active'] }} aktiv)</flux:text>
</div>
</div>
</flux:card>
</div>
{{-- Versions-Typen Übersicht --}}
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3 mt-4">
<flux:card>
<div class="flex items-center gap-3">
<flux:icon name="film" class="text-purple-400" />
<div>
<flux:text class="text-sm font-medium text-zinc-800 dark:text-zinc-200">Video-Display</flux:text>
<flux:text class="text-xs">{{ $this->stats['type_video'] }} {{ $this->stats['type_video'] === 1 ? 'Version' : 'Versionen' }}</flux:text>
</div>
</div>
</flux:card>
<flux:card>
<div class="flex items-center gap-3">
<flux:icon name="photo" class="text-blue-400" />
<div>
<flux:text class="text-sm font-medium text-zinc-800 dark:text-zinc-200">B2in Display</flux:text>
<flux:text class="text-xs">{{ $this->stats['type_b2in'] }} {{ $this->stats['type_b2in'] === 1 ? 'Version' : 'Versionen' }}</flux:text>
</div>
</div>
</flux:card>
<flux:card>
<div class="flex items-center gap-3">
<flux:icon name="tag" class="text-amber-400" />
<div>
<flux:text class="text-sm font-medium text-zinc-800 dark:text-zinc-200">Angebote</flux:text>
<flux:text class="text-xs">{{ $this->stats['type_offers'] }} {{ $this->stats['type_offers'] === 1 ? 'Version' : 'Versionen' }}</flux:text>
</div>
</div>
</flux:card>
</div>
{{-- Beschreibung --}}
<flux:card class="mt-6 max-w-4xl">
<flux:heading size="lg" class="mb-4">So funktioniert das Display-System</flux:heading>
<div class="space-y-5 text-sm text-zinc-600 dark:text-zinc-400">
<div>
<flux:heading size="sm" class="mb-1 flex items-center gap-2">
<flux:icon name="squares-2x2" class="size-4 text-zinc-500" />
Überblick
</flux:heading>
<p>
Das Display-System steuert alle Bildschirme im Cabinet Showroom Bielefeld.
Es besteht aus drei Bereichen, die Sie über die Kacheln oben erreichen:
</p>
<ul class="mt-2 ml-5 list-disc space-y-1">
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Mediathek</strong> Zentrale Verwaltung aller Bilder und Videos fuer die Displays. Dateien bis 50 MB direkt hochladen oder groessere Videos als externe URL (Google Drive, OneDrive) einbinden.</li>
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Versionen</strong> Content-Pakete, die auf den Displays abgespielt werden. Jede Version hat einen bestimmten Typ und enthält passende Inhalte (Videos, Bilder oder Angebots-Slides).</li>
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Displays</strong> Die physischen Bildschirme im Showroom. Jedem Display werden eine oder mehrere Versionen als Playlist zugewiesen.</li>
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Info-Tablet</strong> Das Tablet an der Eingangstür des Showrooms. Hier verwalten Sie Öffnungszeiten, den aktuellen Store-Status und Hinweise für Besucher.</li>
</ul>
</div>
<flux:separator />
<div>
<flux:heading size="sm" class="mb-1 flex items-center gap-2">
<flux:icon name="photo" class="size-4 text-violet-500" />
Mediathek
</flux:heading>
<p>
Die <strong class="font-medium text-zinc-800 dark:text-zinc-200">Display-Mediathek</strong> verwaltet alle Bilder und Videos, die auf den Displays im Showroom angezeigt werden.
Sie ist unabhängig von der Website-Mediathek (Flux CMS) und speziell auf die Anforderungen der Displays zugeschnitten.
</p>
<ul class="mt-2 ml-5 list-disc space-y-1">
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Direkt-Upload:</strong> Bilder und Videos bis 50 MB direkt per Drag-and-drop oder Dateiauswahl hochladen. Die Dateien werden auf dem Server gespeichert und stehen sofort zur Verfügung.</li>
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Externe URLs:</strong> Für Videos über 50 MB (z.&nbsp;B. 4K-Showroom-Rundgänge) können Sie einen Freigabe-Link von Google Drive, OneDrive oder anderen Cloud-Diensten hinterlegen. Diese URL wird wie ein normales Medium in der Mediathek verwaltet und kann genauso in Versionen eingebunden werden.</li>
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Sammlungen:</strong> Ordnen Sie Medien in Sammlungen wie <em>immobilien</em>, <em>moebel</em> oder <em>brand</em>, um bei vielen Dateien den Überblick zu behalten.</li>
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Medienauswahl im Editor:</strong> Beim Bearbeiten einer Version erscheint ein „Aus Mediathek"-Button. Darüber öffnen Sie die Medienauswahl und können bestehende Medien wählen oder direkt neue hochladen.</li>
</ul>
</div>
<flux:separator />
<div>
<flux:heading size="sm" class="mb-1 flex items-center gap-2">
<flux:icon name="rectangle-group" class="size-4 text-purple-500" />
Versionen & Versions-Typen
</flux:heading>
<p>
Eine <strong class="font-medium text-zinc-800 dark:text-zinc-200">Version</strong> ist ein Content-Paket mit einem bestimmten Typ.
Der Typ bestimmt, welche Art von Inhalten hinzugefügt werden können:
</p>
<ul class="mt-2 ml-5 list-disc space-y-1">
<li>
<strong class="font-medium text-zinc-800 dark:text-zinc-200">Video-Display</strong>
Für Video-Playlists mit optionalem Footer. Inhalte: <em>Videos</em> (Dateiname, Titel, Position/Ausschnitt) und <em>Footer-Zeilen</em> (Überschrift, Unterzeile, optionaler QR-Code-Link).
</li>
<li>
<strong class="font-medium text-zinc-800 dark:text-zinc-200">B2in Display</strong>
Für Medien-Rotation (Bilder und Videos) im Marken-Design. Inhalte: <em>Media-Items</em> mit Kategorie (Immobilien / Möbel), Überschrift, Unterzeile und Anzeigedauer. Unterstützt Light-/Dark-Theme.
</li>
<li>
<strong class="font-medium text-zinc-800 dark:text-zinc-200">Angebote</strong>
Für Produkt-Slides im Angebotsformat. Verschiedene Slide-Layouts: Intro, Produkt-Hero, Produkt-Details und Impuls-Slides mit Preisen, Badges und QR-Codes.
</li>
</ul>
<p class="mt-2">
Innerhalb einer Version können Sie beliebig viele Inhalte anlegen, die Reihenfolge per Hoch/Runter-Sortierung festlegen und einzelne Einträge aktivieren oder deaktivieren.
</p>
</div>
<flux:separator />
<div>
<flux:heading size="sm" class="mb-1 flex items-center gap-2">
<flux:icon name="tv" class="size-4 text-blue-500" />
Displays & Playlists
</flux:heading>
<p>
Ein <strong class="font-medium text-zinc-800 dark:text-zinc-200">Display</strong> repräsentiert einen physischen Bildschirm im Showroom.
</p>
<ul class="mt-2 ml-5 list-disc space-y-1">
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Versions-Zuweisung:</strong> Jedem Display können Sie eine oder mehrere Versionen zuordnen. Die Versionen werden in der festgelegten Reihenfolge als Playlist abgespielt.</li>
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Aktiv/Inaktiv:</strong> Über den Aktiv-Status können Sie einzelne Displays vorübergehend deaktivieren, ohne die Konfiguration zu verlieren.</li>
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">API-Anbindung:</strong> Jedes Display ruft seine Inhalte über eine JSON-API ab (<code class="text-xs bg-zinc-100 dark:bg-zinc-800 px-1 py-0.5 rounded">/api/display/{id}/config</code>). Änderungen werden beim nächsten Abruf automatisch übernommen.</li>
</ul>
</div>
<flux:separator />
<div>
<flux:heading size="sm" class="mb-1 flex items-center gap-2">
<flux:icon name="device-tablet" class="size-4 text-teal-500" />
Info-Tablet
</flux:heading>
<p>
Das Info-Tablet zeigt Besuchern am Showroom-Eingang den aktuellen Status und die Öffnungszeiten.
</p>
<ul class="mt-2 ml-5 list-disc space-y-1">
<li>
<strong class="font-medium text-zinc-800 dark:text-zinc-200">Store-Status:</strong>
Vier Modi stehen zur Verfügung <em>Automatisch</em> (berechnet den Status aus den Öffnungszeiten), <em>Geschlossen</em> (manuell), <em>Hinweis</em> (eigene Nachricht) und <em>Warnung</em> (dringende Nachricht).
</li>
<li>
<strong class="font-medium text-zinc-800 dark:text-zinc-200">Öffnungszeiten:</strong>
Für jeden Wochentag (MontagSonntag) können individuelle Öffnungs- und Schließzeiten gepflegt werden. Tage ohne Zeiten gelten als geschlossen.
</li>
<li>
<strong class="font-medium text-zinc-800 dark:text-zinc-200">Tages-Overrides:</strong>
Für Sonderfälle (z.&nbsp;B. früher schließen) können Sie die Zeiten für den heutigen Tag überschreiben. Diese Überschreibungen werden automatisch um Mitternacht zurückgesetzt.
</li>
<li>
<strong class="font-medium text-zinc-800 dark:text-zinc-200">Kontaktdaten & Termine:</strong>
Telefonnummer, E-Mail-Adresse und der nächste Termin werden auf dem Tablet angezeigt und können hier zentral gepflegt werden.
</li>
</ul>
</div>
<flux:separator />
<div>
<flux:heading size="sm" class="mb-1 flex items-center gap-2">
<flux:icon name="arrow-path" class="size-4 text-zinc-500" />
Typischer Workflow
</flux:heading>
<ol class="mt-2 ml-5 list-decimal space-y-1">
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Version erstellen</strong> Unter „Versionen" eine neue Version mit passendem Typ anlegen (z.&nbsp;B. „Frühling 2026 Video").</li>
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Inhalte hinzufügen</strong> In der Version Videos, Medien oder Slides anlegen, Reihenfolge festlegen und aktivieren.</li>
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Display zuweisen</strong> Unter „Displays" die Version einem physischen Bildschirm zuordnen.</li>
<li><strong class="font-medium text-zinc-800 dark:text-zinc-200">Fertig</strong> Das Display lädt die neuen Inhalte automatisch über die API.</li>
</ol>
</div>
</div>
</flux:card>
</div>

View file

@ -0,0 +1,204 @@
<div>
<flux:header class="mb-6">
<flux:heading size="xl">{{ __('Displays') }}</flux:heading>
<flux:subheading>{{ __('Verwalten Sie Ihre physischen Displays und weisen Sie ihnen Versionen zu') }}</flux:subheading>
</flux:header>
@if (session()->has('success'))
<x-success-alert>
{{ session('success') }}
</x-success-alert>
@endif
<flux:card>
<div class="flex items-center justify-between mb-6">
<div>
<flux:heading size="lg">{{ __('Physische Displays') }}</flux:heading>
<flux:subheading>{{ __('Jedem Display können mehrere Versionen als Playlist zugewiesen werden') }}</flux:subheading>
</div>
<flux:button wire:click="openModal" icon="plus" variant="primary">
{{ __('Display hinzufügen') }}
</flux:button>
</div>
@if($displays->isEmpty())
<div class="text-center py-12 text-zinc-500 dark:text-zinc-400">
<flux:icon.tv class="w-16 h-16 mx-auto mb-4 opacity-50" />
<p>{{ __('Noch keine Displays vorhanden. Fügen Sie Ihr erstes Display hinzu!') }}</p>
</div>
@else
<div class="space-y-3">
@foreach($displays as $display)
<div wire:key="display-{{ $display->id }}"
class="flex items-center gap-4 p-4 bg-zinc-50 dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 hover:border-zinc-300 dark:hover:border-zinc-600 transition">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3 mb-1">
<flux:badge :color="$display->is_active ? 'green' : 'zinc'" size="sm">
{{ $display->is_active ? __('Aktiv') : __('Inaktiv') }}
</flux:badge>
<span class="font-semibold text-sm">{{ $display->name }}</span>
@if($display->location)
<span class="text-xs text-zinc-500 dark:text-zinc-400">{{ $display->location }}</span>
@endif
</div>
@if($display->versions->isNotEmpty())
<div class="flex flex-wrap items-center gap-1.5 mt-2">
@foreach($display->versions as $idx => $version)
@if($idx > 0)
<flux:icon.chevron-right class="w-3 h-3 text-zinc-400" />
@endif
<flux:badge color="{{ match($version->type->value) {
'video-display' => 'purple',
'b2in' => 'blue',
'offers' => 'amber',
} }}" size="sm">
{{ $version->name }}
</flux:badge>
@endforeach
</div>
@else
<div class="text-xs text-amber-600 dark:text-amber-400 mt-1">
{{ __('Keine Versionen zugewiesen') }}
</div>
@endif
{{-- Direct display links --}}
<div class="mt-2 flex items-center gap-4">
<a href="/_cabinet/display/index.html?id={{ $display->id }}"
target="_blank"
class="text-xs text-blue-600 dark:text-blue-400 hover:underline inline-flex items-center gap-1">
<flux:icon.play class="w-3 h-3" />
Display öffnen
</a>
<a href="/api/display/{{ $display->id }}/config"
target="_blank"
class="text-xs text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 hover:underline inline-flex items-center gap-1">
<flux:icon.code-bracket class="w-3 h-3" />
API
</a>
</div>
</div>
<div class="flex items-center gap-2">
<flux:button wire:click="toggleActive({{ $display->id }})"
size="sm"
variant="ghost"
:icon="$display->is_active ? 'eye-slash' : 'eye'">
</flux:button>
<flux:button wire:click="openModal({{ $display->id }})"
size="sm"
variant="ghost"
icon="pencil">
</flux:button>
<flux:button wire:click="deleteDisplay({{ $display->id }})"
wire:confirm="Möchten Sie dieses Display wirklich löschen?"
size="sm"
variant="ghost"
icon="trash"
class="text-red-600 hover:text-red-700">
</flux:button>
</div>
</div>
@endforeach
</div>
@endif
</flux:card>
{{-- Display Modal --}}
<flux:modal :open="$showModal" wire:model="showModal">
<form wire:submit.prevent="save">
<div class="space-y-6">
<div>
<flux:heading size="lg">{{ $displayId ? __('Display bearbeiten') : __('Display hinzufügen') }}</flux:heading>
</div>
<flux:input wire:model="displayName" label="Name" placeholder="z.B. Display 1 - Eingang" />
@error('displayName') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
<flux:input wire:model="displayLocation" label="Standort (optional)" placeholder="z.B. Schaufenster links" />
{{-- Version Playlist --}}
<div>
<flux:heading size="sm" class="mb-2">{{ __('Versions-Playlist') }}</flux:heading>
<flux:subheading class="mb-3">{{ __('Versionen werden in dieser Reihenfolge als Schleife abgespielt') }}</flux:subheading>
@if(count($selectedVersionIds) > 0)
<div class="space-y-2 mb-3">
@foreach($selectedVersionIds as $index => $versionId)
@php $ver = $versions->firstWhere('id', $versionId); @endphp
@if($ver)
<div wire:key="playlist-{{ $index }}-{{ $versionId }}"
class="flex items-center gap-2 p-2 bg-zinc-100 dark:bg-zinc-700 rounded border border-zinc-200 dark:border-zinc-600">
<span class="text-xs text-zinc-400 font-mono w-5 text-center">{{ $index + 1 }}</span>
<flux:badge color="{{ match($ver->type->value) {
'video-display' => 'purple',
'b2in' => 'blue',
'offers' => 'amber',
} }}" size="sm">
{{ $ver->type->label() }}
</flux:badge>
<span class="text-sm flex-1">{{ $ver->name }}</span>
<div class="flex items-center gap-1">
<flux:button wire:click="moveVersion({{ $index }}, 'up')"
size="xs"
variant="ghost"
icon="chevron-up"
:disabled="$index === 0">
</flux:button>
<flux:button wire:click="moveVersion({{ $index }}, 'down')"
size="xs"
variant="ghost"
icon="chevron-down"
:disabled="$index === count($selectedVersionIds) - 1">
</flux:button>
<flux:button wire:click="removeVersion({{ $index }})"
size="xs"
variant="ghost"
icon="x-mark"
class="text-red-500">
</flux:button>
</div>
</div>
@endif
@endforeach
</div>
@else
<div class="text-center py-4 text-zinc-400 text-sm border border-dashed border-zinc-300 dark:border-zinc-600 rounded mb-3">
{{ __('Noch keine Versionen hinzugefügt') }}
</div>
@endif
<div class="flex gap-2">
<div class="flex-1">
<flux:select wire:model="addVersionSelect" placeholder="Version hinzufügen...">
@foreach($versions as $version)
<option value="{{ $version->id }}">{{ $version->name }} ({{ $version->type->label() }})</option>
@endforeach
</flux:select>
</div>
<flux:button wire:click="addVersion"
icon="plus"
size="sm"
variant="ghost">
</flux:button>
</div>
</div>
<flux:checkbox wire:model="displayIsActive" label="Display aktiv" />
<div class="flex justify-end gap-3 pt-4">
<flux:button type="button" wire:click="closeModal" variant="ghost">
{{ __('Abbrechen') }}
</flux:button>
<flux:button type="submit" variant="primary">
{{ $displayId ? __('Aktualisieren') : __('Hinzufügen') }}
</flux:button>
</div>
</div>
</form>
</flux:modal>
</div>

View file

@ -0,0 +1,541 @@
<?php
use App\Models\DisplayMedia;
use App\Services\DisplayMediaService;
use Flux\Flux;
use Illuminate\Support\Facades\Storage;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Livewire\WithFileUploads;
use function Livewire\Volt\{layout, title, state, computed, on, uses};
layout('components.layouts.app');
title('Display-Mediathek');
uses([WithFileUploads::class]);
state([
'search' => '',
'filterType' => 'all',
'filterSource' => 'all',
'filterCollection' => '',
'viewMode' => 'grid',
'editingId' => null,
'editTitle' => '',
'editAltText' => '',
'editCollection' => '',
'showDetail' => false,
// Upload
'uploads' => [],
// External URL form
'showUrlModal' => false,
'urlInput' => '',
'urlType' => 'video',
'urlTitle' => '',
'urlCollection' => '',
'urlValidated' => null,
]);
$media = computed(
fn () => DisplayMedia::query()
->when($this->filterType !== 'all', fn ($q) => match ($this->filterType) {
'image' => $q->images(),
'video' => $q->videos(),
default => $q,
})
->when($this->filterSource !== 'all', fn ($q) => match ($this->filterSource) {
'upload' => $q->uploads(),
'external' => $q->externals(),
default => $q,
})
->when($this->filterCollection, fn ($q) => $q->inCollection($this->filterCollection))
->when($this->search, fn ($q) => $q->where('filename', 'like', "%{$this->search}%")
->orWhere('title', 'like', "%{$this->search}%"))
->orderByDesc('created_at')
->paginate(48),
);
$collections = computed(fn () => DisplayMedia::query()
->whereNotNull('collection')
->where('collection', '!=', '')
->distinct()
->pluck('collection')
->sort()
->values()
->toArray());
$stats = computed(fn () => [
'total' => DisplayMedia::count(),
'images' => DisplayMedia::images()->count(),
'videos' => DisplayMedia::videos()->count(),
'uploads' => DisplayMedia::uploads()->count(),
'externals' => DisplayMedia::externals()->count(),
]);
// ========================================
// FILE UPLOAD
// ========================================
$handleUploads = function () {
$this->validate([
'uploads' => 'nullable|array|max:10',
'uploads.*' => 'file|mimes:jpeg,jpg,png,gif,webp,mp4,webm,mov|max:51200',
]);
$service = app(DisplayMediaService::class);
$count = 0;
foreach ($this->uploads as $file) {
$service->storeUpload($file);
$count++;
}
$this->uploads = [];
if ($count > 0) {
Flux::toast(variant: 'success', heading: 'Hochgeladen', text: "{$count} Datei(en) erfolgreich hochgeladen.");
}
};
// ========================================
// EXTERNAL URL
// ========================================
$openUrlModal = function () {
$this->urlInput = '';
$this->urlType = 'video';
$this->urlTitle = '';
$this->urlCollection = '';
$this->urlValidated = null;
$this->showUrlModal = true;
};
$validateUrl = function () {
$this->validate(['urlInput' => 'required|url|max:2048']);
$service = app(DisplayMediaService::class);
$this->urlValidated = $service->validateExternalUrl($this->urlInput);
};
$saveExternalUrl = function () {
$this->validate([
'urlInput' => 'required|url|max:2048',
'urlType' => 'required|in:image,video',
'urlTitle' => 'nullable|string|max:255',
]);
$service = app(DisplayMediaService::class);
$service->createFromUrl(
url: $this->urlInput,
type: $this->urlType,
title: $this->urlTitle ?: null,
collection: $this->urlCollection ?: null,
);
$this->showUrlModal = false;
Flux::toast(variant: 'success', heading: 'Externe URL angelegt', text: 'Das Medium wurde als externe Referenz gespeichert.');
};
// ========================================
// DETAIL / EDIT
// ========================================
$startEdit = function (int $id) {
$media = DisplayMedia::find($id);
if (! $media) {
return;
}
$this->editingId = $id;
$this->editTitle = $media->title ?? '';
$this->editAltText = $media->alt_text ?? '';
$this->editCollection = $media->collection ?? '';
$this->showDetail = true;
};
$saveEdit = function () {
$media = DisplayMedia::find($this->editingId);
if (! $media) {
return;
}
$media->update([
'title' => $this->editTitle ?: null,
'alt_text' => $this->editAltText ?: null,
'collection' => $this->editCollection ?: null,
]);
Flux::toast(variant: 'success', heading: 'Gespeichert', text: 'Medien-Details wurden aktualisiert.');
};
$deleteMedia = function (int $id) {
$media = DisplayMedia::find($id);
if (! $media) {
return;
}
$filename = $media->getDisplayName();
$service = app(DisplayMediaService::class);
$service->delete($media);
$this->editingId = null;
$this->showDetail = false;
Flux::toast(variant: 'success', heading: 'Gelöscht', text: $filename . ' wurde entfernt.');
};
$closeDetail = function () {
$this->showDetail = false;
$this->editingId = null;
};
?>
<div>
<div class="mb-6 flex items-center justify-between">
<div>
<flux:heading size="xl">Display-Mediathek</flux:heading>
<flux:text class="mt-1">Bilder, Videos und externe URLs für Store Displays verwalten.</flux:text>
</div>
<div class="flex items-center gap-3">
<flux:badge color="sky">{{ $this->stats['images'] }} Bilder</flux:badge>
<flux:badge color="purple">{{ $this->stats['videos'] }} Videos</flux:badge>
<flux:badge color="blue">{{ $this->stats['externals'] }} Extern</flux:badge>
</div>
</div>
{{-- Upload + External URL --}}
<flux:card class="mb-6">
<div class="flex items-start gap-4">
<div class="flex-1">
<flux:file-upload wire:model="uploads" multiple
accept="image/jpeg,image/png,image/gif,image/webp,video/mp4,video/webm,.jpg,.jpeg,.png,.webp,.mp4,.webm,.mov">
<flux:file-upload.dropzone
heading="Dateien hochladen"
text="Bilder & Videos bis 50 MB Drag & Drop oder klicken"
with-progress />
</flux:file-upload>
@if (isset($uploads) && count($uploads) > 0)
<div class="mt-3 flex flex-wrap items-center gap-2">
@foreach ($uploads as $index => $upload)
<flux:file-item
:heading="$upload->getClientOriginalName()"
:image="(str_starts_with($upload->getMimeType() ?? '', 'image/') && $upload->isPreviewable())
? $upload->temporaryUrl()
: null"
:size="$upload->getSize()" />
@endforeach
</div>
<flux:button wire:click="handleUploads" variant="primary" size="sm" class="mt-3">
{{ count($uploads) }} Datei(en) hochladen
</flux:button>
@endif
<flux:error name="uploads" />
</div>
<div class="flex flex-col gap-2 pt-2">
<flux:button wire:click="openUrlModal" icon="link" variant="ghost">
Externe URL anlegen
</flux:button>
</div>
</div>
</flux:card>
{{-- Filters --}}
<div class="mb-4 flex flex-wrap items-center gap-3">
<flux:input wire:model.live.debounce.300ms="search" placeholder="Suchen..." icon="magnifying-glass" size="sm" class="w-56" />
<flux:select wire:model.live="filterType" size="sm" class="w-36">
<flux:select.option value="all">Alle Typen</flux:select.option>
<flux:select.option value="image">Bilder</flux:select.option>
<flux:select.option value="video">Videos</flux:select.option>
</flux:select>
<flux:select wire:model.live="filterSource" size="sm" class="w-36">
<flux:select.option value="all">Alle Quellen</flux:select.option>
<flux:select.option value="upload">Uploads</flux:select.option>
<flux:select.option value="external">Extern</flux:select.option>
</flux:select>
@if (! empty($this->collections))
<flux:select wire:model.live="filterCollection" size="sm" class="w-40">
<flux:select.option value="">Alle Sammlungen</flux:select.option>
@foreach ($this->collections as $col)
<flux:select.option value="{{ $col }}">{{ $col }}</flux:select.option>
@endforeach
</flux:select>
@endif
<div class="ml-auto flex items-center gap-1 rounded-lg border border-zinc-200 p-0.5 dark:border-zinc-700">
<button wire:click="$set('viewMode', 'grid')"
class="rounded-md p-1.5 transition {{ $viewMode === 'grid' ? 'bg-zinc-200 text-zinc-900 dark:bg-zinc-600 dark:text-white' : 'text-zinc-400 hover:text-zinc-600' }}">
<x-heroicon-s-squares-2x2 class="h-4 w-4" />
</button>
<button wire:click="$set('viewMode', 'list')"
class="rounded-md p-1.5 transition {{ $viewMode === 'list' ? 'bg-zinc-200 text-zinc-900 dark:bg-zinc-600 dark:text-white' : 'text-zinc-400 hover:text-zinc-600' }}">
<x-heroicon-s-list-bullet class="h-4 w-4" />
</button>
</div>
</div>
{{-- Media Grid / List + Detail --}}
<div class="grid grid-cols-1 gap-6 {{ $showDetail ? 'lg:grid-cols-3' : '' }}">
<div class="{{ $showDetail ? 'lg:col-span-2' : '' }}">
@if ($viewMode === 'grid')
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 {{ $showDetail ? 'lg:grid-cols-3' : 'lg:grid-cols-6' }}">
@forelse ($this->media as $item)
<div wire:key="dm-g-{{ $item->id }}"
class="group relative cursor-pointer overflow-hidden rounded-lg border transition-all
{{ $editingId === $item->id ? 'border-blue-500 ring-2 ring-blue-200 dark:ring-blue-800' : 'border-zinc-200 hover:border-zinc-400 dark:border-zinc-700' }}"
wire:click="startEdit({{ $item->id }})">
<div class="aspect-square bg-zinc-100 dark:bg-zinc-800">
@if ($item->isImage() && $item->isUpload())
<img src="{{ $item->getThumbnailUrl() }}"
alt="{{ $item->alt_text ?? $item->filename }}"
class="h-full w-full object-cover" loading="lazy" />
@elseif ($item->isVideo())
<div class="flex h-full w-full flex-col items-center justify-center gap-2 text-purple-400">
<x-heroicon-o-film class="h-10 w-10" />
<span class="text-xs">Video</span>
</div>
@elseif ($item->isExternal() && $item->isImage())
<div class="flex h-full w-full flex-col items-center justify-center gap-2 text-blue-400">
<x-heroicon-o-photo class="h-10 w-10" />
<span class="text-xs">Extern</span>
</div>
@else
<div class="flex h-full w-full flex-col items-center justify-center gap-2 text-zinc-400">
<x-heroicon-o-link class="h-10 w-10" />
<span class="text-xs">Link</span>
</div>
@endif
</div>
<div class="flex items-center gap-1.5 p-2">
@if ($item->isVideo())
<x-heroicon-s-film class="h-3.5 w-3.5 shrink-0 text-purple-500" />
@elseif ($item->isExternal())
<x-heroicon-s-link class="h-3.5 w-3.5 shrink-0 text-blue-500" />
@else
<x-heroicon-s-photo class="h-3.5 w-3.5 shrink-0 text-sky-500" />
@endif
<div class="min-w-0">
<p class="truncate text-xs font-medium text-zinc-700 dark:text-zinc-300">{{ $item->getDisplayName() }}</p>
<p class="text-xs text-zinc-400">{{ $item->getHumanFileSize() }}</p>
</div>
</div>
@if ($item->collection)
<div class="absolute right-1 top-1">
<flux:badge size="sm" color="blue" class="text-[10px]!">{{ $item->collection }}</flux:badge>
</div>
@endif
</div>
@empty
<div class="col-span-full py-12 text-center">
<x-heroicon-o-photo class="mx-auto mb-3 h-12 w-12 text-zinc-300" />
<flux:text>Noch keine Medien vorhanden. Laden Sie Dateien hoch oder legen Sie externe URLs an.</flux:text>
</div>
@endforelse
</div>
@else
{{-- List View --}}
<div class="overflow-hidden rounded-lg border border-zinc-200 dark:border-zinc-700">
<table class="w-full text-sm">
<thead class="border-b border-zinc-200 bg-zinc-50 text-left text-xs text-zinc-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-400">
<tr>
<th class="w-12 px-3 py-2"></th>
<th class="px-3 py-2">Name</th>
<th class="hidden px-3 py-2 sm:table-cell">Typ</th>
<th class="hidden px-3 py-2 sm:table-cell">Quelle</th>
<th class="hidden px-3 py-2 md:table-cell">Größe</th>
<th class="hidden px-3 py-2 lg:table-cell">Sammlung</th>
<th class="px-3 py-2 text-right">Datum</th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
@forelse ($this->media as $item)
<tr wire:key="dm-l-{{ $item->id }}"
class="cursor-pointer transition {{ $editingId === $item->id ? 'bg-blue-50 dark:bg-blue-900/20' : 'hover:bg-zinc-50 dark:hover:bg-zinc-800/50' }}"
wire:click="startEdit({{ $item->id }})">
<td class="px-3 py-1.5">
<div class="flex h-8 w-8 items-center justify-center overflow-hidden rounded border border-zinc-200 bg-zinc-100 dark:border-zinc-700 dark:bg-zinc-800">
@if ($item->isImage() && $item->isUpload())
<img src="{{ $item->getThumbnailUrl() }}" class="h-full w-full object-cover" loading="lazy" />
@elseif ($item->isVideo())
<x-heroicon-s-film class="h-4 w-4 text-purple-500" />
@else
<x-heroicon-s-link class="h-4 w-4 text-blue-500" />
@endif
</div>
</td>
<td class="px-3 py-1.5">
<span class="font-medium text-zinc-700 dark:text-zinc-300">{{ $item->getDisplayName() }}</span>
</td>
<td class="hidden px-3 py-1.5 sm:table-cell">
<flux:badge size="sm" :color="$item->isVideo() ? 'purple' : 'sky'">
{{ $item->isVideo() ? 'Video' : 'Bild' }}
</flux:badge>
</td>
<td class="hidden px-3 py-1.5 sm:table-cell">
<flux:badge size="sm" :color="$item->isExternal() ? 'blue' : 'zinc'">
{{ $item->isExternal() ? 'Extern' : 'Upload' }}
</flux:badge>
</td>
<td class="hidden px-3 py-1.5 text-zinc-500 md:table-cell">{{ $item->getHumanFileSize() }}</td>
<td class="hidden px-3 py-1.5 lg:table-cell">
@if ($item->collection)
<flux:badge size="sm" color="blue">{{ $item->collection }}</flux:badge>
@else
<span class="text-zinc-300"></span>
@endif
</td>
<td class="px-3 py-1.5 text-right text-zinc-400">{{ $item->created_at->format('d.m.Y') }}</td>
</tr>
@empty
<tr>
<td colspan="7" class="py-12 text-center">
<x-heroicon-o-photo class="mx-auto mb-3 h-12 w-12 text-zinc-300" />
<flux:text>Noch keine Medien vorhanden.</flux:text>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@endif
@if ($this->media->hasPages())
<div class="mt-4">
{{ $this->media->links() }}
</div>
@endif
</div>
{{-- Detail Sidebar --}}
@if ($showDetail && $editingId)
@php $editMedia = \App\Models\DisplayMedia::find($editingId); @endphp
@if ($editMedia)
<div class="lg:col-span-1">
<flux:card>
<div class="mb-4 flex items-center justify-between">
<flux:heading size="sm">Details</flux:heading>
<flux:button size="xs" variant="ghost" icon="x-mark" wire:click="closeDetail" />
</div>
{{-- Preview --}}
<div class="mb-4 overflow-hidden rounded-lg border border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800">
@if ($editMedia->isImage() && $editMedia->isUpload())
<img src="{{ $editMedia->getUrl() }}" alt="{{ $editMedia->filename }}"
class="w-full object-contain" style="max-height: 300px;" />
@elseif ($editMedia->isVideo() && $editMedia->isUpload())
<video controls class="w-full" style="max-height: 300px;">
<source src="{{ $editMedia->getUrl() }}" type="{{ $editMedia->mime_type }}">
</video>
@elseif ($editMedia->isExternal())
<div class="flex flex-col items-center justify-center gap-3 py-8">
<x-heroicon-o-link class="h-12 w-12 text-blue-400" />
<span class="text-sm text-zinc-500">Externe Ressource</span>
</div>
@endif
</div>
{{-- Metadata --}}
<div class="mb-4 space-y-1 text-xs text-zinc-500 dark:text-zinc-400">
<p><strong>Datei:</strong> {{ $editMedia->filename }}</p>
<p><strong>Typ:</strong>
<flux:badge size="sm" :color="$editMedia->isVideo() ? 'purple' : 'sky'" class="inline">
{{ $editMedia->isVideo() ? 'Video' : 'Bild' }}
</flux:badge>
</p>
<p><strong>Quelle:</strong>
<flux:badge size="sm" :color="$editMedia->isExternal() ? 'blue' : 'zinc'" class="inline">
{{ $editMedia->isExternal() ? 'Extern' : 'Upload' }}
</flux:badge>
</p>
@if ($editMedia->isUpload())
<p><strong>Größe:</strong> {{ $editMedia->getHumanFileSize() }}</p>
@if ($editMedia->mime_type)
<p><strong>MIME:</strong> {{ $editMedia->mime_type }}</p>
@endif
@if ($editMedia->metadata && isset($editMedia->metadata['width']))
<p><strong>Abmessungen:</strong> {{ $editMedia->metadata['width'] }}×{{ $editMedia->metadata['height'] }} px</p>
@endif
@endif
<p><strong>Angelegt:</strong> {{ $editMedia->created_at->format('d.m.Y H:i') }}</p>
<p class="break-all"><strong>URL:</strong>
<a href="{{ $editMedia->getUrl() }}" target="_blank" class="text-blue-500 hover:underline">
{{ Str::limit($editMedia->getUrl(), 80) }}
</a>
</p>
</div>
{{-- Edit Form --}}
<div class="space-y-3">
<flux:input wire:model="editTitle" label="Titel" size="sm" placeholder="Anzeigename..." />
<flux:input wire:model="editAltText" label="Alt-Text" size="sm" placeholder="Bildbeschreibung..." />
<flux:input wire:model="editCollection" label="Sammlung" size="sm" placeholder="z.B. immobilien, moebel, brand..." />
<flux:button size="sm" variant="primary" wire:click="saveEdit" class="w-full">
Speichern
</flux:button>
</div>
<div class="mt-5 border-t border-zinc-200 pt-4 dark:border-zinc-700">
<flux:button size="sm" variant="danger" class="w-full" icon="trash"
wire:click="deleteMedia({{ $editMedia->id }})"
wire:confirm="Dieses Medium wirklich löschen?">
Löschen
</flux:button>
</div>
</flux:card>
</div>
@endif
@endif
</div>
{{-- External URL Modal --}}
<flux:modal wire:model="showUrlModal" class="max-w-lg">
<form wire:submit.prevent="saveExternalUrl">
<div class="space-y-6">
<div>
<flux:heading size="lg">Externe URL anlegen</flux:heading>
<flux:text class="mt-1">Binden Sie große Videos oder Medien von Google Drive, OneDrive oder anderen Quellen ein.</flux:text>
</div>
<div class="space-y-4">
<div>
<flux:input wire:model="urlInput" label="URL" placeholder="https://drive.google.com/file/d/..." />
<flux:error name="urlInput" />
</div>
<div class="flex items-end gap-3">
<flux:button type="button" wire:click="validateUrl" size="sm" variant="ghost" icon="arrow-path">
URL prüfen
</flux:button>
@if ($urlValidated === true)
<span class="flex items-center gap-1 text-sm text-green-600">
<x-heroicon-s-check-circle class="h-4 w-4" /> Erreichbar
</span>
@elseif ($urlValidated === false)
<span class="flex items-center gap-1 text-sm text-amber-600">
<x-heroicon-s-exclamation-triangle class="h-4 w-4" /> Nicht erreichbar trotzdem speichern möglich
</span>
@endif
</div>
<flux:select wire:model="urlType" label="Medientyp">
<option value="video">Video</option>
<option value="image">Bild</option>
</flux:select>
<flux:input wire:model="urlTitle" label="Titel (optional)" placeholder="z.B. Showroom-Rundgang 4K" />
<flux:input wire:model="urlCollection" label="Sammlung (optional)" placeholder="z.B. immobilien, moebel..." />
</div>
<div class="flex justify-end gap-3 pt-4">
<flux:button type="button" wire:click="$set('showUrlModal', false)" variant="ghost">
Abbrechen
</flux:button>
<flux:button type="submit" variant="primary">
Anlegen
</flux:button>
</div>
</div>
</form>
</flux:modal>
</div>

View file

@ -0,0 +1,136 @@
<div>
<div class="flex items-end gap-3">
<div class="flex-1">
@if ($selectedMedia)
<div class="flex items-center gap-3 rounded-lg border border-zinc-200 p-2 dark:border-zinc-700">
@if ($selectedMedia->isImage() && $selectedMedia->isUpload())
<img src="{{ $selectedMedia->getThumbnailUrl() }}"
alt="{{ $selectedMedia->filename }}"
class="h-16 w-16 rounded-md object-cover" />
@elseif ($selectedMedia->isVideo())
<div class="flex h-16 w-16 items-center justify-center rounded-md bg-purple-50 dark:bg-purple-900/20">
<x-heroicon-o-film class="h-8 w-8 text-purple-500" />
</div>
@elseif ($selectedMedia->isExternal())
<div class="flex h-16 w-16 items-center justify-center rounded-md bg-blue-50 dark:bg-blue-900/20">
<x-heroicon-o-link class="h-8 w-8 text-blue-500" />
</div>
@else
<div class="flex h-16 w-16 items-center justify-center rounded-md bg-zinc-100 dark:bg-zinc-800">
<x-heroicon-o-photo class="h-8 w-8 text-zinc-400" />
</div>
@endif
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-zinc-700 dark:text-zinc-300">
{{ $selectedMedia->getDisplayName() }}
</p>
<div class="flex items-center gap-2 text-xs text-zinc-400">
<span>{{ $selectedMedia->getHumanFileSize() }}</span>
@if ($selectedMedia->isExternal())
<flux:badge size="sm" color="blue">Extern</flux:badge>
@endif
<flux:badge size="sm" :color="$selectedMedia->isVideo() ? 'purple' : 'sky'">
{{ $selectedMedia->isVideo() ? 'Video' : 'Bild' }}
</flux:badge>
</div>
</div>
<flux:button size="xs" variant="ghost" icon="x-mark" wire:click="clearSelection" />
</div>
@else
<div class="rounded-lg border-2 border-dashed border-zinc-300 px-4 py-3 text-center text-sm text-zinc-400 dark:border-zinc-600">
Kein Medium ausgewählt
</div>
@endif
</div>
<flux:button size="sm" variant="ghost" icon="photo" wire:click="openPicker">
{{ $label }}
</flux:button>
</div>
<flux:modal wire:model="showModal" class="w-full max-w-4xl space-y-4 overflow-y-auto max-h-[85vh]">
<flux:heading size="lg">{{ $label }}</flux:heading>
<div class="flex flex-col gap-3">
<flux:input wire:model.live.debounce.300ms="search" placeholder="Suchen (Dateiname oder Titel)..." icon="magnifying-glass" size="sm" />
<flux:file-upload wire:model="quickUploads" multiple
accept="image/jpeg,image/png,image/gif,image/webp,video/mp4,video/webm,.jpg,.jpeg,.png,.webp,.mp4,.webm,.mov">
<flux:file-upload.dropzone
heading="Datei hochladen"
text="Bilder bis 50 MB, Videos bis 50 MB größere Videos bitte über die Mediathek als externe URL anlegen"
with-progress />
</flux:file-upload>
@if (isset($quickUploads) && count($quickUploads) > 0)
<div class="flex flex-wrap items-center gap-2">
@foreach ($quickUploads as $index => $upload)
<flux:file-item
:heading="$upload->getClientOriginalName()"
:image="(str_starts_with($upload->getMimeType() ?? '', 'image/') && $upload->isPreviewable())
? $upload->temporaryUrl()
: null"
:size="$upload->getSize()">
<x-slot name="actions">
<flux:file-item.remove wire:click="removeQuickUpload({{ $index }})" />
</x-slot>
</flux:file-item>
@endforeach
</div>
@endif
<flux:error name="quickUploads" />
</div>
<div class="grid grid-cols-3 gap-2 sm:grid-cols-4 md:grid-cols-6">
@forelse ($mediaItems as $item)
<div wire:key="dpick-{{ $item->id }}"
class="group cursor-pointer overflow-hidden rounded-lg border transition-all
{{ $value === $item->id ? 'border-blue-500 ring-2 ring-blue-200' : 'border-zinc-200 hover:border-blue-300 dark:border-zinc-700' }}"
wire:click="selectMedia({{ $item->id }})">
<div class="relative aspect-square bg-zinc-100 dark:bg-zinc-800">
@if ($item->isImage() && $item->isUpload())
<img src="{{ $item->getThumbnailUrl() }}"
alt="{{ $item->filename }}" class="h-full w-full object-cover" loading="lazy" />
@elseif ($item->isVideo())
<div class="flex h-full w-full items-center justify-center text-purple-500">
<x-heroicon-o-film class="h-10 w-10" />
</div>
@elseif ($item->isExternal() && $item->isImage())
<div class="flex h-full w-full items-center justify-center text-blue-500">
<x-heroicon-o-photo class="h-10 w-10" />
</div>
@else
<div class="flex h-full w-full items-center justify-center text-zinc-400">
<x-heroicon-o-link class="h-10 w-10" />
</div>
@endif
{{-- Badges --}}
<div class="absolute right-1 top-1 flex flex-col gap-1">
@if ($item->isExternal())
<flux:badge size="sm" color="blue" class="text-[10px]!">URL</flux:badge>
@endif
@if ($item->isVideo())
<flux:badge size="sm" color="purple" class="text-[10px]!">Video</flux:badge>
@endif
</div>
</div>
<div class="p-1.5">
<p class="truncate text-[11px] font-medium text-zinc-600 dark:text-zinc-400">{{ $item->getDisplayName() }}</p>
<p class="truncate text-[10px] text-zinc-400">{{ $item->getHumanFileSize() }}</p>
</div>
</div>
@empty
<div class="col-span-full py-8 text-center">
<flux:text>Keine Medien gefunden. Laden Sie Dateien hoch oder legen Sie externe URLs in der Mediathek an.</flux:text>
</div>
@endforelse
</div>
@if ($mediaItems->hasPages())
<div class="mt-2">
{{ $mediaItems->links() }}
</div>
@endif
</flux:modal>
</div>

View file

@ -0,0 +1,255 @@
<div>
{{-- Header --}}
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-4">
<flux:button :href="route('admin.cms.display-versions')" wire:navigate variant="ghost" icon="arrow-left" size="sm">
{{ __('Zurück') }}
</flux:button>
<div>
<div class="flex items-center gap-3">
<flux:heading size="xl">{{ $version->name }}</flux:heading>
<flux:badge color="{{ match($version->type->value) {
'video-display' => 'purple',
'b2in' => 'blue',
'offers' => 'amber',
} }}">
{{ $version->type->label() }}
</flux:badge>
</div>
<flux:subheading>{{ __('Version bearbeiten') }}</flux:subheading>
</div>
</div>
<div class="flex items-center gap-3">
@if($version->type->value === 'b2in')
<div class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700">
<flux:icon.sun class="w-4 h-4 text-amber-500" />
<flux:switch wire:click="toggleTheme"
:checked="($version->settings['theme'] ?? 'dark') === 'dark'" />
<flux:icon.moon class="w-4 h-4 text-indigo-400" />
</div>
@endif
<flux:button wire:click="openSettingsModal" icon="cog-6-tooth" variant="ghost">
{{ __('Einstellungen') }}
</flux:button>
</div>
</div>
@if (session()->has('success'))
<x-success-alert>
{{ session('success') }}
</x-success-alert>
@endif
{{-- Name bearbeiten --}}
<flux:card class="mb-6">
<form wire:submit.prevent="saveName" class="flex items-end gap-4">
<div class="flex-1">
<flux:input wire:model="versionName" label="Versionsname" />
</div>
<flux:button type="submit" variant="primary" size="sm">{{ __('Speichern') }}</flux:button>
</form>
</flux:card>
{{-- Type-specific content sections --}}
@if($version->type->value === 'video-display')
@include('livewire.admin.cms.partials.version-editor-video', ['items' => $items])
@elseif($version->type->value === 'b2in')
@include('livewire.admin.cms.partials.version-editor-b2in', ['items' => $items])
@elseif($version->type->value === 'offers')
@include('livewire.admin.cms.partials.version-editor-offers', ['items' => $items])
@endif
{{-- Item Modal --}}
<flux:modal :open="$showItemModal" wire:model="showItemModal">
<form wire:submit.prevent="saveItem">
<div class="space-y-6">
<div>
<flux:heading size="lg">{{ $itemId ? __('Inhalt bearbeiten') : __('Inhalt hinzufügen') }}</flux:heading>
</div>
{{-- Video fields --}}
@if($itemType === 'video')
<livewire:admin.cms.display-media-picker
:value="null"
field="videoFilename"
type="video"
label="Video aus Mediathek"
:key="'picker-video-' . ($itemId ?? 'new')" />
<flux:input wire:model="videoFilename" label="Video-Pfad / URL" placeholder="Wird automatisch gesetzt oder manuell eingeben..."
description="Über die Mediathek auswählen oder direkt Pfad/URL eingeben." />
<flux:input wire:model="videoTitle" label="Titel (optional)" placeholder="z.B. Herbst Kollektion 2025" />
<flux:input wire:model="videoPosition" type="number" min="0" max="100" label="Position (%)"
description="Vertikale Position im Video (0 = oben, 100 = unten)" />
<flux:checkbox wire:model="videoIsActive" label="Aktiv" />
@endif
{{-- Footer fields --}}
@if($itemType === 'footer')
<flux:input wire:model="footerHeadline" label="Überschrift" placeholder="z.B. Beratung & Termin" />
<flux:input wire:model="footerSubline" label="Unterzeile" placeholder="z.B. Jetzt Termin vereinbaren." />
<flux:input wire:model="footerUrl" label="URL (optional)" placeholder="https://..."
description="Leer = kein QR-Code. Mit URL = QR-Code wird generiert." />
<flux:checkbox wire:model="footerIsActive" label="Aktiv" />
@endif
{{-- Media fields (B2in) --}}
@if($itemType === 'media')
<flux:select wire:model="mediaType" label="Medientyp">
<option value="image">Bild</option>
<option value="video">Video</option>
</flux:select>
<flux:select wire:model="mediaCategory" label="Kategorie">
<option value="immobilien">Immobilien</option>
<option value="moebel">Möbel</option>
</flux:select>
<livewire:admin.cms.display-media-picker
:value="null"
field="mediaUrl"
:type="$mediaType === 'video' ? 'video' : 'image'"
label="Aus Mediathek"
:key="'picker-media-' . ($itemId ?? 'new')" />
<flux:input wire:model="mediaUrl" label="Medien-URL" placeholder="Wird automatisch gesetzt oder manuell eingeben..."
description="Über die Mediathek auswählen oder direkt URL eingeben." />
<flux:input wire:model="mediaHeadline" label="Überschrift" placeholder="z.B. Ihr Zuhause. Weltweit." />
<flux:input wire:model="mediaSubline" label="Unterzeile" placeholder="z.B. Beratung und Vermittlung." />
@if($mediaType === 'image')
<flux:input wire:model="mediaDuration" type="number" min="1" max="120" label="Dauer (Sekunden)"
description="Nur für Bilder Videos spielen bis zum Ende." />
@endif
<flux:checkbox wire:model="mediaIsActive" label="Aktiv" />
@endif
{{-- Slide fields (Offers) --}}
@if($itemType === 'slide')
{{-- Basis --}}
<flux:select wire:model.live="slideType" label="Slide-Typ">
<option value="intro">Intro</option>
<option value="product-hero">Produkt-Hero</option>
<option value="product-details">Produkt-Details</option>
<option value="product-impulse">Produkt-Impuls</option>
</flux:select>
<flux:input wire:model="slideDuration" type="number" min="1000" label="Dauer (ms)" />
<livewire:admin.cms.display-media-picker
:value="null"
field="slideImageUrl"
type="image"
label="Bild aus Mediathek"
:key="'picker-slide-' . ($itemId ?? 'new')" />
<flux:input wire:model="slideImageUrl" label="Bild-URL" placeholder="Wird automatisch gesetzt oder manuell eingeben..."
description="Über die Mediathek auswählen oder direkt URL eingeben." />
<flux:input wire:model="slideBadge" label="Badge-Text" placeholder="z.B. Einzelstück" />
<flux:input wire:model="slideEyebrow" label="Eyebrow" placeholder="z.B. Hersteller: Sudbrock" />
<flux:input wire:model="slideTitle" label="Titel" placeholder="z.B. GOYA Sideboard" />
{{-- Intro-spezifisch --}}
@if($slideType === 'intro')
<flux:input wire:model="slideDisclaimer" label="Disclaimer" placeholder="z.B. Zwischenverkauf vorbehalten" />
<flux:checkbox wire:model="slideShowBrandText" label="Brand-Text anzeigen" />
@if($slideShowBrandText)
<flux:input wire:model="slideBrandTagline" label="Brand-Tagline" placeholder="z.B. Planung • Beratung • Lieferung & Montage" />
@endif
@endif
{{-- Product-Hero --}}
@if($slideType === 'product-hero')
<flux:input wire:model="slidePrice" label="Preis" placeholder="z.B. 489 €" />
<flux:input wire:model="slideOriginalPrice" label="Originalpreis" placeholder="z.B. statt 4.744 €" />
@endif
{{-- Product-Details --}}
@if($slideType === 'product-details')
<div>
<flux:heading size="sm" class="mb-2">{{ __('Aufzählungspunkte') }}</flux:heading>
<div class="space-y-2">
@foreach($slideBullets as $i => $bullet)
<div class="flex items-center gap-2" wire:key="bullet-{{ $i }}">
<flux:input wire:model="slideBullets.{{ $i }}" placeholder="Punkt {{ $i + 1 }}" class="flex-1" />
<flux:button wire:click="removeBullet({{ $i }})" size="xs" variant="ghost" icon="x-mark" class="text-red-500"></flux:button>
</div>
@endforeach
</div>
<flux:button wire:click="addBullet" size="xs" variant="ghost" icon="plus" class="mt-2">
{{ __('Punkt hinzufügen') }}
</flux:button>
</div>
@endif
{{-- Product-Impulse --}}
@if($slideType === 'product-impulse')
<flux:input wire:model="slideSubline" label="Subline" placeholder="z.B. Heute mitnehmen" />
<flux:input wire:model="slidePrice" label="Preis" placeholder="z.B. 199 €" />
<flux:input wire:model="slideTagText" label="Tag-Text" placeholder="z.B. Im Store verfügbar" />
@endif
{{-- QR --}}
<div class="border-t border-zinc-200 dark:border-zinc-700 pt-4 mt-2">
<flux:heading size="sm" class="mb-3">{{ __('QR-Code & Kontakt') }}</flux:heading>
<div class="space-y-4">
<flux:input wire:model="slideQrUrl" label="QR-URL" placeholder="z.B. https://cabinet-bielefeld.de" />
<flux:input wire:model="slideQrTitle" label="QR-Titel" placeholder="z.B. Reservieren" />
<flux:input wire:model="slideContact" label="Kontakt" placeholder="z.B. 0521 98620100 / Tel. oder WhatsApp" />
</div>
</div>
<flux:checkbox wire:model="slideIsActive" label="Aktiv" />
@endif
<div class="flex justify-end gap-3 pt-4">
<flux:button type="button" wire:click="closeItemModal" variant="ghost">
{{ __('Abbrechen') }}
</flux:button>
<flux:button type="submit" variant="primary">
{{ $itemId ? __('Aktualisieren') : __('Hinzufügen') }}
</flux:button>
</div>
</div>
</form>
</flux:modal>
{{-- Settings Modal --}}
<flux:modal :open="$showSettingsModal" wire:model="showSettingsModal">
<form wire:submit.prevent="saveSettings">
<div class="space-y-6">
<div>
<flux:heading size="lg">{{ __('Einstellungen') }}</flux:heading>
<flux:subheading>{{ $version->type->label() }}</flux:subheading>
</div>
@if($version->type->value === 'b2in')
<flux:select wire:model="settings.theme" label="Theme">
<option value="dark">Dark</option>
<option value="light">Light</option>
</flux:select>
<flux:input wire:model="settings.footer_name" label="Footer Name" placeholder="z.B. Marcel Scheibe" />
<flux:input wire:model="settings.footer_url" label="Footer URL" placeholder="z.B. b2in.de" />
<flux:select wire:model="settings.transition.type" label="Transition">
<option value="crossfade">Crossfade</option>
<option value="fade">Fade</option>
<option value="slide">Slide</option>
</flux:select>
<flux:input wire:model="settings.transition.duration_ms" type="number" label="Transition-Dauer (ms)" />
<flux:input wire:model="settings.default_image_duration" type="number" label="Standard-Bilddauer (Sek.)" />
<flux:checkbox wire:model="settings.display_active" label="Display aktiv" />
@elseif($version->type->value === 'offers')
<flux:checkbox wire:model="settings.loop" label="Endlosschleife" />
<flux:select wire:model="settings.transition.type" label="Transition">
<option value="fade">Fade</option>
<option value="slide">Slide</option>
</flux:select>
<flux:input wire:model="settings.transition.duration" type="number" label="Transition-Dauer (ms)" />
@elseif($version->type->value === 'video-display')
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Keine speziellen Einstellungen für diesen Typ.') }}</p>
@endif
<div class="flex justify-end gap-3 pt-4">
<flux:button type="button" wire:click="$set('showSettingsModal', false)" variant="ghost">
{{ __('Abbrechen') }}
</flux:button>
<flux:button type="submit" variant="primary">
{{ __('Speichern') }}
</flux:button>
</div>
</div>
</form>
</flux:modal>
</div>

View file

@ -0,0 +1,116 @@
<div>
<flux:header class="mb-6">
<flux:heading size="xl">{{ __('Display-Versionen') }}</flux:heading>
<flux:subheading>{{ __('Erstellen und verwalten Sie Inhalts-Versionen für Ihre Displays') }}</flux:subheading>
</flux:header>
@if (session()->has('success'))
<x-success-alert>
{{ session('success') }}
</x-success-alert>
@endif
<flux:card>
<div class="flex items-center justify-between mb-6">
<div>
<flux:heading size="lg">{{ __('Versionen') }}</flux:heading>
<flux:subheading>{{ __('Jede Version enthält Inhalte eines bestimmten Typs') }}</flux:subheading>
</div>
<flux:button wire:click="openCreateModal" icon="plus" variant="primary">
{{ __('Version erstellen') }}
</flux:button>
</div>
@if($versions->isEmpty())
<div class="text-center py-12 text-zinc-500 dark:text-zinc-400">
<flux:icon.rectangle-group class="w-16 h-16 mx-auto mb-4 opacity-50" />
<p>{{ __('Noch keine Versionen vorhanden. Erstellen Sie Ihre erste Version!') }}</p>
</div>
@else
<div class="space-y-3">
@foreach($versions as $version)
<div wire:key="version-{{ $version->id }}"
class="flex items-center gap-4 p-4 bg-zinc-50 dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 hover:border-zinc-300 dark:hover:border-zinc-600 transition">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3 mb-1">
<flux:badge :color="$version->is_active ? 'green' : 'zinc'" size="sm">
{{ $version->is_active ? __('Aktiv') : __('Inaktiv') }}
</flux:badge>
<a href="{{ route('admin.cms.display-version-edit', $version) }}"
wire:navigate
class="font-semibold text-sm hover:underline">
{{ $version->name }}
</a>
<flux:badge color="{{ match($version->type->value) {
'video-display' => 'purple',
'b2in' => 'blue',
'offers' => 'amber',
} }}" size="sm">
{{ $version->type->label() }}
</flux:badge>
</div>
<div class="text-xs text-zinc-600 dark:text-zinc-400 space-x-4">
<span>{{ $version->items_count }} {{ __('Inhalte') }}</span>
<span>{{ $version->displays_count }} {{ __('Displays') }}</span>
</div>
</div>
<div class="flex items-center gap-2">
<flux:button wire:click="toggleActive({{ $version->id }})"
size="sm"
variant="ghost"
:icon="$version->is_active ? 'eye-slash' : 'eye'">
</flux:button>
<flux:button :href="route('admin.cms.display-version-edit', $version)"
wire:navigate
size="sm"
variant="ghost"
icon="pencil">
</flux:button>
<flux:button wire:click="deleteVersion({{ $version->id }})"
wire:confirm="Möchten Sie diese Version wirklich löschen? Alle zugehörigen Inhalte werden ebenfalls gelöscht."
size="sm"
variant="ghost"
icon="trash"
class="text-red-600 hover:text-red-700">
</flux:button>
</div>
</div>
@endforeach
</div>
@endif
</flux:card>
{{-- Create Modal --}}
<flux:modal :open="$showCreateModal" wire:model="showCreateModal">
<form wire:submit.prevent="createVersion">
<div class="space-y-6">
<div>
<flux:heading size="lg">{{ __('Neue Version erstellen') }}</flux:heading>
</div>
<flux:input wire:model="newName" label="Name" placeholder="z.B. Herbst 2025 Video" />
@error('newName') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
<flux:select wire:model="newType" label="Typ" placeholder="Typ auswählen...">
@foreach($types as $type)
<option value="{{ $type->value }}">{{ $type->label() }}</option>
@endforeach
</flux:select>
@error('newType') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
<div class="flex justify-end gap-3 pt-4">
<flux:button type="button" wire:click="$set('showCreateModal', false)" variant="ghost">
{{ __('Abbrechen') }}
</flux:button>
<flux:button type="submit" variant="primary">
{{ __('Erstellen & Bearbeiten') }}
</flux:button>
</div>
</div>
</form>
</flux:modal>
</div>

View file

@ -0,0 +1,441 @@
<?php
use Flux\Flux;
use FluxCms\Core\Models\CmsMedia;
use FluxCms\Core\Services\MediaConversionService;
use Illuminate\Support\Facades\Storage;
use function Livewire\Volt\{layout, title, state, computed, on};
layout('components.layouts.app');
title('Medienbibliothek');
state([
'search' => '',
'filterType' => 'all',
'filterCollection' => '',
'viewMode' => 'grid',
'editingId' => null,
'editLocale' => 'de',
'altText' => '',
'mediaTitle' => '',
'collection' => '',
'showDetail' => false,
'selectedProfiles' => [],
]);
$media = computed(
fn () => CmsMedia::query()
->when(
$this->filterType !== 'all',
fn ($q) => match ($this->filterType) {
'image' => $q->images(),
'pdf' => $q->pdfs(),
'document' => $q->documents(),
default => $q,
},
)
->when($this->filterCollection, fn ($q) => $q->inCollection($this->filterCollection))
->when($this->search, fn ($q) => $q->where('filename', 'like', "%{$this->search}%"))
->orderByDesc('created_at')
->paginate(48),
);
$collections = computed(fn () => CmsMedia::query()->whereNotNull('collection')->where('collection', '!=', '')->distinct()->pluck('collection')->sort()->values()->toArray());
$profiles = computed(fn () => config('flux-cms.media.profiles', []));
$stats = computed(
fn () => [
'total' => CmsMedia::count(),
'images' => CmsMedia::images()->count(),
'pdfs' => CmsMedia::pdfs()->count(),
],
);
on([
'media-library-uploaded' => function ($mediaId) {
$media = CmsMedia::find($mediaId);
if ($media) {
Flux::toast(variant: 'success', heading: 'Hochgeladen', text: $media->filename . ' wurde erfolgreich hochgeladen.');
}
},
]);
$startEdit = function (int $id) {
$media = CmsMedia::find($id);
if (! $media) {
return;
}
$this->editingId = $id;
$this->altText = $media->getTranslation('alt_text', $this->editLocale) ?? '';
$this->mediaTitle = $media->getTranslation('title', $this->editLocale) ?? '';
$this->collection = $media->collection ?? '';
$this->showDetail = true;
};
$switchLocale = function (string $locale) {
$this->editLocale = $locale;
if ($this->editingId) {
$media = CmsMedia::find($this->editingId);
if ($media) {
$this->altText = $media->getTranslation('alt_text', $locale) ?? '';
$this->mediaTitle = $media->getTranslation('title', $locale) ?? '';
}
}
};
$saveEdit = function () {
$media = CmsMedia::find($this->editingId);
if (! $media) {
return;
}
$media->setTranslation('alt_text', $this->editLocale, $this->altText);
$media->setTranslation('title', $this->editLocale, $this->mediaTitle);
$media->collection = $this->collection;
$media->save();
Flux::toast(variant: 'success', heading: 'Gespeichert', text: 'Medien-Details wurden aktualisiert.');
};
$generateConversion = function (string $profile) {
$media = CmsMedia::find($this->editingId);
if (! $media || ! $media->isImage()) {
return;
}
$service = app(MediaConversionService::class);
$result = $service->convert($media, $profile);
if ($result) {
$media->refresh();
Flux::toast(variant: 'success', heading: 'Conversion erstellt', text: "Profil \"{$profile}\" wurde generiert.");
} else {
Flux::toast(variant: 'danger', heading: 'Fehler', text: "Conversion \"{$profile}\" konnte nicht erstellt werden.");
}
};
$generateAllConversions = function () {
$media = CmsMedia::find($this->editingId);
if (! $media || ! $media->isImage()) {
return;
}
$service = app(MediaConversionService::class);
$results = $service->generateAllConversions($media);
$count = count(array_filter($results));
$media->refresh();
Flux::toast(variant: 'success', heading: 'Alle Conversions erstellt', text: "{$count} Profile wurden generiert.");
};
$deleteMedia = function (int $id) {
$media = CmsMedia::find($id);
if (! $media) {
return;
}
$filename = $media->filename;
$service = app(MediaConversionService::class);
$service->deleteAll($media);
$media->delete();
$this->editingId = null;
$this->showDetail = false;
Flux::toast(variant: 'success', heading: 'Gelöscht', text: "{$filename} wurde entfernt.");
};
$closeDetail = function () {
$this->showDetail = false;
$this->editingId = null;
};
?>
<div>
<div class="mb-6 flex items-center justify-between">
<flux:heading size="xl">Medienbibliothek</flux:heading>
<div class="flex items-center gap-3">
<flux:badge color="blue">{{ $this->stats['images'] }} Bilder</flux:badge>
<flux:badge color="amber">{{ $this->stats['pdfs'] }} PDFs</flux:badge>
</div>
</div>
<flux:card class="mb-6">
<div class="flex items-center gap-4">
<div class="flex-1">
<livewire:admin.cms.media-library-uploader key="media-lib-uploader" />
</div>
</div>
</flux:card>
<div class="mb-4 flex flex-wrap items-center gap-3">
<flux:input wire:model.live.debounce.300ms="search" placeholder="Dateiname suchen..." icon="magnifying-glass"
size="sm" class="w-56" />
<flux:select wire:model.live="filterType" size="sm" class="w-36">
<flux:select.option value="all">Alle Typen</flux:select.option>
<flux:select.option value="image">Bilder</flux:select.option>
<flux:select.option value="pdf">PDFs</flux:select.option>
<flux:select.option value="document">Dokumente</flux:select.option>
</flux:select>
@if (! empty($this->collections))
<flux:select wire:model.live="filterCollection" size="sm" class="w-40">
<flux:select.option value="">Alle Ordner</flux:select.option>
@foreach ($this->collections as $col)
<flux:select.option value="{{ $col }}">{{ $col }}</flux:select.option>
@endforeach
</flux:select>
@endif
<div class="ml-auto flex items-center gap-1 rounded-lg border border-zinc-200 p-0.5 dark:border-zinc-700">
<button wire:click="$set('viewMode', 'grid')"
class="rounded-md p-1.5 transition {{ $viewMode === 'grid' ? 'bg-zinc-200 text-zinc-900 dark:bg-zinc-600 dark:text-white' : 'text-zinc-400 hover:text-zinc-600' }}">
<x-heroicon-s-squares-2x2 class="h-4 w-4" />
</button>
<button wire:click="$set('viewMode', 'list')"
class="rounded-md p-1.5 transition {{ $viewMode === 'list' ? 'bg-zinc-200 text-zinc-900 dark:bg-zinc-600 dark:text-white' : 'text-zinc-400 hover:text-zinc-600' }}">
<x-heroicon-s-list-bullet class="h-4 w-4" />
</button>
</div>
</div>
<div class="grid grid-cols-1 gap-6 {{ $showDetail ? 'lg:grid-cols-3' : '' }}">
<div class="{{ $showDetail ? 'lg:col-span-2' : '' }}">
@if ($viewMode === 'grid')
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 {{ $showDetail ? 'lg:grid-cols-3' : 'lg:grid-cols-6' }}">
@forelse ($this->media as $item)
<div wire:key="media-g-{{ $item->id }}"
class="group relative cursor-pointer overflow-hidden rounded-lg border transition-all
{{ $editingId === $item->id ? 'border-blue-500 ring-2 ring-blue-200 dark:ring-blue-800' : 'border-zinc-200 hover:border-zinc-400 dark:border-zinc-700' }}"
wire:click="startEdit({{ $item->id }})">
<div class="aspect-square bg-zinc-100 dark:bg-zinc-800">
@if ($item->isImage())
<img src="{{ $item->hasConversion('thumb') ? $item->getConversionUrl('thumb') : $item->getUrl() }}"
alt="{{ $item->getTranslation('alt_text', $editLocale) ?? $item->filename }}"
class="h-full w-full object-cover" loading="lazy" />
@elseif ($item->isPdf())
<div class="relative h-full w-full">
<iframe src="{{ $item->getUrl() }}#toolbar=0&navpanes=0&scrollbar=0&view=FitH"
class="pointer-events-none h-full w-full scale-100 bg-white"
loading="lazy"></iframe>
<div class="absolute inset-0"></div>
</div>
@else
<div class="flex h-full w-full flex-col items-center justify-center gap-2 text-zinc-400">
<x-heroicon-o-document class="h-10 w-10" />
<span class="text-xs">{{ strtoupper(pathinfo($item->filename, PATHINFO_EXTENSION)) }}</span>
</div>
@endif
</div>
<div class="flex items-center gap-1.5 p-2">
@if ($item->isImage())
<x-heroicon-s-photo class="h-3.5 w-3.5 shrink-0 text-blue-500" />
@elseif ($item->isPdf())
<x-heroicon-s-document-text class="h-3.5 w-3.5 shrink-0 text-red-500" />
@else
<x-heroicon-s-document class="h-3.5 w-3.5 shrink-0 text-zinc-400" />
@endif
<div class="min-w-0">
<p class="truncate text-xs font-medium text-zinc-700 dark:text-zinc-300">{{ $item->filename }}</p>
<p class="text-xs text-zinc-400">{{ $item->getHumanFileSize() }}</p>
</div>
</div>
@if ($item->collection)
<div class="absolute right-1 top-1">
<flux:badge size="sm" color="blue" class="text-[10px]!">{{ $item->collection }}</flux:badge>
</div>
@endif
</div>
@empty
<div class="col-span-full py-12 text-center">
<x-heroicon-o-photo class="mx-auto mb-3 h-12 w-12 text-zinc-300" />
<flux:text>Noch keine Medien hochgeladen.</flux:text>
</div>
@endforelse
</div>
@else
<div class="overflow-hidden rounded-lg border border-zinc-200 dark:border-zinc-700">
<table class="w-full text-sm">
<thead class="border-b border-zinc-200 bg-zinc-50 text-left text-xs text-zinc-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-400">
<tr>
<th class="w-12 px-3 py-2"></th>
<th class="px-3 py-2">Dateiname</th>
<th class="hidden px-3 py-2 sm:table-cell">Typ</th>
<th class="hidden px-3 py-2 md:table-cell">Größe</th>
<th class="hidden px-3 py-2 md:table-cell">Abmessungen</th>
<th class="hidden px-3 py-2 lg:table-cell">Sammlung</th>
<th class="px-3 py-2 text-right">Datum</th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
@forelse ($this->media as $item)
<tr wire:key="media-l-{{ $item->id }}"
class="cursor-pointer transition {{ $editingId === $item->id ? 'bg-blue-50 dark:bg-blue-900/20' : 'hover:bg-zinc-50 dark:hover:bg-zinc-800/50' }}"
wire:click="startEdit({{ $item->id }})">
<td class="px-3 py-1.5">
<div class="h-8 w-8 overflow-hidden rounded border border-zinc-200 bg-zinc-100 dark:border-zinc-700 dark:bg-zinc-800">
@if ($item->isImage())
<img src="{{ $item->hasConversion('thumb') ? $item->getConversionUrl('thumb') : $item->getUrl() }}"
class="h-full w-full object-cover" loading="lazy" />
@elseif ($item->isPdf())
<div class="relative h-full w-full">
<iframe src="{{ $item->getUrl() }}#toolbar=0&navpanes=0&scrollbar=0&view=FitH"
class="pointer-events-none h-full w-full bg-white"
loading="lazy"></iframe>
</div>
@else
<div class="flex h-full w-full items-center justify-center text-zinc-400">
<x-heroicon-s-document class="h-4 w-4" />
</div>
@endif
</div>
</td>
<td class="px-3 py-1.5">
<span class="font-medium text-zinc-700 dark:text-zinc-300">{{ $item->filename }}</span>
</td>
<td class="hidden px-3 py-1.5 text-zinc-500 sm:table-cell">
<flux:badge size="sm" :color="$item->isImage() ? 'blue' : ($item->isPdf() ? 'amber' : 'zinc')">
{{ $item->type }}
</flux:badge>
</td>
<td class="hidden px-3 py-1.5 text-zinc-500 md:table-cell">{{ $item->getHumanFileSize() }}</td>
<td class="hidden px-3 py-1.5 text-zinc-500 md:table-cell">{{ $item->getDimensionsLabel() ?: '—' }}</td>
<td class="hidden px-3 py-1.5 lg:table-cell">
@if ($item->collection)
<flux:badge size="sm" color="blue">{{ $item->collection }}</flux:badge>
@else
<span class="text-zinc-300"></span>
@endif
</td>
<td class="px-3 py-1.5 text-right text-zinc-400">{{ $item->created_at->format('d.m.Y') }}</td>
</tr>
@empty
<tr>
<td colspan="7" class="py-12 text-center">
<x-heroicon-o-photo class="mx-auto mb-3 h-12 w-12 text-zinc-300" />
<flux:text>Noch keine Medien hochgeladen.</flux:text>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@endif
@if ($this->media->hasPages())
<div class="mt-4">
{{ $this->media->links() }}
</div>
@endif
</div>
@if ($showDetail && $editingId)
@php $editMedia = \FluxCms\Core\Models\CmsMedia::find($editingId); @endphp
@if ($editMedia)
<div class="lg:col-span-1">
<flux:card>
<div class="mb-4 flex items-center justify-between">
<flux:heading size="sm">Details</flux:heading>
<flux:button size="xs" variant="ghost" icon="x-mark" wire:click="closeDetail" />
</div>
<div class="mb-4 overflow-hidden rounded-lg border border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800">
@if ($editMedia->isImage())
<img src="{{ $editMedia->getUrl() }}" alt="{{ $editMedia->filename }}"
class="w-full object-contain" style="max-height: 300px;" />
@elseif ($editMedia->isPdf())
<iframe src="{{ $editMedia->getUrl() }}#toolbar=0&navpanes=0"
class="h-64 w-full bg-white" loading="lazy"></iframe>
@endif
</div>
<div class="mb-4 space-y-1 text-xs text-zinc-500 dark:text-zinc-400">
<p><strong>Datei:</strong> {{ $editMedia->filename }}</p>
<p><strong>Typ:</strong> {{ $editMedia->mime_type }}</p>
<p><strong>Größe:</strong> {{ $editMedia->getHumanFileSize() }}</p>
@if ($editMedia->getDimensionsLabel())
<p><strong>Abmessungen:</strong> {{ $editMedia->getDimensionsLabel() }} px</p>
@endif
<p><strong>Hochgeladen:</strong> {{ $editMedia->created_at->format('d.m.Y H:i') }}</p>
<p class="break-all"><strong>URL:</strong>
<a href="{{ $editMedia->getUrl() }}" target="_blank" class="text-blue-500 hover:underline">
{{ $editMedia->getUrl() }}
</a>
</p>
</div>
<div class="mb-3 flex gap-1">
@foreach (config('flux-cms.locales', ['de' => 'DE', 'en' => 'EN']) as $code => $label)
<flux:button size="xs" :variant="$editLocale === $code ? 'primary' : 'ghost'"
wire:click="switchLocale('{{ $code }}')">
{{ strtoupper($code) }}
</flux:button>
@endforeach
</div>
<div class="space-y-3">
<flux:input wire:model="mediaTitle" label="Titel" size="sm" placeholder="Anzeigename..." />
<flux:input wire:model="altText" label="Alt-Text" size="sm" placeholder="Bildbeschreibung für SEO..." />
<flux:input wire:model="collection" label="Ordner / Sammlung" size="sm" placeholder="z.B. hero, team, news..." />
<flux:button size="sm" variant="primary" wire:click="saveEdit" class="w-full">
Speichern
</flux:button>
</div>
@if ($editMedia->isImage() && $editMedia->mime_type !== 'image/svg+xml')
<div class="mt-5 border-t border-zinc-200 pt-4 dark:border-zinc-700">
<div class="mb-3 flex items-center justify-between">
<flux:heading size="sm">Bildgrößen</flux:heading>
<flux:button size="xs" variant="ghost" wire:click="generateAllConversions"
wire:loading.attr="disabled">
Alle generieren
</flux:button>
</div>
<div class="space-y-2">
@foreach ($this->profiles as $profileName => $profileConfig)
@php
$hasConversion = $editMedia->hasConversion($profileName);
$conversionUrl = $hasConversion ? $editMedia->getConversionUrl($profileName) : null;
@endphp
<div class="flex items-center justify-between rounded-lg border border-zinc-200 px-3 py-2 dark:border-zinc-700">
<div>
<span class="text-sm font-medium">{{ $profileName }}</span>
<span class="text-xs text-zinc-400">
{{ $profileConfig['width'] }}×{{ $profileConfig['height'] }}
{{ strtoupper($profileConfig['format'] ?? 'webp') }}
</span>
</div>
<div class="flex items-center gap-2">
@if ($hasConversion)
<flux:badge size="sm" color="green">OK</flux:badge>
@else
<flux:badge size="sm" color="zinc"></flux:badge>
@endif
<flux:button size="xs" variant="ghost" icon="arrow-path"
wire:click="generateConversion('{{ $profileName }}')"
wire:loading.attr="disabled" />
</div>
</div>
@endforeach
</div>
</div>
@endif
<div class="mt-5 border-t border-zinc-200 pt-4 dark:border-zinc-700">
<flux:button size="sm" variant="danger" class="w-full" icon="trash"
wire:click="deleteMedia({{ $editMedia->id }})"
wire:confirm="'{{ $editMedia->filename }}' wirklich löschen? Alle Conversions werden ebenfalls entfernt.">
Datei löschen
</flux:button>
</div>
</flux:card>
</div>
@endif
@endif
</div>
</div>

View file

@ -0,0 +1,30 @@
<div>
<flux:file-upload wire:model="uploads" multiple
accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml,.pdf,.doc,.docx,.jpg,.jpeg,.png">
<flux:file-upload.dropzone
heading="Dateien hochladen"
text="Bilder (JPG, PNG, WebP, SVG) und Dokumente (PDF, DOC) — max. 10 MB pro Datei"
with-progress />
</flux:file-upload>
@if (isset($uploads) && count($uploads) > 0)
<div class="mt-3 flex flex-wrap items-center gap-3">
@foreach ($uploads as $index => $upload)
<flux:file-item
:heading="$upload->getClientOriginalName()"
:image="(str_starts_with($upload->getMimeType() ?? '', 'image/') && $upload->isPreviewable())
? $upload->temporaryUrl()
: null"
:size="$upload->getSize()">
<x-slot name="actions">
<flux:file-item.remove
wire:click="removeUpload({{ $index }})"
aria-label="Entfernen: {{ $upload->getClientOriginalName() }}" />
</x-slot>
</flux:file-item>
@endforeach
</div>
@endif
<flux:error name="uploads" />
</div>

View file

@ -0,0 +1,124 @@
<div>
<div class="flex items-end gap-3">
<div class="flex-1">
@if ($selectedMedia)
<div class="flex items-center gap-3 rounded-lg border border-zinc-200 p-2 dark:border-zinc-700">
@if ($selectedMedia->isImage())
<img src="{{ $selectedMedia->hasConversion('thumb') ? $selectedMedia->getConversionUrl('thumb') : $selectedMedia->getUrl() }}"
alt="{{ $selectedMedia->filename }}"
class="h-16 w-16 rounded-md object-cover" />
@elseif ($selectedMedia->isPdf())
<div class="flex h-16 w-16 items-center justify-center rounded-md bg-red-50 dark:bg-red-900/20">
<x-heroicon-o-document-text class="h-8 w-8 text-red-500" />
</div>
@endif
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-zinc-700 dark:text-zinc-300">
{{ $selectedMedia->filename }}
</p>
<p class="text-xs text-zinc-400">
{{ $selectedMedia->getHumanFileSize() }}
@if ($selectedMedia->getDimensionsLabel())
{{ $selectedMedia->getDimensionsLabel() }}
@endif
</p>
@if ($selectedMedia->isImage() && $selectedMedia->hasConversion($profile))
@php
$pConfig = config("flux-cms.media.profiles.{$profile}", []);
@endphp
<flux:badge size="sm" color="green" class="mt-1">
{{ $profile }}: {{ $pConfig['width'] ?? '?' }}×{{ $pConfig['height'] ?? '?' }}
</flux:badge>
@endif
</div>
<flux:button size="xs" variant="ghost" icon="x-mark" wire:click="clearSelection" />
</div>
@else
<div class="rounded-lg border-2 border-dashed border-zinc-300 px-4 py-3 text-center text-sm text-zinc-400 dark:border-zinc-600">
Kein Medium ausgewählt
</div>
@endif
</div>
<flux:button size="sm" variant="ghost" icon="photo" wire:click="openPicker">
{{ $label }}
</flux:button>
</div>
<flux:modal wire:model="showModal" class="w-full max-w-4xl space-y-4 overflow-y-auto max-h-[85vh]">
<flux:heading size="lg">{{ $label }}</flux:heading>
<div class="flex flex-col gap-3">
<flux:input wire:model.live.debounce.300ms="search" placeholder="Suchen..." icon="magnifying-glass"
size="sm" />
<flux:file-upload wire:model="quickUploads" multiple
accept="{{ $type === 'image' ? 'image/jpeg,image/png,image/gif,image/webp,.jpg,.jpeg,.png' : ($type === 'pdf' ? '.pdf,application/pdf' : '*') }}">
<flux:file-upload.dropzone
heading="Neue Datei hochladen"
text="Direkt hier hochladen und zuweisen"
with-progress />
</flux:file-upload>
@if (isset($quickUploads) && count($quickUploads) > 0)
<div class="flex flex-wrap items-center gap-2">
@foreach ($quickUploads as $index => $upload)
<flux:file-item
:heading="$upload->getClientOriginalName()"
:image="(str_starts_with($upload->getMimeType() ?? '', 'image/') && $upload->isPreviewable())
? $upload->temporaryUrl()
: null"
:size="$upload->getSize()">
<x-slot name="actions">
<flux:file-item.remove wire:click="removeQuickUpload({{ $index }})" />
</x-slot>
</flux:file-item>
@endforeach
</div>
@endif
<flux:error name="quickUploads" />
</div>
@php $profileConfig = config("flux-cms.media.profiles.{$profile}", []); @endphp
@if (!empty($profileConfig))
<flux:text class="text-xs">
Profil <strong>{{ $profile }}</strong>: {{ $profileConfig['width'] }}×{{ $profileConfig['height'] }} px,
{{ strtoupper($profileConfig['format'] ?? 'webp') }},
Qualität {{ $profileConfig['quality'] ?? 85 }}%
</flux:text>
@endif
<div class="grid grid-cols-3 gap-2 sm:grid-cols-4 md:grid-cols-6">
@forelse ($mediaItems as $item)
<div wire:key="pick-{{ $item->id }}"
class="group cursor-pointer overflow-hidden rounded-lg border transition-all
{{ $value === $item->id ? 'border-blue-500 ring-2 ring-blue-200' : 'border-zinc-200 hover:border-blue-300 dark:border-zinc-700' }}"
wire:click="selectMedia({{ $item->id }})">
<div class="aspect-square bg-zinc-100 dark:bg-zinc-800">
@if ($item->isImage())
<img src="{{ $item->hasConversion('thumb') ? $item->getConversionUrl('thumb') : $item->getUrl() }}"
alt="{{ $item->filename }}" class="h-full w-full object-cover" loading="lazy" />
@elseif ($item->isPdf())
<div class="flex h-full w-full items-center justify-center text-red-500">
<x-heroicon-o-document-text class="h-8 w-8" />
</div>
@endif
</div>
<div class="p-1.5">
<p class="truncate text-[11px] text-zinc-600 dark:text-zinc-400">{{ $item->filename }}</p>
</div>
</div>
@empty
<div class="col-span-full py-8 text-center">
<flux:text>Keine Medien gefunden.</flux:text>
</div>
@endforelse
</div>
@if ($mediaItems->hasPages())
<div class="mt-2">
{{ $mediaItems->links() }}
</div>
@endif
</flux:modal>
</div>

View file

@ -0,0 +1,67 @@
{{-- B2in: Media-Playlist --}}
@php
$mediaItems = $items->get('media', collect());
@endphp
<flux:card>
<div class="flex items-center justify-between mb-6">
<div>
<flux:heading size="lg">{{ __('Media-Playlist') }}</flux:heading>
<flux:subheading>{{ __('Bilder und Videos werden rotierend angezeigt') }}</flux:subheading>
</div>
<flux:button wire:click="openItemModal(null, 'media')" icon="plus">
{{ __('Medium hinzufügen') }}
</flux:button>
</div>
@if($mediaItems->isEmpty())
<div class="text-center py-12 text-zinc-500 dark:text-zinc-400">
<flux:icon.photo class="w-16 h-16 mx-auto mb-4 opacity-50" />
<p>{{ __('Noch keine Medien vorhanden.') }}</p>
</div>
@else
<div class="space-y-3">
@foreach($mediaItems as $index => $item)
<div wire:key="item-{{ $item->id }}"
class="flex items-center gap-4 p-4 bg-zinc-50 dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 transition">
<div class="flex flex-col gap-1">
@if($index > 0)
<flux:button wire:click="moveItem({{ $item->id }}, 'up')" size="xs" variant="ghost" icon="chevron-up" class="text-zinc-400 hover:text-zinc-600"></flux:button>
@endif
@if($index < count($mediaItems) - 1)
<flux:button wire:click="moveItem({{ $item->id }}, 'down')" size="xs" variant="ghost" icon="chevron-down" class="text-zinc-400 hover:text-zinc-600"></flux:button>
@endif
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3 mb-1">
<flux:badge :color="$item->is_active ? 'green' : 'zinc'" size="sm">
{{ $item->is_active ? __('Aktiv') : __('Inaktiv') }}
</flux:badge>
<flux:badge color="{{ ($item->content['media_type'] ?? 'image') === 'video' ? 'purple' : 'sky' }}" size="sm">
{{ ($item->content['media_type'] ?? 'image') === 'video' ? 'Video' : 'Bild' }}
</flux:badge>
<flux:badge color="zinc" size="sm">
{{ ucfirst($item->content['category'] ?? '') }}
</flux:badge>
<span class="font-semibold text-sm truncate">{{ $item->content['headline'] ?? '' }}</span>
</div>
<div class="text-xs text-zinc-600 dark:text-zinc-400 space-x-4">
<span>{{ $item->content['subline'] ?? '' }}</span>
@if(($item->content['media_type'] ?? 'image') === 'image')
<span>{{ $item->content['duration_seconds'] ?? 10 }}s</span>
@endif
</div>
</div>
<div class="flex items-center gap-2">
<flux:button wire:click="toggleItemStatus({{ $item->id }})" size="sm" variant="ghost" :icon="$item->is_active ? 'eye-slash' : 'eye'"></flux:button>
<flux:button wire:click="openItemModal({{ $item->id }})" size="sm" variant="ghost" icon="pencil"></flux:button>
<flux:button wire:click="deleteItem({{ $item->id }})" wire:confirm="Möchten Sie diesen Eintrag wirklich löschen?" size="sm" variant="ghost" icon="trash" class="text-red-600 hover:text-red-700"></flux:button>
</div>
</div>
@endforeach
</div>
@endif
</flux:card>

View file

@ -0,0 +1,73 @@
{{-- Offers: Slides --}}
@php
$slides = $items->get('slide', collect());
@endphp
<flux:card>
<div class="flex items-center justify-between mb-6">
<div>
<flux:heading size="lg">{{ __('Slides') }}</flux:heading>
<flux:subheading>{{ __('Angebots-Slides werden in der angegebenen Reihenfolge angezeigt') }}</flux:subheading>
</div>
<flux:button wire:click="openItemModal(null, 'slide')" icon="plus">
{{ __('Slide hinzufügen') }}
</flux:button>
</div>
@if($slides->isEmpty())
<div class="text-center py-12 text-zinc-500 dark:text-zinc-400">
<flux:icon.presentation-chart-bar class="w-16 h-16 mx-auto mb-4 opacity-50" />
<p>{{ __('Noch keine Slides vorhanden.') }}</p>
</div>
@else
<div class="space-y-3">
@foreach($slides as $index => $item)
<div wire:key="item-{{ $item->id }}"
class="flex items-center gap-4 p-4 bg-zinc-50 dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 transition">
<div class="flex flex-col gap-1">
@if($index > 0)
<flux:button wire:click="moveItem({{ $item->id }}, 'up')" size="xs" variant="ghost" icon="chevron-up" class="text-zinc-400 hover:text-zinc-600"></flux:button>
@endif
@if($index < count($slides) - 1)
<flux:button wire:click="moveItem({{ $item->id }}, 'down')" size="xs" variant="ghost" icon="chevron-down" class="text-zinc-400 hover:text-zinc-600"></flux:button>
@endif
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3 mb-1">
<flux:badge :color="$item->is_active ? 'green' : 'zinc'" size="sm">
{{ $item->is_active ? __('Aktiv') : __('Inaktiv') }}
</flux:badge>
<flux:badge color="amber" size="sm">
{{ match($item->content['type'] ?? '') {
'intro' => 'Intro',
'product-hero' => 'Produkt-Hero',
'product-details' => 'Produkt-Details',
'product-impulse' => 'Produkt-Impuls',
default => $item->content['type'] ?? '',
} }}
</flux:badge>
<span class="font-semibold text-sm">{{ $item->content['title'] ?? '' }}</span>
</div>
<div class="text-xs text-zinc-600 dark:text-zinc-400 space-x-4">
@if(!empty($item->content['price']))
<span class="font-medium">{{ $item->content['price'] }}</span>
@endif
<span>{{ number_format(($item->content['duration'] ?? 8000) / 1000, 1) }}s</span>
@if(!empty($item->content['badge_text']))
<span>{{ $item->content['badge_text'] }}</span>
@endif
</div>
</div>
<div class="flex items-center gap-2">
<flux:button wire:click="toggleItemStatus({{ $item->id }})" size="sm" variant="ghost" :icon="$item->is_active ? 'eye-slash' : 'eye'"></flux:button>
<flux:button wire:click="openItemModal({{ $item->id }})" size="sm" variant="ghost" icon="pencil"></flux:button>
<flux:button wire:click="deleteItem({{ $item->id }})" wire:confirm="Möchten Sie diesen Eintrag wirklich löschen?" size="sm" variant="ghost" icon="trash" class="text-red-600 hover:text-red-700"></flux:button>
</div>
</div>
@endforeach
</div>
@endif
</flux:card>

View file

@ -0,0 +1,119 @@
{{-- Video-Display: Video-Playlist + Footer-Inhalte --}}
@php
$videos = $items->get('video', collect());
$footers = $items->get('footer', collect());
@endphp
{{-- Video-Playlist --}}
<flux:card class="mb-8">
<div class="flex items-center justify-between mb-6">
<div>
<flux:heading size="lg">{{ __('Video-Playlist') }}</flux:heading>
<flux:subheading>{{ __('Videos werden in der angegebenen Reihenfolge abgespielt') }}</flux:subheading>
</div>
<flux:button wire:click="openItemModal(null, 'video')" icon="plus">
{{ __('Video hinzufügen') }}
</flux:button>
</div>
@if($videos->isEmpty())
<div class="text-center py-12 text-zinc-500 dark:text-zinc-400">
<flux:icon.film class="w-16 h-16 mx-auto mb-4 opacity-50" />
<p>{{ __('Noch keine Videos vorhanden.') }}</p>
</div>
@else
<div class="space-y-3">
@foreach($videos as $index => $item)
<div wire:key="item-{{ $item->id }}"
class="flex items-center gap-4 p-4 bg-zinc-50 dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 transition">
<div class="flex flex-col gap-1">
@if($index > 0)
<flux:button wire:click="moveItem({{ $item->id }}, 'up')" size="xs" variant="ghost" icon="chevron-up" class="text-zinc-400 hover:text-zinc-600"></flux:button>
@endif
@if($index < count($videos) - 1)
<flux:button wire:click="moveItem({{ $item->id }}, 'down')" size="xs" variant="ghost" icon="chevron-down" class="text-zinc-400 hover:text-zinc-600"></flux:button>
@endif
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3 mb-1">
<flux:badge :color="$item->is_active ? 'green' : 'zinc'" size="sm">
{{ $item->is_active ? __('Aktiv') : __('Inaktiv') }}
</flux:badge>
<span class="font-semibold text-sm">{{ $item->content['title'] ?? $item->content['filename'] ?? '' }}</span>
</div>
<div class="text-xs text-zinc-600 dark:text-zinc-400 space-x-4">
<span>{{ $item->content['filename'] ?? '' }}</span>
<span>Position: {{ $item->content['position'] ?? 25 }}%</span>
</div>
</div>
<div class="flex items-center gap-2">
<flux:button wire:click="toggleItemStatus({{ $item->id }})" size="sm" variant="ghost" :icon="$item->is_active ? 'eye-slash' : 'eye'"></flux:button>
<flux:button wire:click="openItemModal({{ $item->id }})" size="sm" variant="ghost" icon="pencil"></flux:button>
<flux:button wire:click="deleteItem({{ $item->id }})" wire:confirm="Möchten Sie diesen Eintrag wirklich löschen?" size="sm" variant="ghost" icon="trash" class="text-red-600 hover:text-red-700"></flux:button>
</div>
</div>
@endforeach
</div>
@endif
</flux:card>
{{-- Footer-Inhalte --}}
<flux:card>
<div class="flex items-center justify-between mb-6">
<div>
<flux:heading size="lg">{{ __('Footer-Inhalte') }}</flux:heading>
<flux:subheading>{{ __('Inhalte werden im Footer rotiert') }}</flux:subheading>
</div>
<flux:button wire:click="openItemModal(null, 'footer')" icon="plus">
{{ __('Inhalt hinzufügen') }}
</flux:button>
</div>
@if($footers->isEmpty())
<div class="text-center py-12 text-zinc-500 dark:text-zinc-400">
<flux:icon.document-text class="w-16 h-16 mx-auto mb-4 opacity-50" />
<p>{{ __('Noch keine Footer-Inhalte vorhanden.') }}</p>
</div>
@else
<div class="space-y-3">
@foreach($footers as $index => $item)
<div wire:key="item-{{ $item->id }}"
class="flex items-center gap-4 p-4 bg-zinc-50 dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 transition">
<div class="flex flex-col gap-1">
@if($index > 0)
<flux:button wire:click="moveItem({{ $item->id }}, 'up')" size="xs" variant="ghost" icon="chevron-up" class="text-zinc-400 hover:text-zinc-600"></flux:button>
@endif
@if($index < count($footers) - 1)
<flux:button wire:click="moveItem({{ $item->id }}, 'down')" size="xs" variant="ghost" icon="chevron-down" class="text-zinc-400 hover:text-zinc-600"></flux:button>
@endif
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3 mb-1">
<flux:badge :color="$item->is_active ? 'green' : 'zinc'" size="sm">
{{ $item->is_active ? __('Aktiv') : __('Inaktiv') }}
</flux:badge>
<span class="font-semibold text-sm">{{ $item->content['headline'] ?? '' }}</span>
</div>
<div class="text-xs text-zinc-600 dark:text-zinc-400">
{{ $item->content['subline'] ?? '' }}
@if(!empty($item->content['url']))
<span class="ml-2">{{ Str::limit($item->content['url'], 40) }}</span>
@endif
</div>
</div>
<div class="flex items-center gap-2">
<flux:button wire:click="toggleItemStatus({{ $item->id }})" size="sm" variant="ghost" :icon="$item->is_active ? 'eye-slash' : 'eye'"></flux:button>
<flux:button wire:click="openItemModal({{ $item->id }})" size="sm" variant="ghost" icon="pencil"></flux:button>
<flux:button wire:click="deleteItem({{ $item->id }})" wire:confirm="Möchten Sie diesen Eintrag wirklich löschen?" size="sm" variant="ghost" icon="trash" class="text-red-600 hover:text-red-700"></flux:button>
</div>
</div>
@endforeach
</div>
@endif
</flux:card>

View file

@ -0,0 +1,720 @@
<?php
use App\Models\CmsProject;
use Flux\Flux;
use Illuminate\Support\Str;
use function Livewire\Volt\{layout, title, state, computed, on};
layout('components.layouts.app');
title('Projekte verwalten');
state([
'search' => '',
'showForm' => false,
'editingId' => null,
'editLocale' => 'de',
'activeTab' => 'basic',
'slug' => '',
'projectTitle' => '',
'location' => '',
'status' => '',
'launch_date' => '',
'price_from_aed' => '',
'currency' => 'AED',
'image' => '',
'is_published' => true,
'order' => 0,
'highlights' => [''],
'quick_facts' => [['icon' => 'home-modern', 'label' => '', 'value' => '']],
'investTitle' => '',
'investText' => '',
'investViews' => [''],
'galleryItems' => [''],
'locTitle' => '',
'locMapUrl' => '',
'locPoints' => [''],
'contactTitle' => '',
'contactSubtitle' => '',
'contactOptions' => [['key' => '', 'value' => '']],
'trustTitle' => '',
'trustIntro' => '',
'trustColumns' => [
['icon' => 'heroicon-o-lock-closed', 'title' => '', 'text' => ''],
['icon' => 'heroicon-o-building-library', 'title' => '', 'text' => ''],
['icon' => 'heroicon-o-chart-bar', 'title' => '', 'text' => ''],
],
'trustCtaUrl' => '',
'trustCtaLabel' => '',
'furnitureTitle' => '',
'furnitureText' => '',
'furnitureButtonText' => '',
'furnitureButtonLink' => '',
]);
on(['media-selected' => function ($mediaId, $url, $field) {
$media = $mediaId ? \FluxCms\Core\Models\CmsMedia::find($mediaId) : null;
if (! $media) {
return;
}
if ($field === 'project_image') {
$this->image = $media->filename;
}
if (str_starts_with($field, 'gallery_')) {
$idx = (int) str_replace('gallery_', '', $field);
if (isset($this->galleryItems[$idx])) {
$this->galleryItems[$idx] = $media->filename;
}
}
}]);
$projects = computed(
fn () => CmsProject::query()
->when($this->search, fn ($q) => $q->where('slug', 'like', "%{$this->search}%"))
->ordered()
->get(),
);
$resetForm = function () {
$this->editingId = null;
$this->activeTab = 'basic';
$this->slug = '';
$this->projectTitle = '';
$this->location = '';
$this->status = '';
$this->launch_date = '';
$this->price_from_aed = '';
$this->currency = 'AED';
$this->image = '';
$this->is_published = true;
$this->order = 0;
$this->highlights = [''];
$this->quick_facts = [['icon' => 'home-modern', 'label' => '', 'value' => '']];
$this->investTitle = '';
$this->investText = '';
$this->investViews = [''];
$this->galleryItems = [''];
$this->locTitle = '';
$this->locMapUrl = '';
$this->locPoints = [''];
$this->contactTitle = '';
$this->contactSubtitle = '';
$this->contactOptions = [['key' => '', 'value' => '']];
$this->trustTitle = '';
$this->trustIntro = '';
$this->trustColumns = [
['icon' => 'heroicon-o-lock-closed', 'title' => '', 'text' => ''],
['icon' => 'heroicon-o-building-library', 'title' => '', 'text' => ''],
['icon' => 'heroicon-o-chart-bar', 'title' => '', 'text' => ''],
];
$this->trustCtaUrl = '';
$this->trustCtaLabel = '';
$this->furnitureTitle = '';
$this->furnitureText = '';
$this->furnitureButtonText = '';
$this->furnitureButtonLink = '';
};
$loadProjectIntoForm = function (CmsProject $project, string $locale) {
$this->slug = $project->slug;
$this->image = $project->image ?? '';
$this->status = $project->status ?? '';
$this->launch_date = $project->launch_date?->format('Y-m-d') ?? '';
$this->price_from_aed = $project->price_from_aed ?? '';
$this->currency = $project->currency ?? 'AED';
$this->is_published = $project->is_published;
$this->order = $project->order ?? 0;
$this->projectTitle = $project->getTranslation('title', $locale) ?? '';
$this->location = $project->getTranslation('location', $locale) ?? '';
$hl = $project->getTranslation('highlights', $locale);
$this->highlights = is_array($hl) && count($hl) > 0 ? $hl : [''];
$this->quick_facts = is_array($project->quick_facts) && count($project->quick_facts) > 0
? $project->quick_facts
: [['icon' => 'home-modern', 'label' => '', 'value' => '']];
$ic = $project->getTranslation('investment_case', $locale);
$this->investTitle = $ic['title'] ?? '';
$this->investText = $ic['text'] ?? '';
$this->investViews = is_array($ic['views'] ?? null) && count($ic['views']) > 0 ? $ic['views'] : [''];
$this->galleryItems = is_array($project->gallery) && count($project->gallery) > 0
? $project->gallery
: [''];
$li = $project->getTranslation('location_info', $locale);
$this->locTitle = $li['title'] ?? '';
$this->locMapUrl = $li['map_url'] ?? '';
$this->locPoints = is_array($li['points'] ?? null) && count($li['points']) > 0 ? $li['points'] : [''];
$ct = $project->getTranslation('contact', $locale);
$this->contactTitle = $ct['title'] ?? '';
$this->contactSubtitle = $ct['subtitle'] ?? '';
$opts = $ct['options'] ?? [];
$this->contactOptions = count($opts) > 0
? collect($opts)->map(fn ($v, $k) => ['key' => $k, 'value' => $v])->values()->toArray()
: [['key' => '', 'value' => '']];
$it = $project->getTranslation('investor_trust', $locale) ?? [];
$this->trustTitle = $it['title'] ?? '';
$this->trustIntro = $it['intro'] ?? '';
$trustCols = $it['columns'] ?? [];
$this->trustColumns = is_array($trustCols) && count($trustCols) > 0
? $trustCols
: [
['icon' => 'heroicon-o-lock-closed', 'title' => '', 'text' => ''],
['icon' => 'heroicon-o-building-library', 'title' => '', 'text' => ''],
['icon' => 'heroicon-o-chart-bar', 'title' => '', 'text' => ''],
];
$this->trustCtaUrl = $it['cta_url'] ?? '';
$this->trustCtaLabel = $it['cta_label'] ?? '';
$fb = $project->getTranslation('furniture_benefit', $locale) ?? [];
$this->furnitureTitle = $fb['title'] ?? '';
$this->furnitureText = $fb['text'] ?? '';
$this->furnitureButtonText = $fb['button_text'] ?? '';
$this->furnitureButtonLink = $fb['button_link'] ?? '';
};
$openCreate = function () {
$this->resetForm();
$this->showForm = true;
};
$openEdit = function (int $id) {
$project = CmsProject::find($id);
if (! $project) {
return;
}
$this->editingId = $id;
$this->activeTab = 'basic';
$this->loadProjectIntoForm($project, $this->editLocale);
$this->showForm = true;
};
$duplicateProject = function (int $id) {
$project = CmsProject::find($id);
if (! $project) {
return;
}
$this->editingId = null;
$this->activeTab = 'basic';
$this->loadProjectIntoForm($project, $this->editLocale);
$this->slug = $project->slug . '-kopie-' . Str::random(4);
$this->showForm = true;
Flux::toast(heading: 'Dupliziert', text: 'Projekt wurde als Kopie geladen. Bitte Slug anpassen und speichern.');
};
$switchLocale = function (string $locale) {
$this->editLocale = $locale;
if ($this->editingId) {
$project = CmsProject::find($this->editingId);
if ($project) {
$this->projectTitle = $project->getTranslation('title', $locale) ?? '';
$this->location = $project->getTranslation('location', $locale) ?? '';
$hl = $project->getTranslation('highlights', $locale);
$this->highlights = is_array($hl) && count($hl) > 0 ? $hl : [''];
$ic = $project->getTranslation('investment_case', $locale);
$this->investTitle = $ic['title'] ?? '';
$this->investText = $ic['text'] ?? '';
$this->investViews = is_array($ic['views'] ?? null) && count($ic['views']) > 0 ? $ic['views'] : [''];
$li = $project->getTranslation('location_info', $locale);
$this->locTitle = $li['title'] ?? '';
$this->locMapUrl = $li['map_url'] ?? '';
$this->locPoints = is_array($li['points'] ?? null) && count($li['points']) > 0 ? $li['points'] : [''];
$ct = $project->getTranslation('contact', $locale);
$this->contactTitle = $ct['title'] ?? '';
$this->contactSubtitle = $ct['subtitle'] ?? '';
$opts = $ct['options'] ?? [];
$this->contactOptions = count($opts) > 0
? collect($opts)->map(fn ($v, $k) => ['key' => $k, 'value' => $v])->values()->toArray()
: [['key' => '', 'value' => '']];
$it = $project->getTranslation('investor_trust', $locale) ?? [];
$this->trustTitle = $it['title'] ?? '';
$this->trustIntro = $it['intro'] ?? '';
$trustCols = $it['columns'] ?? [];
$this->trustColumns = is_array($trustCols) && count($trustCols) > 0
? $trustCols
: [
['icon' => 'heroicon-o-lock-closed', 'title' => '', 'text' => ''],
['icon' => 'heroicon-o-building-library', 'title' => '', 'text' => ''],
['icon' => 'heroicon-o-chart-bar', 'title' => '', 'text' => ''],
];
$this->trustCtaUrl = $it['cta_url'] ?? '';
$this->trustCtaLabel = $it['cta_label'] ?? '';
$fb = $project->getTranslation('furniture_benefit', $locale) ?? [];
$this->furnitureTitle = $fb['title'] ?? '';
$this->furnitureText = $fb['text'] ?? '';
$this->furnitureButtonText = $fb['button_text'] ?? '';
$this->furnitureButtonLink = $fb['button_link'] ?? '';
}
}
};
$addHighlight = fn () => $this->highlights[] = '';
$removeHighlight = function (int $i) { unset($this->highlights[$i]); $this->highlights = array_values($this->highlights); };
$addQuickFact = fn () => $this->quick_facts[] = ['icon' => 'home-modern', 'label' => '', 'value' => ''];
$removeQuickFact = function (int $i) { unset($this->quick_facts[$i]); $this->quick_facts = array_values($this->quick_facts); };
$addInvestView = fn () => $this->investViews[] = '';
$removeInvestView = function (int $i) { unset($this->investViews[$i]); $this->investViews = array_values($this->investViews); };
$addGalleryItem = fn () => $this->galleryItems[] = '';
$removeGalleryItem = function (int $i) { unset($this->galleryItems[$i]); $this->galleryItems = array_values($this->galleryItems); };
$addLocPoint = fn () => $this->locPoints[] = '';
$removeLocPoint = function (int $i) { unset($this->locPoints[$i]); $this->locPoints = array_values($this->locPoints); };
$addContactOption = fn () => $this->contactOptions[] = ['key' => '', 'value' => ''];
$removeContactOption = function (int $i) { unset($this->contactOptions[$i]); $this->contactOptions = array_values($this->contactOptions); };
$addTrustColumn = fn () => $this->trustColumns[] = ['icon' => 'heroicon-o-lock-closed', 'title' => '', 'text' => ''];
$removeTrustColumn = function (int $i) { unset($this->trustColumns[$i]); $this->trustColumns = array_values($this->trustColumns); };
$save = function () {
$validated = validator([
'slug' => $this->slug,
'projectTitle' => $this->projectTitle,
'location' => $this->location,
'status' => $this->status,
'launch_date' => $this->launch_date,
'price_from_aed' => $this->price_from_aed,
'currency' => $this->currency,
'image' => $this->image,
'is_published' => $this->is_published,
'order' => $this->order,
], [
'slug' => 'required|string|max:255',
'projectTitle' => 'required|string|max:500',
'location' => 'nullable|string|max:500',
'status' => 'nullable|string|max:100',
'launch_date' => 'nullable|date',
'price_from_aed' => 'nullable|integer|min:0',
'currency' => 'nullable|string|max:10',
'image' => 'nullable|string|max:500',
'is_published' => 'boolean',
'order' => 'integer|min:0',
])->validate();
$project = $this->editingId
? CmsProject::findOrFail($this->editingId)
: CmsProject::query()->make();
$project->slug = $validated['slug'];
$project->setTranslation('title', $this->editLocale, $validated['projectTitle']);
$project->setTranslation('location', $this->editLocale, $validated['location'] ?? '');
$project->status = $validated['status'] ?? null;
$project->launch_date = $validated['launch_date'] ?: null;
$project->price_from_aed = $validated['price_from_aed'] ?: null;
$project->currency = $validated['currency'] ?? 'AED';
$project->image = $validated['image'] ?? null;
$project->is_published = $validated['is_published'];
$project->order = $validated['order'];
$project->setTranslation('highlights', $this->editLocale, array_values(array_filter($this->highlights, fn ($h) => trim($h) !== '')));
$project->quick_facts = collect($this->quick_facts)
->filter(fn ($f) => ! empty($f['label']) || ! empty($f['value']))
->values()
->toArray();
$project->setTranslation('investment_case', $this->editLocale, [
'title' => $this->investTitle,
'text' => $this->investText,
'views' => array_values(array_filter($this->investViews, fn ($v) => trim($v) !== '')),
]);
$project->gallery = array_values(array_filter($this->galleryItems, fn ($g) => trim($g) !== ''));
$project->setTranslation('location_info', $this->editLocale, [
'title' => $this->locTitle,
'map_url' => $this->locMapUrl,
'points' => array_values(array_filter($this->locPoints, fn ($p) => trim($p) !== '')),
]);
$opts = [];
foreach ($this->contactOptions as $opt) {
if ($opt['value'] !== '') {
$opts[$opt['key']] = $opt['value'];
}
}
$project->setTranslation('contact', $this->editLocale, [
'title' => $this->contactTitle,
'subtitle' => $this->contactSubtitle,
'options' => $opts,
]);
$trustCols = collect($this->trustColumns)
->filter(fn ($c) => ! empty(trim($c['title'] ?? '')) || ! empty(trim($c['text'] ?? '')))
->values()
->toArray();
$project->setTranslation('investor_trust', $this->editLocale, [
'title' => $this->trustTitle,
'intro' => $this->trustIntro,
'columns' => $trustCols,
'cta_url' => $this->trustCtaUrl,
'cta_label' => $this->trustCtaLabel,
]);
$project->setTranslation('furniture_benefit', $this->editLocale, [
'title' => $this->furnitureTitle,
'text' => $this->furnitureText,
'button_text' => $this->furnitureButtonText,
'button_link' => $this->furnitureButtonLink,
]);
$project->save();
$this->showForm = false;
$this->editingId = null;
Flux::toast(variant: 'success', heading: 'Gespeichert', text: $project->slug . ' wurde erfolgreich gespeichert.');
};
$deleteProject = function (int $id) {
$project = CmsProject::find($id);
if (! $project) {
return;
}
$slug = $project->slug;
$project->delete();
Flux::toast(variant: 'success', heading: 'Gelöscht', text: "{$slug} wurde entfernt.");
};
$cancelForm = function () {
$this->showForm = false;
$this->editingId = null;
};
$setActiveTab = function (string $tab): void {
$this->activeTab = $tab;
};
?>
<div>
<div class="mb-6 flex items-center justify-between">
<flux:heading size="xl">Projekte (Immobilien)</flux:heading>
<flux:button variant="primary" icon="plus" wire:click="openCreate">Neues Projekt</flux:button>
</div>
<div class="mb-4">
<flux:input wire:model.live.debounce.300ms="search" placeholder="Projekt suchen..." icon="magnifying-glass"
size="sm" class="w-64" />
</div>
@if ($showForm)
<flux:card class="mb-6">
<div class="mb-4 flex items-center justify-between">
<flux:heading size="lg">{{ $editingId ? 'Projekt bearbeiten' : 'Neues Projekt' }}</flux:heading>
<div class="flex gap-1">
@foreach (config('flux-cms.locales', ['de' => 'DE', 'en' => 'EN']) as $code => $label)
<flux:button size="xs" :variant="$editLocale === $code ? 'primary' : 'ghost'"
wire:click="switchLocale('{{ $code }}')">{{ strtoupper($code) }}</flux:button>
@endforeach
</div>
</div>
{{-- Explizite Tab-Steuerung: zuverlässiger als nur wire:model auf ui-tabs (Livewire-Sync) --}}
<div class="mb-4 flex flex-wrap gap-1 rounded-lg bg-zinc-800/5 p-1 dark:bg-white/10">
<flux:button size="sm" type="button" :variant="$activeTab === 'basic' ? 'primary' : 'ghost'" icon="information-circle" wire:click="setActiveTab('basic')">Grunddaten</flux:button>
<flux:button size="sm" type="button" :variant="$activeTab === 'facts' ? 'primary' : 'ghost'" icon="list-bullet" wire:click="setActiveTab('facts')">Quick Facts</flux:button>
<flux:button size="sm" type="button" :variant="$activeTab === 'invest' ? 'primary' : 'ghost'" icon="chart-bar" wire:click="setActiveTab('invest')">Investment Case</flux:button>
<flux:button size="sm" type="button" :variant="$activeTab === 'gallery' ? 'primary' : 'ghost'" icon="photo" wire:click="setActiveTab('gallery')">Galerie</flux:button>
<flux:button size="sm" type="button" :variant="$activeTab === 'location' ? 'primary' : 'ghost'" icon="map-pin" wire:click="setActiveTab('location')">Location</flux:button>
<flux:button size="sm" type="button" :variant="$activeTab === 'contact' ? 'primary' : 'ghost'" icon="envelope" wire:click="setActiveTab('contact')">Kontakt</flux:button>
<flux:button size="sm" type="button" :variant="$activeTab === 'trust_synergy' ? 'primary' : 'ghost'" icon="shield-check" wire:click="setActiveTab('trust_synergy')">Trust & Möbel</flux:button>
</div>
<div class="mt-4" wire:key="project-form-panel-{{ $activeTab }}">
{{-- TAB: Grunddaten --}}
@if ($activeTab === 'basic')
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<flux:input wire:model="slug" label="Slug" placeholder="azizi-creek-views-4" />
<flux:input wire:model="projectTitle" label="Titel ({{ strtoupper($editLocale) }})" placeholder="Projektname" />
<flux:input wire:model="location" label="Standort ({{ strtoupper($editLocale) }})" placeholder="Dubai, UAE" />
<flux:input wire:model="status" label="Status" placeholder="z.B. NEW LAUNCH" />
<flux:input wire:model="launch_date" label="Launch-Datum" type="date" />
<flux:input wire:model="price_from_aed" label="Preis ab (AED)" type="number" placeholder="0" />
<flux:input wire:model="currency" label="Währung" placeholder="AED" />
<flux:input wire:model="order" label="Sortierung" type="number" />
<div class="md:col-span-2">
<label class="mb-1 block text-sm font-medium text-zinc-700 dark:text-zinc-300">Hauptbild</label>
<div class="flex items-center gap-3">
@if ($image)
<div class="h-16 w-24 shrink-0 overflow-hidden rounded-lg border border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800">
<img src="{{ media_url($image) }}" class="h-full w-full object-cover" />
</div>
@endif
<div class="flex-1">
<livewire:admin.cms.media-picker :value="null" field="project_image" type="image" profile="card" label="Projektbild wählen" :key="'proj-img-' . ($editingId ?? 'new')" />
</div>
</div>
<flux:input wire:model="image" size="sm" class="mt-2" placeholder="Oder Dateiname manuell..." />
</div>
<div class="md:col-span-2">
<flux:heading size="sm" class="mb-2">Highlights ({{ strtoupper($editLocale) }})</flux:heading>
<div class="space-y-2">
@foreach ($highlights as $i => $hl)
<div wire:key="hl-{{ $i }}" class="flex items-center gap-2">
<flux:input wire:model="highlights.{{ $i }}" placeholder="Highlight-Text" class="flex-1" size="sm" />
@if (count($highlights) > 1)
<flux:button size="xs" variant="ghost" icon="x-mark" wire:click="removeHighlight({{ $i }})" />
@endif
</div>
@endforeach
</div>
<flux:button size="xs" variant="ghost" icon="plus" wire:click="addHighlight" class="mt-2">Highlight</flux:button>
</div>
<div class="md:col-span-2">
<flux:switch wire:model="is_published" label="Veröffentlicht" />
</div>
</div>
@endif
{{-- TAB: Quick Facts --}}
@if ($activeTab === 'facts')
<div class="space-y-3">
@foreach ($quick_facts as $i => $fact)
<div wire:key="qf-{{ $i }}" class="flex items-end gap-2 rounded-lg border border-zinc-200 p-3 dark:border-zinc-700">
<flux:select wire:model="quick_facts.{{ $i }}.icon" label="Icon" class="w-44">
<flux:select.option value="home-modern">Haus</flux:select.option>
<flux:select.option value="squares-2x2">Fläche</flux:select.option>
<flux:select.option value="building-office-2">Gebäude</flux:select.option>
<flux:select.option value="user">Person</flux:select.option>
<flux:select.option value="currency-dollar">Preis</flux:select.option>
<flux:select.option value="calendar">Kalender</flux:select.option>
<flux:select.option value="map-pin">Standort</flux:select.option>
</flux:select>
<flux:input wire:model="quick_facts.{{ $i }}.label" label="Label" placeholder="Typen" class="flex-1" />
<flux:input wire:model="quick_facts.{{ $i }}.value" label="Wert" placeholder="1BR & 3BR" class="flex-1" />
@if (count($quick_facts) > 1)
<flux:button size="sm" variant="ghost" icon="trash" wire:click="removeQuickFact({{ $i }})" />
@endif
</div>
@endforeach
<flux:button size="xs" variant="ghost" icon="plus" wire:click="addQuickFact">Quick Fact</flux:button>
</div>
@endif
{{-- TAB: Investment Case --}}
@if ($activeTab === 'invest')
<div class="space-y-4">
<flux:input wire:model="investTitle" label="Überschrift ({{ strtoupper($editLocale) }})" placeholder="Starkes Investment, hohe Nachfrage." />
<flux:textarea wire:model="investText" label="Text ({{ strtoupper($editLocale) }})" rows="4" placeholder="Beschreibung des Investment Case..." />
<div>
<label class="mb-1 block text-sm font-medium text-zinc-700 dark:text-zinc-300">Verfügbare Views</label>
<div class="space-y-2">
@foreach ($investViews as $i => $view)
<div wire:key="iv-{{ $i }}" class="flex items-center gap-2">
<flux:input wire:model="investViews.{{ $i }}" placeholder="z.B. Road View" class="flex-1" size="sm" />
@if (count($investViews) > 1)
<flux:button size="xs" variant="ghost" icon="x-mark" wire:click="removeInvestView({{ $i }})" />
@endif
</div>
@endforeach
</div>
<flux:button size="xs" variant="ghost" icon="plus" wire:click="addInvestView" class="mt-2">View</flux:button>
</div>
</div>
@endif
{{-- TAB: Galerie --}}
@if ($activeTab === 'gallery')
<div class="space-y-3">
@foreach ($galleryItems as $i => $img)
<div wire:key="gal-{{ $i }}" class="flex items-center gap-3 rounded-lg border border-zinc-200 p-3 dark:border-zinc-700">
@if ($img)
<div class="h-14 w-20 shrink-0 overflow-hidden rounded-lg bg-zinc-100 dark:bg-zinc-700">
<img src="{{ media_url($img) }}" class="h-full w-full object-cover" />
</div>
@endif
<div class="flex-1">
<livewire:admin.cms.media-picker :value="null" :field="'gallery_' . $i" type="image" profile="gallery" label="Bild wählen" :key="'gal-picker-' . $i . '-' . ($editingId ?? 'new')" />
<flux:input wire:model="galleryItems.{{ $i }}" size="sm" class="mt-1" placeholder="Oder Pfad manuell..." />
</div>
@if (count($galleryItems) > 1)
<flux:button size="sm" variant="ghost" icon="trash" wire:click="removeGalleryItem({{ $i }})" />
@endif
</div>
@endforeach
<flux:button size="xs" variant="ghost" icon="plus" wire:click="addGalleryItem">Bild hinzufügen</flux:button>
</div>
@endif
{{-- TAB: Location --}}
@if ($activeTab === 'location')
<div class="space-y-4">
<flux:input wire:model="locTitle" label="Überschrift ({{ strtoupper($editLocale) }})" placeholder="Strategische Location: Al Jaddaf" />
<flux:input wire:model="locMapUrl" label="Google Maps URL" placeholder="https://maps.google.com/?q=..." />
<div>
<label class="mb-1 block text-sm font-medium text-zinc-700 dark:text-zinc-300">Location-Punkte ({{ strtoupper($editLocale) }})</label>
<div class="space-y-2">
@foreach ($locPoints as $i => $point)
<div wire:key="lp-{{ $i }}" class="flex items-center gap-2">
<flux:input wire:model="locPoints.{{ $i }}" placeholder="Beschreibung des Standort-Vorteils" class="flex-1" size="sm" />
@if (count($locPoints) > 1)
<flux:button size="xs" variant="ghost" icon="x-mark" wire:click="removeLocPoint({{ $i }})" />
@endif
</div>
@endforeach
</div>
<flux:button size="xs" variant="ghost" icon="plus" wire:click="addLocPoint" class="mt-2">Punkt</flux:button>
</div>
</div>
@endif
{{-- TAB: Kontakt --}}
@if ($activeTab === 'contact')
<div class="space-y-4">
<flux:input wire:model="contactTitle" label="Überschrift ({{ strtoupper($editLocale) }})" placeholder="Sichern Sie sich eine der Einheiten." />
<flux:input wire:model="contactSubtitle" label="Untertitel ({{ strtoupper($editLocale) }})" placeholder="Ihr Ansprechpartner: Marcel Scheibe" />
<div>
<label class="mb-1 block text-sm font-medium text-zinc-700 dark:text-zinc-300">Auswahloptionen (Key Anzeige)</label>
<div class="space-y-2">
@foreach ($contactOptions as $i => $opt)
<div wire:key="co-{{ $i }}" class="flex items-center gap-2">
<flux:input wire:model="contactOptions.{{ $i }}.key" placeholder="Key (leer=Default)" class="w-40" size="sm" />
<flux:input wire:model="contactOptions.{{ $i }}.value" placeholder="Anzeige-Text" class="flex-1" size="sm" />
@if (count($contactOptions) > 1)
<flux:button size="xs" variant="ghost" icon="x-mark" wire:click="removeContactOption({{ $i }})" />
@endif
</div>
@endforeach
</div>
<flux:button size="xs" variant="ghost" icon="plus" wire:click="addContactOption" class="mt-2">Option</flux:button>
</div>
</div>
@endif
{{-- TAB: Trust-Block & Möbel-Vorteil (Detailseite) --}}
@if ($activeTab === 'trust_synergy')
<div class="space-y-8">
<div>
<flux:heading size="sm" class="mb-3">Trust-Block: Investorenschutz ({{ strtoupper($editLocale) }})</flux:heading>
<div class="space-y-4">
<flux:input wire:model="trustTitle" label="Überschrift" placeholder="Maximale Sicherheit für Ihr Investment" />
<flux:textarea wire:model="trustIntro" label="Einleitung" rows="2" placeholder="Kurzer Intro-Text unter der Überschrift" />
<flux:input wire:model="trustCtaUrl" label="CTA-Link (Magazin o. ä.)" placeholder="/magazin/1" />
<flux:input wire:model="trustCtaLabel" label="CTA-Button-Text" placeholder="Deep Dive: …" />
<div>
<label class="mb-2 block text-sm font-medium text-zinc-700 dark:text-zinc-300">Drei Spalten (Icon = Blade-Name, z. B. heroicon-o-lock-closed)</label>
<div class="space-y-4">
@foreach ($trustColumns as $ti => $tc)
<div wire:key="trust-col-{{ $ti }}" class="rounded-lg border border-zinc-200 p-4 dark:border-zinc-700">
<div class="mb-3 flex items-end gap-2">
<flux:select wire:model="trustColumns.{{ $ti }}.icon" label="Icon" class="min-w-[14rem]">
<flux:select.option value="heroicon-o-lock-closed">Schloss (Escrow)</flux:select.option>
<flux:select.option value="heroicon-o-building-library">Gebäude / DLD</flux:select.option>
<flux:select.option value="heroicon-o-chart-bar">Diagramm</flux:select.option>
<flux:select.option value="heroicon-o-shield-check">Schild</flux:select.option>
<flux:select.option value="heroicon-o-sparkles">Sparkles</flux:select.option>
</flux:select>
@if (count($trustColumns) > 1)
<flux:button size="sm" variant="ghost" icon="trash" wire:click="removeTrustColumn({{ $ti }})" />
@endif
</div>
<flux:input wire:model="trustColumns.{{ $ti }}.title" label="Spaltenüberschrift" class="mb-2" />
<flux:textarea wire:model="trustColumns.{{ $ti }}.text" label="Text" rows="3" />
</div>
@endforeach
</div>
<flux:button size="xs" variant="ghost" icon="plus" wire:click="addTrustColumn" class="mt-2">Spalte hinzufügen</flux:button>
</div>
</div>
</div>
<flux:separator />
<div>
<flux:heading size="sm" class="mb-3">Möbel-Vorteil / Synergie ({{ strtoupper($editLocale) }})</flux:heading>
<flux:textarea wire:model="furnitureTitle" label="Überschrift (HTML erlaubt, z. B. &lt;span class=&quot;text-secondary&quot;&gt;)" rows="2" />
<flux:textarea wire:model="furnitureText" label="Fließtext" rows="4" class="mt-3" />
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<flux:input wire:model="furnitureButtonText" label="Button-Text" placeholder="Mehr zum B2in-Netzwerk" />
<flux:input wire:model="furnitureButtonLink" label="Button-Link" placeholder="/netzwerk" />
</div>
</div>
</div>
@endif
</div>
<div class="mt-6 flex gap-2 border-t border-zinc-200 pt-4 dark:border-zinc-700">
<flux:button variant="primary" wire:click="save">Speichern</flux:button>
<flux:button variant="ghost" wire:click="cancelForm">Abbrechen</flux:button>
</div>
</flux:card>
@endif
{{-- Projektliste --}}
<flux:card>
<div class="divide-y dark:divide-zinc-700">
@forelse ($this->projects as $project)
<div wire:key="project-{{ $project->id }}" class="flex items-center justify-between gap-4 py-3">
<div class="flex items-center gap-4">
@if ($project->image)
<div class="h-12 w-20 shrink-0 overflow-hidden rounded-lg border border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800">
<img src="{{ media_url($project->image) }}" class="h-full w-full object-cover" loading="lazy" />
</div>
@else
<div class="flex h-12 w-20 shrink-0 items-center justify-center rounded-lg border border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800">
<x-heroicon-o-building-office class="h-6 w-6 text-zinc-300" />
</div>
@endif
<div>
<div class="flex items-center gap-2">
<span class="font-medium text-zinc-800 dark:text-zinc-200">{{ $project->title }}</span>
@if ($project->is_published)
<flux:badge size="sm" color="green">Live</flux:badge>
@else
<flux:badge size="sm" color="zinc">Entwurf</flux:badge>
@endif
</div>
<div class="flex items-center gap-3 text-sm text-zinc-500">
<span>{{ $project->location }}</span>
@if ($project->status)
<span>· {{ $project->status }}</span>
@endif
@if ($project->getFormattedPrice())
<span>· {{ $project->getFormattedPrice() }}</span>
@endif
</div>
</div>
</div>
<div class="flex items-center gap-1">
<flux:button size="sm" variant="ghost" icon="pencil" wire:click="openEdit({{ $project->id }})" />
<flux:button size="sm" variant="ghost" icon="document-duplicate" wire:click="duplicateProject({{ $project->id }})" title="Projekt duplizieren" />
<flux:button size="sm" variant="ghost" icon="trash"
wire:click="deleteProject({{ $project->id }})"
wire:confirm="'{{ $project->title }}' wirklich löschen?" />
</div>
</div>
@empty
<div class="py-12 text-center">
<x-heroicon-o-building-office class="mx-auto mb-4 h-12 w-12 text-zinc-400" />
<flux:heading>Keine Projekte</flux:heading>
<flux:text>Erstelle das erste Projekt mit dem Button oben.</flux:text>
</div>
@endforelse
</div>
</flux:card>
</div>

View file

@ -1,80 +1,14 @@
<?php
use function Livewire\Volt\{state, computed};
use Illuminate\Support\Str;
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
use League\CommonMark\MarkdownConverter;
use App\Services\ProjectDocumentationContent;
use function Livewire\Volt\computed;
use function Livewire\Volt\state;
state(['showToc' => false]);
$content = computed(function () {
$mdPath = base_path('dev/entwicklung.md');
if (!file_exists($mdPath)) {
return '<p class="text-red-600">Dokumentation nicht gefunden.</p>';
}
$markdown = file_get_contents($mdPath);
// Configure CommonMark with GitHub Flavored Markdown
$environment = new Environment([
'html_input' => 'allow',
'allow_unsafe_links' => false,
]);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new GithubFlavoredMarkdownExtension());
$converter = new MarkdownConverter($environment);
return $converter->convert($markdown)->getContent();
});
// Extract Table of Contents from markdown
$tableOfContents = computed(function () {
$mdPath = base_path('dev/entwicklung.md');
if (!file_exists($mdPath)) {
return [];
}
$markdown = file_get_contents($mdPath);
$toc = [];
// Extract headings (## and ###)
preg_match_all('/^(#{2,3})\s+(.+)$/m', $markdown, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$level = strlen($match[1]);
$title = trim($match[2]);
$slug = Str::slug($title);
$toc[] = [
'level' => $level,
'title' => $title,
'slug' => $slug,
];
}
return $toc;
});
$fileInfo = computed(function () {
$mdPath = base_path('dev/entwicklung.md');
if (!file_exists($mdPath)) {
return null;
}
return [
'size' => round(filesize($mdPath) / 1024, 1) . ' KB',
'modified' => \Carbon\Carbon::parse(filemtime($mdPath))->format('d.m.Y H:i'),
'lines' => count(file($mdPath)),
];
});
$content = computed(fn () => ProjectDocumentationContent::html());
$tableOfContents = computed(fn () => ProjectDocumentationContent::tableOfContents());
$fileInfo = computed(fn () => ProjectDocumentationContent::fileInfo());
?>

View file

@ -141,7 +141,7 @@ new class extends Component {
href="{{ config('domains.domain_b2in_url') . route('registration.landing', ['role' => $roleOptions[$selectedRole]['slug']], false) }}"
target="_blank"
>
{{ __('B2In') }}
{{ __('B2in') }}
</flux:button>
@if($selectedRole === 'customer')
<flux:button

View file

@ -0,0 +1,304 @@
<div>
<style>
/* ---- Branding ---- */
.qs-brand {
display: flex;
align-items: center;
gap: 10px;
}
.qs-brand-logo {
height: 28px;
opacity: .85;
}
.qs-brand-sep {
width: 1px;
height: 18px;
background: var(--border);
}
.qs-brand-label {
font-size: 12px;
font-weight: 600;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--muted);
}
/* ---- Section headings ---- */
.qs-label {
font-size: 11px;
font-weight: 600;
letter-spacing: .10em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 10px;
}
/* ---- Status grid ---- */
.qs-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.qs-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
padding: 18px 12px;
border-radius: 14px;
border: 2px solid transparent;
background: var(--surface);
cursor: pointer;
transition: border-color 160ms, background 160ms, transform 80ms;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
.qs-btn:active { transform: scale(.96); }
.qs-btn-icon {
width: 44px;
height: 44px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
font-weight: 700;
flex-shrink: 0;
}
.qs-btn-name {
font-size: 14px;
font-weight: 600;
color: var(--fg);
}
.qs-btn-desc {
font-size: 11px;
color: var(--muted);
text-align: center;
line-height: 1.3;
}
/* Color variants */
.qs-btn[data-color="green"] .qs-btn-icon { background: #16a34a; color: #fff; }
.qs-btn[data-color="yellow"] .qs-btn-icon { background: #ca8a04; color: #fff; }
.qs-btn[data-color="orange"] .qs-btn-icon { background: #ea580c; color: #fff; }
.qs-btn[data-color="red"] .qs-btn-icon { background: #dc2626; color: #fff; }
.qs-btn[data-color="green"].active { border-color: #16a34a; background: rgba(22,163,74,.12); }
.qs-btn[data-color="yellow"].active { border-color: #ca8a04; background: rgba(202,138,4,.12); }
.qs-btn[data-color="orange"].active { border-color: #ea580c; background: rgba(234,88,12,.12); }
.qs-btn[data-color="red"].active { border-color: #dc2626; background: rgba(220,38,38,.12); }
/* ---- Inputs ---- */
.qs-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.qs-field-label {
font-size: 12px;
font-weight: 500;
color: var(--muted);
display: flex;
justify-content: space-between;
}
.qs-input {
background: var(--surface);
border: 1.5px solid var(--border);
border-radius: 10px;
color: var(--fg);
font-size: 15px;
padding: 12px 14px;
outline: none;
width: 100%;
font-family: inherit;
transition: border-color 150ms;
}
.qs-input:focus { border-color: rgba(255,255,255,.3); }
.qs-input::placeholder { color: rgba(255,255,255,.2); }
.qs-error {
font-size: 12px;
color: #f87171;
margin-top: 2px;
}
/* ---- Save button ---- */
.qs-save {
width: 100%;
padding: 16px;
border-radius: 14px;
border: none;
font-size: 16px;
font-weight: 700;
font-family: inherit;
letter-spacing: .02em;
cursor: pointer;
background: #ffffff;
color: #0f0f0f;
transition: opacity 150ms, transform 80ms;
-webkit-tap-highlight-color: transparent;
}
.qs-save:active { transform: scale(.98); opacity: .9; }
.qs-save:disabled { opacity: .4; cursor: default; }
/* ---- Success toast ---- */
.qs-toast {
display: flex;
align-items: center;
gap: 10px;
background: rgba(22,163,74,.2);
border: 1px solid rgba(22,163,74,.4);
border-radius: 12px;
padding: 14px 16px;
font-size: 14px;
font-weight: 500;
color: #86efac;
}
.qs-toast-icon {
width: 28px;
height: 28px;
border-radius: 50%;
background: #16a34a;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 700;
flex-shrink: 0;
}
/* ---- Forbidden screen ---- */
.qs-forbidden {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 60px 0;
text-align: center;
}
.qs-forbidden-icon {
font-size: 40px;
opacity: .4;
}
.qs-forbidden-title {
font-size: 18px;
font-weight: 600;
color: var(--fg);
}
.qs-forbidden-sub {
font-size: 14px;
color: var(--muted);
}
/* ---- Fields card wrapper ---- */
.qs-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 14px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 14px;
}
</style>
{{-- Brand header --}}
<header class="qs-brand">
<img src="/_cabinet/logo-cabinet-300.png" alt="CABINET" class="qs-brand-logo">
<div class="qs-brand-sep"></div>
<span class="qs-brand-label">Status</span>
</header>
@if(! $authorized)
{{-- Not authorized --}}
<div class="qs-forbidden">
<div class="qs-forbidden-icon">🔒</div>
<div class="qs-forbidden-title">Kein Zugriff</div>
<div class="qs-forbidden-sub">Ungültiger oder fehlender Key.</div>
</div>
@else
{{-- Status selector --}}
<div>
<div class="qs-label">Status wählen</div>
<div class="qs-grid">
@foreach($statusOptions as $value => $opt)
<button
type="button"
class="qs-btn {{ $storeStatus === $value ? 'active' : '' }}"
data-color="{{ $opt['color'] }}"
wire:click="selectStatus('{{ $value }}')"
>
<div class="qs-btn-icon">{{ $opt['icon'] }}</div>
<div class="qs-btn-name">{{ $opt['label'] }}</div>
<div class="qs-btn-desc">{{ $opt['description'] }}</div>
</button>
@endforeach
</div>
</div>
{{-- Text fields (only for non-auto statuses) --}}
@if($storeStatus !== 'auto')
<div class="qs-card">
<div class="qs-field">
<label class="qs-field-label">
<span>Headline</span>
<span>{{ strlen($noticeHeadline) }}/40</span>
</label>
<input
class="qs-input"
type="text"
wire:model.live="noticeHeadline"
maxlength="40"
placeholder="z.B. Heute erst ab 11:00 Uhr"
autocomplete="off"
>
@error('noticeHeadline') <span class="qs-error">{{ $message }}</span> @enderror
</div>
<div class="qs-field">
<label class="qs-field-label">
<span>Subtext <span style="opacity:.5">(optional)</span></span>
<span>{{ strlen($noticeSubtext) }}/80</span>
</label>
<input
class="qs-input"
type="text"
wire:model.live="noticeSubtext"
maxlength="80"
placeholder="z.B. Wegen Kundentermin öffnen wir später."
autocomplete="off"
>
@error('noticeSubtext') <span class="qs-error">{{ $message }}</span> @enderror
</div>
</div>
@endif
{{-- Success feedback --}}
@if($saved)
<div class="qs-toast">
<div class="qs-toast-icon"></div>
<span>Status wurde gespeichert und ist sofort aktiv.</span>
</div>
@endif
{{-- Save button --}}
<button
type="button"
class="qs-save"
wire:click="save"
wire:loading.attr="disabled"
>
<span wire:loading.remove>Speichern</span>
<span wire:loading>Wird gespeichert…</span>
</button>
@endif
</div>

View file

@ -11,7 +11,7 @@ use Livewire\Attributes\Title;
use Livewire\Volt\Component;
use Spatie\Permission\Models\Role;
new #[Layout('web.layouts.web-master-slot'), Title('Willkommen bei B2In')] class extends Component {
new #[Layout('web.layouts.web-master-slot'), Title('Willkommen bei B2in')] class extends Component {
public string $firstName = '';
public string $lastName = '';
public string $email = '';

View file

@ -11,7 +11,7 @@ use Livewire\Attributes\Title;
use Livewire\Volt\Component;
use Spatie\Permission\Models\Role;
new #[Layout('components.layouts.guest'), Title('Willkommen bei B2In')] class extends Component {
new #[Layout('components.layouts.guest'), Title('Willkommen bei B2in')] class extends Component {
public PartnerInvitation $invitation;
public string $firstName = '';
@ -96,7 +96,7 @@ new #[Layout('components.layouts.guest'), Title('Willkommen bei B2In')] class ex
\DB::commit();
// 6. Weiterleitung zum Setup-Wizard
session()->flash('message', __('Willkommen bei B2In! Vervollständigen Sie nun Ihr Profil.'));
session()->flash('message', __('Willkommen bei B2in! Vervollständigen Sie nun Ihr Profil.'));
$this->redirect(route('partner.setup.wizard'), navigate: true);
} catch (\Exception $e) {

File diff suppressed because it is too large Load diff

View file

@ -2,26 +2,32 @@
use App\Enums\ProductStatus;
use App\Models\Partner;
use App\Models\Product;
use Livewire\Volt\Component;
use function Livewire\Volt\{layout, title};
use function Livewire\Volt\layout;
layout('components.layouts.app');
new class extends Component {
new class extends Component
{
public Partner $partner;
public string $title = '';
public function mount(int $partnerId): void
{
$this->partner = Partner::with(['hub', 'products' => function ($q) {
$q->where('status', ProductStatus::Active)
->where('is_curated', true)
->where('is_available', true)
->with(['categories', 'media'])
->latest()
->limit(6);
}])->findOrFail($partnerId);
$this->partner = Partner::with([
'hub',
'media',
'products' => function ($q) {
$q->where('status', ProductStatus::Active)
->where('is_curated', true)
->where('is_available', true)
->with(['categories', 'media'])
->latest()
->limit(6);
},
])->findOrFail($partnerId);
$this->title = $this->partner->display_name ?? $this->partner->company_name;
}
@ -31,6 +37,9 @@ new class extends Component {
return [
'partner' => $this->partner,
'products' => $this->partner->products,
'teamPhotos' => $this->partner->media->where('type', 'team_photo')->sortBy('order_column')->values(),
'showroomPhotos' => $this->partner->media->where('type', 'showroom')->sortBy('order_column')->values(),
'brandImages' => $this->partner->media->where('type', 'brand_image')->sortBy('order_column')->values(),
];
}
}; ?>
@ -95,7 +104,7 @@ new class extends Component {
</flux:card>
<div class="grid grid-cols-1 gap-8 lg:grid-cols-3">
{{-- Linke Spalte: Story + Spezialisierungen --}}
{{-- Linke Spalte: Story + Showroom + Marken-Bilder + Produkte --}}
<div class="space-y-6 lg:col-span-2">
{{-- Story / Über uns --}}
@if($partner->story_text)
@ -107,6 +116,42 @@ new class extends Component {
</flux:card>
@endif
{{-- Showroom-Galerie (Händler) --}}
@if($showroomPhotos->isNotEmpty())
<flux:card class="shadow-elegant">
<flux:heading size="lg" class="mb-4">{{ __('Unser Showroom') }}</flux:heading>
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3">
@foreach($showroomPhotos as $photo)
<div class="aspect-square overflow-hidden rounded-lg bg-zinc-100 dark:bg-zinc-800">
<img
src="{{ Storage::url($photo->file_path) }}"
alt="{{ __('Showroom') }}"
class="h-full w-full object-cover"
>
</div>
@endforeach
</div>
</flux:card>
@endif
{{-- Marken-Bilder (Hersteller) --}}
@if($brandImages->isNotEmpty())
<flux:card class="shadow-elegant">
<flux:heading size="lg" class="mb-4">{{ __('Unsere Marke') }}</flux:heading>
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3">
@foreach($brandImages as $photo)
<div class="aspect-square overflow-hidden rounded-lg bg-zinc-100 dark:bg-zinc-800">
<img
src="{{ Storage::url($photo->file_path) }}"
alt="{{ __('Markenbild') }}"
class="h-full w-full object-cover"
>
</div>
@endforeach
</div>
</flux:card>
@endif
{{-- Produkte --}}
@if($products->isNotEmpty())
<flux:card class="shadow-elegant">
@ -197,6 +242,24 @@ new class extends Component {
</flux:card>
@endif
{{-- Team-Fotos --}}
@if($teamPhotos->isNotEmpty())
<flux:card class="shadow-elegant">
<flux:heading size="lg" class="mb-4">{{ __('Unser Team') }}</flux:heading>
<div class="grid grid-cols-2 gap-3">
@foreach($teamPhotos as $photo)
<div class="aspect-square overflow-hidden rounded-lg bg-zinc-100 dark:bg-zinc-800">
<img
src="{{ Storage::url($photo->file_path) }}"
alt="{{ __('Team') }}"
class="h-full w-full object-cover"
>
</div>
@endforeach
</div>
</flux:card>
@endif
{{-- Adresse --}}
@if($partner->street || $partner->city)
<flux:card class="shadow-elegant">

View file

@ -557,7 +557,7 @@ new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends C
{{ __('Ihre Marke') }}
</p>
<p class="text-xs text-purple-600 dark:text-purple-300">
{{ __('Unter dieser Marke werden Ihre Produkte auf B2In gelistet. Sie können später weitere Marken hinzufügen.') }}
{{ __('Unter dieser Marke werden Ihre Produkte auf B2in gelistet. Sie können später weitere Marken hinzufügen.') }}
</p>
</div>
@ -650,7 +650,7 @@ new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends C
™️ {{ __('Ihre Marke') }}
</flux:heading>
<flux:subheading>
{{ __('Unter dieser Marke werden Ihre Produkte auf B2In gelistet. (Sie können später weitere Marken hinzufügen)') }}
{{ __('Unter dieser Marke werden Ihre Produkte auf B2in gelistet. (Sie können später weitere Marken hinzufügen)') }}
</flux:subheading>
</div>

View file

@ -566,7 +566,7 @@ new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends C
™️ {{ __('Ihre Marke') }}
</flux:heading>
<flux:subheading>
{{ __('Unter dieser Marke werden Ihre Produkte auf B2In gelistet. (Sie können später weitere Marken hinzufügen)') }}
{{ __('Unter dieser Marke werden Ihre Produkte auf B2in gelistet. (Sie können später weitere Marken hinzufügen)') }}
</flux:subheading>
</div>

View file

@ -6,33 +6,48 @@ use App\Enums\ProductType;
use App\Models\Brand;
use App\Models\Category;
use App\Models\Hub;
use App\Models\Partner;
use App\Models\Product;
use Flux\Flux;
use Illuminate\Support\Facades\Storage;
use Livewire\Volt\Component;
use Livewire\WithFileUploads;
use function Livewire\Volt\{layout, title};
use function Livewire\Volt\layout;
use function Livewire\Volt\title;
layout('components.layouts.app');
title('Standard-Produkt');
new class extends Component {
new class extends Component
{
use WithFileUploads;
public ?Product $product = null;
public bool $isEditing = false;
public string $activeTab = 'basis';
public array $existingMedia = [];
// --- Tab 1: Basis ---
public string $name = '';
public string $descriptionShort = '';
public string $descriptionLong = '';
public string $brandName = '';
public ?int $categoryId = null;
public string $priceType = 'fixed';
public string $priceDisplayText = '';
public string $status = 'active';
public string $partnerProductNumber = '';
// --- Tab 2: Bilder ---
@ -40,64 +55,111 @@ new class extends Component {
// --- Tab 3: Maße & Material ---
public ?int $widthCm = null;
public ?int $heightCm = null;
public ?int $depthCm = null;
public ?int $weightG = null;
public string $assemblyStatus = '';
public string $careInstructions = '';
public string $countryOfOrigin = '';
public string $mainMaterial = '';
public string $surfaceMaterial = '';
public string $coverMaterial = '';
public string $colorFinish = '';
public array $certificates = [];
public ?int $assemblyTimeMin = null;
public ?int $loadCapacityKg = null;
// --- Tab 4: Verpackung & Versand ---
public ?int $packageCount = null;
public ?int $packageWeightG = null;
public ?int $packageWidthCm = null;
public ?int $packageHeightCm = null;
public ?int $packageDepthCm = null;
public string $packagingType = '';
public ?int $packagingRecyclablePercent = null;
public bool $isPalletizable = false;
public string $hsCode = '';
public string $deliveryType = '';
// --- Tab 5: Kommerziell ---
public string $sku = '';
public string $hanMpn = '';
public string $eanGtin = '';
public ?float $sellingPrice = null;
public ?float $purchasePrice = null;
public ?float $msrp = null;
public string $availabilityStatus = 'in_stock';
public string $deliveryTimeText = '';
public string $currency = 'EUR';
public ?int $productionTimeDays = null;
// --- Tab 6: Services & Garantie ---
public bool $assemblyService = false;
public ?int $serviceRadiusKm = null;
public ?int $warrantyMonths = null;
// --- Tab 7: Nachhaltigkeit & EUDR ---
public ?float $co2FootprintKg = null;
public ?int $recyclingPercentage = null;
public bool $isRegionalProduction = false;
public array $woodOrigins = [];
// --- Tab 8: Zuordnung & Verwaltung ---
public ?int $hubId = null;
// Nur für Admins: ausgewählter Partner für das neue Produkt
public ?int $selectedPartnerId = null;
public string $metaTitle = '';
public string $metaDescription = '';
public bool $visibleIsAvailable = false;
public ?string $visibleFrom = null;
public ?string $visibleUntil = null;
public ?int $storageVolumeLiters = null;
public ?int $assemblyEffortScore = null;
public ?int $designScore = null;
public function mount(?Product $product = null): void
@ -126,6 +188,28 @@ new class extends Component {
}
}
public function updatedSelectedPartnerId(): void
{
if ($this->selectedPartnerId) {
$partner = Partner::find($this->selectedPartnerId);
if ($partner) {
$nextNumber = $partner->products()->count() + 1;
$this->partnerProductNumber = sprintf('P%03d-%04d', $partner->id, $nextNumber);
}
}
}
private function resolvePartner(): ?Partner
{
$user = auth()->user();
if ($user->hasAnyRole(['Admin', 'Super-Admin'])) {
return $this->selectedPartnerId ? Partner::find($this->selectedPartnerId) : null;
}
return $user->partner;
}
private function prefillFromProduct(Product $product): void
{
// Basis
@ -200,7 +284,7 @@ new class extends Component {
// Wood origins
$this->woodOrigins = $product->woodOrigins
->map(
fn($wo) => [
fn ($wo) => [
'wood_species' => $wo->wood_species,
'origin_country' => $wo->origin_country,
'origin_region' => $wo->origin_region ?? '',
@ -228,7 +312,7 @@ new class extends Component {
->sortBy('order_column')
->values()
->map(
fn($m) => [
fn ($m) => [
'id' => $m->id,
'file_path' => $m->file_path,
'alt_text' => $m->alt_text,
@ -261,7 +345,7 @@ new class extends Component {
public function removeExistingMedia(int $mediaId): void
{
if (!$this->isEditing) {
if (! $this->isEditing) {
return;
}
@ -269,7 +353,7 @@ new class extends Component {
if ($media) {
Storage::disk('public')->delete($media->file_path);
$media->delete();
$this->existingMedia = collect($this->existingMedia)->reject(fn($m) => $m['id'] === $mediaId)->values()->toArray();
$this->existingMedia = collect($this->existingMedia)->reject(fn ($m) => $m['id'] === $mediaId)->values()->toArray();
}
}
@ -352,7 +436,7 @@ new class extends Component {
private function validationRules(): array
{
$allowedPriceTypes = collect(ProductType::SmartOrder->allowedPriceTypes())
->map(fn(PriceType $pt) => $pt->value)
->map(fn (PriceType $pt) => $pt->value)
->implode(',');
// SKU unique validation ignores own variant when editing
@ -364,7 +448,7 @@ new class extends Component {
}
}
return [
$rules = [
// Basis
'name' => 'required|string|max:255',
'partnerProductNumber' => 'nullable|string|max:100',
@ -440,6 +524,12 @@ new class extends Component {
'assemblyEffortScore' => 'nullable|integer|min:1|max:5',
'designScore' => 'nullable|integer|min:1|max:5',
];
if (! $this->isEditing && auth()->user()->hasAnyRole(['Admin', 'Super-Admin'])) {
$rules['selectedPartnerId'] = 'required|exists:partners,id';
}
return $rules;
}
/**
@ -465,6 +555,8 @@ new class extends Component {
'assemblyEffortScore.max' => __('Der Aufbauaufwand muss zwischen 1 und 5 liegen.'),
'recyclingPercentage.min' => __('Der Recyclinganteil muss zwischen 0 und 100 liegen.'),
'recyclingPercentage.max' => __('Der Recyclinganteil muss zwischen 0 und 100 liegen.'),
'selectedPartnerId.required' => __('Bitte wählen Sie einen Partner/Händler für dieses Produkt aus.'),
'selectedPartnerId.exists' => __('Der gewählte Partner existiert nicht.'),
];
}
@ -498,7 +590,7 @@ new class extends Component {
->sortBy('order_column')
->values()
->map(
fn($m) => [
fn ($m) => [
'id' => $m->id,
'file_path' => $m->file_path,
'alt_text' => $m->alt_text,
@ -546,8 +638,7 @@ new class extends Component {
private function saveNew(): void
{
$user = auth()->user();
$partner = $user->partner;
$partner = $this->resolvePartner();
// Marke suchen oder neu anlegen
$brandId = $this->resolveOrCreateBrand($partner);
@ -566,7 +657,7 @@ new class extends Component {
'name' => $this->name,
'slug' => str($this->name)
->slug()
->append('-' . uniqid()),
->append('-'.uniqid()),
'product_type' => ProductType::SmartOrder,
'status' => $this->status === 'active' ? ProductStatus::Pending : ProductStatus::Draft,
'price_type' => PriceType::from($this->priceType),
@ -583,7 +674,7 @@ new class extends Component {
'surface_material' => $this->surfaceMaterial ?: null,
'cover_material' => $this->coverMaterial ?: null,
'color_finish' => $this->colorFinish ?: null,
'certificates' => !empty($this->certificates) ? $this->certificates : null,
'certificates' => ! empty($this->certificates) ? $this->certificates : null,
'assembly_time_min' => $this->assemblyTimeMin,
'load_capacity_kg' => $this->loadCapacityKg,
'delivery_type' => $this->deliveryType ?: null,
@ -643,7 +734,7 @@ new class extends Component {
// Bilder speichern
$index = 1;
foreach ($this->mainImages as $image) {
$path = $image->store('products/' . $product->id, 'public');
$path = $image->store('products/'.$product->id, 'public');
$product->media()->create([
'file_path' => $path,
'type' => 'image',
@ -701,7 +792,7 @@ new class extends Component {
'surface_material' => $this->surfaceMaterial ?: null,
'cover_material' => $this->coverMaterial ?: null,
'color_finish' => $this->colorFinish ?: null,
'certificates' => !empty($this->certificates) ? $this->certificates : null,
'certificates' => ! empty($this->certificates) ? $this->certificates : null,
'assembly_time_min' => $this->assemblyTimeMin,
'load_capacity_kg' => $this->loadCapacityKg,
'delivery_type' => $this->deliveryType ?: null,
@ -775,7 +866,7 @@ new class extends Component {
$maxOrder = $this->product->media()->max('order_column') ?? 0;
$index = $maxOrder + 1;
foreach ($this->mainImages as $image) {
$path = $image->store('products/' . $this->product->id, 'public');
$path = $image->store('products/'.$this->product->id, 'public');
$this->product->media()->create([
'file_path' => $path,
'type' => 'image',
@ -798,19 +889,19 @@ new class extends Component {
private function resolveOrCreateBrand($partner): ?int
{
if (!$this->brandName) {
if (! $this->brandName) {
return null;
}
$brand = Brand::where('name', $this->brandName)->where(fn($q) => $q->whereNull('partner_id')->orWhere('partner_id', $partner->id))->first();
$brand = Brand::where('name', $this->brandName)->where(fn ($q) => $q->whereNull('partner_id')->orWhere('partner_id', $partner->id))->first();
if (!$brand) {
if (! $brand) {
$brand = Brand::create([
'partner_id' => $partner->id,
'name' => $this->brandName,
'slug' => str($this->brandName)
->slug()
->append('-' . uniqid()),
->append('-'.uniqid()),
'is_active' => true,
]);
}
@ -821,7 +912,7 @@ new class extends Component {
private function saveWoodOrigins(Product $product): void
{
foreach ($this->woodOrigins as $origin) {
if (!empty($origin['wood_species']) && !empty($origin['origin_country'])) {
if (! empty($origin['wood_species']) && ! empty($origin['origin_country'])) {
$product->woodOrigins()->create([
'wood_species' => $origin['wood_species'],
'origin_country' => $origin['origin_country'],
@ -837,13 +928,18 @@ new class extends Component {
public function with(): array
{
$effectivePartnerId = auth()->user()->partner_id ?? $this->selectedPartnerId;
$data = [
'categories' => Category::orderBy('name')->get(['id', 'name']),
'brands' => Brand::where('is_active', true)->where(fn($q) => $q->whereNull('partner_id')->orWhere('partner_id', auth()->user()->partner_id))->orderBy('name')->pluck('name'),
'brands' => Brand::where('is_active', true)->where(fn ($q) => $q->whereNull('partner_id')->orWhere('partner_id', $effectivePartnerId))->orderBy('name')->pluck('name'),
'hubs' => Hub::where('is_active', true)
->orderBy('name')
->get(['id', 'name']),
'allowedPriceTypes' => ProductType::SmartOrder->allowedPriceTypes(),
'adminPartners' => auth()->user()->hasAnyRole(['Admin', 'Super-Admin']) && ! $this->isEditing
? Partner::where('is_active', true)->orderBy('company_name')->get(['id', 'company_name', 'display_name'])
: collect(),
'countries' => collect([
'DE' => 'Deutschland',
'AT' => 'Österreich',
@ -909,6 +1005,28 @@ new class extends Component {
</flux:callout>
@endif
{{-- Admin: Partner-Auswahl (nur bei Neuanlage) --}}
@if (!$isEditing && auth()->user()->hasAnyRole(['Admin', 'Super-Admin']))
<flux:card class="shadow-elegant border-amber-200 dark:border-amber-700">
<div class="mb-4">
<flux:heading size="lg">{{ __('Partner auswählen') }}</flux:heading>
<flux:subheading>{{ __('Als Admin legen Sie das Produkt im Namen eines Partners an.') }}</flux:subheading>
</div>
<flux:separator class="mb-6" />
<flux:field>
<flux:label>{{ __('Partner / Händler') }} <flux:badge size="sm" color="red">{{ __('Pflichtfeld') }}</flux:badge></flux:label>
<flux:select wire:model.live="selectedPartnerId" placeholder="{{ __('Partner wählen...') }}">
@foreach ($adminPartners as $p)
<flux:select.option value="{{ $p->id }}">
{{ $p->display_name ?? $p->company_name }}
</flux:select.option>
@endforeach
</flux:select>
<flux:error name="selectedPartnerId" />
</flux:field>
</flux:card>
@endif
<form wire:submit="save" class="space-y-6">
{{-- Tab Navigation --}}

View file

@ -4,36 +4,50 @@ use App\Enums\PriceType;
use App\Enums\ProductStatus;
use App\Enums\ProductType;
use App\Models\Category;
use App\Models\Partner;
use App\Models\Product;
use App\Models\ProductVariant;
use Flux\Flux;
use Illuminate\Support\Facades\Storage;
use Livewire\Volt\Component;
use Livewire\WithFileUploads;
use function Livewire\Volt\{layout, title};
use function Livewire\Volt\layout;
use function Livewire\Volt\title;
layout('components.layouts.app');
title('Teaser-Produkt');
new class extends Component {
new class extends Component
{
use WithFileUploads;
public ?Product $product = null;
public bool $isEditing = false;
public array $existingMedia = [];
// Produkt-Felder (Typ A Teaser / Local Stock)
public string $name = '';
public string $descriptionShort = '';
public string $priceType = 'from_price';
public string $priceDisplayText = '';
public ?int $categoryId = null;
public string $status = 'active';
public string $partnerProductNumber = '';
// Bildupload
public array $mainImages = [];
// Nur für Admins: ausgewählter Partner für das neue Produkt
public ?int $selectedPartnerId = null;
public function mount(?Product $product = null): void
{
if ($product && $product->exists) {
@ -59,7 +73,7 @@ new class extends Component {
->sortBy('order_column')
->values()
->map(
fn($m) => [
fn ($m) => [
'id' => $m->id,
'file_path' => $m->file_path,
'alt_text' => $m->alt_text,
@ -85,9 +99,31 @@ new class extends Component {
}
}
public function updatedSelectedPartnerId(): void
{
if ($this->selectedPartnerId) {
$partner = Partner::find($this->selectedPartnerId);
if ($partner) {
$nextNumber = $partner->products()->count() + 1;
$this->partnerProductNumber = sprintf('P%03d-%04d', $partner->id, $nextNumber);
}
}
}
private function resolvePartner(): ?Partner
{
$user = auth()->user();
if ($user->hasAnyRole(['Admin', 'Super-Admin'])) {
return $this->selectedPartnerId ? Partner::find($this->selectedPartnerId) : null;
}
return $user->partner;
}
public function removeExistingMedia(int $mediaId): void
{
if (!$this->isEditing) {
if (! $this->isEditing) {
return;
}
@ -95,7 +131,7 @@ new class extends Component {
if ($media) {
Storage::disk('public')->delete($media->file_path);
$media->delete();
$this->existingMedia = collect($this->existingMedia)->reject(fn($m) => $m['id'] === $mediaId)->values()->toArray();
$this->existingMedia = collect($this->existingMedia)->reject(fn ($m) => $m['id'] === $mediaId)->values()->toArray();
}
}
@ -114,7 +150,7 @@ new class extends Component {
*/
public function updateMediaOrder(array $orderedIds): void
{
if (!$this->isEditing) {
if (! $this->isEditing) {
return;
}
@ -188,33 +224,42 @@ new class extends Component {
}
$allowedPriceTypes = collect(ProductType::LocalStock->allowedPriceTypes())
->map(fn(PriceType $pt) => $pt->value)
->map(fn (PriceType $pt) => $pt->value)
->implode(',');
$this->validate(
[
'name' => 'required|string|max:255',
'descriptionShort' => 'required|string|max:180',
'priceType' => "required|in:{$allowedPriceTypes}",
'priceDisplayText' => 'required_if:priceType,from_price|nullable|string|max:100',
'categoryId' => 'required|exists:categories,id',
'status' => 'required|in:active,draft',
'partnerProductNumber' => 'nullable|string|max:100',
'mainImages' => 'nullable|array|min:0|max:10',
'mainImages.*' => 'mimes:jpeg,jpg,png|max:10240',
],
[
'name.required' => __('Bitte geben Sie einen Produktnamen ein.'),
'descriptionShort.required' => __('Bitte geben Sie eine Kurzbeschreibung ein.'),
'descriptionShort.max' => __('Die Kurzbeschreibung darf maximal 180 Zeichen lang sein.'),
'priceType.required' => __('Bitte wählen Sie eine Preisangabe aus.'),
'priceDisplayText.required_if' => __('Bei Ab-Preis ist eine Preisangabe erforderlich (z.B. "Ab 2.500 €").'),
'categoryId.required' => __('Bitte wählen Sie eine Kategorie aus.'),
'mainImages.max' => __('Maximal 10 Produktbilder erlaubt.'),
'mainImages.*.mimes' => __('Nur Bilder (JPG, JPEG, PNG) erlaubt.'),
'mainImages.*.max' => __('Bilder dürfen maximal 10 MB groß sein.'),
],
);
$isAdminWithoutPartner = ! $this->isEditing && auth()->user()->hasAnyRole(['Admin', 'Super-Admin']);
$rules = [
'name' => 'required|string|max:255',
'descriptionShort' => 'required|string|max:180',
'priceType' => "required|in:{$allowedPriceTypes}",
'priceDisplayText' => 'required_if:priceType,from_price|nullable|string|max:100',
'categoryId' => 'required|exists:categories,id',
'status' => 'required|in:active,draft',
'partnerProductNumber' => 'nullable|string|max:100',
'mainImages' => 'nullable|array|min:0|max:10',
'mainImages.*' => 'mimes:jpeg,jpg,png|max:10240',
];
$messages = [
'name.required' => __('Bitte geben Sie einen Produktnamen ein.'),
'descriptionShort.required' => __('Bitte geben Sie eine Kurzbeschreibung ein.'),
'descriptionShort.max' => __('Die Kurzbeschreibung darf maximal 180 Zeichen lang sein.'),
'priceType.required' => __('Bitte wählen Sie eine Preisangabe aus.'),
'priceDisplayText.required_if' => __('Bei Ab-Preis ist eine Preisangabe erforderlich (z.B. "Ab 2.500 €").'),
'categoryId.required' => __('Bitte wählen Sie eine Kategorie aus.'),
'mainImages.max' => __('Maximal 10 Produktbilder erlaubt.'),
'mainImages.*.mimes' => __('Nur Bilder (JPG, JPEG, PNG) erlaubt.'),
'mainImages.*.max' => __('Bilder dürfen maximal 10 MB groß sein.'),
];
if ($isAdminWithoutPartner) {
$rules['selectedPartnerId'] = 'required|exists:partners,id';
$messages['selectedPartnerId.required'] = __('Bitte wählen Sie einen Partner/Händler für dieses Produkt aus.');
$messages['selectedPartnerId.exists'] = __('Der gewählte Partner existiert nicht.');
}
$this->validate($rules, $messages);
if ($this->isEditing) {
$this->saveExisting();
@ -229,7 +274,7 @@ new class extends Component {
->media->sortBy('order_column')
->values()
->map(
fn($m) => [
fn ($m) => [
'id' => $m->id,
'file_path' => $m->file_path,
'alt_text' => $m->alt_text,
@ -249,8 +294,7 @@ new class extends Component {
private function saveNew(): void
{
$user = auth()->user();
$partner = $user->partner;
$partner = $this->resolvePartner();
// Status: 'active' im UI → Pending (zur Freigabe), 'draft' → Draft
$newStatus = $this->status === 'active' ? ProductStatus::Pending : ProductStatus::Draft;
@ -268,7 +312,7 @@ new class extends Component {
'name' => $this->name,
'slug' => str($this->name)
->slug()
->append('-' . uniqid()),
->append('-'.uniqid()),
'product_type' => ProductType::LocalStock,
'status' => $newStatus,
'price_type' => PriceType::from($this->priceType),
@ -284,7 +328,7 @@ new class extends Component {
// Bilder speichern
$index = 1;
foreach ($this->mainImages as $image) {
$path = $image->store('products/' . $product->id, 'public');
$path = $image->store('products/'.$product->id, 'public');
$product->media()->create([
'file_path' => $path,
'type' => 'image',
@ -322,7 +366,7 @@ new class extends Component {
$maxOrder = $this->product->media()->max('order_column') ?? 0;
$index = $maxOrder + 1;
foreach ($this->mainImages as $image) {
$path = $image->store('products/' . $this->product->id, 'public');
$path = $image->store('products/'.$this->product->id, 'public');
$this->product->media()->create([
'file_path' => $path,
'type' => 'image',
@ -345,6 +389,9 @@ new class extends Component {
'categories' => Category::orderBy('name')->get(['id', 'name']),
'allowedPriceTypes' => ProductType::LocalStock->allowedPriceTypes(),
'isEditing' => $this->isEditing,
'adminPartners' => auth()->user()->hasAnyRole(['Admin', 'Super-Admin']) && ! $this->isEditing
? Partner::where('is_active', true)->orderBy('company_name')->get(['id', 'company_name', 'display_name'])
: collect(),
];
if ($this->isEditing) {
@ -384,6 +431,28 @@ new class extends Component {
</flux:callout>
@endif
{{-- Admin: Partner-Auswahl (nur bei Neuanlage) --}}
@if (!$isEditing && auth()->user()->hasAnyRole(['Admin', 'Super-Admin']))
<flux:card class="shadow-elegant border-amber-200 dark:border-amber-700">
<div class="mb-4">
<flux:heading size="lg">{{ __('Partner auswählen') }}</flux:heading>
<flux:subheading>{{ __('Als Admin legen Sie das Produkt im Namen eines Partners an.') }}</flux:subheading>
</div>
<flux:separator class="mb-6" />
<flux:field>
<flux:label>{{ __('Partner / Händler') }} <flux:badge size="sm" color="red">{{ __('Pflichtfeld') }}</flux:badge></flux:label>
<flux:select wire:model.live="selectedPartnerId" placeholder="{{ __('Partner wählen...') }}">
@foreach ($adminPartners as $p)
<flux:select.option value="{{ $p->id }}">
{{ $p->display_name ?? $p->company_name }}
</flux:select.option>
@endforeach
</flux:select>
<flux:error name="selectedPartnerId" />
</flux:field>
</flux:card>
@endif
<form wire:submit="save" class="space-y-6">
{{-- Bild-Upload --}}

View file

@ -21,7 +21,7 @@
<div class="relative">
<div class="relative rounded-3xl overflow-hidden shadow-elevated slide-left delay-300">
<img src="{{ asset('img/assets/' . $content['image']) }}"
<img src="{{ theme_image_url($content['image']) }}"
alt="{{ $content['image_alt'] }}"
class="w-full h-[600px] object-cover" />
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>

View file

@ -20,7 +20,7 @@
<div class="sticky top-8">
<div class="relative rounded-3xl overflow-hidden shadow-elevated slide-right delay-300">
@if(isset($content['image']))
<img src="{{ asset('img/assets/' . $content['image']) }}"
<img src="{{ theme_image_url($content['image']) }}"
alt="{{ $content['image_alt'] ?? 'Benefits Image' }}"
class="w-full h-[400px] md:h-[500px] lg:h-[600px] object-cover" />
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
@ -122,7 +122,7 @@
<div class="sticky top-8">
<div class="relative rounded-3xl overflow-hidden shadow-elevated slide-left delay-300">
@if(isset($content['image']))
<img src="{{ asset('img/assets/' . $content['image']) }}"
<img src="{{ theme_image_url($content['image']) }}"
alt="{{ $content['image_alt'] ?? 'Benefits Image' }}"
class="w-full h-[400px] md:h-[500px] lg:h-[600px] object-cover" />
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>

View file

@ -13,14 +13,17 @@
@foreach ($worlds as $world)
<div class="card-elevated overflow-hidden group hover:shadow-elevated transition-all duration-300 flex flex-col slide-up delay-500">
<div class="relative">
<img src="{{ asset('img/assets/' . $world['image']) }}" alt="{{ $world['title'] }}"
<x-web-picture
src="{{ theme_image_url($world['image']) }}"
alt="{{ $world['title'] }}"
class="w-full h-64 object-cover group-hover:scale-105 transition-transform duration-500" />
</div>
<div class="p-6 spacing-small flex flex-col justify-between flex-grow">
<div class="mb-4">
@if (isset($world['logo']))
<img src="{{ asset($world['logo']) }}" alt="{{ $world['title'] }}"
class="{{ $world['logo_width'] }} h-18 object-contain" />
class="{{ $world['logo_width'] }} h-18 object-contain"
loading="lazy" />
@else
<h3 class="text-xl font-medium">{{ $world['title'] }}</h3>
@endif
@ -30,9 +33,10 @@
</div>
<a href="{{ $world['link'] }}"
class="inline-flex items-center gap-2 text-secondary font-medium hover:gap-3 transition-all duration-300">
Mehr erfahren
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
class="inline-flex items-center gap-2 text-secondary font-medium hover:gap-3 transition-all duration-300"
@if(!empty($world['external'])) target="_blank" rel="noopener" @endif>
{{ __('ui.learn_more') }}
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7">
</path>
</svg>

View file

@ -9,13 +9,13 @@
</div>
{{-- Brand Cards --}}
<div class="grid md:grid-cols-3 gap-8">
<div class="grid md:grid-cols-2 gap-8">
@foreach ($content['cards'] as $index => $card)
<div class="card-elevated overflow-hidden group hover:shadow-elevated transition-all duration-300 flex flex-col slide-up delay-{{ $index * 200 }}">
@if(isset($card['image']))
<div class="relative">
<img src="{{ asset('img/assets/' . $card['image']) }}" alt="{{ $card['title'] }}"
<img src="{{ theme_image_url($card['image']) }}" alt="{{ $card['title'] }}"
class="w-full h-64 object-cover group-hover:scale-105 transition-transform duration-500" />
</div>
@endif
@ -54,7 +54,7 @@
@if(isset($card['link']))
<a href="{{ $card['link'] }}"
class="inline-flex items-center gap-2 text-secondary font-medium hover:gap-3 transition-all duration-300">
Mehr erfahren
{{ __('ui.learn_more') }}
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7">
</path>

View file

@ -13,7 +13,7 @@
@foreach($content['testimonials'] as $index => $testimonial)
<div class="card-elevated rounded-2xl p-8 text-left space-y-6 slide-up delay-{{ $index * 200 }}">
<div class="flex items-center gap-4">
<img src="{{ asset('img/assets/' . $testimonial['image']) }}" alt="{{ $testimonial['author'] }}" class="w-16 h-16 rounded-full object-cover">
<img src="{{ theme_image_url($testimonial['image']) }}" alt="{{ $testimonial['author'] }}" class="w-16 h-16 rounded-full object-cover">
<div>
<h4 class="font-bold text-foreground">{{ $testimonial['author'] }}</h4>
<p class="text-sm text-muted-foreground">{{ $testimonial['author_title'] }}</p>

View file

@ -7,7 +7,7 @@
{{-- Image --}}
<div class="relative lg:order-1">
<div class="card-elevated rounded-3xl overflow-hidden slide-left delay-400">
<img src="{{ asset('img/assets/' . $content['image']) }}" alt="{{ $content['image_alt'] }}"
<img src="{{ theme_image_url($content['image']) }}" alt="{{ $content['image_alt'] }}"
class="w-full h-[500px] object-cover" />
<div
class="absolute bottom-6 left-6 bg-card/95 backdrop-blur-sm rounded-xl p-4 shadow-lg border border-border/50 slide-left delay-500">
@ -58,7 +58,7 @@
{{-- Image --}}
<div class="relative">
<div class="card-elevated rounded-3xl overflow-hidden slide-left delay-400">
<img src="{{ asset('img/assets/' . $content['image']) }}" alt="{{ $content['image_alt'] }}"
<img src="{{ theme_image_url($content['image']) }}" alt="{{ $content['image_alt'] }}"
class="w-full h-full object-cover" />
<div
class="absolute bottom-6 left-6 bg-card/95 backdrop-blur-sm rounded-xl p-4 shadow-lg border border-border/50 slide-left delay-500">

View file

@ -38,7 +38,7 @@
<div class="relative">
<div class="card-elevated bg-muted p-0 overflow-hidden rounded-3xl">
<img src="{{ asset('img/assets/' . $content['image']) }}" alt="{{ $content['image_alt'] }}"
<img src="{{ theme_image_url($content['image']) }}" alt="{{ $content['image_alt'] }}"
class="w-full h-96 object-cover" />
</div>
</div>

View file

@ -1,5 +1,6 @@
{{-- Hero Icons Helper Function --}}
@php
if (!function_exists('renderHeroIcon')) {
function renderHeroIcon($iconName, $style = 'outline', $color = 'text-secondary')
{
$iconPath = public_path("heroicons/optimized/24/{$style}/{$iconName}.svg");
@ -19,13 +20,14 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.563.563 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z"/>
</svg>';
}
}
@endphp
<section class="section-padding {{ $bg }}">
<div class="container-padding">
{{-- Section Title --}}
<div class="text-center mb-16 slide-up delay-300">
<h2 class="text-section-title">{{ $content['title'] }}</h2>
<h2 class="text-section-title">{!! $content['title'] !!}</h2>
@if (isset($content['subtitle']))
<p class="text-large text-muted-foreground mt-4 max-w-2xl mx-auto">
{{ $content['subtitle'] }}
@ -36,7 +38,11 @@
{{-- Pillars Grid --}}
<div class="grid md:grid-cols-3 gap-8 lg:gap-12 slide-up delay-400">
@foreach ($content['pillars'] as $index => $pillar)
@if (isset($pillar['link']))
<a href="{{ $pillar['link'] }}" class="card-elevated rounded-2xl p-8 text-center block hover:shadow-lg transition-shadow duration-300">
@else
<div class="card-elevated rounded-2xl p-8 text-center">
@endif
<div class="text-center spacing-content group">
<div
class="mx-auto w-20 h-20 icon-secondary-linear glow-soft group-hover:glow-medium rounded-2xl flex items-center justify-center transition-colors duration-300">
@ -50,7 +56,11 @@
</p>
</div>
</div>
@if (isset($pillar['link']))
</a>
@else
</div>
@endif
@endforeach
</div>
</div>

View file

@ -51,7 +51,7 @@
<div class="relative">
<div class="relative rounded-3xl overflow-hidden shadow-elevated">
<img src="{{ asset('img/assets/' . $content['image']) }}"
<img src="{{ theme_image_url($content['image']) }}"
alt="{{ $content['image_alt'] }}"
class="w-full h-[600px] object-cover" />
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>

View file

@ -55,7 +55,7 @@
</div>
<div class="relative">
<div class="relative rounded-3xl overflow-hidden shadow-elevated">
<img src="{{ asset('img/assets/' . $content['image']) }}"
<img src="{{ theme_image_url($content['image']) }}"
alt="{{ $content['image_alt'] }}"
class="w-full h-[600px] object-cover" />
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>

View file

@ -1,11 +1,12 @@
@php($faqCounter = 0)
<div x-data="{ openIndex: null }">
<section class="section-padding {{ $bg }}">
<div class="container-padding">
{{-- Section Title --}}
<div class="text-center mb-16">
<h2 class="text-hero mb-6">
<h1 class="text-hero mb-6">
{!! $content['title'] !!}
</h2>
</h1>
@if(!empty($content['subtitle']))
<p class="text-large text-muted-foreground max-w-2xl mx-auto">
{!! $content['subtitle'] !!}
@ -14,10 +15,12 @@
</div>
{{-- FAQ Container --}}
<div class="max-w-4xl mx-auto">
<div class="max-w-4xl mx-auto space-y-12">
{{-- Allgemeine Fragen (ungrouped) --}}
@if(!empty($content['questions']))
<div class="space-y-4">
@foreach($content['questions'] as $index => $faq)
@foreach($content['questions'] as $faq)
@php($index = $faqCounter++)
<div class="card-elevated rounded-xl overflow-hidden">
<dt>
<button
@ -55,8 +58,66 @@
</div>
@endforeach
</div>
@else
{{-- Fallback wenn keine FAQ-Daten vorhanden --}}
@endif
{{-- Gruppierte Sektionen --}}
@if(!empty($content['sections']))
@foreach($content['sections'] as $section)
<div>
{{-- Sektions-Header --}}
<div class="flex items-center gap-3 mb-6">
<div class="flex items-center justify-center w-10 h-10 rounded-full bg-secondary/10 text-secondary shrink-0">
@svg('heroicon-o-' . ($section['icon'] ?? 'question-mark-circle'), 'w-5 h-5')
</div>
<h2 class="text-lg font-semibold text-foreground">{{ $section['title'] }}</h2>
<div class="flex-1 h-px bg-border"></div>
</div>
<div class="space-y-4">
@foreach($section['questions'] as $faq)
@php($index = $faqCounter++)
<div class="card-elevated rounded-xl overflow-hidden">
<dt>
<button
@click="openIndex = openIndex === {{ $index }} ? null : {{ $index }}"
class="flex w-full items-center justify-between p-6 text-left hover:bg-muted/30 transition-colors duration-200"
:aria-expanded="openIndex === {{ $index }}"
>
<span class="text-lg font-medium text-foreground pr-6">
{{ $faq['question'] }}
</span>
<span class="flex-shrink-0 ml-6 flex h-8 w-8 items-center justify-center rounded-full bg-secondary/10 transition-all duration-200"
:class="openIndex === {{ $index }} ? 'bg-secondary text-white' : 'text-secondary hover:bg-secondary/20'">
<svg class="w-5 h-5 transition-transform duration-200"
:class="openIndex === {{ $index }} ? '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>
</span>
</button>
</dt>
<dd x-show="openIndex === {{ $index }}"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 transform -translate-y-2"
x-transition:enter-end="opacity-100 transform translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 transform translate-y-0"
x-transition:leave-end="opacity-0 transform -translate-y-2"
class="px-6 pb-6">
<div class="border-t border-border pt-4">
<p class="text-muted-foreground leading-relaxed">
{{ $faq['answer'] }}
</p>
</div>
</dd>
</div>
@endforeach
</div>
</div>
@endforeach
@endif
@if(empty($content['questions']) && empty($content['sections']))
<div class="text-center py-12">
<p class="text-muted-foreground">Keine FAQ-Inhalte verfügbar.</p>
</div>

View file

@ -0,0 +1,24 @@
<section class="py-8 bg-background border-b border-border/30">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex flex-col sm:flex-row items-center gap-6 sm:gap-8">
@if(isset($content['image']))
<div class="shrink-0">
<x-web-picture
src="{{ theme_image_url($content['image']) }}"
alt="{{ $content['name'] ?? 'Founder' }}"
class="w-16 h-16 sm:w-20 sm:h-20 rounded-full object-cover border-2 border-secondary/30 shadow-lg"
width="80" height="80" />
</div>
@endif
<div class="text-center sm:text-left">
<p class="text-lg sm:text-xl font-medium text-foreground">
{!! $content['statement'] ?? '' !!}
</p>
<p class="text-sm text-muted-foreground mt-1">
{{ $content['name'] ?? '' }} · {{ $content['title'] ?? '' }}
</p>
</div>
</div>
</div>
</section>

View file

@ -25,7 +25,7 @@
{{-- Full Width Hero Image --}}
<div class="relative w-full">
<div class="relative rounded-2xl overflow-hidden shadow-elevated">
<img src="{{ asset('img/assets/' . ($content['hero_image'] ?? $content['tiles'][0]['image'])) }}"
<img src="{{ theme_image_url($content['hero_image'] ?? $content['tiles'][0]['image']) }}"
alt="{{ $content['hero_image_alt'] ?? $content['tiles'][0]['alt'] ?? '' }}"
class="w-full h-64 md:h-80 lg:h-96 object-cover" />
<div class="absolute inset-0 bg-gradient-to-t from-black/30 via-black/10 to-transparent"></div>

View file

@ -10,7 +10,7 @@
:class="currentSlide === {{ $index }} ? 'opacity-100' : 'opacity-0'"
>
<img
src="{{ asset('img/assets/' . $slide['image']) }}"
src="{{ theme_image_url($slide['image']) }}"
alt="{{ $slide['image_alt'] }}"
class="size-full object-cover"
/>
@ -38,7 +38,7 @@
@click="setSlide({{ $index }})"
class="w-3 h-3 rounded-full border-2 border-white/50 transition-all duration-300"
:class="currentSlide === {{ $index }} ? 'bg-white border-white' : 'bg-transparent hover:border-white/80'"
aria-label="Slide {{ $index + 1 }} anzeigen"
aria-label="{{ __('ui.show_slide', ['number' => $index + 1]) }}"
></button>
@endforeach
</div>
@ -50,7 +50,7 @@
<button
@click="previousSlide()"
class="absolute left-6 top-1/2 transform -translate-y-1/2 text-white/70 hover:text-white transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-white/50 rounded-full p-2"
aria-label="Vorheriges Bild"
aria-label="{{ __('ui.previous_image') }}"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
@ -60,7 +60,7 @@
<button
@click="nextSlide()"
class="absolute right-6 top-1/2 transform -translate-y-1/2 text-white/70 hover:text-white transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-white/50 rounded-full p-2"
aria-label="Nächstes Bild"
aria-label="{{ __('ui.next_image') }}"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>

View file

@ -37,7 +37,7 @@
{{-- First Column --}}
<div class="ml-auto w-44 flex-none space-y-8 pt-32 sm:ml-0 sm:pt-80 lg:order-last lg:pt-36 xl:order-0 xl:pt-80">
<div class="relative rounded-3xl overflow-hidden shadow-elevated">
<img src="{{ asset('img/assets/' . $content['tiles'][0]['image']) }}"
<img src="{{ theme_image_url($content['tiles'][0]['image']) }}"
alt="{{ $content['tiles'][0]['alt'] ?? '' }}"
class="aspect-2/3 w-full object-cover" />
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
@ -47,13 +47,13 @@
{{-- Second Column --}}
<div class="mr-auto w-44 flex-none space-y-8 sm:mr-0 sm:pt-52 lg:pt-36">
<div class="relative rounded-3xl overflow-hidden shadow-elevated">
<img src="{{ asset('img/assets/' . $content['tiles'][1]['image']) }}"
<img src="{{ theme_image_url($content['tiles'][1]['image']) }}"
alt="{{ $content['tiles'][1]['alt'] ?? '' }}"
class="aspect-2/3 w-full object-cover" />
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
</div>
<div class="relative rounded-3xl overflow-hidden shadow-elevated">
<img src="{{ asset('img/assets/' . $content['tiles'][2]['image']) }}"
<img src="{{ theme_image_url($content['tiles'][2]['image']) }}"
alt="{{ $content['tiles'][2]['alt'] ?? '' }}"
class="aspect-2/3 w-full object-cover" />
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
@ -63,13 +63,13 @@
{{-- Third Column --}}
<div class="w-44 flex-none space-y-8 pt-32 sm:pt-0">
<div class="relative rounded-3xl overflow-hidden shadow-elevated">
<img src="{{ asset('img/assets/' . $content['tiles'][3]['image']) }}"
<img src="{{ theme_image_url($content['tiles'][3]['image']) }}"
alt="{{ $content['tiles'][3]['alt'] ?? '' }}"
class="aspect-2/3 w-full object-cover" />
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
</div>
<div class="relative rounded-3xl overflow-hidden shadow-elevated">
<img src="{{ asset('img/assets/' . $content['tiles'][4]['image']) }}"
<img src="{{ theme_image_url($content['tiles'][4]['image']) }}"
alt="{{ $content['tiles'][4]['alt'] ?? '' }}"
class="aspect-2/3 w-full object-cover" />
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>

View file

@ -37,7 +37,7 @@
{{-- Right Image --}}
<div class="relative">
<div class="relative rounded-3xl overflow-hidden shadow-elevated slide-left delay-300">
<img src="{{ asset('img/assets/' . $content['image']) }}" alt="{{ $content['image_alt'] }}"
<img src="{{ theme_image_url($content['image']) }}" alt="{{ $content['image_alt'] }}"
class="w-full h-[600px] object-cover" />
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
</div>

View file

@ -0,0 +1,21 @@
@if (!empty($content))
<section class="relative h-[30vh] min-h-[220px] overflow-hidden">
<x-web-picture
src="{{ theme_image_url($content['image'] ?? '') }}"
alt="{{ $content['image_alt'] ?? 'Dekoratives Bild' }}"
class="absolute inset-0 w-full h-full object-cover" />
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-black/30 to-black/10"></div>
@if (isset($content['quote']))
<div class="absolute inset-0 flex items-end justify-center pb-12 px-6">
<div class="text-center slide-up delay-300">
<p class="text-xl lg:text-2xl font-medium text-white italic max-w-2xl leading-relaxed">
"{{ $content['quote'] }}"
</p>
@if (isset($content['author']))
<p class="text-sm text-white/70 mt-3"> {{ $content['author'] }}</p>
@endif
</div>
</div>
@endif
</section>
@endif

View file

@ -0,0 +1,149 @@
<div>
@if ($success)
<div class="flex flex-col items-center text-center py-6 gap-4">
<div class="flex items-center justify-center w-16 h-16 rounded-full bg-green-100">
<svg class="w-8 h-8 text-green-600" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
</div>
<div>
<p class="text-lg font-semibold text-foreground">{{ __('ui.immobilien_form.success_title') }}</p>
<p class="text-muted-foreground mt-1 text-sm">{{ __('ui.immobilien_form.success_message') }}</p>
</div>
</div>
@else
<form wire:submit="submit" class="space-y-6">
{{-- Honeypot --}}
<div class="opacity-0 absolute top-0 left-0 h-0 w-0 -z-10 overflow-hidden" aria-hidden="true">
<label for="website_hp">Website</label>
<input type="text" id="website_hp" name="website" wire:model="website" tabindex="-1" autocomplete="off">
</div>
@if (!empty($interestOptions))
<div>
<label for="interest" class="block text-sm font-medium text-foreground mb-2">{{ __('ui.immobilien_form.interest') }}</label>
<div class="grid grid-cols-1">
<select
wire:model="interest"
id="interest"
class="col-start-1 row-start-1 w-full appearance-none rounded-lg border border-border bg-background px-4 py-3 text-foreground focus:border-secondary focus:ring-1 focus:ring-secondary sm:text-sm/6">
<option value="">{{ __('ui.immobilien_form.please_select') }}</option>
@foreach ($interestOptions as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</select>
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"
class="pointer-events-none col-start-1 row-start-1 mr-3 size-4 self-center justify-self-end text-muted-foreground">
<path d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" fill-rule="evenodd" />
</svg>
</div>
@error('interest') <span class="text-red-500 text-xs mt-1 block">{{ $message }}</span> @enderror
</div>
@endif
<div class="grid sm:grid-cols-2 gap-4">
<div>
<label for="firstName" class="block text-sm font-medium text-foreground mb-2">{{ __('ui.immobilien_form.first_name') }}</label>
<input
wire:model="firstName"
id="firstName"
type="text"
placeholder="{{ __('ui.immobilien_form.first_name_placeholder') }}"
class="w-full rounded-lg border border-border bg-background px-4 py-3 text-foreground focus:border-secondary focus:ring-1 focus:ring-secondary sm:text-sm"
/>
@error('firstName') <span class="text-red-500 text-xs mt-1 block">{{ $message }}</span> @enderror
</div>
<div>
<label for="lastName" class="block text-sm font-medium text-foreground mb-2">{{ __('ui.immobilien_form.last_name') }}</label>
<input
wire:model="lastName"
id="lastName"
type="text"
placeholder="{{ __('ui.immobilien_form.last_name_placeholder') }}"
class="w-full rounded-lg border border-border bg-background px-4 py-3 text-foreground focus:border-secondary focus:ring-1 focus:ring-secondary sm:text-sm"
/>
@error('lastName') <span class="text-red-500 text-xs mt-1 block">{{ $message }}</span> @enderror
</div>
</div>
<div class="grid sm:grid-cols-2 gap-4">
<div>
<label for="email" class="block text-sm font-medium text-foreground mb-2">{{ __('ui.immobilien_form.email') }}</label>
<input
wire:model="email"
id="email"
type="email"
placeholder="{{ __('ui.immobilien_form.email_placeholder') }}"
class="w-full rounded-lg border border-border bg-background px-4 py-3 text-foreground focus:border-secondary focus:ring-1 focus:ring-secondary sm:text-sm"
/>
@error('email') <span class="text-red-500 text-xs mt-1 block">{{ $message }}</span> @enderror
</div>
<div>
<label for="phone" class="block text-sm font-medium text-foreground mb-2">
{{ __('ui.immobilien_form.phone') }}
</label>
<input
wire:model="phone"
id="phone"
type="tel"
placeholder="+49 ..."
class="w-full rounded-lg border border-border bg-background px-4 py-3 text-foreground focus:border-secondary focus:ring-1 focus:ring-secondary sm:text-sm"
/>
@error('phone') <span class="text-red-500 text-xs mt-1 block">{{ $message }}</span> @enderror
</div>
</div>
<div>
<label for="message" class="block text-sm font-medium text-foreground mb-2">
{{ __('ui.immobilien_form.message') }}
</label>
<textarea
wire:model="message"
id="message"
rows="3"
placeholder="{{ __('ui.immobilien_form.message_placeholder') }}"
class="w-full rounded-lg border border-border bg-background px-4 py-3 text-foreground focus:border-secondary focus:ring-1 focus:ring-secondary sm:text-sm resize-none"
></textarea>
@error('message') <span class="text-red-500 text-xs mt-1 block">{{ $message }}</span> @enderror
</div>
<div x-data="{ on: $wire.entangle('privacy') }">
<label class="flex items-start gap-3 cursor-pointer">
<input
x-model="on"
id="privacy_immo"
type="checkbox"
class="sr-only"
/>
<span
role="switch"
:aria-checked="on.toString()"
:class="on ? 'bg-green-500' : 'bg-muted'"
class="relative mt-0.5 w-11 h-6 shrink-0 rounded-full transition-colors duration-200"
>
<span
:class="on ? 'translate-x-5' : 'translate-x-0'"
class="absolute top-0.5 left-0.5 h-5 w-5 rounded-full bg-white shadow-sm transition-transform duration-200 pointer-events-none"
></span>
</span>
<span class="text-sm text-muted-foreground">
{{ __('ui.immobilien_form.privacy_prefix') }}
<a href="{{ route('privacy') }}" target="_blank" class="text-secondary underline underline-offset-2 hover:no-underline">Datenschutzerklärung</a>
{{ __('ui.immobilien_form.privacy_suffix') }}
</span>
</label>
@error('privacy') <span class="text-red-500 text-xs mt-1 block">{{ $message }}</span> @enderror
</div>
<button
type="submit"
wire:loading.attr="disabled"
class="w-full btn-primary-accent"
>
<span wire:loading.remove>{{ __('ui.immobilien_form.submit') }}</span>
<span wire:loading>{{ __('ui.immobilien_form.sending') }}</span>
</button>
</form>
@endif
</div>

View file

@ -15,7 +15,7 @@
<div class="card-elevated rounded-3xl overflow-hidden hover:scale-105 transition-all duration-300">
<div class="relative">
<img
src="{{ asset('img/assets/' . $member['image']) }}"
src="{{ theme_image_url($member['image']) }}"
alt="{{ $member['name'] }} - {{ $member['position'] }}"
class="w-full h-80 object-cover"
/>

View file

@ -38,16 +38,16 @@
<!-- Featured Image -->
<div class="mb-12 overflow-hidden rounded-lg shadow-md slide-up delay-200">
<img
src="{{ asset('img/assets/' . $article['image']) }}"
<x-web-picture
src="{{ theme_image_url($article['image']) }}"
alt="{{ $article['title'] }}"
class="w-full h-64 md:h-96 object-cover"
>
/>
</div>
<div class="grid md:grid-cols-4 gap-12">
<!-- Main Content -->
<div class="md:col-span-3">
<div class="md:col-span-4">
<div class="prose prose-lg max-w-none">
<p class="text-lg text-muted-foreground leading-relaxed mb-8 slide-up delay-200">
{{ $article['content']['intro'] }}
@ -67,7 +67,7 @@
</div>
<!-- Sidebar -->
<div class="md:col-span-1">
{{-- <div class="md:col-span-1">
<div class="sticky top-24">
<div class="card-elevated rounded-lg p-2 slide-left delay-400">
@ -93,7 +93,7 @@
</div>
</div>
</div>
</div>
</div> --}}
</div>
</div>
</article>

View file

@ -1,9 +1,9 @@
<section class="section-padding">
<div class="container-padding">
<div class="text-center mb-16">
<h2 class="text-hero mb-6">
<h1 class="text-hero mb-6">
{!! $content['title'] !!}
</h2>
</h1>
<p class="text-large text-muted-foreground mt-4 max-w-3xl mx-auto">
{{ $content['subtitle'] }}
</p>
@ -13,13 +13,13 @@
@foreach($this->posts as $post)
<article class="group">
<div class="card-elevated rounded-3xl overflow-hidden h-full transition-all duration-300">
<div class="flex flex-col md:flex-row">
<div class="relative md:w-3/4 aspect-[2/1] md:aspect-[2/1]">
<img
src="{{ asset('img/assets/' . $post['image']) }}"
<div class="flex flex-col lg:flex-row">
<div class="relative lg:w-3/4 aspect-[2/1] lg:aspect-[2/1]">
<x-web-picture
src="{{ theme_image_url($post['image']) }}"
alt="{{ $post['title'] }}"
class="w-full h-full object-cover"
>
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
</div>
@ -45,14 +45,14 @@
<div class="flex items-center justify-between mt-6">
<div class="flex items-center text-secondary font-medium group-hover:text-secondary-dark transition-colors duration-200">
<a href="/magazin/{{ $post['id'] }}" class="text-sm">{{ $content['read_more'] }}</a>
<svg class="w-4 h-4 ml-2 transform group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-4 h-4 ml-2 transform group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</div>
<div class="w-10 h-10 bg-primary rounded-full flex items-center justify-center group-hover:bg-secondary transition-colors duration-200">
<a href="/magazin/{{ $post['id'] }}">
<svg class="w-4 h-4 text-white transform group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<a href="/magazin/{{ $post['id'] }}" aria-label="{{ __('ui.read_article', ['title' => $post['title']]) }}">
<svg class="w-4 h-4 text-white transform group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</a>
@ -65,10 +65,10 @@
@endforeach
</div>
<div class="text-center mt-16">
{{-- <div class="text-center mt-16">
<a href="/magazin?page=2" class="btn-primary-accent">
{{ $content['load_more'] }}
</a>
</div>
</div> --}}
</div>
</section>

View file

@ -7,7 +7,7 @@
</div>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
<div class="grid md:grid-cols-2 lg:grid-cols-2 gap-8">
@foreach($content['timeline'] as $index => $card)
<div class="group {{ $index === 4 ? 'md:col-span-2 lg:col-span-1 lg:col-start-2' : '' }}">
<div class="card-elevated overflow-hidden group hover:shadow-elevated transition-all duration-300 flex flex-col h-full slide-up delay-{{ $index * 200 }}">

View file

@ -1,5 +1,6 @@
{{-- Hero Icons Helper Function --}}
@php
if (!function_exists('renderHeroIcon')) {
function renderHeroIcon($iconName, $style = 'outline') {
$iconPath = public_path("heroicons/optimized/24/{$style}/{$iconName}.svg");
$fallbackPath = public_path("heroicons/optimized/24/outline/sparkles.svg");
@ -18,8 +19,9 @@ function renderHeroIcon($iconName, $style = 'outline') {
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.563.563 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z"/>
</svg>';
}
}
@endphp
<section class="section-padding">
<section class="section-padding bg-accent">
<div class="container-padding">
<div class="text-center mb-16 slide-up delay-300">
<h2 class="text-section-title">{!! $content['title'] !!}</h2>

View file

@ -138,7 +138,7 @@
<div class="relative">
<div class="card-elevated p-0 overflow-hidden rounded-xl">
<img src="{{ asset('img/assets/' . $content['supplier']['highlight']['image']) }}" alt="{{ $content['supplier']['highlight']['alt'] }}"
<img src="{{ theme_image_url($content['supplier']['highlight']['image']) }}" alt="{{ $content['supplier']['highlight']['alt'] }}"
class="w-full h-48 object-cover" />
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent flex items-end">
<div class="p-6 text-white">

View file

@ -10,14 +10,7 @@
{{ $content['subtitle'] }}
</p>
<div class="grid md:grid-cols-3 gap-8 py-8 slide-up delay-400">
@foreach($content['stats'] as $stat)
<div class="text-center space-y-3">
<div class="text-4xl font-light text-secondary-foreground">{{ $stat['number'] }}</div>
<p class="text-secondary-foreground text-sm">{{ $stat['label'] }}</p>
</div>
@endforeach
</div>
<div class="spacing-content slide-up delay-500">
<a href="{{ $content['button_link'] }}" class="btn-primary-accent">

View file

@ -42,7 +42,7 @@
<div class="relative">
<div class="relative rounded-3xl overflow-hidden shadow-elevated slide-left delay-300">
<img src="{{ asset('img/assets/' . $content['image']) }}"
<img src="{{ theme_image_url($content['image']) }}"
alt="{{ $content['image_alt'] }}"
class="w-full h-[600px] object-cover" />
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>

View file

@ -49,7 +49,7 @@
<!-- Image Container -->
<div class="relative aspect-[4/3] overflow-hidden cursor-pointer"
wire:click="openModal({{ json_encode($project) }})">
<img src="{{ asset('img/assets/' . $project['image']) }}"
<img src="{{ theme_image_url($project['image']) }}"
alt="{{ $project['title'] }}"
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110">
@ -64,7 +64,7 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
<span class="font-medium"> Ansehen</span>
<span class="font-medium"> {{ __('ui.view') }}</span>
</div>
</button>
</div>
@ -128,8 +128,8 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
</svg>
</div>
<h3 class="text-lg font-medium text-foreground mb-2">Keine Projekte gefunden</h3>
<p class="text-muted-foreground">Versuchen Sie einen anderen Filter</p>
<h3 class="text-lg font-medium text-foreground mb-2">{{ __('ui.portfolio.no_projects') }}</h3>
<p class="text-muted-foreground">{{ __('ui.portfolio.try_other_filter') }}</p>
</div>
@endif
</div>
@ -175,7 +175,7 @@
<div class="h-full max-h-[90vh] overflow-y-auto">
<!-- Image -->
<div class="aspect-[16/10] relative overflow-hidden rounded-t-2xl">
<img src="{{ asset('img/assets/' . $selectedProject['image']) }}"
<img src="{{ theme_image_url($selectedProject['image']) }}"
alt="{{ $selectedProject['title'] }}"
class="w-full h-full object-cover">
@ -200,7 +200,7 @@
<!-- Features -->
<div class="mb-6">
<h4 class="font-semibold text-foreground mb-3">Ausstattung</h4>
<h4 class="font-semibold text-foreground mb-3">{{ __('ui.portfolio.amenities') }}</h4>
<div class="flex flex-wrap gap-2">
@foreach($selectedProject['features'] as $feature)
<span class="bg-muted text-muted-foreground px-3 py-1 rounded-full text-sm">
@ -213,7 +213,7 @@
<!-- Details Sidebar -->
<div class="lg:w-80 card-elevated rounded-lg p-6">
<h4 class="font-semibold text-foreground mb-4">Projektdetails</h4>
<h4 class="font-semibold text-foreground mb-4">{{ __('ui.portfolio.project_details') }}</h4>
<div class="space-y-4">
@if(isset($selectedProject['location']) && $selectedProject['location'] != '')
@ -223,7 +223,7 @@
<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>
<div>
<span class="text-sm text-muted-foreground">Standort</span>
<span class="text-sm text-muted-foreground">{{ __('ui.portfolio.location') }}</span>
<div class="font-medium text-foreground">{{ $selectedProject['location'] }}</div>
</div>
</div>
@ -235,7 +235,7 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"></path>
</svg>
<div>
<span class="text-sm text-muted-foreground">Preis</span>
<span class="text-sm text-muted-foreground">{{ __('ui.portfolio.price') }}</span>
<div class="font-bold text-lg text-secondary">{{ $selectedProject['price'] }}</div>
</div>
</div>
@ -247,7 +247,7 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4a1 1 0 011-1h4m0 0V4m0-1h6m0 1v3M4 8h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V8z"></path>
</svg>
<div>
<span class="text-sm text-muted-foreground">Größe</span>
<span class="text-sm text-muted-foreground">{{ __('ui.portfolio.size') }}</span>
<div class="font-medium text-foreground">{{ $selectedProject['size'] }}</div>
</div>
</div>
@ -256,7 +256,7 @@
<!-- CTA Button -->
<button class="btn-primary-accent w-full mt-6">
Kontakt aufnehmen
{{ __('ui.contact') }}
</button>
</div>
</div>

View file

@ -6,7 +6,7 @@
{{-- Image --}}
<div class="relative">
<div class="card-elevated rounded-3xl overflow-hidden slide-left delay-400">
<img src="{{ asset('img/assets/' . $content['image']) }}" alt="{{ $content['image_alt'] }}"
<img src="{{ theme_image_url($content['image']) }}" alt="{{ $content['image_alt'] }}"
class="w-full h-full object-cover" />
<div

View file

@ -3,12 +3,12 @@
<div class="grid lg:grid-cols-2 gap-16 items-center">
<div class="space-y-8">
<h1 class="text-5xl lg:text-6xl font-light text-foreground">
Über <span class="text-secondary">B2In</span>
Über <span class="text-secondary">B2in</span>
</h1>
<blockquote class="text-xl lg:text-2xl text-muted-foreground italic leading-relaxed border-l-4 border-secondary pl-6">
"Unsere Vision ist es, Unternehmen durch innovative Konnektivitätslösungen zu verbinden
und nachhaltiges Wachstum in der digitalen Welt zu ermöglichen. Bei B2In schaffen wir
und nachhaltiges Wachstum in der digitalen Welt zu ermöglichen. Bei B2in schaffen wir
nicht nur Verbindungen wir bauen Brücken in die Zukunft."
</blockquote>
@ -16,7 +16,7 @@
<div class="w-16 h-px bg-secondary"></div>
<div>
<p class="font-semibold text-foreground">Marcel Scheibe</p>
<p class="text-sm text-muted-foreground">Gründer & CEO, B2In</p>
<p class="text-sm text-muted-foreground">Gründer & CEO, B2in</p>
</div>
</div>
</div>
@ -25,9 +25,8 @@
<div class="card-elevated rounded-3xl overflow-hidden">
<img
src="{{ asset('img/assets/marcel-scheibe.jpg') }}"
alt="Marcel Scheibe, Gründer und CEO von B2In"
class="w-full h-96 lg:h-[500px] object-cover"
/>
alt="Marcel Scheibe, Gründer und CEO von B2in"
class="w-full h-96 lg:h-[500px] object-cover" />
</div>
<div class="absolute -bottom-6 -right-6 bg-secondary text-secondary-foreground p-6 rounded-2xl">
<div class="text-3xl font-bold">2019</div>
@ -36,4 +35,4 @@
</div>
</div>
</div>
</section>
</section>

View file

@ -3,7 +3,7 @@
<div class="grid lg:grid-cols-2 gap-16 items-center">
<div class="space-y-8">
<h1 class="text-5xl lg:text-7xl font-light text-foreground">
B2In <span class="text-primary">Ecosystem</span>
B2in <span class="text-primary">Ecosystem</span>
</h1>
<p class="text-xl lg:text-2xl text-muted-foreground leading-relaxed">
@ -14,39 +14,39 @@
<div class="grid grid-cols-2 gap-6">
@foreach ($this->features as $feature)
<div class="flex items-center gap-3">
<div class="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center">
@if ($feature['icon'] === 'users')
<svg class="w-6 h-6 text-primary" 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.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
@elseif($feature['icon'] === 'building-2')
<svg class="w-6 h-6 text-primary" 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" />
</svg>
@elseif($feature['icon'] === 'network')
<svg class="w-6 h-6 text-primary" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.367 2.684 3 3 0 00-5.367-2.684z" />
</svg>
@else
<svg class="w-6 h-6 text-primary" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
@endif
</div>
<div>
<p class="font-semibold text-foreground">{{ $feature['title'] }}</p>
<p class="text-sm text-muted-foreground">{{ $feature['description'] }}</p>
</div>
<div class="flex items-center gap-3">
<div class="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center">
@if ($feature['icon'] === 'users')
<svg class="w-6 h-6 text-primary" 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.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
@elseif($feature['icon'] === 'building-2')
<svg class="w-6 h-6 text-primary" 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" />
</svg>
@elseif($feature['icon'] === 'network')
<svg class="w-6 h-6 text-primary" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.367 2.684 3 3 0 00-5.367-2.684z" />
</svg>
@else
<svg class="w-6 h-6 text-primary" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
@endif
</div>
<div>
<p class="font-semibold text-foreground">{{ $feature['title'] }}</p>
<p class="text-sm text-muted-foreground">{{ $feature['description'] }}</p>
</div>
</div>
@endforeach
</div>
</div>
@ -64,7 +64,7 @@
d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.367 2.684 3 3 0 00-5.367-2.684z" />
</svg>
</div>
<h3 class="text-xl font-semibold text-foreground">B2In Portal</h3>
<h3 class="text-xl font-semibold text-foreground">B2in Portal</h3>
<p class="text-sm text-muted-foreground">Zentrale Plattform</p>
</div>

View file

@ -2,7 +2,7 @@
<div class="max-w-7xl mx-auto">
<div class="text-center mb-16">
<h2 class="text-4xl lg:text-5xl font-light text-foreground mb-6">
B2In <span class="text-secondary">Magazin</span>
B2in <span class="text-secondary">Magazin</span>
</h2>
<p class="text-muted-foreground text-lg max-w-2xl mx-auto">
Entdecken Sie die neuesten Trends, Insights und Geschichten aus der Welt
@ -12,57 +12,56 @@
<div class="space-y-8">
@foreach($this->posts as $post)
<article class="group">
<div class="card-elevated rounded-3xl overflow-hidden h-full hover:scale-[1.02] transition-all duration-300">
<div class="flex flex-col md:flex-row">
<div class="relative md:w-3/4 aspect-[2/1] md:aspect-[2/1]">
<img
src="{{ asset('images/' . $post['image']) }}"
alt="{{ $post['title'] }}"
class="w-full h-full object-cover"
>
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
</div>
<article class="group">
<div class="card-elevated rounded-3xl overflow-hidden h-full hover:scale-[1.02] transition-all duration-300">
<div class="flex flex-col md:flex-row">
<div class="relative md:w-3/4 aspect-[2/1] md:aspect-[2/1]">
<img
src="{{ asset('images/' . $post['image']) }}"
alt="{{ $post['title'] }}"
class="w-full h-full object-cover">
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
</div>
<div class="lg:w-2/3 p-6 lg:p-8 flex flex-col justify-between">
<div class="space-y-4">
<div class="flex items-center gap-4 text-sm text-muted-foreground">
<time>{{ $post['date'] }}</time>
<span class="w-1 h-1 bg-muted-foreground rounded-full"></span>
<span>{{ $post['readTime'] }}</span>
</div>
<h3 class="text-xl lg:text-2xl font-semibold text-foreground leading-tight group-hover:text-secondary transition-colors duration-200">
<a href="/magazin/{{ $post['id'] }}" class="stretched-link">
{{ $post['title'] }}
</a>
</h3>
<p class="text-muted-foreground leading-relaxed text-base lg:text-lg">
{{ $post['excerpt'] }}
</p>
<div class="lg:w-2/3 p-6 lg:p-8 flex flex-col justify-between">
<div class="space-y-4">
<div class="flex items-center gap-4 text-sm text-muted-foreground">
<time>{{ $post['date'] }}</time>
<span class="w-1 h-1 bg-muted-foreground rounded-full"></span>
<span>{{ $post['readTime'] }}</span>
</div>
<div class="flex items-center justify-between mt-6">
<div class="flex items-center text-secondary font-medium group-hover:text-secondary-dark transition-colors duration-200">
<a href="/magazin/{{ $post['id'] }}" class="text-sm">Weiterlesen</a>
<svg class="w-4 h-4 ml-2 transform group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</div>
<h3 class="text-xl lg:text-2xl font-semibold text-foreground leading-tight group-hover:text-secondary transition-colors duration-200">
<a href="/magazin/{{ $post['id'] }}" class="stretched-link">
{{ $post['title'] }}
</a>
</h3>
<div class="w-10 h-10 bg-primary rounded-full flex items-center justify-center group-hover:bg-secondary transition-colors duration-200">
<a href="/magazin/{{ $post['id'] }}">
<p class="text-muted-foreground leading-relaxed text-base lg:text-lg">
{{ $post['excerpt'] }}
</p>
</div>
<div class="flex items-center justify-between mt-6">
<div class="flex items-center text-secondary font-medium group-hover:text-secondary-dark transition-colors duration-200">
<a href="/magazin/{{ $post['id'] }}" class="text-sm">Weiterlesen</a>
<svg class="w-4 h-4 ml-2 transform group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</div>
<div class="w-10 h-10 bg-primary rounded-full flex items-center justify-center group-hover:bg-secondary transition-colors duration-200">
<a href="/magazin/{{ $post['id'] }}">
<svg class="w-4 h-4 text-white transform group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</a>
</div>
</a>
</div>
</div>
</div>
</div>
</article>
</div>
</article>
@endforeach
</div>

View file

@ -6,21 +6,21 @@
<div class="grid md:grid-cols-3 gap-8 mb-16">
@foreach($this->timeline as $item)
<div class="spacing-small">
<div class="w-12 h-12 mx-auto bg-secondary/20 rounded-full flex items-center justify-center">
<div class="w-6 h-6 bg-secondary rounded-full"></div>
</div>
<h3 class="text-xl font-semibold text-[hsl(var(--dark-text))]">{{ $item['title'] }}</h3>
<p class="text-dark-muted text-sm leading-relaxed">
{{ $item['description'] }}
</p>
<div class="spacing-small">
<div class="w-12 h-12 mx-auto bg-secondary/20 rounded-full flex items-center justify-center">
<div class="w-6 h-6 bg-secondary rounded-full"></div>
</div>
<h3 class="text-xl font-semibold text-[hsl(var(--dark-text))]">{{ $item['title'] }}</h3>
<p class="text-dark-muted text-sm leading-relaxed">
{{ $item['description'] }}
</p>
</div>
@endforeach
</div>
<p class="text-large text-dark-muted leading-relaxed max-w-3xl mx-auto">
Was als Vision begann, traditionelle Geschäftsprozesse zu revolutionieren, ist heute eine
bewährte Plattform für digitale Innovation. B2In schließt die Lücke zwischen
bewährte Plattform für digitale Innovation. B2in schließt die Lücke zwischen
traditionellen Unternehmen und modernen, digitalen Lösungen durch maßgeschneiderte
Konnektivitätsservices, die Effizienz steigern und nachhaltiges Wachstum fördern.
</p>

View file

@ -5,7 +5,7 @@
Warum Partner werden?
</h2>
<p class="text-muted-foreground text-lg max-w-3xl mx-auto">
Entdecken Sie die Vorteile einer Partnerschaft mit B2In und
Entdecken Sie die Vorteile einer Partnerschaft mit B2in und
wie Sie von unserem innovativen Ecosystem profitieren können.
</p>
</div>
@ -29,41 +29,41 @@
<div class="space-y-6">
@foreach ($this->brokerBenefits as $index => $benefit)
<div class="card-elevated p-6 rounded-xl">
<div class="flex gap-4">
<div
class="flex-shrink-0 w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center">
@if ($benefit['icon'] === 'trending-up')
<svg class="w-6 h-6 text-primary" 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>
@elseif($benefit['icon'] === 'target')
<svg class="w-6 h-6 text-primary" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
@else
<svg class="w-6 h-6 text-primary" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z">
</path>
</svg>
@endif
</div>
<div>
<h4 class="text-lg font-semibold text-foreground mb-2">
{{ $benefit['title'] }}
</h4>
<p class="text-muted-foreground">
{{ $benefit['description'] }}
</p>
</div>
<div class="card-elevated p-6 rounded-xl">
<div class="flex gap-4">
<div
class="flex-shrink-0 w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center">
@if ($benefit['icon'] === 'trending-up')
<svg class="w-6 h-6 text-primary" 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>
@elseif($benefit['icon'] === 'target')
<svg class="w-6 h-6 text-primary" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
@else
<svg class="w-6 h-6 text-primary" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z">
</path>
</svg>
@endif
</div>
<div>
<h4 class="text-lg font-semibold text-foreground mb-2">
{{ $benefit['title'] }}
</h4>
<p class="text-muted-foreground">
{{ $benefit['description'] }}
</p>
</div>
</div>
</div>
@endforeach
</div>
@ -96,45 +96,45 @@
<div class="space-y-6">
@foreach ($this->supplierBenefits as $index => $benefit)
<div class="card-elevated p-6 rounded-xl">
<div class="flex gap-4">
<div
class="flex-shrink-0 w-12 h-12 rounded-xl bg-accent/20 flex items-center justify-center">
@if ($benefit['icon'] === 'globe')
<svg class="w-6 h-6 text-accent-foreground" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z">
</path>
</svg>
@elseif($benefit['icon'] === 'handshake')
<svg class="w-6 h-6 text-accent-foreground" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.367 2.684 3 3 0 00-5.367-2.684z">
</path>
</svg>
@else
<svg class="w-6 h-6 text-accent-foreground" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z">
</path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
@endif
</div>
<div>
<h4 class="text-lg font-semibold text-foreground mb-2">
{{ $benefit['title'] }}
</h4>
<p class="text-muted-foreground">
{{ $benefit['description'] }}
</p>
</div>
<div class="card-elevated p-6 rounded-xl">
<div class="flex gap-4">
<div
class="flex-shrink-0 w-12 h-12 rounded-xl bg-accent/20 flex items-center justify-center">
@if ($benefit['icon'] === 'globe')
<svg class="w-6 h-6 text-accent-foreground" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z">
</path>
</svg>
@elseif($benefit['icon'] === 'handshake')
<svg class="w-6 h-6 text-accent-foreground" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.367 2.684 3 3 0 00-5.367-2.684z">
</path>
</svg>
@else
<svg class="w-6 h-6 text-accent-foreground" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z">
</path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
@endif
</div>
<div>
<h4 class="text-lg font-semibold text-foreground mb-2">
{{ $benefit['title'] }}
</h4>
<p class="text-muted-foreground">
{{ $benefit['description'] }}
</p>
</div>
</div>
</div>
@endforeach
</div>

View file

@ -9,28 +9,28 @@
<div class="w-16 h-px bg-secondary mx-auto"></div>
<p class="text-large text-dark-muted leading-relaxed max-w-2xl mx-auto">
Werden Sie Teil des B2In-Partnernetzwerks und erschließen Sie neue
Werden Sie Teil des B2in-Partnernetzwerks und erschließen Sie neue
Geschäftsmöglichkeiten durch innovative Konnektivitätslösungen.
</p>
<div class="grid md:grid-cols-3 gap-8 py-8">
@foreach($this->stats as $stat)
<div class="text-center space-y-3">
<div class="text-4xl font-light text-secondary">{{ $stat['number'] }}</div>
<p class="text-dark-muted text-sm">{{ $stat['label'] }}</p>
</div>
<div class="text-center space-y-3">
<div class="text-4xl font-light text-secondary">{{ $stat['number'] }}</div>
<p class="text-dark-muted text-sm">{{ $stat['label'] }}</p>
</div>
@endforeach
</div>
<div class="spacing-content">
<a href="/contact" class="btn-accent px-12 py-6 rounded-2xl text-lg">
Werden Sie B2In Partner
Werden Sie B2in Partner
</a>
<p class="text-dark-muted text-sm">
Entdecken Sie die Vorteile einer strategischen Partnerschaft mit B2In
Entdecken Sie die Vorteile einer strategischen Partnerschaft mit B2in
</p>
</div>
</div>
</div>
</section>
</section>

View file

@ -4,50 +4,50 @@
<div class="space-y-8">
<h1 class="text-hero">
Wachsen Sie mit uns.<br />
Werden Sie <span class="text-secondary">B2In Partner</span>.
Werden Sie <span class="text-secondary">B2in Partner</span>.
</h1>
<p class="text-lg text-muted-foreground max-w-md leading-relaxed">
Werden Sie Teil des B2In Ecosystems und profitieren Sie von innovativen
Werden Sie Teil des B2in Ecosystems und profitieren Sie von innovativen
Geschäftsmodellen, die nachhaltiges Wachstum und langfristigen Erfolg ermöglichen.
Gemeinsam gestalten wir die Zukunft der Immobilienbranche.
</p>
<div class="grid grid-cols-2 gap-6">
@foreach ($this->partnerTypes as $partner)
<div class="flex items-center gap-3">
<div class="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center">
@if ($partner['icon'] === 'trending-up')
<svg class="w-6 h-6 text-primary" 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" />
</svg>
@elseif($partner['icon'] === 'globe')
<svg class="w-6 h-6 text-primary" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
@elseif($partner['icon'] === 'handshake')
<svg class="w-6 h-6 text-primary" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z" />
</svg>
@else
<svg class="w-6 h-6 text-primary" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
</svg>
@endif
</div>
<div>
<p class="font-semibold text-foreground">{{ $partner['title'] }}</p>
<p class="text-sm text-muted-foreground">{{ $partner['description'] }}</p>
</div>
<div class="flex items-center gap-3">
<div class="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center">
@if ($partner['icon'] === 'trending-up')
<svg class="w-6 h-6 text-primary" 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" />
</svg>
@elseif($partner['icon'] === 'globe')
<svg class="w-6 h-6 text-primary" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
@elseif($partner['icon'] === 'handshake')
<svg class="w-6 h-6 text-primary" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z" />
</svg>
@else
<svg class="w-6 h-6 text-primary" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
</svg>
@endif
</div>
<div>
<p class="font-semibold text-foreground">{{ $partner['title'] }}</p>
<p class="text-sm text-muted-foreground">{{ $partner['description'] }}</p>
</div>
</div>
@endforeach
</div>
</div>

View file

@ -5,70 +5,70 @@
So werden Sie <span class="text-primary">Partner</span>
</h2>
<p class="text-muted-foreground text-lg max-w-3xl mx-auto">
In nur drei einfachen Schritten werden Sie Teil des B2In Ecosystems
In nur drei einfachen Schritten werden Sie Teil des B2in Ecosystems
und können von allen Vorteilen unserer Partnerschaft profitieren.
</p>
</div>
<div class="grid md:grid-cols-3 gap-8 mb-16">
@foreach ($this->steps as $index => $step)
<div class="card-elevated p-0 overflow-hidden group hover:shadow-elevated transition-all duration-300">
<div class="relative overflow-hidden">
<img src="{{ asset('img/assets/' . $step['image']) }}" alt="{{ $step['title'] }}"
class="w-full h-64 object-cover group-hover:scale-105 transition-transform duration-300" />
<div
class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
</div>
{{-- Step Number Badge --}}
<div
class="absolute top-4 left-4 w-12 h-12 rounded-full bg-primary text-white flex items-center justify-center font-bold text-lg">
{{ $step['step'] }}
</div>
<div class="card-elevated p-0 overflow-hidden group hover:shadow-elevated transition-all duration-300">
<div class="relative overflow-hidden">
<img src="{{ asset('img/assets/' . $step['image']) }}" alt="{{ $step['title'] }}"
class="w-full h-64 object-cover group-hover:scale-105 transition-transform duration-300" />
<div
class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
</div>
<div class="p-8">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
@if ($step['icon'] === 'file-text')
<svg class="w-5 h-5 text-primary" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z">
</path>
</svg>
@elseif($step['icon'] === 'search')
<svg class="w-5 h-5 text-primary" 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>
@else
<svg class="w-5 h-5 text-primary" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
@endif
</div>
<h3 class="text-2xl font-medium text-foreground">
{{ $step['title'] }}
</h3>
</div>
<p class="text-muted-foreground leading-relaxed mb-6">
{{ $step['description'] }}
</p>
@if ($index === count($steps) - 1)
<a href="/contact">
<button class="btn-secondary w-full">
Jetzt starten
</button>
</a>
@endif
{{-- Step Number Badge --}}
<div
class="absolute top-4 left-4 w-12 h-12 rounded-full bg-primary text-white flex items-center justify-center font-bold text-lg">
{{ $step['step'] }}
</div>
</div>
<div class="p-8">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
@if ($step['icon'] === 'file-text')
<svg class="w-5 h-5 text-primary" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z">
</path>
</svg>
@elseif($step['icon'] === 'search')
<svg class="w-5 h-5 text-primary" 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>
@else
<svg class="w-5 h-5 text-primary" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
@endif
</div>
<h3 class="text-2xl font-medium text-foreground">
{{ $step['title'] }}
</h3>
</div>
<p class="text-muted-foreground leading-relaxed mb-6">
{{ $step['description'] }}
</p>
@if ($index === count($steps) - 1)
<a href="/contact">
<button class="btn-secondary w-full">
Jetzt starten
</button>
</a>
@endif
</div>
</div>
@endforeach
</div>
@ -79,7 +79,7 @@
Bereit für den nächsten <span class="text-primary">Schritt</span>?
</h3>
<p class="text-muted-foreground text-lg mb-8 max-w-2xl mx-auto">
Werden Sie noch heute Teil des B2In Ecosystems und profitieren Sie
Werden Sie noch heute Teil des B2in Ecosystems und profitieren Sie
von innovativen Geschäftsmodellen und nachhaltigen Erfolgsstrategien.
</p>
<a href="/contact">

View file

@ -0,0 +1,42 @@
@if (!empty($content) && ($content['enabled'] ?? false))
<div x-data="{ dismissed: localStorage.getItem('announcement_dismissed_{{ $content['id'] ?? 'default' }}') === 'true' }"
x-show="!dismissed"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 max-h-16"
x-transition:leave-end="opacity-0 max-h-0"
class="bg-secondary text-secondary-foreground overflow-hidden"
id="topbar">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="relative flex items-center justify-center gap-x-3 gap-y-1 flex-wrap min-h-10 py-2 pr-8 text-sm">
@if (isset($content['badge']))
<span class="hidden sm:inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider bg-white/20 shrink-0">
{{ $content['badge'] }}
</span>
@endif
<span class="font-medium text-center leading-snug">
{{ $content['text'] ?? '' }}
</span>
@if (isset($content['link_text']) && isset($content['link_url']))
<a href="{{ $content['link_url'] }}"
class="inline-flex items-center gap-1 font-semibold underline underline-offset-2 decoration-white/50 hover:decoration-white transition-colors shrink-0">
{{ $content['link_text'] }}
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
</svg>
</a>
@endif
<button
@click="dismissed = true; localStorage.setItem('announcement_dismissed_{{ $content['id'] ?? 'default' }}', 'true')"
class="absolute right-0 top-1/2 -translate-y-1/2 p-1.5 text-white/60 hover:text-white transition-colors"
aria-label="{{ __('ui.announcement_close') }}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
@endif

View file

@ -4,7 +4,7 @@
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20 bg-[hsl(var(--hero-container))] rounded-[20px] w-[95%]">
<div class="grid lg:grid-cols-2 gap-12 items-center">
<!-- Left Side - Hero Text -->
<div class="slide-right delay-200">
<div class="">
<h1 class="text-hero mb-6 tracking-wide">
{!! $content['hero']['title'] ?? 'Send us a<br /><span class="text-secondary font-medium">message.</span>' !!}
</h1>
@ -14,18 +14,27 @@
</div>
<!-- Right Side - Contact Form -->
<div class="card-elevated p-8 slide-left delay-200">
@if (session()->has('message'))
<div class="mb-6 p-4 bg-gray-50 border border-secondary/20 rounded-lg text-secondary">
{{ session('message') }}
<div class="card-elevated p-8 ">
@if ($success)
<div class="flex flex-col items-center text-center py-8 gap-5">
<div class="flex items-center justify-center w-20 h-20 rounded-full bg-green-100">
<svg class="w-10 h-10 text-green-600" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
</div>
<div>
<p class="text-2xl font-semibold text-foreground">{{ __('ui.contact_form.success_title') }}</p>
<p class="text-muted-foreground mt-2 max-w-sm mx-auto">
{{ $content['form']['success_message'] ?? __('ui.contact_form.success_message') }}
</p>
</div>
</div>
@endif
@else
<form wire:submit="submit" class="space-y-6">
<div class="grid md:grid-cols-2 gap-4">
<div>
<label for="firstName" class="text-foreground mb-2 block text-sm font-medium">
{{ $content['form']['labels']['first_name'] ?? 'First name *' }}
{{ $content['form']['labels']['first_name'] ?? __('ui.contact_form.first_name') }}
</label>
<input
wire:model="firstName"
@ -38,7 +47,7 @@
</div>
<div>
<label for="lastName" class="text-foreground mb-2 block text-sm font-medium">
{{ $content['form']['labels']['last_name'] ?? 'Last name *' }}
{{ $content['form']['labels']['last_name'] ?? __('ui.contact_form.last_name') }}
</label>
<input
wire:model="lastName"
@ -53,7 +62,7 @@
<div>
<label for="company" class="text-foreground mb-2 block text-sm font-medium">
{{ $content['form']['labels']['company'] ?? 'Company' }}
{{ $content['form']['labels']['company'] ?? __('ui.contact_form.company') }}
</label>
<input
wire:model="company"
@ -67,7 +76,7 @@
<div class="grid md:grid-cols-2 gap-4">
<div>
<label for="email" class="text-foreground mb-2 block text-sm font-medium">
{{ $content['form']['labels']['email'] ?? 'Email *' }}
{{ $content['form']['labels']['email'] ?? __('ui.contact_form.email') }}
</label>
<input
wire:model="email"
@ -80,7 +89,7 @@
</div>
<div>
<label for="phone" class="text-foreground mb-2 block text-sm font-medium">
{{ $content['form']['labels']['phone'] ?? 'Phone' }}
{{ $content['form']['labels']['phone'] ?? __('ui.contact_form.phone') }}
</label>
<input
wire:model="phone"
@ -94,7 +103,7 @@
<div>
<label for="subject" class="text-foreground mb-2 block text-sm font-medium">
{{ $content['form']['labels']['subject'] ?? 'Subject *' }}
{{ $content['form']['labels']['subject'] ?? __('ui.contact_form.subject') }}
</label>
<div class="grid grid-cols-1">
<select
@ -116,34 +125,70 @@
<div>
<label for="message" class="text-foreground mb-2 block text-sm font-medium">
{{ $content['form']['labels']['message'] ?? 'Message *' }}
{{ $content['form']['labels']['message'] ?? __('ui.contact_form.message') }}
</label>
<textarea
wire:model="message"
id="message"
rows="5"
class="w-full px-3 py-2 bg-gray-50 border border-border rounded-lg outline-1 -outline-offset-1 outline-gray-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-gray-600 sm:text-sm/6 resize-none"
placeholder="{{ $content['form']['placeholders']['message'] ?? 'Ihre Nachricht...' }}"
placeholder="{{ $content['form']['placeholders']['message'] ?? __('ui.contact_form.message_placeholder') }}"
required
></textarea>
@error('message') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
</div>
{{-- Honeypot --}}
<div class="opacity-0 absolute top-0 left-0 h-0 w-0 -z-10 overflow-hidden" aria-hidden="true">
<label for="website">Website</label>
<input type="text" id="website" name="website" wire:model="website" tabindex="-1" autocomplete="off">
</div>
<div x-data="{ on: $wire.entangle('privacy') }">
<label class="flex items-start gap-3 cursor-pointer">
<input
x-model="on"
id="privacy"
type="checkbox"
class="sr-only"
/>
<span
role="switch"
:aria-checked="on.toString()"
:class="on ? 'bg-green-500' : 'bg-muted'"
class="relative mt-0.5 w-11 h-6 shrink-0 rounded-full transition-colors duration-200"
>
<span
:class="on ? 'translate-x-5' : 'translate-x-0'"
class="absolute top-0.5 left-0.5 h-5 w-5 rounded-full bg-white shadow-sm transition-transform duration-200 pointer-events-none"
></span>
</span>
<span class="text-sm text-muted-foreground">
{{ __('ui.contact_form.privacy_prefix') }}
<a href="{{ route('privacy') }}" target="_blank" class="text-secondary underline underline-offset-2 hover:no-underline">Datenschutzerklärung</a>
{{ __('ui.contact_form.privacy_suffix') }}
</span>
</label>
@error('privacy') <span class="text-red-500 text-xs mt-1 block">{{ $message }}</span> @enderror
</div>
<button
type="submit"
class="w-full btn-primary flex items-center justify-center"
wire:loading.attr="disabled"
>
<div class="flex items-center justify-center">
<span wire:loading.remove>{{ $content['form']['button_text'] ?? 'Senden' }}</span>
<span wire:loading>{{ $content['form']['button_loading'] ?? 'Wird gesendet...' }}</span>
<span wire:loading.remove>{{ $content['form']['button_text'] ?? __('ui.contact_form.send') }}</span>
<span wire:loading>{{ $content['form']['button_loading'] ?? __('ui.contact_form.sending') }}</span>
<svg wire:loading.remove class="ml-2 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"/>
</svg>
</div>
</button>
<p class="text-sm text-muted-foreground">{{ __('ui.required_fields') }}</p>
</form>
@endif
</div>
</div>
</div>
@ -172,7 +217,7 @@
@endif
</div>
<h3 class="text-xl mb-2 text-foreground">{{ $info['title'] }}</h3>
<div class="text-muted-foreground text-sm">
<div class="text-muted-foreground text-base">
@foreach($info['info'] as $line)
<p>{{ $line }}</p>
@endforeach
@ -184,7 +229,7 @@
</section>
<!-- Social Media Section -->
<section class="section-padding bg-accent">
{{-- <section class="section-padding bg-accent">
<div class="container-padding">
<div class="grid lg:grid-cols-2 gap-12 items-center">
<div class="slide-right delay-200">
@ -212,4 +257,5 @@
</div>
</div>
</section>
--}}
</div>

View file

@ -5,14 +5,16 @@
<div class="spacing-section">
<div class="flex items-center justify-center">
<img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPath('negative')) }}"
alt="{{ $domainName ?? 'B2In' }} Logo" class="h-14 w-auto" />
alt="{{ $domainName ?? 'B2in' }} Logo" class="h-14 w-auto"
width="120" height="56"
loading="lazy" />
</div>
<div class="container-narrow spacing-content">
<h2 class="text-section-title text-dark-text leading-tight">
Connecting Design and <span class="text-secondary">Property</span>
</h2>
<p class="text-dark-muted text-sm mt-4">Marcel Scheibe {{ __('ui.founder_ceo') }}</p>
</div>
<hr class="border-t border-dark-muted/30 mt-12 pt-4">
@ -21,21 +23,19 @@
{{-- Links --}}
<div class="grid grid-cols-2 md:grid-cols-4 gap-8 text-left max-w-4xl mx-auto">
<div class="spacing-small text-center">
<a href="#" class="block hover-text-secondary transition-colors">Privacy Policy</a>
<a href="{{ route('privacy') }}" class="block hover-text-secondary transition-colors">Privacy Policy</a>
</div>
<div class="spacing-small text-center">
<a href="#" class="block hover-text-secondary transition-colors">Terms of Service</a>
<a href="{{ route('terms') }}" class="block hover-text-secondary transition-colors">Terms of Service</a>
</div>
<div class="spacing-small text-center">
<a href="#" class="block hover-text-secondary transition-colors">Cookie Policy</a>
<a href="{{ route('cookie-policy') }}" class="block hover-text-secondary transition-colors">Cookie Policy</a>
</div>
<div class="spacing-small text-center">
<a href="#" class="block hover-text-secondary transition-colors">Impressum</a>
<a href="{{ route('impressum') }}" class="block hover-text-secondary transition-colors">{{ __('ui.legal_notice') }}</a>
</div>
</div>
</div>
@ -43,25 +43,18 @@
{{-- Bottom Bar --}}
<div class="border-t border-dark-muted/30 mt-12 pt-8">
<div class="flex flex-col md:flex-row justify-between items-center spacing-small md:space-y-0">
<div class="text-dark-muted text-sm">
© {{ date('Y') }} B2In. All rights reserved.
<div class="text-dark-muted text-sm flex flex-col sm:flex-row items-center gap-2 sm:gap-4">
<span>© {{ date('Y') }} B2in. All rights reserved.</span>
<a href="#" onclick="window.dispatchEvent(new CustomEvent('open-cookie-settings')); return false;"
class="hover-text-secondary transition-colors">
{{ __('ui.cookie_settings') }}
</a>
</div>
<div class="flex items-center space-x-6 text-dark-muted text-sm">
<div class="flex space-x-4">
<a href="#" class="hover-text-secondary transition-colors" aria-label="Facebook">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path
d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
</svg>
</a>
<a href="#" class="hover-text-secondary transition-colors" aria-label="Instagram">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path
d="M12.017 0C5.396 0 .029 5.367.029 11.987c0 6.62 5.367 11.987 11.988 11.987s11.987-5.367 11.987-11.987C24.004 5.367 18.637.001 12.017.001zM8.449 16.988c-1.297 0-2.448-.49-3.323-1.297C4.198 14.895 3.708 13.744 3.708 12.447s.49-2.448 1.418-3.323c.875-.807 2.026-1.297 3.323-1.297s2.448.49 3.323 1.297c.928.875 1.418 2.026 1.418 3.323s-.49 2.448-1.418 3.244c-.875.807-2.026 1.297-3.323 1.297zm7.83-9.281c-.49 0-.928-.175-1.297-.49-.368-.315-.49-.753-.49-1.243s.122-.928.49-1.243c.369-.315.807-.49 1.297-.49s.928.175 1.297.49c.368.315.49.753.49 1.243s-.122.928-.49 1.243c-.369.315-.807.49-1.297.49z" />
</svg>
</a>
<a href="#" class="hover-text-secondary transition-colors" aria-label="LinkedIn">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<a href="https://www.linkedin.com/in/marcel-scheibe/" target="_blank" class="hover-text-secondary transition-colors" aria-label="LinkedIn">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path
d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
</svg>

View file

@ -6,7 +6,7 @@
alt="{{ $domainName ?? 'B2IN' }} Logo" class="h-10 w-auto" />
</a>
<nav class="hidden md:flex items-center space-x-8">
<nav class="hidden md:flex items-center space-x-8" aria-label="{{ __('ui.main_navigation') }}">
@if(isset($content['navigation']) && is_array($content['navigation']))
@foreach ($content['navigation'] as $navItem)
<a href="{{ $navItem['url'] }}"
@ -21,19 +21,38 @@
</nav>
<div class="flex items-center space-x-4">
<a href="{{ config('domains.domain_portal_url') }}"
class=" md:block rounded-md px-3 py-2 text-sm font-medium bg-secondary hover-bg-primary glow-medium text-white transition-all duration-200">
{{ $content['portal_login'] ?? 'Portal Login' }}
</a>
{{-- Language Switcher --}}
<div class="hidden md:flex items-center space-x-1 text-sm" role="group" aria-label="Language">
@foreach ($availableLocales as $locale => $label)
<button wire:click="switchLanguage('{{ $locale }}')"
class="px-2 py-1 rounded transition-colors duration-200
{{ $currentLocale === $locale
? 'text-secondary font-semibold'
: 'text-muted-foreground hover:text-foreground' }}"
@if($currentLocale === $locale) aria-current="true" @endif>
{{ $label }}
</button>
@if (!$loop->last)
<span class="text-border">|</span>
@endif
@endforeach
</div>
<a href="{{ route('contact') }}"
class="hidden md:block rounded-md px-5 py-2.5 text-sm font-medium bg-secondary hover-bg-primary glow-medium text-white transition-all duration-200">
{{ __('ui.contact') }}
</a>
<button wire:click="toggleMobileMenu"
class="md:hidden w-5 h-5 text-muted-foreground hover:text-foreground transition-colors">
class="md:hidden w-5 h-5 text-muted-foreground hover:text-foreground transition-colors"
aria-label="{{ $this->isMobileMenuOpen ? __('ui.menu_close') : __('ui.menu_open') }}"
aria-expanded="{{ $this->isMobileMenuOpen ? 'true' : 'false' }}">
@if ($this->isMobileMenuOpen)
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
</svg>
@else
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
@ -45,7 +64,7 @@
{{-- Mobile Menu --}}
@if ($this->isMobileMenuOpen)
<div class="md:hidden border-t border-border bg-background/95 backdrop-blur-sm">
<nav class="px-4 py-6 space-y-4">
<nav class="px-4 py-6 space-y-4" aria-label="{{ __('ui.mobile_navigation') }}">
@if(isset($content['navigation']) && is_array($content['navigation']))
@foreach ($content['navigation'] as $navItem)
<a href="{{ $navItem['url'] }}"
@ -58,10 +77,27 @@
</a>
@endforeach
@endif
<div class="pt-4 border-t border-border">
<a href="{{ config('domains.domain_portal_url') }}"
class="block w-full btn-secondary-accent text-center">
{{ $content['portal_login'] ?? 'Portal Login' }}
<div class="pt-4 border-t border-border space-y-3">
{{-- Mobile Language Switcher --}}
<div class="flex items-center space-x-1 text-sm px-3" role="group" aria-label="Language">
@foreach ($availableLocales as $locale => $label)
<button wire:click="switchLanguage('{{ $locale }}')"
class="px-3 py-1.5 rounded transition-colors duration-200
{{ $currentLocale === $locale
? 'text-secondary font-semibold bg-secondary/10'
: 'text-muted-foreground hover:text-foreground' }}"
@if($currentLocale === $locale) aria-current="true" @endif>
{{ $label }}
</button>
@if (!$loop->last)
<span class="text-border">|</span>
@endif
@endforeach
</div>
<a href="{{ route('contact') }}"
class="block rounded-md px-5 py-2.5 text-sm font-medium bg-secondary hover-bg-primary glow-medium text-white transition-all duration-200 text-center">
{{ __('ui.contact') }}
</a>
</div>
</nav>

View file

@ -4,17 +4,26 @@
<title>{{ $title ?? config('app.name') }}</title>
<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="57x57" href="/favicon/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/favicon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/favicon/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/favicon/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/favicon/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/favicon/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/favicon/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/favicon/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/favicon/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="96x96" href="/favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
<link rel="shortcut icon" href="/favicon/favicon.ico">
<link rel="manifest" href="/favicon/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/favicon/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700|ibm-plex-sans:400,500,600,700"
rel="stylesheet" />
@vite(['resources/css/portal.css', 'resources/js/app.js'], 'build/portal')
@vite(['resources/css/portal.css', 'resources/js/app.js'], 'build/portal')
@livewireStyles
@fluxAppearance

View file

@ -1,6 +1,7 @@
@extends('web.layouts.web-master')
@section('title', 'Über B2IN - Unser Team & Geschichte')
@section('meta_description', 'Lernen Sie B2in kennen gegründet von Marcel Scheibe. Unsere Vision: Design & Immobilien intelligent verbinden.')
@section('content')
<div class="bg-background">
@ -8,8 +9,9 @@
<main class="variante-glass-flow">
<livewire:web.components.sections.about-hero />
<livewire:web.components.sections.founder-bar />
<livewire:web.components.sections.our-story />
<livewire:web.components.sections.leadership-team />
<livewire:web.components.sections.image-break section="about_image_break" />
<livewire:web.components.sections.our-values />
<livewire:web.components.sections.partner-c-t-a />
</main>

View file

@ -0,0 +1,32 @@
@extends('web.layouts.web-master')
@section('title', 'Über B2IN - Unser Team & Geschichte')
@section('content')
<div class="bg-background">
<livewire:web.components.ui.header />
<main class="variante-glass-flow">
<livewire:web.components.sections.about-hero />
<livewire:web.components.sections.founder-bar />
<livewire:web.components.sections.our-story />
<livewire:web.components.sections.leadership-team />
<livewire:web.components.sections.our-values />
<livewire:web.components.sections.partner-c-t-a />
</main>
<livewire:web.components.ui.footer />
</div>
@endsection
@push('styles')
<style>
[x-cloak] {
display: none !important;
}
</style>
@endpush
@push('scripts')
{{-- Alpine.js wird zentral im Layout geladen --}}
@endpush

View file

@ -9,11 +9,10 @@
<main>
<livewire:web.components.sections.hero />
<livewire:web.components.sections.ecosystem-core />
<livewire:web.components.sections.founder-bar />
<livewire:web.components.sections.content-section section="synergie_section" layout="left" />
<livewire:web.components.sections.vision-section bg="bg-accent" />
<livewire:web.components.sections.brand-worlds />
<livewire:web.components.sections.content-section layout="right" bg="bg-accent"
section="integriertes_modell_b2in" />
<livewire:web.components.sections.ecosystem-core />
<livewire:web.components.sections.c-t-a-section />
</main>

View file

@ -1,6 +1,7 @@
@extends('web.layouts.web-master')
@section('title', 'Kontakt - B2In')
@section('title', 'Kontakt - B2in')
@section('meta_description', 'Nehmen Sie Kontakt mit B2in auf persönliche Beratung zu Immobilien in Dubai & Einrichtungslösungen.')
@section('content')
<div class="bg-background">

View file

@ -0,0 +1,50 @@
@extends('web.layouts.web-master')
@php
$legal = legal_page('cookie_policy');
@endphp
@section('title', $legal['meta_title'] . ' - ' . ($domainName ?? config('app.name')))
@section('content')
<div class="bg-background">
<livewire:web.components.ui.header />
<main class="variante-glass-flow">
<section class="section-padding">
<div class="container-padding">
<div class="max-w-3xl mx-auto">
<h1 class="text-hero mb-8">{{ $legal['title'] }}</h1>
<p class="text-muted-foreground text-sm mb-12">
{{ $legal['subtitle'] }}
</p>
<div class="prose-legal space-y-8 text-foreground">
{!! $legal['content'] !!}
</div>
{{-- Cookie-Einstellungen & aktuelle Status-Anzeige --}}
<div class="prose-legal space-y-8 text-foreground">
<x-cookie-consent::privacy-info :show-analytics-info="false" />
</div>
<div class="mt-12 pt-8 border-t border-border/30">
<a href="{{ url()->previous() }}" class="text-secondary hover:underline text-sm">
{{ $legal['back_link'] }}
</a>
</div>
</div>
</div>
</section>
</main>
<livewire:web.components.ui.footer />
</div>
@endsection
@push('styles')
<style>
[x-cloak] { display: none !important; }
.prose-legal a { transition: color 0.2s; }
</style>
@endpush

View file

@ -0,0 +1,134 @@
@extends('web.layouts.web-master')
@section('title', 'Internationale Immobilien - B2in')
@section('content')
<div class="bg-background">
<livewire:web.components.ui.header />
<main class="variante-glass-flow">
<livewire:web.components.sections.partner-hero section="immobilien_hero" />
<livewire:web.components.sections.founder-bar />
{{-- Aktuelle Projekte --}}
@php
$projects = cms_theme_section('immobilien_projects');
$moebelVorteil = cms_theme_section('immobilien_moebel_vorteil');
@endphp
@if (!empty($projects))
<section class="section-padding">
<div class="container-padding">
<div class="text-center mb-12 slide-up delay-300">
<h2 class="text-section-title">{!! $projects['title'] ?? '' !!}</h2>
@if (isset($projects['subtitle']))
<p class="text-large text-muted-foreground mt-4 max-w-2xl mx-auto">
{{ $projects['subtitle'] }}
</p>
@endif
</div>
<div class="grid md:grid-cols-1 lg:grid-cols-2 gap-8 slide-up delay-400">
@foreach ($projects['projects'] ?? [] as $project)
<div class="card-elevated rounded-2xl overflow-hidden">
@if (isset($project['image']))
<div class="relative h-56 overflow-hidden">
<img src="{{ theme_image_url($project['image']) }}"
alt="{{ $project['title'] }}"
class="w-full h-full object-cover" />
@if (isset($project['status']))
<div class="absolute top-4 left-4">
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold bg-secondary text-white">
{{ $project['status'] }}
@if (isset($project['launch_date']))
<span class="opacity-80">({{ $project['launch_date'] }})</span>
@endif
</span>
</div>
@endif
</div>
@endif
<div class="p-6 lg:p-8">
<div class="mb-4">
<h3 class="text-xl font-semibold text-foreground">{{ $project['title'] }}</h3>
@if (isset($project['location']))
<p class="text-sm text-muted-foreground mt-1">{{ $project['location'] }}</p>
@endif
</div>
@if (isset($project['price_from']))
<p class="text-lg font-medium text-secondary mb-4">{{ $project['price_from'] }}</p>
@endif
@if (isset($project['highlights']))
<ul class="space-y-2 mb-6">
@foreach ($project['highlights'] as $highlight)
<li class="flex items-start gap-2 text-sm text-muted-foreground">
<svg class="w-4 h-4 text-secondary mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
{{ $highlight }}
</li>
@endforeach
</ul>
@endif
<a href="{{ isset($project['slug']) ? route('immobilien.show', $project['slug']) : ($projects['cta_link'] ?? '/contact') }}"
class="inline-flex items-center gap-2 btn-primary-accent">
{{ isset($project['slug']) ? 'Exposé ansehen' : ($projects['cta_text'] ?? 'Anfragen') }}
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</a>
</div>
</div>
@endforeach
</div>
</div>
</section>
@endif
{{-- Möbel-Vorteil Banner --}}
@if (!empty($moebelVorteil))
<section class="section-padding bg-secondary/5">
<div class="container-padding">
<div class="max-w-3xl mx-auto text-center slide-up delay-300">
<h2 class="text-section-title">{!! $moebelVorteil['title'] ?? '' !!}</h2>
<p class="text-large text-muted-foreground mt-4">
{{ $moebelVorteil['text'] ?? '' }}
</p>
@if (isset($moebelVorteil['button_text']))
<div class="mt-8">
<a href="{{ $moebelVorteil['button_link'] ?? '/partner' }}"
class="btn-secondary-accent">
{{ $moebelVorteil['button_text'] }}
</a>
</div>
@endif
</div>
</div>
</section>
@endif
{{-- Trust & Kontakt --}}
<livewire:web.components.sections.content-section section="immobilien_trust" layout="right" />
<livewire:web.components.sections.c-t-a-section />
</main>
<livewire:web.components.ui.footer />
</div>
@endsection
@push('styles')
<style>
[x-cloak] {
display: none !important;
}
</style>
@endpush
@push('scripts')
{{-- Alpine.js wird zentral im Layout geladen --}}
@endpush

View file

@ -0,0 +1,160 @@
@extends('web.layouts.web-master')
@section('title', 'Einrichtungsnetzwerk - B2in Interior')
@section('content')
<div class="bg-background">
<livewire:web.components.ui.header />
<main class="variante-glass-flow">
<livewire:web.components.sections.partner-hero section="interior_hero" />
<livewire:web.components.sections.founder-bar />
{{-- Local-for-Local Konzept --}}
<livewire:web.components.sections.content-section section="interior_concept" layout="left" />
{{-- Zwei Marken --}}
@php
$brands = cms_theme_section('interior_brands');
$zielgruppen = cms_theme_section('interior_zielgruppen');
$process = cms_theme_section('interior_process');
@endphp
@if (!empty($brands))
<section class="section-padding bg-accent">
<div class="container-padding">
<div class="text-center mb-12 slide-up delay-300">
<h2 class="text-section-title">{!! $brands['title'] ?? '' !!}</h2>
@if (isset($brands['subtitle']))
<p class="text-large text-muted-foreground mt-4 max-w-2xl mx-auto">
{{ $brands['subtitle'] }}
</p>
@endif
</div>
<div class="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto slide-up delay-400">
@foreach ($brands['brands'] ?? [] as $brand)
<div class="card-elevated rounded-2xl overflow-hidden">
<div class="p-8 lg:p-10">
@if (isset($brand['logo']))
<div class="mb-6">
<img src="{{ asset($brand['logo']) }}"
alt="{{ $brand['name'] }}"
class="{{ $brand['logo_width'] ?? 'w-32' }}" />
</div>
@endif
<p class="text-xs font-semibold uppercase tracking-wider text-secondary mb-3">
{{ $brand['tagline'] ?? '' }}
</p>
<p class="text-muted-foreground leading-relaxed">
{{ $brand['description'] ?? '' }}
</p>
@if (isset($brand['audience']))
<p class="text-sm font-medium text-foreground mt-4">
{{ $brand['audience'] }}
</p>
@endif
@if (isset($brand['link']))
<div class="mt-6">
<a href="{{ $brand['link'] }}"
target="_blank" rel="noopener"
class="inline-flex items-center gap-2 text-sm font-medium text-secondary hover:text-secondary/80 transition-colors">
{{ $brand['name'] }} entdecken
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
</div>
@endif
</div>
</div>
@endforeach
</div>
</div>
</section>
@endif
{{-- Für wen? Zielgruppen --}}
@if (!empty($zielgruppen))
<section class="section-padding">
<div class="container-padding">
<div class="text-center mb-12 slide-up delay-300">
<h2 class="text-section-title">{!! $zielgruppen['title'] ?? '' !!}</h2>
</div>
<div class="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto slide-up delay-400">
@foreach ($zielgruppen['groups'] ?? [] as $group)
<div class="text-center p-6">
<div class="inline-flex items-center justify-center w-14 h-14 rounded-full bg-secondary/10 text-secondary mb-5">
<svg class="w-7 h-7" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
@switch($group['icon'] ?? '')
@case('home')
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
@break
@case('building-office-2')
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 21h19.5m-18-18v18m10.5-18v18m6-13.5V21M6.75 6.75h.75m-.75 3h.75m-.75 3h.75m3-6h.75m-.75 3h.75m-.75 3h.75M6.75 21v-3.375c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21M3 3h12m-.75 4.5H21m-3.75 3.75h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008z" />
@break
@case('clipboard-document-check')
<path stroke-linecap="round" stroke-linejoin="round" d="M11.35 3.836c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m8.9-4.414c.376.023.75.05 1.124.08 1.131.094 1.976 1.057 1.976 2.192V16.5A2.25 2.25 0 0118 18.75h-2.25m-7.5-10.5H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V18.75m-7.5-10.5h6.375c.621 0 1.125.504 1.125 1.125v9.375m-8.25-3l1.5 1.5 3-3.75" />
@break
@endswitch
</svg>
</div>
<h3 class="text-lg font-semibold text-foreground mb-3">{{ $group['title'] ?? '' }}</h3>
<p class="text-sm text-muted-foreground leading-relaxed">{{ $group['description'] ?? '' }}</p>
</div>
@endforeach
</div>
</div>
</section>
@endif
{{-- So funktioniert es - Prozess --}}
@if (!empty($process))
<section class="section-padding bg-accent">
<div class="container-padding">
<div class="text-center mb-12 slide-up delay-300">
<h2 class="text-section-title">{!! $process['title'] ?? '' !!}</h2>
</div>
<div class="max-w-3xl mx-auto slide-up delay-400">
<div class="space-y-8">
@foreach ($process['steps'] ?? [] as $step)
<div class="flex gap-6 items-start">
<div class="shrink-0 w-14 h-14 rounded-full bg-secondary text-white flex items-center justify-center text-lg font-bold">
{{ $step['number'] ?? '' }}
</div>
<div class="pt-2">
<h3 class="text-lg font-semibold text-foreground mb-2">{{ $step['title'] ?? '' }}</h3>
<p class="text-muted-foreground leading-relaxed">{{ $step['description'] ?? '' }}</p>
</div>
</div>
@endforeach
</div>
</div>
</div>
</section>
@endif
{{-- Trust / Marcel Scheibe --}}
<livewire:web.components.sections.content-section section="interior_trust" layout="right" />
{{-- CTA --}}
<livewire:web.components.sections.c-t-a-section />
</main>
<livewire:web.components.ui.footer />
</div>
@endsection
@push('styles')
<style>
[x-cloak] {
display: none !important;
}
</style>
@endpush
@push('scripts')
{{-- Alpine.js wird zentral im Layout geladen --}}
@endpush

View file

@ -0,0 +1,38 @@
@extends('web.layouts.web-master')
@section('title', 'Für Entwickler & Partner - B2in')
@section('content')
<div class="bg-background">
<livewire:web.components.ui.header />
<main class="variante-glass-flow">
<livewire:web.components.sections.partner-hero />
<livewire:web.components.sections.founder-bar />
<livewire:web.components.sections.content-section section="supply_chain_intro" layout="left" bg="bg-accent" />
<livewire:web.components.sections.card-section section="partner_card_section" />
<livewire:web.components.sections.benefits-section section="partner_benefits_developer" />
<livewire:web.components.sections.benefits-section section="partner_benefits_retailer" layout="right" bg="bg-accent" />
<livewire:web.components.sections.benefits-section section="partner_benefits_supplier" />
<livewire:web.components.sections.benefits-section section="partner_benefits_broker" layout="right" bg="bg-accent" />
<livewire:web.components.sections.partner-process />
<livewire:web.components.sections.commitment-section />
<livewire:web.components.sections.partner-c-t-a />
</main>
<livewire:web.components.ui.footer />
</div>
@endsection
@push('styles')
<style>
[x-cloak] {
display: none !important;
}
</style>
@endpush
@push('scripts')
{{-- Alpine.js wird zentral im Layout geladen --}}
@endpush

View file

@ -0,0 +1,120 @@
@extends('web.layouts.web-master')
@section('title', 'Dev Sitemap - B2in')
@section('content')
<div class="bg-background">
<livewire:web.components.ui.header />
<main class="section-padding variante-glass-flow">
<div class="container-padding">
<div class="max-w-4xl mx-auto">
<div class="mb-12">
<h1 class="text-hero mb-4">Dev <span class="text-secondary">Sitemap</span></h1>
<p class="text-lg text-muted-foreground">Übersicht aller Seiten inklusive Archiv-Versionen und Entwicklungsseiten.</p>
</div>
{{-- Live-Seiten --}}
<div class="mb-12">
<h2 class="text-xl font-semibold text-foreground mb-6 flex items-center gap-2">
<span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-green-500/10 text-green-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" /></svg>
</span>
Live-Seiten
</h2>
<div class="grid gap-3">
@php
$livePages = [
['url' => '/', 'label' => 'Home', 'description' => 'Startseite B2in'],
['url' => '/immobilien', 'label' => 'Immobilien', 'description' => 'Dubai Investments Hauptseite'],
['url' => '/immobilien/azizi-creek-views-4', 'label' => 'Azizi Creek Views 4', 'description' => 'Projekt-Exposé'],
['url' => '/netzwerk', 'label' => 'Netzwerk', 'description' => 'B2in Ökosystem Teaser (Soft Launch)'],
['url' => '/magazin', 'label' => 'Magazin', 'description' => 'Artikel & Insights'],
['url' => '/about', 'label' => 'Über B2in', 'description' => 'Über uns & Team'],
['url' => '/contact', 'label' => 'Kontakt', 'description' => 'Kontaktformular'],
['url' => '/faq', 'label' => 'FAQ', 'description' => 'Häufige Fragen'],
];
@endphp
@foreach ($livePages as $page)
<a href="{{ $page['url'] }}" class="flex items-center justify-between p-4 rounded-xl border border-border/50 hover:border-secondary/50 hover:bg-secondary/5 transition-all group">
<div>
<span class="font-medium text-foreground group-hover:text-secondary transition-colors">{{ $page['label'] }}</span>
<span class="text-sm text-muted-foreground ml-3">{{ $page['description'] }}</span>
</div>
<code class="text-xs text-muted-foreground bg-accent px-2 py-1 rounded">{{ $page['url'] }}</code>
</a>
@endforeach
</div>
</div>
{{-- Archiv-Seiten --}}
<div class="mb-12">
<h2 class="text-xl font-semibold text-foreground mb-6 flex items-center gap-2">
<span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-amber-500/10 text-amber-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" /></svg>
</span>
Archiv (Pre-Soft-Launch Versionen)
</h2>
<div class="grid gap-3">
@php
$archivePages = [
['url' => '/dev/immobilien-v1', 'label' => 'Immobilien (v1)', 'description' => 'Originalversion vor Soft Launch'],
['url' => '/dev/interior-v1', 'label' => 'Interior / Einrichtungsnetzwerk (v1)', 'description' => 'Originalversion vor Soft Launch'],
['url' => '/dev/partner-v1', 'label' => 'Für Entwickler & Partner (v1)', 'description' => 'Originalversion vor Soft Launch'],
];
@endphp
@foreach ($archivePages as $page)
<a href="{{ $page['url'] }}" class="flex items-center justify-between p-4 rounded-xl border border-border/50 hover:border-amber-500/50 hover:bg-amber-500/5 transition-all group">
<div>
<span class="font-medium text-foreground group-hover:text-amber-600 transition-colors">{{ $page['label'] }}</span>
<span class="text-sm text-muted-foreground ml-3">{{ $page['description'] }}</span>
</div>
<code class="text-xs text-muted-foreground bg-accent px-2 py-1 rounded">{{ $page['url'] }}</code>
</a>
@endforeach
</div>
</div>
{{-- Dev-Tools --}}
<div>
<h2 class="text-xl font-semibold text-foreground mb-6 flex items-center gap-2">
<span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-blue-500/10 text-blue-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17l-5.384-3.116A2.25 2.25 0 015.25 10.08V6.75a2.25 2.25 0 011.786-1.975l5.384-3.116a2.25 2.25 0 012.16 0l5.384 3.116A2.25 2.25 0 0120.75 6.75v3.33a2.25 2.25 0 01-.786 1.975l-5.384 3.116a2.25 2.25 0 01-2.16 0z" /></svg>
</span>
Dev-Tools
</h2>
<div class="grid gap-3">
@php
$devPages = [
['url' => '/theme-demo', 'label' => 'Theme Demo', 'description' => 'Farben, Logos, Buttons aller Themes'],
['url' => '/dev/sitemap', 'label' => 'Dev Sitemap', 'description' => 'Diese Seite'],
];
@endphp
@foreach ($devPages as $page)
<a href="{{ $page['url'] }}" class="flex items-center justify-between p-4 rounded-xl border border-border/50 hover:border-blue-500/50 hover:bg-blue-500/5 transition-all group">
<div>
<span class="font-medium text-foreground group-hover:text-blue-600 transition-colors">{{ $page['label'] }}</span>
<span class="text-sm text-muted-foreground ml-3">{{ $page['description'] }}</span>
</div>
<code class="text-xs text-muted-foreground bg-accent px-2 py-1 rounded">{{ $page['url'] }}</code>
</a>
@endforeach
</div>
</div>
</div>
</div>
</main>
<livewire:web.components.ui.footer />
</div>
@endsection
@push('styles')
<style>
[x-cloak] {
display: none !important;
}
</style>
@endpush

View file

@ -1,6 +1,6 @@
@extends('web.layouts.web-master')
@section('title', 'B2In Ecosystem - Intelligentes Netzwerk')
@section('title', 'B2in Ecosystem - Intelligentes Netzwerk')
@section('content')
<div class="bg-background">

View file

@ -1,6 +1,7 @@
@extends('web.layouts.web-master')
@section('title', 'Stileigentum - Premium Immobilien & Zeitlose Eleganz')
@section('title', 'FAQ - B2in')
@section('meta_description', 'Häufig gestellte Fragen zu B2in Immobilien in Dubai, Einrichtungsservice und mehr.')
@section('content')
<div class="bg-background">
@ -21,7 +22,3 @@
}
</style>
@endpush
@push('scripts')
{{-- Alpine.js wird zentral im Layout geladen --}}
@endpush

View file

@ -1,6 +1,7 @@
@extends('web.layouts.web-master')
@section('title', 'B2IN - Connecting Design and Property')
@section('meta_description', 'B2in verbindet exklusive Immobilien in Dubai mit europäischem Einrichtungsdesign. Ihr Partner für Investment & Interior.')
@section('content')
<div class="bg-background">
@ -8,12 +9,10 @@
<main class="variante-glass-flow">
<livewire:web.components.sections.hero />
<livewire:web.components.sections.vision-section />
<livewire:web.components.sections.ecosystem-core bg="bg-accent" />
<livewire:web.components.sections.brand-worlds />
<livewire:web.components.sections.content-section layout="left" bg="bg-accent"
section="integriertes_modell_b2in" />
<livewire:web.components.sections.founder-bar />
<livewire:web.components.sections.content-section section="synergie_section" layout="left" />
<livewire:web.components.sections.vision-section bg="bg-accent" />
<livewire:web.components.sections.ecosystem-core />
<livewire:web.components.sections.c-t-a-section />
</main>

View file

@ -0,0 +1,363 @@
@extends('web.layouts.web-master')
@section('title', ($project['title'] ?? 'Exposé') . ' - B2in Immobilien')
@section('meta_description', ($project['investment_case']['text'] ?? 'Exklusives Off-Market-Immobilienprojekt in Dubai jetzt Exposé ansehen.'))
@section('content')
<div class="bg-background">
<livewire:web.components.ui.header />
<main class="variante-glass-flow">
{{-- 1. Hero --}}
<section class="relative h-[50vh] min-h-[400px] overflow-hidden">
<x-web-picture
src="{{ theme_image_url($project['image'] ?? '') }}"
alt="{{ $project['title'] ?? '' }}"
class="absolute inset-0 w-full h-full object-cover"
loading="" />
<div class="absolute inset-0 bg-gradient-to-t from-black/70 via-black/30 to-transparent"></div>
<div class="absolute bottom-0 left-0 right-0 p-8 lg:p-12">
<div class="container-padding">
@if (isset($project['status']))
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold bg-secondary text-white mb-4">
{{ $project['status'] }}
@if (isset($project['launch_date']))
<span class="opacity-80">({{ $project['launch_date'] }})</span>
@endif
</span>
@endif
<h1 class="text-3xl lg:text-4xl font-bold text-white">{{ $project['title'] ?? '' }}</h1>
@if (isset($project['location']))
<p class="text-lg text-white/80 mt-2">{{ $project['location'] }}</p>
@endif
@if (isset($project['price_from']))
<p class="text-xl font-semibold text-white mt-3">{{ $project['price_from'] }}</p>
@endif
</div>
</div>
</section>
{{-- 2. Quick Facts --}}
{{-- Editierbar über CMS je Projekt --}}
@if (!empty($project['quick_facts']))
<section class=" bg-accent">
<div class="container-padding">
<div class="grid grid-cols-2 lg:grid-cols-4 gap-6">
@foreach ($project['quick_facts'] as $fact)
<div class="text-center p-4">
<div class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-secondary/10 text-secondary mb-3">
@if (isset($fact['icon']))
<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
@switch($fact['icon'])
@case('home-modern')
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 21v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21m0 0h4.5V3.545M12.75 21h7.5V10.75M2.25 21h1.5m18 0h-18M2.25 9l4.5-1.636M18.75 3l-1.5.545m0 6.205l3 1m1.5.5l-1.5-.5M6.75 7.364V3h-3v18m3-13.636l10.5-3.819" />
@break
@case('squares-2x2')
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
@break
@case('building-office-2')
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 21h19.5m-18-18v18m10.5-18v18m6-13.5V21M6.75 6.75h.75m-.75 3h.75m-.75 3h.75m3-6h.75m-.75 3h.75m-.75 3h.75M6.75 21v-3.375c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21M3 3h12m-.75 4.5H21m-3.75 3.75h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008z" />
@break
@case('user')
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
@break
@endswitch
</svg>
@endif
</div>
<p class="text-xs text-muted-foreground uppercase tracking-wider mb-1">{{ $fact['label'] ?? '' }}</p>
<p class="text-sm font-semibold text-foreground">{{ $fact['value'] ?? '' }}</p>
</div>
@endforeach
</div>
</div>
</section>
@endif
{{-- 3. Investment Case --}}
@if (!empty($project['investment_case']))
<section class="section-padding">
<div class="container-padding">
<div class="max-w-3xl mx-auto">
<h2 class="text-section-title text-left">{{ $project['investment_case']['title'] ?? '' }}</h2>
<p class="text-large text-muted-foreground mt-6 leading-relaxed">
{{ $project['investment_case']['text'] ?? '' }}
</p>
@if (!empty($project['investment_case']['views']))
<div class="mt-8">
<p class="text-sm font-semibold text-foreground mb-3">Verfügbare Views:</p>
<div class="flex flex-wrap gap-2">
@foreach ($project['investment_case']['views'] as $view)
<span class="inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-secondary/10 text-secondary">
{{ $view }}
</span>
@endforeach
</div>
</div>
@endif
@if (!empty($project['highlights']))
<ul class="mt-8 space-y-3">
@foreach ($project['highlights'] as $highlight)
<li class="flex items-start gap-3 text-muted-foreground">
<svg class="w-5 h-5 text-secondary mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
{{ $highlight }}
</li>
@endforeach
</ul>
@endif
</div>
</div>
</section>
@endif
{{-- 4. Bildergalerie --}}
@if (!empty($project['gallery']))
<div x-data="{ lightbox: false, current: 0 }">
<section class="section-padding bg-accent">
<div class="container-padding">
<h2 class="text-section-title text-center mb-8">Galerie</h2>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-3 gap-4">
@foreach ($project['gallery'] as $index => $image)
<div class="relative aspect-[4/3] rounded-xl overflow-hidden cursor-pointer group"
@click="lightbox = true; current = {{ $index }}">
<x-web-picture
src="{{ theme_image_url($image) }}"
alt="{{ $project['title'] ?? '' }} Bild {{ $index + 1 }}"
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105" />
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors duration-300"></div>
</div>
@endforeach
</div>
</div>
</section>
{{-- Lightbox (via x-teleport ins body, umgeht alle Eltern-Constraints) --}}
<template x-teleport="body">
<div x-show="lightbox" x-cloak
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"
class="fixed inset-0 z-50 isolate bg-black/90"
@keydown.escape.window="lightbox = false"
@keydown.arrow-right.window="current = (current + 1) % {{ count($project['gallery']) }}"
@keydown.arrow-left.window="current = (current - 1 + {{ count($project['gallery']) }}) % {{ count($project['gallery']) }}">
{{-- Bildbereich (eigene Ebene, dahinter) --}}
<div class="absolute inset-0 flex items-center justify-center p-4 z-0">
@foreach ($project['gallery'] as $index => $image)
<x-web-picture x-show="current === {{ $index }}"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
src="{{ theme_image_url($image) }}"
alt="{{ $project['title'] ?? '' }} Bild {{ $index + 1 }}"
class="max-h-[85vh] max-w-[90vw] object-contain rounded-lg"
loading="" />
@endforeach
</div>
{{-- Close Button (oben rechts mit Abstand) --}}
<button @click="lightbox = false"
class="cursor-pointer absolute top-6 left-auto right-6 z-10 flex items-center justify-center w-12 h-12 rounded-full bg-white/10 hover:bg-white/20 text-white/90 hover:text-white border border-white/20 transition-all">
<svg class="w-6 h-6" 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" />
</svg>
</button>
{{-- Previous (links, vertikal mittig) --}}
<button @click="current = (current - 1 + {{ count($project['gallery']) }}) % {{ count($project['gallery']) }}"
class="cursor-pointer absolute left-6 top-1/2 -translate-y-1/2 z-10 flex items-center justify-center w-14 h-14 rounded-full bg-white/10 hover:bg-white/20 text-white/90 hover:text-white border border-white/20 transition-all">
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
{{-- Next (rechts, vertikal mittig) --}}
<button @click="current = (current + 1) % {{ count($project['gallery']) }}"
class="cursor-pointer absolute left-auto right-6 top-1/2 -translate-y-1/2 z-10 flex items-center justify-center w-14 h-14 rounded-full bg-white/10 hover:bg-white/20 text-white/90 hover:text-white border border-white/20 transition-all">
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
{{-- Counter --}}
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 text-white/60 text-sm">
<span x-text="current + 1"></span> / {{ count($project['gallery']) }}
</div>
</div>
</template>
</div>
@endif
{{-- 5. Trust-Block: Investorenschutz (pro Projekt im CMS, siehe investor_trust) --}}
@php
$trust = $project['investor_trust'] ?? [];
$trustColumns = $trust['columns'] ?? [];
@endphp
@if (! empty($trust['title']) || count($trustColumns) > 0)
<section class="section-padding">
<div class="container-padding">
<div class="max-w-4xl mx-auto">
@if (! empty($trust['title']) || ! empty($trust['intro']))
<div class="text-center mb-10">
@if (! empty($trust['title']))
<h2 class="text-section-title">{{ $trust['title'] }}</h2>
@endif
@if (! empty($trust['intro']))
<p class="text-large text-muted-foreground mt-4 max-w-2xl mx-auto">
{{ $trust['intro'] }}
</p>
@endif
</div>
@endif
@if (count($trustColumns) > 0)
<div class="grid md:grid-cols-3 gap-6 mb-10">
@foreach ($trustColumns as $col)
<div class="card-elevated rounded-2xl p-6 text-center">
<div class="inline-flex items-center justify-center w-14 h-14 rounded-full bg-secondary/10 text-secondary mb-5">
@if (! empty($col['icon']))
@svg($col['icon'], 'w-7 h-7')
@endif
</div>
<h3 class="text-base font-semibold text-foreground mb-3">{{ $col['title'] ?? '' }}</h3>
<p class="text-sm text-muted-foreground leading-relaxed">
{{ $col['text'] ?? '' }}
</p>
</div>
@endforeach
</div>
@endif
@if (! empty($trust['cta_url']) && ! empty($trust['cta_label']))
<div class="text-center">
<a href="{{ $trust['cta_url'] }}" class="btn-secondary-accent transition-colors duration-200">
<span class="flex items-center justify-center gap-2">
@svg('heroicon-o-book-open', 'w-4 h-4')
<span>{{ $trust['cta_label'] }}</span>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</span>
</a>
</div>
@endif
</div>
</div>
</section>
@endif
{{-- 6. Location & Map --}}
@if (!empty($project['location_info']))
<section class="section-padding bg-accent">
<div class="container-padding">
<div class="max-w-3xl mx-auto">
<h2 class="text-section-title text-left">{{ $project['location_info']['title'] ?? '' }}</h2>
@if (!empty($project['location_info']['points']))
<ul class="mt-8 space-y-4">
@foreach ($project['location_info']['points'] as $point)
<li class="flex items-start gap-3 text-muted-foreground">
<svg class="w-5 h-5 text-secondary mt-0.5 shrink-0 inline-block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" />
</svg>
{{ $point }}
</li>
@endforeach
</ul>
@endif
@if (isset($project['location_info']['map_url']))
<div class="mt-8 text-center">
<a href="{{ $project['location_info']['map_url'] }}"
target="_blank" rel="noopener"
class="inline-flex items-center gap-2 btn-secondary-accent">
<svg class="w-4 h-4 inline-block mr-2 mb-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
</svg>
Auf Google Maps ansehen
</a>
</div>
@endif
</div>
</div>
</section>
@endif
{{-- Möbel-Vorteil / Synergie (pro Projekt: furniture_benefit; Fallback: Theme-Config) --}}
@php
$moebelVorteil = $project['furniture_benefit'] ?? [];
if (empty($moebelVorteil) || (empty($moebelVorteil['title'] ?? null) && empty($moebelVorteil['text'] ?? null))) {
$moebelVorteil = cms_theme_section('immobilien_moebel_vorteil');
}
@endphp
@if (! empty($moebelVorteil))
<section class="section-padding bg-secondary/5">
<div class="container-padding">
<div class="max-w-3xl mx-auto text-center">
<h2 class="text-section-title">{!! $moebelVorteil['title'] ?? '' !!}</h2>
<p class="text-large text-muted-foreground mt-4">
{{ $moebelVorteil['text'] ?? '' }}
</p>
@if (! empty($moebelVorteil['button_text'] ?? null))
<div class="mt-8">
<a href="{{ $moebelVorteil['button_link'] ?? '/partner' }}"
class="btn-secondary-accent">
{{ $moebelVorteil['button_text'] }}
</a>
</div>
@endif
</div>
</div>
</section>
@endif
{{-- 7. Lead-Generierung / Kontakt --}}
@if (!empty($project['contact']))
<section class="section-padding bg-accent">
<div class="container-padding">
<div class="max-w-2xl mx-auto text-center">
<h2 class="text-section-title">{{ $project['contact']['title'] ?? '' }}</h2>
@if (isset($project['contact']['subtitle']))
<p class="text-large text-muted-foreground mt-3">{{ $project['contact']['subtitle'] }}</p>
@endif
<div class="mt-8 card-elevated rounded-2xl p-8 text-left">
<livewire:web.components.sections.immobilien-contact-form
:projectSlug="$project['slug'] ?? ''"
:projectTitle="$project['title'] ?? ''"
:interestOptions="$project['contact']['options'] ?? []"
/>
</div>
</div>
</div>
</section>
@endif
</main>
<livewire:web.components.ui.footer />
</div>
@endsection
@push('styles')
<style>
[x-cloak] {
display: none !important;
}
</style>
@endpush

View file

@ -0,0 +1,314 @@
@extends('web.layouts.web-master')
@section('title', 'Immobilien Dubai - B2in')
@section('meta_description', 'Exklusive Off-Market-Immobilien in Dubai. Persönliche Beratung, steuerfreie Renditen & Turnkey-Einrichtung aus einer Hand.')
@section('content')
<div class="bg-background">
<livewire:web.components.ui.header />
<main class="variante-glass-flow">
@php
$heroV2 = cms_theme_section('immobilien_hero_v2');
$warumDubai = cms_theme_section('immobilien_warum_dubai');
$kaufprozess = cms_theme_section('immobilien_kaufprozess');
$bruecke = cms_theme_section('immobilien_bruecke');
$mindset = cms_theme_section('immobilien_mindset');
$projects = cms_theme_section('immobilien_projects');
$moebelVorteil = cms_theme_section('immobilien_moebel_vorteil');
@endphp
{{-- Sektion 1: Hero --}}
@if (!empty($heroV2))
<section class="relative min-h-[70vh] flex items-center overflow-hidden">
<x-web-picture
src="{{ theme_image_url($heroV2['image'] ?? '') }}"
alt="{{ $heroV2['image_alt'] ?? 'Immobilien Investment Dubai' }}"
class="absolute inset-0 w-full h-full object-cover"
loading="" />
<div class="absolute inset-0 bg-gradient-to-r from-black/80 via-black/60 to-black/30"></div>
<div class="relative z-10 container-padding py-24 lg:py-32">
<div class="max-w-3xl slide-right delay-300">
<h1 class="text-4xl lg:text-5xl xl:text-6xl font-bold text-white leading-tight">
{!! $heroV2['title'] !!}
</h1>
<p class="text-lg lg:text-xl text-white/80 mt-6 max-w-2xl leading-relaxed">
{{ $heroV2['subtitle'] ?? '' }}
</p>
@if (isset($heroV2['cta_text']))
<div class="mt-10">
<a href="{{ $heroV2['cta_link'] ?? '#projekte' }}"
class="btn-primary-accent px-8 py-4 text-lg">
{{ $heroV2['cta_text'] }}
</a>
</div>
@endif
</div>
</div>
</section>
@endif
<livewire:web.components.sections.founder-bar />
{{-- Sektion 2: Warum Dubai? --}}
@if (!empty($warumDubai))
<section class="section-padding">
<div class="container-padding">
<div class="max-w-3xl mx-auto text-center mb-12 slide-up delay-300">
<h2 class="text-section-title">{!! $warumDubai['title'] !!}</h2>
@if (isset($warumDubai['intro']))
<p class="text-large text-muted-foreground mt-4 leading-relaxed">
{{ $warumDubai['intro'] }}
</p>
@endif
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-6 max-w-5xl mx-auto">
@foreach ($warumDubai['facts'] ?? [] as $index => $fact)
<div class="card-elevated rounded-2xl p-6 lg:p-8 text-center slide-up delay-{{ 300 + $index * 100 }}">
<div class="inline-flex items-center justify-center w-14 h-14 rounded-full bg-secondary/10 text-secondary mb-5">
@svg('heroicon-o-' . ($fact['icon'] ?? 'check'), 'w-7 h-7')
</div>
<h3 class="text-lg font-semibold text-foreground mb-2">{{ $fact['title'] ?? '' }}</h3>
<p class="text-sm text-muted-foreground leading-relaxed">{{ $fact['description'] ?? '' }}</p>
</div>
@endforeach
</div>
</div>
</section>
@endif
<livewire:web.components.sections.image-break section="immobilien_image_break" />
{{-- Sektion 3: Kaufprozess --}}
@if (!empty($kaufprozess))
<section class="section-padding bg-accent">
<div class="container-padding">
<div class="max-w-3xl mx-auto text-center mb-12 slide-up delay-300">
<h2 class="text-section-title">{!! $kaufprozess['title'] !!}</h2>
@if (isset($kaufprozess['intro']))
<p class="text-large text-muted-foreground mt-4 leading-relaxed">
{{ $kaufprozess['intro'] }}
</p>
@endif
</div>
<div class="max-w-4xl mx-auto">
<div class="grid md:grid-cols-2 gap-6">
@foreach ($kaufprozess['steps'] ?? [] as $index => $step)
<div class="relative card-elevated rounded-2xl p-6 lg:p-8 slide-up delay-{{ 300 + $index * 100 }}">
<div class="flex gap-5 items-start">
<div class="shrink-0 w-12 h-12 rounded-full bg-secondary text-white flex items-center justify-center text-lg font-bold">
{{ $step['number'] ?? '' }}
</div>
<div>
<h3 class="text-lg font-semibold text-foreground mb-2">{{ $step['title'] ?? '' }}</h3>
<p class="text-sm text-muted-foreground leading-relaxed">{{ $step['description'] ?? '' }}</p>
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
</section>
@endif
{{-- Sektion 4: Die Brücke Marcels Pitch --}}
@if (!empty($bruecke))
<section class="section-padding">
<div class="container-padding">
<div class="grid lg:grid-cols-2 gap-12 lg:gap-16 items-center">
<div class="relative lg:order-1">
<div class="card-elevated rounded-3xl overflow-hidden slide-left delay-400">
<x-web-picture
src="{{ theme_image_url($bruecke['image'] ?? '') }}"
alt="{{ $bruecke['image_alt'] ?? 'Marcel Scheibe Ihre Brücke nach Dubai' }}"
class="w-full h-[500px] object-cover" />
@if (isset($bruecke['image_caption']))
<div class="absolute bottom-6 left-6 bg-card/95 backdrop-blur-sm rounded-xl p-4 shadow-lg border border-border/50 slide-left delay-500">
{{ $bruecke['image_caption'] }}
</div>
@endif
</div>
</div>
<div class="spacing-section lg:order-2">
<div class="spacing-content slide-right delay-300">
<h2 class="text-section-title">{!! $bruecke['title'] !!}</h2>
<div class="spacing-small text-large text-muted-foreground leading-relaxed">
@foreach ($bruecke['paragraphs'] ?? [] as $paragraph)
<p>{!! $paragraph !!}</p>
@endforeach
</div>
@if (isset($bruecke['advantage_title']))
<div class="mt-8 p-6 rounded-2xl bg-secondary/5 border border-secondary/20">
<h3 class="text-lg font-semibold text-foreground mb-2">{{ $bruecke['advantage_title'] }}</h3>
<p class="text-muted-foreground leading-relaxed">{{ $bruecke['advantage_text'] ?? '' }}</p>
</div>
@endif
@if (isset($bruecke['cta_text']))
<div class="mt-8">
<a href="{{ $bruecke['cta_link'] ?? '/contact' }}"
class="btn-primary-accent px-6 py-3">
{{ $bruecke['cta_text'] }}
</a>
</div>
@endif
</div>
</div>
</div>
</div>
</section>
@endif
{{-- Aktuelle Projekte --}}
@if (!empty($projects))
<section id="projekte" class="section-padding bg-accent">
<div class="container-padding">
<div class="text-center mb-12 slide-up delay-300">
<h2 class="text-section-title">{!! $projects['title'] ?? '' !!}</h2>
@if (isset($projects['subtitle']))
<p class="text-large text-muted-foreground mt-4 max-w-2xl mx-auto">
{{ $projects['subtitle'] }}
</p>
@endif
</div>
@php
$projectCount = count($projects['projects'] ?? []);
@endphp
<div class="grid md:grid-cols-1 lg:grid-cols-2 gap-8 slide-up delay-400">
@foreach ($projects['projects'] ?? [] as $project)
<div class="card-elevated rounded-2xl overflow-hidden {{ $projectCount === 1 ? 'lg:col-span-2 lg:max-w-[calc((100%-2rem)/2)] lg:mx-auto' : '' }}">
@if (isset($project['image']))
<div class="relative h-56 overflow-hidden">
<x-web-picture
src="{{ theme_image_url($project['image']) }}"
alt="{{ $project['title'] }}"
class="w-full h-full object-cover" />
@if (isset($project['status']))
<div class="absolute top-4 left-4">
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold bg-secondary text-white">
{{ $project['status'] }}
@if (isset($project['launch_date']))
<span class="opacity-80">({{ $project['launch_date'] }})</span>
@endif
</span>
</div>
@endif
</div>
@endif
<div class="p-6 lg:p-8">
<div class="mb-4">
<h3 class="text-xl font-semibold text-foreground">{{ $project['title'] }}</h3>
@if (isset($project['location']))
<p class="text-sm text-muted-foreground mt-1">{{ $project['location'] }}</p>
@endif
</div>
@if (isset($project['price_from']))
<p class="text-lg font-medium text-secondary mb-4">{{ $project['price_from'] }}</p>
@endif
@if (isset($project['highlights']))
<ul class="space-y-2 mb-6">
@foreach ($project['highlights'] as $highlight)
<li class="flex items-start gap-2 text-sm text-muted-foreground">
<svg class="w-4 h-4 text-secondary mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
{{ $highlight }}
</li>
@endforeach
</ul>
@endif
<a href="{{ isset($project['slug']) ? route('immobilien.show', $project['slug']) : ($projects['cta_link'] ?? '/contact') }}"
class="inline-flex items-center gap-2 whitespace-nowrap btn-primary-accent">
<span>{{ isset($project['slug']) ? 'Exposé ansehen' : ($projects['cta_text'] ?? 'Anfragen') }}</span>
<svg class="w-4 h-4 shrink-0 inline-block ml-2 mb-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</a>
</div>
</div>
@endforeach
</div>
</div>
</section>
@endif
{{-- Sektion 5: Mindset-Check --}}
@if (!empty($mindset))
<section class="section-padding">
<div class="container-padding">
<div class="max-w-4xl mx-auto">
<div class="rounded-2xl border-2 border-secondary/20 bg-secondary/5 p-8 lg:p-12 slide-up delay-300">
<h2 class="text-section-title text-center">{!! $mindset['title'] !!}</h2>
<div class="mt-8 space-y-6 text-large leading-relaxed">
@if (isset($mindset['text_positive']))
<p class="text-foreground">{!! $mindset['text_positive'] !!}</p>
@endif
@if (isset($mindset['text_negative']))
<p class="text-muted-foreground">{{ $mindset['text_negative'] }}</p>
@endif
@if (isset($mindset['closing']))
<p class="text-foreground pt-4 border-t border-secondary/20">{!! $mindset['closing'] !!}</p>
@endif
</div>
@if (isset($mindset['cta_text']))
<div class="mt-8 text-center">
<a href="{{ $mindset['cta_link'] ?? '/contact' }}"
class="btn-primary-accent px-8 py-4 text-lg">
{{ $mindset['cta_text'] }}
</a>
</div>
@endif
</div>
</div>
</div>
</section>
@endif
{{-- Möbel-Vorteil Banner (Teaser) --}}
@if (!empty($moebelVorteil))
<section class="section-padding bg-accent">
<div class="container-padding">
<div class="max-w-3xl mx-auto text-center slide-up delay-300">
<h2 class="text-section-title">{!! $moebelVorteil['title'] ?? '' !!}</h2>
<p class="text-large text-muted-foreground mt-4">
{{ $moebelVorteil['text'] ?? '' }}
</p>
@if (isset($moebelVorteil['button_text']))
<div class="mt-8">
<a href="{{ $moebelVorteil['button_link'] ?? '/netzwerk' }}"
class="btn-secondary-accent">
{{ $moebelVorteil['button_text'] }}
</a>
</div>
@endif
</div>
</div>
</section>
@endif
<livewire:web.components.sections.c-t-a-section />
</main>
<livewire:web.components.ui.footer />
</div>
@endsection
@push('styles')
<style>
[x-cloak] {
display: none !important;
}
</style>
@endpush

View file

@ -0,0 +1,45 @@
@extends('web.layouts.web-master')
@php
$legal = legal_page('impressum');
@endphp
@section('title', $legal['meta_title'] . ' - ' . ($domainName ?? config('app.name')))
@section('content')
<div class="bg-background">
<livewire:web.components.ui.header />
<main class="variante-glass-flow">
<section class="section-padding">
<div class="container-padding">
<div class="max-w-3xl mx-auto">
<h1 class="text-hero mb-8">{{ $legal['title'] }}</h1>
<p class="text-muted-foreground text-sm mb-12">
{{ $legal['subtitle'] }}
</p>
<div class="prose-legal space-y-8 text-foreground">
{!! $legal['content'] !!}
</div>
<div class="mt-12 pt-8 border-t border-border/30">
<a href="{{ url()->previous() }}" class="text-secondary hover:underline text-sm">
{{ $legal['back_link'] }}
</a>
</div>
</div>
</div>
</section>
</main>
<livewire:web.components.ui.footer />
</div>
@endsection
@push('styles')
<style>
[x-cloak] { display: none !important; }
.prose-legal a { transition: color 0.2s; }
</style>
@endpush

View file

@ -0,0 +1,160 @@
@extends('web.layouts.web-master')
@section('title', 'Einrichtungsnetzwerk - B2in Interior')
@section('content')
<div class="bg-background">
<livewire:web.components.ui.header />
<main class="variante-glass-flow">
<livewire:web.components.sections.partner-hero section="interior_hero" />
<livewire:web.components.sections.founder-bar />
{{-- Local-for-Local Konzept --}}
<livewire:web.components.sections.content-section section="interior_concept" layout="left" />
{{-- Zwei Marken --}}
@php
$brands = cms_theme_section('interior_brands');
$zielgruppen = cms_theme_section('interior_zielgruppen');
$process = cms_theme_section('interior_process');
@endphp
@if (!empty($brands))
<section class="section-padding bg-accent">
<div class="container-padding">
<div class="text-center mb-12 slide-up delay-300">
<h2 class="text-section-title">{!! $brands['title'] ?? '' !!}</h2>
@if (isset($brands['subtitle']))
<p class="text-large text-muted-foreground mt-4 max-w-2xl mx-auto">
{{ $brands['subtitle'] }}
</p>
@endif
</div>
<div class="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto slide-up delay-400">
@foreach ($brands['brands'] ?? [] as $brand)
<div class="card-elevated rounded-2xl overflow-hidden">
<div class="p-8 lg:p-10">
@if (isset($brand['logo']))
<div class="mb-6">
<img src="{{ asset($brand['logo']) }}"
alt="{{ $brand['name'] }}"
class="{{ $brand['logo_width'] ?? 'w-32' }}" />
</div>
@endif
<p class="text-xs font-semibold uppercase tracking-wider text-secondary mb-3">
{{ $brand['tagline'] ?? '' }}
</p>
<p class="text-muted-foreground leading-relaxed">
{{ $brand['description'] ?? '' }}
</p>
@if (isset($brand['audience']))
<p class="text-sm font-medium text-foreground mt-4">
{{ $brand['audience'] }}
</p>
@endif
@if (isset($brand['link']))
<div class="mt-6">
<a href="{{ $brand['link'] }}"
target="_blank" rel="noopener"
class="inline-flex items-center gap-2 text-sm font-medium text-secondary hover:text-secondary/80 transition-colors">
{{ $brand['name'] }} entdecken
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
</div>
@endif
</div>
</div>
@endforeach
</div>
</div>
</section>
@endif
{{-- Für wen? Zielgruppen --}}
@if (!empty($zielgruppen))
<section class="section-padding">
<div class="container-padding">
<div class="text-center mb-12 slide-up delay-300">
<h2 class="text-section-title">{!! $zielgruppen['title'] ?? '' !!}</h2>
</div>
<div class="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto slide-up delay-400">
@foreach ($zielgruppen['groups'] ?? [] as $group)
<div class="text-center p-6">
<div class="inline-flex items-center justify-center w-14 h-14 rounded-full bg-secondary/10 text-secondary mb-5">
<svg class="w-7 h-7" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
@switch($group['icon'] ?? '')
@case('home')
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
@break
@case('building-office-2')
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 21h19.5m-18-18v18m10.5-18v18m6-13.5V21M6.75 6.75h.75m-.75 3h.75m-.75 3h.75m3-6h.75m-.75 3h.75m-.75 3h.75M6.75 21v-3.375c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21M3 3h12m-.75 4.5H21m-3.75 3.75h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008z" />
@break
@case('clipboard-document-check')
<path stroke-linecap="round" stroke-linejoin="round" d="M11.35 3.836c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m8.9-4.414c.376.023.75.05 1.124.08 1.131.094 1.976 1.057 1.976 2.192V16.5A2.25 2.25 0 0118 18.75h-2.25m-7.5-10.5H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V18.75m-7.5-10.5h6.375c.621 0 1.125.504 1.125 1.125v9.375m-8.25-3l1.5 1.5 3-3.75" />
@break
@endswitch
</svg>
</div>
<h3 class="text-lg font-semibold text-foreground mb-3">{{ $group['title'] ?? '' }}</h3>
<p class="text-sm text-muted-foreground leading-relaxed">{{ $group['description'] ?? '' }}</p>
</div>
@endforeach
</div>
</div>
</section>
@endif
{{-- So funktioniert es - Prozess --}}
@if (!empty($process))
<section class="section-padding bg-accent">
<div class="container-padding">
<div class="text-center mb-12 slide-up delay-300">
<h2 class="text-section-title">{!! $process['title'] ?? '' !!}</h2>
</div>
<div class="max-w-3xl mx-auto slide-up delay-400">
<div class="space-y-8">
@foreach ($process['steps'] ?? [] as $step)
<div class="flex gap-6 items-start">
<div class="shrink-0 w-14 h-14 rounded-full bg-secondary text-white flex items-center justify-center text-lg font-bold">
{{ $step['number'] ?? '' }}
</div>
<div class="pt-2">
<h3 class="text-lg font-semibold text-foreground mb-2">{{ $step['title'] ?? '' }}</h3>
<p class="text-muted-foreground leading-relaxed">{{ $step['description'] ?? '' }}</p>
</div>
</div>
@endforeach
</div>
</div>
</div>
</section>
@endif
{{-- Trust / Marcel Scheibe --}}
<livewire:web.components.sections.content-section section="interior_trust" layout="right" />
{{-- CTA --}}
<livewire:web.components.sections.c-t-a-section />
</main>
<livewire:web.components.ui.footer />
</div>
@endsection
@push('styles')
<style>
[x-cloak] {
display: none !important;
}
</style>
@endpush
@push('scripts')
{{-- Alpine.js wird zentral im Layout geladen --}}
@endpush

View file

@ -7,15 +7,27 @@
<title>{{ $title ?? ($domainName ?? config('app.name', 'Laravel')) }}</title>
<!-- Domain-spezifisches Favicon -->
<link rel="icon" href="{{ asset(\App\Helpers\ThemeHelper::getFaviconPath()) }}">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<!-- Favicons (wie Backend) -->
<link rel="apple-touch-icon" sizes="57x57" href="/favicon/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/favicon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/favicon/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/favicon/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/favicon/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/favicon/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/favicon/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/favicon/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/favicon/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="96x96" href="/favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
<link rel="shortcut icon" href="{{ asset(\App\Helpers\ThemeHelper::getFaviconPath()) }}">
<link rel="manifest" href="/favicon/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/favicon/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
@php
$primaryFont = \App\Helpers\ThemeHelper::getPrimaryFont();
$secondaryFont = \App\Helpers\ThemeHelper::getSecondaryFont();
$theme = config('app.theme', 'b2in');
@endphp
@ -59,21 +71,6 @@
</script>
@stack('styles')
@if ($primaryFont === 'Inter' && $secondaryFont === 'IBM Plex Sans')
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700|ibm-plex-sans:400,500,600,700"
rel="stylesheet" />
@elseif($primaryFont === 'Inter' && $secondaryFont === 'Merriweather')
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700|merriweather:400,700" rel="stylesheet" />
@elseif($primaryFont === 'Inter' && $secondaryFont === 'Ephesis')
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700|ephesis:400" rel="stylesheet" />
@elseif($primaryFont === 'Inter' && $secondaryFont === 'EB Garamond')
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700|eb-garamond:400,500,600,700"
rel="stylesheet" />
@else
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700|ibm-plex-sans:400,500,600,700"
rel="stylesheet" />
@endif
</head>
<body class="antialiased bg-background text-foreground">
<livewire:web.components.ui.top-bar />

View file

@ -8,15 +8,42 @@
<title>@yield('title', $domainName ?? config('app.name', 'Laravel'))</title>
<!-- Domain-spezifisches Favicon -->
<link rel="icon" href="{{ asset(\App\Helpers\ThemeHelper::getFaviconPath()) }}">
<meta name="description" content="@yield('meta_description', 'B2in Connecting Design and Property. Exklusive Immobilien in Dubai & europäisches Einrichtungsnetzwerk.')">
@hasSection('meta_image')
<meta property="og:image" content="@yield('meta_image')">
@else
<meta property="og:image" content="{{ asset('img/assets/b2in-og-default.jpg') }}">
@endif
<meta property="og:title" content="@yield('title', $domainName ?? config('app.name', 'Laravel'))">
<meta property="og:description" content="@yield('meta_description', 'B2in Connecting Design and Property. Exklusive Immobilien in Dubai & europäisches Einrichtungsnetzwerk.')">
<meta property="og:type" content="website">
<meta property="og:url" content="{{ url()->current() }}">
<meta property="og:locale" content="de_DE">
<meta name="twitter:card" content="summary_large_image">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
@stack('meta')
<!-- Favicons (wie Backend) -->
<link rel="apple-touch-icon" sizes="57x57" href="/favicon/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/favicon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/favicon/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/favicon/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/favicon/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/favicon/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/favicon/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/favicon/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/favicon/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="96x96" href="/favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
<link rel="shortcut icon" href="{{ asset(\App\Helpers\ThemeHelper::getFaviconPath()) }}">
<link rel="manifest" href="/favicon/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/favicon/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
@php
$primaryFont = \App\Helpers\ThemeHelper::getPrimaryFont();
$secondaryFont = \App\Helpers\ThemeHelper::getSecondaryFont();
$theme = config('app.theme', 'b2in');
@endphp
@ -35,63 +62,59 @@
let topbarHeight = topbar.offsetHeight;
let isHeaderSticky = false;
let ticking = false;
function updateHeaderPosition() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
if (scrollTop >= topbarHeight && !isHeaderSticky) {
// TopBar ist nicht mehr sichtbar - Header wird sticky
header.classList.remove('header-normal');
header.classList.add('header-sticky');
isHeaderSticky = true;
} else if (scrollTop < topbarHeight && isHeaderSticky) {
// TopBar ist wieder sichtbar - Header wird normal
header.classList.remove('header-sticky');
header.classList.add('header-normal');
isHeaderSticky = false;
}
ticking = false;
}
// Initial check
updateHeaderPosition();
// Listen for scroll events
window.addEventListener('scroll', updateHeaderPosition);
window.addEventListener('scroll', function() {
if (!ticking) {
requestAnimationFrame(updateHeaderPosition);
ticking = true;
}
}, { passive: true });
// Listen for resize events (in case topbar height changes)
window.addEventListener('resize', function() {
topbarHeight = topbar.offsetHeight;
updateHeaderPosition();
});
}, { passive: true });
});
</script>
<!-- Additional Styles -->
@stack('styles')
<!-- Domain-spezifische Fonts -->
@if ($primaryFont === 'Inter' && $secondaryFont === 'IBM Plex Sans')
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700|ibm-plex-sans:400,500,600,700"
rel="stylesheet" />
@elseif($primaryFont === 'Inter' && $secondaryFont === 'Merriweather')
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700|merriweather:400,700" rel="stylesheet" />
@elseif($primaryFont === 'Inter' && $secondaryFont === 'Ephesis')
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700|ephesis:400" rel="stylesheet" />
@elseif($primaryFont === 'Inter' && $secondaryFont === 'EB Garamond')
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700|eb-garamond:400,500,600,700"
rel="stylesheet" />
@else
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700|ibm-plex-sans:400,500,600,700"
rel="stylesheet" />
@endif
</head>
<body class="antialiased bg-background text-foreground">
<!-- TopBar - statisch oben -->
<livewire:web.components.ui.top-bar />
{{-- GTM noscript (nur bei GTM-Nutzung) --}}
<x-cookie-consent::gtm-noscript />
<!-- Announcement Bar -->
<livewire:web.components.ui.announcement-bar />
{{-- TopBar (Backup: Sprachwechsel & Social Icons folgt später) --}}
{{-- <livewire:web.components.ui.top-bar /> --}}
@yield('content')
{{-- Cookie Consent Manager (vor Livewire-Scripts) --}}
<x-cookie-consent::manager />
<!-- Additional Scripts -->
@stack('scripts')
</body>

View file

@ -1,6 +1,6 @@
@extends('web.layouts.web-master')
@section('title', 'Magazin Artikel - B2In')
@section('title', 'Magazin Artikel - B2in')
@section('content')
<div class="bg-background">

View file

@ -1,6 +1,7 @@
@extends('web.layouts.web-master')
@section('title', 'B2In Magazin - Insights & Trends')
@section('title', 'B2in Magazin - Insights & Trends')
@section('meta_description', 'Insights rund um Immobilien-Investments in Dubai, Einrichtungstrends & Supply-Chain-Strategien im B2in Magazin.')
@section('content')
<div class="bg-background">
@ -19,11 +20,5 @@
[x-cloak] {
display: none !important;
}
</style>
@endpush
@push('scripts')
{{-- Alpine.js wird zentral im Layout geladen --}}
@endpush

View file

@ -0,0 +1,183 @@
@extends('web.layouts.web-master')
@section('title', 'Netzwerk & Ökosystem - B2in')
@section('meta_description', 'Das B2in-Netzwerk: Europäisches Einrichtungsnetzwerk, Fachhändler-Partnerschaften & Marken-Kooperationen in Entwicklung.')
@section('content')
<div class="bg-background">
<livewire:web.components.ui.header />
<main class="variante-glass-flow">
@php
$hero = cms_theme_section('netzwerk_hero');
$teasers = cms_theme_section('netzwerk_teasers');
$cta = cms_theme_section('netzwerk_cta');
$cabinet = cms_theme_section('netzwerk_cabinet_partner');
$immobilienHint = cms_theme_section('netzwerk_immobilien_hint');
@endphp
{{-- Hero --}}
@if (!empty($hero))
<section class="section-padding flex items-center relative border-b border-border/30">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20 bg-hero-container rounded-[20px] w-[95%]">
<div class="grid lg:grid-cols-2 gap-16 items-center">
<div class="space-y-8">
<div class="slide-right delay-300">
<h1 class="text-hero mb-6">
{!! $hero['title'] !!}
</h1>
<p class="text-lg text-muted-foreground max-w-md leading-relaxed">
{{ $hero['subtitle'] ?? '' }}
</p>
</div>
</div>
<div class="relative">
<div class="relative rounded-3xl overflow-hidden shadow-elevated slide-left delay-300">
<x-web-picture
src="{{ theme_image_url($hero['image'] ?? '') }}"
alt="{{ $hero['image_alt'] ?? 'B2in Netzwerk & Ökosystem' }}"
class="w-full h-[500px] object-cover"
loading="" />
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
</div>
</div>
</div>
</div>
</section>
@endif
<livewire:web.components.sections.founder-bar />
{{-- Teaser-Kacheln --}}
@if (!empty($teasers))
<section class="section-padding">
<div class="container-padding">
<div class="text-center mb-12 slide-up delay-300">
<h2 class="text-section-title">{!! $teasers['title'] !!}</h2>
</div>
<div class="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto">
@foreach ($teasers['cards'] ?? [] as $index => $card)
<div class="card-elevated rounded-2xl p-8 text-center slide-up delay-{{ 300 + $index * 100 }}">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-secondary/10 text-secondary mb-6">
@svg('heroicon-o-' . ($card['icon'] ?? 'squares-2x2'), 'w-8 h-8')
</div>
<h3 class="text-lg font-semibold text-foreground mb-3">{{ $card['title'] ?? '' }}</h3>
<p class="text-sm text-muted-foreground leading-relaxed mb-6">{{ $card['description'] ?? '' }}</p>
@if (isset($card['status']))
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold bg-secondary/10 text-secondary">
<span class="w-1.5 h-1.5 rounded-full bg-secondary animate-pulse"></span>
{{ $card['status'] }}
</span>
@endif
</div>
@endforeach
</div>
</div>
</section>
@endif
<livewire:web.components.sections.image-break section="netzwerk_image_break" />
{{-- CABINET Premiumpartner --}}
@if (! empty($cabinet))
<section class="section-padding">
<div class="container-padding">
<div class="max-w-6xl mx-auto">
<div class="grid lg:grid-cols-2 gap-12 items-center">
<div class="space-y-6 slide-right delay-300">
@if (! empty($cabinet['badge'] ?? null))
<div class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-secondary/10 text-secondary text-sm font-semibold">
<span class="w-2 h-2 rounded-full bg-secondary"></span>
{{ $cabinet['badge'] }}
</div>
@endif
@if (! empty($cabinet['title'] ?? null))
<h2 class="text-section-title">
{!! $cabinet['title'] !!}
</h2>
@endif
@if (! empty($cabinet['lead'] ?? null))
<p class="text-lg font-medium text-foreground">{{ $cabinet['lead'] }}</p>
@endif
@foreach ($cabinet['paragraphs'] ?? [] as $paragraph)
<p class="text-muted-foreground leading-relaxed">{{ $paragraph }}</p>
@endforeach
</div>
<div class="relative slide-left delay-300">
<div class="card-elevated rounded-3xl overflow-hidden p-10 flex items-center justify-center bg-white min-h-[300px]">
<img
src="{{ theme_image_url($cabinet['image'] ?? 'b2in/cabinet_logo.png') }}"
alt="{{ $cabinet['image_alt'] ?? 'CABINET' }}"
class="max-w-xs w-full object-contain"
/>
</div>
</div>
</div>
</div>
</div>
</section>
@endif
{{-- Immobilien-Vorteil Hinweis --}}
@if (! empty($immobilienHint))
<section class="section-padding bg-accent">
<div class="container-padding">
<div class="max-w-3xl mx-auto text-center slide-up delay-300">
<div class="inline-flex items-center justify-center w-14 h-14 rounded-full bg-secondary/10 text-secondary mb-6">
@svg('heroicon-o-building-office-2', 'w-7 h-7')
</div>
@if (! empty($immobilienHint['title'] ?? null))
<h2 class="text-section-title">{!! $immobilienHint['title'] !!}</h2>
@endif
@if (! empty($immobilienHint['description'] ?? null))
<p class="text-large text-muted-foreground mt-4 leading-relaxed">
{{ $immobilienHint['description'] }}
</p>
@endif
@if (! empty($immobilienHint['button_text'] ?? null))
<div class="mt-8">
<a href="{{ url($immobilienHint['button_link'] ?? '/immobilien') }}" class="btn-primary-accent px-8 py-4 text-lg">
{{ $immobilienHint['button_text'] }}
</a>
</div>
@endif
</div>
</div>
</section>
@endif
{{-- CTA --}}
@if (!empty($cta))
<section class="section-padding">
<div class="container-padding">
<div class="max-w-3xl mx-auto text-center slide-up delay-300">
<h2 class="text-section-title">{!! $cta['title'] !!}</h2>
<p class="text-large text-muted-foreground mt-4 leading-relaxed">
{{ $cta['text'] ?? '' }}
</p>
@if (isset($cta['button_text']))
<div class="mt-8">
<a href="{{ $cta['button_link'] ?? '/contact' }}"
class="btn-primary-accent px-8 py-4 text-lg">
{{ $cta['button_text'] }}
</a>
</div>
@endif
</div>
</div>
</section>
@endif
</main>
<livewire:web.components.ui.footer />
</div>
@endsection
@push('styles')
<style>
[x-cloak] {
display: none !important;
}
</style>
@endpush

View file

@ -1,6 +1,6 @@
@extends('web.layouts.web-master')
@section('title', 'Partner werden - B2In Ecosystem')
@section('title', 'Für Entwickler & Partner - B2in')
@section('content')
<div class="bg-background">
@ -8,10 +8,13 @@
<main class="variante-glass-flow">
<livewire:web.components.sections.partner-hero />
<livewire:web.components.sections.card-section bg="bg-accent" section="partner_card_section" />
<livewire:web.components.sections.benefits-section section="partner_benefits_retailer" />
<livewire:web.components.sections.benefits-section section="partner_benefits_supplier" layout="right" bg="bg-accent" />
<livewire:web.components.sections.benefits-section section="partner_benefits_broker" />
<livewire:web.components.sections.founder-bar />
<livewire:web.components.sections.content-section section="supply_chain_intro" layout="left" bg="bg-accent" />
<livewire:web.components.sections.card-section section="partner_card_section" />
<livewire:web.components.sections.benefits-section section="partner_benefits_developer" />
<livewire:web.components.sections.benefits-section section="partner_benefits_retailer" layout="right" bg="bg-accent" />
<livewire:web.components.sections.benefits-section section="partner_benefits_supplier" />
<livewire:web.components.sections.benefits-section section="partner_benefits_broker" layout="right" bg="bg-accent" />
<livewire:web.components.sections.partner-process />
<livewire:web.components.sections.commitment-section />
<livewire:web.components.sections.partner-c-t-a />

View file

@ -1,6 +1,6 @@
@extends('web.layouts.web-master')
@section('title', 'B2In Magazin - Insights & Trends')
@section('title', 'B2in Magazin - Insights & Trends')
@section('content')
<div class="bg-background">

View file

@ -0,0 +1,50 @@
@extends('web.layouts.web-master')
@php
$legal = legal_page('privacy');
@endphp
@section('title', $legal['meta_title'] . ' - ' . ($domainName ?? config('app.name')))
@section('content')
<div class="bg-background">
<livewire:web.components.ui.header />
<main class="variante-glass-flow">
<section class="section-padding">
<div class="container-padding">
<div class="max-w-3xl mx-auto">
<h1 class="text-hero mb-8">{{ $legal['title'] }}</h1>
<p class="text-muted-foreground text-sm mb-12">
{{ $legal['subtitle'] }}
</p>
<div class="prose-legal space-y-8 text-foreground">
{!! $legal['content'] !!}
</div>
{{-- Cookie-Einstellungen & Google-Analytics-Infos (CookieConsent-Paket) --}}
<div class="prose-legal space-y-8 text-foreground">
<x-cookie-consent::privacy-info />
</div>
<div class="mt-12 pt-8 border-t border-border/30">
<a href="{{ url()->previous() }}" class="text-secondary hover:underline text-sm">
{{ $legal['back_link'] }}
</a>
</div>
</div>
</div>
</section>
</main>
<livewire:web.components.ui.footer />
</div>
@endsection
@push('styles')
<style>
[x-cloak] { display: none !important; }
.prose-legal a { transition: color 0.2s; }
</style>
@endpush

View file

@ -0,0 +1,45 @@
@extends('web.layouts.web-master')
@php
$legal = legal_page('terms');
@endphp
@section('title', $legal['meta_title'] . ' - ' . ($domainName ?? config('app.name')))
@section('content')
<div class="bg-background">
<livewire:web.components.ui.header />
<main class="variante-glass-flow">
<section class="section-padding">
<div class="container-padding">
<div class="max-w-3xl mx-auto">
<h1 class="text-hero mb-8">{{ $legal['title'] }}</h1>
<p class="text-muted-foreground text-sm mb-12">
{{ $legal['subtitle'] }}
</p>
<div class="prose-legal space-y-8 text-foreground">
{!! $legal['content'] !!}
</div>
<div class="mt-12 pt-8 border-t border-border/30">
<a href="{{ url()->previous() }}" class="text-secondary hover:underline text-sm">
{{ $legal['back_link'] }}
</a>
</div>
</div>
</div>
</section>
</main>
<livewire:web.components.ui.footer />
</div>
@endsection
@push('styles')
<style>
[x-cloak] { display: none !important; }
.prose-legal a { transition: color 0.2s; }
</style>
@endpush