10-04-2026
This commit is contained in:
parent
4d6b4930b2
commit
4bb89aad8c
836 changed files with 52961 additions and 5950 deletions
|
|
@ -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>
|
||||
|
|
|
|||
18
resources/views/components/web-picture.blade.php
Normal file
18
resources/views/components/web-picture.blade.php
Normal 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
|
||||
73
resources/views/layouts/cabinet-quick.blade.php
Normal file
73
resources/views/layouts/cabinet-quick.blade.php
Normal 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>
|
||||
367
resources/views/livewire/admin/cms/articles-index.blade.php
Normal file
367
resources/views/livewire/admin/cms/articles-index.blade.php
Normal 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>
|
||||
165
resources/views/livewire/admin/cms/cabinet-info-tablet.blade.php
Normal file
165
resources/views/livewire/admin/cms/cabinet-info-tablet.blade.php
Normal 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>
|
||||
782
resources/views/livewire/admin/cms/content-index.blade.php
Normal file
782
resources/views/livewire/admin/cms/content-index.blade.php
Normal 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>
|
||||
156
resources/views/livewire/admin/cms/dashboard.blade.php
Normal file
156
resources/views/livewire/admin/cms/dashboard.blade.php
Normal 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. 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. 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. 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. 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>
|
||||
294
resources/views/livewire/admin/cms/display-dashboard.blade.php
Normal file
294
resources/views/livewire/admin/cms/display-dashboard.blade.php
Normal 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. 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 (Montag–Sonntag) 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. 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. 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>
|
||||
204
resources/views/livewire/admin/cms/display-list.blade.php
Normal file
204
resources/views/livewire/admin/cms/display-list.blade.php
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
441
resources/views/livewire/admin/cms/media-index.blade.php
Normal file
441
resources/views/livewire/admin/cms/media-index.blade.php
Normal 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>
|
||||
|
|
@ -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>
|
||||
124
resources/views/livewire/admin/cms/media-picker.blade.php
Normal file
124
resources/views/livewire/admin/cms/media-picker.blade.php
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
720
resources/views/livewire/admin/cms/projects-index.blade.php
Normal file
720
resources/views/livewire/admin/cms/projects-index.blade.php
Normal 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. <span class="text-secondary">)" 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>
|
||||
|
|
@ -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());
|
||||
|
||||
?>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
304
resources/views/livewire/cabinet/quick-status.blade.php
Normal file
304
resources/views/livewire/cabinet/quick-status.blade.php
Normal 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>
|
||||
|
|
@ -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 = '';
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 --}}
|
||||
|
|
|
|||
|
|
@ -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 --}}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 }}">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
32
resources/views/web/about_bak.blade.php
Normal file
32
resources/views/web/about_bak.blade.php
Normal 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
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
50
resources/views/web/cookie-policy.blade.php
Normal file
50
resources/views/web/cookie-policy.blade.php
Normal 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
|
||||
134
resources/views/web/dev/immobilien-v1.blade.php
Normal file
134
resources/views/web/dev/immobilien-v1.blade.php
Normal 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
|
||||
160
resources/views/web/dev/interior-v1.blade.php
Normal file
160
resources/views/web/dev/interior-v1.blade.php
Normal 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
|
||||
38
resources/views/web/dev/partner-v1.blade.php
Normal file
38
resources/views/web/dev/partner-v1.blade.php
Normal 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
|
||||
120
resources/views/web/dev/sitemap.blade.php
Normal file
120
resources/views/web/dev/sitemap.blade.php
Normal 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
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
363
resources/views/web/immobilien-show.blade.php
Normal file
363
resources/views/web/immobilien-show.blade.php
Normal 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
|
||||
|
||||
314
resources/views/web/immobilien.blade.php
Normal file
314
resources/views/web/immobilien.blade.php
Normal 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
|
||||
45
resources/views/web/impressum.blade.php
Normal file
45
resources/views/web/impressum.blade.php
Normal 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
|
||||
160
resources/views/web/interior.blade.php
Normal file
160
resources/views/web/interior.blade.php
Normal 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
|
||||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
183
resources/views/web/netzwerk.blade.php
Normal file
183
resources/views/web/netzwerk.blade.php
Normal 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
|
||||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
50
resources/views/web/privacy.blade.php
Normal file
50
resources/views/web/privacy.blade.php
Normal 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
|
||||
45
resources/views/web/terms.blade.php
Normal file
45
resources/views/web/terms.blade.php
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue