10-04-2026
This commit is contained in:
parent
4d6b4930b2
commit
4bb89aad8c
836 changed files with 52961 additions and 5950 deletions
|
|
@ -1,9 +1,9 @@
|
|||
# Entwicklungsplan: B2In / Local for Local Marktplatz-Ökosystem
|
||||
# Entwicklungsplan: B2in / Local for Local Marktplatz-Ökosystem
|
||||
|
||||
**Erstellt:** 12.02.2026
|
||||
**Letzte Aktualisierung:** 12.02.2026
|
||||
**Basis:** konzeption.md (Version 1.1)
|
||||
**Status:** Phase 1 ✅, Phase 2 ✅, Phase 2.5 Produkt-Bearbeitung ✅, Phase 2.6 Refactoring & UX ✅, Phase 2.7 Admin-Produktverwaltung & Freigabe ✅, Phase 3 Kern ✅
|
||||
**Status:** Phase 1 ✅, Phase 2 ✅, Phase 2.5 Produkt-Bearbeitung ✅, Phase 2.6 Refactoring & UX ✅, Phase 2.7 Admin-Produktverwaltung & Freigabe ✅, Phase 3 Kern ✅, Phase 2.8 Partner-Präsentation ✅
|
||||
**Docker** Projekt läuft in Docker, root /var/www/html nutze php artisan ... nicht vendor/bin/sail artisan ...
|
||||
---
|
||||
|
||||
|
|
@ -581,6 +581,31 @@ Alle Preistypen erlaubt; Standard-Varianten mit Festpreis möglich.
|
|||
- [ ] Notification an Händler/Hersteller bei Freigabe/Ablehnung – ausstehend
|
||||
- [ ] Admin kann `product_type` bei Bedarf nachträglich ändern – ausstehend
|
||||
|
||||
#### 2.4a Nachbesserungen Phase 2 – ✅ Abgeschlossen (27.02.2026)
|
||||
|
||||
**Fix: `curate products` Permission fehlte in der Datenbank**
|
||||
- Die Permission war im `RoleSeeder` definiert, aber nie in die Produktions-DB eingespielt
|
||||
- `Attempt to read property "id" on null` beim Öffnen von `/admin/products`
|
||||
- Lösung: Migration `2026_02_27_154145_add_curate_products_permission.php`
|
||||
- `Permission::firstOrCreate(['name' => 'curate products', 'guard_name' => 'web'])`
|
||||
- Admin-Rolle bekommt Permission via `Role::where('name', 'Admin')->first()` (kein `findByName` = würde Exception werfen wenn Rolle fehlt)
|
||||
- Tests (`ProductCurationTest`, `ProductPolicyTest`, `PartnerPolicyTest`): `Permission::create()` → `Permission::firstOrCreate()` (verhindert Fehler wenn Migration die Permission bereits angelegt hat)
|
||||
|
||||
**Fix: Admin kann Produkte anlegen (Partner-Selector)**
|
||||
- Admin hat keine eigene `partner_id` → `$user->partner` war `null` → Crash beim Speichern
|
||||
- Lösung in `form-teaser.blade.php` und `form-standard.blade.php`:
|
||||
- Neues Property `$selectedPartnerId` + `resolvePartner()` Methode
|
||||
- Für Admins: Partner-Auswahl-Karte erscheint vor dem Formular (nur bei Neuanlage)
|
||||
- `updatedSelectedPartnerId()` aktualisiert Produktnummer automatisch nach Partnerwahl
|
||||
- Validierung: `selectedPartnerId` Pflichtfeld für Admins bei Neuanlage
|
||||
- Alle bestehenden 158 Produkt-Tests weiterhin grün ✅
|
||||
|
||||
**Fix: Portal Textarea Dark-Mode Kontrast**
|
||||
- Korrektur-Textfeld in der Kuration zeigte weißen Text auf weißem Hintergrund
|
||||
- Ursache: `@apply dark:text-zinc-500` innerhalb von `::placeholder` Pseudo-Elementen kompiliert in Tailwind v4 zu ungültigem `:is():where(.dark,.dark *)` Selektor
|
||||
- Lösung in `resources/css/portal.css`: Explizites CSS mit `:where(.dark,.dark *)` als Vorfahren-Selektor statt `@apply dark:`
|
||||
- Portal-Build (`npm run build:portal`) aktualisiert
|
||||
|
||||
#### 2.5 Tests Phase 2 – ✅ Kern abgeschlossen
|
||||
- [x] Feature-Tests: `PartnerPolicyTest` (10 Tests) – viewAny, view, update, curateProducts
|
||||
- [x] Feature-Tests: `ProductPolicyTest` (14 Tests) – alle Policy-Methoden
|
||||
|
|
@ -596,6 +621,59 @@ Alle Preistypen erlaubt; Standard-Varianten mit Festpreis möglich.
|
|||
|
||||
---
|
||||
|
||||
### Phase 2.8: Partner-Präsentation & Selbst-Service-Profil – ✅ ABGESCHLOSSEN (27.02.2026)
|
||||
**Geschätzter Aufwand:** 2-3 Tage | **Tatsächlich:** 1 Sitzung
|
||||
|
||||
#### 2.8.1 Händler-Profil (Retailer) – Präsentation
|
||||
- [x] `my-data.blade.php` erweitert um:
|
||||
- [x] **Story-Text** – Freitext max. 2000 Zeichen mit Live-Zeichenzähler, Feld: `story_text`
|
||||
- [x] **Öffnungszeiten** – 7-Tage-Eingabe (Mo–So), `open`/`close`-Felder + "Geschlossen"-Checkbox, Feld: `opening_hours` (JSON)
|
||||
- [x] **Spezialisierungen** – kommagetrennte Eingabe → gespeichert als JSON-Array, Feld: `specialties`
|
||||
- [x] **Gründungsjahr** – Zahlfeld mit Validierung (1800–heute), Feld: `founded_year`
|
||||
- [x] **Team-Fotos** – Mehrfach-Upload via `WithFileUploads`, gespeichert als `Media.type = 'team_photo'`
|
||||
- [x] **Showroom-Galerie** – Mehrfach-Upload, `Media.type = 'showroom'`
|
||||
- [x] Foto-Löschen per Klick (mit `wire:confirm`)
|
||||
- [x] Formular in 4 separate `flux:card`-Abschnitte gegliedert: Stammdaten, Präsentation & Story, Öffnungszeiten, Fotos
|
||||
|
||||
#### 2.8.2 Hersteller-Profil (Manufacturer) – Firma & Marke
|
||||
- [x] `my-data.blade.php` erweitert um:
|
||||
- [x] **Story-Text** – Unternehmensgeschichte, Feld: `story_text`
|
||||
- [x] **Gründungsjahr** – Feld: `founded_year`
|
||||
- [x] **Spezialisierungen** – Feld: `specialties`
|
||||
- [x] **Marken-Bilder** – Mehrfach-Upload, `Media.type = 'brand_image'`
|
||||
- [x] Abschnitte: Stammdaten, Über das Unternehmen, Marke (mit Markenname, Beschreibung, Bilder)
|
||||
|
||||
#### 2.8.3 Öffentliches Partnerprofil (`livewire/partner/profile.blade.php`)
|
||||
- [x] Story-Text anzeigen wenn vorhanden (bereits vorhanden)
|
||||
- [x] Öffnungszeiten anzeigen (bereits vorhanden)
|
||||
- [x] Spezialisierungen + Gründungsjahr anzeigen (bereits vorhanden)
|
||||
- [x] **Showroom-Galerie** – 3-spaltige Bild-Galerie unterhalb der Story
|
||||
- [x] **Marken-Bilder** (Hersteller) – 3-spaltige Galerie unter der Story
|
||||
- [x] **Team-Fotos** – 2-spaltige Galerie in der rechten Sidebar
|
||||
|
||||
#### 2.8.4 Datenbank
|
||||
- [x] Alle Felder vorhanden: `story_text`, `opening_hours`, `specialties`, `founded_year` (Migration `2026_02_12_000003_add_profile_fields_to_partners_table`)
|
||||
- [x] Media-System: Custom `Media` Model mit `type`-Feld (kein Spatie) – `Partner::media()` morph-Relation bereits vorhanden
|
||||
|
||||
#### 2.8.5 Tests Phase 2.8
|
||||
- [x] `PartnerSelfServiceProfileTest` (15 Tests) – alle bestanden ✅
|
||||
- Händler kann Story, Öffnungszeiten, Spezialisierungen, Gründungsjahr speichern
|
||||
- Händler kann Team-Fotos und Showroom-Fotos hochladen
|
||||
- Händler kann Foto löschen
|
||||
- Hersteller kann Story, Markendaten, Gründungsjahr speichern
|
||||
- Hersteller kann Marken-Bilder hochladen
|
||||
- Öffentliches Profil zeigt Story und Spezialisierungen an
|
||||
- Validierung: Story-Text max. 2000 Zeichen, Gründungsjahr-Grenzen
|
||||
|
||||
**Erstellte/geänderte Dateien (2):**
|
||||
|
||||
| Datei | Änderungen |
|
||||
|-------|-----------|
|
||||
| `resources/views/livewire/partner/my-data.blade.php` | Komplett überarbeitet: 4 Karten-Abschnitte, neue Profil-Felder, Foto-Upload mit `WithFileUploads` |
|
||||
| `resources/views/livewire/partner/profile.blade.php` | Showroom-Galerie, Marken-Bilder, Team-Fotos hinzugefügt; `media` eager-loaded |
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Kunden-Frontend & Local Feed – ✅ KERN ABGESCHLOSSEN (12.02.2026)
|
||||
**Geschätzter Aufwand:** 4-5 Tage | **Tatsächlich:** 1 Tag (12.02.2026)
|
||||
**Priorität:** HOCH – Kunden-Facing Funktionalität
|
||||
|
|
@ -721,7 +799,7 @@ Enum: App\Enums\TransactionStatus
|
|||
- [ ] `PendingMerchant` – Beleg hochgeladen, Händler muss bestätigen
|
||||
- [ ] `Confirmed` – Händler hat bestätigt → Rechnung an Händler generieren
|
||||
- [ ] `Invoiced` – Rechnung erstellt → Zahlung ausstehend
|
||||
- [ ] `Paid` – Händler hat Provision an B2In überwiesen
|
||||
- [ ] `Paid` – Händler hat Provision an B2in überwiesen
|
||||
- [ ] `Distributed` – Provisionen an Makler/Kunde ausgeschüttet
|
||||
- [ ] `Rejected` – Händler hat abgelehnt
|
||||
- [ ] `Disputed` – Streitfall
|
||||
|
|
@ -778,7 +856,7 @@ Migration: create_ledger_entries_table
|
|||
#### 6.2 Provisions-Berechnung
|
||||
- [ ] Service: `App\Services\CommissionService`
|
||||
- `calculateSplit(Transaction $transaction): CommissionSplit`
|
||||
- Berechnet: Makler-Anteil, Kunden-Cashback, B2In-Marge
|
||||
- Berechnet: Makler-Anteil, Kunden-Cashback, B2in-Marge
|
||||
- Nutzt `partner.provision_rate_percentage` und `partner.provision_fixed_amount`
|
||||
- [ ] Event Listener für `TransactionPaid`:
|
||||
- Erstellt Ledger-Einträge
|
||||
|
|
@ -856,7 +934,7 @@ Migration: create_ledger_entries_table
|
|||
> **Entscheidung:** Individuell pro Partner. Admin-Settings-Seite mit Feldern für:
|
||||
> - Makler-Provision (%)
|
||||
> - Kunden-Cashback (%)
|
||||
> - B2In-Marge (Rest)
|
||||
> - B2in-Marge (Rest)
|
||||
> Felder werden pro Partner im Admin-Backend konfiguriert.
|
||||
|
||||
**Frage 8: Ticket-Gültigkeit** ✅
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ Konzept-Update: "Local for Local" Marktplatz
|
|||
1. Strategische Neuausrichtung: Die Domains
|
||||
Die Trennung wird schärfer.
|
||||
|
||||
B2In (Backend/B2B): Bleibt das "Maschinenraum"-Portal für Händler, Hersteller und Makler. Hier wird verwaltet.
|
||||
B2in (Backend/B2B): Bleibt das "Maschinenraum"-Portal für Händler, Hersteller und Makler. Hier wird verwaltet.
|
||||
|
||||
Local for Local (Customer Frontend): Das wird das Gesicht zum Kunden. Der Kunde fühlt sich nicht auf einer abstrakten "B2In"-Seite, sondern in seinem regionalen Hub (z.B. "Local for Local OWL").
|
||||
Local for Local (Customer Frontend): Das wird das Gesicht zum Kunden. Der Kunde fühlt sich nicht auf einer abstrakten "B2in"-Seite, sondern in seinem regionalen Hub (z.B. "Local for Local OWL").
|
||||
|
||||
To-Do: Wir benötigen ggf. die Domain localforlocal.de (o.ä.) und routen diese auf die Endkunden-Ansicht.
|
||||
|
||||
|
|
@ -83,11 +83,11 @@ Anstatt eines klassischen "Warenkorbs" bauen wir für Typ A Produkte einen "Tick
|
|||
Das ist technisch einfacher als ein Checkout mit Payment-Provider! Wir generieren ein PDF/QR-Code und senden eine Mail.
|
||||
|
||||
|
||||
Konzeptpapier: B2In / Local for Local Marktplatz-Ökosystem
|
||||
Konzeptpapier: B2in / Local for Local Marktplatz-Ökosystem
|
||||
Status: Final | Version: 1.1 (Update: Marken-Hierarchie)
|
||||
|
||||
1. Executive Summary
|
||||
Das B2In-Ökosystem ist ein hybrider Marktplatz, der den Immobilienkauf ("Moment of Need") mit der Einrichtung verbindet. Es agiert als "Closed Shop" (Zugang nur über Makler). B2In ist die zentrale B2B-Plattform und Technologie. Local for Local ist das verbindende Prinzip innerhalb des Marktplatzes, das die Endkunden-Marken (style2own, stileigentum) mit den regionalen Händlern verknüpft.
|
||||
Das B2in-Ökosystem ist ein hybrider Marktplatz, der den Immobilienkauf ("Moment of Need") mit der Einrichtung verbindet. Es agiert als "Closed Shop" (Zugang nur über Makler). B2in ist die zentrale B2B-Plattform und Technologie. Local for Local ist das verbindende Prinzip innerhalb des Marktplatzes, das die Endkunden-Marken (style2own, stileigentum) mit den regionalen Händlern verknüpft.
|
||||
|
||||
Der USP liegt in der Transparenz lokaler Verfügbarkeit (Säule A: Local Express) und exklusiven Insider-Konditionen (Säule B: Smart Club), abgesichert durch ein Cashback-System.
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ Der USP liegt in der Transparenz lokaler Verfügbarkeit (Säule A: Local Express
|
|||
Wir unterscheiden strikt zwischen dem B2B-Zugang (Partner) und den B2C-Einstiegen (Endkunden).
|
||||
|
||||
A. Der B2B-Kanal (Die Dachmarke)
|
||||
Marke: B2In
|
||||
Marke: B2in
|
||||
|
||||
Zielgruppe: Immobilienmakler, Händler, Hersteller.
|
||||
|
||||
|
|
@ -163,9 +163,9 @@ Ticket: Kunde zieht im Portal einen QR-Code für Händler X.
|
|||
|
||||
Kauf: Kunde kauft vor Ort, verhandelt Preise individuell.
|
||||
|
||||
Upload (Der Trigger): Kunde lädt Kaufbeleg im B2In-Portal hoch, um sein Cashback anzufordern.
|
||||
Upload (Der Trigger): Kunde lädt Kaufbeleg im B2in-Portal hoch, um sein Cashback anzufordern.
|
||||
|
||||
Clearing: Händler bestätigt Umsatz im Backend -> Händler zahlt Gesamt-Provision an B2In.
|
||||
Clearing: Händler bestätigt Umsatz im Backend -> Händler zahlt Gesamt-Provision an B2in.
|
||||
|
||||
Ausschüttung: Sobald Geld eingeht, verteilt das System automatisch:
|
||||
|
||||
|
|
@ -173,7 +173,7 @@ Provision an Makler (Lead-Vergütung).
|
|||
|
||||
Cashback an Kunden (Motivation & Datentreue).
|
||||
|
||||
Marge an B2In.
|
||||
Marge an B2in.
|
||||
|
||||
|
||||
|
||||
|
|
@ -240,7 +240,7 @@ pending_merchant: Händler muss bestätigen ("Ja, Kunde war da und hat für 3.00
|
|||
|
||||
confirmed: Händler hat bestätigt -> Rechnung an Händler wird generiert.
|
||||
|
||||
paid: Händler hat Provision an B2In überwiesen.
|
||||
paid: Händler hat Provision an B2in überwiesen.
|
||||
|
||||
distributed: Provision wurde an Makler/Kunde ausgeschüttet.
|
||||
|
||||
|
|
@ -264,7 +264,7 @@ Händler Setup-Buchung:
|
|||
|
||||
Ein kleiner "Service-Store" im Händler-Backend.
|
||||
|
||||
Button: "Setup-Paket buchen (399€)". Löst eine interne Notification an das B2In-Team aus (Ticket für Fotografen).
|
||||
Button: "Setup-Paket buchen (399€)". Löst eine interne Notification an das B2in-Team aus (Ticket für Fotografen).
|
||||
|
||||
5. Frontend-Modul: Die Weichenstellung
|
||||
Wie die Landingpages mit dem System reden.
|
||||
|
|
|
|||
95
dev/12-03-2026/content.md
Normal file
95
dev/12-03-2026/content.md
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
### 1. Nachschärfung des Escrow-Textes (Magazin-Artikel 1)
|
||||
Wir ersetzen die bisherigen drei Punkte im ersten Magazin-Artikel resources/views/livewire/web/components/sections/magazin-list.blade.php („Sicherheit statt Bürokratie“) exakt durch den vom Kunden gelieferten Text. Das macht den Artikel rechtlich und fachlich wasserdicht.
|
||||
|
||||
So sieht der aktualisierte Block für den Artikel aus:
|
||||
|
||||
1. Das Escrow-System: Sicherheit durch staatlich regulierte Treuhandkonten
|
||||
Der wichtigste Schutzmechanismus beim Kauf einer Off-Plan-Immobilie in Dubai ist das gesetzlich vorgeschriebene Escrow-System. Käuferzahlungen fließen nicht direkt an den Bauträger, sondern werden auf ein projektbezogenes Treuhandkonto bei einer zugelassenen Bank eingezahlt. Dieses Konto wird von der Real Estate Regulatory Agency (RERA), einer Regulierungsbehörde des Dubai Land Department (DLD), überwacht. Der Entwickler erhält Zugriff auf die Gelder nur entsprechend dem tatsächlichen Baufortschritt, der von zertifizierten Ingenieuren bestätigt werden muss.
|
||||
|
||||
2. Strenge Kontrolle durch das Dubai Land Department (DLD)
|
||||
Das Dubai Land Department ist die zentrale staatliche Institution für Immobilien in Dubai. Über seine Regulierungsbehörde RERA überwacht es sämtliche Off-Plan-Projekte und sorgt dafür, dass Bauträger strenge gesetzliche Vorgaben einhalten. Jeder Kaufvertrag (SPA) wird offiziell registriert, und Bauprojekte müssen im sogenannten Oqood-System erfasst werden. Zahlungen der Käufer fließen auf projektbezogene Escrow-Konten und dürfen nur entsprechend dem bestätigten Baufortschritt freigegeben werden.
|
||||
|
||||
3. Transparenz und hohe Planbarkeit
|
||||
Durch die enge Verknüpfung von Käuferzahlungen mit dem tatsächlichen Baufortschritt entsteht ein hohes Maß an Transparenz für Investoren. Bauträger dürfen Mittel aus dem Escrow-Konto nur entsprechend dem bestätigten Baufortschritt abrufen. Dadurch wird sichergestellt, dass Investorengelder projektgebunden eingesetzt werden und nicht für andere Bauvorhaben verwendet werden können. Für Käufer bedeutet dies: Die Dynamik des Immobilienmarktes in Dubai wird mit einem klar regulierten System kombiniert, das international als vergleichsweise investorenfreundlich gilt.
|
||||
|
||||
|
||||
### 2. Integration von CABINET auf der "Netzwerk"-Seite
|
||||
|
||||
resources/views/web/netzwerk.blade.php
|
||||
Netzwerk-Seite hier sieht man, dass die Seite aktuell noch sehr leer ist ("Was wir aufbauen"). Hier ziehen wir CABINET als starkes Zugpferd (Anchor-Partner) hinein.
|
||||
|
||||
Neuer Text-Block für die Netzwerk-Seite:
|
||||
|
||||
Headline: Unser Premiumpartner: CABINET Einbauschränke
|
||||
Subline: Maßgefertigte Exzellenz für höchste Ansprüche.
|
||||
|
||||
Text:
|
||||
Im B2in-Ökosystem setzen wir auf Qualität, die Bestand hat. Wir sind stolz darauf, CABINET als Premiumpartner in unserem Netzwerk zu präsentieren. Der CABINET Store in Bielefeld (unser physischer Ankerpunkt) steht für maßgefertigte Einbaulösungen, die Design, Funktionalität und handwerkliche Präzision perfekt vereinen.
|
||||
|
||||
Ob begehbare Kleiderschränke, smarte Raumlösungen oder exklusive Innenausbauten – CABINET liefert den Premium-Standard, den unsere Immobilien-Investoren und Entwickler für ihre High-End-Projekte fordern. Diese Partnerschaft ist ein lebendiger Beweis für unsere Philosophie: Wir verbinden internationale Immobilien mit deutscher Einrichtungsexzellenz.
|
||||
|
||||
hier liegt das Logo public/img/assets/b2in/cabinet_logo.png
|
||||
(Visuell: Hierzu am besten ein sehr hochwertiges Foto aus dem CABINET-Showroom in Bielefeld oder ein Detailbild eines maßgefertigten Schranks einbauen.)
|
||||
|
||||
### 3. Die Inhalte für die FAQ-Seite
|
||||
Damit ihr die Seite direkt live nehmen könnt, habe ich hier die perfekten, verkaufspsychologischen Antworten formuliert, die exakt zu unserer neuen Ausrichtung passen:
|
||||
|
||||
1. Was ist B2in und welche Services bieten Sie an?
|
||||
|
||||
B2in ist die Brücke zwischen internationalen Premium-Immobilien und exklusiven Einrichtungskonzepten. Wir bieten Immobilieninvestoren Zugang zu Off-Market-Projekten (Fokus: Dubai) und begleiten sie durch den gesamten Kaufprozess. Gleichzeitig bieten wir Bauträgern ein knallhartes Supply-Chain-Management für die Beschaffung deutscher Qualitätsmöbel und bauen ein innovatives Einrichtungsnetzwerk für lokale Händler auf.
|
||||
|
||||
2. Was bedeutet Supply-Chain-Management bei B2in?
|
||||
|
||||
Für internationale Immobilienentwickler fungieren wir als verlängerter Arm in Deutschland. Wir übernehmen die operative und strategische Steuerung der Möbel- und Innenausstattungsbeschaffung. Das bedeutet: Wir sichern Lieferverträge ab, überwachen Meilensteine direkt bei den Herstellern und eskalieren bei Abweichungen sofort auf Managementebene. Unser Ziel ist absolute Vertragssicherheit und termingerechte Lieferung ohne Reibungsverluste.
|
||||
|
||||
3. Wie kann ich Partner bei B2in werden?
|
||||
|
||||
Unser Netzwerk richtet sich an Immobilienentwickler, Makler und den regionalen Möbelfachhandel. Wenn Sie als Entwickler deutsche Beschaffungssicherheit suchen oder als Händler Teil unseres künftigen "Local for Local"-Netzwerks werden möchten, um qualifizierte Leads von Immobilienkäufern zu erhalten, nutzen Sie einfach unser Kontaktformular. Wir prüfen jede Partnerschaft individuell auf Qualität und Passgenauigkeit.
|
||||
|
||||
4. Wie funktioniert das Immobilien-Investment über B2in?
|
||||
|
||||
Wir vermitteln nicht nur, wir begleiten Sie. Marcel Scheibe ist Ihr persönlicher Berater, der den Markt (insbesondere Dubai) aus eigener Investorensicht kennt. Nach einer Bedarfsanalyse stellen wir Ihnen exklusive Projekte vor (z. B. von Azizi Developments). Der Kaufprozess selbst ist durch das staatliche Escrow-System in Dubai maximal abgesichert. Ihr besonderer Vorteil: Als B2in-Kunde erhalten Sie im Anschluss exklusiven Zugang zu unserem Netzwerk, um Ihr Investment schlüsselfertig (Turnkey) und renditeoptimiert einrichten zu lassen.
|
||||
|
||||
5. Was macht B2in zu einem vertrauenswürdigen Partner?
|
||||
|
||||
Vertrauen entsteht durch Transparenz und eigene Markterfahrung. B2in-Gründer Marcel Scheibe investiert selbst in die Märkte, die wir anbieten, und ist regelmäßig vor Ort, um Baufortschritte zu prüfen. Wir verbinden die immense Dynamik internationaler Märkte (wie Dubai) mit deutscher Verlässlichkeit, striktem Vertragsmanagement und einem kuratierten Netzwerk von Premiumpartnern wie CABINET. Bei uns haben Sie immer einen persönlichen Ansprechpartner, der Ihre Interessen vertritt.
|
||||
|
||||
Die weite FAQ direkt mit in die Navigation aufnehmen.
|
||||
resources/views/web/faq.blade.php
|
||||
|
||||
### 3. Die Inhalte für die FAQ-Seite
|
||||
resources/views/web/immobilien.blade.php
|
||||
|
||||
Auf einer Immobilien-Detailseite resources/views/web/immobilien-show.blade.php
|
||||
(wie beim Azizi Creek Views 4) geht es um viel Geld. Hier ist die größte Conversion-Hürde das Thema Vertrauen und Risiko. Wenn wir dort einen endlosen Fließtext über das Escrow-System einbauen, zerstören wir die emotionale Verkaufs-Ästhetik der Bilder. Wenn wir es ganz weglassen, springen vorsichtige deutsche Käufer ab.
|
||||
|
||||
Drei kleine Info-Boxen (Cards) mit einem "Deep-Dive"-Link ins Magazin sind die absolute Best Practice im E-Commerce und bei High-End-Investments. So halten wir die Seite optisch clean (Snackable Content), fangen aber die Skeptiker perfekt ab.
|
||||
|
||||
Hier ist mein Konzept, wie ihr diesen "Trust-Block" direkt auf der Detailseite ( unter der Bildergalerie) einbauen solltet:
|
||||
|
||||
UI-Konzept für die Detailseite: Der "Trust-Block"
|
||||
Sektions-Headline: Maximale Sicherheit für Ihr Investment
|
||||
Sektions-Subline: Der Kaufprozess in Dubai ist durch strenge staatliche Regularien international führend im Investorenschutz.
|
||||
|
||||
(Darunter ein sauberes Grid mit 3 kleinen Kacheln/Boxen)
|
||||
|
||||
Box 1 🔒
|
||||
|
||||
Titel: Staatliches Escrow-System
|
||||
|
||||
Kurztext: Ihre Zahlungen fließen nicht an den Bauträger, sondern auf speziell regulierte Treuhandkonten der RERA (Real Estate Regulatory Agency).
|
||||
|
||||
Box 2 🏛️
|
||||
|
||||
Titel: Strenge DLD-Kontrolle
|
||||
|
||||
Kurztext: Jeder Kaufvertrag (SPA) wird offiziell beim Dubai Land Department registriert. Lückenlose behördliche Überwachung jedes Bauprojekts.
|
||||
|
||||
Box 3 📊
|
||||
|
||||
Titel: Transparente Planbarkeit
|
||||
|
||||
Kurztext: Gelder werden nur streng nach zertifiziertem Baufortschritt freigegeben. Eine Zweckentfremdung Ihres Kapitals ist gesetzlich ausgeschlossen.
|
||||
|
||||
(Direkt unter den 3 Boxen zentriert als Text-Link Button)
|
||||
Link/CTA: 📖 Deep Dive: Erfahren Sie im Magazin im Detail, wie das Escrow-System in Dubai Sie als Käufer schützt → [Link zum Magazin-Artikel]
|
||||
102
dev/12-03-2026/magazin.md
Normal file
102
dev/12-03-2026/magazin.md
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
Das ist eine hervorragende Herangehensweise. Wenn wir direkt alle fünf Artikel im passenden Format bereithalten, kann der Kunde frei wählen und das Magazin wirkt vom Start weg professionell und gut gefüllt.
|
||||
|
||||
Ich habe die Struktur exakt an den Screenshot angepasst: **Ein starker Einleitungstext (ohne eigene Überschrift), gefolgt von durchnummerierten, in sich geschlossenen Textblöcken.** Hier sind die fünf fertigen Magazin-Artikel für euren Live-Gang:
|
||||
|
||||
---
|
||||
|
||||
### Artikel 1: Sicherheit statt Bürokratie: Warum das Dubai Escrow-System deutsche Investoren überrascht
|
||||
|
||||
**[Einleitungstext - Fließtext oben]**
|
||||
Deutsche Investoren schätzen beim Immobilienkauf vor allem eines: maximale Sicherheit. Der vertraute Prozess über Notar und Grundbuchamt vermittelt ein Gefühl von Kontrolle, ist jedoch oft mit langen Wartezeiten und enormer Bürokratie verbunden. Wer zum ersten Mal auf den Immobilienmarkt in Dubai blickt, ist oft überrascht von der unglaublichen Dynamik und Geschwindigkeit. Doch bedeutet dieses Tempo ein höheres Risiko? Im Gegenteil: Dubai hat mit dem Escrow-System einen der sichersten und transparentesten regulatorischen Rahmenwerke der Welt geschaffen, der Investorengelder konsequent schützt.
|
||||
|
||||
**1. Das Escrow-System: Sicherheit durch Treuhand**
|
||||
Der wohl wichtigste Schutzmechanismus in Dubai ist das staatlich regulierte Escrow-System (Treuhandkonten). Wenn Sie eine Off-Plan-Immobilie (Neubau) erwerben, fließt Ihr Geld niemals direkt an den Bauträger. Stattdessen wird es auf ein unabhängiges, von der Regierung überwachtes Treuhandkonto eingezahlt. Der Entwickler erhält erst dann Zugriff auf diese Mittel, wenn nachweislich bestimmte Baufortschritte erreicht wurden.
|
||||
|
||||
**2. Strenge Kontrolle durch das Dubai Land Department (DLD)**
|
||||
Das Dubai Land Department agiert als zentrale Kontrollinstanz und garantiert, dass alle Transaktionen rechtmäßig ablaufen. Jeder Kaufvertrag (SPA) muss offiziell beim DLD registriert werden. Das DLD entsendet regelmäßig eigene Ingenieure auf die Baustellen, um den tatsächlichen Baufortschritt zu verifizieren, bevor Gelder vom Escrow-Konto an den Bauträger freigegeben werden.
|
||||
|
||||
**3. Transparenz und absolute Planbarkeit**
|
||||
Durch diese strikte Koppelung von Zahlungen an den physischen Baufortschritt haben Investoren jederzeit absolute Transparenz. Bauträger werden gezwungen, termingerecht und in hoher Qualität zu liefern. Das System eliminiert das Risiko, dass Entwickler Investorengelder für andere Projekte abzweigen. Für Käufer bedeutet das: Die Dynamik Dubais wird mit einer Verlässlichkeit kombiniert, die internationalen Standards weit voraus ist.
|
||||
|
||||
**[CTA-Bereich]**
|
||||
*Möchten Sie mehr über sichere Investments in Dubai erfahren? Vereinbaren Sie jetzt ein persönliches Beratungsgespräch mit Marcel Scheibe.*
|
||||
|
||||
---
|
||||
|
||||
### Artikel 2: Spotlight Al Jaddaf: Warum smarte Investoren jetzt auf diesen aufstrebenden Hotspot setzen
|
||||
|
||||
**[Einleitungstext - Fließtext oben]**
|
||||
Die Wahl der richtigen Lage ist der entscheidende Faktor für eine hohe Rendite und langfristige Wertsteigerung. Während weltbekannte Areale wie Downtown Dubai oder die Palm Jumeirah hohe Einstiegspreise aufrufen, suchen smarte Investoren nach den "Hidden Champions" – Vierteln, die vor einer massiven Aufwertung stehen. Eines der spannendsten Entwicklungsgebiete der Stadt ist aktuell Al Jaddaf. Historisch als Werft-Viertel am Dubai Creek bekannt, transformiert sich Al Jaddaf rasend schnell zu einem modernen, strategisch extrem wichtigen Knotenpunkt für exklusives Wohnen und Lifestyle.
|
||||
|
||||
**1. Die strategische Waterfront-Location**
|
||||
Al Jaddaf bietet eine seltene Kombination aus direkter Wasserlage (Waterfront) und zentraler Anbindung. Eingebettet zwischen dem historischen Dubai Creek und der modernen Erweiterung, bietet es unverbaubare Blicke auf die Skyline. Gleichzeitig sind Hotspots wie Downtown Dubai, der Burj Khalifa sowie der Dubai International Airport in nur wenigen Autominuten erreichbar. Diese Logistik macht den Standort unvergleichlich.
|
||||
|
||||
**2. Hohe Mietnachfrage durch perfekte Infrastruktur**
|
||||
Das Viertel zieht zunehmend Young Professionals, Expats und Familien an, die zentral, aber dennoch ruhig und exklusiv leben möchten. Die Nähe zur Healthcare City, zu kulturellen Highlights wie dem Jameel Arts Centre und die Anbindung an die Metro sorgen für eine exzellente Infrastruktur. Dies garantiert Investoren eine kontinuierlich hohe Mietnachfrage und minimale Leerstandsquoten.
|
||||
|
||||
**3. Enormes Potenzial für Capital Appreciation (Wertsteigerung)**
|
||||
Aktuell bietet Al Jaddaf noch Einstiegspreise, die ein exzellentes Preis-Leistungs-Verhältnis darstellen – insbesondere im Vergleich zu bereits etablierten Premium-Vierteln. Mit neuen High-End-Projekten (wie etwa den Azizi Creek Views) wird das Viertel massiv aufgewertet. Wer jetzt investiert, profitiert in den kommenden Jahren nicht nur von attraktiven Mietrenditen, sondern vor allem von einer signifikanten Wertsteigerung der Immobilie.
|
||||
|
||||
**[CTA-Bereich]**
|
||||
*Entdecken Sie unsere aktuellen Off-Market-Projekte in Al Jaddaf und sichern Sie sich Ihre Einheit.*
|
||||
|
||||
---
|
||||
|
||||
### Artikel 3: Turnkey-Investments: Wie die richtige Möblierung Ihre Mietrendite in Dubai maximiert
|
||||
|
||||
**[Einleitungstext - Fließtext oben]**
|
||||
Der Kauf einer Premium-Immobilie in Dubai ist der erste Schritt zu einem erfolgreichen Investment. Doch erst die richtige Nutzung entscheidet über die tatsächliche Rendite. Besonders lukrativ ist die Kurzzeitvermietung (Short-Term-Rental, z. B. via Airbnb), die in Dubai florierende Umsätze generiert. Die Herausforderung für viele internationale Investoren: Wie richtet man eine Wohnung über Tausende Kilometer Entfernung so ein, dass sie aus der Masse heraussticht und Premium-Preise erzielt? Die Antwort lautet "Turnkey" (schlüsselfertig) – intelligente Einrichtungskonzepte, die Ästhetik, Langlebigkeit und Effizienz vereinen.
|
||||
|
||||
**1. Premium-Look für höhere Übernachtungspreise**
|
||||
Der erste Eindruck auf Buchungsplattformen entscheidet. Ein durchschnittlich eingerichtetes Apartment erzielt durchschnittliche Preise. Maßgeschneiderte Design-Konzepte, die perfekt auf die Architektur der Immobilie und die demografische Zielgruppe (Business-Reisende oder Urlauber) abgestimmt sind, erlauben es Ihnen, sich im Premium-Segment zu positionieren und die Mieteinnahmen signifikant zu steigern.
|
||||
|
||||
**2. Der "Turnkey"-Vorteil aus der Ferne**
|
||||
Niemand möchte sich aus Europa heraus um Lieferverzögerungen, fehlende Schrauben oder Handwerkertermine in Dubai kümmern. Ein intelligentes Turnkey-Konzept nimmt Ihnen diesen gesamten Prozess ab. Über das exklusive B2in-Netzwerk erhalten Käufer Zugang zu einem Service, der von der ersten Design-Skizze bis zum fertig bezogenen Bett alles abdeckt – komplett gesteuert durch deutsches Projektmanagement.
|
||||
|
||||
**3. Langlebigkeit durch deutsche Qualitätsstandards**
|
||||
Bei einer hohen Auslastung in der Kurzzeitvermietung werden Möbel stark beansprucht. Billige Ausstattungen müssen oft schon nach kurzer Zeit ersetzt werden, was die Rendite schmälert. Der Fokus auf langlebige Materialien und erstklassige Verarbeitungsqualität – oft gesichert durch Zugänge zu deutschen und europäischen Premium-Herstellern – reduziert die Instandhaltungskosten auf ein Minimum und erhält den Wert Ihres Investments.
|
||||
|
||||
**[CTA-Bereich]**
|
||||
*Als B2in-Immobilienkunde profitieren Sie exklusiv von unserem Einrichtungsnetzwerk. Sprechen Sie uns darauf an!*
|
||||
|
||||
---
|
||||
|
||||
### Artikel 4: Supply-Chain-Management für Entwickler: Warum Vertragssicherheit den Unterschied macht
|
||||
|
||||
**[Einleitungstext - Fließtext oben]**
|
||||
Internationale Immobilienentwickler kennen das Problem: Ein Luxusprojekt ist nahezu fertiggestellt, doch die Innenausstattung aus Europa verzögert sich. Mangelhafte Kommunikation, versteckte Klauseln oder logistische Engpässe führen zu Bauverzögerungen, die Millionen kosten können. In einer globalisierten Welt reicht es nicht aus, Möbel und Materialien nur zu bestellen – man muss ihre Ankunft garantieren. Genau hier setzt professionelles Supply-Chain-Management an. Als verlängerter Arm vor Ort in Deutschland sorgt B2in dafür, dass Verträge nicht nur auf dem Papier existieren, sondern in der Realität pünktlich erfüllt werden.
|
||||
|
||||
**1. Eskalation auf Managementebene**
|
||||
Wenn Lieferungen ins Stocken geraten, verpuffen E-Mails oft im Kundenservice. Effektives Supply-Chain-Management erfordert Durchsetzungskraft und die richtigen Kontakte. Durch ein tief verankertes Netzwerk in der europäischen Einrichtungsbranche greifen wir bei Abweichungen sofort ein und eskalieren Probleme direkt auf Managementebene der Hersteller, um sofortige Lösungen zu erzwingen.
|
||||
|
||||
**2. Aktives Vertragsmanagement statt Hoffnungsprinzip**
|
||||
Vertragssicherheit beginnt vor der Unterschrift. Es geht darum, klare Leistungs- und Qualitätsparameter sowie harte Meilensteine zu definieren. Ein proaktives Management überwacht diese Parameter laufend und sichert Zahlungs- sowie Lieferbedingungen so ab, dass der Entwickler zu jedem Zeitpunkt die volle Kontrolle über den Prozess behält, ohne selbst operative Reibungsverluste zu erleiden.
|
||||
|
||||
**3. Lückenloses Tracking und Qualitätskontrolle**
|
||||
Vertrauen ist gut, Kontrolle vor Ort ist besser. Um Ausfälle zu vermeiden, werden Produktionsfortschritte direkt an der Quelle überwacht. Durch regelmäßige, persönliche Qualitätskontrollen bei den Herstellern stellen wir sicher, dass die Ware nicht nur pünktlich verladen wird, sondern exakt den geforderten Premium-Standards der Immobilien-Projektentwickler entspricht.
|
||||
|
||||
**[CTA-Bereich]**
|
||||
*Suchen Sie einen starken Partner für Ihre Beschaffung aus Deutschland? Kontaktieren Sie unser B2B-Team.*
|
||||
|
||||
---
|
||||
|
||||
### Artikel 5: Local for Local: Wie der regionale Möbelhandel die Zukunft des Wohnens prägt
|
||||
|
||||
**[Einleitungstext - Fließtext oben]**
|
||||
In den letzten Jahren schien der Trend unaufhaltsam: Gigantische Online-Plattformen dominierten den Möbelmarkt. Doch der Markt wandelt sich. Konsumenten sehnen sich wieder nach Haptik, persönlicher Beratung und sofortiger Verfügbarkeit. Gleichzeitig schlummern in den regionalen Möbelhäusern echte Schätze ("Hidden Gems"), die online oft unsichtbar bleiben. Das Konzept "Local for Local" setzt genau hier an: Es ist ein digitaler Marktplatz-Ansatz, der nicht den anonymen Großhandel, sondern den lokalen Fachhandel stärkt – und damit eine Brücke zwischen digitaler Bequemlichkeit und regionaler Stärke baut.
|
||||
|
||||
**1. Digitale Sichtbarkeit für lokale Bestände**
|
||||
Die Frage "Was ist heute in meiner Nähe verfügbar?" konnte der lokale Handel digital oft nicht beantworten. Moderne Marktplatz-Technologien ändern das. Sie geben regionalen Händlern die Werkzeuge an die Hand, ihre sofort verfügbare Ausstellungs- und Lagerware einem breiten, kaufkräftigen Publikum (wie etwa neuen Immobilienbesitzern) sichtbar zu machen – ganz ohne komplexe eigene IT-Infrastruktur.
|
||||
|
||||
**2. Support your Locals – Ein Gewinn für alle**
|
||||
Das "David gegen Goliath"-Prinzip bringt die Kunden zurück in die Geschäfte. Käufer profitieren von exklusiven Preisen für Ausstellungsstücke und Markenware, die oft günstiger ist als im Großmarkt. Der Händler wiederum steigert seine Frequenz vor Ort, baut Liquidität durch schnellen Abverkauf auf und gewinnt Neukunden, die den Wert echter, physischer Beratung schätzen.
|
||||
|
||||
**3. Smarte Vernetzung statt anonymer Plattform**
|
||||
Die Zukunft gehört nicht den geschlossenen Online-Shops, sondern vernetzten Ökosystemen. Wenn Immobilienmakler, Kunden und regionale Händler auf einer Plattform zusammenkommen, entsteht ein Kreislauf des Vertrauens. Der Immobilienkauf wird zum Auslöser für den Möbelkauf, und der lokale Fachhandel wird zum verlässlichen Partner für die perfekte Einrichtung – lokal gedacht, intelligent vernetzt.
|
||||
|
||||
**[CTA-Bereich]**
|
||||
*Das B2in-Einrichtungsnetzwerk startet bald. Möchten Sie als Händler von Anfang an dabei sein? Sprechen Sie uns an.*
|
||||
|
||||
---
|
||||
|
||||
|
||||
210
dev/12-03-2026/tasks.md
Normal file
210
dev/12-03-2026/tasks.md
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
### Briefing
|
||||
|
||||
Der aktuelle Stand der Webseite mit den Inhalten wurde für sehr gut befunden. Jedoch gilt es jetzt vorab eine Version online zu schalten, die schon mal hauptsächlich den Immobilienbereich abgedeckt. D.h. der Immobilienbereich soll noch etwas mehr erklärt werden. Die Möbel Inhalte müssen etwas zurück rücken beziehungsweise erst mal nur teasern.
|
||||
|
||||
### Wichtig
|
||||
Alle bestehenden Inhalte, die jetzt vorliegen, sollen erhalten bleiben und als Back-up im System zurückbleiben. Diese sollen nicht gelöscht oder überschrieben werden, sondern wenn wir jetzt gerade im Möbelbereich neue Inhalte definieren, gilt es die alten zu konservieren, so dass wir auch schnell diese zugänglich haben. Am besten wäre es, wenn wir hier direkt subseiten erstellen, die wir über einen Link mit einer Site Map erreichen können. So können wir die Webseite online stellen mit den nötigen Inhalten und gleichzeitig aber auch an neuen Inhalten arbeiten, ohne dass sie Direkt zugänglich sind. Es gibt ja auch eine Theme Seite, so ähnlich können wir es mit aufbauen oder am besten einfach mit einbauen.
|
||||
|
||||
### Konzept
|
||||
Das ist ein hervorragender Plan für den "Soft Launch". Wenn das Möbel-Portal noch in der Entwicklung ist, wäre es ein strategischer Fehler, jetzt schon zu tief in die technischen Details (wie Cashback-Prozesse oder Portal-Logins) zu gehen. Wir teasern die Vision an, aber legen den **vollen Fokus und die Conversion-Power auf den Immobilien-Part**, der ab sofort Geld bringt.
|
||||
|
||||
Die Screenshots der aktuellen Seite sehen extrem clean, hochwertig und vertrauenserweckend aus. Das Design trägt die Premium-Positionierung perfekt.
|
||||
|
||||
Und jetzt zu Marcels E-Mail: **Dieser Text ist verkaufspsychologisch pures Gold.** Er nutzt einen genialen Ansatz: Er spricht die typisch deutsche Skepsis direkt an, nimmt sie ernst ("Ich bin selbst so") und positioniert sich nicht als typischer Makler, sondern als "Brückenbauer" und Insider. Das schafft massiv Vertrauen.
|
||||
|
||||
Damit dieser starke Text auf der Website aber nicht als ermüdende "Textwüste" wahrgenommen wird, müssen wir ihn für das Web **scannbar strukturieren**.
|
||||
|
||||
Hier ist mein strategischer Umsetzungsplan für den Live-Gang:
|
||||
|
||||
### 1. Fokus-Bereich: Immobilien (Die Vertrauens-Bühne)
|
||||
|
||||
Wir nutzen Marcels Text als zentrales Element auf der `/immobilien`-Seite. Ich empfehle, die Seite in klare Informations-Häppchen aufzuteilen:
|
||||
|
||||
* **Sektion 1: Hero & Aktuelle Projekte (Bleibt wie im Screenshot)**
|
||||
* Der User sieht sofort: Hier gibt es konkrete, hochkarätige Off-Market-Deals (wie das Azizi Creek Views 4).
|
||||
|
||||
|
||||
* **Sektion 2: Warum Dubai? (Fakten & Rendite)**
|
||||
* *Format:* Ein cleanes Grid oder eine Icon-Liste.
|
||||
* *Inhalt aus Marcels Text:* Keine Einkommensteuer, keine Kapitalertragsteuer, Escrow-System (Sicherheit!), 6-9% Rendite, Golden Visa.
|
||||
|
||||
|
||||
* **Sektion 3: Der Kaufprozess in Dubai (Transparenz)**
|
||||
* *Format:* Ein visuelles Stufen-Modell (Schritt 1 bis 4) oder ein Akkordeon (Aufklapp-Menü).
|
||||
* *Inhalt:* Reservierung (3-10%) -> Anzahlung & SPA (10%) -> DLD Registrierung (4%) -> Finaler Kaufvertrag.
|
||||
|
||||
|
||||
* **Sektion 4: Persönliche Begleitung & "Die Brücke" (Der stärkste Part!)**
|
||||
* *Format:* Ein großes Bild von Marcel (vielleicht vor Ort in Dubai) mit einem direkten Zitat.
|
||||
* *Inhalt:* Hier bringen wir seinen "Anti-Sales-Pitch" unter. *"Der Markt spricht für sich – dafür brauchen Sie mich nicht. Meine Aufgabe ist eine andere: Ich baue die Brücke zwischen der deutschen Denkweise und dem Tempo in Dubai."* Hier erwähnen wir auch zwingend die **Möblierungs-Synergie** (sein B2in-Möbel-Netzwerk für die spätere Vermietung).
|
||||
|
||||
|
||||
* **Sektion 5: Passt ein Dubai-Investment zu Ihnen? (Der Mindset-Check)**
|
||||
* *Format:* Eine farblich abgesetzte Box ("Wer hier richtig ist – und wer nicht"). Das qualifiziert die Leads extrem gut vor und spart Marcel Zeit mit den falschen Kunden.
|
||||
|
||||
|
||||
|
||||
### 2. Reduktions-Bereich: Möbel & B2B (Die Vision teasern)
|
||||
|
||||
Da das Portal noch in der Entwicklung ist, müssen wir auf den Seiten **"Einrichtungsnetzwerk"** und **"Für Entwickler & Partner"** den Fuß vom Gas nehmen.
|
||||
|
||||
* **Was raus muss:** Streiche alle Erklärungen, die nach sofortiger Handlung klingen, die noch nicht möglich ist (z.B. tiefe Erklärungen zum Beleg-Upload, genaue Cashback-Prozentsätze oder Login-Aufforderungen).
|
||||
* **Was bleibt (Das Wording):** Wir wandeln diese Seiten in "Visions- und Wartelisten-Seiten" um.
|
||||
* *Neuer Fokus:* "Wir bauen aktuell das intelligenteste Local-for-Local Einrichtungsnetzwerk. Als Immobilienkunde von B2in profitieren Sie in Zukunft exklusiv von unserem Closed-Shop. Sind Sie lokaler Händler? Kontaktieren Sie uns für eine Vorab-Registrierung."
|
||||
* Das hält die Seiten relevant, erklärt die Synergie (Möbel + Immobilien), aber erzeugt keinen Frust über fehlende Features.
|
||||
|
||||
|
||||
|
||||
---
|
||||
---
|
||||
---
|
||||
|
||||
|
||||
### Inhaltlicher Vorschlag für die Seiten
|
||||
|
||||
Textvorschlag vom Kunden
|
||||
|
||||
Warum sich eine Immobilieninvestition in Dubai lohnt
|
||||
Dubai hat sich in den letzten Jahren zu einem der dynamischsten Immobilienmärkte der Welt entwickelt. Investoren aus Europa, Asien und Amerika schätzen nicht nur die attraktiven Renditen, sondern vor allem die klaren rechtlichen Strukturen, die internationale Investitionen ermöglichen und schützen.
|
||||
Doch eine Investition in Dubai ist mehr als nur eine finanzielle Entscheidung. Sie ist auch eine Entscheidung für einen internationalen Lebensstil, wirtschaftliche Stabilität und Zugang zu einem der wachstumsstärksten Märkte der Welt.
|
||||
Wie der Kaufprozess in Dubai typischerweise abläuft
|
||||
Der Immobilienkauf in Dubai folgt einem klar strukturierten Ablauf.
|
||||
1. Reservierung der Wohnung
|
||||
Sobald Sie sich für eine Wohnung entschieden haben, wird diese zunächst für Sie reserviert. Dafür wird üblicherweise eine Reservierungsgebühr (Booking Fee) gezahlt, die häufig zwischen etwa 3 % und 10 % des Kaufpreises liegt. Mit dieser Zahlung wird die gewünschte Einheit vorübergehend aus dem Verkauf genommen und für Sie blockiert.
|
||||
2. Anzahlung und Vertragsvorbereitung
|
||||
Anschließend wird in der Regel eine erste Anzahlung von etwa 10 % des Kaufpreises geleistet. Auf dieser Grundlage wird der offizielle Kaufvertrag vorbereitet – der sogenannte Sales & Purchase Agreement (SPA).
|
||||
3. Registrierung beim Dubai Land Department
|
||||
Zusätzlich wird eine staatliche Registrierungsgebühr von 4 % des Kaufpreises an das Dubai Land Department (DLD) gezahlt. Erst mit dieser Registrierung wird der Immobilienkauf offiziell im staatlichen Register verankert.
|
||||
4. Finaler Kaufvertrag
|
||||
Nach Abschluss dieser Schritte wird der endgültige Kaufvertrag ausgestellt und beim Dubai Land Department registriert. Damit ist Ihr Eigentumsrecht offiziell gesichert.
|
||||
Ein wichtiger Sicherheitsmechanismus in Dubai ist dabei das Escrow-System: Zahlungen werden auf Treuhandkonten geleistet, sodass Bauträger Gelder nur entsprechend dem Baufortschritt erhalten.
|
||||
Warum Dubai für Investoren so attraktiv ist
|
||||
Dubai bietet eine Reihe von Rahmenbedingungen, die weltweit nahezu einzigartig sind:
|
||||
keine Einkommensteuer auf Mieteinnahmen
|
||||
keine Kapitalertragsteuer beim Verkauf
|
||||
ein staatlich regulierter Immobilienmarkt
|
||||
hohe internationale Nachfrage nach Wohnraum
|
||||
eine stabile Währung (AED an den US-Dollar gekoppelt)
|
||||
attraktive Mietrenditen im internationalen Vergleich
|
||||
Viele Investoren erzielen jährliche Renditen zwischen 6 % und 9 %, je nach Lage und Nutzung der Immobilie.
|
||||
Darüber hinaus bietet der Besitz einer Immobilie in Dubai häufig auch Vorteile bei Aufenthaltsgenehmigungen, beispielsweise über sogenannte Investor- oder Golden-Visa-Programme.
|
||||
Warum gerade deutsche Käufer Unterstützung schätzen
|
||||
Für viele deutsche Investoren wirkt der Immobilienkauf in Dubai zunächst ungewohnt.
|
||||
In Deutschland ist man gewohnt, dass ein Immobilienkauf über Notar, Grundbuch und einen stark formalisierten Prozess abgewickelt wird. Der Markt in Dubai funktioniert anders: Die Abläufe sind deutlich schneller, digitaler und stärker projektbezogen organisiert.
|
||||
Gerade diese Unterschiede führen häufig zu Unsicherheiten.
|
||||
Der Markt und die Qualität vieler Projekte sprechen in der Regel für sich – dafür brauchen Sie mich im Grunde nicht. Meine Aufgabe ist eine andere: Ich begleite Sie durch die Abwicklungen und sorge dafür, dass Sie jederzeit verstehen, wie der Prozess funktioniert und welche Schritte als nächstes folgen.
|
||||
Persönliche Begleitung statt reiner Vermittlung
|
||||
Ich habe selbst eine Wohnung in Dubai erworben und kenne daher nicht nur die Theorie, sondern auch die praktischen Abläufe eines Immobilienkaufs vor Ort.
|
||||
Ich stehe im permanenten Austausch mit Bauträgern und Projektentwicklern und verfolge die Entwicklungen der einzelnen Projekte sehr genau. Mehrmals im Jahr reise ich persönlich nach Dubai, um mir vor Ort ein Bild vom Baufortschritt zu machen und mit den Projektpartnern zu sprechen.
|
||||
Diese Nähe zum Markt ermöglicht es mir, meine Kunden nicht nur bei der Auswahl einer passenden Immobilie zu unterstützen, sondern sie auch durch den gesamten Kaufprozess zu begleiten.
|
||||
Meine Unterstützung endet jedoch nicht mit dem Kaufvertrag. Viele Käufer interessieren sich auch dafür, ihre Immobilie später zu vermieten, beispielsweise über Kurzzeitvermietungsmodelle wie Airbnb, oder möchten ihre Wohnung professionell möblieren, um eine attraktive Rendite zu erzielen.
|
||||
Auch bei diesen Themen stehe ich Ihnen zur Seite – von der Möblierung über Einrichtungskonzepte bis hin zur Vorbereitung der Vermietung. Gerade hier kann meine Erfahrung aus der Möbelbranche und meine internationalen Kontakte ein zusätzlicher Vorteil sein.
|
||||
Mein Ziel ist nicht, möglichst viele Immobilien zu verkaufen. Mein Ziel ist es, dass Sie eine fundierte Entscheidung treffen und Ihr Investment langfristig erfolgreich ist.
|
||||
Sind Sie der richtige Kunde für ein Investment in Dubai?
|
||||
Eine Investition in Dubai ist nicht für jeden die richtige Entscheidung. Sie richtet sich an Menschen, die international denken, Chancen erkennen und bereit sind, neue Wege zu gehen.
|
||||
Dubai steht für Dynamik, Geschwindigkeit und globale Vernetzung. Der Staat setzt auf hohe Sicherheitsstandards im öffentlichen Raum, auf moderne Infrastruktur und auf eine klare wirtschaftliche Wachstumsstrategie. Gleichzeitig bedeutet das auch, dass der Umgang mit Themen wie Datenschutz und Regulierung anders organisiert ist als in Deutschland.
|
||||
Wer in Dubai investiert, entscheidet sich bewusst für ein System, das Effizienz, wirtschaftliche Entwicklung und internationale Offenheit priorisiert, auch wenn die Rahmenbedingungen sich von den in Deutschland gewohnten Strukturen unterscheiden.
|
||||
Wenn Sie daran glauben, dass sich wirtschaftliche Machtzentren in der Welt verschieben – und dass Städte wie Dubai in den kommenden Jahrzehnten eine noch größere Rolle spielen werden –, dann kann eine Immobilie dort ein strategisch sinnvoller Baustein sein. Viele Beobachter gehen davon aus, dass Dubai in den nächsten 20 Jahren eine Bedeutung erreichen könnte, die heute mit Metropolen wie New York, London oder Singapur verbunden wird.
|
||||
Dann sind Sie bei uns genau richtig.
|
||||
Wenn Sie hingegen ein System bevorzugen, das maximale formale Sicherheit verspricht, das jedoch häufig mit langen Entscheidungswegen, umfangreicher Bürokratie und sehr komplexen Abläufen verbunden ist, dann wird der Immobilienmarkt in Dubai möglicherweise nicht zu Ihren Erwartungen passen.
|
||||
Diese Haltung ist übrigens sehr typisch für viele deutsche Investoren – und ich selbst gehöre im Grunde auch zu dieser Denkweise. Genau deshalb weiß ich aus eigener Erfahrung, wie groß dieser Schritt zunächst wirken kann, selbst wenn die wirtschaftlichen Argumente eigentlich klar erscheinen.
|
||||
In der Praxis zeigt sich jedoch häufig, dass dieser Schritt gar nicht so groß ist, wie man ihn sich zunächst vorstellt.
|
||||
Meine Aufgabe ist es nicht, Ihr Mindset zu verändern – das möchte und kann ich auch gar nicht. Meine Aufgabe ist vielmehr, eine Brücke zu bauen: zwischen der deutschen Denkweise und den Abläufen eines internationalen Immobilienmarktes wie in Dubai.
|
||||
Ich begleite Sie durch die Prozesse, erkläre die Unterschiede und sorge dafür, dass Sie sich bei jedem Schritt sicher fühlen. Ob Sie diese Brücke am Ende überqueren möchten, entscheiden selbstverständlich Sie selbst.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Sektion 1: Hero-Bereich (Ganz oben)
|
||||
|
||||
*Visuell: Großes, hochwertiges Hintergrundbild (z.B. Dubai Skyline oder Premium-Immobilie).*
|
||||
|
||||
**Headline:** Investieren in globale Dynamik. Mit deutscher Verlässlichkeit.
|
||||
|
||||
**Subline:** Exklusive Off-Market-Projekte, attraktive Renditen und eine Begleitung, die weit über den Kaufvertrag hinausgeht. Entdecken Sie den Immobilienmarkt in Dubai.
|
||||
|
||||
**Button:** [ Aktuelle Projekte ansehen ] *(Scrollt zu den Projekt-Kacheln, z.B. Azizi)*
|
||||
|
||||
---
|
||||
|
||||
### Sektion 2: Warum Dubai? (Die harten Fakten)
|
||||
|
||||
*Visuell: Ein cleanes Grid mit Icons für schnelle Lesbarkeit.*
|
||||
|
||||
**Headline:** Warum sich ein Investment in Dubai lohnt
|
||||
|
||||
**Einleitungstext (Kurz):** Dubai ist nicht nur eine finanzielle Entscheidung, sondern der Zugang zu einem der wachstumsstärksten Märkte der Welt. Investoren schätzen die klaren rechtlichen Strukturen und Rahmenbedingungen, die weltweit nahezu einzigartig sind:
|
||||
|
||||
**Bulletpoints (mit Icons):**
|
||||
|
||||
* **0 % Steuern:** Keine Einkommensteuer auf Mieteinnahmen, keine Kapitalertragsteuer beim Verkauf.
|
||||
* **Starke Renditen:** Attraktive Mietrenditen von historisch 6 % bis 9 % jährlich.
|
||||
* **Hohe Sicherheit:** Ein staatlich regulierter Markt mit dem sicheren Escrow-Treuhand-System.
|
||||
* **Stabile Währung:** Der Dirham (AED) ist fest an den US-Dollar gekoppelt.
|
||||
* **Golden Visa:** Sichern Sie sich Aufenthaltsgenehmigungen über attraktive Investor-Programme.
|
||||
|
||||
---
|
||||
|
||||
### Sektion 3: Der Kaufprozess (Transparenz schaffen)
|
||||
|
||||
*Visuell: Eine Zeitleiste, 4 nummerierte Kacheln oder ein Aufklapp-Menü (Akkordeon).*
|
||||
|
||||
**Headline:** Klar strukturiert: Der Kaufprozess in Dubai
|
||||
|
||||
**Einleitung:** Der Markt in Dubai ist schneller, digitaler und projektbezogen organisiert. Jeder Schritt ist durch das staatliche Escrow-System (Treuhandkonten nach Baufortschritt) maximal abgesichert.
|
||||
|
||||
* **1. Reservierung (Booking Fee):** Mit einer Gebühr von ca. 3–10 % wird Ihre Wunsch-Einheit offiziell aus dem Verkauf genommen und für Sie blockiert.
|
||||
* **2. Anzahlung & Vertrag (SPA):** Nach einer ersten Anzahlung (meist 10 %) wird der offizielle Kaufvertrag, das Sales & Purchase Agreement (SPA), erstellt.
|
||||
* **3. Staatliche Registrierung (DLD):** Durch die Zahlung der Registrierungsgebühr (4 %) an das Dubai Land Department wird Ihr Eigentum offiziell im staatlichen Register verankert.
|
||||
* **4. Finaler Kaufvertrag:** Ihr Eigentumsrecht ist offiziell gesichert. Die weiteren Zahlungen erfolgen streng nach Baufortschritt auf sichere Treuhandkonten.
|
||||
|
||||
---
|
||||
|
||||
### Sektion 4: "Die Brücke" & Der Möbel-USP (Marcels Pitch)
|
||||
|
||||
*Visuell: Hochwertiges Foto von Marcel Scheibe (am besten vor Ort / Business-Look). Text daneben.*
|
||||
|
||||
**Headline:** "Der Markt spricht für sich. Meine Aufgabe ist eine andere."
|
||||
|
||||
**Text (Zitatformat oder direkter Fließtext):**
|
||||
"Für viele deutsche Investoren wirkt der Immobilienkauf in Dubai zunächst ungewohnt. In Deutschland sind wir Notare, Grundbücher und hochbürokratische Prozesse gewohnt. Dubai ist dynamischer.
|
||||
|
||||
*Ich bin nicht hier, um Ihnen den Markt zu verkaufen – die Qualität der Projekte spricht für sich. Meine Aufgabe ist es, Ihre Brücke zu sein.* Als Investor, der selbst in Dubai gekauft hat, kenne ich die Praxis. Ich bin regelmäßig vor Ort, stehe im permanenten Austausch mit Bauträgern und begleite Sie durch den kompletten Prozess. Ich übersetze die internationale Geschwindigkeit in deutsche Verlässlichkeit.
|
||||
|
||||
**Ihr B2in-Vorteil:** Meine Begleitung endet nicht beim Kauf. Planen Sie die lukrative Kurzzeitvermietung (z.B. Airbnb)? Über unser B2in-Netzwerk und meine Wurzeln in der Möbelbranche realisieren wir für Sie komplette Einrichtungskonzepte – für maximale Rendite bei minimalem Aufwand."
|
||||
|
||||
**Button:**
|
||||
[ Persönliches Beratungsgespräch vereinbaren ]
|
||||
|
||||
---
|
||||
|
||||
### Sektion 5: Der Mindset-Check (Leads qualifizieren)
|
||||
|
||||
*Visuell: Leicht farblich abgesetzte Box am Ende der Seite.*
|
||||
|
||||
**Headline:** Sind Sie der richtige Investor für Dubai?
|
||||
|
||||
**Text:**
|
||||
Dubai richtet sich an Menschen, die international denken. Der Staat priorisiert Effizienz, wirtschaftliche Entwicklung und enorme Geschwindigkeit.
|
||||
|
||||
Wenn Sie an die Verschiebung weltweiter Wirtschaftszentren glauben und ein dynamisches System schätzen, ist Dubai der strategisch perfekte Baustein für Ihr Portfolio. Dann sind Sie bei uns genau richtig.
|
||||
|
||||
Wenn Sie jedoch ein System bevorzugen, das auf langsamen Entscheidungswegen und maximaler Bürokratie aufbaut, wird dieser Markt nicht zu Ihren Erwartungen passen.
|
||||
|
||||
**Der Schritt ist kleiner, als Sie denken.** Lassen Sie uns gemeinsam herausfinden, ob ein Investment in Dubai in Ihre Strategie passt.
|
||||
|
||||
---
|
||||
|
||||
### Wie geht es weiter?
|
||||
|
||||
Mit diesen Texten ist die Immobilien-Seite **inhaltlich sofort startklar**.
|
||||
|
||||
Damit der Rollout perfekt wird: Soll ich für die Seiten **"Einrichtungsnetzwerk"** und **"Für Entwickler & Partner"** (die ja jetzt im "Soft Launch" bleiben und nicht zu viel versprechen dürfen) noch jeweils 2-3 anpassende Sätze schreiben, damit dort keine "Baustellen-Texte" oder falschen Versprechungen stehen?
|
||||
|
||||
|
||||
|
||||
|
||||
Anpassung an den Inhalten und Optik der Seite
|
||||
|
||||
Unter about
|
||||
- Das Führungsteam erst mal rausnehmen, Den grauen Hintergrund dann bei unsere Werte hinterlegen,
|
||||
121
dev/27-02-2026/B2in Website-Architektur.md
Normal file
121
dev/27-02-2026/B2in Website-Architektur.md
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
|
||||
|
||||
# 🏗️ Umsetzungskonzept: B2in Website-Architektur 02.03.2026
|
||||
|
||||
## 1. Anpassungen auf der Startseite (`home.blade.php` / `b2in.de`)
|
||||
|
||||
**Ziel:** Die Startseite wird zur "Weiche" (Triage). Sie darf nicht mehr versuchen, alle Details zu erklären, sondern muss den Besucher sofort in seine richtige "Welt" (Immobilien oder B2B/Möbel) leiten.
|
||||
|
||||
* **1.1. Hero-Bereich (Ganz oben)**
|
||||
* **Änderung:** Der aktuelle Text wird durch eine übergeordnete "Klammer" ersetzt.
|
||||
* **Neue Headline:** *B2in – Wo exklusive Immobilien-Investments auf smartes Interior treffen.*
|
||||
* **Neue Subline:** *Ihr Partner für renditestarke internationale Immobilien und globales Supply-Chain-Management im Interior-Bereich.*
|
||||
* **Neue Call-to-Actions (Buttons):** Es müssen zwingend zwei gleichwertige Buttons nebeneinander (oder optisch stark getrennt) platziert werden:
|
||||
* Button 1 (Gold/Premium-Look): **"Zu den Immobilien-Projekten"** -> *Verlinkt auf `/immobilien*`
|
||||
* Button 2 (Corporate/Clean-Look): **"Für Entwickler & Partner"** -> *Verlinkt auf `/supply-chain*`
|
||||
|
||||
|
||||
|
||||
|
||||
* **1.2. FounderBar (Bereits umgesetzt)**
|
||||
* **Behalten:** Bleibt direkt unter dem Hero-Bereich als Vertrauensanker ("B2in by Marcel Scheibe").
|
||||
|
||||
|
||||
* **1.3. Die "Synergie-Sektion" (NEU auf der Startseite)**
|
||||
* **Inhalt:** Ein kurzer, starker Block, der erklärt, *warum* wir beides machen.
|
||||
* **Text-Idee:** *"Das B2in-Ökosystem: Wir verbinden den Immobilienkauf mit der perfekten Einrichtung. Immobilien-Investoren profitieren von unserem exklusiven Möbel-Netzwerk – Projektentwickler von unserer deutschen Vertragssicherheit im Supply-Chain-Management."*
|
||||
|
||||
|
||||
* **1.4. Was von der Startseite VERSCHWINDEN muss (Aufräumen!)**
|
||||
* Alle tiefgreifenden Details zum "Local for Local" Möbel-Marktplatz, Cashback-Regeln oder das harte B2B-Supply-Chain-Wording müssen von der Startseite runter. Diese ziehen um auf die Unterseiten.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 2. NEUE Landingpage: Immobilien (`/immobilien`)
|
||||
|
||||
**Ziel:** Emotionale Verkaufsbühne für B2C-Kunden und Investoren. Vertrauen, Rendite, Exklusivität.
|
||||
|
||||
* **2.1. Hero-Bereich**
|
||||
* **Headline:** *Investieren Sie in die Zukunft – Dubai, Lissabon & mehr.*
|
||||
* **Subline:** *Exklusive Off-Market-Projekte und High-Yield-Investments. Persönlich kuratiert und begleitet von Marcel Scheibe.*
|
||||
|
||||
|
||||
* **2.2. Aktuelle Launches & Projekte (Hier kommt der neue Text rein!)**
|
||||
* **Design:** Hochwertige Kachel- oder Listenansicht.
|
||||
* **Inhalt (Beispiel Azizi):**
|
||||
* *Label:* 🚀 NEW LAUNCH (03.03.2026)
|
||||
* *Titel:* **Azizi Developments: Creek Views 4 (Al Jaddaf, Dubai)**
|
||||
* *Highlights:* Prime Waterfront Views, 1BR ab 1,125,000 AED, Exklusives 3BR Penthouse (Single Inventory!).
|
||||
* *Fokus:* High Rental Demand & Capital Appreciation.
|
||||
* *CTA:* "Jetzt Exposé & Verfügbarkeit anfragen" -> *Öffnet Kontaktformular an Marcel.*
|
||||
|
||||
|
||||
|
||||
|
||||
* **2.3. Der "B2in Möbel-Vorteil" (Der Synergie-Hack)**
|
||||
* **Inhalt:** Ein auffälliges Banner auf dieser Seite.
|
||||
* **Text:** *"Ihr Investment, Ihr Vorteil: Als Käufer einer B2in-Immobilie erhalten Sie exklusiven Insider-Zugang zu unserem B2in-Möbelnetzwerk. Richten Sie Ihre Immobilie zu unschlagbaren Partner-Konditionen ein."*
|
||||
|
||||
|
||||
* **2.4. Trust & Kontakt**
|
||||
* **Inhalt:** Fokus auf "Investor Evenings" und direkte Terminbuchung (Calendly-Link o.ä.) mit Marcel Scheibe.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 3. NEUE Landingpage: Supply-Chain & B2B-Netzwerk (`/supply-chain` oder `/b2b`)
|
||||
|
||||
**Ziel:** Rationale, prozessorientierte Verkaufsseite für Projektentwickler (Developer), Händler und Makler. Fokus auf "Durchsetzungskraft" und "Netzwerk".
|
||||
|
||||
* **3.1. Hero-Bereich**
|
||||
* **Headline:** *Globales Supply-Chain-Management & Local for Local Netzwerk.*
|
||||
* **Subline:** *Wir sind der verlängerte Arm für Immobilienentwickler und das stärkste Netzwerk für den lokalen Möbelfachhandel.*
|
||||
|
||||
|
||||
* **3.2. Sektion 1: Für Immobilienentwickler (Der Text vom Kunden)**
|
||||
* **Design:** Klare Struktur, evtl. mit Icons für die 3 Schritte.
|
||||
* **Inhalt (Direkt übernommen):**
|
||||
* Einleitung: *"Wir übernehmen die operative und strategische Steuerung Ihrer Beschaffung in Deutschland..."*
|
||||
* Punkt 1: *Vertragsmanagement* (Absicherung, Qualitätsdefinition).
|
||||
* Punkt 2: *Vertragssicherung & Durchsetzung* (Eskalation auf Managementebene!).
|
||||
* Punkt 3: *Tracking & Qualitätskontrolle.*
|
||||
|
||||
|
||||
|
||||
|
||||
* **3.3. Sektion 2: Für Makler & Händler (Das Möbel-Ökosystem)**
|
||||
* **Inhalt:** Hier wird das "Local for Local"-Konzept kurz angeteasert.
|
||||
* **Text für Makler:** *"Nutzen Sie unser Möbelnetzwerk als Closing-Geschenk für Ihre Immobilienkunden."*
|
||||
* **Text für Händler:** *"Werden Sie Teil unseres Netzwerks und erhalten Sie qualifizierte Leads von Immobilienkäufern."*
|
||||
* **CTA:** "Partner-Zugang anfragen" oder "Login für Partner".
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 4. Navigation & Menüstruktur (Header & Footer)
|
||||
|
||||
Das Hauptmenü (Navigation) muss die neue Struktur widerspiegeln, damit sich niemand verläuft.
|
||||
|
||||
**Altes/Bisheriges Menü (gedanklich):**
|
||||
Oft vermischt (Über uns, Magazin, Marktplatz, Partner, FAQ).
|
||||
|
||||
**Neues Hauptmenü (Vorschlag):**
|
||||
|
||||
1. **Immobilien** (Verlinkt auf `/immobilien`)
|
||||
2. **Supply-Chain & B2B** (Verlinkt auf `/supply-chain`)
|
||||
3. **Magazin** (Bleibt)
|
||||
4. **Über B2in** (Führt zu einer "About"-Sektion mit der Vision)
|
||||
5. **[Button: Partner-Login]** (Ganz rechts, hervorgehoben für das Möbel-Clearing/Portal)
|
||||
|
||||
---
|
||||
|
||||
### Nächste Schritte für dich:
|
||||
|
||||
Mit diesem Dokument kannst du (oder der Webdesigner) exakt sehen, welche Texte wohin gehören.
|
||||
|
||||
1. **Abnahme:** Passt diese Aufteilung für dich und den Kunden?
|
||||
2. **Texte:** Fehlen dir für einen dieser Blöcke (z.B. die "Synergie-Sektion" auf der Startseite) noch die final ausformulierten, werblichen Sätze? Wenn ja, schreibe ich dir die sofort passgenau auf.
|
||||
267
dev/27-02-2026/b2in-local-for-local.md
Normal file
267
dev/27-02-2026/b2in-local-for-local.md
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
Konzeptpapier: B2in / Local for Local Marktplatz-Ökosystem
|
||||
|
||||
**Status:** Final | **Version:** 1.1 (Update: Marken-Hierarchie) 27.02.2026
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
Das B2in-Ökosystem ist ein hybrider Marktplatz, der den Immobilienkauf ("Moment of Need") mit der Einrichtung verbindet. Es agiert als "Closed Shop" (Zugang nur über Makler).
|
||||
|
||||
**B2in** ist die zentrale B2B-Plattform und Technologie.
|
||||
|
||||
**Local for Local** ist das verbindende Prinzip innerhalb des Marktplatzes, das die Endkunden-Marken (`style2own`, `stileigentum`) mit den regionalen Händlern verknüpft.
|
||||
|
||||
Der USP liegt in der **Transparenz lokaler Verfügbarkeit** (Säule A: Local Express) und **exklusiven Insider-Konditionen** (Säule B: Smart Club), abgesichert durch ein **Cashback-System**.
|
||||
|
||||
---
|
||||
|
||||
## 2. Marken-Architektur & Entry-Points
|
||||
|
||||
Wir unterscheiden strikt zwischen dem **B2B-Zugang** (Partner) und den **B2C-Einstiegen** (Endkunden).
|
||||
|
||||
### A. Der B2B-Kanal (Die Dachmarke)
|
||||
|
||||
- **Marke:** **B2in**
|
||||
- **Zielgruppe:** Immobilienmakler, Händler, Hersteller.
|
||||
- **Funktion:** Akquise, Partner-Login, Verwaltung ("Maschinenraum").
|
||||
- **Positionierung:** "Das Netzwerk für Immobilien & Einrichtung." Hier findet das Business statt.
|
||||
|
||||
### B. Die B2C-Kanäle (Die Zielgruppen-Türen)
|
||||
|
||||
Der Makler entscheidet anhand der Käuferstruktur, über welche "Tür" der Kunde das System betritt. Beide Landingpages führen in denselben Marktplatz:
|
||||
|
||||
**1. Marke: Style2Own**
|
||||
|
||||
- **Zielgruppe:** Young Professionals, Erstbezug, Urban, Trend-orientiert.
|
||||
- **Tonalität:** Modern, "Du"-Ansprache, Lifestyle-Fokus.
|
||||
- **Story:** "Dein Style. Deine Stadt."
|
||||
|
||||
**2. Marke: StilEigentum**
|
||||
|
||||
- **Zielgruppe:** Gehobenes Segment, Best Ager, Villen, Qualitäts-orientiert.
|
||||
- **Tonalität:** Exklusiv, "Sie"-Ansprache, Werte-Fokus.
|
||||
- **Story:** "Exzellenz und Tradition."
|
||||
|
||||
### C. Das verbindende Element (Inside the Portal)
|
||||
|
||||
- **Prinzip:** **Local for Local**
|
||||
- **Funktion:** Sobald der Kunde (egal ob über `style2own` oder `stileigentum`) eingeloggt ist, greift die "Local for Local"-Logik.
|
||||
- **Erlebnis:** Das Portal passt sich dem Hub des Kunden an und zeigt die lokalen Händler als "Local Heroes". Es ist die Klammer, die den Marktplatz definiert.
|
||||
|
||||
---
|
||||
|
||||
## 3. Marktplatz & Produktstrategie (Das 2-Säulen-Modell)
|
||||
|
||||
Im Portal angekommen, wird das Angebot nach Bedürfnis (**Zeit vs. Planung**) getrennt.
|
||||
|
||||
### Säule A: "Local Express" (Phase 1 Focus)
|
||||
|
||||
- **Narrativ:** "David gegen Goliath" (Support your Locals).
|
||||
- **Angebot:** Sofort verfügbare Ausstellungsstücke, Lagerware und kuratierte "Hidden Gems" der lokalen Fachhändler.
|
||||
- **Kunden-Vorteil:**
|
||||
- **Verfügbarkeit:** "Was ist *heute* in meiner Nähe abholbereit?" (Schlägt Google Maps).
|
||||
- **Preis/Leistung:** Markenware oft günstiger als im Großmarkt.
|
||||
- **Händler-Vorteil:** Abverkauf von Ausstellungsware (Liquidität), Frequenz im Laden.
|
||||
|
||||
### Säule B: "Smart Club" (Phase 2 Focus)
|
||||
|
||||
- **Narrativ:** "Insider Access".
|
||||
- **Angebot:** Frei konfigurierbare Neuware (Bestellung).
|
||||
- **Kunden-Vorteil:** Zugriff auf "Closed Shop"-Konditionen (Objekt-Preise), die öffentlich nicht verfügbar sind.
|
||||
- **Strategie:** Hersteller können hier Rabatte geben, ohne ihre öffentlichen Marktpreise zu zerstören.
|
||||
|
||||
---
|
||||
|
||||
## 4. Monetarisierung & Tracking (Das Cashback-Clearing)
|
||||
|
||||
Das System löst das Problem der fehlenden Transparenz bei Offline-Käufen durch Inzentivierung des Kunden.
|
||||
|
||||
**Der Prozess:**
|
||||
|
||||
1. **Ticket:** Kunde zieht im Portal einen QR-Code für Händler X.
|
||||
2. **Kauf:** Kunde kauft vor Ort, verhandelt Preise individuell.
|
||||
3. **Upload (Der Trigger):** Kunde lädt Kaufbeleg im B2in-Portal hoch, um sein **Cashback** anzufordern.
|
||||
4. **Clearing:** Händler bestätigt Umsatz im Backend -> Händler zahlt Gesamt-Provision an B2in.
|
||||
5. **Ausschüttung:** Sobald Geld eingeht, verteilt das System automatisch:
|
||||
- Provision an **Makler** (Lead-Vergütung).
|
||||
- Cashback an **Kunden** (Motivation & Datentreue).
|
||||
- Marge an **B2in**.
|
||||
|
||||
---
|
||||
|
||||
## 5. Quality Management & Content (Das Setup)
|
||||
|
||||
Um einen professionellen Look zu garantieren, wird das Onboarding als Marketing-Dienstleistung verkauft.
|
||||
|
||||
**Das "Launch-Paket" (ca. 399 € einmalig):**
|
||||
|
||||
- **Leistung:** Professioneller Fotograf kommt zum Händler (Laden, Team, Top-10 Produkte).
|
||||
- **Service:** B2in pflegt die Daten initial ein.
|
||||
- **Marketing-Synergie:** Die entstandenen Bilder werden für eine **Social-Media-Kampagne** auf den B2in-Kanälen genutzt.
|
||||
- **Effekt:** Händler zahlt nicht für "Verwaltung", sondern für "Content & Reichweite".
|
||||
|
||||
---
|
||||
|
||||
## 6. Rollen & Synergien im Ökosystem
|
||||
|
||||
| **Rolle** | **Aufgabe** | **Motivation ("What's in it for me?")** |
|
||||
| --- | --- | --- |
|
||||
| **Makler** | Verteilt Zugang (Voucher) beim Hauskauf. | 1. Exklusives Closing-Geschenk für Kunden.
|
||||
2. Passive Provision an Möbelumsätzen. |
|
||||
| **Händler** | Präsentiert Ware (Local Express), bietet Service. | 1. Qualifizierte Leads (Hausbesitzer).
|
||||
2. Marketing-Content (Fotos) & Reichweite. |
|
||||
| **Kunde** | Nutzt Portal via Style2Own/StilEigentum. | 1. Geldwerter Vorteil (Cashback).
|
||||
2. Transparenz über lokale Bestände. |
|
||||
| **Hersteller** | (Später) Liefert Dropshipping-Ware. | 1. Zugang zu kaufkräftiger Zielgruppe ohne Streuverlust.
|
||||
2. Preisschutz durch Closed Shop. |
|
||||
|
||||
---
|
||||
|
||||
## 7. System-Architektur (Technik)
|
||||
|
||||
Das Backend (**B2in Core**) steuert zentral alle Frontends:
|
||||
|
||||
1. **Mandantenfähigkeit/Hubs:** Regionale Zuordnung (z.B. User aus Bielefeld sieht nur Hub OWL).
|
||||
2. **Rollen-System:** Unterschiedliche Dashboards für Makler (Provisions-Übersicht), Händler (Produkt-Upload & Clearing), Kunden (Cashback & Shopping).
|
||||
3. **Ticket-Engine:** Generierung einzigartiger QR-Codes pro Händler-Besuch.
|
||||
4. **Frontend-Weiche:** Das System erkennt, ob der User über `style2own` oder `stileigentum` kommt, und passt das Branding im Header leicht an, während der Inhalt (Local for Local Produkte) identisch bleibt.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Zu dieser Logik gibt es folgende E-Mail an den Kunden
|
||||
|
||||
Hallo Marcel,
|
||||
|
||||
ich habe mir Gedanken gemacht, wie wir die Außendarstellung am CABINET Store und die B2in konzeptionell sauber aufsetzen – ohne die Marken zu verwässern, aber so, dass am Ende auch der Endkunde versteht, wofür B2in steht.
|
||||
|
||||
---
|
||||
|
||||
DAS GRUNDPRINZIP: KEINE VERMISCHUNG, KLARE POSITIONIERUNG
|
||||
|
||||
CABINET bleibt CABINET – das Fachgeschäft für maßgefertigte Einbaulösungen. Daran ändert sich nichts. Was dazukommt, ist B2in als eigenständige Marke, die dich als Person und Experten positioniert.
|
||||
|
||||
Der Ansatz, den ich empfehle: "B2in by Marcel Scheibe" – also B2in als professionelle Marke, die skalierbar ist und international funktioniert, aber mit dir als sichtbarem Kopf dahinter.
|
||||
|
||||
Warum genau dieser Ansatz?
|
||||
|
||||
B2in allein ist ein Markenname, der Seriosität und Professionalität ausstrahlt. Aber gerade im Immobiliengeschäft kauft niemand von einem anonymen Portal – man kauft von jemandem, dem man vertraut. Die Marke gibt die professionelle Hülle, du als Person gibst das Vertrauen.
|
||||
|
||||
Das funktioniert wie ein Architekturbüro: Der Name des Gründers steht dahinter, aber die Marke funktioniert eigenständig. Und wenn du irgendwann skalierst, Mitarbeiter einstellst oder weitere Partner mit reinnimmst, ist die Marke B2in nicht an deinen Namen gekettet – du bleibst aber als Gründer und Gesicht sichtbar.
|
||||
|
||||
Auf der Website bedeutet das: B2in steht groß mit dem Claim im Hero, und direkt darunter oder daneben bist du mit Bild sichtbar – nicht versteckt auf einer "Über uns"-Seite, sondern als Teil der Markenaussage. Der Besucher soll sofort verstehen: Hinter B2in steht eine echte Person mit Expertise.
|
||||
|
||||
---
|
||||
|
||||
CABINET STORE: BESCHILDERUNG & DISPLAYS
|
||||
|
||||
1. Außenbeschilderung
|
||||
Ergänzend zum CABINET-Schild kommt ein zweites Schild: "B2in" mit einer Unterzeile wie "Ihr Partner für Immobilien & Wohnen". Das Schild soll neugierig machen, ohne alles zu erklären. Wer mehr wissen will, fragt oder googelt – und findet die B2in-Welt.
|
||||
|
||||
2. Displays Schaufenster (2 Screens)
|
||||
Hier empfehle ich einen der beiden Screens für B2in zu nutzen. Laufkundschaft hat maximal 3–5 Sekunden Aufmerksamkeit. Deshalb arbeiten wir mit einem festen Frame-System:
|
||||
|
||||
- Oben: B2in-Logo + Claim "Connecting Design & Property" (steht permanent)
|
||||
- Mitte: Dein 16:9-Videomaterial läuft hier im Querformat, mit Gradient-Übergängen oben und unten
|
||||
- Darunter: Kurzes Textfeld, das zum jeweiligen Video rotiert
|
||||
- Unten: "Marcel Scheibe" + B2in.de + QR-Code (steht permanent)
|
||||
|
||||
Die Rotation: ca. 70% Immobilien-Content, 30% Möbel/Einrichtung. Textbeispiele:
|
||||
- Immobilien: "Internationale Immobilien – Ihr Einstieg." / "Dubai. Lissabon. Und morgen?" / "Ihr Zuhause. Weltweit."
|
||||
- Möbel: "Exklusive Einrichtung – Lokal. Für Sie." / "Lokale Händler. Echte Stücke."
|
||||
|
||||
3. Displays innen (2 Screens)
|
||||
Rotationsmodus: ca. 80% CABINET-Content, ca. 20% B2in-Welt (gleicher Frame wie Schaufenster).
|
||||
|
||||
4. Lead-Qualifizierung
|
||||
Die spannendste Zielgruppe sind deine CABINET-Kunden. Nach erfolgreicher Beratung kannst du beiläufig auf internationale Immobilien hinweisen – z.B. mit einer Einladung zu einem "Investor Evening".
|
||||
|
||||
5. Event-Konzept: "Investor Evenings"
|
||||
Quartalsweise kleine, exklusive Veranstaltungen im Store. CABINET-Ambiente nutzen, ausgewählter Kreis, du präsentierst internationale Projekte persönlich.
|
||||
|
||||
---
|
||||
|
||||
B2in WEBSITE: HERO-ANPASSUNG
|
||||
|
||||
Der aktuelle Hero-Text beschreibt nur die Möbel-/(Local-for-Local)-Seite:
|
||||
"Brücken zwischen lokalen Kunden, lokalem Handel und inspirierenden Designs. Wo lokaler Handel und namhafte Lieferanten digital, aber lokal, in einem Netzwerk zusammenarbeiten."
|
||||
|
||||
Das passt nicht mehr, weil Immobilien jetzt die dominante Seite von B2in sind. Der Claim "B2in – Connecting Design and Property" funktioniert weiterhin sehr gut.
|
||||
|
||||
Mein Vorschlag für den neuen Hero:
|
||||
|
||||
B2in – Connecting Design and Property
|
||||
Ihr Partner für exklusive Einrichtungskonzepte und internationale Immobilien.
|
||||
|
||||
Darunter: Dein Name und Bild als Teil der Markenaussage.
|
||||
|
||||
Die Immobilien-Seite wird der dominante Bereich der Website. Der Möbelmarktplatz läuft als ergänzender Bereich mit.
|
||||
|
||||
---
|
||||
|
||||
WORAUF WIR ACHTEN MÜSSEN (WICHTIG)
|
||||
|
||||
Damit die Außendarstellung funktioniert, brauchen wir Disziplin in der Kommunikation. Eine Marke wird nicht stärker, indem man mehr draufpackt – sondern indem man konsequent weniger, aber das Richtige zeigt. Ein Passant am Schaufenster, ein Website-Besucher, ein potenzieller Investor hat immer nur wenige Sekunden. In diesen Sekunden muss eine einzige Botschaft ankommen – nicht drei.
|
||||
|
||||
Konkret heißt das:
|
||||
|
||||
JA, SO MACHEN WIR ES:
|
||||
- Ein Display, eine Botschaft. Immobilien ODER Möbel, nie beides gleichzeitig auf einem Screen.
|
||||
- Nur B2in nach außen kommunizieren. Keine Partnerlogos (kein Azizi, kein Herstellername) auf Displays, Schildern oder im Hero. Nur max im Video, wenn es sich nicht ausblenden lässt)
|
||||
- Texte kurz halten. Maximal ein Satz als Headline, ein Satz als Subline. Wer mehr wissen will, scannt den QR-Code.
|
||||
- Marcel Scheibe als Person sichtbar, aber nicht als Werbefigur. Dein Name steht dezent im Footer, dein Bild auf der Website – nicht auf jedem Display in Großformat.
|
||||
- Konsistenz: Der Frame, das Gerüst (Logo, Claim, Footer) bleibt vom Aufbau auf allen Displays identisch. Nur der Content in der Mitte wechselt.
|
||||
|
||||
DAS VERMEIDEN WIR:
|
||||
- Zu viele Informationen auf einem Screen. Keine Grundrisse, keine Preislisten, keine Aufzählung aller Services auf einem Display.
|
||||
- Marken mischen. Kein "CABINET + B2in + Azizi + Style2Own" auf einem Schild oder Screen. Das verwirrt und wirkt unseriös.
|
||||
- Alles erklären wollen. Die Displays sollen Neugier wecken, nicht informieren. Die Information kommt auf der Website oder im persönlichen Gespräch.
|
||||
- Zu viele verschiedene Botschaften gleichzeitig. Nicht auf dem einen Display Dubai, auf dem nächsten Local-for-Local, auf dem dritten CABINET-Referenzen und auf dem vierten B2A. Das versteht keiner.
|
||||
- Spontan Content ändern ohne Konzept. Jede Änderung an den Displays sollte sich an diesem Framework orientieren.
|
||||
|
||||
Der rote Faden: Jeder Touchpoint (Display, Schild, Website) muss in 3 Sekunden eine einzige klare Aussage vermitteln. Wenn jemand nach dem Vorbeigehen gefragt wird "Was war das?", soll die Antwort sein: "B2in – irgendwas mit internationalen Immobilien, sah hochwertig aus." Nicht: "Keine Ahnung, da war ganz viel drauf."
|
||||
|
||||
---
|
||||
|
||||
ZUSAMMENGEFASST:
|
||||
- CABINET Store = CABINET. Sauber getrennt.
|
||||
- B2in = Deine Marke. Am Store als zweites Schild + ein Schaufenster-Display, auf der Website als Hauptmarke mit dir als Gesicht.
|
||||
- Ein Frame, zwei Content-Varianten (Immobilien dominant, Möbel ergänzend).
|
||||
- Weniger ist mehr: Eine Botschaft pro Touchpoint, keine Marken-Vermischung, Neugier statt Erklärung.
|
||||
|
||||
Lass mich wissen, wie du das siehst – dann gehen wir in die Detail-Umsetzung.
|
||||
|
||||
|
||||
|
||||
######
|
||||
## Folgendes muss bitte noch integriert werden Anforderungen von Kunden
|
||||
|
||||
Supply-Chain-Management für Immobilienentwickler
|
||||
|
||||
Wir übernehmen die operative und strategische Steuerung Ihrer Beschaffung in Deutschland.
|
||||
|
||||
Für Immobilienentwickler, die Möbel oder Innenausstattung aus Deutschland beziehen möchten, fungieren wir als verlängerter Arm vor Ort – mit klarem Fokus auf Vertragssicherheit, Termintreue und Durchsetzungskraft.
|
||||
|
||||
Unsere Leistungen im Überblick
|
||||
|
||||
1. Vertragsmanagement
|
||||
• Unterstützung bei der Ausarbeitung und Strukturierung von Lieferverträgen
|
||||
• Definition klarer Leistungs- und Qualitätsparameter
|
||||
• Absicherung von Zahlungs- und Lieferbedingungen
|
||||
|
||||
2. Vertragssicherung & Durchsetzung
|
||||
• Aktive Überwachung der vereinbarten Meilensteine
|
||||
• Eskalation auf Managementebene bei Abweichungen
|
||||
• Konsequente Nachverfolgung offener Punkte
|
||||
|
||||
3. Tracking & Qualitätskontrolle
|
||||
• Laufende Produktions- und Lieferüberwachung
|
||||
• Persönliche Kontrolle bei Bedarf
|
||||
• Sicherstellung termingerechter Auslieferung
|
||||
|
||||
Unser Ansatz
|
||||
|
||||
Wir kombinieren Marktkenntnis, Netzwerk und operative Erfahrung.
|
||||
Durch unsere direkte Anbindung an Hersteller und Entscheider sorgen wir dafür, dass Vereinbarungen nicht nur auf dem Papier bestehen, sondern tatsächlich umgesetzt werden.
|
||||
|
||||
Unser Anspruch:
|
||||
Transparenz, Verlässlichkeit und planbare Lieferung – ohne operative Reibungsverluste für den Entwickler.
|
||||
168
dev/27-02-2026/b2in-umsetzung-changelog.md
Normal file
168
dev/27-02-2026/b2in-umsetzung-changelog.md
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
# B2in Website – Umsetzung Changelog
|
||||
|
||||
**Datum:** 27.02.2026
|
||||
**Basis:** `b2in-website-umsetzungsplan.md`
|
||||
**Status:** Abgeschlossen – alle Tests bestanden (336/336)
|
||||
|
||||
---
|
||||
|
||||
## Backups
|
||||
|
||||
Alle Original-Dateien vor der Änderung liegen in:
|
||||
`dev/27-02-2026/backup-before-relaunch/`
|
||||
|
||||
| Datei | Original-Pfad |
|
||||
|-------|---------------|
|
||||
| `content.php` | `config/content.php` |
|
||||
| `home.blade.php` | `resources/views/web/home.blade.php` |
|
||||
| `b2in.blade.php` | `resources/views/web/b2in.blade.php` |
|
||||
| `partner.blade.php` | `resources/views/web/partner.blade.php` |
|
||||
| `ecosystem.blade.php` | `resources/views/web/ecosystem.blade.php` |
|
||||
| `about.blade.php` | `resources/views/web/about.blade.php` |
|
||||
| `contact.blade.php` | `resources/views/web/contact.blade.php` |
|
||||
| `magazin.blade.php` | `resources/views/web/magazin.blade.php` |
|
||||
| `footer.blade.php` | `resources/views/livewire/web/components/ui/footer.blade.php` |
|
||||
| `hero.blade.php` | `resources/views/livewire/web/components/sections/hero.blade.php` |
|
||||
| `web.php` | `routes/web.php` |
|
||||
|
||||
---
|
||||
|
||||
## Durchgeführte Änderungen
|
||||
|
||||
### Phase 1 – Homepage-Neuausrichtung
|
||||
|
||||
#### 1.1 Hero-Sektion (`config/content.php` → `themes.b2in.hero`)
|
||||
- **Title:** Von "Brücken zwischen lokalen Kunden..." → "B2in – Connecting Design and Property"
|
||||
- **Subtitle:** Von Möbel-Fokus → "Ihr Partner für exklusive Einrichtungskonzepte und internationale Immobilien."
|
||||
- **CTAs:** "Für lokale Händler / Für Hersteller" → "Internationale Immobilien / Einrichtungskonzepte"
|
||||
- **Stats:** → "Internationale Immobilien", "Exklusive Einrichtung", "Persönliche Beratung"
|
||||
|
||||
#### 1.2 Vision-Sektion (`config/content.php` → `themes.b2in.vision_section`)
|
||||
- **Title:** "Gebaut auf Vertrauen" → "Gebaut auf Expertise und Vertrauen"
|
||||
- **Text:** Komplett neu – jetzt Dual-Fokus (Immobilien + Einrichtung), persönliche Ansprache durch Marcel Scheibe, Nennung konkreter Märkte (Dubai, Lissabon)
|
||||
|
||||
#### 1.3 Ecosystem-Core (`config/content.php` → `themes.b2in.ecosystem_core`)
|
||||
- **Title:** "Ein Ökosystem, drei Stärken" → "Ein Ökosystem, drei Säulen"
|
||||
- **Säule 1:** Kuratierter Marktplatz → **Internationale Immobilien** (Icon: globe-alt)
|
||||
- **Säule 2:** Logistik & Service → **Exklusive Einrichtungskonzepte** (Icon: cube-transparent)
|
||||
- **Säule 3:** Verkaufsnetzwerk → **Supply-Chain-Management** (Icon: clipboard-document-check)
|
||||
|
||||
#### 1.4 CTA-Sektion (`config/content.php` → `themes.b2in.cta_section`)
|
||||
- **Title:** "Werden Sie Partner im führenden Möbel-Netzwerk" → "Ihr nächster Schritt – ob Investment oder Einrichtung"
|
||||
- **Subtitle:** Erweitert um Immobilien, Supply Chain und Einrichtungs-Netzwerk
|
||||
|
||||
### Phase 2 – Content-Erweiterung
|
||||
|
||||
#### 2.1 Brand-Worlds (`config/content.php` → `themes.b2in.brand_worlds`)
|
||||
- **Title:** "Unsere Markenwelten: Qualifizierte Kunden für Ihr Möbel-Sortiment" → "Unsere Welten: Design, Immobilien und internationaler Handel"
|
||||
- **Subtitle:** Komplett neu – Immobilien + Einrichtung + Handel
|
||||
- **Reihenfolge:** Stileigentum → Style2Own → B2A (vorher B2A zuerst)
|
||||
- **Beschreibungen:** Angepasst an neue Positionierung
|
||||
|
||||
#### 2.2 Content-Sektion (`config/content.php` → `themes.b2in.integriertes_modell_b2in`)
|
||||
- **Title:** "Das Beste aus zwei Welten: in jeder Region" → "...Immobilien und Einrichtung"
|
||||
- **Text:** Neu – verbindet Immobilienkauf mit Einrichtungsservice
|
||||
|
||||
#### 2.3 Footer (`resources/views/livewire/web/components/ui/footer.blade.php`)
|
||||
- **Neu:** "Marcel Scheibe – Gründer & CEO" dezent unter dem Claim ergänzt
|
||||
|
||||
#### 2.4 Contact (`config/content.php` → `themes.b2in.contact_form`)
|
||||
- **Neue Betreff-Optionen:** "Internationale Immobilien" und "Supply-Chain-Management" hinzugefügt
|
||||
|
||||
### Phase 3 – Partner, Ecosystem, About, Magazin
|
||||
|
||||
#### 3.1 Partner-Seite
|
||||
|
||||
**Partner-Hero** (`themes.b2in.partner_hero`):
|
||||
- Title: "Das Ökosystem für Ihren Erfolg" → "Das Netzwerk für Immobilien & Einrichtung"
|
||||
- Neuer Partner-Typ: "Immobilienentwickler" (Supply-Chain-Management aus Deutschland)
|
||||
|
||||
**Partner-Card-Section** (`themes.b2in.partner_card_section`):
|
||||
- Neue Karte: "Für Immobilienentwickler" an erster Position
|
||||
- Subtitle angepasst
|
||||
|
||||
**Neue Config-Sektion:** `partner_benefits_developer` (komplett neu)
|
||||
- 4 Features: Vertragsmanagement, Vertragssicherung, Tracking & QK, Netzwerk & Marktkenntnis
|
||||
- Highlight: "100% – Transparenz, Verlässlichkeit und planbare Lieferung"
|
||||
|
||||
**Partner-Blade** (`resources/views/web/partner.blade.php`):
|
||||
- Neue Benefits-Section `partner_benefits_developer` eingefügt
|
||||
- Layout der 4 Sektionen mit abwechselndem Left/Right-Layout und Hintergründen
|
||||
|
||||
**Partner-CTA** (`themes.b2in.partner_cta`):
|
||||
- Subtitle angepasst an Immobilienentwickler + Einrichtung
|
||||
|
||||
#### 3.2 Ecosystem-Seite
|
||||
|
||||
**Ecosystem-Hero** (`themes.b2in.ecosystem_hero`):
|
||||
- Subtitle: Erweitert um Immobilien und Entwickler
|
||||
- Features: Endkunden → Immobilien, Makler → Einrichtung, Lieferanten → Supply Chain, Technologie bleibt
|
||||
|
||||
**Ecosystem-Stats** (`themes.b2in.ecosystem_stats`):
|
||||
- "Möbel-Projekte" → "Realisierte Projekte" (Immobilien + Einrichtung)
|
||||
- Beschreibungen angepasst
|
||||
|
||||
**Ecosystem-Start** (`themes.b2in.ecosystem_start`):
|
||||
- "Kundenwunsch" → "Moment of Need" (Immobilienkauf als Trigger)
|
||||
- Text: Makler als Einstiegspunkt, Marken als Rahmen
|
||||
|
||||
**Ecosystem-Hub** (`themes.b2in.ecosystem_hub`):
|
||||
- Erweitert um Supply-Chain-Perspektive für Entwickler
|
||||
|
||||
**Ecosystem-Result** (`themes.b2in.ecosystem_result`):
|
||||
- Neuer Punkt: Immobilienentwickler als Gewinner des Kreislaufs
|
||||
- Bestehende Punkte angepasst
|
||||
|
||||
#### 3.3 About-Seite
|
||||
|
||||
**About-Hero** (`themes.b2in.about_hero`):
|
||||
- Zitat komplett neu: Dual-Fokus, persönliche "Meine Mission"-Ansprache
|
||||
|
||||
**Our Story** (`themes.b2in.our_story`):
|
||||
- Timeline: 3 → 4 Einträge (+ "Die Erweiterung" 2025/2026)
|
||||
- Summary: Neu – Immobilien + Einrichtung + Marcel Scheibe als Gesicht
|
||||
|
||||
**Our Values** (`themes.b2in.our_values`):
|
||||
- Alle 6 Werte-Beschreibungen angepasst (Immobilien + Lieferketten integriert)
|
||||
|
||||
#### 3.4 Magazin
|
||||
|
||||
**Magazin-List** (`themes.b2in.magazin_list`):
|
||||
- Subtitle: "Business-Konnektivität" → "Immobilien, des Designs und der Business-Konnektivität"
|
||||
|
||||
#### 3.5 FAQ
|
||||
|
||||
**FAQ** (`themes.b2in.faq`):
|
||||
- 5 Fragen komplett überarbeitet
|
||||
- Neue Frage: "Was bedeutet Supply-Chain-Management bei B2in?"
|
||||
- Immobilien-Investment mit Marcel Scheibe als persönlichem Begleiter
|
||||
- Vertrauens-Frage: Marcel Scheibe als Gesicht statt anonyme Plattform
|
||||
|
||||
### Neue Komponente
|
||||
|
||||
**FounderBar** – neue Livewire-Sektion
|
||||
- `app/Livewire/Web/Components/Sections/FounderBar.php`
|
||||
- `resources/views/livewire/web/components/sections/founder-bar.blade.php`
|
||||
- Config: `themes.b2in.founder_bar` (Bild, Name, Titel, Statement)
|
||||
- Eingebunden in `home.blade.php` und `b2in.blade.php` direkt nach dem Hero
|
||||
- Zeigt Marcel Scheibe mit Profilbild und "B2in by Marcel Scheibe" Statement
|
||||
|
||||
---
|
||||
|
||||
## Nicht geändert
|
||||
|
||||
- Keine Blade-Komponenten-Templates geändert (außer Footer + Partner-Layout)
|
||||
- Keine CSS-/Styling-Änderungen
|
||||
- Keine Änderungen an anderen Themes (b2a, stileigentum, style2own)
|
||||
- Keine Route-Änderungen
|
||||
- Keine Datenbank-Änderungen
|
||||
- Admin-Portal unberührt
|
||||
|
||||
---
|
||||
|
||||
## Offene Punkte (aus Umsetzungsplan)
|
||||
|
||||
- [ ] Eigene `/immobilien`-Landingpage (optional – muss mit Kunde geklärt werden)
|
||||
- [ ] Investor Evenings Event-Sektion (optional)
|
||||
- [ ] Hochwertiges Marcel Scheibe Porträtfoto für Founder-Bar (aktuell nutzt es `marcel-scheibe.jpg`)
|
||||
- [ ] B2A-Markenwelt: Bleibt vorerst erhalten – Klärung ob durch Immobilien-Karte ersetzt werden soll
|
||||
333
dev/27-02-2026/b2in-website-umsetzungsplan.md
Normal file
333
dev/27-02-2026/b2in-website-umsetzungsplan.md
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
# B2in Website – Umsetzungsplan: Konzeptionelle Neuausrichtung
|
||||
|
||||
**Datum:** 27.02.2026
|
||||
**Basis:** `b2in-local-for-local.md` (Konzeptpapier v1.1) + Ist-Analyse aller b2in-Seiten
|
||||
**Ziel:** B2in von einer reinen Möbel-/Local-for-Local-Plattform hin zu **"B2in – Connecting Design and Property"** transformieren, wobei **Immobilien die dominante Seite** und der **Möbelmarktplatz der ergänzende Bereich** wird.
|
||||
|
||||
---
|
||||
|
||||
## 0. Zusammenfassung: IST vs. SOLL
|
||||
|
||||
| Aspekt | IST (aktuell) | SOLL (neu) |
|
||||
|--------|---------------|------------|
|
||||
| **Positionierung** | Reine B2B-Möbelplattform, Local-for-Local | Doppelfokus: Internationale Immobilien (dominant) + Einrichtungskonzepte (ergänzend) |
|
||||
| **Hero-Botschaft** | "Brücken zwischen lokalen Kunden, lokalem Handel und inspirierenden Designs" | "Ihr Partner für exklusive Einrichtungskonzepte und internationale Immobilien" |
|
||||
| **Persönliche Marke** | Marcel Scheibe versteckt auf About-Seite | Marcel Scheibe als Gesicht der Marke, prominent im Hero |
|
||||
| **Markenwelten** | B2A, Stileigentum, Style2Own (alle Möbel-fokussiert) | Immobilien-Bereich (dominant) + Möbel-Markenwelten (ergänzend) |
|
||||
| **Partner-Typen** | Händler, Hersteller, Makler (alle Möbel-Kontext) | + Immobilienentwickler, Investoren, Supply-Chain-Kunden |
|
||||
| **Neue Inhalte** | — | Supply-Chain-Management, Investor Evenings, Immobilien-Services |
|
||||
|
||||
---
|
||||
|
||||
## 1. Homepage (`/`) – `web/home.blade.php` + `web/b2in.blade.php`
|
||||
|
||||
### 1.1 Hero-Sektion (KRITISCH – höchste Priorität)
|
||||
|
||||
**Datei:** `config/content.php` → `themes.b2in.hero`
|
||||
|
||||
| Feld | Alt | Neu |
|
||||
|------|-----|-----|
|
||||
| `title` | "B2in – Brücken zwischen lokalen Kunden, lokalem Handel und inspirierenden Designs." | `"<span class='text-secondary'>B2in</span> – Connecting Design and <span class='text-secondary'>Property</span>"` |
|
||||
| `subtitle` | "Wo lokaler Handel und namhafte Lieferanten digital, aber lokal, in einem Netzwerk zusammenarbeiten." | "Ihr Partner für exklusive Einrichtungskonzepte und internationale Immobilien." |
|
||||
| `cta1_text` | "Für lokale Händler" | "Internationale Immobilien" |
|
||||
| `cta1_link` | `/services` | `/immobilien` (neue Seite) oder Anchor `#immobilien` |
|
||||
| `cta2_text` | "Für Hersteller & Marken" | "Einrichtungskonzepte" |
|
||||
| `cta2_link` | `/beratung` | `/partner` oder `#einrichtung` |
|
||||
| `stats` | Exklusive Auswahl, Persönlicher Service, Werte die bleiben | "Internationale Immobilien", "Exklusive Einrichtung", "Persönliche Beratung" |
|
||||
|
||||
**Neue Anforderung:** Marcel Scheibe muss direkt im Hero oder unmittelbar darunter sichtbar sein – mit Bild und Name als Teil der Markenaussage. Optionen:
|
||||
|
||||
- **Option A:** Hero-Sektion um ein Porträtbild + "Marcel Scheibe, Gründer" erweitern (neben oder unter dem Subtitle)
|
||||
- **Option B:** Neue schmale Sektion direkt nach dem Hero: "B2in by Marcel Scheibe" – Bild links, Kurztext rechts
|
||||
- **Empfehlung:** Option B, da der Hero clean bleibt und trotzdem die persönliche Marke sofort sichtbar ist
|
||||
|
||||
### 1.2 Vision-Sektion
|
||||
|
||||
**Datei:** `config/content.php` → `themes.b2in.vision_section`
|
||||
|
||||
Die aktuelle Vision-Sektion fokussiert auf Möbelhandel. **Neuer Vorschlag:**
|
||||
|
||||
```
|
||||
title: "Gebaut auf Expertise und Vertrauen"
|
||||
paragraphs:
|
||||
- "B2in (Bridges2international) verbindet zwei Welten: internationale Immobilien
|
||||
und exklusive Einrichtungskonzepte. Als Ihr persönlicher Partner navigiere ich
|
||||
Sie durch beide Bereiche – mit Expertise, Netzwerk und dem Anspruch, dass jede
|
||||
Entscheidung auf Vertrauen basiert."
|
||||
- "Ob ein Investment in Dubai, eine Villa in Lissabon oder die maßgeschneiderte
|
||||
Einrichtung Ihres neuen Zuhauses – bei B2in laufen alle Fäden zusammen."
|
||||
- "Regional verwurzelt, international vernetzt – das ist B2in."
|
||||
image_caption: "Marcel Scheibe, Gründer & CEO"
|
||||
```
|
||||
|
||||
→ Die Sektion behält Marcel Scheibe als Bild, aber der Text wird vom reinen Möbelfokus auf den Dual-Fokus umgestellt.
|
||||
|
||||
### 1.3 Ecosystem-Core-Sektion
|
||||
|
||||
**Datei:** `config/content.php` → `themes.b2in.ecosystem_core`
|
||||
|
||||
Aktuell 3 Säulen (alle Möbel). **Neuer Vorschlag – 3 Säulen mit neuem Fokus:**
|
||||
|
||||
| Säule | Alt | Neu |
|
||||
|-------|-----|-----|
|
||||
| 1 | Kuratierter Marktplatz (Möbel) | **Internationale Immobilien** – "Zugang zu exklusiven Immobilien-Investments weltweit – von Dubai bis Lissabon. Professionelle Begleitung vom ersten Interesse bis zum Closing." |
|
||||
| 2 | Intelligente Logistik & Service | **Exklusive Einrichtungskonzepte** – "Über unser Local-for-Local-Netzwerk verbinden wir Sie mit den besten lokalen Fachexperten und europäischen Design-Marken für Ihr neues Zuhause." |
|
||||
| 3 | Starkes Verkaufsnetzwerk | **Supply-Chain-Management** – "Für Immobilienentwickler: Operative Steuerung der Beschaffung aus Deutschland – Vertragsmanagement, Qualitätskontrolle und termingerechte Lieferung." |
|
||||
|
||||
### 1.4 Brand-Worlds-Sektion
|
||||
|
||||
Aktuell: B2A, Stileigentum, Style2Own (alle Möbel-fokussiert).
|
||||
|
||||
**Neuer Vorschlag:** Titel und Subtitle anpassen + Karten umstrukturieren:
|
||||
|
||||
```
|
||||
title: "Unsere Welten"
|
||||
subtitle: "Von internationalen Immobilien-Investments über exklusive Einrichtungskonzepte
|
||||
bis zum transatlantischen Handel – B2in verbindet die Welten, die zusammengehören."
|
||||
```
|
||||
|
||||
Karten-Reihenfolge und Content anpassen:
|
||||
1. **B2in Immobilien** (NEU) – "Internationale Immobilien-Investments. Ihr Einstieg in Märkte wie Dubai, Lissabon und darüber hinaus."
|
||||
2. **Stileigentum** – Bestehendes beibehalten (Premium-Einrichtung)
|
||||
3. **Style2Own** – Bestehendes beibehalten (Lifestyle-Einrichtung)
|
||||
4. **B2A** – Bestehendes beibehalten (Transatlantischer Handel)
|
||||
|
||||
**Alternative:** B2A rausnehmen und stattdessen den Supply-Chain-Service als eigene Karte zeigen.
|
||||
|
||||
### 1.5 Content-Sektion ("Das Beste aus zwei Welten")
|
||||
|
||||
**Datei:** `config/content.php` → `themes.b2in.integriertes_modell_b2in`
|
||||
|
||||
Text anpassen, um beide Welten (Immobilien + Einrichtung) zu reflektieren statt nur Möbel.
|
||||
|
||||
### 1.6 CTA-Sektion
|
||||
|
||||
**Datei:** `config/content.php` → `themes.b2in.cta_section`
|
||||
|
||||
```
|
||||
title: "Ihr nächster Schritt – ob Investment oder Einrichtung"
|
||||
subtitle: "Ob Sie internationale Immobilien entdecken, Ihre Supply Chain optimieren oder
|
||||
Teil unseres Einrichtungs-Netzwerks werden möchten – sprechen Sie mit uns."
|
||||
button_text: "Kontakt aufnehmen"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Partner-Seite (`/partner`) – `web/partner.blade.php`
|
||||
|
||||
### 2.1 Partner-Hero
|
||||
|
||||
Aktuell fokussiert auf Möbel-Ökosystem. **Anpassung:**
|
||||
|
||||
- Title: "Das Netzwerk für <span>Immobilien & Einrichtung</span>"
|
||||
- Subtitle: Erweitern um Immobilien-Kontext
|
||||
- Partner-Types: **Neuen Typ hinzufügen:** "Immobilienentwickler" mit Icon `globe-alt` und Description "Supply-Chain-Management, Beschaffung aus Deutschland"
|
||||
|
||||
### 2.2 Partner-Card-Section
|
||||
|
||||
**Neuen vierten Karten-Typ hinzufügen:**
|
||||
|
||||
```php
|
||||
[
|
||||
'title' => 'Für Immobilienentwickler',
|
||||
'description' => 'Operative Steuerung Ihrer Beschaffung aus Deutschland –
|
||||
Vertragsmanagement, Qualitätskontrolle und termingerechte Lieferung.',
|
||||
'icon' => 'globe-alt',
|
||||
'button' => '#partner-benefits-developer',
|
||||
'button_text' => 'Ihre Vorteile als Entwickler',
|
||||
]
|
||||
```
|
||||
|
||||
### 2.3 Neue Benefits-Sektion: Immobilienentwickler
|
||||
|
||||
**Neue Config-Sektion:** `partner_benefits_developer`
|
||||
|
||||
```php
|
||||
'partner_benefits_developer' => [
|
||||
'id' => 'partner-benefits-developer',
|
||||
'tag' => 'Ihre Vorteile als Immobilienentwickler',
|
||||
'tag_icon' => 'globe-alt',
|
||||
'tag_title' => 'Ihr verlängerter Arm <span class="text-secondary">in Deutschland.</span>',
|
||||
'features' => [
|
||||
['title' => 'Vertragsmanagement', 'description' => 'Unterstützung bei Ausarbeitung und Strukturierung von Lieferverträgen, Definition klarer Leistungs- und Qualitätsparameter.', 'icon' => 'document-check'],
|
||||
['title' => 'Vertragssicherung & Durchsetzung', 'description' => 'Aktive Überwachung vereinbarter Meilensteine, Eskalation bei Abweichungen, konsequente Nachverfolgung.', 'icon' => 'shield-check'],
|
||||
['title' => 'Tracking & Qualitätskontrolle', 'description' => 'Laufende Produktions- und Lieferüberwachung, persönliche Kontrolle bei Bedarf, termingerechte Auslieferung.', 'icon' => 'magnifying-glass'],
|
||||
['title' => 'Netzwerk & Marktkenntnis', 'description' => 'Direkte Anbindung an Hersteller und Entscheider. Vereinbarungen werden umgesetzt, nicht nur auf Papier festgehalten.', 'icon' => 'link'],
|
||||
],
|
||||
],
|
||||
```
|
||||
|
||||
**In `web/partner.blade.php` einbinden:**
|
||||
|
||||
```blade
|
||||
<livewire:web.components.sections.benefits-section section="partner_benefits_developer" bg="bg-accent" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Ecosystem-Seite (`/ecosystem`) – `web/ecosystem.blade.php`
|
||||
|
||||
### 3.1 Ecosystem-Hero
|
||||
|
||||
Aktuell: Reiner Möbel-/Netzwerk-Fokus.
|
||||
|
||||
**Anpassen:** Immobilien als Teil des Ökosystems einbinden. Der Ecosystem-Fluss wird:
|
||||
1. **Einstieg:** Kunde kommt über Immobilienkauf (Makler) oder direkt über style2own/stileigentum
|
||||
2. **Hub:** Lokale Händler + europäische Hersteller + internationale Immobilien
|
||||
3. **Ergebnis:** Jeder profitiert (inkl. Immobilienentwickler)
|
||||
|
||||
### 3.2 Ecosystem-Stats
|
||||
|
||||
Zahlen um Immobilien-KPIs ergänzen (z.B. "Internationale Märkte", "Realisierte Projekte").
|
||||
|
||||
### 3.3 Content-Sektionen
|
||||
|
||||
Die 3 Content-Sektionen (ecosystem_start, ecosystem_hub, ecosystem_result) inhaltlich erweitern, um den Immobilien-Fluss mit einzubeziehen.
|
||||
|
||||
---
|
||||
|
||||
## 4. About-Seite (`/about`) – `web/about.blade.php`
|
||||
|
||||
### 4.1 About-Hero
|
||||
|
||||
**Anpassen:**
|
||||
- Zitat aktualisieren: Von reinem Möbelfokus auf "Design & Property"
|
||||
- Marcel Scheibe stärker als Gesicht beider Welten positionieren
|
||||
|
||||
### 4.2 Our Story
|
||||
|
||||
**Timeline erweitern:**
|
||||
- Aktuell 3 Schritte (Idee → Mission → Zukunft), alle Möbel-fokussiert
|
||||
- **Immobilien-Pivot** als neuen Timeline-Punkt einfügen oder in "Die Zukunft" integrieren:
|
||||
"2025/2026 erweitert B2in sein Ökosystem um internationale Immobilien und Supply-Chain-Services für Entwickler."
|
||||
|
||||
### 4.3 Our Values
|
||||
|
||||
Werte sind aktuell Möbel-fokussiert (Innovation, Konnektivität, Qualität, Vertrauen, Nachhaltigkeit, Design-Exzellenz). Diese sind generisch genug, um auch für den Dual-Fokus zu funktionieren. **Minimale Textanpassungen** in den Beschreibungen, um Immobilien mit einzubeziehen.
|
||||
|
||||
---
|
||||
|
||||
## 5. Magazin-Seite (`/magazin`)
|
||||
|
||||
**Minimal-Änderung:** Subtitle kann erweitert werden um "...aus der Welt der Immobilien, des Designs und der Business-Konnektivität."
|
||||
|
||||
---
|
||||
|
||||
## 6. Contact-Seite (`/contact`)
|
||||
|
||||
### 6.1 Betreff-Auswahl erweitern
|
||||
|
||||
```php
|
||||
'subjects' => [
|
||||
'' => 'Wählen Sie einen Betreff',
|
||||
'immobilien' => 'Internationale Immobilien',
|
||||
'supply_chain' => 'Supply-Chain-Management',
|
||||
'general' => 'Allgemeine Anfrage',
|
||||
'partnership' => 'Partnerschaft',
|
||||
'press' => 'Presse',
|
||||
'career' => 'Karriere',
|
||||
],
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Footer
|
||||
|
||||
### 7.1 Marcel Scheibe sichtbar machen
|
||||
|
||||
Gemäß Konzept: "Dein Name steht dezent im Footer". Vorschlag:
|
||||
- Unter dem Logo/Claim: "Marcel Scheibe – Gründer & CEO"
|
||||
- Klein, dezent, aber sichtbar
|
||||
|
||||
---
|
||||
|
||||
## 8. Neue Seite: Immobilien / Services (OPTIONAL)
|
||||
|
||||
Wenn eine eigene Immobilien-Landingpage gewünscht ist:
|
||||
|
||||
### Route
|
||||
```php
|
||||
Route::get('/immobilien', fn() => view('web.immobilien'))->name('immobilien');
|
||||
```
|
||||
|
||||
### Navigation erweitern
|
||||
```php
|
||||
'navigation' => [
|
||||
['label' => 'Home', 'url' => '/'],
|
||||
['label' => 'Immobilien', 'url' => '/immobilien'], // NEU
|
||||
['label' => 'Partner', 'url' => '/partner'],
|
||||
['label' => 'Ecosystem', 'url' => '/ecosystem'],
|
||||
['label' => 'Magazin', 'url' => '/magazin'],
|
||||
['label' => 'About', 'url' => '/about'],
|
||||
['label' => 'Contact', 'url' => '/contact'],
|
||||
]
|
||||
```
|
||||
|
||||
### Mögliche Sektionen
|
||||
1. Hero: Internationale Immobilien-Investments
|
||||
2. Content: Märkte (Dubai, Lissabon, etc.)
|
||||
3. Supply-Chain-Management als Service-Sektion
|
||||
4. Investor Evenings (Event-Konzept)
|
||||
5. CTA: Kontakt / Persönliches Gespräch
|
||||
|
||||
---
|
||||
|
||||
## 9. Umsetzungs-Reihenfolge (Priorisierung)
|
||||
|
||||
### Phase 1 – Sofort (Homepage-Neuausrichtung)
|
||||
1. **Hero-Text** in `config/content.php` anpassen (30 min)
|
||||
2. **Marcel Scheibe im Hero-Bereich** sichtbar machen – neue Mini-Sektion oder Hero-Erweiterung (1-2h)
|
||||
3. **Vision-Sektion** Text aktualisieren (30 min)
|
||||
4. **Ecosystem-Core** Säulen neu texten (30 min)
|
||||
5. **CTA-Sektion** aktualisieren (15 min)
|
||||
|
||||
### Phase 2 – Kurzfristig (Content-Erweiterung)
|
||||
6. **Brand-Worlds** Karten und Texte anpassen (1h)
|
||||
7. **Content-Sektion** "Beste aus zwei Welten" anpassen (30 min)
|
||||
8. **Footer** Marcel Scheibe dezent ergänzen (15 min)
|
||||
9. **Contact** Betreff-Optionen erweitern (15 min)
|
||||
|
||||
### Phase 3 – Mittelfristig (Partner & Ecosystem)
|
||||
10. **Partner-Seite:** Neue Karte "Immobilienentwickler" + Benefits-Sektion (2h)
|
||||
11. **Ecosystem-Seite:** Texte um Immobilien-Dimension erweitern (1-2h)
|
||||
12. **About-Seite:** Story/Timeline + Werte-Texte anpassen (1h)
|
||||
|
||||
### Phase 4 – Optional (Neue Seiten)
|
||||
13. **Immobilien-Landingpage** als eigene Route + View + Content (4-6h)
|
||||
14. **Supply-Chain-Service-Seite** oder eigene Sektion (2-3h)
|
||||
15. **Navigation** erweitern (15 min)
|
||||
|
||||
---
|
||||
|
||||
## 10. Technische Änderungen (Übersicht)
|
||||
|
||||
| Datei | Änderung | Aufwand |
|
||||
|-------|----------|---------|
|
||||
| `config/content.php` | Hero, Vision, Ecosystem-Core, Brand-Worlds, CTA, Contact-Subjects, About, Partner, Magazin | Hoch (zentrale Datei) |
|
||||
| `resources/views/web/home.blade.php` | Ggf. neue Sektion nach Hero für Marcel Scheibe | Gering |
|
||||
| `resources/views/web/b2in.blade.php` | Ggf. neue Sektion nach Hero für Marcel Scheibe | Gering |
|
||||
| `resources/views/web/partner.blade.php` | Neue benefits-section für Immobilienentwickler einfügen | Gering |
|
||||
| `resources/views/livewire/web/components/ui/footer.blade.php` | Marcel Scheibe Name ergänzen | Gering |
|
||||
| `routes/web.php` | Ggf. neue Route `/immobilien` | Gering |
|
||||
| `resources/views/web/immobilien.blade.php` | Neue Seite (optional) | Mittel |
|
||||
|
||||
---
|
||||
|
||||
## 11. Was NICHT geändert wird
|
||||
|
||||
- **Blade-Komponenten-Struktur:** Alle bestehenden Livewire-Sections bleiben erhalten, nur der Content (aus config) ändert sich.
|
||||
- **Styling/CSS:** Kein Redesign, nur Content-Änderungen.
|
||||
- **Andere Themes:** b2a, stileigentum, style2own bleiben unverändert.
|
||||
- **Admin-Portal:** Kein Einfluss auf portal.b2in.test.
|
||||
- **CABINET-Bezug:** CABINET bleibt komplett getrennt, wird auf der b2in-Website nicht erwähnt.
|
||||
|
||||
---
|
||||
|
||||
## 12. Offene Fragen an den Kunden
|
||||
|
||||
1. **Eigene Immobilien-Seite?** Soll `/immobilien` als eigenständige Seite existieren oder reicht die Homepage-Integration?
|
||||
2. **Supply-Chain-Management:** Eigene Seite oder Sektion auf der Partner-Seite?
|
||||
3. **Investor Evenings:** Soll das Event-Konzept auf der Website abgebildet werden (z.B. als Sektion oder eigene Seite)?
|
||||
4. **B2A-Markenwelt:** Bleibt B2A als Brand-World-Karte erhalten oder wird sie durch "Immobilien" ersetzt?
|
||||
5. **Marcel Scheibe Foto:** Ist ein hochwertiges Porträtfoto vorhanden, das im Hero-Bereich verwendet werden kann?
|
||||
6. **Konkreter Immobilien-Content:** Welche Märkte/Projekte sollen initial gezeigt werden (Dubai, Lissabon, etc.)?
|
||||
31
dev/27-02-2026/backup-before-relaunch/about.blade.php
Normal file
31
dev/27-02-2026/backup-before-relaunch/about.blade.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
@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.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
|
||||
34
dev/27-02-2026/backup-before-relaunch/b2in.blade.php
Normal file
34
dev/27-02-2026/backup-before-relaunch/b2in.blade.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
@extends('web.layouts.web-master')
|
||||
|
||||
@section('title', 'B2IN - Connecting Design and Property')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="bg-background">
|
||||
<livewire:web.components.ui.header />
|
||||
|
||||
<main>
|
||||
<livewire:web.components.sections.hero />
|
||||
<livewire:web.components.sections.ecosystem-core />
|
||||
<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.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
|
||||
27
dev/27-02-2026/backup-before-relaunch/contact.blade.php
Normal file
27
dev/27-02-2026/backup-before-relaunch/contact.blade.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
@extends('web.layouts.web-master')
|
||||
|
||||
@section('title', 'Kontakt - B2in')
|
||||
|
||||
@section('content')
|
||||
<div class="bg-background">
|
||||
<livewire:web.components.ui.header />
|
||||
|
||||
<main class="variante-glass-flow">
|
||||
<livewire:web.components.ui.contact-form />
|
||||
</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
|
||||
2316
dev/27-02-2026/backup-before-relaunch/content.php
Normal file
2316
dev/27-02-2026/backup-before-relaunch/content.php
Normal file
File diff suppressed because it is too large
Load diff
36
dev/27-02-2026/backup-before-relaunch/ecosystem.blade.php
Normal file
36
dev/27-02-2026/backup-before-relaunch/ecosystem.blade.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
@extends('web.layouts.web-master')
|
||||
|
||||
@section('title', 'B2in Ecosystem - Intelligentes Netzwerk')
|
||||
|
||||
@section('content')
|
||||
<div class="bg-background">
|
||||
<livewire:web.components.ui.header />
|
||||
|
||||
<main class="variante-glass-flow">
|
||||
<livewire:web.components.sections.partner-hero section="ecosystem_hero" />
|
||||
<livewire:web.components.sections.ecosystem-stats />
|
||||
<livewire:web.components.sections.content-section layout="right" bg="bg-accent"
|
||||
section="ecosystem_start" />
|
||||
<livewire:web.components.sections.content-section layout="left" bg=""
|
||||
section="ecosystem_hub" />
|
||||
<livewire:web.components.sections.content-section layout="right" bg="bg-accent"
|
||||
section="ecosystem_result" />
|
||||
<livewire:web.components.sections.digital-core />
|
||||
<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
|
||||
74
dev/27-02-2026/backup-before-relaunch/footer.blade.php
Normal file
74
dev/27-02-2026/backup-before-relaunch/footer.blade.php
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<footer class="section-dark section-padding">
|
||||
<div class="container-padding">
|
||||
{{-- Main Content --}}
|
||||
<div class="text-center spacing-section">
|
||||
<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" />
|
||||
</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>
|
||||
</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="#" 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>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="spacing-small text-center">
|
||||
<a href="#" 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>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 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>
|
||||
<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">
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
54
dev/27-02-2026/backup-before-relaunch/hero.blade.php
Normal file
54
dev/27-02-2026/backup-before-relaunch/hero.blade.php
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<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-12 lg:gap-16 items-center">
|
||||
{{-- Left Content --}}
|
||||
<div class="space-y-8">
|
||||
<div class="space-y-6 slide-right delay-200">
|
||||
<h1 class="text-hero-alt">
|
||||
{!! $content['title'] !!}
|
||||
</h1>
|
||||
<p class="text-xl text-muted-foreground max-w-md leading-relaxed">
|
||||
{!! $content['subtitle'] !!}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 slide-right delay-300">
|
||||
<a href="{{ $content['cta1_link'] }}" class="btn-primary-accent">
|
||||
{{ $content['cta1_text'] }}
|
||||
</a>
|
||||
<a href="{{ $content['cta2_link'] }}" class="btn-secondary-accent">
|
||||
{{ $content['cta2_text'] }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if(isset($content['stats']))
|
||||
<div class="flex flex-wrap items-center gap-6 pt-10 mt-10 border-t border-border/80 slide-right delay-300">
|
||||
@foreach ($content['stats'] as $stat)
|
||||
<div class="flex items-center gap-2 text-md text-muted-foreground font-light">
|
||||
@svg('heroicon-o-check-circle', 'w-6 h-6 text-secondary')
|
||||
<span>{{ $stat }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
{{-- 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'] }}"
|
||||
class="w-full h-[600px] object-cover" />
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
|
||||
</div>
|
||||
|
||||
{{-- Floating info card --}}
|
||||
<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-400">
|
||||
<div class="text-xl font-medium text-muted-foreground">{{ $content['card_title'] }}</div>
|
||||
<div class="text-lg font-medium font-secondary">{!! $content['card_text'] !!}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
34
dev/27-02-2026/backup-before-relaunch/home.blade.php
Normal file
34
dev/27-02-2026/backup-before-relaunch/home.blade.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
@extends('web.layouts.web-master')
|
||||
|
||||
@section('title', 'B2IN - Connecting Design and Property')
|
||||
|
||||
@section('content')
|
||||
<div class="bg-background">
|
||||
<livewire:web.components.ui.header />
|
||||
|
||||
<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.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
|
||||
29
dev/27-02-2026/backup-before-relaunch/magazin.blade.php
Normal file
29
dev/27-02-2026/backup-before-relaunch/magazin.blade.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
@extends('web.layouts.web-master')
|
||||
|
||||
@section('title', 'B2in Magazin - Insights & Trends')
|
||||
|
||||
@section('content')
|
||||
<div class="bg-background">
|
||||
<livewire:web.components.ui.header />
|
||||
|
||||
<main class="variante-glass-flow">
|
||||
<livewire:web.components.sections.magazin-list />
|
||||
</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
|
||||
35
dev/27-02-2026/backup-before-relaunch/partner.blade.php
Normal file
35
dev/27-02-2026/backup-before-relaunch/partner.blade.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
@extends('web.layouts.web-master')
|
||||
|
||||
@section('title', 'Partner werden - B2in Ecosystem')
|
||||
|
||||
@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.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.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
|
||||
103
dev/27-02-2026/backup-before-relaunch/web.php
Normal file
103
dev/27-02-2026/backup-before-relaunch/web.php
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Livewire\Volt\Volt;
|
||||
|
||||
// Gemeinsame Web-Routes für alle Pages)
|
||||
// Jede Landing-Page hat das gleiche Gerüst, aber unterschiedliches Styling
|
||||
|
||||
// Hauptseite - lädt automatisch das richtige Theme basierend auf der Domain
|
||||
Route::get('/', function () {
|
||||
$theme = config('app.theme', 'b2in');
|
||||
|
||||
// Use theme-specific view if it exists (e.g., web.b2a, web.stileigentum)
|
||||
// The view name for b2in theme is 'web.b2in' and not 'web.home'
|
||||
if ($theme === 'b2in') {
|
||||
return view('web.home');
|
||||
}
|
||||
if (view()->exists('web.'.$theme)) {
|
||||
return view('web.'.$theme);
|
||||
}
|
||||
|
||||
// Fallback to the default home view
|
||||
return view('web.home');
|
||||
})->name('home');
|
||||
|
||||
// Willkommensseite
|
||||
Route::get('/welcome', function () {
|
||||
return view('web.welcome');
|
||||
})->name('welcome');
|
||||
|
||||
// Weitere gemeinsame Webseiten hier...
|
||||
Route::get('/about', function () {
|
||||
return view('web.about');
|
||||
})->name('about');
|
||||
Route::get('/ecosystem', function () {
|
||||
return view('web.ecosystem');
|
||||
})->name('ecosystem');
|
||||
Route::get('/partner', function () {
|
||||
return view('web.partner');
|
||||
})->name('partner');
|
||||
Route::get('/magazin', function () {
|
||||
return view('web.magazin');
|
||||
})->name('magazin');
|
||||
Route::get('/magazin/{id}', function ($id) {
|
||||
return view('web.magazin-detail', compact('id'));
|
||||
})->name('magazin.detail');
|
||||
Route::get('/contact', function () {
|
||||
return view('web.contact');
|
||||
})->name('contact');
|
||||
|
||||
Route::get('/service', function () {
|
||||
return view('web.service');
|
||||
})->name('service');
|
||||
|
||||
Route::get('/portfolio', function () {
|
||||
return view('web.portfolio');
|
||||
})->name('portfolio');
|
||||
|
||||
Route::get('/faq', function () {
|
||||
return view('web.faq');
|
||||
})->name('faq');
|
||||
|
||||
// Theme Demo Route
|
||||
Route::get('/theme-demo', function () {
|
||||
return view('web.theme-demo');
|
||||
})->name('theme-demo');
|
||||
|
||||
// Pfad-basierte Theme-Routen für lokale Entwicklung wurden entfernt
|
||||
// Die Themensauswahl wird nun über den ThemeServiceProvider gesteuert (Domain oder ?theme=... GET-Parameter)
|
||||
|
||||
Route::get('/partner/invitation/expired/{token}', function (string $token) {
|
||||
$invitation = \App\Models\PartnerInvitation::with('role')->where('token', $token)->firstOrFail();
|
||||
|
||||
return view('partner.invitation-expired', compact('invitation'));
|
||||
})->name('partner.invitation.expired');
|
||||
|
||||
Route::get('/partner/invitation/used/{token}', function (string $token) {
|
||||
$invitation = \App\Models\PartnerInvitation::with('role')->where('token', $token)->firstOrFail();
|
||||
|
||||
return view('partner.invitation-used', compact('invitation'));
|
||||
})->name('partner.invitation.used');
|
||||
|
||||
Volt::route('/partner/invitation/{token}', 'partner.invitation-accept')
|
||||
->name('partner.invitation.accept');
|
||||
|
||||
Volt::route('/partner/create-account', 'partner.create-account')
|
||||
->name('partner.create.account');
|
||||
|
||||
// Öffentliche Registrierung per QR-/Code (Landing, code-check)
|
||||
Volt::route('/reg/{role}', 'reg.landing')
|
||||
->name('registration.landing');
|
||||
|
||||
Volt::route('/registration/thank-you', 'reg.thank-you')
|
||||
->name('registration.thank-you');
|
||||
|
||||
// Partner Setup Wizard & Daten
|
||||
Route::middleware('auth')->group(function () {
|
||||
Volt::route('/partner/setup', 'partner.setup-wizard')
|
||||
->name('partner.setup.wizard');
|
||||
});
|
||||
|
||||
// Authentifizierungs-Routen werden in domains.php eingebunden
|
||||
// require __DIR__ . '/auth.php';
|
||||
196
dev/27-02-2026/review-architektur-02-03-2026.md
Normal file
196
dev/27-02-2026/review-architektur-02-03-2026.md
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
# Review & Finaler Umsetzungsplan: B2in Website-Architektur (02.03.2026)
|
||||
|
||||
**Reviewer:** Claude Code
|
||||
**Datum:** 02.03.2026
|
||||
**Status:** Finale Entscheidungen getroffen
|
||||
|
||||
**Basis-Dokumente:**
|
||||
- `B2in Website-Architektur.md` (Neues Konzept vom 02.03.2026)
|
||||
- `b2in-website-umsetzungsplan.md` (Originaler Umsetzungsplan vom 27.02.2026)
|
||||
- `b2in-umsetzung-changelog.md` (Was bereits umgesetzt wurde)
|
||||
- `b2in-local-for-local.md` (Konzeptpapier v1.1)
|
||||
|
||||
---
|
||||
|
||||
## 1. Was bereits umgesetzt ist (Stand 27.02.2026)
|
||||
|
||||
Die erste Umsetzungsphase hat die b2in-Website erfolgreich vom reinen Möbel-Fokus auf den Dual-Fokus (Immobilien + Einrichtung) umgestellt:
|
||||
|
||||
| Bereich | Status | Details |
|
||||
|---------|--------|---------|
|
||||
| Hero-Text | Umgesetzt | "Connecting Design and Property" + neue CTAs |
|
||||
| FounderBar-Komponente | Umgesetzt | Marcel Scheibe als Vertrauensanker nach Hero |
|
||||
| Vision-Sektion | Umgesetzt | Dual-Fokus Text (Immobilien + Einrichtung) |
|
||||
| Ecosystem-Core (3 Säulen) | Umgesetzt | Immobilien / Einrichtung / Supply-Chain |
|
||||
| Brand Worlds | Umgesetzt | Reihenfolge angepasst, Texte aktualisiert |
|
||||
| CTA-Sektion | Umgesetzt | "Investment oder Einrichtung" |
|
||||
| Partner-Seite | Umgesetzt | Neue Developer-Benefits-Sektion (Supply-Chain) |
|
||||
| Footer | Umgesetzt | "Marcel Scheibe – Gründer & CEO" |
|
||||
| Contact-Formular | Umgesetzt | Neue Betreffs: Immobilien, Supply-Chain |
|
||||
| About-Seite | Umgesetzt | Timeline erweitert, Werte angepasst |
|
||||
| Ecosystem-Seite | Umgesetzt | Immobilien-Dimension integriert |
|
||||
| FAQ | Umgesetzt | 5 Fragen komplett überarbeitet |
|
||||
|
||||
**Backups aller Original-Dateien:** `dev/27-02-2026/backup-before-relaunch/`
|
||||
|
||||
---
|
||||
|
||||
## 2. Finale Entscheidungen (02.03.2026)
|
||||
|
||||
Nach Review des Architektur-Konzepts und Abstimmung wurden folgende Entscheidungen getroffen:
|
||||
|
||||
### 2.1 Homepage: Weiche mit visuellem Gewicht
|
||||
|
||||
Die Homepage wird zur "Triage"-Seite, behält aber visuelles Gewicht durch 3 reduzierte Ecosystem-Kacheln.
|
||||
|
||||
**Seitenstruktur (von oben nach unten):**
|
||||
|
||||
1. **Hero-Bereich** — Neuer Text + 2 gleichwertige CTA-Buttons
|
||||
- Headline: "B2in – Wo exklusive Immobilien-Investments auf smartes Interior treffen."
|
||||
- Subline: "Ihr Partner für renditestarke internationale Immobilien und globales Supply-Chain-Management im Interior-Bereich."
|
||||
- Button 1 (Gold/Premium): "Zu den Immobilien-Projekten" → `/immobilien`
|
||||
- Button 2 (Corporate/Clean): "Für Entwickler & Partner" → `/partner`
|
||||
|
||||
2. **FounderBar** — Bleibt unverändert (Vertrauensanker)
|
||||
|
||||
3. **Synergie-Sektion (NEU)** — Kurzer, starker Block
|
||||
- Text-Idee: "Das B2in-Ökosystem: Wir verbinden den Immobilienkauf mit der perfekten Einrichtung..."
|
||||
|
||||
4. **3 Ecosystem-Kacheln (VEREINFACHT)** — Nur Icon + Titel + 1 Satz
|
||||
- Kachel 1 (Icon: Gebäude/Schlüssel): **Immobilien & Investments** — "Exklusive Off-Market-Projekte & High-Yield Renditeobjekte."
|
||||
- Kachel 2 (Icon: Sofa/Netzwerk): **Local-for-Local Marktplatz** — "Das Netzwerk für den regionalen Möbelfachhandel und Makler."
|
||||
- Kachel 3 (Icon: Zahnrad/Vertrag): **Supply-Chain-Management** — "Deutsche Vertragssicherheit für internationale Immobilienentwickler."
|
||||
|
||||
5. **CTA-Sektion** — Kontakt / Nächster Schritt
|
||||
|
||||
**Was von der Homepage ENTFERNT wird:**
|
||||
- Brand Worlds (detaillierte Karten)
|
||||
- Content-Sektion ("Beste aus zwei Welten" — zu detailliert)
|
||||
- Vision-Sektion (ersetzt durch Synergie-Sektion)
|
||||
- Detaillierte Ecosystem-Texte (nur noch Kacheln)
|
||||
|
||||
### 2.2 Navigation
|
||||
|
||||
**Neues Hauptmenü:**
|
||||
|
||||
| Position | Label | Ziel |
|
||||
|----------|-------|------|
|
||||
| 1 | Immobilien | `/immobilien` |
|
||||
| 2 | Für Entwickler & Partner | `/partner` |
|
||||
| 3 | Magazin | `/magazin` |
|
||||
| 4 | Über B2in | `/about` |
|
||||
| 5 (Button, rechts) | Partner-Login | Externer Link → `portal.b2in.test` (target="_blank") |
|
||||
|
||||
**Entscheidung:** "Supply-Chain & B2B" verworfen zugunsten von "Für Entwickler & Partner".
|
||||
|
||||
### 2.3 Seitenstruktur (Was bleibt, was geht, was kommt)
|
||||
|
||||
| Seite | Entscheidung | Details |
|
||||
|-------|-------------|---------|
|
||||
| `/` (Homepage) | **Umbauen** | Weiche/Triage mit visuellem Gewicht (siehe 2.1) |
|
||||
| `/immobilien` | **NEU erstellen** | Emotionale Landingpage für Investoren (Phase B — braucht CMS) |
|
||||
| `/partner` | **Erweitern** | Supply-Chain als Top-Sektion integrieren, 4 Rollen darunter behalten |
|
||||
| `/about` | **Behalten** | Unverändert — zwingend für Trust im Investment-Bereich |
|
||||
| `/ecosystem` | **Absorbieren** | Content migriert nach `/partner` und `/about`, Route als Redirect auf `/partner` |
|
||||
| `/magazin` | **Behalten** | Unverändert |
|
||||
| `/contact` | **Behalten** | Unverändert (Betreffs bereits aktualisiert) |
|
||||
|
||||
### 2.4 Partner-Seite als B2B-Hub
|
||||
|
||||
Die Partner-Seite wird zum zentralen B2B-Einstiegspunkt. Neue Struktur:
|
||||
|
||||
1. **Neuer Hero** — "Für Entwickler & Partner" (Menüpunkt-Ziel)
|
||||
2. **Supply-Chain-Management Top-Sektion (NEU)** — Text aus Kundenanforderungen:
|
||||
- Einleitung: "Wir übernehmen die operative und strategische Steuerung..."
|
||||
- 3 Punkte: Vertragsmanagement, Vertragssicherung & Durchsetzung, Tracking & QK
|
||||
3. **Makler & Händler Teaser** — Local-for-Local kurz angeteasert
|
||||
4. **Bestehende 4 Rollen-Sektionen** — Developer, Retailer, Supplier, Broker (bleiben)
|
||||
5. **Partner-CTA** — Kontakt / Partnerschaft anfragen
|
||||
|
||||
### 2.5 Immobilien-Seite mit Mini-CMS
|
||||
|
||||
**Entscheidung:** Zwingend dynamisch über Admin-Panel verwaltbar.
|
||||
|
||||
**Datenstruktur (Model: `Property` / `Immobilie`):**
|
||||
|
||||
| Feld | Typ | Beispiel |
|
||||
|------|-----|---------|
|
||||
| `title` | string | "Azizi Developments: Creek Views 4" |
|
||||
| `subtitle` | string | "Al Jaddaf, Dubai" |
|
||||
| `status` | enum | NEW_LAUNCH / AVAILABLE / SOLD |
|
||||
| `price_from` | string | "ab 1,125,000 AED" |
|
||||
| `highlights` | json/text | Bulletpoints (Prime Waterfront Views, etc.) |
|
||||
| `image` | string/media | Projekt-Bild |
|
||||
| `launch_date` | date | 2026-03-03 |
|
||||
| `description` | text | Langtext |
|
||||
| `sort_order` | integer | Sortierung auf der Seite |
|
||||
| `is_published` | boolean | Sichtbar auf Frontend |
|
||||
|
||||
**Frontend (`/immobilien`):**
|
||||
1. Hero: "Investieren Sie in die Zukunft – Dubai, Lissabon & mehr."
|
||||
2. Aktuelle Projekte: Kachel-Ansicht (dynamisch aus DB)
|
||||
3. "B2in Möbel-Vorteil" Banner (Synergie-Hack)
|
||||
4. Trust & Kontakt (Investor Evenings, Terminbuchung)
|
||||
|
||||
**Admin-Panel:** CRUD-Verwaltung im bestehenden Portal (portal.b2in.test)
|
||||
|
||||
### 2.6 Partner-Login (Cross-Domain)
|
||||
|
||||
**Entscheidung:** Einfacher externer Link.
|
||||
|
||||
```html
|
||||
<a href="https://portal.b2in.de" target="_blank" rel="noopener">Partner-Login</a>
|
||||
```
|
||||
|
||||
Kein SSO, kein Session-Sharing — sauberer Absprung ins eigenständige Portal.
|
||||
|
||||
---
|
||||
|
||||
## 3. Umsetzungs-Phasen
|
||||
|
||||
### Phase A – Sofort umsetzbar (kein neuer Content vom Kunden nötig)
|
||||
|
||||
| # | Aufgabe | Dateien | Aufwand |
|
||||
|---|---------|---------|--------|
|
||||
| A1 | Homepage zur Weiche umbauen | `home.blade.php`, `b2in.blade.php`, `config/content.php` | 2-3h |
|
||||
| A2 | Synergie-Sektion erstellen | Neue Livewire-Komponente oder Config-Sektion | 1h |
|
||||
| A3 | 3 Kacheln vereinfachen | `config/content.php` (ecosystem_core) | 30min |
|
||||
| A4 | Navigation anpassen | `config/content.php` (navigation) + Header-Komponente | 1h |
|
||||
| A5 | Partner-Seite erweitern | `partner.blade.php`, `config/content.php` | 2h |
|
||||
| A6 | `/ecosystem` → Redirect auf `/partner` | Routes | 15min |
|
||||
| A7 | Partner-Login Button im Header | Header-Komponente | 30min |
|
||||
|
||||
**Gesamt Phase A: ~7-8h**
|
||||
|
||||
### Phase B – Braucht Content/Technik vom Kunden
|
||||
|
||||
| # | Aufgabe | Dateien | Aufwand |
|
||||
|---|---------|---------|--------|
|
||||
| B1 | Property/Immobilie Model + Migration + Factory | Model, Migration, Factory, Seeder | 2h |
|
||||
| B2 | Admin-CRUD für Immobilien | Livewire-Komponenten im Admin-Portal | 4-6h |
|
||||
| B3 | `/immobilien` Frontend-Landingpage | Blade-View, Route, Livewire-Komponenten | 3-4h |
|
||||
| B4 | Investor Evenings / Terminbuchung | Sektion auf /immobilien | 1-2h |
|
||||
|
||||
**Gesamt Phase B: ~10-14h**
|
||||
|
||||
---
|
||||
|
||||
## 4. Offene Punkte (an Kunden)
|
||||
|
||||
1. **Immobilien-Content:** Welche Projekte initial? Bilder vorhanden?
|
||||
2. **Calendly vs. Kontaktformular** für Investor Evenings
|
||||
3. **B2A-Markenwelt:** Bleibt erhalten oder wird entfernt?
|
||||
4. **Porträtfoto Marcel Scheibe:** Hochwertiges Bild für Hero/Founder-Bar vorhanden?
|
||||
|
||||
---
|
||||
|
||||
## 5. Dateien in diesem Ordner
|
||||
|
||||
| Datei | Beschreibung |
|
||||
|-------|-------------|
|
||||
| `B2in Website-Architektur.md` | Neues Architektur-Konzept (02.03.2026) — Grundlage für die Überarbeitung |
|
||||
| `b2in-local-for-local.md` | Konzeptpapier v1.1 — Local for Local Marktplatz-Ökosystem + E-Mail an Kunden |
|
||||
| `b2in-website-umsetzungsplan.md` | Originaler Umsetzungsplan (27.02.2026) — IST vs. SOLL der ersten Phase |
|
||||
| `b2in-umsetzung-changelog.md` | Changelog der ersten Umsetzung — was wurde am 27.02. geändert |
|
||||
| `review-architektur-02-03-2026.md` | **Dieses Dokument** — Finaler Review mit Entscheidungen und Umsetzungsplan |
|
||||
| `backup-before-relaunch/` | Backups aller Original-Dateien vor der ersten Umsetzung |
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { ArrowRight } from "lucide-react";
|
||||
import room1 from "../assets/room-1.jpg";
|
||||
import room2 from "../assets/room-2.jpg";
|
||||
import room2 from "../assets/room-2.jpg";
|
||||
import room3 from "../assets/room-3.jpg";
|
||||
|
||||
const BrandWorlds = () => {
|
||||
|
|
@ -15,7 +15,7 @@ const BrandWorlds = () => {
|
|||
{
|
||||
id: 2,
|
||||
image: room2,
|
||||
title: "stileigentum",
|
||||
title: "stileigentum",
|
||||
description: "Für exklusive Premium-Immobilien und zeitlose Eleganz mit höchsten Ansprüchen.",
|
||||
link: "/rooms"
|
||||
},
|
||||
|
|
@ -35,30 +35,30 @@ const BrandWorlds = () => {
|
|||
<div className="text-center mb-16">
|
||||
<h2 className="text-section-title">Unsere Markenwelten</h2>
|
||||
<p className="text-large text-muted-foreground mt-4 max-w-2xl mx-auto">
|
||||
Entdecken Sie die Welten von B2In – drei Bereiche, ein Ökosystem.
|
||||
Entdecken Sie die Welten von B2in – drei Bereiche, ein Ökosystem.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Brand Cards */}
|
||||
<div className="grid md:grid-cols-3 gap-8">
|
||||
{worlds.map((world) => (
|
||||
<div key={world.id} className="card-elevated overflow-hidden group hover:shadow-[var(--shadow-elevated)] transition-all duration-300">
|
||||
<div className="relative">
|
||||
<img
|
||||
src={world.image}
|
||||
<img
|
||||
src={world.image}
|
||||
alt={world.title}
|
||||
className="w-full h-64 object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/40 to-transparent" />
|
||||
</div>
|
||||
|
||||
|
||||
<div className="p-6 spacing-small">
|
||||
<h3 className="text-xl font-medium">{world.title}</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
{world.description}
|
||||
</p>
|
||||
|
||||
<a
|
||||
|
||||
<a
|
||||
href={world.link}
|
||||
className="inline-flex items-center gap-2 text-secondary font-medium hover:gap-3 transition-all duration-300"
|
||||
>
|
||||
|
|
@ -74,4 +74,4 @@ const BrandWorlds = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default BrandWorlds;
|
||||
export default BrandWorlds;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const BrokerSection = () => {
|
|||
{
|
||||
icon: Target,
|
||||
title: "Qualifizierte Leads",
|
||||
description: "Vorgefilterte, interessierte Kunden durch das B2In-Portal und Premium-Mitgliedschaften"
|
||||
description: "Vorgefilterte, interessierte Kunden durch das B2in-Portal und Premium-Mitgliedschaften"
|
||||
},
|
||||
{
|
||||
icon: Award,
|
||||
|
|
@ -38,7 +38,7 @@ const BrokerSection = () => {
|
|||
Lifetime-Vergütung
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="bg-accent/30 rounded-xl p-6">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
|
|
@ -49,7 +49,7 @@ const BrokerSection = () => {
|
|||
<div className="bg-primary h-2 rounded-full w-[35%]"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="bg-accent/30 rounded-xl p-6">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm text-muted-foreground">Folgegeschäfte</span>
|
||||
|
|
@ -59,7 +59,7 @@ const BrokerSection = () => {
|
|||
<div className="bg-primary h-2 rounded-full w-[60%]"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="bg-primary/10 rounded-xl p-6">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium text-foreground">Lifetime Value</span>
|
||||
|
|
@ -72,26 +72,26 @@ const BrokerSection = () => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-8 order-1 lg:order-2">
|
||||
<div>
|
||||
<div className="inline-flex items-center gap-2 bg-primary/10 text-primary px-4 py-2 rounded-full text-sm font-medium mb-6">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
Für Makler
|
||||
</div>
|
||||
|
||||
|
||||
<h2 className="text-4xl lg:text-5xl font-light text-foreground mb-6">
|
||||
Nachhaltiger <span className="text-primary">Erfolg</span> durch Innovation
|
||||
</h2>
|
||||
|
||||
|
||||
<p className="text-xl text-muted-foreground leading-relaxed">
|
||||
Unser revolutionäres Lifetime-Vergütungsmodell belohnt langfristige
|
||||
Kundenbeziehungen. Durch durchdachte Wohnkonzepte vermarkten Sie
|
||||
Immobilien nicht nur schneller, sondern bauen nachhaltige
|
||||
Unser revolutionäres Lifetime-Vergütungsmodell belohnt langfristige
|
||||
Kundenbeziehungen. Durch durchdachte Wohnkonzepte vermarkten Sie
|
||||
Immobilien nicht nur schneller, sondern bauen nachhaltige
|
||||
Einnahmequellen auf.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-6">
|
||||
{benefits.map((benefit, index) => (
|
||||
<div key={index} className="flex gap-4">
|
||||
|
|
@ -116,4 +116,4 @@ const BrokerSection = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default BrokerSection;
|
||||
export default BrokerSection;
|
||||
|
|
|
|||
|
|
@ -42,14 +42,14 @@ const DigitalCore = () => {
|
|||
<Zap className="w-4 h-4" />
|
||||
Das digitale Herzstück
|
||||
</div>
|
||||
|
||||
|
||||
<h2 className="text-4xl lg:text-5xl font-light text-foreground mb-6">
|
||||
B2In <span className="text-primary">Portal</span>
|
||||
B2in <span className="text-primary">Portal</span>
|
||||
</h2>
|
||||
|
||||
|
||||
<p className="text-xl text-muted-foreground max-w-3xl mx-auto leading-relaxed">
|
||||
Unsere zentrale technologische Plattform verbindet alle Ecosystem-Teilnehmer
|
||||
nahtlos miteinander. Modernste Technologie trifft auf intuitive Bedienung
|
||||
Unsere zentrale technologische Plattform verbindet alle Ecosystem-Teilnehmer
|
||||
nahtlos miteinander. Modernste Technologie trifft auf intuitive Bedienung
|
||||
und schafft einzigartige digitale Erlebnisse.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -66,7 +66,7 @@ const DigitalCore = () => {
|
|||
Zentrale Plattform
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3 bg-background/50 rounded-xl p-4">
|
||||
<div className="w-8 h-8 rounded-full bg-green-500 flex items-center justify-center">
|
||||
|
|
@ -77,7 +77,7 @@ const DigitalCore = () => {
|
|||
<p className="text-xs text-muted-foreground">Online & Verfügbar</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center gap-3 bg-background/50 rounded-xl p-4">
|
||||
<div className="w-8 h-8 rounded-full bg-green-500 flex items-center justify-center">
|
||||
<div className="w-3 h-3 rounded-full bg-white"></div>
|
||||
|
|
@ -87,7 +87,7 @@ const DigitalCore = () => {
|
|||
<p className="text-xs text-muted-foreground">Online & Verfügbar</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center gap-3 bg-background/50 rounded-xl p-4">
|
||||
<div className="w-8 h-8 rounded-full bg-green-500 flex items-center justify-center">
|
||||
<div className="w-3 h-3 rounded-full bg-white"></div>
|
||||
|
|
@ -97,7 +97,7 @@ const DigitalCore = () => {
|
|||
<p className="text-xs text-muted-foreground">Online & Verfügbar</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center gap-3 bg-background/50 rounded-xl p-4">
|
||||
<div className="w-8 h-8 rounded-full bg-green-500 flex items-center justify-center">
|
||||
<div className="w-3 h-3 rounded-full bg-white"></div>
|
||||
|
|
@ -111,21 +111,21 @@ const DigitalCore = () => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h3 className="text-3xl font-light text-foreground mb-6">
|
||||
Technische <span className="text-primary">Excellence</span>
|
||||
</h3>
|
||||
|
||||
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
Das B2In-Portal ist mehr als nur eine Software – es ist das
|
||||
technologische Rückgrat unseres gesamten Ecosystems. Entwickelt
|
||||
mit modernsten Standards für Sicherheit, Performance und
|
||||
Das B2in-Portal ist mehr als nur eine Software – es ist das
|
||||
technologische Rückgrat unseres gesamten Ecosystems. Entwickelt
|
||||
mit modernsten Standards für Sicherheit, Performance und
|
||||
Benutzerfreundlichkeit.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center gap-3 bg-accent/30 rounded-xl p-4">
|
||||
<Shield className="w-6 h-6 text-primary" />
|
||||
|
|
@ -134,7 +134,7 @@ const DigitalCore = () => {
|
|||
<p className="text-sm text-muted-foreground">Garantierte Verfügbarkeit</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center gap-3 bg-accent/30 rounded-xl p-4">
|
||||
<Cpu className="w-6 h-6 text-primary" />
|
||||
<div>
|
||||
|
|
@ -142,7 +142,7 @@ const DigitalCore = () => {
|
|||
<p className="text-sm text-muted-foreground">Blitzschnelle Performance</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center gap-3 bg-accent/30 rounded-xl p-4">
|
||||
<Database className="w-6 h-6 text-primary" />
|
||||
<div>
|
||||
|
|
@ -153,18 +153,18 @@ const DigitalCore = () => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{features.map((feature, index) => (
|
||||
<div key={index} className="card-elevated rounded-2xl p-8">
|
||||
<div className="w-14 h-14 rounded-xl bg-primary/10 flex items-center justify-center mb-6">
|
||||
<feature.icon className="w-7 h-7 text-primary" />
|
||||
</div>
|
||||
|
||||
|
||||
<h3 className="text-xl font-semibold text-foreground mb-4">
|
||||
{feature.title}
|
||||
</h3>
|
||||
|
||||
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
{feature.description}
|
||||
</p>
|
||||
|
|
@ -176,4 +176,4 @@ const DigitalCore = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default DigitalCore;
|
||||
export default DigitalCore;
|
||||
|
|
|
|||
|
|
@ -7,15 +7,15 @@ const EcosystemHero = () => {
|
|||
<div className="grid lg:grid-cols-2 gap-16 items-center">
|
||||
<div className="space-y-8">
|
||||
<h1 className="text-5xl lg:text-7xl font-light text-foreground">
|
||||
B2In <span className="text-primary">Ecosystem</span>
|
||||
B2in <span className="text-primary">Ecosystem</span>
|
||||
</h1>
|
||||
|
||||
|
||||
<p className="text-xl lg:text-2xl text-muted-foreground leading-relaxed">
|
||||
Ein intelligentes Netzwerk, das Endkunden, Makler, Lieferanten und
|
||||
Technologie nahtlos miteinander verbindet. Jeder Teilnehmer profitiert
|
||||
Ein intelligentes Netzwerk, das Endkunden, Makler, Lieferanten und
|
||||
Technologie nahtlos miteinander verbindet. Jeder Teilnehmer profitiert
|
||||
vom gesamten System und schafft gemeinsam außergewöhnliche Immobilienerlebnisse.
|
||||
</p>
|
||||
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
|
|
@ -26,7 +26,7 @@ const EcosystemHero = () => {
|
|||
<p className="text-sm text-muted-foreground">Exklusive Erlebnisse</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Building2 className="w-6 h-6 text-primary" />
|
||||
|
|
@ -36,7 +36,7 @@ const EcosystemHero = () => {
|
|||
<p className="text-sm text-muted-foreground">Lifetime-Vergütung</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Network className="w-6 h-6 text-primary" />
|
||||
|
|
@ -46,7 +46,7 @@ const EcosystemHero = () => {
|
|||
<p className="text-sm text-muted-foreground">Kuratierte Plattform</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Zap className="w-6 h-6 text-primary" />
|
||||
|
|
@ -58,7 +58,7 @@ const EcosystemHero = () => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="relative">
|
||||
<div className="card-elevated rounded-3xl p-12 bg-background/80 backdrop-blur-sm">
|
||||
<div className="space-y-8">
|
||||
|
|
@ -67,10 +67,10 @@ const EcosystemHero = () => {
|
|||
<div className="w-24 h-24 mx-auto rounded-full bg-primary flex items-center justify-center mb-4">
|
||||
<Network className="w-12 h-12 text-white" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-foreground">B2In Portal</h3>
|
||||
<h3 className="text-xl font-semibold text-foreground">B2in Portal</h3>
|
||||
<p className="text-sm text-muted-foreground">Zentrale Plattform</p>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Connection Lines */}
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
<div className="text-center">
|
||||
|
|
@ -79,21 +79,21 @@ const EcosystemHero = () => {
|
|||
</div>
|
||||
<p className="font-medium text-sm">Endkunden</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 mx-auto rounded-full bg-accent flex items-center justify-center mb-3">
|
||||
<Building2 className="w-8 h-8 text-accent-foreground" />
|
||||
</div>
|
||||
<p className="font-medium text-sm">Makler</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 mx-auto rounded-full bg-accent flex items-center justify-center mb-3">
|
||||
<Network className="w-8 h-8 text-accent-foreground" />
|
||||
</div>
|
||||
<p className="font-medium text-sm">Lieferanten</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 mx-auto rounded-full bg-accent flex items-center justify-center mb-3">
|
||||
<Zap className="w-8 h-8 text-accent-foreground" />
|
||||
|
|
@ -110,4 +110,4 @@ const EcosystemHero = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default EcosystemHero;
|
||||
export default EcosystemHero;
|
||||
|
|
|
|||
|
|
@ -8,27 +8,27 @@ const Footer = () => {
|
|||
<div className="text-center spacing-section">
|
||||
<div className="spacing-section">
|
||||
<div className="flex items-center justify-center">
|
||||
<img src={b2inLogo} alt="B2In Logo" className="h-12 w-auto" />
|
||||
<img src={b2inLogo} alt="B2in Logo" className="h-12 w-auto" />
|
||||
</div>
|
||||
|
||||
|
||||
<div className="container-narrow spacing-content">
|
||||
<h2 className="text-section-title text-[hsl(var(--dark-text))] leading-tight">
|
||||
We're committed to your <span className="text-secondary">comfort</span> and <br />
|
||||
<span className="text-secondary">satisfaction</span> for <br />
|
||||
unforgettable experiences
|
||||
</h2>
|
||||
|
||||
|
||||
<p className="text-large text-dark-muted leading-relaxed max-w-2xl mx-auto">
|
||||
Our dedicated team works around the clock to ensure every aspect of your stay
|
||||
Our dedicated team works around the clock to ensure every aspect of your stay
|
||||
exceeds expectations. From booking to checkout, we're here for you.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<button className="btn-accent">
|
||||
Start Your Journey
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Links */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 text-left max-w-4xl mx-auto">
|
||||
<div className="spacing-small">
|
||||
|
|
@ -40,7 +40,7 @@ const Footer = () => {
|
|||
<a href="#" className="block hover:text-secondary transition-colors">Blog</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="spacing-small">
|
||||
<h4 className="font-medium text-[hsl(var(--dark-text))]">Services</h4>
|
||||
<div className="spacing-small text-dark-muted text-sm">
|
||||
|
|
@ -50,7 +50,7 @@ const Footer = () => {
|
|||
<a href="#" className="block hover:text-secondary transition-colors">Villas</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="spacing-small">
|
||||
<h4 className="font-medium text-[hsl(var(--dark-text))]">Support</h4>
|
||||
<div className="spacing-small text-dark-muted text-sm">
|
||||
|
|
@ -60,7 +60,7 @@ const Footer = () => {
|
|||
<a href="#" className="block hover:text-secondary transition-colors">Cancellation</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="spacing-small">
|
||||
<h4 className="font-medium text-[hsl(var(--dark-text))]">Legal</h4>
|
||||
<div className="spacing-small text-dark-muted text-sm">
|
||||
|
|
@ -72,12 +72,12 @@ const Footer = () => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Bottom Bar */}
|
||||
<div className="border-t border-[hsl(var(--dark-muted))]/30 mt-12 pt-8">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center spacing-small md:space-y-0">
|
||||
<div className="text-dark-muted text-sm">
|
||||
© 2024 B2In. All rights reserved.
|
||||
© 2024 B2in. All rights reserved.
|
||||
</div>
|
||||
<div className="flex items-center space-x-6 text-dark-muted text-sm">
|
||||
<a href="#" className="hover:text-secondary transition-colors">English</a>
|
||||
|
|
@ -95,4 +95,4 @@ const Footer = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
export default Footer;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const Hero = () => {
|
|||
Das globale Ökosystem für Immobilieninvestoren, Makler und Designliebhaber.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<button className="btn-primary">
|
||||
Ecosystem entdecken
|
||||
|
|
@ -25,7 +25,7 @@ const Hero = () => {
|
|||
Partner werden
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center space-x-8 text-sm text-muted-foreground">
|
||||
<span>1.7M+ Nutzer</span>
|
||||
<span>•</span>
|
||||
|
|
@ -34,21 +34,21 @@ const Hero = () => {
|
|||
<span>24/7 Platform</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Right Image */}
|
||||
<div className="relative">
|
||||
<div className="relative rounded-3xl overflow-hidden shadow-[var(--shadow-elevated)]">
|
||||
<img
|
||||
src={heroImage}
|
||||
<img
|
||||
src={heroImage}
|
||||
alt="Modern international skyline showcasing architectural design"
|
||||
className="w-full h-[600px] object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent" />
|
||||
</div>
|
||||
|
||||
|
||||
{/* Floating info card */}
|
||||
<div className="absolute bottom-6 left-6 bg-card/95 backdrop-blur-sm rounded-xl p-4 shadow-lg border border-border/50">
|
||||
<div className="text-sm text-muted-foreground">B2In Ecosystem</div>
|
||||
<div className="text-sm text-muted-foreground">B2in Ecosystem</div>
|
||||
<div className="text-2xl font-medium">Global<span className="text-sm text-muted-foreground"> vernetzt</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -58,4 +58,4 @@ const Hero = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default Hero;
|
||||
export default Hero;
|
||||
|
|
|
|||
|
|
@ -7,29 +7,29 @@ const NewAboutHero = () => {
|
|||
<div className="grid lg:grid-cols-2 gap-16 items-center">
|
||||
<div className="space-y-8">
|
||||
<h1 className="text-5xl lg:text-6xl font-light text-foreground">
|
||||
Über <span className="text-secondary">B2In</span>
|
||||
Über <span className="text-secondary">B2in</span>
|
||||
</h1>
|
||||
|
||||
|
||||
<blockquote className="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
|
||||
"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
|
||||
nicht nur Verbindungen – wir bauen Brücken in die Zukunft."
|
||||
</blockquote>
|
||||
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-16 h-px bg-secondary"></div>
|
||||
<div>
|
||||
<p className="font-semibold text-foreground">Marcel Scheibe</p>
|
||||
<p className="text-sm text-muted-foreground">Gründer & CEO, B2In</p>
|
||||
<p className="text-sm text-muted-foreground">Gründer & CEO, B2in</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="relative">
|
||||
<div className="card-elevated rounded-3xl overflow-hidden">
|
||||
<img
|
||||
src={marcelImage}
|
||||
alt="Marcel Scheibe, Gründer und CEO von B2In"
|
||||
<img
|
||||
src={marcelImage}
|
||||
alt="Marcel Scheibe, Gründer und CEO von B2in"
|
||||
className="w-full h-96 lg:h-[500px] object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -44,4 +44,4 @@ const NewAboutHero = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default NewAboutHero;
|
||||
export default NewAboutHero;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const OurStory = () => {
|
|||
<h2 className="text-section-title text-[hsl(var(--dark-text))] mb-12">
|
||||
Unsere <span className="text-secondary">Geschichte</span>
|
||||
</h2>
|
||||
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8 mb-16">
|
||||
<div className="spacing-small">
|
||||
<div className="w-12 h-12 mx-auto bg-secondary/20 rounded-full flex items-center justify-center">
|
||||
|
|
@ -13,38 +13,38 @@ const OurStory = () => {
|
|||
</div>
|
||||
<h3 className="text-xl font-semibold text-[hsl(var(--dark-text))]">Die Idee</h3>
|
||||
<p className="text-dark-muted text-sm leading-relaxed">
|
||||
2019 erkannten wir eine kritische Marktlücke: Unternehmen benötigten intelligente,
|
||||
2019 erkannten wir eine kritische Marktlücke: Unternehmen benötigten intelligente,
|
||||
nachhaltige Konnektivitätslösungen für die digitale Transformation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="spacing-small">
|
||||
<div className="w-12 h-12 mx-auto bg-secondary/20 rounded-full flex items-center justify-center">
|
||||
<div className="w-6 h-6 bg-secondary rounded-full"></div>
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-[hsl(var(--dark-text))]">Die Mission</h3>
|
||||
<p className="text-dark-muted text-sm leading-relaxed">
|
||||
Wir entwickelten innovative B2B-Lösungen, die Unternehmen dabei unterstützen,
|
||||
Wir entwickelten innovative B2B-Lösungen, die Unternehmen dabei unterstützen,
|
||||
effizienter zu arbeiten und nachhaltiges Wachstum zu erzielen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="spacing-small">
|
||||
<div className="w-12 h-12 mx-auto bg-secondary/20 rounded-full flex items-center justify-center">
|
||||
<div className="w-6 h-6 bg-secondary rounded-full"></div>
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-[hsl(var(--dark-text))]">Die Zukunft</h3>
|
||||
<p className="text-dark-muted text-sm leading-relaxed">
|
||||
Heute sind wir stolz darauf, hunderte Unternehmen dabei zu unterstützen,
|
||||
Heute sind wir stolz darauf, hunderte Unternehmen dabei zu unterstützen,
|
||||
ihre digitalen Ziele zu erreichen und neue Märkte zu erschließen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<p className="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
|
||||
traditionellen Unternehmen und modernen, digitalen Lösungen durch maßgeschneiderte
|
||||
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
|
||||
traditionellen Unternehmen und modernen, digitalen Lösungen durch maßgeschneiderte
|
||||
Konnektivitätsservices, die Effizienz steigern und nachhaltiges Wachstum fördern.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -52,4 +52,4 @@ const OurStory = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default OurStory;
|
||||
export default OurStory;
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ const PartnerBenefits = () => {
|
|||
Warum Partner werden?
|
||||
</h2>
|
||||
<p className="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>
|
||||
|
|
@ -128,8 +128,8 @@ const PartnerBenefits = () => {
|
|||
|
||||
<div className="relative">
|
||||
<div className="card-elevated p-0 overflow-hidden rounded-xl">
|
||||
<img
|
||||
src={accommodationImage}
|
||||
<img
|
||||
src={accommodationImage}
|
||||
alt="Partner success visualization"
|
||||
className="w-full h-48 object-cover"
|
||||
/>
|
||||
|
|
@ -148,4 +148,4 @@ const PartnerBenefits = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default PartnerBenefits;
|
||||
export default PartnerBenefits;
|
||||
|
|
|
|||
|
|
@ -10,14 +10,14 @@ const PartnerCTA = () => {
|
|||
Wachsen Sie <br />
|
||||
<span className="text-secondary">mit uns</span>
|
||||
</h2>
|
||||
|
||||
|
||||
<div className="w-16 h-px bg-secondary mx-auto"></div>
|
||||
|
||||
|
||||
<p className="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 className="grid md:grid-cols-3 gap-8 py-8">
|
||||
<div className="text-center space-y-3">
|
||||
<div className="text-4xl font-light text-secondary">500+</div>
|
||||
|
|
@ -32,16 +32,16 @@ const PartnerCTA = () => {
|
|||
<p className="text-dark-muted text-sm">Partner-Support</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="spacing-content">
|
||||
<Link to="/contact">
|
||||
<button className="btn-accent px-12 py-6 rounded-2xl text-lg">
|
||||
Werden Sie B2In Partner
|
||||
Werden Sie B2in Partner
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
|
||||
<p className="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>
|
||||
|
|
@ -50,4 +50,4 @@ const PartnerCTA = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default PartnerCTA;
|
||||
export default PartnerCTA;
|
||||
|
|
|
|||
|
|
@ -8,15 +8,15 @@ const PartnerHero = () => {
|
|||
<div className="space-y-8">
|
||||
<h1 className="text-5xl lg:text-7xl font-light text-foreground">
|
||||
Wachsen Sie mit uns.<br />
|
||||
Werden Sie <span className="text-primary">B2In Partner</span>.
|
||||
Werden Sie <span className="text-primary">B2in Partner</span>.
|
||||
</h1>
|
||||
|
||||
|
||||
<p className="text-xl lg:text-2xl text-muted-foreground leading-relaxed">
|
||||
Werden Sie Teil des B2In Ecosystems und profitieren Sie von innovativen
|
||||
Geschäftsmodellen, die nachhaltiges Wachstum und langfristigen Erfolg ermöglichen.
|
||||
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 className="grid grid-cols-2 gap-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
|
|
@ -27,7 +27,7 @@ const PartnerHero = () => {
|
|||
<p className="text-sm text-muted-foreground">Lifetime-Vergütung</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Globe className="w-6 h-6 text-primary" />
|
||||
|
|
@ -37,7 +37,7 @@ const PartnerHero = () => {
|
|||
<p className="text-sm text-muted-foreground">Globale Märkte</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Handshake className="w-6 h-6 text-primary" />
|
||||
|
|
@ -47,7 +47,7 @@ const PartnerHero = () => {
|
|||
<p className="text-sm text-muted-foreground">Faire Konditionen</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Award className="w-6 h-6 text-primary" />
|
||||
|
|
@ -59,7 +59,7 @@ const PartnerHero = () => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="relative">
|
||||
<div className="card-elevated rounded-3xl p-12 bg-background/80 backdrop-blur-sm">
|
||||
<div className="space-y-8">
|
||||
|
|
@ -71,7 +71,7 @@ const PartnerHero = () => {
|
|||
<h3 className="text-xl font-semibold text-foreground">Partner Network</h3>
|
||||
<p className="text-sm text-muted-foreground">Werden Sie Teil unseres Ecosystems</p>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Partner Types */}
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
<div className="text-center">
|
||||
|
|
@ -81,7 +81,7 @@ const PartnerHero = () => {
|
|||
<p className="font-medium text-sm">Makler</p>
|
||||
<p className="text-xs text-muted-foreground">Lifetime-Modell</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 mx-auto rounded-full bg-accent flex items-center justify-center mb-3">
|
||||
<Globe className="w-8 h-8 text-accent-foreground" />
|
||||
|
|
@ -89,7 +89,7 @@ const PartnerHero = () => {
|
|||
<p className="font-medium text-sm">Lieferanten</p>
|
||||
<p className="text-xs text-muted-foreground">Global Markets</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 mx-auto rounded-full bg-accent flex items-center justify-center mb-3">
|
||||
<Target className="w-8 h-8 text-accent-foreground" />
|
||||
|
|
@ -97,7 +97,7 @@ const PartnerHero = () => {
|
|||
<p className="font-medium text-sm">Erfolg</p>
|
||||
<p className="text-xs text-muted-foreground">Messbare Ziele</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 mx-auto rounded-full bg-accent flex items-center justify-center mb-3">
|
||||
<Award className="w-8 h-8 text-accent-foreground" />
|
||||
|
|
@ -115,4 +115,4 @@ const PartnerHero = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default PartnerHero;
|
||||
export default PartnerHero;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ const PartnerProcess = () => {
|
|||
image: room1Image
|
||||
},
|
||||
{
|
||||
step: "2",
|
||||
step: "2",
|
||||
title: "Prüfung",
|
||||
description: "Unser Expertenteam überprüft Ihre Bewerbung sorgfältig. Bei positivem Ergebnis laden wir Sie zu einem persönlichen Gespräch ein.",
|
||||
icon: Search,
|
||||
|
|
@ -37,7 +37,7 @@ const PartnerProcess = () => {
|
|||
So werden Sie <span className="text-primary">Partner</span>
|
||||
</h2>
|
||||
<p className="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>
|
||||
|
|
@ -46,19 +46,19 @@ const PartnerProcess = () => {
|
|||
{steps.map((step, index) => (
|
||||
<div key={index} className="card-elevated p-0 overflow-hidden group hover:shadow-[var(--shadow-elevated)] transition-all duration-300">
|
||||
<div className="relative overflow-hidden">
|
||||
<img
|
||||
<img
|
||||
src={step.image}
|
||||
alt={step.title}
|
||||
className="w-full h-64 object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
<div className="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 className="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 className="p-8">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
||||
|
|
@ -68,11 +68,11 @@ const PartnerProcess = () => {
|
|||
{step.title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
|
||||
<p className="text-muted-foreground leading-relaxed mb-6">
|
||||
{step.description}
|
||||
</p>
|
||||
|
||||
|
||||
{index === steps.length - 1 && (
|
||||
<Link to="/contact">
|
||||
<button className="btn-secondary w-full">
|
||||
|
|
@ -92,7 +92,7 @@ const PartnerProcess = () => {
|
|||
Bereit für den nächsten <span className="text-primary">Schritt</span>?
|
||||
</h3>
|
||||
<p className="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>
|
||||
<Link to="/contact">
|
||||
|
|
@ -107,4 +107,4 @@ const PartnerProcess = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default PartnerProcess;
|
||||
export default PartnerProcess;
|
||||
|
|
|
|||
|
|
@ -11,28 +11,28 @@ const VisionSection = () => {
|
|||
<h2 className="text-section-title">Gebaut auf Vertrauen</h2>
|
||||
<div className="spacing-small text-large text-muted-foreground leading-relaxed">
|
||||
<p>
|
||||
Unsere Basis ist Vertrauen, angetrieben von Technologie und Innovation.
|
||||
B2In ist nicht nur eine Holding, sondern ein aktiver Gestalter der Immobilienzukunft.
|
||||
Unsere Basis ist Vertrauen, angetrieben von Technologie und Innovation.
|
||||
B2in ist nicht nur eine Holding, sondern ein aktiver Gestalter der Immobilienzukunft.
|
||||
</p>
|
||||
<p>
|
||||
Wir vereinfachen komplexe Prozesse durch eine zentrale digitale Plattform –
|
||||
das B2In-Portal – und schaffen dabei Transparenz, Qualität und Innovation
|
||||
Wir vereinfachen komplexe Prozesse durch eine zentrale digitale Plattform –
|
||||
das B2in-Portal – und schaffen dabei Transparenz, Qualität und Innovation
|
||||
in jedem Schritt unserer Zusammenarbeit.
|
||||
</p>
|
||||
<p>
|
||||
Unser Engagement für Exzellenz zeigt sich in der Art, wie wir
|
||||
Markenwerte leben und Partnerschaften aufbauen, die nachhaltigen
|
||||
Unser Engagement für Exzellenz zeigt sich in der Art, wie wir
|
||||
Markenwerte leben und Partnerschaften aufbauen, die nachhaltigen
|
||||
Erfolg für alle Beteiligten schaffen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Image */}
|
||||
<div className="relative">
|
||||
<div className="card-elevated rounded-3xl overflow-hidden">
|
||||
<img
|
||||
src={teamImage}
|
||||
<img
|
||||
src={teamImage}
|
||||
alt="Professionelles Team in kollaborativem Meeting"
|
||||
className="w-full h-[500px] object-cover"
|
||||
/>
|
||||
|
|
@ -45,4 +45,4 @@ const VisionSection = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default VisionSection;
|
||||
export default VisionSection;
|
||||
|
|
|
|||
222
dev/entwicklung copy.md
Normal file
222
dev/entwicklung copy.md
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
# B2IN – Projekt-Entwicklungsstand
|
||||
|
||||
**Stand:** März 2026 · **Laravel** 12 · **Livewire** 4 · **Volt** · **Flux UI** (Pro)
|
||||
|
||||
---
|
||||
|
||||
## Projektübersicht
|
||||
|
||||
Die Anwendung ist eine **Multi-Domain-Laravel-Plattform** für Partner-Onboarding, Produkt- und Hub-Verwaltung, **öffentliche Marketing-Websites** (mehrere Marken/Domains) und **Digital Signage** (Cabinet/Displays). Das **Flux-CMS** (`packages/flux-cms`) ist als Paket eingebunden und steuert zunehmend **Content und Medien** für die Web-Auftritte; klassische Display-Verwaltung (Videos, Footer, Versionen) bleibt über **eigene Admin-Livewire-Komponenten** erreichbar.
|
||||
|
||||
---
|
||||
|
||||
## Wichtige ToDos
|
||||
|
||||
- **System-E-Mail / Zustellung:** Teilweise Ausfälle oder Blocklisten (z. B. Gmail mit SPF/DKIM-Themen bei united-domains). Langfristig: saubere Domain-Auth, Bounce-Handling, Monitoring.
|
||||
- **Bestellsystem / Orders:** Berechtigungen sind im `RoleSeeder` vorbereitet (`view orders`, `manage orders`); fachliche Umsetzung und UI folgen.
|
||||
|
||||
---
|
||||
|
||||
## Kern-Technologien
|
||||
|
||||
| Bereich | Technologie |
|
||||
|--------|-------------|
|
||||
| Backend | Laravel 12, PHP ^8.2 (in der Praxis oft 8.4 in der Dev-Umgebung) |
|
||||
| Frontend (Portal) | Livewire 4, Volt (Single-File-Komponenten), Flux UI Pro |
|
||||
| Frontend (öffentliche Sites) | Livewire-Sections, Tailwind CSS 4, Alpine.js |
|
||||
| Auth | Laravel Fortify, Sanctum (API), E-Mail-Verifizierung, 2FA möglich |
|
||||
| Rechte | Spatie Laravel Permission |
|
||||
| Tests | Pest, PHPUnit 11 |
|
||||
| Assets | Vite (getrennte Builds z. B. Portal / Web) |
|
||||
| Dev | Laravel Sail **oder** Dev Container – in `CLAUDE.md` sind direkte `php`/`composer`/`npm`-Kommandos beschrieben |
|
||||
|
||||
**Hinweis Volt + Livewire 4:** Funktionale Volt-Dateien sollten im PHP-Block **kein erstes `new …`** enthalten (Compiler-Einschränkung); objektorientierte Hilfsklassen liegen z. B. unter `app/Services/`.
|
||||
|
||||
---
|
||||
|
||||
## Rollen & Berechtigungen (Ist-Zustand)
|
||||
|
||||
Definiert u. a. in `database/seeders/RoleSeeder.php` (Namen exakt so in der Datenbank):
|
||||
|
||||
| Rolle | Kurzbeschreibung |
|
||||
|-------|------------------|
|
||||
| **Customer** | Endkunde, Produkt-/Order-sichtbar (eigene Orders) |
|
||||
| **Estate-Agent** | Makler – Dashboard, Partner-Übersicht, Hubs einsehen |
|
||||
| **Retailer** | Händler – eigene Produkte anlegen/bearbeiten, Miet-Optionen |
|
||||
| **Manufacturer** | Hersteller – analog Retailer |
|
||||
| **Admin** | Voller operativer Zugriff inkl. Partner, Hubs, **Produkt-Kuration**, User |
|
||||
| **Super-Admin** | für erweiterte Systemzugriffe vorgesehen (Gate/Policy-Pattern) |
|
||||
|
||||
Die ältere Doku sprach teils von „Broker“ – im Code heißt die Rolle **Estate-Agent**.
|
||||
|
||||
---
|
||||
|
||||
## Entwickelte Module & Funktionen (Auswahl)
|
||||
|
||||
### 1. Multi-Domain-System
|
||||
|
||||
**Status:** produktiv genutzt
|
||||
|
||||
- Domains konfigurierbar (`config/domains.php`, siehe `dev/DOMAINS-CONFIG.md`)
|
||||
- **Portal:** z. B. `portal.b2in.test` – Admin-Backend
|
||||
- **Öffentliche Sites:** z. B. `b2in.test`, weitere Marken-Domains mit Theme-Wechsel
|
||||
- **ThemeServiceProvider**, domainbezogene Vite-Build-Verzeichnisse
|
||||
- Optional: Simulation einer Domain per `.env` für lokales Testen
|
||||
|
||||
---
|
||||
|
||||
### 2. Partner-Management & Onboarding
|
||||
|
||||
**Status:** stabil im Einsatz
|
||||
|
||||
- Einladungen (`PartnerInvitationMail`), Token, Ablauf
|
||||
- **Partner-Setup-Wizard** (`EnsurePartnerSetupCompleted` – unvollständiges Setup leitet zum Wizard)
|
||||
- Registrierung über Codes, rollenspezifische Landing-Routes (`/reg/...`)
|
||||
- Hierarchien und Provisionen: **Datenmodell und Berechtigungen** vorhanden; Ausbaustufen für Abrechnung siehe Roadmap
|
||||
|
||||
---
|
||||
|
||||
### 3. Hub-Management
|
||||
|
||||
**Status:** CRUD und Verknüpfungen nutzbar
|
||||
|
||||
- Admin-Routen für Hubs und Standorte (`hub_locations`)
|
||||
- Detailausbauten (Analytics, automatische Zuweisung) roadmap
|
||||
|
||||
---
|
||||
|
||||
### 4. Produkt-Management
|
||||
|
||||
**Status:** deutlich über „Grundgerüst“ hinaus
|
||||
|
||||
- Partner können u. a. **Standard-** und **Teaser-Produkte** anlegen/bearbeiten (Volt-Formulare)
|
||||
- **Admin-Kuration:** Rolle `Admin`, Permission `curate products` – Freigabe/Status (Tests in `tests/Feature/ProductCurationTest.php`)
|
||||
- Katalog-Modell: Produkte, Varianten, Kategorien, Marken, Tags, Logistik u. a.
|
||||
|
||||
---
|
||||
|
||||
### 5. Digital Signage (Cabinet / Displays)
|
||||
|
||||
**Status:** betrieben; technische Doku unter `dev/DISPLAY_CMS_README.md`, `dev/DISPLAY_SETUP_LIVE.md`
|
||||
|
||||
- Admin: Display-Versionen, Playlists, Footer-Links, Shortlink/Tracking (siehe bestehende README)
|
||||
- Öffentliche/Showroom-Assets u. a. unter `public/_cabinet/`
|
||||
- APIs z. B. Display-Konfiguration (`routes/domains.php`)
|
||||
|
||||
Parallel existiert die **Flux-CMS-Medienverwaltung** für Website-Assets – beide Welten können koexistieren (unterschiedliche Einsatzgebiete).
|
||||
|
||||
---
|
||||
|
||||
### 6. Flux CMS (Package-Integration)
|
||||
|
||||
**Status:** **eingebunden und im Portal bedienbar** – kontinuierliche Content-Pflege und Ausbau
|
||||
|
||||
- Paketpfad: `packages/flux-cms/` (`core`, `components`, `starter-components`)
|
||||
- Composer: `flux-cms/core` (path repository)
|
||||
- **Portal-Routen (Auszug):** u. a. `admin/flux-cms` (Dashboard), Content, Projekte, Medien, Artikel – Namen wie `cms.dashboard`, `cms.content.index`, …
|
||||
- **App-Integration:** Hilfsfunktionen (`cms()`, `media_url()`, … in `app/helpers.php`), Models wie `CmsContent`, `CmsProject`, `CmsArticle` (Migrationen u. a. `cms_projects`, `cms_articles`)
|
||||
- Architekturüberblick: `packages/flux-cms/ARCHITECTURE.md`
|
||||
- **Ziel laut `dev/flux-cms/tasks.md`:** bestehende B2IN-Website schrittweise ins CMS überführen; weitere Subseiten später
|
||||
|
||||
---
|
||||
|
||||
### 7. Öffentliche Website & Content-Strategie
|
||||
|
||||
**Status:** laufende inhaltliche Ausrichtung
|
||||
|
||||
- Viele Seiten als **Livewire-Section-Komponenten** unter `app/Livewire/Web/Components/Sections/`
|
||||
- **Immobilien-Fokus (Soft Launch):** strategische Ausrichtung und Text-Briefings liegen in `dev/12-03-2026/tasks.md` (Fokus Dubai/Immobilien, Möbel/Einrichtung zunächst nur Teaser; bestehende Inhalte konservieren, ggf. über ergänzende/„versteckte“ Pfade wie bei Theme-Demos)
|
||||
- Routen u. a. `/immobilien`, `/immobilien/{slug}` – Sektionen u. a. über `cms_theme_section('…')` pro Theme-Key (siehe `resources/views/web/immobilien.blade.php`)
|
||||
|
||||
---
|
||||
|
||||
### 8. Authentifizierung & Einstellungen
|
||||
|
||||
**Status:** produktiv
|
||||
|
||||
- Fortify-Features (Login, Register, Reset, Verify, 2FA)
|
||||
- Profil, Passwort, Erscheinungsbild (Volt)
|
||||
|
||||
---
|
||||
|
||||
### 9. Admin-Extras
|
||||
|
||||
- **Projekt-Dokumentation** im Portal: Route `admin/documentation` rendert diese Datei `dev/entwicklung.md` (Service `App\Services\ProjectDocumentationContent`)
|
||||
- **Impersonation** für Admins
|
||||
- **CMS/Display-Admin:** getrennte Oberflächen für Flux-CMS und klassische Display-Pflege (siehe Routen oben)
|
||||
|
||||
---
|
||||
|
||||
## Datenbank (Auszug)
|
||||
|
||||
Neben den Kern-Tabellen (User, Partner, Produkte, Hubs, Display-Tabellen, …) u. a.:
|
||||
|
||||
- **`cms_projects`**, **`cms_articles`** – projekt-/artikelbezogene CMS-Daten (App-Models)
|
||||
- Flux-CMS-Paket kann je nach Migration weitere `flux_cms_*`-Tabellen mitbringen – bei Deploy/Tests Migrationen aus Core + App berücksichtigen
|
||||
|
||||
---
|
||||
|
||||
## Dokumentation im Repository
|
||||
|
||||
| Pfad | Inhalt |
|
||||
|------|--------|
|
||||
| `Readme.md` | Projektüberblick |
|
||||
| `CLAUDE.md` | Dev-Container, Befehle, Tests (SQLite) |
|
||||
| `dev/DOMAINS-CONFIG.md` | Domains & `.env` |
|
||||
| `dev/LOCAL-DEVELOPMENT.md` | Lokales Setup |
|
||||
| `dev/PARTNER-SETUP-WIZARD.md` | Partner-Wizard |
|
||||
| `dev/DISPLAY_*.md` | Display/Cabinet |
|
||||
| `dev/THEME-SWITCHING.md` | Themes |
|
||||
| `dev/12-03-2026/tasks.md` | Aktuelle Web-/Immobilien-Briefings |
|
||||
| `packages/flux-cms/ARCHITECTURE.md` | CMS-Architektur |
|
||||
|
||||
*(Es gibt kein `dev/HERO-ICONS-USAGE.md` mehr – Icons: Flux/Heroicons nach Projektstandard.)*
|
||||
|
||||
---
|
||||
|
||||
## Tests & Qualität
|
||||
|
||||
- **Pest**-Feature-Tests u. a. für Auth, Partner, Produkte, CMS-Admin, Display-APIs
|
||||
- Vor Releases sinnvoll: `php artisan test` (bzw. im Container/Sail das Projekt-Äquivalent)
|
||||
- **Laravel Pint** für PHP-Code-Stil
|
||||
|
||||
---
|
||||
|
||||
## Roadmap (Kurz)
|
||||
|
||||
**Kurzfristig**
|
||||
|
||||
- E-Mail-Zustellung / Domain-Reputation stabilisieren
|
||||
- Immobilien- und Teaser-Content gemäß `dev/12-03-2026/tasks.md` weiterziehen
|
||||
- Flux-CMS: bestehende Startseite/Kernseiten vollständig pflegbar machen
|
||||
|
||||
**Mittelfristig**
|
||||
|
||||
- Bestellprozess end-to-end
|
||||
- Provisionslogik
|
||||
- Reporting
|
||||
|
||||
**Langfristig**
|
||||
|
||||
- API-Erweiterungen, ggf. Mobile-Clients über Sanctum
|
||||
- Internationalisierung dort, wo fachlich nötig
|
||||
|
||||
---
|
||||
|
||||
## Projektstatus – Kompakt
|
||||
|
||||
| Bereich | Einordnung |
|
||||
|---------|------------|
|
||||
| Multi-Domain & Themes | stabil |
|
||||
| Auth & Rollen | stabil |
|
||||
| Partner & Wizard | stabil |
|
||||
| Produkte inkl. Kuration | weit fortgeschritten |
|
||||
| Hubs | nutzbar, Ausbau möglich |
|
||||
| Display/Cabinet | stabil |
|
||||
| Flux CMS | **integriert**, Content-Ausbau läuft |
|
||||
| Öffentliche Website | live-tauglich, Schwerpunkt Immobilien/Soft Launch |
|
||||
| Bestellsystem | vorbereitet, nicht abgeschlossen |
|
||||
|
||||
---
|
||||
|
||||
*Diese Datei ist die Referenz für die interne Projekt-Dokumentation (`admin/documentation`) und soll bei größeren Meilensteinen angepasst werden.*
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Projektübersicht
|
||||
|
||||
Das B2IN-Projekt ist eine umfassende Multi-Domain-Laravel-Anwendung, die als zentrale Plattform für Partner-Management, Produkt-Verwaltung und Digital Signage dient. Die Anwendung basiert auf Laravel 12, Livewire 3 und Flux UI und bietet ein modernes, rollenbasiertes System für verschiedene Geschäftspartner.
|
||||
Das B2IN-Projekt ist eine umfassende Multi-Domain-Laravel-Anwendung, die als zentrale Plattform für Partner-Management, Produkt-Verwaltung, Digital Signage und Immobilien-Investment dient. Die Anwendung basiert auf Laravel 12, Livewire 3 und Flux UI und bietet ein modernes, rollenbasiertes System für verschiedene Geschäftspartner. Seit Q1 2026 wurde die Plattform strategisch um den Bereich **Dubai Real Estate Investment** als primären Geschäftszweig erweitert – B2in positioniert sich nun als "International Real Estate + Design Ecosystem".
|
||||
---
|
||||
|
||||
## 🎯 Wichtige ToDos
|
||||
|
|
@ -16,13 +16,15 @@ In der Zukunft benötigen wir einen response bounce etc.
|
|||
|
||||
## 🎯 Kern-Technologien
|
||||
|
||||
- **Framework**: Laravel 12 mit PHP 8.2+
|
||||
- **Framework**: Laravel 12 mit PHP 8.4+
|
||||
- **Frontend**: Livewire 3 mit Volt (Single-File Components)
|
||||
- **UI-Framework**: Flux UI (Pro-Version) mit Tailwind CSS
|
||||
- **UI-Framework**: Flux UI (Pro-Version v2) mit Tailwind CSS v4
|
||||
- **Authentifizierung**: Laravel Fortify mit Sanctum
|
||||
- **Berechtigungen**: Spatie Laravel-Permission
|
||||
- **Mehrsprachigkeit**: Spatie Laravel-Translatable (für CMS-Inhalte)
|
||||
- **Icons**: Heroicons (Blade-Integration)
|
||||
- **Entwicklungsumgebung**: Laravel Sail (Docker)
|
||||
- **Entwicklungsumgebung**: Dev Container (Docker) – Befehle direkt ohne Sail-Prefix
|
||||
- **MCP-Integration**: Laravel Boost MCP-Server für Entwicklungsunterstützung
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -229,9 +231,9 @@ Rollenbasiertes Dashboard mit individuellen KPIs:
|
|||
|
||||
---
|
||||
|
||||
### 7. Digital Signage / Display-CMS
|
||||
### 7. Digital Signage / Display-CMS (Legacy)
|
||||
|
||||
**Status**: ✅ Vollständig implementiert
|
||||
**Status**: ✅ Vollständig implementiert (Legacy-System → siehe auch Abschnitt 14 für Neuarchitektur)
|
||||
|
||||
Professionelles CMS-System für Digital Signage im Cabinet Showroom Bielefeld:
|
||||
|
||||
|
|
@ -309,16 +311,24 @@ Modernes Auth-System basierend auf Laravel Fortify & Sanctum:
|
|||
Moderne, responsive Website mit zahlreichen Sections:
|
||||
|
||||
#### Implementierte Seiten:
|
||||
- **Home** - Hauptseite mit Hero-Slider
|
||||
- **About** - Über uns
|
||||
- **Ecosystem** - Ökosystem-Darstellung
|
||||
- **Partner** - Partner-Informationen
|
||||
- **Home** - Hauptseite mit Hero-Slider (Q1 2026: Repositionierung "Connecting Design and Property")
|
||||
- **About** - Über uns (Q1 2026: Neue Timeline "Die Erweiterung 2025/2026")
|
||||
- **Ecosystem** - Ökosystem-Darstellung (Q1 2026: Drei-Säulen-Modell)
|
||||
- **Partner** - Partner-Informationen (Q1 2026: "Für Immobilienentwickler"-Bereich)
|
||||
- **Portfolio** - Produkt-Portfolio
|
||||
- **Magazin** - Blog/Magazin mit Detail-Ansichten
|
||||
- **Contact** - Kontaktformular
|
||||
- **Magazin** - Blog/Magazin mit Detail-Ansichten (Q1 2026: 5 vollständige Artikel de/en)
|
||||
- **Contact** - Kontaktformular (Q1 2026: Neue Betreff-Optionen Immobilien/Supply-Chain)
|
||||
- **Service** - Service-Seite
|
||||
- **FAQ** - Häufig gestellte Fragen
|
||||
- **FAQ** - Häufig gestellte Fragen (Q1 2026: Immobilien & Supply-Chain Fokus)
|
||||
- **Theme-Demo** - Theme-Vorschau
|
||||
- **Immobilien** *(NEU)* - Dubai Immobilien-Übersicht
|
||||
- **Immobilien Detail** *(NEU)* - Projekt-Einzelansicht mit Investment-Case
|
||||
- **Interior** *(NEU)* - Interior Design / Einrichtungs-Showcase
|
||||
- **Netzwerk** *(NEU)* - Partner-Netzwerk mit Cabinet-Integration
|
||||
- **Impressum** *(NEU)* - Rechtliches Impressum
|
||||
- **Datenschutz** *(NEU)* - Datenschutzerklärung
|
||||
- **AGB** *(NEU)* - Allgemeine Geschäftsbedingungen
|
||||
- **Cookie-Policy** *(NEU)* - Cookie-Richtlinie
|
||||
|
||||
#### Wiederverwendbare Sections:
|
||||
- Hero (Standard, mit Bild, Slider, Tiles)
|
||||
|
|
@ -345,6 +355,9 @@ Moderne, responsive Website mit zahlreichen Sections:
|
|||
- Spotlights-Section
|
||||
- Supplier-Section
|
||||
- Vision-Section
|
||||
- **FounderBar** *(NEU)* - Gründer/CEO-Vorstellung mit Statement
|
||||
- **ImageBreak** *(NEU)* - Wiederverwendbare Bildtrennung (konfigurierbar per Section-Name)
|
||||
- **ImmobilienContactForm** *(NEU)* - Spezialisiertes Kontaktformular für Immobilien
|
||||
|
||||
#### UI-Komponenten:
|
||||
- Header mit Navigation
|
||||
|
|
@ -352,6 +365,8 @@ Moderne, responsive Website mit zahlreichen Sections:
|
|||
- Top-Bar
|
||||
- Kontaktformular
|
||||
- Theme-Switcher (für Demos)
|
||||
- **AnnouncementBar** *(NEU)* - Ankündigungsleiste
|
||||
- **web-picture** *(NEU)* - Blade-Komponente für responsive Bilder
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -397,6 +412,193 @@ Eigenes CMS-Package in `packages/flux-cms/`:
|
|||
- `components/` - Livewire Backend & Frontend Components
|
||||
- `starter-components/` - Vorgefertigte Starter-Components
|
||||
|
||||
#### Neue CMS-Models (seit Q1 2026):
|
||||
- `CmsContent` - Gruppen/Key-basierter Content mit übersetzbaren Werten
|
||||
- `CmsDownload` - Mediendatei-Verwaltung
|
||||
- `CmsFaq` - FAQ mit übersetzbaren Frage-Antwort-Paaren
|
||||
- `CmsIndustry` - Branchen-Taxonomie
|
||||
- `CmsLinkedinPost` - Social-Media-Integration
|
||||
- `CmsMedia` - Media-Library mit Konvertierungs-Tracking
|
||||
- `CmsNewsItem` - News/Artikel
|
||||
- `CmsSearchIndex` - Volltextsuche-Index
|
||||
|
||||
#### Neue Services:
|
||||
- `CmsContentService` - Content-Abruf und -Verwaltung
|
||||
- `HeroiconOutlineList` - Icon-Referenz-Service
|
||||
- `MediaConversionService` - Bild/Video-Konvertierungs-Pipeline
|
||||
|
||||
---
|
||||
|
||||
### 13. Immobilien-Plattform (Dubai Real Estate)
|
||||
|
||||
**Status**: ✅ Soft-Launch implementiert (seit März 2026)
|
||||
|
||||
Vollständige Dubai-Immobilien-Investitionsplattform als neuer Hauptgeschäftszweig:
|
||||
|
||||
#### Seiten:
|
||||
- **Immobilien-Übersicht** (`/immobilien`) - Listing mit Hero, Fakten, Kaufprozess, Trust-Blocks, Mindset-Check
|
||||
- **Projekt-Detail** (`/immobilien/{slug}`) - Einzelansicht mit Galerie, Investment-Case, Trust/Möbel-Vorteile
|
||||
|
||||
#### Datenmodelle:
|
||||
- **CmsProject** - Immobilienprojekte mit mehrsprachigen Feldern (Spatie Translatable)
|
||||
- Slug, Titel, Standort, Beschreibung, Features (JSON)
|
||||
- Preis in AED mit automatischer EUR/USD-Umrechnung
|
||||
- Investor-Trust-Block (Escrow, DLD-Kontrolle, Transparenz)
|
||||
- Möbel-Vorteil-Block (exklusives B2in-Einrichtungsnetzwerk)
|
||||
- **CmsArticle** - Magazin-Artikel mit Mehrsprachigkeit (de/en)
|
||||
- Kategorie, Autor, Lesezeit, Veröffentlichungsstatus
|
||||
- Content als JSON-Struktur (Sections mit Intro, Text, Listen, Zitaten)
|
||||
|
||||
#### Magazin-Artikel (5 vollständige Artikel):
|
||||
1. Escrow-System Dubai – Wie Käufer geschützt werden
|
||||
2. Spotlight: Al Jaddaf – Dubais aufstrebender Kreativdistrikt
|
||||
3. Turnkey-Investments – Schlüsselfertig vom Plan bis zur Einrichtung
|
||||
4. Supply-Chain-Management – Deutsche Qualität für internationale Projekte
|
||||
5. Local-for-Local – Wie B2in lokale Händler mit internationalen Investoren verbindet
|
||||
|
||||
#### PriceHelper:
|
||||
- Statische Preisformatierung AED → EUR/USD
|
||||
- Feste Wechselkurse: AED/USD 3.6725, USD/EUR 1.08
|
||||
- Format: "ab 1.125.000 AED (ca. 284.000 EUR / 306.000 USD)"
|
||||
|
||||
#### Seeders:
|
||||
- `CmsProjectSeeder` - Immobilienprojekte aus Lang-Dateien
|
||||
- `CmsArticleSeeder` - Magazin-Artikel aus Lang-Dateien (de/en)
|
||||
|
||||
---
|
||||
|
||||
### 14. Display-Versions-System (Neuarchitektur)
|
||||
|
||||
**Status**: ✅ Vollständig implementiert (Februar 2026)
|
||||
|
||||
Komplett überarbeitetes Display-Management mit Versions-basiertem Content:
|
||||
|
||||
#### Drei-Tabellen-Architektur:
|
||||
- **Display** - Physische Display-Geräte (Name, Standort, Aktiv-Status)
|
||||
- **DisplayVersion** - Content-Versionen mit Typ-basiertem System
|
||||
- **DisplayVersionItem** - Einzelne Content-Items (Video, Footer, Media, Slides) mit JSON-Content
|
||||
|
||||
#### DisplayVersionType Enum:
|
||||
- `video-display` - Video-Playlists
|
||||
- `b2in` - B2in-Marken-Content
|
||||
- `offers` - Angebots-Slides
|
||||
|
||||
#### API-Endpunkte:
|
||||
- `GET /api/display/config` - Vollständige Playlist-Konfiguration als JSON
|
||||
- `GET /api/display/check` - Lightweight Update-Check (Timestamp + Status)
|
||||
|
||||
#### Legacy-Migration:
|
||||
- `MigrateLegacyDisplays`-Kommando konvertiert alte DisplayVideo/DisplayFooterContent-Daten in neues System
|
||||
|
||||
---
|
||||
|
||||
### 15. Cabinet Tablet-Management
|
||||
|
||||
**Status**: ✅ Vollständig implementiert (Februar/März 2026)
|
||||
|
||||
Verwaltungssystem für Tablet-Displays im Cabinet Showroom Bielefeld:
|
||||
|
||||
#### CabinetTabletSetting (Singleton-Pattern):
|
||||
- Store-Status-Modi: open, notice, closed, warning
|
||||
- Tägliche Override-Zeiten (override_open_today, override_close_today) mit Mitternacht-Auto-Reset
|
||||
- 7-Tage-Öffnungszeiten mit deutschen Labels (Montag–Sonntag)
|
||||
- Nächster Termin (Datum + Uhrzeit)
|
||||
- Kontaktdaten (Telefon, E-Mail)
|
||||
- Status-Berechnung basierend auf Berliner Zeitzone
|
||||
|
||||
#### API-Endpunkte:
|
||||
- `GET /api/cabinet-tablet/status` - Vollständige Settings (Öffnungszeiten, Kontakt, Termine, Hinweise)
|
||||
- `GET /api/cabinet-tablet/check` - Schnell-Poll für Status-Änderungen
|
||||
|
||||
#### Admin-Oberfläche:
|
||||
- `CabinetInfoTablet` - Livewire-Komponente für Öffnungszeiten und Kontaktdaten
|
||||
- `QuickStatus` - Livewire-Komponente für schnelle Status-Änderungen (Auto/Geschlossen/Hinweis/Warnung)
|
||||
- Key-basierte Autorisierung über Query-Parameter
|
||||
|
||||
---
|
||||
|
||||
### 16. Homepage & Brand-Repositionierung
|
||||
|
||||
**Status**: ✅ Umgesetzt (Februar/März 2026)
|
||||
|
||||
Komplette Neuausrichtung der B2in-Marke:
|
||||
|
||||
#### Strategische Änderungen:
|
||||
- **Neuer Hero**: "B2in – Connecting Design and Property" mit dualem Positioning (Immobilien + Einrichtung)
|
||||
- **Founder Bar**: Marcel Scheibe als CEO/Founder mit persönlichem Statement auf allen relevanten Seiten
|
||||
- **Ecosystem-Drei-Säulen-Modell**:
|
||||
1. Internationale Immobilien
|
||||
2. Exklusive Einrichtung
|
||||
3. Supply-Chain-Management
|
||||
- **Partner-Section**: Neuer "Für Immobilienentwickler"-Bereich (Supply-Chain-Fokus)
|
||||
- **FAQ-Update**: 5 Fragen mit Immobilien- und Supply-Chain-Fokus
|
||||
- **Brand Worlds Neuordnung**: Stileigentum → Style2Own → B2A
|
||||
|
||||
#### Aktualisierte Seiten:
|
||||
- Home, About (neue Timeline: "Die Erweiterung 2025/2026"), Ecosystem, Partner
|
||||
- Contact (neue Betreff-Optionen für Immobilien/Supply-Chain)
|
||||
- Magazin (mit Immobilien-Artikeln)
|
||||
|
||||
---
|
||||
|
||||
### 17. Neue Webseiten & Rechtliches
|
||||
|
||||
**Status**: ✅ Implementiert (März 2026)
|
||||
|
||||
#### Neue Seiten:
|
||||
- `/immobilien` - Dubai Immobilien-Plattform
|
||||
- `/immobilien/{slug}` - Projekt-Detailseite
|
||||
- `/interior` - Interior Design / Einrichtungs-Showcase
|
||||
- `/netzwerk` - Partner-Netzwerk (mit Cabinet-Integration)
|
||||
- `/impressum` - Impressum
|
||||
- `/privacy` - Datenschutzerklärung
|
||||
- `/terms` - AGB
|
||||
- `/cookie-policy` - Cookie-Richtlinie
|
||||
|
||||
#### Neue Livewire-Sections:
|
||||
- `FounderBar` - Gründer/CEO-Vorstellung (Marcel Scheibe)
|
||||
- `ImageBreak` - Wiederverwendbare Bildtrennungs-Komponente
|
||||
- `ImmobilienContactForm` - Spezialisiertes Kontaktformular für Immobilien-Anfragen
|
||||
|
||||
---
|
||||
|
||||
### 18. Mehrsprachigkeit & Lokalisierung
|
||||
|
||||
**Status**: ✅ Implementiert (März 2026)
|
||||
|
||||
#### SetLocale-Middleware:
|
||||
- Neue Middleware in `bootstrap/app.php` registriert
|
||||
- Erkennung über `session('locale')`, setzt App-Locale (de/en)
|
||||
- Integriert in Web-Middleware-Gruppe
|
||||
|
||||
#### Sprachdateien:
|
||||
- `resources/lang/de/b2in.php` - Deutsche Übersetzungen (Immobilien, Magazin, UI)
|
||||
- `resources/lang/en/b2in.php` - Englische Übersetzungen
|
||||
- `resources/lang/de/b2in_legal.php` / `en/b2in_legal.php` - Rechtliche Texte
|
||||
- `resources/lang/de/ui.php` / `en/ui.php` - UI-Elemente
|
||||
|
||||
---
|
||||
|
||||
### 19. Artisan-Kommandos
|
||||
|
||||
**Status**: ✅ Implementiert
|
||||
|
||||
#### Neue Kommandos:
|
||||
- **ConvertImagesToWebP** - Batch-Konvertierung JPG/PNG → WebP
|
||||
- Optionen: `--path`, `--quality` (Standard 85%), `--force`, `--dry-run`
|
||||
- Zeigt Kompressions-Statistiken (Dateigrößen-Reduktion %)
|
||||
- **MigrateLegacyDisplays** - Einmalige Migration von altem DisplayVideo/DisplayFooterContent ins neue System
|
||||
- **ResetCabinetTabletOverrides** - Geplante Aufgabe zum Zurücksetzen täglicher Zeitüberschreibungen um Mitternacht
|
||||
|
||||
---
|
||||
|
||||
### 20. Produkt-Kuration
|
||||
|
||||
**Status**: 🔄 Erweitert (Februar 2026)
|
||||
|
||||
- Neue Berechtigung `curate_products` hinzugefügt (Migration `2026_02_27_154145`)
|
||||
- Ermöglicht spezifische Produktkurations-Rechte unabhängig von allgemeinem Produkt-Management
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ Datenbank-Struktur
|
||||
|
|
@ -435,9 +637,32 @@ Eigenes CMS-Package in `packages/flux-cms/`:
|
|||
- `tax_rates` - Steuersätze
|
||||
- `shipping_classes` - Versandklassen
|
||||
|
||||
#### Display/CMS:
|
||||
- `display_videos` - Video-Playlist für Digital Signage
|
||||
- `display_footer_contents` - Footer-Inhalte mit Short-Links & Tracking
|
||||
#### Display/CMS (Legacy):
|
||||
- `display_videos` - Video-Playlist für Digital Signage (Legacy)
|
||||
- `display_footer_contents` - Footer-Inhalte mit Short-Links & Tracking (Legacy)
|
||||
|
||||
#### Display-Versions-System (Neu, Feb 2026):
|
||||
- `display_versions` - Content-Versionen (Name, Typ, Settings JSON, Aktiv-Status)
|
||||
- `display_version_items` - Content-Items (FK, Item-Typ, Content JSON, Sortierung)
|
||||
- `displays` - Physische Display-Geräte (Name, Standort, Aktiv-Status)
|
||||
- `display_display_version` - Pivot-Tabelle (Many-to-Many mit Sortierung)
|
||||
|
||||
#### Cabinet Tablet (Neu, Feb/März 2026):
|
||||
- `cabinet_tablet_settings` - Showroom-Status, Öffnungszeiten (7 Tage), Kontaktdaten, Overrides
|
||||
|
||||
#### CMS-Inhalte (Neu, März 2026):
|
||||
- `cms_projects` - Immobilienprojekte (JSON-Felder für Mehrsprachigkeit, Preis in AED)
|
||||
- `cms_articles` - Magazin-Artikel (JSON für Titel/Untertitel/Content, Kategorie, Autor)
|
||||
|
||||
#### Flux CMS Package (Neu, 2026):
|
||||
- `flux_cms_contents` - Gruppen/Key-basierter Content
|
||||
- `flux_cms_downloads` - Downloads
|
||||
- `flux_cms_linkedin_posts` - LinkedIn-Posts
|
||||
- `flux_cms_faqs` - FAQ-Einträge
|
||||
- `flux_cms_news_items` - News
|
||||
- `flux_cms_industries` - Branchen
|
||||
- `flux_cms_media` - Media-Library
|
||||
- `flux_cms_search_index` - Volltextsuche
|
||||
|
||||
#### System:
|
||||
- `media` - Media-Verwaltung
|
||||
|
|
@ -489,6 +714,11 @@ Umfangreiche Entwicklungs-Dokumentation:
|
|||
- `dev/ENV_VARIABLES_DISPLAY.md` - Display-Umgebungsvariablen
|
||||
- `dev/THEME-SWITCHING.md` - Theme-System
|
||||
- `dev/THEME-DEMO-COMPONENTS.md` - Theme-Demo
|
||||
- `dev/27-02-2026/` - Entwicklungsdokumentation Relaunch Februar 2026
|
||||
- `dev/12-03-2026/` - Entwicklungsdokumentation Immobilien-Launch März 2026
|
||||
- `packages/flux-cms/ARCHITECTURE.md` - CMS-Package Architektur
|
||||
- `packages/flux-cms/MIGRATION.md` - CMS-Migrations-Leitfaden
|
||||
- `packages/flux-cms/SETUP.md` - CMS-Setup-Anleitung
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -536,8 +766,42 @@ composer dev # Startet Server, Queue, Logs & Vite parallel
|
|||
- TestCase-Struktur vorhanden
|
||||
- Browser-Testing mit Laravel Dusk
|
||||
- Feature & Unit Tests vorbereitet
|
||||
- SQLite in-memory für Tests (MySQL wird nicht berührt)
|
||||
- Automatischer Config-Cache-Reset vor jedem Test-Run
|
||||
|
||||
**Hinweis**: Test-Implementierung kann bei Bedarf erweitert werden.
|
||||
### Neue Tests (seit Q1 2026):
|
||||
|
||||
#### Display & Tablet Tests:
|
||||
- `DisplayVersionTest` - Model-Beziehungen, activeItems()
|
||||
- `DisplayVersionApiTest` - API-Config/Check-Endpunkte
|
||||
- `DisplayListTest` - Display CRUD-Operationen
|
||||
- `CabinetInfoTabletTest` - Info-Tablet-Display-Integration
|
||||
- `CabinetTabletApiTest` - API-Endpunkte (Status/Check)
|
||||
- `CabinetQuickStatusTest` - Livewire-Komponenten-Interaktion
|
||||
- `ResetCabinetTabletOverridesTest` - Mitternacht-Reset-Logik
|
||||
|
||||
#### Immobilien/CMS Tests:
|
||||
- `ImmobilienShowTest` - Projekt-Detailseite-Rendering
|
||||
- `CmsLegalSeederTest` - Rechtliche Inhalte (Privacy/Terms/Impressum)
|
||||
- `MagazinPageTest` - Magazin-Seiten
|
||||
- `ContactFormTest` - Kontaktformular
|
||||
|
||||
#### Weitere Tests:
|
||||
- `AboutPageTest` - About-Seite
|
||||
- `AnnouncementBarTest` - Announcement-Bar
|
||||
- `InteriorPageTest` - Interior-Seite
|
||||
- `SoftLaunchDevPagesTest` - Dev-Seiten im Soft-Launch
|
||||
- `PartnerSelfServiceProfileTest` - Partner Self-Service
|
||||
- `CmsFluxEditorHtmlTransformerTest` (Unit) - HTML-Transformation
|
||||
- `CabinetTabletSettingTest` (Unit) - Status-Berechnung, Öffnungszeiten
|
||||
|
||||
#### Factories:
|
||||
- `CabinetTabletSettingFactory`
|
||||
- `CmsArticleFactory` (mehrsprachig)
|
||||
- `CmsProjectFactory` (mehrsprachig)
|
||||
- `DisplayFactory`
|
||||
- `DisplayVersionFactory` (mit Typ)
|
||||
- `DisplayVersionItemFactory`
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -566,7 +830,9 @@ composer dev # Startet Server, Queue, Logs & Vite parallel
|
|||
- `livewire/flux-pro` ^2.6
|
||||
- `livewire/volt` ^1.7.0
|
||||
- `spatie/laravel-permission` ^6.17
|
||||
- `spatie/laravel-translatable` (NEU - für CMS-Inhalte)
|
||||
- `blade-ui-kit/blade-heroicons` ^2.6
|
||||
- `laravel/mcp` ^0.x (NEU - MCP-Server-Integration)
|
||||
|
||||
### Dev-Abhängigkeiten:
|
||||
- `laravel/sail` ^1.41
|
||||
|
|
@ -612,11 +878,21 @@ composer dev # Startet Server, Queue, Logs & Vite parallel
|
|||
- Authentifizierung & Sicherheit
|
||||
- Website-Frontend (Grundstruktur)
|
||||
- Dokumentation
|
||||
- **NEU**: Immobilien-Plattform (Dubai Real Estate) mit Soft-Launch
|
||||
- **NEU**: Display-Versions-System (Neuarchitektur)
|
||||
- **NEU**: Cabinet Tablet-Management
|
||||
- **NEU**: Homepage & Brand-Repositionierung
|
||||
- **NEU**: Mehrsprachigkeit (de/en) mit SetLocale-Middleware
|
||||
- **NEU**: Rechtliche Seiten (Impressum, Datenschutz, AGB, Cookie-Policy)
|
||||
- **NEU**: Magazin mit 5 vollständigen Artikeln (de/en)
|
||||
- **NEU**: Interior- und Netzwerk-Seiten
|
||||
- **NEU**: WebP-Konvertierungs-Tool
|
||||
- **NEU**: Umfangreiche Test-Suite mit Factories
|
||||
|
||||
### 🔄 In Arbeit:
|
||||
- Flux-CMS-Package (Architektur steht)
|
||||
- Erweiterte Produkt-Features
|
||||
- Test-Abdeckung
|
||||
- Flux-CMS-Package (Architektur steht, neue Models und Services hinzugefügt)
|
||||
- Erweiterte Produkt-Features + Produkt-Kuration
|
||||
- Weitere Immobilienprojekte (aktuell: Azizi Creek Views 4)
|
||||
|
||||
### 📋 Bereit für Umsetzung:
|
||||
- Bestellsystem
|
||||
|
|
@ -629,11 +905,13 @@ composer dev # Startet Server, Queue, Logs & Vite parallel
|
|||
## 💡 Besonderheiten & Highlights
|
||||
|
||||
### Technische Exzellenz:
|
||||
- **Modern Stack** - Neueste Laravel/Livewire-Versionen
|
||||
- **Modern Stack** - Neueste Laravel 12 / Livewire 3 / PHP 8.4+ / Tailwind CSS v4
|
||||
- **Component-Architecture** - Wiederverwendbare Livewire-Komponenten
|
||||
- **Type-Safety** - PHP 8.2+ Features
|
||||
- **Performance** - Optimierte Queries, Caching
|
||||
- **Type-Safety** - PHP 8.4+ Features inkl. Enums
|
||||
- **Performance** - Optimierte Queries, Caching, WebP-Bildkonvertierung
|
||||
- **Skalierbarkeit** - Modularer Aufbau für Wachstum
|
||||
- **API-First Display-System** - JSON-basierte Konfiguration für Signage-Geräte
|
||||
- **Mehrsprachigkeit** - DE/EN mit Spatie Translatable und Laravel-Lang-Dateien
|
||||
|
||||
### Business-Features:
|
||||
- **Multi-Tenant-Ready** - Verschiedene Partner-Typen
|
||||
|
|
@ -641,6 +919,9 @@ composer dev # Startet Server, Queue, Logs & Vite parallel
|
|||
- **Flexibles Provisionsmodell** - Prozentsatz oder Festbetrag
|
||||
- **Umfangreiches Tracking** - Display-Klicks, User-Aktivitäten
|
||||
- **White-Label-Fähigkeit** - Domain-spezifisches Branding
|
||||
- **Immobilien-Investment** - Vollständige Dubai Real Estate-Plattform mit Preisumrechnung
|
||||
- **Content-Management** - Magazin-Artikel, Projekte, rechtliche Inhalte
|
||||
- **Showroom-Management** - Cabinet Tablet mit Öffnungszeiten und Live-Status
|
||||
|
||||
### User-Experience:
|
||||
- **Intuitives Onboarding** - Setup-Wizard für neue Partner
|
||||
|
|
@ -648,6 +929,8 @@ composer dev # Startet Server, Queue, Logs & Vite parallel
|
|||
- **Real-time Updates** - Livewire für nahtlose Interaktion
|
||||
- **Responsive Design** - Funktioniert auf allen Geräten
|
||||
- **Dark Mode** - Moderne UI-Präferenzen
|
||||
- **Founder-Vertrauen** - CEO-Positioning auf relevanten Seiten
|
||||
- **Trust-Building** - Escrow, DLD-Kontrolle, Investoren-Transparenz
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -664,13 +947,14 @@ Das System ist produktionsbereit und kann bei Bedarf mit folgenden Services erwe
|
|||
|
||||
---
|
||||
|
||||
**Entwicklungsstand**: Dezember 2025
|
||||
**Version**: 1.0 (Production-Ready)
|
||||
**Framework**: Laravel 12
|
||||
**PHP**: 8.2+
|
||||
**Entwicklungsstand**: März 2026
|
||||
**Version**: 2.0
|
||||
**Framework**: Laravel 12
|
||||
**PHP**: 8.4+
|
||||
**Lizenz**: MIT
|
||||
|
||||
---
|
||||
|
||||
*Diese Dokumentation wurde automatisch generiert und gibt den aktuellen Stand der Entwicklung wieder.*
|
||||
*Diese Dokumentation wird fortlaufend aktualisiert und gibt den aktuellen Stand der Entwicklung wieder.*
|
||||
*Letzte Aktualisierung: März 2026*
|
||||
|
||||
|
|
|
|||
458
dev/file-upload/README.md
Normal file
458
dev/file-upload/README.md
Normal file
|
|
@ -0,0 +1,458 @@
|
|||
# File Upload mit Livewire Volt + Flux UI
|
||||
|
||||
Vollständige Referenz für den Bild-Upload, wie er in `resources/views/livewire/products/form-teaser.blade.php` implementiert ist.
|
||||
|
||||
---
|
||||
|
||||
## Überblick
|
||||
|
||||
Der Upload nutzt:
|
||||
- **Livewire `WithFileUploads`** – verwaltet temporäre Uploads via signierter URL
|
||||
- **Flux UI `flux:file-upload`** – UI-Komponente (Dropzone + Vorschau)
|
||||
- **Laravel `Storage::disk('public')`** – Permanente Speicherung
|
||||
- **Polymorphe `media`-Tabelle** – Zuordnung von Dateien zu beliebigen Models
|
||||
- **Alpine.js** – Drag-&-Drop-Sortierung der vorhandenen Bilder
|
||||
|
||||
---
|
||||
|
||||
## 1. PHP / Livewire Volt – Komponentenlogik
|
||||
|
||||
### Trait einbinden
|
||||
|
||||
```php
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
new class extends Component {
|
||||
use WithFileUploads;
|
||||
|
||||
/** @var array<\Livewire\Features\SupportFileUploads\TemporaryUploadedFile> */
|
||||
public array $mainImages = [];
|
||||
```
|
||||
|
||||
`WithFileUploads` muss zwingend eingebunden sein. Ohne ihn reagiert `wire:model` nicht auf Datei-Inputs.
|
||||
|
||||
### Validierung
|
||||
|
||||
```php
|
||||
'mainImages' => 'nullable|array|min:0|max:10',
|
||||
'mainImages.*' => 'mimes:jpeg,jpg,png|max:10240',
|
||||
```
|
||||
|
||||
- `mainImages` ist ein **Array** (wegen `multiple`-Upload)
|
||||
- `mainImages.*` validiert jede einzelne Datei
|
||||
- `max:10240` = 10 MB in Kilobyte
|
||||
- Livewire hat intern ein Default-Limit von 12 MB – das greift, bevor die eigene Validierung läuft (Achtung: Wert muss ≤ 12288 KB sein oder `livewire.temporary_file_upload.rules` anpassen)
|
||||
|
||||
### Einzelnes Bild entfernen (Vorschauliste)
|
||||
|
||||
```php
|
||||
public function removePhoto(int $index): void
|
||||
{
|
||||
if (isset($this->mainImages[$index])) {
|
||||
unset($this->mainImages[$index]);
|
||||
$this->mainImages = array_values($this->mainImages);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Nach `unset()` unbedingt `array_values()` aufrufen, damit die Array-Indizes wieder bei 0 beginnen – sonst bricht `@foreach` mit `$index` im Template.
|
||||
|
||||
### Vorhandenes Bild aus der DB löschen
|
||||
|
||||
```php
|
||||
public function removeExistingMedia(int $mediaId): void
|
||||
{
|
||||
$media = $this->product->media()->find($mediaId);
|
||||
if ($media) {
|
||||
Storage::disk('public')->delete($media->file_path);
|
||||
$media->delete();
|
||||
$this->existingMedia = collect($this->existingMedia)
|
||||
->reject(fn ($m) => $m['id'] === $mediaId)
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Immer erst die **Datei** vom Disk löschen, dann den **DB-Eintrag**. Anschließend `$this->existingMedia` synchronisieren, damit Livewire den State neu rendert.
|
||||
|
||||
### Reihenfolge aktualisieren (Drag & Drop)
|
||||
|
||||
```php
|
||||
public function updateMediaOrder(array $orderedIds): void
|
||||
{
|
||||
foreach ($orderedIds as $position => $mediaId) {
|
||||
$this->product->media()
|
||||
->where('id', $mediaId)
|
||||
->update(['order_column' => $position + 1]);
|
||||
}
|
||||
|
||||
// Lokalen State synchronisieren
|
||||
$this->existingMedia = collect($orderedIds)
|
||||
->map(fn ($id, $index) => collect($this->existingMedia)->firstWhere('id', $id)
|
||||
? array_merge(collect($this->existingMedia)->firstWhere('id', $id), ['order_column' => $index + 1])
|
||||
: null
|
||||
)
|
||||
->filter()
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
```
|
||||
|
||||
### Bilder permanent speichern (Neu-Anlage)
|
||||
|
||||
```php
|
||||
$index = 1;
|
||||
foreach ($this->mainImages as $image) {
|
||||
$path = $image->store('products/' . $product->id, 'public');
|
||||
$product->media()->create([
|
||||
'file_path' => $path,
|
||||
'type' => 'image',
|
||||
'alt_text' => $this->name,
|
||||
'order_column' => $index++,
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
`$image->store(...)` verschiebt die temporäre Livewire-Datei in den endgültigen Pfad auf dem `public`-Disk.
|
||||
|
||||
### Bilder permanent speichern (Bearbeiten – neue Bilder hinzufügen)
|
||||
|
||||
```php
|
||||
$maxOrder = $this->product->media()->max('order_column') ?? 0;
|
||||
$index = $maxOrder + 1;
|
||||
foreach ($this->mainImages as $image) {
|
||||
$path = $image->store('products/' . $this->product->id, 'public');
|
||||
$this->product->media()->create([
|
||||
'file_path' => $path,
|
||||
'type' => 'image',
|
||||
'alt_text' => $this->name,
|
||||
'order_column' => $index++,
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
`order_column` an bestehenden Max-Wert anhängen, nicht von 1 neu beginnen.
|
||||
|
||||
### Nach dem Speichern zurücksetzen
|
||||
|
||||
```php
|
||||
// Neue Bilder leeren
|
||||
$this->mainImages = [];
|
||||
|
||||
// Vorhandene Bilder aus DB neu laden (mit sortBy)
|
||||
$this->existingMedia = $this->product->fresh()->media
|
||||
->sortBy('order_column')
|
||||
->values()
|
||||
->map(fn ($m) => [
|
||||
'id' => $m->id,
|
||||
'file_path' => $m->file_path,
|
||||
'alt_text' => $m->alt_text,
|
||||
'order_column' => $m->order_column,
|
||||
])
|
||||
->toArray();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Blade / Flux UI – Template
|
||||
|
||||
### Upload-Dropzone
|
||||
|
||||
```blade
|
||||
<flux:file-upload wire:model="mainImages" label="Upload files" multiple
|
||||
accept="image/jpeg,image/png,.jpg,.jpeg,.png">
|
||||
<flux:file-upload.dropzone
|
||||
heading="Bilder hochladen"
|
||||
text="Nur JPEG oder PNG – max. 10 MB"
|
||||
with-progress />
|
||||
</flux:file-upload>
|
||||
```
|
||||
|
||||
- `wire:model="mainImages"` – bindet an das Array-Property
|
||||
- `multiple` – erlaubt Mehrfachauswahl
|
||||
- `accept` – schränkt den Browser-Dateidialog ein (kein serverseitiger Schutz!)
|
||||
- `with-progress` – zeigt Upload-Fortschrittsbalken
|
||||
|
||||
### Vorschauliste der neu hinzugefügten Bilder
|
||||
|
||||
```blade
|
||||
@if (isset($mainImages) && count($mainImages) > 0)
|
||||
<div class="mt-4 flex flex-wrap items-center gap-3">
|
||||
@foreach ($mainImages as $index => $image)
|
||||
<flux:file-item
|
||||
:heading="$image->getClientOriginalName()"
|
||||
:image="(str_starts_with($image->getMimeType() ?? '', 'image/') && $image->isPreviewable())
|
||||
? $image->temporaryUrl()
|
||||
: null"
|
||||
:size="$image->getSize()">
|
||||
<x-slot name="actions">
|
||||
<flux:file-item.remove
|
||||
wire:click="removePhoto({{ $index }})"
|
||||
aria-label="{{ 'Remove file: ' . $image->getClientOriginalName() }}" />
|
||||
</x-slot>
|
||||
</flux:file-item>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
```
|
||||
|
||||
**Wichtig bei `temporaryUrl()`:**
|
||||
Die Methode gibt nur dann eine URL zurück, wenn die Datei auch vorschaubar ist (`isPreviewable()` prüft die MIME-Type-Whitelist in `config/livewire.php`). Immer beide Bedingungen prüfen, sonst Fehler.
|
||||
|
||||
### Fehleranzeige
|
||||
|
||||
```blade
|
||||
<flux:error name="mainImages" />
|
||||
```
|
||||
|
||||
Zeigt Validierungsfehler für das gesamte Array an (z. B. „Maximal 10 Bilder erlaubt.").
|
||||
Für Fehler auf einzelnen Dateien würde `name="mainImages.0"` etc. verwendet.
|
||||
|
||||
### Drag-&-Drop-Sortierung vorhandener Bilder
|
||||
|
||||
```blade
|
||||
<div x-data="{
|
||||
dragging: null,
|
||||
dragOver: null,
|
||||
items: @js(collect($existingMedia)->pluck('id')->toArray()),
|
||||
onDragStart(e, id) {
|
||||
this.dragging = id;
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', id);
|
||||
},
|
||||
onDragOver(e, id) {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
this.dragOver = id;
|
||||
},
|
||||
onDrop(e, targetId) {
|
||||
e.preventDefault();
|
||||
if (this.dragging === targetId) { this.dragOver = null; return; }
|
||||
const fromIdx = this.items.indexOf(this.dragging);
|
||||
const toIdx = this.items.indexOf(targetId);
|
||||
this.items.splice(fromIdx, 1);
|
||||
this.items.splice(toIdx, 0, this.dragging);
|
||||
this.dragging = null;
|
||||
this.dragOver = null;
|
||||
$wire.updateMediaOrder(this.items);
|
||||
},
|
||||
onDragEnd() {
|
||||
this.dragging = null;
|
||||
this.dragOver = null;
|
||||
}
|
||||
}" class="flex flex-wrap items-start gap-3">
|
||||
|
||||
@foreach ($existingMedia as $mediaIndex => $media)
|
||||
<div wire:key="existing-media-{{ $media['id'] }}"
|
||||
draggable="true"
|
||||
x-on:dragstart="onDragStart($event, {{ $media['id'] }})"
|
||||
x-on:dragover="onDragOver($event, {{ $media['id'] }})"
|
||||
x-on:drop="onDrop($event, {{ $media['id'] }})"
|
||||
x-on:dragend="onDragEnd()"
|
||||
:class="{
|
||||
'opacity-50 scale-95': dragging === {{ $media['id'] }},
|
||||
'ring-2 ring-blue-400 ring-offset-2': dragOver === {{ $media['id'] }} && dragging !== {{ $media['id'] }}
|
||||
}"
|
||||
class="group relative cursor-grab active:cursor-grabbing transition-all duration-150">
|
||||
|
||||
@if ($mediaIndex === 0)
|
||||
<div class="absolute -top-1 -left-1 z-10 bg-blue-600 text-white text-[10px] font-bold px-1.5 py-0.5 rounded-md shadow">
|
||||
Standard
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<img src="{{ Storage::url($media['file_path']) }}"
|
||||
alt="{{ $media['alt_text'] ?? '' }}"
|
||||
class="h-24 w-24 rounded-lg object-cover border border-zinc-200 dark:border-zinc-700
|
||||
{{ $mediaIndex === 0 ? 'ring-2 ring-blue-500' : '' }}" />
|
||||
|
||||
<flux:button
|
||||
wire:click="removeExistingMedia({{ $media['id'] }})"
|
||||
wire:confirm="Bild wirklich löschen?"
|
||||
variant="filled" size="xs" icon="trash"
|
||||
class="absolute -top-2 -right-2 !bg-red-500 !text-white hover:!bg-red-600" />
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
```
|
||||
|
||||
**Wichtig:** `wire:key="existing-media-{{ $media['id'] }}"` ist zwingend, damit Livewire beim Re-Render die DOM-Elemente korrekt zuordnet.
|
||||
|
||||
---
|
||||
|
||||
## 3. Datenbank – Media-Tabelle
|
||||
|
||||
```php
|
||||
// Migration: database/migrations/xxxx_create_media_table.php
|
||||
|
||||
Schema::create('media', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('model_type'); // z. B. "App\Models\Product"
|
||||
$table->unsignedBigInteger('model_id');
|
||||
$table->string('file_path'); // relativer Pfad auf dem public-Disk
|
||||
$table->string('type')->default('image'); // 'image', 'video', 'pdf', '3d_model'
|
||||
$table->string('alt_text')->nullable();
|
||||
$table->integer('order_column')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['model_type', 'model_id']);
|
||||
});
|
||||
```
|
||||
|
||||
### Media-Model (`app/Models/Media.php`)
|
||||
|
||||
```php
|
||||
class Media extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'model_type', 'model_id', 'file_path', 'type', 'alt_text', 'order_column',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['order_column' => 'integer'];
|
||||
}
|
||||
|
||||
/** Polymorphe Beziehung zum Eltern-Model */
|
||||
public function model(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Beziehung im Parent-Model (`app/Models/Product.php`)
|
||||
|
||||
```php
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
|
||||
public function media(): MorphMany
|
||||
{
|
||||
return $this->morphMany(Media::class, 'model');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Filesystem-Konfiguration (`config/filesystems.php`)
|
||||
|
||||
```php
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => env('APP_URL') . '/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
],
|
||||
```
|
||||
|
||||
### Symlink anlegen
|
||||
|
||||
```bash
|
||||
php artisan storage:link
|
||||
```
|
||||
|
||||
Erstellt `public/storage` → `storage/app/public`. **Ohne diesen Symlink sind hochgeladene Bilder nicht über den Browser erreichbar.**
|
||||
|
||||
---
|
||||
|
||||
## 5. Kritische System-Anpassungen
|
||||
|
||||
### 5a. `bootstrap/app.php` – Reverse-Proxy / HTTPS
|
||||
|
||||
```php
|
||||
->withMiddleware(function (Middleware $middleware) {
|
||||
// Traefik/nginx Proxy: X-Forwarded-Proto-Header vertrauen
|
||||
// ZWINGEND für korrekte signierte Upload-URLs hinter einem HTTPS-Proxy
|
||||
$middleware->trustProxies(at: '*');
|
||||
})
|
||||
```
|
||||
|
||||
**Warum?**
|
||||
Livewire generiert für temporäre Uploads **signierte URLs**. Wenn die App hinter einem Reverse-Proxy (Traefik, nginx, Load Balancer) läuft und der Proxy HTTPS terminiert, glaubt Laravel intern, es sei HTTP. Die signierte URL wird dann mit `http://` generiert, der Browser sendet aber `https://` – die Signatur stimmt nicht, Upload schlägt fehl mit `403`.
|
||||
|
||||
### 5b. `app/Providers/AppServiceProvider.php` – Schema erzwingen
|
||||
|
||||
```php
|
||||
public function boot(): void
|
||||
{
|
||||
// X-Forwarded-Proto auswerten und Schema erzwingen
|
||||
// Nötig für Livewire Upload-URLs hinter Traefik
|
||||
$scheme = request()->header('X-Forwarded-Proto')
|
||||
?? request()->server('HTTP_X_FORWARDED_PROTO')
|
||||
?? (request()->secure() ? 'https' : 'http');
|
||||
|
||||
if ($scheme === 'https') {
|
||||
URL::forceScheme('https');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Warum zusätzlich zum `trustProxies`?**
|
||||
`trustProxies` reicht in manchen Proxy-Setups nicht aus, wenn der Header-Name variiert. `URL::forceScheme('https')` ist die sichere Ergänzung, die sicherstellt, dass alle generierten URLs das korrekte Schema haben.
|
||||
|
||||
**Ohne diese beiden Maßnahmen** scheitert der Upload mit einer `403 Signature mismatch`-Fehlermeldung in der Browser-Console – besonders frustrierend, weil kein PHP-Fehler erscheint.
|
||||
|
||||
---
|
||||
|
||||
## 6. Livewire-Konfiguration (`config/livewire.php`)
|
||||
|
||||
```php
|
||||
'temporary_file_upload' => [
|
||||
'disk' => null, // null = default-Disk (meist 'local')
|
||||
'rules' => null, // null = ['required', 'file', 'max:12288'] (12 MB Default)
|
||||
'directory' => null, // null = 'livewire-tmp'
|
||||
'middleware' => null, // null = 'throttle:60,1'
|
||||
'preview_mimes' => [ // Diese MIME-Types erlauben temporaryUrl()
|
||||
'png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 'webp',
|
||||
'pdf', 'mp4', 'mov', 'avi', 'wmv', 'mp3', ...
|
||||
],
|
||||
'max_upload_time' => 5, // Minuten bis Upload ungültig wird
|
||||
'cleanup' => true, // Tmp-Dateien > 24h automatisch löschen
|
||||
],
|
||||
```
|
||||
|
||||
**Wichtig:** Das interne Default-Limit ist **12 MB** (`max:12288`). Eigene Validierungsregeln wie `max:10240` müssen immer unter diesem Wert liegen. Soll das Limit erhöht werden, muss `rules` hier überschrieben werden.
|
||||
|
||||
---
|
||||
|
||||
## 7. Checkliste für ein neues Projekt
|
||||
|
||||
| Schritt | Was | Wo |
|
||||
|---------|-----|----|
|
||||
| ✅ | `use WithFileUploads` im Volt/Livewire-Component | Komponentenklasse |
|
||||
| ✅ | `public array $images = []` Property anlegen | Komponentenklasse |
|
||||
| ✅ | `'images.*' => 'mimes:jpeg,png\|max:10240'` Validierung | `save()`-Methode |
|
||||
| ✅ | `$image->store('pfad', 'public')` beim Speichern | `save()`-Methode |
|
||||
| ✅ | `$this->images = []` nach dem Speichern leeren | `save()`-Methode |
|
||||
| ✅ | `php artisan storage:link` ausführen | Terminal / Deploy |
|
||||
| ✅ | `$middleware->trustProxies(at: '*')` | `bootstrap/app.php` |
|
||||
| ✅ | `URL::forceScheme('https')` bei HTTPS-Proxy | `AppServiceProvider.php` |
|
||||
| ✅ | `wire:key` in Foreach-Schleifen | Blade-Template |
|
||||
| ✅ | `array_values()` nach `unset()` auf dem Array | `removePhoto()` |
|
||||
| ✅ | `isPreviewable()` vor `temporaryUrl()` prüfen | Blade-Template |
|
||||
|
||||
---
|
||||
|
||||
## 8. Häufige Fallstricke
|
||||
|
||||
### Upload schlägt fehl mit 403 (Signature mismatch)
|
||||
→ Reverse-Proxy-Problem. Siehe Punkt 5a und 5b.
|
||||
|
||||
### Vorschau-Thumbnail zeigt nichts an
|
||||
→ `isPreviewable()` gibt `false` zurück, wenn der MIME-Type nicht in `preview_mimes` steht. In der Livewire-Config prüfen.
|
||||
|
||||
### Nach `removePhoto()` stimmen die Indizes nicht
|
||||
→ `array_values()` vergessen. Livewire sendet den Index als Parameter – ohne Reindizierung kommt es zu Off-by-One-Fehlern.
|
||||
|
||||
### Upload-Limit-Fehler vor der Validierung
|
||||
→ PHP `upload_max_filesize` und `post_max_size` in `php.ini` überprüfen. Auch Livewires internes `max:12288`-Limit beachten.
|
||||
|
||||
### `temporaryUrl()` wirft eine Exception
|
||||
→ Bei lokalen Disks ohne `serve: true` in `filesystems.php` funktioniert `temporaryUrl()` nicht. Entweder `serve: true` setzen oder S3 verwenden. Im Template immer mit `isPreviewable()` absichern.
|
||||
|
||||
### Bilder nach Deploy nicht sichtbar
|
||||
→ `php artisan storage:link` auf dem Produktionssystem ausführen. Im Docker-Container nach jedem `down/up` prüfen, ob der Symlink noch existiert.
|
||||
302
dev/file-upload/cms/CabinetDisplay.php
Normal file
302
dev/file-upload/cms/CabinetDisplay.php
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use App\Models\DisplayFooterContent;
|
||||
use App\Models\DisplayVideo;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Livewire\Component;
|
||||
|
||||
class CabinetDisplay extends Component
|
||||
{
|
||||
// Video-Verwaltung
|
||||
public $videoId = null;
|
||||
|
||||
public $videoFilename = '';
|
||||
|
||||
public $videoTitle = '';
|
||||
|
||||
public $videoPosition = 25;
|
||||
|
||||
public $videoIsActive = true;
|
||||
|
||||
public $showVideoModal = false;
|
||||
|
||||
public $availableVideos = [];
|
||||
|
||||
// Footer-Content-Verwaltung
|
||||
public $footerId = null;
|
||||
|
||||
public $footerHeadline = '';
|
||||
|
||||
public $footerSubline = '';
|
||||
|
||||
public $footerUrl = '';
|
||||
|
||||
public $footerIsActive = true;
|
||||
|
||||
public $showFooterModal = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->loadAvailableVideos();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt alle verfügbaren Video-Dateien aus dem assets-Ordner
|
||||
*/
|
||||
public function loadAvailableVideos()
|
||||
{
|
||||
$assetsPath = public_path('_cabinet/assets');
|
||||
|
||||
if (File::exists($assetsPath)) {
|
||||
$files = File::files($assetsPath);
|
||||
$this->availableVideos = collect($files)
|
||||
->map(fn ($file) => $file->getFilename())
|
||||
->filter(fn ($filename) => in_array(pathinfo($filename, PATHINFO_EXTENSION), ['mp4', 'webm', 'mov']))
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// VIDEO-VERWALTUNG
|
||||
// ========================================
|
||||
|
||||
public function openVideoModal($id = null)
|
||||
{
|
||||
if ($id) {
|
||||
$video = DisplayVideo::findOrFail($id);
|
||||
$this->videoId = $video->id;
|
||||
$this->videoFilename = $video->filename;
|
||||
$this->videoTitle = $video->title ?? '';
|
||||
$this->videoPosition = $video->position;
|
||||
$this->videoIsActive = $video->is_active;
|
||||
} else {
|
||||
$this->resetVideoForm();
|
||||
}
|
||||
$this->showVideoModal = true;
|
||||
}
|
||||
|
||||
public function saveVideo()
|
||||
{
|
||||
$this->validate([
|
||||
'videoFilename' => 'required|string',
|
||||
'videoPosition' => 'required|integer|min:0|max:100',
|
||||
], [
|
||||
'videoFilename.required' => 'Bitte wählen Sie ein Video aus.',
|
||||
'videoPosition.required' => 'Die Position ist erforderlich.',
|
||||
'videoPosition.min' => 'Die Position muss zwischen 0 und 100 liegen.',
|
||||
'videoPosition.max' => 'Die Position muss zwischen 0 und 100 liegen.',
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'filename' => $this->videoFilename,
|
||||
'title' => $this->videoTitle,
|
||||
'position' => $this->videoPosition,
|
||||
'is_active' => $this->videoIsActive,
|
||||
];
|
||||
|
||||
if ($this->videoId) {
|
||||
$video = DisplayVideo::findOrFail($this->videoId);
|
||||
$video->update($data);
|
||||
session()->flash('success', 'Video erfolgreich aktualisiert!');
|
||||
} else {
|
||||
$maxSortOrder = DisplayVideo::max('sort_order') ?? 0;
|
||||
$data['sort_order'] = $maxSortOrder + 1;
|
||||
DisplayVideo::create($data);
|
||||
session()->flash('success', 'Video erfolgreich hinzugefügt!');
|
||||
}
|
||||
|
||||
$this->closeVideoModal();
|
||||
}
|
||||
|
||||
public function deleteVideo($id)
|
||||
{
|
||||
DisplayVideo::findOrFail($id)->delete();
|
||||
session()->flash('success', 'Video erfolgreich gelöscht!');
|
||||
}
|
||||
|
||||
public function toggleVideoStatus($id)
|
||||
{
|
||||
$video = DisplayVideo::findOrFail($id);
|
||||
$video->update(['is_active' => ! $video->is_active]);
|
||||
}
|
||||
|
||||
public function moveVideo($id, $direction)
|
||||
{
|
||||
$video = DisplayVideo::findOrFail($id);
|
||||
$currentOrder = $video->sort_order;
|
||||
|
||||
if ($direction === 'up' && $currentOrder > 0) {
|
||||
$swapVideo = DisplayVideo::where('sort_order', $currentOrder - 1)->first();
|
||||
if ($swapVideo) {
|
||||
$video->update(['sort_order' => $currentOrder - 1]);
|
||||
$swapVideo->update(['sort_order' => $currentOrder]);
|
||||
}
|
||||
} elseif ($direction === 'down') {
|
||||
$swapVideo = DisplayVideo::where('sort_order', $currentOrder + 1)->first();
|
||||
if ($swapVideo) {
|
||||
$video->update(['sort_order' => $currentOrder + 1]);
|
||||
$swapVideo->update(['sort_order' => $currentOrder]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function resetVideoForm()
|
||||
{
|
||||
$this->videoId = null;
|
||||
$this->videoFilename = '';
|
||||
$this->videoTitle = '';
|
||||
$this->videoPosition = 25;
|
||||
$this->videoIsActive = true;
|
||||
}
|
||||
|
||||
public function closeVideoModal()
|
||||
{
|
||||
$this->showVideoModal = false;
|
||||
$this->resetVideoForm();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// FOOTER-CONTENT-VERWALTUNG
|
||||
// ========================================
|
||||
|
||||
public function openFooterModal($id = null)
|
||||
{
|
||||
if ($id) {
|
||||
$footer = DisplayFooterContent::findOrFail($id);
|
||||
$this->footerId = $footer->id;
|
||||
$this->footerHeadline = $footer->headline;
|
||||
$this->footerSubline = $footer->subline;
|
||||
$this->footerUrl = $footer->url;
|
||||
$this->footerIsActive = $footer->is_active;
|
||||
} else {
|
||||
$this->resetFooterForm();
|
||||
}
|
||||
$this->showFooterModal = true;
|
||||
}
|
||||
|
||||
public function saveFooter()
|
||||
{
|
||||
$this->validate([
|
||||
'footerHeadline' => 'required|string|max:255',
|
||||
'footerSubline' => 'required|string|max:255',
|
||||
'footerUrl' => 'nullable|url',
|
||||
], [
|
||||
'footerHeadline.required' => 'Die Überschrift ist erforderlich.',
|
||||
'footerSubline.required' => 'Die Unterzeile ist erforderlich.',
|
||||
'footerUrl.url' => 'Bitte geben Sie eine gültige URL ein.',
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'headline' => $this->footerHeadline,
|
||||
'subline' => $this->footerSubline,
|
||||
'url' => $this->footerUrl ?: null,
|
||||
'is_active' => $this->footerIsActive,
|
||||
];
|
||||
|
||||
if ($this->footerId) {
|
||||
$footer = DisplayFooterContent::findOrFail($this->footerId);
|
||||
$footer->update($data);
|
||||
|
||||
// Short-Code generieren falls URL vorhanden aber noch kein Short-Code
|
||||
if ($footer->url && ! $footer->short_code) {
|
||||
$footer->short_code = DisplayFooterContent::generateUniqueShortCode();
|
||||
$footer->save();
|
||||
}
|
||||
|
||||
session()->flash('success', 'Footer-Inhalt erfolgreich aktualisiert!');
|
||||
} else {
|
||||
$maxSortOrder = DisplayFooterContent::max('sort_order') ?? 0;
|
||||
$data['sort_order'] = $maxSortOrder + 1;
|
||||
|
||||
$footer = DisplayFooterContent::create($data);
|
||||
|
||||
// Short-Code nur generieren wenn URL vorhanden
|
||||
if ($footer->url) {
|
||||
$footer->short_code = DisplayFooterContent::generateUniqueShortCode();
|
||||
$footer->save();
|
||||
session()->flash('success', 'Footer-Inhalt erfolgreich hinzugefügt! Short-Link: '.$footer->short_url);
|
||||
} else {
|
||||
session()->flash('success', 'Footer-Inhalt erfolgreich hinzugefügt! (Ohne QR-Code)');
|
||||
}
|
||||
}
|
||||
|
||||
$this->closeFooterModal();
|
||||
}
|
||||
|
||||
public function regenerateShortCode($id)
|
||||
{
|
||||
$footer = DisplayFooterContent::findOrFail($id);
|
||||
$footer->short_code = DisplayFooterContent::generateUniqueShortCode();
|
||||
$footer->save();
|
||||
session()->flash('success', 'Short-Code wurde neu generiert!');
|
||||
}
|
||||
|
||||
public function resetClicks($id)
|
||||
{
|
||||
$footer = DisplayFooterContent::findOrFail($id);
|
||||
$footer->clicks = 0;
|
||||
$footer->save();
|
||||
session()->flash('success', 'Klick-Zähler wurde zurückgesetzt!');
|
||||
}
|
||||
|
||||
public function deleteFooter($id)
|
||||
{
|
||||
DisplayFooterContent::findOrFail($id)->delete();
|
||||
session()->flash('success', 'Footer-Inhalt erfolgreich gelöscht!');
|
||||
}
|
||||
|
||||
public function toggleFooterStatus($id)
|
||||
{
|
||||
$footer = DisplayFooterContent::findOrFail($id);
|
||||
$footer->update(['is_active' => ! $footer->is_active]);
|
||||
}
|
||||
|
||||
public function moveFooter($id, $direction)
|
||||
{
|
||||
$footer = DisplayFooterContent::findOrFail($id);
|
||||
$currentOrder = $footer->sort_order;
|
||||
|
||||
if ($direction === 'up' && $currentOrder > 0) {
|
||||
$swapFooter = DisplayFooterContent::where('sort_order', $currentOrder - 1)->first();
|
||||
if ($swapFooter) {
|
||||
$footer->update(['sort_order' => $currentOrder - 1]);
|
||||
$swapFooter->update(['sort_order' => $currentOrder]);
|
||||
}
|
||||
} elseif ($direction === 'down') {
|
||||
$swapFooter = DisplayFooterContent::where('sort_order', $currentOrder + 1)->first();
|
||||
if ($swapFooter) {
|
||||
$footer->update(['sort_order' => $currentOrder + 1]);
|
||||
$swapFooter->update(['sort_order' => $currentOrder]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function resetFooterForm()
|
||||
{
|
||||
$this->footerId = null;
|
||||
$this->footerHeadline = '';
|
||||
$this->footerSubline = '';
|
||||
$this->footerUrl = '';
|
||||
$this->footerIsActive = true;
|
||||
}
|
||||
|
||||
public function closeFooterModal()
|
||||
{
|
||||
$this->showFooterModal = false;
|
||||
$this->resetFooterForm();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$videos = DisplayVideo::orderBy('sort_order')->get();
|
||||
$footerContents = DisplayFooterContent::orderBy('sort_order')->get();
|
||||
|
||||
return view('livewire.admin.cms.cabinet-display', [
|
||||
'videos' => $videos,
|
||||
'footerContents' => $footerContents,
|
||||
]);
|
||||
}
|
||||
}
|
||||
189
dev/file-upload/cms/CabinetInfoTablet.php
Normal file
189
dev/file-upload/cms/CabinetInfoTablet.php
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use App\Models\CabinetTabletSetting;
|
||||
use Livewire\Component;
|
||||
|
||||
class CabinetInfoTablet extends Component
|
||||
{
|
||||
// Store status mode
|
||||
public string $storeStatus = 'auto';
|
||||
|
||||
public string $noticeHeadline = '';
|
||||
|
||||
public string $noticeSubtext = '';
|
||||
|
||||
// Override times for today
|
||||
public ?string $overrideOpenToday = '';
|
||||
|
||||
public ?string $overrideCloseToday = '';
|
||||
|
||||
// Appointment
|
||||
public ?string $nextAppointmentDate = null;
|
||||
|
||||
public ?string $nextAppointmentTime = '';
|
||||
|
||||
// Structured opening hours per weekday (open + close, empty = closed)
|
||||
public ?string $hoursMondayOpen = '10:00';
|
||||
|
||||
public ?string $hoursMondayClose = '18:00';
|
||||
|
||||
public ?string $hoursTuesdayOpen = '10:00';
|
||||
|
||||
public ?string $hoursTuesdayClose = '18:00';
|
||||
|
||||
public ?string $hoursWednesdayOpen = '10:00';
|
||||
|
||||
public ?string $hoursWednesdayClose = '18:00';
|
||||
|
||||
public ?string $hoursThursdayOpen = '10:00';
|
||||
|
||||
public ?string $hoursThursdayClose = '18:00';
|
||||
|
||||
public ?string $hoursFridayOpen = '10:00';
|
||||
|
||||
public ?string $hoursFridayClose = '18:00';
|
||||
|
||||
public ?string $hoursSaturdayOpen = '10:00';
|
||||
|
||||
public ?string $hoursSaturdayClose = '14:00';
|
||||
|
||||
public ?string $hoursSundayOpen = '';
|
||||
|
||||
public ?string $hoursSundayClose = '';
|
||||
|
||||
// Contact
|
||||
public string $contactPhone = '';
|
||||
|
||||
public string $contactEmail = '';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$s = CabinetTabletSetting::current();
|
||||
|
||||
$this->storeStatus = $s->store_status ?? 'auto';
|
||||
$this->noticeHeadline = $s->notice_headline ?? '';
|
||||
$this->noticeSubtext = $s->notice_subtext ?? '';
|
||||
$this->overrideOpenToday = $s->override_open_today ?? '';
|
||||
$this->overrideCloseToday = $s->override_close_today ?? '';
|
||||
$this->nextAppointmentDate = $s->next_appointment_date?->format('Y-m-d');
|
||||
$this->nextAppointmentTime = $s->next_appointment_time ?? '';
|
||||
$this->hoursMondayOpen = $s->hours_monday_open ?? '';
|
||||
$this->hoursMondayClose = $s->hours_monday_close ?? '';
|
||||
$this->hoursTuesdayOpen = $s->hours_tuesday_open ?? '';
|
||||
$this->hoursTuesdayClose = $s->hours_tuesday_close ?? '';
|
||||
$this->hoursWednesdayOpen = $s->hours_wednesday_open ?? '';
|
||||
$this->hoursWednesdayClose = $s->hours_wednesday_close ?? '';
|
||||
$this->hoursThursdayOpen = $s->hours_thursday_open ?? '';
|
||||
$this->hoursThursdayClose = $s->hours_thursday_close ?? '';
|
||||
$this->hoursFridayOpen = $s->hours_friday_open ?? '';
|
||||
$this->hoursFridayClose = $s->hours_friday_close ?? '';
|
||||
$this->hoursSaturdayOpen = $s->hours_saturday_open ?? '';
|
||||
$this->hoursSaturdayClose = $s->hours_saturday_close ?? '';
|
||||
$this->hoursSundayOpen = $s->hours_sunday_open ?? '';
|
||||
$this->hoursSundayClose = $s->hours_sunday_close ?? '';
|
||||
$this->contactPhone = $s->contact_phone ?? '';
|
||||
$this->contactEmail = $s->contact_email ?? '';
|
||||
}
|
||||
|
||||
private function timeRule(): array
|
||||
{
|
||||
return ['nullable', 'string', 'regex:/^(\d{2}:\d{2})?$/'];
|
||||
}
|
||||
|
||||
private function toNullIfEmpty(?string $value): ?string
|
||||
{
|
||||
return $value !== null && trim($value) !== '' ? trim($value) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $hours Optional: Time picker values from DOM (bypasses wire:model sync issues)
|
||||
*/
|
||||
public function save(array $hours = []): void
|
||||
{
|
||||
foreach ($hours as $prop => $value) {
|
||||
if (property_exists($this, $prop)) {
|
||||
$this->{$prop} = $value ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
$timeRule = $this->timeRule();
|
||||
|
||||
$this->validate([
|
||||
'storeStatus' => 'required|in:auto,notice,warning,closed',
|
||||
'noticeHeadline' => 'nullable|string|max:40',
|
||||
'noticeSubtext' => 'nullable|string|max:80',
|
||||
'overrideOpenToday' => $timeRule,
|
||||
'overrideCloseToday' => $timeRule,
|
||||
'nextAppointmentDate' => 'nullable|date',
|
||||
'nextAppointmentTime' => $timeRule,
|
||||
'hoursMondayOpen' => $timeRule,
|
||||
'hoursMondayClose' => $timeRule,
|
||||
'hoursTuesdayOpen' => $timeRule,
|
||||
'hoursTuesdayClose' => $timeRule,
|
||||
'hoursWednesdayOpen' => $timeRule,
|
||||
'hoursWednesdayClose' => $timeRule,
|
||||
'hoursThursdayOpen' => $timeRule,
|
||||
'hoursThursdayClose' => $timeRule,
|
||||
'hoursFridayOpen' => $timeRule,
|
||||
'hoursFridayClose' => $timeRule,
|
||||
'hoursSaturdayOpen' => $timeRule,
|
||||
'hoursSaturdayClose' => $timeRule,
|
||||
'hoursSundayOpen' => $timeRule,
|
||||
'hoursSundayClose' => $timeRule,
|
||||
'contactPhone' => 'nullable|string|max:50',
|
||||
'contactEmail' => 'nullable|email|max:100',
|
||||
], [
|
||||
'storeStatus.required' => 'Der Store-Status ist erforderlich.',
|
||||
'storeStatus.in' => 'Ungültiger Status. Erlaubt: auto, notice, warning, closed.',
|
||||
'noticeHeadline.max' => 'Die Headline darf maximal 40 Zeichen haben.',
|
||||
'noticeSubtext.max' => 'Der Subtext darf maximal 80 Zeichen haben.',
|
||||
'overrideOpenToday.regex' => 'Bitte im Format HH:MM eingeben.',
|
||||
'overrideCloseToday.regex' => 'Bitte im Format HH:MM eingeben.',
|
||||
'nextAppointmentTime.regex' => 'Bitte im Format HH:MM eingeben.',
|
||||
'contactEmail.email' => 'Bitte eine gültige E-Mail-Adresse eingeben.',
|
||||
]);
|
||||
CabinetTabletSetting::current()->update([
|
||||
'store_status' => $this->storeStatus,
|
||||
'notice_headline' => $this->toNullIfEmpty($this->noticeHeadline),
|
||||
'notice_subtext' => $this->toNullIfEmpty($this->noticeSubtext),
|
||||
'override_open_today' => $this->toNullIfEmpty($this->overrideOpenToday),
|
||||
'override_close_today' => $this->toNullIfEmpty($this->overrideCloseToday),
|
||||
'next_appointment_date' => $this->toNullIfEmpty($this->nextAppointmentDate),
|
||||
'next_appointment_time' => $this->toNullIfEmpty($this->nextAppointmentTime),
|
||||
'hours_monday_open' => $this->toNullIfEmpty($this->hoursMondayOpen),
|
||||
'hours_monday_close' => $this->toNullIfEmpty($this->hoursMondayClose),
|
||||
'hours_tuesday_open' => $this->toNullIfEmpty($this->hoursTuesdayOpen),
|
||||
'hours_tuesday_close' => $this->toNullIfEmpty($this->hoursTuesdayClose),
|
||||
'hours_wednesday_open' => $this->toNullIfEmpty($this->hoursWednesdayOpen),
|
||||
'hours_wednesday_close' => $this->toNullIfEmpty($this->hoursWednesdayClose),
|
||||
'hours_thursday_open' => $this->toNullIfEmpty($this->hoursThursdayOpen),
|
||||
'hours_thursday_close' => $this->toNullIfEmpty($this->hoursThursdayClose),
|
||||
'hours_friday_open' => $this->toNullIfEmpty($this->hoursFridayOpen),
|
||||
'hours_friday_close' => $this->toNullIfEmpty($this->hoursFridayClose),
|
||||
'hours_saturday_open' => $this->toNullIfEmpty($this->hoursSaturdayOpen),
|
||||
'hours_saturday_close' => $this->toNullIfEmpty($this->hoursSaturdayClose),
|
||||
'hours_sunday_open' => $this->toNullIfEmpty($this->hoursSundayOpen),
|
||||
'hours_sunday_close' => $this->toNullIfEmpty($this->hoursSundayClose),
|
||||
'contact_phone' => $this->toNullIfEmpty($this->contactPhone),
|
||||
'contact_email' => $this->toNullIfEmpty($this->contactEmail),
|
||||
]);
|
||||
|
||||
session()->flash('success', 'Info-Tablet Einstellungen gespeichert!');
|
||||
}
|
||||
|
||||
public function clearOverrides(): void
|
||||
{
|
||||
CabinetTabletSetting::current()->clearOverrides();
|
||||
$this->overrideOpenToday = '';
|
||||
$this->overrideCloseToday = '';
|
||||
|
||||
session()->flash('success', 'Sonderöffnungszeiten wurden zurückgesetzt!');
|
||||
}
|
||||
|
||||
public function render(): \Illuminate\View\View
|
||||
{
|
||||
return view('livewire.admin.cms.cabinet-info-tablet');
|
||||
}
|
||||
}
|
||||
153
dev/file-upload/cms/DisplayList.php
Normal file
153
dev/file-upload/cms/DisplayList.php
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use App\Models\Display;
|
||||
use App\Models\DisplayVersion;
|
||||
use Livewire\Component;
|
||||
|
||||
class DisplayList extends Component
|
||||
{
|
||||
public $showModal = false;
|
||||
|
||||
public $displayId = null;
|
||||
|
||||
public $displayName = '';
|
||||
|
||||
public $displayLocation = '';
|
||||
|
||||
/** @var array<int> */
|
||||
public $selectedVersionIds = [];
|
||||
|
||||
public $displayIsActive = true;
|
||||
|
||||
public $addVersionSelect = null;
|
||||
|
||||
public function openModal(?int $id = null): void
|
||||
{
|
||||
if ($id) {
|
||||
$display = Display::with('versions')->findOrFail($id);
|
||||
$this->displayId = $display->id;
|
||||
$this->displayName = $display->name;
|
||||
$this->displayLocation = $display->location ?? '';
|
||||
$this->selectedVersionIds = $display->versions->pluck('id')->toArray();
|
||||
$this->displayIsActive = $display->is_active;
|
||||
} else {
|
||||
$this->resetForm();
|
||||
}
|
||||
|
||||
$this->showModal = true;
|
||||
}
|
||||
|
||||
public function addVersion(?int $versionId = null): void
|
||||
{
|
||||
$id = $versionId ?? $this->addVersionSelect;
|
||||
|
||||
if ($id && ! in_array((int) $id, $this->selectedVersionIds)) {
|
||||
$this->selectedVersionIds[] = (int) $id;
|
||||
}
|
||||
|
||||
$this->addVersionSelect = null;
|
||||
}
|
||||
|
||||
public function removeVersion(int $index): void
|
||||
{
|
||||
array_splice($this->selectedVersionIds, $index, 1);
|
||||
}
|
||||
|
||||
public function moveVersion(int $index, string $direction): void
|
||||
{
|
||||
$newIndex = $direction === 'up' ? $index - 1 : $index + 1;
|
||||
|
||||
if ($newIndex < 0 || $newIndex >= count($this->selectedVersionIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$temp = $this->selectedVersionIds[$index];
|
||||
$this->selectedVersionIds[$index] = $this->selectedVersionIds[$newIndex];
|
||||
$this->selectedVersionIds[$newIndex] = $temp;
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->validate([
|
||||
'displayName' => 'required|string|max:255',
|
||||
'displayLocation' => 'nullable|string|max:255',
|
||||
'selectedVersionIds' => 'array',
|
||||
'selectedVersionIds.*' => 'exists:display_versions,id',
|
||||
], [
|
||||
'displayName.required' => 'Bitte geben Sie einen Namen ein.',
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'name' => $this->displayName,
|
||||
'location' => $this->displayLocation ?: null,
|
||||
'is_active' => $this->displayIsActive,
|
||||
];
|
||||
|
||||
if ($this->displayId) {
|
||||
$display = Display::findOrFail($this->displayId);
|
||||
$display->update($data);
|
||||
session()->flash('success', 'Display erfolgreich aktualisiert!');
|
||||
} else {
|
||||
$display = Display::create($data);
|
||||
session()->flash('success', 'Display erfolgreich erstellt!');
|
||||
}
|
||||
|
||||
// Sync versions with sort_order
|
||||
$syncData = [];
|
||||
foreach ($this->selectedVersionIds as $sortOrder => $versionId) {
|
||||
$syncData[$versionId] = ['sort_order' => $sortOrder];
|
||||
}
|
||||
$display->versions()->sync($syncData);
|
||||
|
||||
$this->closeModal();
|
||||
}
|
||||
|
||||
public function deleteDisplay(int $id): void
|
||||
{
|
||||
$display = Display::findOrFail($id);
|
||||
$name = $display->name;
|
||||
$display->delete();
|
||||
|
||||
session()->flash('success', 'Display "'.$name.'" wurde gelöscht!');
|
||||
}
|
||||
|
||||
public function toggleActive(int $id): void
|
||||
{
|
||||
$display = Display::findOrFail($id);
|
||||
$display->update(['is_active' => ! $display->is_active]);
|
||||
}
|
||||
|
||||
public function closeModal(): void
|
||||
{
|
||||
$this->showModal = false;
|
||||
$this->resetForm();
|
||||
}
|
||||
|
||||
public function resetForm(): void
|
||||
{
|
||||
$this->displayId = null;
|
||||
$this->displayName = '';
|
||||
$this->displayLocation = '';
|
||||
$this->selectedVersionIds = [];
|
||||
$this->displayIsActive = true;
|
||||
$this->addVersionSelect = null;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$displays = Display::with('versions')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
$versions = DisplayVersion::active()
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('livewire.admin.cms.display-list', [
|
||||
'displays' => $displays,
|
||||
'versions' => $versions,
|
||||
]);
|
||||
}
|
||||
}
|
||||
437
dev/file-upload/cms/DisplayVersionEditor.php
Normal file
437
dev/file-upload/cms/DisplayVersionEditor.php
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use App\Enums\DisplayVersionType;
|
||||
use App\Models\DisplayVersion;
|
||||
use App\Models\DisplayVersionItem;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Livewire\Component;
|
||||
|
||||
class DisplayVersionEditor extends Component
|
||||
{
|
||||
public DisplayVersion $version;
|
||||
|
||||
public string $versionName = '';
|
||||
|
||||
// Item Modal
|
||||
public bool $showItemModal = false;
|
||||
|
||||
public ?int $itemId = null;
|
||||
|
||||
public string $itemType = '';
|
||||
|
||||
// Video-Display: Video fields
|
||||
public string $videoFilename = '';
|
||||
|
||||
public string $videoTitle = '';
|
||||
|
||||
public int $videoPosition = 25;
|
||||
|
||||
public bool $videoIsActive = true;
|
||||
|
||||
// Video-Display: Footer fields
|
||||
public string $footerHeadline = '';
|
||||
|
||||
public string $footerSubline = '';
|
||||
|
||||
public string $footerUrl = '';
|
||||
|
||||
public bool $footerIsActive = true;
|
||||
|
||||
// B2in: Media fields
|
||||
public string $mediaType = 'image';
|
||||
|
||||
public string $mediaCategory = 'immobilien';
|
||||
|
||||
public string $mediaUrl = '';
|
||||
|
||||
public string $mediaHeadline = '';
|
||||
|
||||
public string $mediaSubline = '';
|
||||
|
||||
public int $mediaDuration = 10;
|
||||
|
||||
public bool $mediaIsActive = true;
|
||||
|
||||
// Offers: Slide fields
|
||||
public string $slideType = 'product-hero';
|
||||
|
||||
public int $slideDuration = 8000;
|
||||
|
||||
public string $slideImageUrl = '';
|
||||
|
||||
public string $slideBadge = '';
|
||||
|
||||
public string $slideEyebrow = '';
|
||||
|
||||
public string $slideTitle = '';
|
||||
|
||||
public string $slideSubline = '';
|
||||
|
||||
public string $slidePrice = '';
|
||||
|
||||
public string $slideOriginalPrice = '';
|
||||
|
||||
public string $slideTagText = '';
|
||||
|
||||
/** @var array<string> */
|
||||
public array $slideBullets = [];
|
||||
|
||||
public string $slideDisclaimer = '';
|
||||
|
||||
public string $slideQrUrl = '';
|
||||
|
||||
public string $slideQrTitle = '';
|
||||
|
||||
public string $slideContact = '';
|
||||
|
||||
public bool $slideShowBrandText = false;
|
||||
|
||||
public string $slideBrandTagline = '';
|
||||
|
||||
public bool $slideIsActive = true;
|
||||
|
||||
// Settings Modal
|
||||
public bool $showSettingsModal = false;
|
||||
|
||||
public array $settings = [];
|
||||
|
||||
/** @var array<string> */
|
||||
public array $availableVideos = [];
|
||||
|
||||
public function mount(DisplayVersion $displayVersion): void
|
||||
{
|
||||
$this->version = $displayVersion;
|
||||
$this->versionName = $displayVersion->name;
|
||||
$this->settings = $displayVersion->settings ?? [];
|
||||
|
||||
if ($this->version->type === DisplayVersionType::VideoDisplay) {
|
||||
$this->loadAvailableVideos();
|
||||
}
|
||||
}
|
||||
|
||||
public function loadAvailableVideos(): void
|
||||
{
|
||||
$assetsPath = public_path('_cabinet/assets');
|
||||
|
||||
if (File::exists($assetsPath)) {
|
||||
$this->availableVideos = collect(File::files($assetsPath))
|
||||
->map(fn ($file) => $file->getFilename())
|
||||
->filter(fn ($filename) => in_array(pathinfo($filename, PATHINFO_EXTENSION), ['mp4', 'webm', 'mov']))
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
public function toggleTheme(): void
|
||||
{
|
||||
$settings = $this->version->settings ?? [];
|
||||
$settings['theme'] = ($settings['theme'] ?? 'dark') === 'dark' ? 'light' : 'dark';
|
||||
$this->version->update(['settings' => $settings]);
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
public function saveName(): void
|
||||
{
|
||||
$this->validate([
|
||||
'versionName' => 'required|string|max:255',
|
||||
]);
|
||||
|
||||
$this->version->update(['name' => $this->versionName]);
|
||||
session()->flash('success', 'Name aktualisiert!');
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SETTINGS
|
||||
// ========================================
|
||||
|
||||
public function openSettingsModal(): void
|
||||
{
|
||||
$this->settings = $this->version->settings ?? [];
|
||||
$this->showSettingsModal = true;
|
||||
}
|
||||
|
||||
public function saveSettings(): void
|
||||
{
|
||||
$this->version->update(['settings' => $this->settings]);
|
||||
$this->showSettingsModal = false;
|
||||
session()->flash('success', 'Einstellungen gespeichert!');
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// ITEM CRUD
|
||||
// ========================================
|
||||
|
||||
public function openItemModal(?int $id = null, string $type = ''): void
|
||||
{
|
||||
$this->resetItemForm();
|
||||
|
||||
if ($id) {
|
||||
$item = DisplayVersionItem::findOrFail($id);
|
||||
$this->itemId = $item->id;
|
||||
$this->itemType = $item->item_type;
|
||||
$this->loadItemContent($item);
|
||||
} else {
|
||||
$this->itemType = $type ?: $this->defaultItemType();
|
||||
}
|
||||
|
||||
$this->showItemModal = true;
|
||||
}
|
||||
|
||||
public function saveItem(): void
|
||||
{
|
||||
$content = $this->buildItemContent();
|
||||
$isActive = $this->getActiveFlag();
|
||||
|
||||
if ($this->itemId) {
|
||||
$item = DisplayVersionItem::findOrFail($this->itemId);
|
||||
$item->update([
|
||||
'content' => $content,
|
||||
'is_active' => $isActive,
|
||||
]);
|
||||
session()->flash('success', 'Inhalt aktualisiert!');
|
||||
} else {
|
||||
$maxSort = DisplayVersionItem::where('display_version_id', $this->version->id)
|
||||
->where('item_type', $this->itemType)
|
||||
->max('sort_order') ?? -1;
|
||||
|
||||
DisplayVersionItem::create([
|
||||
'display_version_id' => $this->version->id,
|
||||
'item_type' => $this->itemType,
|
||||
'content' => $content,
|
||||
'sort_order' => $maxSort + 1,
|
||||
'is_active' => $isActive,
|
||||
]);
|
||||
session()->flash('success', 'Inhalt hinzugefügt!');
|
||||
}
|
||||
|
||||
$this->closeItemModal();
|
||||
}
|
||||
|
||||
public function deleteItem(int $id): void
|
||||
{
|
||||
DisplayVersionItem::findOrFail($id)->delete();
|
||||
session()->flash('success', 'Inhalt gelöscht!');
|
||||
}
|
||||
|
||||
public function toggleItemStatus(int $id): void
|
||||
{
|
||||
$item = DisplayVersionItem::findOrFail($id);
|
||||
$item->update(['is_active' => ! $item->is_active]);
|
||||
}
|
||||
|
||||
public function moveItem(int $id, string $direction): void
|
||||
{
|
||||
$item = DisplayVersionItem::findOrFail($id);
|
||||
$currentOrder = $item->sort_order;
|
||||
|
||||
$swapItem = DisplayVersionItem::where('display_version_id', $this->version->id)
|
||||
->where('item_type', $item->item_type)
|
||||
->where('sort_order', $direction === 'up' ? $currentOrder - 1 : $currentOrder + 1)
|
||||
->first();
|
||||
|
||||
if ($swapItem) {
|
||||
$item->update(['sort_order' => $swapItem->sort_order]);
|
||||
$swapItem->update(['sort_order' => $currentOrder]);
|
||||
}
|
||||
}
|
||||
|
||||
public function addBullet(): void
|
||||
{
|
||||
$this->slideBullets[] = '';
|
||||
}
|
||||
|
||||
public function removeBullet(int $index): void
|
||||
{
|
||||
unset($this->slideBullets[$index]);
|
||||
$this->slideBullets = array_values($this->slideBullets);
|
||||
}
|
||||
|
||||
public function closeItemModal(): void
|
||||
{
|
||||
$this->showItemModal = false;
|
||||
$this->resetItemForm();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// HELPERS
|
||||
// ========================================
|
||||
|
||||
private function loadItemContent(DisplayVersionItem $item): void
|
||||
{
|
||||
$content = $item->content;
|
||||
|
||||
match ($item->item_type) {
|
||||
'video' => $this->loadVideoContent($content),
|
||||
'footer' => $this->loadFooterContent($content),
|
||||
'media' => $this->loadMediaContent($content),
|
||||
'slide' => $this->loadSlideContent($content),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function loadVideoContent(array $content): void
|
||||
{
|
||||
$this->videoFilename = $content['filename'] ?? '';
|
||||
$this->videoTitle = $content['title'] ?? '';
|
||||
$this->videoPosition = $content['position'] ?? 25;
|
||||
$this->videoIsActive = true;
|
||||
}
|
||||
|
||||
private function loadFooterContent(array $content): void
|
||||
{
|
||||
$this->footerHeadline = $content['headline'] ?? '';
|
||||
$this->footerSubline = $content['subline'] ?? '';
|
||||
$this->footerUrl = $content['url'] ?? '';
|
||||
$this->footerIsActive = true;
|
||||
}
|
||||
|
||||
private function loadMediaContent(array $content): void
|
||||
{
|
||||
$this->mediaType = $content['media_type'] ?? 'image';
|
||||
$this->mediaCategory = $content['category'] ?? 'immobilien';
|
||||
$this->mediaUrl = $content['media_url'] ?? '';
|
||||
$this->mediaHeadline = $content['headline'] ?? '';
|
||||
$this->mediaSubline = $content['subline'] ?? '';
|
||||
$this->mediaDuration = $content['duration_seconds'] ?? 10;
|
||||
$this->mediaIsActive = true;
|
||||
}
|
||||
|
||||
private function loadSlideContent(array $content): void
|
||||
{
|
||||
$this->slideType = $content['type'] ?? 'product-hero';
|
||||
$this->slideDuration = $content['duration'] ?? 8000;
|
||||
$this->slideImageUrl = $content['image_url'] ?? '';
|
||||
$this->slideBadge = $content['badge_text'] ?? '';
|
||||
$this->slideEyebrow = $content['eyebrow'] ?? '';
|
||||
$this->slideTitle = $content['title'] ?? '';
|
||||
$this->slideSubline = $content['subline'] ?? '';
|
||||
$this->slidePrice = $content['price'] ?? '';
|
||||
$this->slideOriginalPrice = $content['original_price'] ?? '';
|
||||
$this->slideTagText = $content['tag_text'] ?? '';
|
||||
$this->slideBullets = $content['bullets'] ?? [];
|
||||
$this->slideDisclaimer = $content['disclaimer'] ?? '';
|
||||
$this->slideQrUrl = $content['qr_url'] ?? '';
|
||||
$this->slideQrTitle = $content['qr_title'] ?? '';
|
||||
$this->slideContact = $content['contact'] ?? '';
|
||||
$this->slideShowBrandText = $content['show_brand_text'] ?? false;
|
||||
$this->slideBrandTagline = $content['brand_tagline'] ?? '';
|
||||
$this->slideIsActive = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildItemContent(): array
|
||||
{
|
||||
return match ($this->itemType) {
|
||||
'video' => [
|
||||
'filename' => $this->videoFilename,
|
||||
'title' => $this->videoTitle,
|
||||
'position' => $this->videoPosition,
|
||||
],
|
||||
'footer' => [
|
||||
'headline' => $this->footerHeadline,
|
||||
'subline' => $this->footerSubline,
|
||||
'url' => $this->footerUrl ?: null,
|
||||
],
|
||||
'media' => [
|
||||
'media_type' => $this->mediaType,
|
||||
'category' => $this->mediaCategory,
|
||||
'media_url' => $this->mediaUrl,
|
||||
'headline' => $this->mediaHeadline,
|
||||
'subline' => $this->mediaSubline,
|
||||
'duration_seconds' => $this->mediaDuration,
|
||||
],
|
||||
'slide' => [
|
||||
'type' => $this->slideType,
|
||||
'duration' => $this->slideDuration,
|
||||
'image_url' => $this->slideImageUrl,
|
||||
'badge_text' => $this->slideBadge,
|
||||
'eyebrow' => $this->slideEyebrow,
|
||||
'title' => $this->slideTitle,
|
||||
'subline' => $this->slideSubline,
|
||||
'price' => $this->slidePrice,
|
||||
'original_price' => $this->slideOriginalPrice,
|
||||
'tag_text' => $this->slideTagText,
|
||||
'bullets' => $this->slideBullets,
|
||||
'disclaimer' => $this->slideDisclaimer,
|
||||
'qr_url' => $this->slideQrUrl,
|
||||
'qr_title' => $this->slideQrTitle,
|
||||
'contact' => $this->slideContact,
|
||||
'show_brand_text' => $this->slideShowBrandText,
|
||||
'brand_tagline' => $this->slideBrandTagline,
|
||||
],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
private function getActiveFlag(): bool
|
||||
{
|
||||
return match ($this->itemType) {
|
||||
'video' => $this->videoIsActive,
|
||||
'footer' => $this->footerIsActive,
|
||||
'media' => $this->mediaIsActive,
|
||||
'slide' => $this->slideIsActive,
|
||||
default => true,
|
||||
};
|
||||
}
|
||||
|
||||
private function defaultItemType(): string
|
||||
{
|
||||
return match ($this->version->type) {
|
||||
DisplayVersionType::VideoDisplay => 'video',
|
||||
DisplayVersionType::B2in => 'media',
|
||||
DisplayVersionType::Offers => 'slide',
|
||||
};
|
||||
}
|
||||
|
||||
private function resetItemForm(): void
|
||||
{
|
||||
$this->itemId = null;
|
||||
$this->itemType = '';
|
||||
$this->videoFilename = '';
|
||||
$this->videoTitle = '';
|
||||
$this->videoPosition = 25;
|
||||
$this->videoIsActive = true;
|
||||
$this->footerHeadline = '';
|
||||
$this->footerSubline = '';
|
||||
$this->footerUrl = '';
|
||||
$this->footerIsActive = true;
|
||||
$this->mediaType = 'image';
|
||||
$this->mediaCategory = 'immobilien';
|
||||
$this->mediaUrl = '';
|
||||
$this->mediaHeadline = '';
|
||||
$this->mediaSubline = '';
|
||||
$this->mediaDuration = 10;
|
||||
$this->mediaIsActive = true;
|
||||
$this->slideType = 'product-hero';
|
||||
$this->slideDuration = 8000;
|
||||
$this->slideImageUrl = '';
|
||||
$this->slideBadge = '';
|
||||
$this->slideEyebrow = '';
|
||||
$this->slideTitle = '';
|
||||
$this->slideSubline = '';
|
||||
$this->slidePrice = '';
|
||||
$this->slideOriginalPrice = '';
|
||||
$this->slideTagText = '';
|
||||
$this->slideBullets = [];
|
||||
$this->slideDisclaimer = '';
|
||||
$this->slideQrUrl = '';
|
||||
$this->slideQrTitle = '';
|
||||
$this->slideContact = '';
|
||||
$this->slideShowBrandText = false;
|
||||
$this->slideBrandTagline = '';
|
||||
$this->slideIsActive = true;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$items = $this->version->items()->get()->groupBy('item_type');
|
||||
|
||||
return view('livewire.admin.cms.display-version-editor', [
|
||||
'items' => $items,
|
||||
]);
|
||||
}
|
||||
}
|
||||
102
dev/file-upload/cms/DisplayVersionList.php
Normal file
102
dev/file-upload/cms/DisplayVersionList.php
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use App\Enums\DisplayVersionType;
|
||||
use App\Models\DisplayVersion;
|
||||
use Livewire\Component;
|
||||
|
||||
class DisplayVersionList extends Component
|
||||
{
|
||||
public $showCreateModal = false;
|
||||
|
||||
public $newName = '';
|
||||
|
||||
public $newType = '';
|
||||
|
||||
public function openCreateModal(): void
|
||||
{
|
||||
$this->newName = '';
|
||||
$this->newType = '';
|
||||
$this->showCreateModal = true;
|
||||
}
|
||||
|
||||
public function createVersion(): void
|
||||
{
|
||||
$this->validate([
|
||||
'newName' => 'required|string|max:255',
|
||||
'newType' => 'required|string|in:video-display,b2in,offers',
|
||||
], [
|
||||
'newName.required' => 'Bitte geben Sie einen Namen ein.',
|
||||
'newType.required' => 'Bitte wählen Sie einen Typ aus.',
|
||||
]);
|
||||
|
||||
$version = DisplayVersion::create([
|
||||
'name' => $this->newName,
|
||||
'type' => $this->newType,
|
||||
'settings' => $this->defaultSettingsForType($this->newType),
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->showCreateModal = false;
|
||||
$this->newName = '';
|
||||
$this->newType = '';
|
||||
|
||||
session()->flash('success', 'Version "'.$version->name.'" wurde erstellt!');
|
||||
|
||||
$this->redirect(
|
||||
route('admin.cms.display-version-edit', $version),
|
||||
navigate: true
|
||||
);
|
||||
}
|
||||
|
||||
public function deleteVersion(int $id): void
|
||||
{
|
||||
$version = DisplayVersion::findOrFail($id);
|
||||
$name = $version->name;
|
||||
$version->delete();
|
||||
|
||||
session()->flash('success', 'Version "'.$name.'" wurde gelöscht!');
|
||||
}
|
||||
|
||||
public function toggleActive(int $id): void
|
||||
{
|
||||
$version = DisplayVersion::findOrFail($id);
|
||||
$version->update(['is_active' => ! $version->is_active]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function defaultSettingsForType(string $type): array
|
||||
{
|
||||
return match ($type) {
|
||||
'b2in' => [
|
||||
'theme' => 'dark',
|
||||
'footer_name' => '',
|
||||
'footer_url' => '',
|
||||
'transition' => ['type' => 'crossfade', 'duration_ms' => 800],
|
||||
'default_image_duration' => 10,
|
||||
'rotation_weights' => ['immobilien' => 70, 'moebel' => 30],
|
||||
'display_active' => true,
|
||||
],
|
||||
'offers' => [
|
||||
'loop' => true,
|
||||
'transition' => ['type' => 'fade', 'duration' => 600],
|
||||
],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$versions = DisplayVersion::withCount(['items', 'displays'])
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('livewire.admin.cms.display-version-list', [
|
||||
'versions' => $versions,
|
||||
'types' => DisplayVersionType::cases(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
45
dev/file-upload/cms/MediaLibraryUploader.php
Normal file
45
dev/file-upload/cms/MediaLibraryUploader.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use FluxCms\Core\Services\MediaConversionService;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
class MediaLibraryUploader extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
/** @var array<\Livewire\Features\SupportFileUploads\TemporaryUploadedFile> */
|
||||
public array $uploads = [];
|
||||
|
||||
public function updatedUploads(): void
|
||||
{
|
||||
$this->validate([
|
||||
'uploads' => 'nullable|array|max:20',
|
||||
'uploads.*' => 'file|mimes:jpeg,jpg,png,gif,webp,svg,pdf,doc,docx|max:10240',
|
||||
]);
|
||||
|
||||
$service = app(MediaConversionService::class);
|
||||
|
||||
foreach ($this->uploads as $file) {
|
||||
$media = $service->storeUpload($file);
|
||||
$this->dispatch('media-library-uploaded', mediaId: $media->id);
|
||||
}
|
||||
|
||||
$this->uploads = [];
|
||||
}
|
||||
|
||||
public function removeUpload(int $index): void
|
||||
{
|
||||
if (isset($this->uploads[$index])) {
|
||||
unset($this->uploads[$index]);
|
||||
$this->uploads = array_values($this->uploads);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.admin.cms.media-library-uploader');
|
||||
}
|
||||
}
|
||||
139
dev/file-upload/cms/MediaPicker.php
Normal file
139
dev/file-upload/cms/MediaPicker.php
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use FluxCms\Core\Models\CmsMedia;
|
||||
use FluxCms\Core\Services\MediaConversionService;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
class MediaPicker extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
use WithPagination;
|
||||
|
||||
public ?int $value = null;
|
||||
|
||||
public string $field = 'media_id';
|
||||
|
||||
public string $type = 'image';
|
||||
|
||||
public string $profile = 'thumbnail';
|
||||
|
||||
public string $label = 'Bild auswählen';
|
||||
|
||||
public bool $showModal = false;
|
||||
|
||||
public string $search = '';
|
||||
|
||||
/** @var array<\Livewire\Features\SupportFileUploads\TemporaryUploadedFile> */
|
||||
public array $quickUploads = [];
|
||||
|
||||
public function mount(?int $value = null): void
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function openPicker(): void
|
||||
{
|
||||
$this->showModal = true;
|
||||
}
|
||||
|
||||
public function selectMedia(int $id): void
|
||||
{
|
||||
$media = CmsMedia::find($id);
|
||||
if (! $media) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($media->isImage() && $this->profile) {
|
||||
$service = app(MediaConversionService::class);
|
||||
if (! $media->hasConversion($this->profile)) {
|
||||
$service->convert($media, $this->profile);
|
||||
$media->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
$this->value = $media->id;
|
||||
$this->showModal = false;
|
||||
|
||||
$this->dispatch('media-selected', field: $this->field, mediaId: $media->id, url: $media->getConversionUrl($this->profile));
|
||||
}
|
||||
|
||||
public function clearSelection(): void
|
||||
{
|
||||
$this->value = null;
|
||||
$this->dispatch('media-selected', field: $this->field, mediaId: null, url: null);
|
||||
}
|
||||
|
||||
public function updatedQuickUploads(): void
|
||||
{
|
||||
$this->validate([
|
||||
'quickUploads' => 'nullable|array|max:5',
|
||||
'quickUploads.*' => 'file|mimes:jpeg,jpg,png,gif,webp,svg,pdf,doc,docx|max:10240',
|
||||
]);
|
||||
|
||||
$service = app(MediaConversionService::class);
|
||||
$lastMedia = null;
|
||||
|
||||
foreach ($this->quickUploads as $file) {
|
||||
$lastMedia = $service->storeUpload($file);
|
||||
|
||||
if ($lastMedia->isImage() && $this->profile) {
|
||||
$service->convert($lastMedia, $this->profile);
|
||||
$lastMedia->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
$this->quickUploads = [];
|
||||
|
||||
if ($lastMedia) {
|
||||
$this->value = $lastMedia->id;
|
||||
$this->showModal = false;
|
||||
|
||||
$this->dispatch('media-selected', field: $this->field, mediaId: $lastMedia->id, url: $lastMedia->getConversionUrl($this->profile));
|
||||
}
|
||||
}
|
||||
|
||||
public function removeQuickUpload(int $index): void
|
||||
{
|
||||
if (isset($this->quickUploads[$index])) {
|
||||
unset($this->quickUploads[$index]);
|
||||
$this->quickUploads = array_values($this->quickUploads);
|
||||
}
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.admin.cms.media-picker', [
|
||||
'selectedMedia' => $this->resolveSelectedMedia(),
|
||||
'mediaItems' => $this->resolveMediaItems(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveSelectedMedia(): ?CmsMedia
|
||||
{
|
||||
if (! $this->value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return CmsMedia::find($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LengthAwarePaginator<int, CmsMedia>
|
||||
*/
|
||||
private function resolveMediaItems(): LengthAwarePaginator
|
||||
{
|
||||
return CmsMedia::query()
|
||||
->when($this->type === 'image', fn ($q) => $q->images())
|
||||
->when($this->type === 'pdf', fn ($q) => $q->pdfs())
|
||||
->when($this->type === 'document', fn ($q) => $q->documents())
|
||||
->when($this->search, fn ($q) => $q->where('filename', 'like', "%{$this->search}%"))
|
||||
->orderByDesc('created_at')
|
||||
->paginate(18);
|
||||
}
|
||||
}
|
||||
45
dev/flux-cms/Admin/Cms/MediaLibraryUploader.php
Normal file
45
dev/flux-cms/Admin/Cms/MediaLibraryUploader.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use FluxCms\Core\Services\MediaConversionService;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
class MediaLibraryUploader extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
/** @var array<\Livewire\Features\SupportFileUploads\TemporaryUploadedFile> */
|
||||
public array $uploads = [];
|
||||
|
||||
public function updatedUploads(): void
|
||||
{
|
||||
$this->validate([
|
||||
'uploads' => 'nullable|array|max:20',
|
||||
'uploads.*' => 'file|mimes:jpeg,jpg,png,gif,webp,svg,pdf,doc,docx|max:10240',
|
||||
]);
|
||||
|
||||
$service = app(MediaConversionService::class);
|
||||
|
||||
foreach ($this->uploads as $file) {
|
||||
$media = $service->storeUpload($file);
|
||||
$this->dispatch('media-library-uploaded', mediaId: $media->id);
|
||||
}
|
||||
|
||||
$this->uploads = [];
|
||||
}
|
||||
|
||||
public function removeUpload(int $index): void
|
||||
{
|
||||
if (isset($this->uploads[$index])) {
|
||||
unset($this->uploads[$index]);
|
||||
$this->uploads = array_values($this->uploads);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.admin.cms.media-library-uploader');
|
||||
}
|
||||
}
|
||||
139
dev/flux-cms/Admin/Cms/MediaPicker.php
Normal file
139
dev/flux-cms/Admin/Cms/MediaPicker.php
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use FluxCms\Core\Models\CmsMedia;
|
||||
use FluxCms\Core\Services\MediaConversionService;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
class MediaPicker extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
use WithPagination;
|
||||
|
||||
public ?int $value = null;
|
||||
|
||||
public string $field = 'media_id';
|
||||
|
||||
public string $type = 'image';
|
||||
|
||||
public string $profile = 'thumbnail';
|
||||
|
||||
public string $label = 'Bild auswählen';
|
||||
|
||||
public bool $showModal = false;
|
||||
|
||||
public string $search = '';
|
||||
|
||||
/** @var array<\Livewire\Features\SupportFileUploads\TemporaryUploadedFile> */
|
||||
public array $quickUploads = [];
|
||||
|
||||
public function mount(?int $value = null): void
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function openPicker(): void
|
||||
{
|
||||
$this->showModal = true;
|
||||
}
|
||||
|
||||
public function selectMedia(int $id): void
|
||||
{
|
||||
$media = CmsMedia::find($id);
|
||||
if (! $media) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($media->isImage() && $this->profile) {
|
||||
$service = app(MediaConversionService::class);
|
||||
if (! $media->hasConversion($this->profile)) {
|
||||
$service->convert($media, $this->profile);
|
||||
$media->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
$this->value = $media->id;
|
||||
$this->showModal = false;
|
||||
|
||||
$this->dispatch('media-selected', field: $this->field, mediaId: $media->id, url: $media->getConversionUrl($this->profile));
|
||||
}
|
||||
|
||||
public function clearSelection(): void
|
||||
{
|
||||
$this->value = null;
|
||||
$this->dispatch('media-selected', field: $this->field, mediaId: null, url: null);
|
||||
}
|
||||
|
||||
public function updatedQuickUploads(): void
|
||||
{
|
||||
$this->validate([
|
||||
'quickUploads' => 'nullable|array|max:5',
|
||||
'quickUploads.*' => 'file|mimes:jpeg,jpg,png,gif,webp,svg,pdf,doc,docx|max:10240',
|
||||
]);
|
||||
|
||||
$service = app(MediaConversionService::class);
|
||||
$lastMedia = null;
|
||||
|
||||
foreach ($this->quickUploads as $file) {
|
||||
$lastMedia = $service->storeUpload($file);
|
||||
|
||||
if ($lastMedia->isImage() && $this->profile) {
|
||||
$service->convert($lastMedia, $this->profile);
|
||||
$lastMedia->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
$this->quickUploads = [];
|
||||
|
||||
if ($lastMedia) {
|
||||
$this->value = $lastMedia->id;
|
||||
$this->showModal = false;
|
||||
|
||||
$this->dispatch('media-selected', field: $this->field, mediaId: $lastMedia->id, url: $lastMedia->getConversionUrl($this->profile));
|
||||
}
|
||||
}
|
||||
|
||||
public function removeQuickUpload(int $index): void
|
||||
{
|
||||
if (isset($this->quickUploads[$index])) {
|
||||
unset($this->quickUploads[$index]);
|
||||
$this->quickUploads = array_values($this->quickUploads);
|
||||
}
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.admin.cms.media-picker', [
|
||||
'selectedMedia' => $this->resolveSelectedMedia(),
|
||||
'mediaItems' => $this->resolveMediaItems(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveSelectedMedia(): ?CmsMedia
|
||||
{
|
||||
if (! $this->value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return CmsMedia::find($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LengthAwarePaginator<int, CmsMedia>
|
||||
*/
|
||||
private function resolveMediaItems(): LengthAwarePaginator
|
||||
{
|
||||
return CmsMedia::query()
|
||||
->when($this->type === 'image', fn ($q) => $q->images())
|
||||
->when($this->type === 'pdf', fn ($q) => $q->pdfs())
|
||||
->when($this->type === 'document', fn ($q) => $q->documents())
|
||||
->when($this->search, fn ($q) => $q->where('filename', 'like', "%{$this->search}%"))
|
||||
->orderByDesc('created_at')
|
||||
->paginate(18);
|
||||
}
|
||||
}
|
||||
39
dev/flux-cms/Admin/Cms/MediaUploader.php
Normal file
39
dev/flux-cms/Admin/Cms/MediaUploader.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
class MediaUploader extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
public string $field = '';
|
||||
|
||||
public string $accept = 'image/*';
|
||||
|
||||
public string $disk = 'public';
|
||||
|
||||
public string $directory = 'cms/uploads';
|
||||
|
||||
#[Validate('file|max:10240')]
|
||||
public $file;
|
||||
|
||||
public function updatedFile(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$path = $this->file->store($this->directory, $this->disk);
|
||||
|
||||
$this->dispatch('media-uploaded', field: $this->field, path: '/storage/'.$path);
|
||||
|
||||
$this->file = null;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.admin.cms.media-uploader');
|
||||
}
|
||||
}
|
||||
302
dev/flux-cms/Cms/CabinetDisplay.php
Normal file
302
dev/flux-cms/Cms/CabinetDisplay.php
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use App\Models\DisplayFooterContent;
|
||||
use App\Models\DisplayVideo;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Livewire\Component;
|
||||
|
||||
class CabinetDisplay extends Component
|
||||
{
|
||||
// Video-Verwaltung
|
||||
public $videoId = null;
|
||||
|
||||
public $videoFilename = '';
|
||||
|
||||
public $videoTitle = '';
|
||||
|
||||
public $videoPosition = 25;
|
||||
|
||||
public $videoIsActive = true;
|
||||
|
||||
public $showVideoModal = false;
|
||||
|
||||
public $availableVideos = [];
|
||||
|
||||
// Footer-Content-Verwaltung
|
||||
public $footerId = null;
|
||||
|
||||
public $footerHeadline = '';
|
||||
|
||||
public $footerSubline = '';
|
||||
|
||||
public $footerUrl = '';
|
||||
|
||||
public $footerIsActive = true;
|
||||
|
||||
public $showFooterModal = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->loadAvailableVideos();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt alle verfügbaren Video-Dateien aus dem assets-Ordner
|
||||
*/
|
||||
public function loadAvailableVideos()
|
||||
{
|
||||
$assetsPath = public_path('_cabinet/assets');
|
||||
|
||||
if (File::exists($assetsPath)) {
|
||||
$files = File::files($assetsPath);
|
||||
$this->availableVideos = collect($files)
|
||||
->map(fn ($file) => $file->getFilename())
|
||||
->filter(fn ($filename) => in_array(pathinfo($filename, PATHINFO_EXTENSION), ['mp4', 'webm', 'mov']))
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// VIDEO-VERWALTUNG
|
||||
// ========================================
|
||||
|
||||
public function openVideoModal($id = null)
|
||||
{
|
||||
if ($id) {
|
||||
$video = DisplayVideo::findOrFail($id);
|
||||
$this->videoId = $video->id;
|
||||
$this->videoFilename = $video->filename;
|
||||
$this->videoTitle = $video->title ?? '';
|
||||
$this->videoPosition = $video->position;
|
||||
$this->videoIsActive = $video->is_active;
|
||||
} else {
|
||||
$this->resetVideoForm();
|
||||
}
|
||||
$this->showVideoModal = true;
|
||||
}
|
||||
|
||||
public function saveVideo()
|
||||
{
|
||||
$this->validate([
|
||||
'videoFilename' => 'required|string',
|
||||
'videoPosition' => 'required|integer|min:0|max:100',
|
||||
], [
|
||||
'videoFilename.required' => 'Bitte wählen Sie ein Video aus.',
|
||||
'videoPosition.required' => 'Die Position ist erforderlich.',
|
||||
'videoPosition.min' => 'Die Position muss zwischen 0 und 100 liegen.',
|
||||
'videoPosition.max' => 'Die Position muss zwischen 0 und 100 liegen.',
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'filename' => $this->videoFilename,
|
||||
'title' => $this->videoTitle,
|
||||
'position' => $this->videoPosition,
|
||||
'is_active' => $this->videoIsActive,
|
||||
];
|
||||
|
||||
if ($this->videoId) {
|
||||
$video = DisplayVideo::findOrFail($this->videoId);
|
||||
$video->update($data);
|
||||
session()->flash('success', 'Video erfolgreich aktualisiert!');
|
||||
} else {
|
||||
$maxSortOrder = DisplayVideo::max('sort_order') ?? 0;
|
||||
$data['sort_order'] = $maxSortOrder + 1;
|
||||
DisplayVideo::create($data);
|
||||
session()->flash('success', 'Video erfolgreich hinzugefügt!');
|
||||
}
|
||||
|
||||
$this->closeVideoModal();
|
||||
}
|
||||
|
||||
public function deleteVideo($id)
|
||||
{
|
||||
DisplayVideo::findOrFail($id)->delete();
|
||||
session()->flash('success', 'Video erfolgreich gelöscht!');
|
||||
}
|
||||
|
||||
public function toggleVideoStatus($id)
|
||||
{
|
||||
$video = DisplayVideo::findOrFail($id);
|
||||
$video->update(['is_active' => ! $video->is_active]);
|
||||
}
|
||||
|
||||
public function moveVideo($id, $direction)
|
||||
{
|
||||
$video = DisplayVideo::findOrFail($id);
|
||||
$currentOrder = $video->sort_order;
|
||||
|
||||
if ($direction === 'up' && $currentOrder > 0) {
|
||||
$swapVideo = DisplayVideo::where('sort_order', $currentOrder - 1)->first();
|
||||
if ($swapVideo) {
|
||||
$video->update(['sort_order' => $currentOrder - 1]);
|
||||
$swapVideo->update(['sort_order' => $currentOrder]);
|
||||
}
|
||||
} elseif ($direction === 'down') {
|
||||
$swapVideo = DisplayVideo::where('sort_order', $currentOrder + 1)->first();
|
||||
if ($swapVideo) {
|
||||
$video->update(['sort_order' => $currentOrder + 1]);
|
||||
$swapVideo->update(['sort_order' => $currentOrder]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function resetVideoForm()
|
||||
{
|
||||
$this->videoId = null;
|
||||
$this->videoFilename = '';
|
||||
$this->videoTitle = '';
|
||||
$this->videoPosition = 25;
|
||||
$this->videoIsActive = true;
|
||||
}
|
||||
|
||||
public function closeVideoModal()
|
||||
{
|
||||
$this->showVideoModal = false;
|
||||
$this->resetVideoForm();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// FOOTER-CONTENT-VERWALTUNG
|
||||
// ========================================
|
||||
|
||||
public function openFooterModal($id = null)
|
||||
{
|
||||
if ($id) {
|
||||
$footer = DisplayFooterContent::findOrFail($id);
|
||||
$this->footerId = $footer->id;
|
||||
$this->footerHeadline = $footer->headline;
|
||||
$this->footerSubline = $footer->subline;
|
||||
$this->footerUrl = $footer->url;
|
||||
$this->footerIsActive = $footer->is_active;
|
||||
} else {
|
||||
$this->resetFooterForm();
|
||||
}
|
||||
$this->showFooterModal = true;
|
||||
}
|
||||
|
||||
public function saveFooter()
|
||||
{
|
||||
$this->validate([
|
||||
'footerHeadline' => 'required|string|max:255',
|
||||
'footerSubline' => 'required|string|max:255',
|
||||
'footerUrl' => 'nullable|url',
|
||||
], [
|
||||
'footerHeadline.required' => 'Die Überschrift ist erforderlich.',
|
||||
'footerSubline.required' => 'Die Unterzeile ist erforderlich.',
|
||||
'footerUrl.url' => 'Bitte geben Sie eine gültige URL ein.',
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'headline' => $this->footerHeadline,
|
||||
'subline' => $this->footerSubline,
|
||||
'url' => $this->footerUrl ?: null,
|
||||
'is_active' => $this->footerIsActive,
|
||||
];
|
||||
|
||||
if ($this->footerId) {
|
||||
$footer = DisplayFooterContent::findOrFail($this->footerId);
|
||||
$footer->update($data);
|
||||
|
||||
// Short-Code generieren falls URL vorhanden aber noch kein Short-Code
|
||||
if ($footer->url && ! $footer->short_code) {
|
||||
$footer->short_code = DisplayFooterContent::generateUniqueShortCode();
|
||||
$footer->save();
|
||||
}
|
||||
|
||||
session()->flash('success', 'Footer-Inhalt erfolgreich aktualisiert!');
|
||||
} else {
|
||||
$maxSortOrder = DisplayFooterContent::max('sort_order') ?? 0;
|
||||
$data['sort_order'] = $maxSortOrder + 1;
|
||||
|
||||
$footer = DisplayFooterContent::create($data);
|
||||
|
||||
// Short-Code nur generieren wenn URL vorhanden
|
||||
if ($footer->url) {
|
||||
$footer->short_code = DisplayFooterContent::generateUniqueShortCode();
|
||||
$footer->save();
|
||||
session()->flash('success', 'Footer-Inhalt erfolgreich hinzugefügt! Short-Link: '.$footer->short_url);
|
||||
} else {
|
||||
session()->flash('success', 'Footer-Inhalt erfolgreich hinzugefügt! (Ohne QR-Code)');
|
||||
}
|
||||
}
|
||||
|
||||
$this->closeFooterModal();
|
||||
}
|
||||
|
||||
public function regenerateShortCode($id)
|
||||
{
|
||||
$footer = DisplayFooterContent::findOrFail($id);
|
||||
$footer->short_code = DisplayFooterContent::generateUniqueShortCode();
|
||||
$footer->save();
|
||||
session()->flash('success', 'Short-Code wurde neu generiert!');
|
||||
}
|
||||
|
||||
public function resetClicks($id)
|
||||
{
|
||||
$footer = DisplayFooterContent::findOrFail($id);
|
||||
$footer->clicks = 0;
|
||||
$footer->save();
|
||||
session()->flash('success', 'Klick-Zähler wurde zurückgesetzt!');
|
||||
}
|
||||
|
||||
public function deleteFooter($id)
|
||||
{
|
||||
DisplayFooterContent::findOrFail($id)->delete();
|
||||
session()->flash('success', 'Footer-Inhalt erfolgreich gelöscht!');
|
||||
}
|
||||
|
||||
public function toggleFooterStatus($id)
|
||||
{
|
||||
$footer = DisplayFooterContent::findOrFail($id);
|
||||
$footer->update(['is_active' => ! $footer->is_active]);
|
||||
}
|
||||
|
||||
public function moveFooter($id, $direction)
|
||||
{
|
||||
$footer = DisplayFooterContent::findOrFail($id);
|
||||
$currentOrder = $footer->sort_order;
|
||||
|
||||
if ($direction === 'up' && $currentOrder > 0) {
|
||||
$swapFooter = DisplayFooterContent::where('sort_order', $currentOrder - 1)->first();
|
||||
if ($swapFooter) {
|
||||
$footer->update(['sort_order' => $currentOrder - 1]);
|
||||
$swapFooter->update(['sort_order' => $currentOrder]);
|
||||
}
|
||||
} elseif ($direction === 'down') {
|
||||
$swapFooter = DisplayFooterContent::where('sort_order', $currentOrder + 1)->first();
|
||||
if ($swapFooter) {
|
||||
$footer->update(['sort_order' => $currentOrder + 1]);
|
||||
$swapFooter->update(['sort_order' => $currentOrder]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function resetFooterForm()
|
||||
{
|
||||
$this->footerId = null;
|
||||
$this->footerHeadline = '';
|
||||
$this->footerSubline = '';
|
||||
$this->footerUrl = '';
|
||||
$this->footerIsActive = true;
|
||||
}
|
||||
|
||||
public function closeFooterModal()
|
||||
{
|
||||
$this->showFooterModal = false;
|
||||
$this->resetFooterForm();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$videos = DisplayVideo::orderBy('sort_order')->get();
|
||||
$footerContents = DisplayFooterContent::orderBy('sort_order')->get();
|
||||
|
||||
return view('livewire.admin.cms.cabinet-display', [
|
||||
'videos' => $videos,
|
||||
'footerContents' => $footerContents,
|
||||
]);
|
||||
}
|
||||
}
|
||||
189
dev/flux-cms/Cms/CabinetInfoTablet.php
Normal file
189
dev/flux-cms/Cms/CabinetInfoTablet.php
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use App\Models\CabinetTabletSetting;
|
||||
use Livewire\Component;
|
||||
|
||||
class CabinetInfoTablet extends Component
|
||||
{
|
||||
// Store status mode
|
||||
public string $storeStatus = 'auto';
|
||||
|
||||
public string $noticeHeadline = '';
|
||||
|
||||
public string $noticeSubtext = '';
|
||||
|
||||
// Override times for today
|
||||
public ?string $overrideOpenToday = '';
|
||||
|
||||
public ?string $overrideCloseToday = '';
|
||||
|
||||
// Appointment
|
||||
public ?string $nextAppointmentDate = null;
|
||||
|
||||
public ?string $nextAppointmentTime = '';
|
||||
|
||||
// Structured opening hours per weekday (open + close, empty = closed)
|
||||
public ?string $hoursMondayOpen = '10:00';
|
||||
|
||||
public ?string $hoursMondayClose = '18:00';
|
||||
|
||||
public ?string $hoursTuesdayOpen = '10:00';
|
||||
|
||||
public ?string $hoursTuesdayClose = '18:00';
|
||||
|
||||
public ?string $hoursWednesdayOpen = '10:00';
|
||||
|
||||
public ?string $hoursWednesdayClose = '18:00';
|
||||
|
||||
public ?string $hoursThursdayOpen = '10:00';
|
||||
|
||||
public ?string $hoursThursdayClose = '18:00';
|
||||
|
||||
public ?string $hoursFridayOpen = '10:00';
|
||||
|
||||
public ?string $hoursFridayClose = '18:00';
|
||||
|
||||
public ?string $hoursSaturdayOpen = '10:00';
|
||||
|
||||
public ?string $hoursSaturdayClose = '14:00';
|
||||
|
||||
public ?string $hoursSundayOpen = '';
|
||||
|
||||
public ?string $hoursSundayClose = '';
|
||||
|
||||
// Contact
|
||||
public string $contactPhone = '';
|
||||
|
||||
public string $contactEmail = '';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$s = CabinetTabletSetting::current();
|
||||
|
||||
$this->storeStatus = $s->store_status ?? 'auto';
|
||||
$this->noticeHeadline = $s->notice_headline ?? '';
|
||||
$this->noticeSubtext = $s->notice_subtext ?? '';
|
||||
$this->overrideOpenToday = $s->override_open_today ?? '';
|
||||
$this->overrideCloseToday = $s->override_close_today ?? '';
|
||||
$this->nextAppointmentDate = $s->next_appointment_date?->format('Y-m-d');
|
||||
$this->nextAppointmentTime = $s->next_appointment_time ?? '';
|
||||
$this->hoursMondayOpen = $s->hours_monday_open ?? '';
|
||||
$this->hoursMondayClose = $s->hours_monday_close ?? '';
|
||||
$this->hoursTuesdayOpen = $s->hours_tuesday_open ?? '';
|
||||
$this->hoursTuesdayClose = $s->hours_tuesday_close ?? '';
|
||||
$this->hoursWednesdayOpen = $s->hours_wednesday_open ?? '';
|
||||
$this->hoursWednesdayClose = $s->hours_wednesday_close ?? '';
|
||||
$this->hoursThursdayOpen = $s->hours_thursday_open ?? '';
|
||||
$this->hoursThursdayClose = $s->hours_thursday_close ?? '';
|
||||
$this->hoursFridayOpen = $s->hours_friday_open ?? '';
|
||||
$this->hoursFridayClose = $s->hours_friday_close ?? '';
|
||||
$this->hoursSaturdayOpen = $s->hours_saturday_open ?? '';
|
||||
$this->hoursSaturdayClose = $s->hours_saturday_close ?? '';
|
||||
$this->hoursSundayOpen = $s->hours_sunday_open ?? '';
|
||||
$this->hoursSundayClose = $s->hours_sunday_close ?? '';
|
||||
$this->contactPhone = $s->contact_phone ?? '';
|
||||
$this->contactEmail = $s->contact_email ?? '';
|
||||
}
|
||||
|
||||
private function timeRule(): array
|
||||
{
|
||||
return ['nullable', 'string', 'regex:/^(\d{2}:\d{2})?$/'];
|
||||
}
|
||||
|
||||
private function toNullIfEmpty(?string $value): ?string
|
||||
{
|
||||
return $value !== null && trim($value) !== '' ? trim($value) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $hours Optional: Time picker values from DOM (bypasses wire:model sync issues)
|
||||
*/
|
||||
public function save(array $hours = []): void
|
||||
{
|
||||
foreach ($hours as $prop => $value) {
|
||||
if (property_exists($this, $prop)) {
|
||||
$this->{$prop} = $value ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
$timeRule = $this->timeRule();
|
||||
|
||||
$this->validate([
|
||||
'storeStatus' => 'required|in:auto,notice,warning,closed',
|
||||
'noticeHeadline' => 'nullable|string|max:40',
|
||||
'noticeSubtext' => 'nullable|string|max:80',
|
||||
'overrideOpenToday' => $timeRule,
|
||||
'overrideCloseToday' => $timeRule,
|
||||
'nextAppointmentDate' => 'nullable|date',
|
||||
'nextAppointmentTime' => $timeRule,
|
||||
'hoursMondayOpen' => $timeRule,
|
||||
'hoursMondayClose' => $timeRule,
|
||||
'hoursTuesdayOpen' => $timeRule,
|
||||
'hoursTuesdayClose' => $timeRule,
|
||||
'hoursWednesdayOpen' => $timeRule,
|
||||
'hoursWednesdayClose' => $timeRule,
|
||||
'hoursThursdayOpen' => $timeRule,
|
||||
'hoursThursdayClose' => $timeRule,
|
||||
'hoursFridayOpen' => $timeRule,
|
||||
'hoursFridayClose' => $timeRule,
|
||||
'hoursSaturdayOpen' => $timeRule,
|
||||
'hoursSaturdayClose' => $timeRule,
|
||||
'hoursSundayOpen' => $timeRule,
|
||||
'hoursSundayClose' => $timeRule,
|
||||
'contactPhone' => 'nullable|string|max:50',
|
||||
'contactEmail' => 'nullable|email|max:100',
|
||||
], [
|
||||
'storeStatus.required' => 'Der Store-Status ist erforderlich.',
|
||||
'storeStatus.in' => 'Ungültiger Status. Erlaubt: auto, notice, warning, closed.',
|
||||
'noticeHeadline.max' => 'Die Headline darf maximal 40 Zeichen haben.',
|
||||
'noticeSubtext.max' => 'Der Subtext darf maximal 80 Zeichen haben.',
|
||||
'overrideOpenToday.regex' => 'Bitte im Format HH:MM eingeben.',
|
||||
'overrideCloseToday.regex' => 'Bitte im Format HH:MM eingeben.',
|
||||
'nextAppointmentTime.regex' => 'Bitte im Format HH:MM eingeben.',
|
||||
'contactEmail.email' => 'Bitte eine gültige E-Mail-Adresse eingeben.',
|
||||
]);
|
||||
CabinetTabletSetting::current()->update([
|
||||
'store_status' => $this->storeStatus,
|
||||
'notice_headline' => $this->toNullIfEmpty($this->noticeHeadline),
|
||||
'notice_subtext' => $this->toNullIfEmpty($this->noticeSubtext),
|
||||
'override_open_today' => $this->toNullIfEmpty($this->overrideOpenToday),
|
||||
'override_close_today' => $this->toNullIfEmpty($this->overrideCloseToday),
|
||||
'next_appointment_date' => $this->toNullIfEmpty($this->nextAppointmentDate),
|
||||
'next_appointment_time' => $this->toNullIfEmpty($this->nextAppointmentTime),
|
||||
'hours_monday_open' => $this->toNullIfEmpty($this->hoursMondayOpen),
|
||||
'hours_monday_close' => $this->toNullIfEmpty($this->hoursMondayClose),
|
||||
'hours_tuesday_open' => $this->toNullIfEmpty($this->hoursTuesdayOpen),
|
||||
'hours_tuesday_close' => $this->toNullIfEmpty($this->hoursTuesdayClose),
|
||||
'hours_wednesday_open' => $this->toNullIfEmpty($this->hoursWednesdayOpen),
|
||||
'hours_wednesday_close' => $this->toNullIfEmpty($this->hoursWednesdayClose),
|
||||
'hours_thursday_open' => $this->toNullIfEmpty($this->hoursThursdayOpen),
|
||||
'hours_thursday_close' => $this->toNullIfEmpty($this->hoursThursdayClose),
|
||||
'hours_friday_open' => $this->toNullIfEmpty($this->hoursFridayOpen),
|
||||
'hours_friday_close' => $this->toNullIfEmpty($this->hoursFridayClose),
|
||||
'hours_saturday_open' => $this->toNullIfEmpty($this->hoursSaturdayOpen),
|
||||
'hours_saturday_close' => $this->toNullIfEmpty($this->hoursSaturdayClose),
|
||||
'hours_sunday_open' => $this->toNullIfEmpty($this->hoursSundayOpen),
|
||||
'hours_sunday_close' => $this->toNullIfEmpty($this->hoursSundayClose),
|
||||
'contact_phone' => $this->toNullIfEmpty($this->contactPhone),
|
||||
'contact_email' => $this->toNullIfEmpty($this->contactEmail),
|
||||
]);
|
||||
|
||||
session()->flash('success', 'Info-Tablet Einstellungen gespeichert!');
|
||||
}
|
||||
|
||||
public function clearOverrides(): void
|
||||
{
|
||||
CabinetTabletSetting::current()->clearOverrides();
|
||||
$this->overrideOpenToday = '';
|
||||
$this->overrideCloseToday = '';
|
||||
|
||||
session()->flash('success', 'Sonderöffnungszeiten wurden zurückgesetzt!');
|
||||
}
|
||||
|
||||
public function render(): \Illuminate\View\View
|
||||
{
|
||||
return view('livewire.admin.cms.cabinet-info-tablet');
|
||||
}
|
||||
}
|
||||
153
dev/flux-cms/Cms/DisplayList.php
Normal file
153
dev/flux-cms/Cms/DisplayList.php
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use App\Models\Display;
|
||||
use App\Models\DisplayVersion;
|
||||
use Livewire\Component;
|
||||
|
||||
class DisplayList extends Component
|
||||
{
|
||||
public $showModal = false;
|
||||
|
||||
public $displayId = null;
|
||||
|
||||
public $displayName = '';
|
||||
|
||||
public $displayLocation = '';
|
||||
|
||||
/** @var array<int> */
|
||||
public $selectedVersionIds = [];
|
||||
|
||||
public $displayIsActive = true;
|
||||
|
||||
public $addVersionSelect = null;
|
||||
|
||||
public function openModal(?int $id = null): void
|
||||
{
|
||||
if ($id) {
|
||||
$display = Display::with('versions')->findOrFail($id);
|
||||
$this->displayId = $display->id;
|
||||
$this->displayName = $display->name;
|
||||
$this->displayLocation = $display->location ?? '';
|
||||
$this->selectedVersionIds = $display->versions->pluck('id')->toArray();
|
||||
$this->displayIsActive = $display->is_active;
|
||||
} else {
|
||||
$this->resetForm();
|
||||
}
|
||||
|
||||
$this->showModal = true;
|
||||
}
|
||||
|
||||
public function addVersion(?int $versionId = null): void
|
||||
{
|
||||
$id = $versionId ?? $this->addVersionSelect;
|
||||
|
||||
if ($id && ! in_array((int) $id, $this->selectedVersionIds)) {
|
||||
$this->selectedVersionIds[] = (int) $id;
|
||||
}
|
||||
|
||||
$this->addVersionSelect = null;
|
||||
}
|
||||
|
||||
public function removeVersion(int $index): void
|
||||
{
|
||||
array_splice($this->selectedVersionIds, $index, 1);
|
||||
}
|
||||
|
||||
public function moveVersion(int $index, string $direction): void
|
||||
{
|
||||
$newIndex = $direction === 'up' ? $index - 1 : $index + 1;
|
||||
|
||||
if ($newIndex < 0 || $newIndex >= count($this->selectedVersionIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$temp = $this->selectedVersionIds[$index];
|
||||
$this->selectedVersionIds[$index] = $this->selectedVersionIds[$newIndex];
|
||||
$this->selectedVersionIds[$newIndex] = $temp;
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->validate([
|
||||
'displayName' => 'required|string|max:255',
|
||||
'displayLocation' => 'nullable|string|max:255',
|
||||
'selectedVersionIds' => 'array',
|
||||
'selectedVersionIds.*' => 'exists:display_versions,id',
|
||||
], [
|
||||
'displayName.required' => 'Bitte geben Sie einen Namen ein.',
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'name' => $this->displayName,
|
||||
'location' => $this->displayLocation ?: null,
|
||||
'is_active' => $this->displayIsActive,
|
||||
];
|
||||
|
||||
if ($this->displayId) {
|
||||
$display = Display::findOrFail($this->displayId);
|
||||
$display->update($data);
|
||||
session()->flash('success', 'Display erfolgreich aktualisiert!');
|
||||
} else {
|
||||
$display = Display::create($data);
|
||||
session()->flash('success', 'Display erfolgreich erstellt!');
|
||||
}
|
||||
|
||||
// Sync versions with sort_order
|
||||
$syncData = [];
|
||||
foreach ($this->selectedVersionIds as $sortOrder => $versionId) {
|
||||
$syncData[$versionId] = ['sort_order' => $sortOrder];
|
||||
}
|
||||
$display->versions()->sync($syncData);
|
||||
|
||||
$this->closeModal();
|
||||
}
|
||||
|
||||
public function deleteDisplay(int $id): void
|
||||
{
|
||||
$display = Display::findOrFail($id);
|
||||
$name = $display->name;
|
||||
$display->delete();
|
||||
|
||||
session()->flash('success', 'Display "'.$name.'" wurde gelöscht!');
|
||||
}
|
||||
|
||||
public function toggleActive(int $id): void
|
||||
{
|
||||
$display = Display::findOrFail($id);
|
||||
$display->update(['is_active' => ! $display->is_active]);
|
||||
}
|
||||
|
||||
public function closeModal(): void
|
||||
{
|
||||
$this->showModal = false;
|
||||
$this->resetForm();
|
||||
}
|
||||
|
||||
public function resetForm(): void
|
||||
{
|
||||
$this->displayId = null;
|
||||
$this->displayName = '';
|
||||
$this->displayLocation = '';
|
||||
$this->selectedVersionIds = [];
|
||||
$this->displayIsActive = true;
|
||||
$this->addVersionSelect = null;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$displays = Display::with('versions')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
$versions = DisplayVersion::active()
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('livewire.admin.cms.display-list', [
|
||||
'displays' => $displays,
|
||||
'versions' => $versions,
|
||||
]);
|
||||
}
|
||||
}
|
||||
437
dev/flux-cms/Cms/DisplayVersionEditor.php
Normal file
437
dev/flux-cms/Cms/DisplayVersionEditor.php
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use App\Enums\DisplayVersionType;
|
||||
use App\Models\DisplayVersion;
|
||||
use App\Models\DisplayVersionItem;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Livewire\Component;
|
||||
|
||||
class DisplayVersionEditor extends Component
|
||||
{
|
||||
public DisplayVersion $version;
|
||||
|
||||
public string $versionName = '';
|
||||
|
||||
// Item Modal
|
||||
public bool $showItemModal = false;
|
||||
|
||||
public ?int $itemId = null;
|
||||
|
||||
public string $itemType = '';
|
||||
|
||||
// Video-Display: Video fields
|
||||
public string $videoFilename = '';
|
||||
|
||||
public string $videoTitle = '';
|
||||
|
||||
public int $videoPosition = 25;
|
||||
|
||||
public bool $videoIsActive = true;
|
||||
|
||||
// Video-Display: Footer fields
|
||||
public string $footerHeadline = '';
|
||||
|
||||
public string $footerSubline = '';
|
||||
|
||||
public string $footerUrl = '';
|
||||
|
||||
public bool $footerIsActive = true;
|
||||
|
||||
// B2in: Media fields
|
||||
public string $mediaType = 'image';
|
||||
|
||||
public string $mediaCategory = 'immobilien';
|
||||
|
||||
public string $mediaUrl = '';
|
||||
|
||||
public string $mediaHeadline = '';
|
||||
|
||||
public string $mediaSubline = '';
|
||||
|
||||
public int $mediaDuration = 10;
|
||||
|
||||
public bool $mediaIsActive = true;
|
||||
|
||||
// Offers: Slide fields
|
||||
public string $slideType = 'product-hero';
|
||||
|
||||
public int $slideDuration = 8000;
|
||||
|
||||
public string $slideImageUrl = '';
|
||||
|
||||
public string $slideBadge = '';
|
||||
|
||||
public string $slideEyebrow = '';
|
||||
|
||||
public string $slideTitle = '';
|
||||
|
||||
public string $slideSubline = '';
|
||||
|
||||
public string $slidePrice = '';
|
||||
|
||||
public string $slideOriginalPrice = '';
|
||||
|
||||
public string $slideTagText = '';
|
||||
|
||||
/** @var array<string> */
|
||||
public array $slideBullets = [];
|
||||
|
||||
public string $slideDisclaimer = '';
|
||||
|
||||
public string $slideQrUrl = '';
|
||||
|
||||
public string $slideQrTitle = '';
|
||||
|
||||
public string $slideContact = '';
|
||||
|
||||
public bool $slideShowBrandText = false;
|
||||
|
||||
public string $slideBrandTagline = '';
|
||||
|
||||
public bool $slideIsActive = true;
|
||||
|
||||
// Settings Modal
|
||||
public bool $showSettingsModal = false;
|
||||
|
||||
public array $settings = [];
|
||||
|
||||
/** @var array<string> */
|
||||
public array $availableVideos = [];
|
||||
|
||||
public function mount(DisplayVersion $displayVersion): void
|
||||
{
|
||||
$this->version = $displayVersion;
|
||||
$this->versionName = $displayVersion->name;
|
||||
$this->settings = $displayVersion->settings ?? [];
|
||||
|
||||
if ($this->version->type === DisplayVersionType::VideoDisplay) {
|
||||
$this->loadAvailableVideos();
|
||||
}
|
||||
}
|
||||
|
||||
public function loadAvailableVideos(): void
|
||||
{
|
||||
$assetsPath = public_path('_cabinet/assets');
|
||||
|
||||
if (File::exists($assetsPath)) {
|
||||
$this->availableVideos = collect(File::files($assetsPath))
|
||||
->map(fn ($file) => $file->getFilename())
|
||||
->filter(fn ($filename) => in_array(pathinfo($filename, PATHINFO_EXTENSION), ['mp4', 'webm', 'mov']))
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
public function toggleTheme(): void
|
||||
{
|
||||
$settings = $this->version->settings ?? [];
|
||||
$settings['theme'] = ($settings['theme'] ?? 'dark') === 'dark' ? 'light' : 'dark';
|
||||
$this->version->update(['settings' => $settings]);
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
public function saveName(): void
|
||||
{
|
||||
$this->validate([
|
||||
'versionName' => 'required|string|max:255',
|
||||
]);
|
||||
|
||||
$this->version->update(['name' => $this->versionName]);
|
||||
session()->flash('success', 'Name aktualisiert!');
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SETTINGS
|
||||
// ========================================
|
||||
|
||||
public function openSettingsModal(): void
|
||||
{
|
||||
$this->settings = $this->version->settings ?? [];
|
||||
$this->showSettingsModal = true;
|
||||
}
|
||||
|
||||
public function saveSettings(): void
|
||||
{
|
||||
$this->version->update(['settings' => $this->settings]);
|
||||
$this->showSettingsModal = false;
|
||||
session()->flash('success', 'Einstellungen gespeichert!');
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// ITEM CRUD
|
||||
// ========================================
|
||||
|
||||
public function openItemModal(?int $id = null, string $type = ''): void
|
||||
{
|
||||
$this->resetItemForm();
|
||||
|
||||
if ($id) {
|
||||
$item = DisplayVersionItem::findOrFail($id);
|
||||
$this->itemId = $item->id;
|
||||
$this->itemType = $item->item_type;
|
||||
$this->loadItemContent($item);
|
||||
} else {
|
||||
$this->itemType = $type ?: $this->defaultItemType();
|
||||
}
|
||||
|
||||
$this->showItemModal = true;
|
||||
}
|
||||
|
||||
public function saveItem(): void
|
||||
{
|
||||
$content = $this->buildItemContent();
|
||||
$isActive = $this->getActiveFlag();
|
||||
|
||||
if ($this->itemId) {
|
||||
$item = DisplayVersionItem::findOrFail($this->itemId);
|
||||
$item->update([
|
||||
'content' => $content,
|
||||
'is_active' => $isActive,
|
||||
]);
|
||||
session()->flash('success', 'Inhalt aktualisiert!');
|
||||
} else {
|
||||
$maxSort = DisplayVersionItem::where('display_version_id', $this->version->id)
|
||||
->where('item_type', $this->itemType)
|
||||
->max('sort_order') ?? -1;
|
||||
|
||||
DisplayVersionItem::create([
|
||||
'display_version_id' => $this->version->id,
|
||||
'item_type' => $this->itemType,
|
||||
'content' => $content,
|
||||
'sort_order' => $maxSort + 1,
|
||||
'is_active' => $isActive,
|
||||
]);
|
||||
session()->flash('success', 'Inhalt hinzugefügt!');
|
||||
}
|
||||
|
||||
$this->closeItemModal();
|
||||
}
|
||||
|
||||
public function deleteItem(int $id): void
|
||||
{
|
||||
DisplayVersionItem::findOrFail($id)->delete();
|
||||
session()->flash('success', 'Inhalt gelöscht!');
|
||||
}
|
||||
|
||||
public function toggleItemStatus(int $id): void
|
||||
{
|
||||
$item = DisplayVersionItem::findOrFail($id);
|
||||
$item->update(['is_active' => ! $item->is_active]);
|
||||
}
|
||||
|
||||
public function moveItem(int $id, string $direction): void
|
||||
{
|
||||
$item = DisplayVersionItem::findOrFail($id);
|
||||
$currentOrder = $item->sort_order;
|
||||
|
||||
$swapItem = DisplayVersionItem::where('display_version_id', $this->version->id)
|
||||
->where('item_type', $item->item_type)
|
||||
->where('sort_order', $direction === 'up' ? $currentOrder - 1 : $currentOrder + 1)
|
||||
->first();
|
||||
|
||||
if ($swapItem) {
|
||||
$item->update(['sort_order' => $swapItem->sort_order]);
|
||||
$swapItem->update(['sort_order' => $currentOrder]);
|
||||
}
|
||||
}
|
||||
|
||||
public function addBullet(): void
|
||||
{
|
||||
$this->slideBullets[] = '';
|
||||
}
|
||||
|
||||
public function removeBullet(int $index): void
|
||||
{
|
||||
unset($this->slideBullets[$index]);
|
||||
$this->slideBullets = array_values($this->slideBullets);
|
||||
}
|
||||
|
||||
public function closeItemModal(): void
|
||||
{
|
||||
$this->showItemModal = false;
|
||||
$this->resetItemForm();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// HELPERS
|
||||
// ========================================
|
||||
|
||||
private function loadItemContent(DisplayVersionItem $item): void
|
||||
{
|
||||
$content = $item->content;
|
||||
|
||||
match ($item->item_type) {
|
||||
'video' => $this->loadVideoContent($content),
|
||||
'footer' => $this->loadFooterContent($content),
|
||||
'media' => $this->loadMediaContent($content),
|
||||
'slide' => $this->loadSlideContent($content),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function loadVideoContent(array $content): void
|
||||
{
|
||||
$this->videoFilename = $content['filename'] ?? '';
|
||||
$this->videoTitle = $content['title'] ?? '';
|
||||
$this->videoPosition = $content['position'] ?? 25;
|
||||
$this->videoIsActive = true;
|
||||
}
|
||||
|
||||
private function loadFooterContent(array $content): void
|
||||
{
|
||||
$this->footerHeadline = $content['headline'] ?? '';
|
||||
$this->footerSubline = $content['subline'] ?? '';
|
||||
$this->footerUrl = $content['url'] ?? '';
|
||||
$this->footerIsActive = true;
|
||||
}
|
||||
|
||||
private function loadMediaContent(array $content): void
|
||||
{
|
||||
$this->mediaType = $content['media_type'] ?? 'image';
|
||||
$this->mediaCategory = $content['category'] ?? 'immobilien';
|
||||
$this->mediaUrl = $content['media_url'] ?? '';
|
||||
$this->mediaHeadline = $content['headline'] ?? '';
|
||||
$this->mediaSubline = $content['subline'] ?? '';
|
||||
$this->mediaDuration = $content['duration_seconds'] ?? 10;
|
||||
$this->mediaIsActive = true;
|
||||
}
|
||||
|
||||
private function loadSlideContent(array $content): void
|
||||
{
|
||||
$this->slideType = $content['type'] ?? 'product-hero';
|
||||
$this->slideDuration = $content['duration'] ?? 8000;
|
||||
$this->slideImageUrl = $content['image_url'] ?? '';
|
||||
$this->slideBadge = $content['badge_text'] ?? '';
|
||||
$this->slideEyebrow = $content['eyebrow'] ?? '';
|
||||
$this->slideTitle = $content['title'] ?? '';
|
||||
$this->slideSubline = $content['subline'] ?? '';
|
||||
$this->slidePrice = $content['price'] ?? '';
|
||||
$this->slideOriginalPrice = $content['original_price'] ?? '';
|
||||
$this->slideTagText = $content['tag_text'] ?? '';
|
||||
$this->slideBullets = $content['bullets'] ?? [];
|
||||
$this->slideDisclaimer = $content['disclaimer'] ?? '';
|
||||
$this->slideQrUrl = $content['qr_url'] ?? '';
|
||||
$this->slideQrTitle = $content['qr_title'] ?? '';
|
||||
$this->slideContact = $content['contact'] ?? '';
|
||||
$this->slideShowBrandText = $content['show_brand_text'] ?? false;
|
||||
$this->slideBrandTagline = $content['brand_tagline'] ?? '';
|
||||
$this->slideIsActive = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildItemContent(): array
|
||||
{
|
||||
return match ($this->itemType) {
|
||||
'video' => [
|
||||
'filename' => $this->videoFilename,
|
||||
'title' => $this->videoTitle,
|
||||
'position' => $this->videoPosition,
|
||||
],
|
||||
'footer' => [
|
||||
'headline' => $this->footerHeadline,
|
||||
'subline' => $this->footerSubline,
|
||||
'url' => $this->footerUrl ?: null,
|
||||
],
|
||||
'media' => [
|
||||
'media_type' => $this->mediaType,
|
||||
'category' => $this->mediaCategory,
|
||||
'media_url' => $this->mediaUrl,
|
||||
'headline' => $this->mediaHeadline,
|
||||
'subline' => $this->mediaSubline,
|
||||
'duration_seconds' => $this->mediaDuration,
|
||||
],
|
||||
'slide' => [
|
||||
'type' => $this->slideType,
|
||||
'duration' => $this->slideDuration,
|
||||
'image_url' => $this->slideImageUrl,
|
||||
'badge_text' => $this->slideBadge,
|
||||
'eyebrow' => $this->slideEyebrow,
|
||||
'title' => $this->slideTitle,
|
||||
'subline' => $this->slideSubline,
|
||||
'price' => $this->slidePrice,
|
||||
'original_price' => $this->slideOriginalPrice,
|
||||
'tag_text' => $this->slideTagText,
|
||||
'bullets' => $this->slideBullets,
|
||||
'disclaimer' => $this->slideDisclaimer,
|
||||
'qr_url' => $this->slideQrUrl,
|
||||
'qr_title' => $this->slideQrTitle,
|
||||
'contact' => $this->slideContact,
|
||||
'show_brand_text' => $this->slideShowBrandText,
|
||||
'brand_tagline' => $this->slideBrandTagline,
|
||||
],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
private function getActiveFlag(): bool
|
||||
{
|
||||
return match ($this->itemType) {
|
||||
'video' => $this->videoIsActive,
|
||||
'footer' => $this->footerIsActive,
|
||||
'media' => $this->mediaIsActive,
|
||||
'slide' => $this->slideIsActive,
|
||||
default => true,
|
||||
};
|
||||
}
|
||||
|
||||
private function defaultItemType(): string
|
||||
{
|
||||
return match ($this->version->type) {
|
||||
DisplayVersionType::VideoDisplay => 'video',
|
||||
DisplayVersionType::B2in => 'media',
|
||||
DisplayVersionType::Offers => 'slide',
|
||||
};
|
||||
}
|
||||
|
||||
private function resetItemForm(): void
|
||||
{
|
||||
$this->itemId = null;
|
||||
$this->itemType = '';
|
||||
$this->videoFilename = '';
|
||||
$this->videoTitle = '';
|
||||
$this->videoPosition = 25;
|
||||
$this->videoIsActive = true;
|
||||
$this->footerHeadline = '';
|
||||
$this->footerSubline = '';
|
||||
$this->footerUrl = '';
|
||||
$this->footerIsActive = true;
|
||||
$this->mediaType = 'image';
|
||||
$this->mediaCategory = 'immobilien';
|
||||
$this->mediaUrl = '';
|
||||
$this->mediaHeadline = '';
|
||||
$this->mediaSubline = '';
|
||||
$this->mediaDuration = 10;
|
||||
$this->mediaIsActive = true;
|
||||
$this->slideType = 'product-hero';
|
||||
$this->slideDuration = 8000;
|
||||
$this->slideImageUrl = '';
|
||||
$this->slideBadge = '';
|
||||
$this->slideEyebrow = '';
|
||||
$this->slideTitle = '';
|
||||
$this->slideSubline = '';
|
||||
$this->slidePrice = '';
|
||||
$this->slideOriginalPrice = '';
|
||||
$this->slideTagText = '';
|
||||
$this->slideBullets = [];
|
||||
$this->slideDisclaimer = '';
|
||||
$this->slideQrUrl = '';
|
||||
$this->slideQrTitle = '';
|
||||
$this->slideContact = '';
|
||||
$this->slideShowBrandText = false;
|
||||
$this->slideBrandTagline = '';
|
||||
$this->slideIsActive = true;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$items = $this->version->items()->get()->groupBy('item_type');
|
||||
|
||||
return view('livewire.admin.cms.display-version-editor', [
|
||||
'items' => $items,
|
||||
]);
|
||||
}
|
||||
}
|
||||
102
dev/flux-cms/Cms/DisplayVersionList.php
Normal file
102
dev/flux-cms/Cms/DisplayVersionList.php
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use App\Enums\DisplayVersionType;
|
||||
use App\Models\DisplayVersion;
|
||||
use Livewire\Component;
|
||||
|
||||
class DisplayVersionList extends Component
|
||||
{
|
||||
public $showCreateModal = false;
|
||||
|
||||
public $newName = '';
|
||||
|
||||
public $newType = '';
|
||||
|
||||
public function openCreateModal(): void
|
||||
{
|
||||
$this->newName = '';
|
||||
$this->newType = '';
|
||||
$this->showCreateModal = true;
|
||||
}
|
||||
|
||||
public function createVersion(): void
|
||||
{
|
||||
$this->validate([
|
||||
'newName' => 'required|string|max:255',
|
||||
'newType' => 'required|string|in:video-display,b2in,offers',
|
||||
], [
|
||||
'newName.required' => 'Bitte geben Sie einen Namen ein.',
|
||||
'newType.required' => 'Bitte wählen Sie einen Typ aus.',
|
||||
]);
|
||||
|
||||
$version = DisplayVersion::create([
|
||||
'name' => $this->newName,
|
||||
'type' => $this->newType,
|
||||
'settings' => $this->defaultSettingsForType($this->newType),
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->showCreateModal = false;
|
||||
$this->newName = '';
|
||||
$this->newType = '';
|
||||
|
||||
session()->flash('success', 'Version "'.$version->name.'" wurde erstellt!');
|
||||
|
||||
$this->redirect(
|
||||
route('admin.cms.display-version-edit', $version),
|
||||
navigate: true
|
||||
);
|
||||
}
|
||||
|
||||
public function deleteVersion(int $id): void
|
||||
{
|
||||
$version = DisplayVersion::findOrFail($id);
|
||||
$name = $version->name;
|
||||
$version->delete();
|
||||
|
||||
session()->flash('success', 'Version "'.$name.'" wurde gelöscht!');
|
||||
}
|
||||
|
||||
public function toggleActive(int $id): void
|
||||
{
|
||||
$version = DisplayVersion::findOrFail($id);
|
||||
$version->update(['is_active' => ! $version->is_active]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function defaultSettingsForType(string $type): array
|
||||
{
|
||||
return match ($type) {
|
||||
'b2in' => [
|
||||
'theme' => 'dark',
|
||||
'footer_name' => '',
|
||||
'footer_url' => '',
|
||||
'transition' => ['type' => 'crossfade', 'duration_ms' => 800],
|
||||
'default_image_duration' => 10,
|
||||
'rotation_weights' => ['immobilien' => 70, 'moebel' => 30],
|
||||
'display_active' => true,
|
||||
],
|
||||
'offers' => [
|
||||
'loop' => true,
|
||||
'transition' => ['type' => 'fade', 'duration' => 600],
|
||||
],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$versions = DisplayVersion::withCount(['items', 'displays'])
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('livewire.admin.cms.display-version-list', [
|
||||
'versions' => $versions,
|
||||
'types' => DisplayVersionType::cases(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
45
dev/flux-cms/Cms/MediaLibraryUploader.php
Normal file
45
dev/flux-cms/Cms/MediaLibraryUploader.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use FluxCms\Core\Services\MediaConversionService;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
class MediaLibraryUploader extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
/** @var array<\Livewire\Features\SupportFileUploads\TemporaryUploadedFile> */
|
||||
public array $uploads = [];
|
||||
|
||||
public function updatedUploads(): void
|
||||
{
|
||||
$this->validate([
|
||||
'uploads' => 'nullable|array|max:20',
|
||||
'uploads.*' => 'file|mimes:jpeg,jpg,png,gif,webp,svg,pdf,doc,docx|max:10240',
|
||||
]);
|
||||
|
||||
$service = app(MediaConversionService::class);
|
||||
|
||||
foreach ($this->uploads as $file) {
|
||||
$media = $service->storeUpload($file);
|
||||
$this->dispatch('media-library-uploaded', mediaId: $media->id);
|
||||
}
|
||||
|
||||
$this->uploads = [];
|
||||
}
|
||||
|
||||
public function removeUpload(int $index): void
|
||||
{
|
||||
if (isset($this->uploads[$index])) {
|
||||
unset($this->uploads[$index]);
|
||||
$this->uploads = array_values($this->uploads);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.admin.cms.media-library-uploader');
|
||||
}
|
||||
}
|
||||
139
dev/flux-cms/Cms/MediaPicker.php
Normal file
139
dev/flux-cms/Cms/MediaPicker.php
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin\Cms;
|
||||
|
||||
use FluxCms\Core\Models\CmsMedia;
|
||||
use FluxCms\Core\Services\MediaConversionService;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
class MediaPicker extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
use WithPagination;
|
||||
|
||||
public ?int $value = null;
|
||||
|
||||
public string $field = 'media_id';
|
||||
|
||||
public string $type = 'image';
|
||||
|
||||
public string $profile = 'thumbnail';
|
||||
|
||||
public string $label = 'Bild auswählen';
|
||||
|
||||
public bool $showModal = false;
|
||||
|
||||
public string $search = '';
|
||||
|
||||
/** @var array<\Livewire\Features\SupportFileUploads\TemporaryUploadedFile> */
|
||||
public array $quickUploads = [];
|
||||
|
||||
public function mount(?int $value = null): void
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function openPicker(): void
|
||||
{
|
||||
$this->showModal = true;
|
||||
}
|
||||
|
||||
public function selectMedia(int $id): void
|
||||
{
|
||||
$media = CmsMedia::find($id);
|
||||
if (! $media) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($media->isImage() && $this->profile) {
|
||||
$service = app(MediaConversionService::class);
|
||||
if (! $media->hasConversion($this->profile)) {
|
||||
$service->convert($media, $this->profile);
|
||||
$media->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
$this->value = $media->id;
|
||||
$this->showModal = false;
|
||||
|
||||
$this->dispatch('media-selected', field: $this->field, mediaId: $media->id, url: $media->getConversionUrl($this->profile));
|
||||
}
|
||||
|
||||
public function clearSelection(): void
|
||||
{
|
||||
$this->value = null;
|
||||
$this->dispatch('media-selected', field: $this->field, mediaId: null, url: null);
|
||||
}
|
||||
|
||||
public function updatedQuickUploads(): void
|
||||
{
|
||||
$this->validate([
|
||||
'quickUploads' => 'nullable|array|max:5',
|
||||
'quickUploads.*' => 'file|mimes:jpeg,jpg,png,gif,webp,svg,pdf,doc,docx|max:10240',
|
||||
]);
|
||||
|
||||
$service = app(MediaConversionService::class);
|
||||
$lastMedia = null;
|
||||
|
||||
foreach ($this->quickUploads as $file) {
|
||||
$lastMedia = $service->storeUpload($file);
|
||||
|
||||
if ($lastMedia->isImage() && $this->profile) {
|
||||
$service->convert($lastMedia, $this->profile);
|
||||
$lastMedia->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
$this->quickUploads = [];
|
||||
|
||||
if ($lastMedia) {
|
||||
$this->value = $lastMedia->id;
|
||||
$this->showModal = false;
|
||||
|
||||
$this->dispatch('media-selected', field: $this->field, mediaId: $lastMedia->id, url: $lastMedia->getConversionUrl($this->profile));
|
||||
}
|
||||
}
|
||||
|
||||
public function removeQuickUpload(int $index): void
|
||||
{
|
||||
if (isset($this->quickUploads[$index])) {
|
||||
unset($this->quickUploads[$index]);
|
||||
$this->quickUploads = array_values($this->quickUploads);
|
||||
}
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.admin.cms.media-picker', [
|
||||
'selectedMedia' => $this->resolveSelectedMedia(),
|
||||
'mediaItems' => $this->resolveMediaItems(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveSelectedMedia(): ?CmsMedia
|
||||
{
|
||||
if (! $this->value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return CmsMedia::find($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LengthAwarePaginator<int, CmsMedia>
|
||||
*/
|
||||
private function resolveMediaItems(): LengthAwarePaginator
|
||||
{
|
||||
return CmsMedia::query()
|
||||
->when($this->type === 'image', fn ($q) => $q->images())
|
||||
->when($this->type === 'pdf', fn ($q) => $q->pdfs())
|
||||
->when($this->type === 'document', fn ($q) => $q->documents())
|
||||
->when($this->search, fn ($q) => $q->where('filename', 'like', "%{$this->search}%"))
|
||||
->orderByDesc('created_at')
|
||||
->paginate(18);
|
||||
}
|
||||
}
|
||||
524
dev/flux-cms/PLAN.md
Normal file
524
dev/flux-cms/PLAN.md
Normal file
|
|
@ -0,0 +1,524 @@
|
|||
# Flux CMS Integration – Entwicklungsplan
|
||||
|
||||
> **Ziel:** Integration des flux-cms Packages für die b2in-Webseite.
|
||||
> **Scope:** Nur b2in – weitere Subseiten (b2a, stileigentum, style2own) folgen später.
|
||||
> **Stand:** 2026-03-18
|
||||
|
||||
---
|
||||
|
||||
## Ausgangslage
|
||||
|
||||
### Was existiert
|
||||
- **Package:** `packages/flux-cms/` (core, components, starter-components) liegt im Projekt
|
||||
- **Composer:** `packages/*/*` Repository-Pfad ist in `composer.json` konfiguriert
|
||||
- **Frontend:** Vollständige b2in-Webseite mit ~15 Seiten
|
||||
- **Content-Quelle:** `config/content.php` – statische PHP-Arrays pro Theme
|
||||
- **Content-Zugriff:** Livewire-Sections laden via `config("content.themes.{$theme}.{$section}")`
|
||||
- **Immobilien/Projekte:** Komplett in `config/content.php` definiert (Slug, Titel, Preise, Galerie, Quick Facts, Investment Case, Location etc.), Route `immobilien/{slug}` liest direkt aus Config
|
||||
- **Admin-Portal:** `portal.b2in.test` mit eigenem CMS (Cabinets/Displays), User-/Partner-Management
|
||||
- **Domain-System:** Multi-Domain-Setup in `config/domains.php`
|
||||
|
||||
### Was fehlt
|
||||
- flux-cms/core ist **nicht** als Composer-Dependency installiert
|
||||
- Keine `app/helpers.php` mit `cms()`/`tcms()`/`cms_media_url()`
|
||||
- Keine `config/flux-cms.php` publiziert
|
||||
- Keine `flux_cms_*` Datenbanktabellen
|
||||
- Keine Admin-Views/Routes für das CMS
|
||||
- Keine Medienbibliothek
|
||||
- Kein Immobilien-Model (Projekte leben in Config)
|
||||
|
||||
### Scope-Einschränkung Phase 1
|
||||
|
||||
**Nur diese Tabellen werden initial benötigt:**
|
||||
- `flux_cms_contents` – Alle Seiteninhalte (Key-Value mit Übersetzungen)
|
||||
- `flux_cms_media` – Medienbibliothek (Bilder, PDFs)
|
||||
|
||||
**Nicht benötigt (kommt später bei Bedarf):**
|
||||
- ~~`flux_cms_news_items`~~ – Nachrichteneinträge
|
||||
- ~~`flux_cms_industries`~~ – Branchen
|
||||
- ~~`flux_cms_faqs`~~ – FAQ-Einträge
|
||||
- ~~`flux_cms_downloads`~~ – Downloads
|
||||
- ~~`flux_cms_linkedin_posts`~~ – LinkedIn-Posts
|
||||
- ~~`flux_cms_search_index`~~ – Suchindex
|
||||
|
||||
**Zusätzlich benötigt:**
|
||||
- Neues **Immobilien/Projekte-Model** mit eigenem CRUD im CMS-Backend (ersetzt die statischen Projekte aus `config/content.php`)
|
||||
|
||||
---
|
||||
|
||||
## Phasen-Übersicht
|
||||
|
||||
| Phase | Beschreibung | Abhängig von |
|
||||
|-------|-------------|--------------|
|
||||
| **1** | Package-Installation & Infrastruktur | – |
|
||||
| **2** | Immobilien-Model & CRUD | Phase 1 |
|
||||
| **3** | Content-Migration (config → DB) | Phase 1 |
|
||||
| **4** | CMS Admin-Backend | Phase 1 |
|
||||
| **5** | Frontend-Umstellung (config → cms) | Phase 2, 3 |
|
||||
| **6** | Medienbibliothek & Bilder | Phase 1, 4 |
|
||||
| **7** | Tests | Phase 2–6 |
|
||||
| **8** | Feinschliff & Dokumentation | Phase 5–7 |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 – Package-Installation & Infrastruktur
|
||||
|
||||
### 1.1 Composer-Dependency installieren
|
||||
```bash
|
||||
composer require flux-cms/core:@dev
|
||||
composer require intervention/image
|
||||
```
|
||||
|
||||
**Prüfen:**
|
||||
- [ ] `FluxCmsServiceProvider` wird automatisch geladen
|
||||
- [ ] Keine Konflikte mit bestehenden Dependencies
|
||||
|
||||
### 1.2 Konfiguration publizieren
|
||||
```bash
|
||||
php artisan vendor:publish --tag=flux-cms-config
|
||||
```
|
||||
|
||||
**Anpassen in `config/flux-cms.php`:**
|
||||
- `default_locale` → `'de'`
|
||||
- `locales` → `['de' => 'Deutsch', 'en' => 'English']`
|
||||
- `media.profiles` → an b2in-Bildgrößen anpassen
|
||||
- `routes.enabled` → `false` (eigene Admin-Routes im Portal)
|
||||
|
||||
### 1.3 Migrations ausführen
|
||||
|
||||
**Nur die benötigten Migrations:**
|
||||
- `create_flux_cms_contents_table`
|
||||
- `create_flux_cms_media_table`
|
||||
|
||||
Die restlichen Migrations (news, industries, faqs, downloads, linkedin, search_index) werden **nicht ausgeführt** – sie liegen im Package und können bei Bedarf später migriert werden.
|
||||
|
||||
**Optionen:**
|
||||
- A) Alle Migrations laufen lassen (Tabellen existieren, werden aber nicht genutzt) – einfacher
|
||||
- B) Nur selektiv migrieren – sauberer, erfordert ggf. Anpassung am ServiceProvider
|
||||
|
||||
→ **Empfehlung: Option A** – leere Tabellen stören nicht und vereinfachen spätere Erweiterung.
|
||||
|
||||
```bash
|
||||
php artisan migrate
|
||||
```
|
||||
|
||||
### 1.4 Helper-Funktionen einrichten
|
||||
|
||||
**Erstellen:** `app/helpers.php` mit `cms()`, `tcms()`, `cms_media_url()`, `media_url()`
|
||||
|
||||
**Registrieren in `composer.json`:**
|
||||
```json
|
||||
"autoload": {
|
||||
"files": ["app/helpers.php"]
|
||||
}
|
||||
```
|
||||
```bash
|
||||
composer dump-autoload
|
||||
```
|
||||
|
||||
### 1.5 Storage-Link
|
||||
```bash
|
||||
php artisan storage:link
|
||||
```
|
||||
|
||||
**Ergebnis Phase 1:** Package installiert, DB-Tabellen vorhanden, Helper verfügbar.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 – Immobilien/Projekte-Model & CRUD
|
||||
|
||||
### 2.1 Datenstruktur analysieren
|
||||
|
||||
Aktuelle Projekt-Daten aus `config/content.php` (am Beispiel Azizi Creek Views 4):
|
||||
|
||||
| Feld | Typ | Beispiel |
|
||||
|------|-----|---------|
|
||||
| `slug` | string | `azizi-creek-views-4` |
|
||||
| `title` | string | `Azizi Developments: Creek Views 4` |
|
||||
| `location` | string | `Al Jaddaf, Dubai` |
|
||||
| `status` | string | `NEW LAUNCH` |
|
||||
| `launch_date` | string | `03.03.2026` |
|
||||
| `price_from` | integer (AED) | `1125000` |
|
||||
| `image` | string (Pfad) | `expose/a1/image-4.jpeg` |
|
||||
| `highlights` | array\<string\> | `['Prime Waterfront Views', ...]` |
|
||||
| `quick_facts` | array\<{icon, label, value}\> | Typen, Größe, Einheiten, Entwickler |
|
||||
| `investment_case` | object | `{title, text, views[]}` |
|
||||
| `gallery` | array\<string\> | Bildpfade |
|
||||
| `location_info` | object | `{title, map_url, points[]}` |
|
||||
| `contact` | object | `{title, subtitle, options[]}` |
|
||||
|
||||
### 2.2 Migration erstellen
|
||||
|
||||
Neue Migration `create_cms_projects_table`:
|
||||
|
||||
```php
|
||||
Schema::create('cms_projects', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('slug')->unique();
|
||||
$table->json('title'); // translatable
|
||||
$table->json('location'); // translatable
|
||||
$table->string('status')->nullable();
|
||||
$table->date('launch_date')->nullable();
|
||||
$table->unsignedInteger('price_from_aed')->nullable();
|
||||
$table->string('currency')->default('AED');
|
||||
$table->string('image')->nullable(); // CmsMedia Referenz
|
||||
$table->json('highlights')->nullable(); // translatable array
|
||||
$table->json('quick_facts')->nullable(); // [{icon, label, value}]
|
||||
$table->json('investment_case')->nullable(); // {title, text, views[]}
|
||||
$table->json('gallery')->nullable(); // [filename, ...]
|
||||
$table->json('location_info')->nullable(); // {title, map_url, points[]}
|
||||
$table->json('contact')->nullable(); // {title, subtitle, options[]}
|
||||
$table->boolean('is_published')->default(false);
|
||||
$table->unsignedInteger('order')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
```
|
||||
|
||||
### 2.3 Model erstellen
|
||||
|
||||
`App\Models\CmsProject` mit:
|
||||
- `HasTranslations` (Spatie) für `title`, `location`, `highlights`
|
||||
- Scopes: `published()`, `ordered()`
|
||||
- `toFrontendArray()` → kompatibles Array für bestehende Blade-Views
|
||||
- `getFormattedPrice()` → nutzt `PriceHelper::formatAed()`
|
||||
- Accessors für `gallery_urls`, `image_url` etc.
|
||||
|
||||
### 2.4 Factory & Seeder
|
||||
|
||||
- **Factory:** `CmsProjectFactory` für Tests
|
||||
- **Seeder:** `CmsProjectSeeder` – importiert die bestehenden Projekte aus `config/content.php` in die DB
|
||||
|
||||
### 2.5 Admin CRUD (Volt-Komponente)
|
||||
|
||||
Admin-View `admin.cms.projects-index`:
|
||||
- Liste aller Projekte (Titel, Status, Preis, Published, Order)
|
||||
- Erstellen/Bearbeiten Modal oder Inline
|
||||
- Felder: alle aus 2.2
|
||||
- Galerie-Management via MediaPicker
|
||||
- Bild-Upload via MediaLibraryUploader
|
||||
- Sortierung per Drag & Drop oder Order-Feld
|
||||
- Publish/Unpublish Toggle
|
||||
|
||||
### 2.6 Route für Immobilien-Show anpassen
|
||||
|
||||
```php
|
||||
// Vorher:
|
||||
Route::get('/immobilien/{slug}', function (string $slug) {
|
||||
$project = config("content.themes.{$theme}.immobilien_projects.projects.{$slug}");
|
||||
...
|
||||
});
|
||||
|
||||
// Nachher:
|
||||
Route::get('/immobilien/{slug}', function (string $slug) {
|
||||
$project = CmsProject::where('slug', $slug)->published()->firstOrFail();
|
||||
return view('web.immobilien-show', ['project' => $project->toFrontendArray()]);
|
||||
});
|
||||
```
|
||||
|
||||
**Ergebnis Phase 2:** Immobilien-Projekte in DB, editierbar im CMS, Frontend nutzt Model.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 – Content-Migration (config → DB)
|
||||
|
||||
### 3.1 Content-Struktur analysieren
|
||||
|
||||
Die `config/content.php` enthält das b2in-Theme mit folgenden Sektionen:
|
||||
|
||||
| Page | Section | Typ |
|
||||
|------|---------|-----|
|
||||
| global | `announcement_bar` | Text + Links |
|
||||
| global | `header` | Navigation |
|
||||
| global | `footer` | Text + Links |
|
||||
| home | `hero` | Text + Bild + Stats |
|
||||
| home | `founder_bar` | Text + Bild |
|
||||
| home | `synergie_section` | Text + Bild |
|
||||
| home | `vision_section` | Text + Bild |
|
||||
| home | `ecosystem_core` | Text + Items |
|
||||
| home | `cta_section` | Text + Link |
|
||||
| home | `brand_worlds` | Text + Items |
|
||||
| about | `about_hero`, `our_story`, `our_values`, `leadership_team` | Diverse |
|
||||
| immobilien | `immobilien_hero_v2`, `immobilien_warum_dubai`, `immobilien_kaufprozess`, `immobilien_bruecke`, `immobilien_mindset`, `immobilien_moebel_vorteil` | Diverse |
|
||||
| faq | FAQ-Kategorien | Q&A |
|
||||
| netzwerk | Hero, Stats, Sections | Diverse |
|
||||
| contact | Form-Info | Text |
|
||||
| impressum/privacy/terms/cookie-policy | Langtext | HTML |
|
||||
|
||||
### 3.2 CmsContentSeeder erstellen
|
||||
|
||||
**Strategie:** Alle b2in-Inhalte aus `config/content.php` in `flux_cms_contents` überführen.
|
||||
|
||||
- **Key-Schema:** `{page}.{section}.{field}` (z.B. `home.hero.title`, `about.our_story.title`)
|
||||
- **Typen:** `text`, `html`, `image`, `json` (für Arrays wie `pillars`, `stats`, `navigation`)
|
||||
- **Gruppen:** `home`, `about`, `immobilien`, `netzwerk`, `faq`, `contact`, `impressum`, `privacy`, `terms`, `cookie_policy`, `global` (Header, Footer)
|
||||
|
||||
**Datei:** `database/seeders/CmsContentSeeder.php` – liest `config/content.php` und schreibt in DB.
|
||||
|
||||
### 3.3 Seeders ausführen & verifizieren
|
||||
```bash
|
||||
php artisan db:seed --class=CmsContentSeeder
|
||||
```
|
||||
|
||||
**Ergebnis Phase 3:** Alle b2in-Inhalte in der DB, abrufbar über `cms('home.hero.title')`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 – CMS Admin-Backend
|
||||
|
||||
### 4.1 CMS als eigener Menüpunkt
|
||||
|
||||
Das CMS wird im Admin-Portal (`portal.b2in.test`) als **eigener Top-Level-Menüpunkt "CMS"** in der Sidebar integriert.
|
||||
|
||||
**Sidebar-Erweiterung** (`resources/views/components/layouts/app/sidebar.blade.php`):
|
||||
```
|
||||
CMS
|
||||
├── Dashboard (Übersicht: Anzahl Contents, Medien, Projekte)
|
||||
├── Inhalte (Content-Editor für Text/HTML/Image/JSON Keys)
|
||||
├── Projekte (Immobilien-CRUD – aus Phase 2)
|
||||
├── Medienbibliothek (Upload, Grid, Conversions)
|
||||
```
|
||||
|
||||
### 4.2 CMS-Layout
|
||||
|
||||
**Entscheidung:** Die CMS-Views nutzen das bestehende Admin-Portal-Layout (`admin-master.blade.php` / `app.blade.php`), damit Navigation und User-Menü konsistent bleiben.
|
||||
|
||||
→ Das Reference-Layout `layout-cms.blade.php` wird **nicht** als separates Layout genutzt, sondern als Vorlage für die Content-Struktur innerhalb des bestehenden Layouts.
|
||||
|
||||
### 4.3 Admin-Views erstellen
|
||||
|
||||
Aus den Package-Referenz-Views nur die benötigten kopieren und anpassen:
|
||||
|
||||
| View | Quelle | Ziel |
|
||||
|------|--------|------|
|
||||
| Dashboard | `admin-reference/cms/dashboard.blade.php` | `livewire/admin/cms/dashboard.blade.php` |
|
||||
| Content-Editor | `admin-reference/cms/content-index.blade.php` | `livewire/admin/cms/content-index.blade.php` |
|
||||
| Medienbibliothek | `admin-reference/cms/media-index.blade.php` | `livewire/admin/cms/media-index.blade.php` |
|
||||
| MediaPicker | `admin-reference/cms/media-picker.blade.php` | `livewire/admin/cms/media-picker.blade.php` |
|
||||
| MediaUploader | `admin-reference/cms/media-library-uploader.blade.php` | `livewire/admin/cms/media-library-uploader.blade.php` |
|
||||
| **Projekte** | **Neu erstellen** | `livewire/admin/cms/projects-index.blade.php` |
|
||||
|
||||
**Nicht benötigt (vorerst):** news-index, industries-index, faqs-index, linkedin-index, downloads-index, team-index, search-index
|
||||
|
||||
### 4.4 Livewire-Komponenten einrichten
|
||||
|
||||
```bash
|
||||
mkdir -p app/Livewire/Admin/Cms/
|
||||
```
|
||||
|
||||
Aus Package kopieren + Namespace anpassen:
|
||||
- `MediaLibraryUploader.php` → `App\Livewire\Admin\Cms`
|
||||
- `MediaPicker.php` → `App\Livewire\Admin\Cms`
|
||||
- `MediaUploader.php` → `App\Livewire\Admin\Cms`
|
||||
|
||||
### 4.5 Admin-Routes registrieren
|
||||
|
||||
In `routes/admin.php` innerhalb der bestehenden `auth`-Middleware-Gruppe:
|
||||
|
||||
```php
|
||||
// Flux CMS Routes
|
||||
Volt::route('admin/cms', 'admin.cms.dashboard')->name('cms.dashboard');
|
||||
Volt::route('admin/cms/content', 'admin.cms.content-index')->name('cms.content.index');
|
||||
Volt::route('admin/cms/media', 'admin.cms.media-index')->name('cms.media.index');
|
||||
Volt::route('admin/cms/projects', 'admin.cms.projects-index')->name('cms.projects.index');
|
||||
```
|
||||
|
||||
**Ergebnis Phase 4:** Funktionierendes Admin-Backend unter `portal.b2in.test/admin/cms` mit Dashboard, Content-Editor, Medienbibliothek und Projekte-CRUD.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 – Frontend-Umstellung (config → cms)
|
||||
|
||||
### 5.1 Strategie: Dualer Betrieb mit Fallback
|
||||
|
||||
Die Umstellung erfolgt inkrementell. Jede Section einzeln umstellen, mit Fallback auf `config()`:
|
||||
|
||||
```php
|
||||
// Vorher (Hero.php):
|
||||
$this->content = config("content.themes.{$theme}.hero", []);
|
||||
|
||||
// Nachher:
|
||||
$this->content = $this->loadFromCms('home.hero');
|
||||
|
||||
// Fallback-Methode in einem Trait oder Base-Class:
|
||||
protected function loadFromCms(string $group): array
|
||||
{
|
||||
$cmsContent = app(CmsContentService::class)->getGroup($group);
|
||||
if (empty($cmsContent)) {
|
||||
$theme = config('app.theme', 'b2in');
|
||||
$section = Str::afterLast($group, '.');
|
||||
return config("content.themes.{$theme}.{$section}", []);
|
||||
}
|
||||
return $cmsContent;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Sections schrittweise umstellen
|
||||
|
||||
**Reihenfolge (nach Priorität / Sichtbarkeit):**
|
||||
|
||||
1. **Home-Page Sections:**
|
||||
- [ ] Hero → `home.hero`
|
||||
- [ ] FounderBar → `home.founder_bar`
|
||||
- [ ] ContentSection → `home.{section}` (dynamisch)
|
||||
- [ ] VisionSection → `home.vision_section`
|
||||
- [ ] EcosystemCore → `home.ecosystem_core`
|
||||
- [ ] CTASection → `home.cta_section`
|
||||
|
||||
2. **Globale Elemente:**
|
||||
- [ ] Header (Navigation) → `global.header`
|
||||
- [ ] Footer → `global.footer`
|
||||
- [ ] AnnouncementBar → `global.announcement_bar`
|
||||
|
||||
3. **Immobilien-Seite (statischer Content):**
|
||||
- [ ] HeroV2 → `immobilien.hero_v2`
|
||||
- [ ] WarumDubai → `immobilien.warum_dubai`
|
||||
- [ ] Kaufprozess → `immobilien.kaufprozess`
|
||||
- [ ] Brücke → `immobilien.bruecke`
|
||||
- [ ] Mindset → `immobilien.mindset`
|
||||
- [ ] MöbelVorteil → `immobilien.moebel_vorteil`
|
||||
- [ ] Projekte-Liste → **CmsProject::published()->ordered()** (aus Phase 2)
|
||||
|
||||
4. **Unterseiten:**
|
||||
- [ ] About
|
||||
- [ ] FAQ
|
||||
- [ ] Contact
|
||||
- [ ] Impressum / Privacy / Terms / Cookie-Policy
|
||||
- [ ] Netzwerk
|
||||
- [ ] Service / Portfolio
|
||||
|
||||
### 5.3 Immobilien-Route umstellen
|
||||
|
||||
```php
|
||||
// routes/web.php – von config auf Model:
|
||||
Route::get('/immobilien/{slug}', function (string $slug) {
|
||||
$project = \App\Models\CmsProject::where('slug', $slug)
|
||||
->published()
|
||||
->firstOrFail();
|
||||
|
||||
return view('web.immobilien-show', ['project' => $project->toFrontendArray()]);
|
||||
})->name('immobilien.show');
|
||||
```
|
||||
|
||||
Die `immobilien.blade.php` Projekte-Liste ebenfalls umstellen:
|
||||
```php
|
||||
// Vorher: $projects = config("content.themes.{$theme}.immobilien_projects", []);
|
||||
// Nachher: $projects = CmsProject::published()->ordered()->get();
|
||||
```
|
||||
|
||||
### 5.4 Bilder umstellen
|
||||
|
||||
```diff
|
||||
- asset('img/assets/' . $heroV2['image'])
|
||||
+ cms_media_url('immobilien.hero_v2.image', 'hero')
|
||||
```
|
||||
|
||||
**Ergebnis Phase 5:** b2in-Frontend liest Inhalte aus DB + CmsProject-Model, editierbar über Admin.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 – Medienbibliothek & Bilder
|
||||
|
||||
### 6.1 Bestehende Bilder importieren
|
||||
|
||||
Alle b2in-Bilder (aus `public/img/assets/`) in die CMS-Medienbibliothek importieren:
|
||||
- `b2in/` – allgemeine b2in-Bilder (Hero, Founder, Sections)
|
||||
- `expose/` – Immobilien-Projektbilder
|
||||
|
||||
### 6.2 CmsMediaSeeder
|
||||
|
||||
Seeder erstellt `CmsMedia`-Einträge für alle bestehenden Bilder und verknüpft sie mit den CmsContent-Keys vom Typ `image`.
|
||||
|
||||
### 6.3 Bildprofile definieren
|
||||
|
||||
In `config/flux-cms.php`:
|
||||
- `hero` → 1920×800 (Hero-Banner)
|
||||
- `card` → 768×512 (Kacheln, Sections)
|
||||
- `thumbnail` → 400×300 (Listen, Übersichten)
|
||||
- `avatar` → 400×400 (Team-Fotos, Founder)
|
||||
- `gallery` → 1200×900 (Projekt-Galerie)
|
||||
- `og_image` → 1200×630 (Social Sharing)
|
||||
|
||||
### 6.4 Conversions generieren
|
||||
|
||||
```bash
|
||||
php artisan flux-cms:clear-cache
|
||||
```
|
||||
|
||||
**Ergebnis Phase 6:** Alle Bilder in Medienbibliothek, Conversions generiert, URLs aufgelöst.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7 – Tests
|
||||
|
||||
### 7.1 Referenz-Tests kopieren (selektiv)
|
||||
```bash
|
||||
mkdir -p tests/Feature/Cms
|
||||
cp packages/flux-cms/core/tests-reference/Feature/Cms/CmsMediaTest.php tests/Feature/Cms/
|
||||
cp packages/flux-cms/core/tests-reference/Feature/Cms/CmsContentServiceTest.php tests/Feature/Cms/
|
||||
```
|
||||
|
||||
### 7.2 Projektspezifische Tests
|
||||
|
||||
- **CmsProjectTest** – CRUD, published/unpublished, toFrontendArray(), Validierung
|
||||
- **CmsContentSeederTest** – Prüft ob alle Keys aus config in DB existieren
|
||||
- **CmsAdminAccessTest** – Prüft Zugriffskontrolle auf CMS-Admin-Routen
|
||||
- **ImmobilienRouteTest** – Prüft `/immobilien/{slug}` mit DB-Daten
|
||||
- **FrontendFallbackTest** – Prüft dualen Betrieb (CMS → config Fallback)
|
||||
- **MediaIntegrationTest** – Upload, Conversion, URL-Auflösung
|
||||
|
||||
### 7.3 Tests ausführen
|
||||
```bash
|
||||
php artisan test --compact --filter=Cms
|
||||
php artisan test --compact --filter=Immobilien
|
||||
```
|
||||
|
||||
**Ergebnis Phase 7:** Alle CMS-Funktionalitäten sind getestet.
|
||||
|
||||
---
|
||||
|
||||
## Phase 8 – Feinschliff & Dokumentation
|
||||
|
||||
### 8.1 Cache & Performance
|
||||
- CMS-Content-Cache aktivieren
|
||||
- Eager Loading optimieren
|
||||
- Prüfen: Keine N+1-Queries auf Frontseiten
|
||||
|
||||
### 8.2 config/content.php aufräumen
|
||||
- Nach vollständiger Umstellung: b2in-Theme-Daten als `@deprecated` markieren
|
||||
- Noch nicht entfernen (Fallback für andere Themes!)
|
||||
- Immobilien-Projekte aus Config entfernen (leben jetzt in DB)
|
||||
|
||||
### 8.3 Sidebar-Berechtigungen
|
||||
- CMS-Zugang über Spatie-Permission absichern (`permission:manage-cms`)
|
||||
- CMS-Menüpunkt nur für berechtigte User anzeigen
|
||||
|
||||
### 8.4 Dokumentation
|
||||
- Diesen Plan aktualisieren mit Status
|
||||
- Notizen für Integration weiterer Subseiten (b2a, stileigentum, style2own)
|
||||
|
||||
---
|
||||
|
||||
## Offene Fragen / Entscheidungen
|
||||
|
||||
| # | Frage | Entscheidung |
|
||||
|---|-------|-------------|
|
||||
| 1 | Alle flux-cms Migrations laufen lassen oder nur contents + media? | Empfehlung: Alle (leere Tabellen stören nicht) |
|
||||
| 2 | CmsProject: Eigene Migration oder flux-cms erweitern? | Eigene Migration im Projekt |
|
||||
| 3 | FAQ-Daten: Vorerst in config belassen oder direkt in `flux_cms_faqs`? | Vorerst config |
|
||||
| 4 | Magazin: Eigenes System belassen oder später auf CMS? | Vorerst belassen |
|
||||
| 5 | Andere Themes (b2a etc.) weiterhin via `config()`? | Ja |
|
||||
|
||||
---
|
||||
|
||||
## Fortschritt
|
||||
|
||||
| Phase | Status | Notizen |
|
||||
|-------|--------|---------|
|
||||
| 1 – Installation & Infrastruktur | ✅ Fertig | 2026-03-18: Package installiert, Config angepasst, Migrations gelaufen, Helpers eingerichtet, 434 Tests grün |
|
||||
| 2 – Immobilien-Model & CRUD | ✅ Fertig | 2026-03-18: Migration, Model, Factory, Seeder, 9 Tests grün. CRUD Admin-View folgt in Phase 4. |
|
||||
| 3 – Content-Migration | ✅ Fertig | 2026-03-18: 61 Sections in 8 Gruppen migriert, Section-als-JSON-Ansatz, 8 Tests grün |
|
||||
| 4 – CMS Admin-Backend | ✅ Fertig | 2026-03-18: Dashboard, Content-Editor, Projekte-CRUD, Medienbibliothek, Sidebar-Menü, MediaPicker/Uploader, 16 Tests grün |
|
||||
| 5 – Frontend-Umstellung | ⬜ Offen | |
|
||||
| 6 – Medienbibliothek | ⬜ Offen | |
|
||||
| 7 – Tests | ⬜ Offen | |j
|
||||
| 8 – Feinschliff | ⬜ Offen | |
|
||||
106
dev/flux-cms/helpers.php
Normal file
106
dev/flux-cms/helpers.php
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<?php
|
||||
|
||||
use FluxCms\Core\Models\CmsMedia;
|
||||
use FluxCms\Core\Services\CmsContentService;
|
||||
|
||||
if (! function_exists('legal_page')) {
|
||||
/**
|
||||
* Rechtstexte: zuerst CMS (Gruppe „legal“), sonst Lang-Datei b2in_legal.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function legal_page(string $key): array
|
||||
{
|
||||
$fromCms = app(CmsContentService::class)->get('legal.'.$key);
|
||||
|
||||
if (is_array($fromCms) && isset($fromCms['content'])) {
|
||||
return $fromCms;
|
||||
}
|
||||
|
||||
return trans('b2in_legal.'.$key);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('cms')) {
|
||||
/**
|
||||
* Resolve a CMS content value by dot-notation key.
|
||||
*
|
||||
* First segment is the group, rest is the key: "home.hero.title"
|
||||
* Falls back to __() if no DB entry exists.
|
||||
*
|
||||
* @param array<string, string> $replace
|
||||
*/
|
||||
function cms(string $key, array $replace = [], ?string $locale = null): mixed
|
||||
{
|
||||
return app(CmsContentService::class)->get($key, $replace, $locale);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('tcms')) {
|
||||
/**
|
||||
* Typed CMS – always returns a string.
|
||||
*
|
||||
* @param array<string, string> $replace
|
||||
*/
|
||||
function tcms(string $key, array $replace = [], ?string $locale = null): string
|
||||
{
|
||||
$text = cms($key, $replace, $locale);
|
||||
|
||||
return is_string($text) ? $text : (string) $text;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('cms_media_url')) {
|
||||
/**
|
||||
* Resolve a CMS content key (type=image) to a full media URL.
|
||||
* Falls back to asset('assets/images/...') if not found in media library.
|
||||
*/
|
||||
function cms_media_url(string $key, string $profile = ''): string
|
||||
{
|
||||
$filename = cms($key);
|
||||
|
||||
if (! $filename || ! is_string($filename) || $filename === $key) {
|
||||
$fallback = str_replace('.', '/', $key);
|
||||
|
||||
return asset('assets/images/'.basename($fallback));
|
||||
}
|
||||
|
||||
return media_url($filename, $profile);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('media_url')) {
|
||||
/**
|
||||
* Resolve a CmsMedia filename to its full storage URL.
|
||||
* Uses in-memory cache to avoid repeated DB queries.
|
||||
*/
|
||||
function media_url(?string $filename, string $profile = ''): string
|
||||
{
|
||||
if (! $filename || $filename === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
static $resolved = [];
|
||||
$cacheKey = $filename.'|'.$profile;
|
||||
|
||||
if (isset($resolved[$cacheKey])) {
|
||||
return $resolved[$cacheKey];
|
||||
}
|
||||
|
||||
$media = CmsMedia::where('filename', $filename)->first();
|
||||
|
||||
if (! $media) {
|
||||
$resolved[$cacheKey] = asset('assets/images/'.$filename);
|
||||
|
||||
return $resolved[$cacheKey];
|
||||
}
|
||||
|
||||
if ($profile && $media->hasConversion($profile)) {
|
||||
$resolved[$cacheKey] = $media->getConversionUrl($profile);
|
||||
} else {
|
||||
$resolved[$cacheKey] = $media->getUrl();
|
||||
}
|
||||
|
||||
return $resolved[$cacheKey];
|
||||
}
|
||||
}
|
||||
15
dev/flux-cms/tasks.md
Normal file
15
dev/flux-cms/tasks.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
### Aufgabe
|
||||
|
||||
Integration des selbst entwickelten Packages flux-cms
|
||||
packages/flux-cms
|
||||
|
||||
Das Package wurde entwickelt, um in einem stehenden laravel framework mit livewire / (Volt) / Fluxi Eingesetzt zu werden.
|
||||
|
||||
Der derzeitige Stand ist aus einem anderen Projekt, wo es spezifische Aufgaben schon sehr gut erledigt und genau das tut was es soll.
|
||||
|
||||
In diesem Fall geht es darum im ersten Punkt die aktuelle b2in Webseite in das System zu integrieren.
|
||||
Wichtig die weiteren Subseite Müssen später folgen. Hier geht es jetzt primär erst mal um den aktuellen Stand, der auch online ist.
|
||||
|
||||
Nutze diesen Ordner für den Prozess der Integration und dokumentiere ihn hier, so dass die Arbeit jederzeit wieder aufgenommen werden kann.
|
||||
|
||||
Erstelle einen Entwicklungsplan, der dann Stück Stück abgearbeitet werden kann, um eine saubere Integration zu gewährleisten
|
||||
Loading…
Add table
Add a link
Reference in a new issue