B2in Interior: Relaunch der Website, Kernmarken-CMS und Terminanfrage
Umsetzung von AP0-AP6 aus dev/2026-08-28/entwicklungsplan.md sowie der vorab gefundenen Befunde T1 und B1-B13. Befunde - CmsContentSeeder überschrieb bei jedem Lauf alle redaktionellen Inhalte; jetzt Redaktionsschutz plus gezielter Relaunch-Seeder (B1) - Magazin-CMS war nicht ans Frontend angebunden, Artikel jetzt über Slugs mit 301 von den alten numerischen URLs (B2) - Vier veröffentlichte Display-Playlists zeigten auf b2in.test und wären live nicht ladbar gewesen (B13) - Info-Tablet speicherte geleerte Öffnungszeiten nicht (T1) - /service und /portfolio lieferten seit Februar 500 (B12) Website - Startseite neu strukturiert und umgetextet, eigene Seite /marken mit Detailseite je Kernmarke, /netzwerk und Altrouten leiten dorthin - Galerie je Marke, getrennt nach realisierten Projekten und Visualisierungen; ohne gesetzte Kategorie gilt ein Bild als Entwurf - Seite /referenzen, gespeist aus den Markengalerien - Showroom-Display im 9:16-Rahmen über eine schreibgeschützte Embed-Route ohne Preview-Token - Terminanfrage-Widget auf allen Seiten, Markenauswahl aus dem CMS, WhatsApp per Klick-zum-Chat-Link mit vorbereitetem Text - FAQ, Über-uns, Kontakt und Magazin inhaltlich auf Interior umgestellt Sonstiges - Vorschau-Instanzen liefern noindex und verlinken sich bei unbekanntem Host auf sich selbst statt auf eine fremde Domain - Seeder-Zugangsdaten aus dem Code in die Konfiguration (B8) - Ablauf für die Vorschau: dev/2026-08-28/preview-deployment.md 833 Tests grün (3084 Assertions), Pint sauber. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
193d37b1ce
commit
6d53243ea6
132 changed files with 8672 additions and 335 deletions
|
|
@ -0,0 +1,51 @@
|
|||
@php
|
||||
if (!function_exists('renderHeroIcon')) {
|
||||
function renderHeroIcon($iconName, $style = 'outline', $color = 'text-secondary')
|
||||
{
|
||||
$iconPath = public_path("heroicons/optimized/24/{$style}/{$iconName}.svg");
|
||||
$fallbackPath = public_path('heroicons/optimized/24/outline/sparkles.svg');
|
||||
|
||||
if (file_exists($iconPath)) {
|
||||
return str_replace('<svg', '<svg class="w-10 h-10 ' . $color . '"', file_get_contents($iconPath));
|
||||
}
|
||||
|
||||
if (file_exists($fallbackPath)) {
|
||||
return str_replace('<svg', '<svg class="w-10 h-10 ' . $color . '"', file_get_contents($fallbackPath));
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
<section class="section-padding {{ $bg }}" id="wege">
|
||||
<div class="container-padding">
|
||||
<div class="text-center mb-16 slide-up delay-300">
|
||||
<h2 class="text-section-title">{!! $content['title'] ?? '' !!}</h2>
|
||||
@if (!empty($content['subtitle']))
|
||||
<p class="text-large text-muted-foreground mt-4 max-w-2xl mx-auto">{{ $content['subtitle'] }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-8 lg:gap-12 slide-up delay-400">
|
||||
@foreach ($content['cards'] ?? [] as $index => $card)
|
||||
<div class="card-elevated rounded-2xl p-8 flex flex-col" wire:key="audience-card-{{ $index }}">
|
||||
<div class="w-20 h-20 icon-secondary-linear glow-soft rounded-2xl flex items-center justify-center mb-6">
|
||||
{!! renderHeroIcon($card['icon'] ?? 'home', 'outline', 'text-secondary-foreground') !!}
|
||||
</div>
|
||||
|
||||
<h3 class="text-xl font-medium mb-3">{{ $card['title'] ?? '' }}</h3>
|
||||
<p class="text-muted-foreground leading-relaxed grow">{!! $card['description'] ?? '' !!}</p>
|
||||
|
||||
@if (!empty($card['button_text']))
|
||||
<div class="pt-6">
|
||||
<a href="{{ $card['button_link'] ?? '/contact' }}" class="btn-primary-accent px-6 py-3 inline-block">
|
||||
{{ $card['button_text'] }}
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
{{-- Detailseiten-Vorlage laut umsetzung-inhalte.md §5:
|
||||
Header (Logo + Claim + Stimmungsbild) → Einleitung → Leistungen → Galerie → CTA --}}
|
||||
<article>
|
||||
{{-- 1. Header --}}
|
||||
<section class="section-padding">
|
||||
<div class="container-padding">
|
||||
<a href="{{ route('marken') }}"
|
||||
class="inline-flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors mb-8">
|
||||
<span aria-hidden="true">←</span> {{ __('ui.all_brands') }}
|
||||
</a>
|
||||
|
||||
<div class="grid lg:grid-cols-2 gap-12 items-center">
|
||||
<div>
|
||||
@if (!empty($brand['logo']))
|
||||
<img src="{{ theme_image_url($brand['logo']) }}" alt="{{ $brand['name'] }}"
|
||||
class="h-10 w-auto mb-6 object-contain object-left" />
|
||||
@endif
|
||||
|
||||
<h1 class="text-section-title">{{ $brand['name'] }}</h1>
|
||||
<p class="text-large text-secondary font-medium mt-2">{{ $brand['tagline'] }}</p>
|
||||
|
||||
@if (!empty($brand['showroom']))
|
||||
<p class="text-muted-foreground mt-4 inline-flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-secondary" fill="none" stroke="currentColor" stroke-width="1.5"
|
||||
viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
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>
|
||||
{{ $brand['showroom'] }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@if (!empty($brand['image']))
|
||||
<img src="{{ theme_image_url($brand['image']) }}" alt="{{ $brand['name'] }}"
|
||||
class="w-full rounded-2xl object-cover" />
|
||||
@else
|
||||
<div class="w-full aspect-[4/3] rounded-2xl bg-muted flex items-center justify-center">
|
||||
<span class="text-muted-foreground text-sm">{{ $brand['name'] }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{-- 2. Einleitung --}}
|
||||
@if (!empty($brand['description']))
|
||||
<section class="pb-16">
|
||||
<div class="container-narrow">
|
||||
<div class="prose-content text-large text-muted-foreground leading-relaxed">
|
||||
{!! $brand['description'] !!}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
{{-- 3. Leistungen --}}
|
||||
@if (!empty($brand['services']))
|
||||
<section class="section-padding bg-accent">
|
||||
<div class="container-padding">
|
||||
<h2 class="text-section-title text-center mb-12">{{ __('ui.brand_services') }}</h2>
|
||||
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
@foreach ($brand['services'] as $index => $service)
|
||||
<div class="card-elevated rounded-2xl p-6" wire:key="service-{{ $index }}">
|
||||
<h3 class="text-lg font-medium">{{ $service['title'] ?? '' }}</h3>
|
||||
<p class="text-muted-foreground leading-relaxed mt-2">{{ $service['description'] ?? '' }}</p>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
{{-- 4. Galerie: realisierte Projekte und Inspirationen strikt getrennt --}}
|
||||
@php
|
||||
$galleryProjects = $gallery['projects'] ?? [];
|
||||
$galleryInspirations = $gallery['inspirations'] ?? [];
|
||||
$hasBothGalleryGroups = $galleryProjects !== [] && $galleryInspirations !== [];
|
||||
$initialGalleryTab = $galleryProjects !== [] ? 'projects' : 'inspirations';
|
||||
|
||||
$galleryLightbox = collect(['projects' => $galleryProjects, 'inspirations' => $galleryInspirations])
|
||||
->map(fn (array $items) => collect($items)->map(fn (array $item) => [
|
||||
'src' => theme_image_url($item['image']),
|
||||
'caption' => $item['caption'] ?? '',
|
||||
'badge' => $item['category'] === \App\Models\CmsBrand::GALLERY_PROJECT
|
||||
? __('ui.brand_gallery_badge_project')
|
||||
: __('ui.brand_gallery_badge_inspiration'),
|
||||
])->values()->all())
|
||||
->all();
|
||||
@endphp
|
||||
|
||||
@if ($galleryProjects || $galleryInspirations)
|
||||
<section id="galerie" class="section-padding"
|
||||
x-data="{
|
||||
tab: '{{ $initialGalleryTab }}',
|
||||
groups: @js($galleryLightbox),
|
||||
lightbox: false,
|
||||
current: 0,
|
||||
get items() { return this.groups[this.tab] ?? [] },
|
||||
open(index) { this.current = index; this.lightbox = true },
|
||||
next() { this.current = (this.current + 1) % this.items.length },
|
||||
prev() { this.current = (this.current - 1 + this.items.length) % this.items.length },
|
||||
}">
|
||||
<div class="container-padding">
|
||||
<div class="text-center max-w-2xl mx-auto">
|
||||
<h2 class="text-section-title">{{ __('ui.brand_gallery_title') }}</h2>
|
||||
<p class="text-muted-foreground leading-relaxed mt-4">{{ __('ui.brand_gallery_intro') }}</p>
|
||||
</div>
|
||||
|
||||
@if ($hasBothGalleryGroups)
|
||||
<div class="flex justify-center mt-10" role="tablist" aria-label="{{ __('ui.brand_gallery_title') }}">
|
||||
<div class="inline-flex rounded-full bg-accent p-1 gap-1">
|
||||
@foreach (['projects' => __('ui.brand_gallery_tab_projects'), 'inspirations' => __('ui.brand_gallery_tab_inspirations')] as $tabKey => $tabLabel)
|
||||
<button type="button" role="tab" wire:key="gallery-tab-{{ $tabKey }}"
|
||||
x-on:click="tab = '{{ $tabKey }}'"
|
||||
x-bind:aria-selected="tab === '{{ $tabKey }}' ? 'true' : 'false'"
|
||||
x-bind:class="tab === '{{ $tabKey }}'
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground'"
|
||||
class="px-6 py-2.5 rounded-full text-sm font-medium transition-colors cursor-pointer">
|
||||
{{ $tabLabel }}
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@foreach (['projects' => $galleryProjects, 'inspirations' => $galleryInspirations] as $groupKey => $groupItems)
|
||||
@if ($groupItems)
|
||||
<div wire:key="gallery-group-{{ $groupKey }}"
|
||||
@if ($hasBothGalleryGroups) x-show="tab === '{{ $groupKey }}'" x-cloak @endif
|
||||
class="mt-10">
|
||||
@unless ($hasBothGalleryGroups)
|
||||
<h3 class="text-xl font-medium text-center mb-8">
|
||||
{{ $groupKey === 'projects' ? __('ui.brand_gallery_tab_projects') : __('ui.brand_gallery_tab_inspirations') }}
|
||||
</h3>
|
||||
@endunless
|
||||
|
||||
@if ($groupKey === 'inspirations')
|
||||
<p class="text-sm text-muted-foreground text-center max-w-2xl mx-auto mb-8">
|
||||
{{ __('ui.brand_gallery_inspiration_note') }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
@foreach ($groupItems as $index => $item)
|
||||
<figure wire:key="gallery-{{ $groupKey }}-{{ $index }}" class="group">
|
||||
<button type="button" x-on:click="open({{ $index }})"
|
||||
class="relative block w-full overflow-hidden rounded-2xl cursor-pointer">
|
||||
<img src="{{ theme_image_url($item['image']) }}"
|
||||
alt="{{ $item['caption'] ?: $brand['name'] }}" loading="lazy"
|
||||
class="w-full aspect-[4/3] object-cover transition-transform duration-300 group-hover:scale-105" />
|
||||
<span class="absolute top-3 left-3 rounded-full bg-black/60 px-3 py-1 text-xs font-medium text-white backdrop-blur-sm">
|
||||
{{ $groupKey === 'projects' ? __('ui.brand_gallery_badge_project') : __('ui.brand_gallery_badge_inspiration') }}
|
||||
</span>
|
||||
</button>
|
||||
@if (!empty($item['caption']))
|
||||
<figcaption class="text-sm text-muted-foreground mt-3">{{ $item['caption'] }}</figcaption>
|
||||
@endif
|
||||
</figure>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- Lightbox: ins body geteleportet, damit keine Eltern-Constraints stören --}}
|
||||
<template x-teleport="body">
|
||||
<div x-show="lightbox" x-cloak class="fixed inset-0 z-50 bg-black/90"
|
||||
x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0"
|
||||
x-transition:leave="transition ease-in duration-200" x-transition:leave-end="opacity-0"
|
||||
x-on:keydown.escape.window="lightbox = false"
|
||||
x-on:keydown.arrow-right.window="lightbox && next()"
|
||||
x-on:keydown.arrow-left.window="lightbox && prev()">
|
||||
<div class="absolute inset-0 flex flex-col items-center justify-center gap-4 p-6">
|
||||
<img x-bind:src="items[current]?.src" x-bind:alt="items[current]?.caption"
|
||||
class="max-h-[80vh] max-w-[90vw] rounded-lg object-contain" />
|
||||
<p class="text-center text-sm text-white/80">
|
||||
<span class="rounded-full border border-white/30 px-3 py-1" x-text="items[current]?.badge"></span>
|
||||
<span class="ml-3" x-text="items[current]?.caption"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button type="button" x-on:click="lightbox = false" aria-label="{{ __('ui.close') }}"
|
||||
class="absolute top-6 right-6 flex h-12 w-12 cursor-pointer items-center justify-center rounded-full border border-white/20 bg-white/10 text-white/90 transition-all hover:bg-white/20 hover:text-white">
|
||||
<svg class="h-6 w-6" 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" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button type="button" x-show="items.length > 1" x-on:click="prev()" aria-label="{{ __('ui.previous') }}"
|
||||
class="absolute left-4 top-1/2 flex h-12 w-12 -translate-y-1/2 cursor-pointer items-center justify-center rounded-full border border-white/20 bg-white/10 text-white/90 transition-all hover:bg-white/20">
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button type="button" x-show="items.length > 1" x-on:click="next()" aria-label="{{ __('ui.next') }}"
|
||||
class="absolute right-4 top-1/2 flex h-12 w-12 -translate-y-1/2 cursor-pointer items-center justify-center rounded-full border border-white/20 bg-white/10 text-white/90 transition-all hover:bg-white/20">
|
||||
<svg class="h-6 w-6" 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" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
{{-- 5. CTA --}}
|
||||
<section class="section-padding bg-secondary text-secondary-foreground">
|
||||
<div class="container-narrow text-center spacing-section">
|
||||
<h2 class="text-section-title text-white">{{ __('ui.brand_cta_title', ['name' => $brand['name']]) }}</h2>
|
||||
<p class="text-large max-w-2xl mx-auto leading-relaxed">{{ __('ui.brand_cta_text') }}</p>
|
||||
|
||||
<div class="pt-4 flex flex-wrap items-center justify-center gap-4">
|
||||
<a href="{{ route('contact') }}" class="btn-primary-accent px-8 py-4 text-lg">
|
||||
{{ __('ui.request_appointment') }}
|
||||
</a>
|
||||
@if (!empty($brand['website_url']))
|
||||
<a href="{{ $brand['website_url'] }}" target="_blank" rel="noopener"
|
||||
class="px-8 py-4 text-lg underline underline-offset-4">
|
||||
{{ __('ui.brand_website') }}
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
{{-- Der WhatsApp-Button kommt mit AP3, sobald die Nummer bestätigt ist. --}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{-- Weitere Marken --}}
|
||||
@if (!empty($otherBrands))
|
||||
<section class="section-padding bg-accent">
|
||||
<div class="container-padding">
|
||||
<h2 class="text-section-title text-center mb-12">{{ __('ui.other_brands') }}</h2>
|
||||
|
||||
<div class="grid sm:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
||||
@foreach ($otherBrands as $other)
|
||||
<a href="{{ route('marken.show', ['slug' => $other['slug']]) }}"
|
||||
wire:key="other-{{ $other['slug'] }}"
|
||||
class="card-elevated rounded-2xl p-8 block hover:shadow-lg transition-shadow duration-300">
|
||||
<h3 class="text-xl font-medium">{{ $other['name'] }}</h3>
|
||||
<p class="text-secondary font-medium mt-1">{{ $other['tagline'] }}</p>
|
||||
<p class="text-muted-foreground leading-relaxed mt-3">{!! $other['teaser'] !!}</p>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
</article>
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
{{-- Übersichtsseite /marken: je Marke Logo, Bild, Claim, Text, drei Merkmale und der Weg
|
||||
zur Detailseite. Jede Marke steht in einem eigenen Rahmen, damit klar ist, was zusammengehört.
|
||||
Abwechselnde Bildseite, damit die Liste nicht monoton wird. --}}
|
||||
<section class="section-padding {{ $bg }}">
|
||||
<div class="container-padding space-y-10 lg:space-y-14">
|
||||
@foreach ($brands as $index => $brand)
|
||||
<article class="card-elevated rounded-3xl p-6 sm:p-8 lg:p-12" wire:key="brand-{{ $brand['slug'] }}">
|
||||
<div class="grid lg:grid-cols-2 gap-10 lg:gap-16 items-center">
|
||||
<div class="{{ $loop->even ? '' : 'lg:order-2' }}">
|
||||
@if (!empty($brand['image']))
|
||||
<img src="{{ theme_image_url($brand['image']) }}" alt="{{ $brand['name'] }}"
|
||||
class="w-full rounded-2xl object-cover aspect-[4/3]" loading="lazy" />
|
||||
@else
|
||||
{{-- Platzhalter, solange die Bildmotive fehlen (offene Zulieferung). --}}
|
||||
<div class="w-full aspect-[4/3] rounded-2xl bg-muted flex items-center justify-center">
|
||||
<span class="text-muted-foreground text-sm">{{ $brand['name'] }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="{{ $loop->even ? '' : 'lg:order-1' }}">
|
||||
@if (!empty($brand['logo']))
|
||||
<img src="{{ theme_image_url($brand['logo']) }}" alt="{{ $brand['name'] }}"
|
||||
class="h-9 w-auto mb-5 object-contain object-left" loading="lazy" />
|
||||
@endif
|
||||
|
||||
<h2 class="text-2xl lg:text-3xl font-medium">{{ $brand['name'] }}</h2>
|
||||
<p class="text-large text-secondary font-medium mt-1">{{ $brand['tagline'] }}</p>
|
||||
|
||||
@foreach (preg_split('/\R{2,}/', trim((string) ($brand['summary'] ?: $brand['teaser']))) as $i => $paragraph)
|
||||
<p class="text-muted-foreground leading-relaxed mt-4">{!! nl2br(e($paragraph)) !!}</p>
|
||||
@endforeach
|
||||
|
||||
@if (!empty($brand['highlights']))
|
||||
<ul class="mt-6 space-y-2 pt-6 border-t border-border/60">
|
||||
@foreach ($brand['highlights'] as $i => $highlight)
|
||||
<li class="flex items-start gap-3" wire:key="hl-{{ $brand['slug'] }}-{{ $i }}">
|
||||
<svg class="w-5 h-5 text-secondary shrink-0 mt-0.5" fill="none" stroke="currentColor"
|
||||
stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
<span class="text-muted-foreground">{{ is_array($highlight) ? ($highlight['title'] ?? '') : $highlight }}</span>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
|
||||
@if (!empty($brand['has_detail_page']))
|
||||
<a href="{{ route('marken.show', ['slug' => $brand['slug']]) }}"
|
||||
class="btn-primary-accent px-6 py-3 mt-8 inline-block">
|
||||
{{ __('ui.more_about', ['name' => $brand['name']]) }}
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@endforeach
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
<section class="section-padding {{ $bg }}" id="marken">
|
||||
<div class="container-padding">
|
||||
<div class="text-center mb-16 slide-up delay-300">
|
||||
<h2 class="text-section-title">{!! $content['title'] ?? '' !!}</h2>
|
||||
@if (!empty($content['subtitle']))
|
||||
<p class="text-large text-muted-foreground mt-4 max-w-2xl mx-auto">{{ $content['subtitle'] }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-3 gap-8 slide-up delay-400">
|
||||
@foreach ($brands as $brand)
|
||||
<div class="card-elevated rounded-2xl overflow-hidden flex flex-col"
|
||||
wire:key="brand-{{ $brand['slug'] }}">
|
||||
@if (!empty($brand['image']))
|
||||
<img src="{{ theme_image_url($brand['image']) }}" alt="{{ $brand['name'] }}"
|
||||
class="w-full h-56 object-cover" loading="lazy" />
|
||||
@else
|
||||
{{-- Platzhalter, solange die Bildmotive fehlen (offene Zulieferung). --}}
|
||||
<div class="w-full h-56 bg-muted flex items-center justify-center">
|
||||
<span class="text-muted-foreground text-sm">{{ $brand['name'] }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="p-8 flex flex-col grow">
|
||||
@if (!empty($brand['logo']))
|
||||
<img src="{{ theme_image_url($brand['logo']) }}" alt="{{ $brand['name'] }}"
|
||||
class="h-8 w-auto mb-4 object-contain object-left" loading="lazy" />
|
||||
@endif
|
||||
|
||||
<h3 class="text-xl font-medium">{{ $brand['name'] }}</h3>
|
||||
<p class="text-secondary font-medium mt-1">{{ $brand['tagline'] }}</p>
|
||||
<p class="text-muted-foreground leading-relaxed mt-4 grow">{!! $brand['teaser'] !!}</p>
|
||||
|
||||
@if (!empty($brand['has_detail_page']))
|
||||
<a href="{{ route('marken.show', ['slug' => $brand['slug']]) }}"
|
||||
class="text-secondary font-medium mt-6 inline-flex items-center gap-1 hover:underline">
|
||||
{{ __('ui.more_about', ['name' => $brand['name']]) }}
|
||||
<span aria-hidden="true">→</span>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- Die Startseite bleibt ein Teaser – die vollstaendige Darstellung steht auf /marken. --}}
|
||||
<div class="text-center mt-12 slide-up delay-500">
|
||||
<a href="{{ route('marken') }}" class="btn-primary-accent px-8 py-4 text-lg inline-block">
|
||||
{{ __('ui.all_brands_cta') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
{{-- Livewire braucht ein Wurzelelement. Solange kein Display veröffentlicht ist, bleibt es leer –
|
||||
lieber gar keine Sektion als ein Rahmen mit ewigem Ladebalken. --}}
|
||||
<div>
|
||||
@if ($displayId !== null)
|
||||
<section class="section-padding {{ $bg }}">
|
||||
<div class="container-padding">
|
||||
<div class="grid lg:grid-cols-2 gap-12 lg:gap-16 items-center">
|
||||
<div>
|
||||
<h2 class="text-section-title">{!! $content['title'] ?? '' !!}</h2>
|
||||
<p class="text-large text-muted-foreground leading-relaxed mt-6">{{ $content['text'] ?? '' }}</p>
|
||||
|
||||
@if (!empty($content['note']))
|
||||
<p class="text-sm text-muted-foreground mt-4">{{ $content['note'] }}</p>
|
||||
@endif
|
||||
|
||||
@if ($showLink && !empty($content['link_text']))
|
||||
<a href="{{ $content['link_url'] ?? route('referenzen') }}"
|
||||
class="btn-primary-accent px-6 py-3 mt-8 inline-block">
|
||||
{{ $content['link_text'] }}
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- 9:16 wie das physische Display im Showroom --}}
|
||||
<div class="flex justify-center">
|
||||
<div class="w-full max-w-[320px] aspect-[9/16] overflow-hidden rounded-2xl shadow-elevated bg-black">
|
||||
<iframe src="{{ route('display.embed', ['display' => $displayId]) }}"
|
||||
title="{{ $content['iframe_title'] ?? __('ui.display_feed_title') }}"
|
||||
loading="lazy" scrolling="no"
|
||||
class="h-full w-full border-0"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
</div>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{{-- Bewusst klein und dezent: wer gezielt sucht, findet den Weg; wer wegen Einrichtung
|
||||
kommt, wird nicht abgelenkt. Keine Preise, keine Renditeversprechen. --}}
|
||||
<section class="py-12 {{ $bg }}">
|
||||
<div class="container-narrow">
|
||||
<div class="border-t border-border pt-10 text-center">
|
||||
<h2 class="text-xl font-medium">{!! $content['title'] ?? '' !!}</h2>
|
||||
<p class="text-muted-foreground leading-relaxed mt-3 max-w-2xl mx-auto">{{ $content['text'] ?? '' }}</p>
|
||||
|
||||
@if (!empty($content['link_text']))
|
||||
<a href="{{ $content['link_url'] ?? '/immobilien' }}"
|
||||
class="text-secondary font-medium mt-4 inline-flex items-center gap-1 hover:underline">
|
||||
{{ $content['link_text'] }} <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
</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">
|
||||
<a href="{{ route('magazin.detail', ['slug' => $post['slug']]) }}" class="stretched-link">
|
||||
{!! $post['title'] !!}
|
||||
</a>
|
||||
</h3>
|
||||
|
|
@ -44,14 +44,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>
|
||||
<a href="{{ route('magazin.detail', ['slug' => $post['slug']]) }}" 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" 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'] }}" aria-label="{{ __('ui.read_article', ['title' => $post['title']]) }}">
|
||||
<a href="{{ route('magazin.detail', ['slug' => $post['slug']]) }}" 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>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
<section class="section-padding bg-secondary text-secondary-foreground">
|
||||
<div class="container-narrow text-center spacing-section">
|
||||
<h2 class="text-section-title text-white">{!! $content['title'] ?? '' !!}</h2>
|
||||
<p class="text-large max-w-2xl mx-auto leading-relaxed">{{ $content['text'] ?? '' }}</p>
|
||||
|
||||
<div class="pt-4 flex flex-wrap items-center justify-center gap-4">
|
||||
<a href="{{ $content['button_link'] ?? '/contact' }}" class="btn-primary-accent px-8 py-4 text-lg">
|
||||
{{ $content['button_text'] ?? __('ui.request_appointment') }}
|
||||
</a>
|
||||
|
||||
{{-- WhatsApp erscheint erst, wenn die Nummer im CMS hinterlegt ist (global.contact.whatsapp).
|
||||
Das Widget selbst kommt mit AP3. --}}
|
||||
@php $whatsapp = whatsapp_link(); @endphp
|
||||
@if ($whatsapp)
|
||||
<a href="{{ $whatsapp }}" target="_blank" rel="noopener"
|
||||
class="px-8 py-4 text-lg underline underline-offset-4">
|
||||
{{ $content['whatsapp_text'] ?? __('ui.whatsapp') }}
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{{-- Optionaler Block laut Spec – über das CMS-Feld `enabled` abschaltbar. --}}
|
||||
@if ($content['enabled'] ?? true)
|
||||
<section class="py-16 {{ $bg }}">
|
||||
<div class="container-narrow text-center">
|
||||
<h2 class="text-2xl font-medium">{!! $content['title'] ?? '' !!}</h2>
|
||||
<p class="text-muted-foreground leading-relaxed mt-4">{{ $content['text'] ?? '' }}</p>
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
@php
|
||||
if (!function_exists('renderHeroIcon')) {
|
||||
function renderHeroIcon($iconName, $style = 'outline', $color = 'text-secondary')
|
||||
{
|
||||
$iconPath = public_path("heroicons/optimized/24/{$style}/{$iconName}.svg");
|
||||
$fallbackPath = public_path('heroicons/optimized/24/outline/sparkles.svg');
|
||||
|
||||
if (file_exists($iconPath)) {
|
||||
return str_replace('<svg', '<svg class="w-10 h-10 ' . $color . '"', file_get_contents($iconPath));
|
||||
}
|
||||
|
||||
if (file_exists($fallbackPath)) {
|
||||
return str_replace('<svg', '<svg class="w-10 h-10 ' . $color . '"', file_get_contents($fallbackPath));
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
<section class="section-padding {{ $bg }}">
|
||||
<div class="container-padding">
|
||||
<div class="container-narrow text-center">
|
||||
<h2 class="text-section-title">{!! $content['title'] ?? '' !!}</h2>
|
||||
@foreach ($content['paragraphs'] ?? [] as $i => $paragraph)
|
||||
<p class="text-large text-muted-foreground leading-relaxed mt-5" wire:key="p-{{ $i }}">
|
||||
{!! $paragraph !!}
|
||||
</p>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if (!empty($content['points']))
|
||||
<div class="grid md:grid-cols-3 gap-8 mt-16">
|
||||
@foreach ($content['points'] as $index => $point)
|
||||
<div class="card-elevated rounded-2xl p-8 text-center" wire:key="point-{{ $index }}">
|
||||
<div class="mx-auto w-20 h-20 icon-secondary-linear glow-soft rounded-2xl flex items-center justify-center mb-5">
|
||||
{!! renderHeroIcon($point['icon'] ?? 'user', 'outline', 'text-secondary-foreground') !!}
|
||||
</div>
|
||||
<h3 class="text-lg font-medium">{{ $point['title'] ?? '' }}</h3>
|
||||
<p class="text-muted-foreground leading-relaxed mt-2">{{ $point['description'] ?? '' }}</p>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
{{-- Seitenkopf für Unterseiten – abgesetzte graue Fläche wie der Hero auf Startseite und
|
||||
Netzwerkseite, damit er sich klar von den Blöcken darunter absetzt. --}}
|
||||
<section class="section-padding flex items-center relative border-b border-border/30 {{ $bg }}">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 lg:py-20 bg-hero-container rounded-[20px] w-[95%]">
|
||||
{{-- Breadcrumb: Home → Marken --}}
|
||||
<nav aria-label="Breadcrumb" class="mb-8">
|
||||
<ol class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<li><a href="{{ route('home') }}" class="hover:text-foreground transition-colors">Home</a></li>
|
||||
<li aria-hidden="true">/</li>
|
||||
<li class="text-foreground" aria-current="page">{{ $breadcrumb }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
@if (!empty($content['image']))
|
||||
{{-- 3:2 statt hälftig geteilt – der Text bleibt der Hauptdarsteller, das Bild begleitet ihn nur. --}}
|
||||
<div class="grid lg:grid-cols-5 gap-10 lg:gap-14 items-center">
|
||||
<div class="lg:col-span-3 slide-right delay-200">
|
||||
<h1 class="text-hero-alt">{!! $content['title'] ?? '' !!}</h1>
|
||||
<p class="text-large text-muted-foreground leading-relaxed mt-6">{{ $content['text'] ?? '' }}</p>
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-2 relative rounded-2xl overflow-hidden shadow-elevated slide-left delay-300">
|
||||
<img src="{{ theme_image_url($content['image']) }}" alt="{{ $content['image_alt'] ?? '' }}"
|
||||
class="w-full aspect-[4/3] object-cover" />
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/15 to-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
{{-- Ohne Motiv bleibt der Textblock allein stehen – kein leerer Platzhalterrahmen. --}}
|
||||
<div class="max-w-3xl slide-right delay-200">
|
||||
<h1 class="text-hero-alt">{!! $content['title'] ?? '' !!}</h1>
|
||||
<p class="text-large text-muted-foreground leading-relaxed mt-6">{{ $content['text'] ?? '' }}</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<section class="section-padding {{ $bg }}">
|
||||
<div class="container-padding">
|
||||
<div class="text-center max-w-2xl mx-auto">
|
||||
<h2 class="text-section-title">{!! $content['title'] ?? '' !!}</h2>
|
||||
@if (!empty($content['text']))
|
||||
<p class="text-large text-muted-foreground leading-relaxed mt-4">{{ $content['text'] }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($projects)
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-6 mt-12">
|
||||
@foreach ($projects as $project)
|
||||
<figure class="card-elevated rounded-2xl overflow-hidden group" wire:key="ref-{{ $project['key'] }}">
|
||||
<div class="relative overflow-hidden">
|
||||
<img src="{{ theme_image_url($project['image']) }}"
|
||||
alt="{{ $project['caption'] ?: $project['brand'] }}" loading="lazy"
|
||||
class="w-full aspect-[4/3] object-cover transition-transform duration-300 group-hover:scale-105" />
|
||||
<span class="absolute top-3 left-3 rounded-full bg-black/60 px-3 py-1 text-xs font-medium text-white backdrop-blur-sm">
|
||||
{{ __('ui.brand_gallery_badge_project') }}
|
||||
</span>
|
||||
</div>
|
||||
<figcaption class="p-5">
|
||||
@if (!empty($project['caption']))
|
||||
<p class="font-medium">{{ $project['caption'] }}</p>
|
||||
@endif
|
||||
@if ($project['has_detail_page'])
|
||||
<a href="{{ route('marken.show', ['slug' => $project['brand_slug']]) }}"
|
||||
class="text-sm text-secondary underline underline-offset-2 hover:no-underline mt-1 inline-block">
|
||||
{{ $project['brand'] }}
|
||||
</a>
|
||||
@else
|
||||
<p class="text-sm text-muted-foreground mt-1">{{ $project['brand'] }}</p>
|
||||
@endif
|
||||
</figcaption>
|
||||
</figure>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
{{-- Ehrlicher Leerzustand statt einer erfundenen Referenz. --}}
|
||||
<div class="mt-12 rounded-2xl border border-dashed border-border p-10 text-center">
|
||||
<p class="text-muted-foreground">{{ $content['empty'] ?? __('ui.references_empty') }}</p>
|
||||
<a href="{{ route('contact') }}" class="btn-primary-accent px-6 py-3 mt-6 inline-block">
|
||||
{{ __('ui.request_appointment') }}
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<section class="section-padding {{ $bg }}">
|
||||
<div class="container-padding">
|
||||
<div class="text-center mb-12 slide-up delay-300">
|
||||
<h2 class="text-section-title">{!! $content['title'] ?? '' !!}</h2>
|
||||
@if (!empty($content['subtitle']))
|
||||
<p class="text-large text-muted-foreground mt-4 max-w-2xl mx-auto">{{ $content['subtitle'] }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Kachel je Verein: Logo oben auf Weiß (die Logos sind für weißen Grund gemacht),
|
||||
darunter ein kurzer Text. Bewusst ohne Verlinkung – reine Nennung. --}}
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-8 max-w-5xl mx-auto slide-up delay-400">
|
||||
@foreach ($content['logos'] ?? [] as $index => $logo)
|
||||
<div class="card-elevated rounded-2xl overflow-hidden" wire:key="sponsor-{{ $index }}">
|
||||
{{-- Feste Bühnenhöhe und Deckel in beide Richtungen: die Logos haben sehr
|
||||
unterschiedliche Seitenverhältnisse (1:1 bis 3,3:1) und wirken sonst
|
||||
unterschiedlich groß. --}}
|
||||
<div class="bg-white flex items-center justify-center h-36 px-8">
|
||||
<img src="{{ theme_image_url($logo['image']) }}" alt="{{ $logo['name'] ?? '' }}"
|
||||
loading="lazy" class="max-h-20 max-w-[85%] w-auto object-contain" />
|
||||
</div>
|
||||
|
||||
<div class="p-6 border-t border-border/60">
|
||||
<h3 class="font-medium">{{ $logo['name'] ?? '' }}</h3>
|
||||
@if (!empty($logo['text']))
|
||||
<p class="text-sm text-muted-foreground leading-relaxed mt-2">{{ $logo['text'] }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -1,5 +1,16 @@
|
|||
@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"
|
||||
{{-- Kein x-cloak: Livewire setzt global [x-cloak]{display:none!important}. Mit x-cloak wuerde
|
||||
die Leiste dauerhaft verschwinden, sobald Alpine das Attribut nicht entfernt. Ohne Alpine
|
||||
ist sie sichtbar – das ist hier der richtige Standardfall.
|
||||
Der localStorage-Zugriff steht bewusst im x-data (kein Aufblitzen) und ist abgesichert,
|
||||
weil er im privaten Modus oder bei blockiertem Storage wirft. --}}
|
||||
<div x-data="(() => {
|
||||
const key = @js($this->dismissKey);
|
||||
let dismissed = false;
|
||||
try { dismissed = localStorage.getItem(key) === 'true'; } catch (e) {}
|
||||
return { key, dismissed };
|
||||
})()"
|
||||
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-primary text-secondary-foreground overflow-hidden" id="topbar">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
|
@ -28,7 +39,7 @@
|
|||
@endif
|
||||
|
||||
<button
|
||||
@click="dismissed = true; localStorage.setItem('announcement_dismissed_{{ $content['id'] ?? 'default' }}', 'true')"
|
||||
@click="dismissed = true; try { localStorage.setItem(key, 'true') } catch (e) {}"
|
||||
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">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,175 @@
|
|||
{{-- Schwebende Terminanfrage, unten rechts. Der Cookie-Button des Consent-Managers sitzt unten
|
||||
links (z-40), sein Dialog auf z-50 – dieses Widget bleibt darunter, damit die Einwilligung
|
||||
immer erreichbar bleibt. --}}
|
||||
<div class="fixed bottom-4 right-4 z-40 flex flex-col items-end gap-3 print:hidden"
|
||||
x-data="{
|
||||
open: false,
|
||||
whatsappBase: @js($this->whatsappUrl),
|
||||
topicLabels: @js($this->topics),
|
||||
greeting: @js($content['whatsapp_text'] ?? __('ui.appointment.whatsapp_text')),
|
||||
/* Baut den Nachrichtentext aus dem, was schon im Formular steht. Der Besucher sieht ihn
|
||||
in WhatsApp und schickt ihn selbst ab – wir verschicken nichts automatisch. */
|
||||
get whatsappHref() {
|
||||
const lines = [this.greeting];
|
||||
const topic = this.topicLabels[$wire.topic];
|
||||
if (topic) { lines.push('Anliegen: ' + topic) }
|
||||
if ($wire.preferredDate) { lines.push('Wunschtermin: ' + $wire.preferredDate) }
|
||||
if ($wire.name) { lines.push('Mein Name: ' + $wire.name) }
|
||||
return this.whatsappBase + '?text=' + encodeURIComponent(lines.join('\n'));
|
||||
},
|
||||
}" x-on:keydown.escape.window="open = false">
|
||||
|
||||
{{-- Panel --}}
|
||||
<div x-show="open" x-cloak x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 translate-y-2" x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-end="opacity-0 translate-y-2" x-on:click.outside="open = false"
|
||||
role="dialog" aria-modal="true" aria-labelledby="appointment-widget-title"
|
||||
class="card-elevated w-[calc(100vw-2rem)] sm:w-[26rem] max-h-[80vh] overflow-y-auto rounded-2xl p-6">
|
||||
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<h2 id="appointment-widget-title" class="text-xl font-medium">
|
||||
{{ $content['title'] ?? __('ui.request_appointment') }}
|
||||
</h2>
|
||||
<button type="button" x-on:click="open = false" aria-label="{{ __('ui.close') }}"
|
||||
class="shrink-0 text-muted-foreground hover:text-foreground transition-colors cursor-pointer">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"
|
||||
aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if ($success)
|
||||
<div class="mt-6 rounded-lg border border-green-200 bg-green-50 p-4 text-sm text-green-800">
|
||||
{{ $content['success'] ?? __('ui.appointment.success') }}
|
||||
</div>
|
||||
<button type="button" wire:click="startOver"
|
||||
class="btn-secondary-accent w-full mt-4 px-6 py-3 cursor-pointer">
|
||||
{{ __('ui.appointment.new_request') }}
|
||||
</button>
|
||||
@else
|
||||
@if (!empty($content['intro']))
|
||||
<p class="text-sm text-muted-foreground leading-relaxed mt-2">{{ $content['intro'] }}</p>
|
||||
@endif
|
||||
|
||||
<form wire:submit="submit" class="space-y-4 mt-6">
|
||||
<div>
|
||||
<label for="appointment-name" class="text-foreground mb-2 block text-sm font-medium">
|
||||
{{ __('ui.appointment.name') }} <span aria-hidden="true">*</span>
|
||||
</label>
|
||||
<input wire:model="name" id="appointment-name" type="text" required
|
||||
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" />
|
||||
@error('name') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="appointment-email" class="text-foreground mb-2 block text-sm font-medium">
|
||||
{{ __('ui.appointment.email') }} <span aria-hidden="true">*</span>
|
||||
</label>
|
||||
<input wire:model="email" id="appointment-email" type="email" required
|
||||
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" />
|
||||
@error('email') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="appointment-phone" class="text-foreground mb-2 block text-sm font-medium">
|
||||
{{ __('ui.appointment.phone') }}
|
||||
</label>
|
||||
<input wire:model="phone" id="appointment-phone" type="tel"
|
||||
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" />
|
||||
@error('phone') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="appointment-topic" class="text-foreground mb-2 block text-sm font-medium">
|
||||
{{ __('ui.appointment.topic') }}
|
||||
</label>
|
||||
<select wire:model="topic" id="appointment-topic"
|
||||
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">
|
||||
<option value="">{{ __('ui.appointment.topic_placeholder') }}</option>
|
||||
@foreach ($this->topics as $value => $label)
|
||||
<option value="{{ $value }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('topic') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="appointment-date" class="text-foreground mb-2 block text-sm font-medium">
|
||||
{{ __('ui.appointment.preferred_date') }}
|
||||
</label>
|
||||
<input wire:model="preferredDate" id="appointment-date" type="date"
|
||||
min="{{ now()->toDateString() }}"
|
||||
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" />
|
||||
@error('preferredDate') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="appointment-message" class="text-foreground mb-2 block text-sm font-medium">
|
||||
{{ __('ui.appointment.message') }}
|
||||
</label>
|
||||
<textarea wire:model="message" id="appointment-message" rows="3"
|
||||
placeholder="{{ __('ui.appointment.message_placeholder') }}"
|
||||
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"></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="appointment-website">Website</label>
|
||||
<input type="text" id="appointment-website" wire:model="website" tabindex="-1" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<label class="flex items-start gap-3 cursor-pointer">
|
||||
<input wire:model="privacy" id="appointment-privacy" type="checkbox"
|
||||
class="mt-1 shrink-0 rounded border-border" />
|
||||
<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 block">{{ $message }}</span> @enderror
|
||||
|
||||
<button type="submit" wire:loading.attr="disabled"
|
||||
class="btn-primary-accent w-full px-6 py-3 cursor-pointer disabled:opacity-70">
|
||||
<span wire:loading.remove wire:target="submit">{{ __('ui.appointment.submit') }}</span>
|
||||
<span wire:loading wire:target="submit">{{ __('ui.appointment.sending') }}</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{{-- Alternative: WhatsApp. Nur sichtbar, wenn im CMS eine Nummer hinterlegt ist.
|
||||
Der Link ist ein normaler Klick-zum-Chat-Link – auf dem Handy öffnet die
|
||||
installierte App, sonst WhatsApp Web. Der Text ist vorbereitet, abgeschickt
|
||||
wird er vom Besucher selbst. --}}
|
||||
@if ($this->whatsappUrl !== '')
|
||||
<div class="mt-6 pt-6 border-t border-border/60">
|
||||
<p class="text-sm text-muted-foreground">{{ __('ui.appointment.whatsapp_intro') }}</p>
|
||||
<a x-bind:href="whatsappHref" target="_blank" rel="noopener"
|
||||
class="mt-3 flex w-full items-center justify-center gap-2 rounded-lg bg-[#25D366] px-6 py-3 font-medium text-white transition-transform hover:scale-[1.02]">
|
||||
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.372-.025-.521-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51l-.57-.01c-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.872.118.571-.085 1.758-.719 2.006-1.413.247-.694.247-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884a9.82 9.82 0 016.988 2.896 9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.885-9.885 9.885M20.52 3.449C18.24 1.245 15.24 0 12.045 0 5.463 0 .104 5.359.101 11.945c0 2.096.549 4.142 1.595 5.945L0 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.582 0 11.941-5.359 11.944-11.945a11.87 11.87 0 00-3.417-8.4" />
|
||||
</svg>
|
||||
{{ __('ui.whatsapp') }}
|
||||
</a>
|
||||
<p class="mt-2 text-xs text-muted-foreground">{{ __('ui.appointment.whatsapp_note') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<button type="button" x-on:click="open = !open" x-bind:aria-expanded="open.toString()"
|
||||
aria-controls="appointment-widget-title"
|
||||
class="btn-secondary-accent flex items-center gap-2 rounded-full px-5 py-3 shadow-lg cursor-pointer">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"
|
||||
aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
|
||||
</svg>
|
||||
<span x-show="!open">{{ $content['button_label'] ?? __('ui.request_appointment') }}</span>
|
||||
<span x-show="open" x-cloak>{{ __('ui.close') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -5,38 +5,38 @@
|
|||
<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"
|
||||
width="120" height="56"
|
||||
alt="{{ $domainName ?? 'B2in Interior' }} Logo" class="h-16 w-auto"
|
||||
width="81" height="64"
|
||||
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>
|
||||
{!! $content['claim'] ?? 'Premium-Einrichtung. <span class="text-secondary">Persönlich gemacht.</span>' !!}
|
||||
</h2>
|
||||
<p class="text-dark-muted text-sm mt-4">Marcel Scheibe – {{ __('ui.founder_ceo') }}</p>
|
||||
<p class="text-dark-muted text-sm mt-4">{{ $content['byline'] ?? 'Marcel Scheibe' }} – {{ __('ui.founder_ceo') }}</p>
|
||||
</div>
|
||||
|
||||
<hr class="border-t border-dark-muted/30 mt-12 pt-4">
|
||||
</div>
|
||||
|
||||
{{-- 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="{{ route('privacy') }}" class="block hover-text-secondary transition-colors">Privacy Policy</a>
|
||||
</div>
|
||||
|
||||
<div class="spacing-small text-center">
|
||||
<a href="{{ route('terms') }}" class="block hover-text-secondary transition-colors">Terms of Service</a>
|
||||
</div>
|
||||
|
||||
<div class="spacing-small text-center">
|
||||
<a href="{{ route('cookie-policy') }}" class="block hover-text-secondary transition-colors">Cookie Policy</a>
|
||||
</div>
|
||||
|
||||
<div class="spacing-small text-center">
|
||||
<a href="{{ route('impressum') }}" class="block hover-text-secondary transition-colors">{{ __('ui.legal_notice') }}</a>
|
||||
</div>
|
||||
@php
|
||||
$footerLinks = $content['links'] ?? [
|
||||
['label' => __('ui.faq'), 'url' => '/faq'],
|
||||
['label' => __('ui.privacy_policy'), 'url' => '/privacy'],
|
||||
['label' => __('ui.terms_of_service'), 'url' => '/terms'],
|
||||
['label' => __('ui.cookie_policy'), 'url' => '/cookie-policy'],
|
||||
['label' => __('ui.legal_notice'), 'url' => '/impressum'],
|
||||
];
|
||||
@endphp
|
||||
<div class="grid grid-cols-2 md:grid-cols-5 gap-8 text-left max-w-4xl mx-auto">
|
||||
@foreach ($footerLinks as $footerLink)
|
||||
<div class="spacing-small text-center" wire:key="footer-link-{{ $loop->index }}">
|
||||
<a href="{{ $footerLink['url'] }}"
|
||||
class="block hover-text-secondary transition-colors">{{ $footerLink['label'] }}</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<div class="flex items-center justify-between h-16">
|
||||
<a href="{{ $domainUrl }}" class="flex items-center">
|
||||
<img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPath('positive')) }}"
|
||||
alt="{{ $domainName ?? 'B2IN' }} Logo" class="h-10 w-auto" />
|
||||
alt="{{ $domainName ?? 'B2in Interior' }} Logo" class="h-12 w-auto" />
|
||||
</a>
|
||||
|
||||
<nav class="hidden md:flex items-center space-x-8" aria-label="{{ __('ui.main_navigation') }}">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue