19-05-2026 Rebrand Pressekonto, Hub-Flux UI und Legacy-Media-Migration
Umbenennung presseportale → pressekonto in Domains, Themes und Dokumentation. Design-Tokens, Portal-Shell, Customer-Dashboard, Auth- und Admin-PM-Views. Artisan-Befehl migrate:legacy-media mit Tests und Hub-Flux-Entwicklungsdocs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
092ee0e918
commit
0a3e52d603
112 changed files with 8464 additions and 1649 deletions
|
|
@ -17,6 +17,6 @@ args = [
|
|||
command = "npx"
|
||||
|
||||
[mcp_servers.laravel-boost]
|
||||
command = "vendor/bin/sail"
|
||||
command = "php"
|
||||
args = ["artisan", "boost:mcp"]
|
||||
cwd = "/var/www/html"
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ Das Dockerfile wurde angepasst um:
|
|||
Falls die automatische Installation fehlschlägt, können Sie den Container manuell bauen:
|
||||
|
||||
```bash
|
||||
cd /Users/pandora/Sites/presseportale.com
|
||||
cd /Users/pandora/Sites/pressekonto.de
|
||||
docker build --build-arg WWWUSER=501 --build-arg WWWGROUP=20 -f docker/8.4/Dockerfile -t sail-8.4/app docker/8.4
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "Presseportale (Dev Container)",
|
||||
"name": "Pressekonto (Dev Container)",
|
||||
"dockerComposeFile": [
|
||||
"../docker-compose.yml"
|
||||
],
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -56,4 +56,5 @@ Icon
|
|||
_static/
|
||||
_work/
|
||||
_storage/
|
||||
_businessportal24.com/
|
||||
_businessportal24.com/
|
||||
dev/migration/
|
||||
|
|
@ -4,11 +4,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||
|
||||
## Project Overview
|
||||
|
||||
This is a multi-domain Laravel application ("Presseportale") that supports different domains with distinct themes and styling. The application uses Laravel with Livewire, Volt, and Fortify for authentication, along with Flux UI components.
|
||||
This is a multi-domain Laravel application ("Pressekonto") that supports different domains with distinct themes and styling. The application uses Laravel with Livewire, Volt, and Fortify for authentication, along with Flux UI components.
|
||||
|
||||
### Supported Domains
|
||||
|
||||
- **Main Portal**: presseportale.test (local) / presseportale.com (live) – Main admin portal page
|
||||
- **Main Portal**: pressekonto.test (local) / pressekonto.de (live) – Main admin portal page
|
||||
- **Presseecho**: presseecho.test - Landing page with presseecho theme
|
||||
- **Business Portal**: businessportal24.test - Landing page with business portal theme
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
Diese Laravel-Anwendung unterstützt verschiedene Domains mit unterschiedlichen Styles:
|
||||
|
||||
- **Haupt-Website**: https://presseportale.test (lokal) / https://presseportale.com (live) – Haupt-Portal Admin-Page
|
||||
- **Haupt-Website**: https://pressekonto.test (lokal) / https://pressekonto.de (live) – Haupt-Portal Admin-Page
|
||||
- **APP_PRESSEECHO**: https://presseecho.test
|
||||
- **APP_BUSINESSPORTAL**: https://businessportal24.test
|
||||
|
||||
|
|
|
|||
|
|
@ -8,18 +8,18 @@ Für dein Multi-Domain-Setup empfehle ich folgende Asset-URLs:
|
|||
|
||||
| Bereich | Domain | Asset-URL | Port | Verwendung |
|
||||
|---------|--------|-----------|------|------------|
|
||||
| **Backend** | presseportale.test | `assets.presseportale.test` | 5177 | Portal + FluxUI |
|
||||
| **Frontend** | presseecho.test<br>businessportal24.test | `assets-web.presseportale.test` | 5178 | Beide Frontend-Domains |
|
||||
| **Backend** | pressekonto.test | `assets.pressekonto.test` | 5177 | Portal + FluxUI |
|
||||
| **Frontend** | presseecho.test<br>businessportal24.test | `assets-web.pressekonto.test` | 5178 | Beide Frontend-Domains |
|
||||
|
||||
### Warum diese URLs?
|
||||
|
||||
#### 1. **assets.presseportale.test** (Portal/Backend)
|
||||
#### 1. **assets.pressekonto.test** (Portal/Backend)
|
||||
- ✅ Kurz und prägnant
|
||||
- ✅ Eindeutig dem Portal zugeordnet
|
||||
- ✅ Keine zusätzliche Subdomain-Tiefe
|
||||
- ✅ Folgt gängiger Konvention
|
||||
|
||||
#### 2. **assets-web.presseportale.test** (Web/Frontend)
|
||||
#### 2. **assets-web.pressekonto.test** (Web/Frontend)
|
||||
- ✅ Klar als "Web" (Frontend) gekennzeichnet
|
||||
- ✅ Ein Asset-Server für beide Frontend-Domains
|
||||
- ✅ Gute Trennung zu Portal-Assets
|
||||
|
|
@ -31,23 +31,23 @@ Falls du andere URLs bevorzugst, hier sind Alternativen:
|
|||
|
||||
### Option A: Mit Suffix-Präfix
|
||||
```
|
||||
portal-assets.presseportale.test → Port 5177
|
||||
web-assets.presseportale.test → Port 5178
|
||||
portal-assets.pressekonto.test → Port 5177
|
||||
web-assets.pressekonto.test → Port 5178
|
||||
```
|
||||
- ⚠️ Etwas länger
|
||||
- ✅ Sehr explizit
|
||||
|
||||
### Option B: Mit "vite" im Namen
|
||||
```
|
||||
vite.presseportale.test → Port 5177
|
||||
vite-web.presseportale.test → Port 5178
|
||||
vite.pressekonto.test → Port 5177
|
||||
vite-web.pressekonto.test → Port 5178
|
||||
```
|
||||
- ⚠️ Technologie-spezifisch (was wenn du später zu einem anderen Build-Tool wechselst?)
|
||||
- ⚠️ Weniger klar was geladen wird
|
||||
|
||||
### Option C: Separate Domains pro Frontend
|
||||
```
|
||||
assets.presseportale.test → Port 5177 (Portal)
|
||||
assets.pressekonto.test → Port 5177 (Portal)
|
||||
assets.presseecho.test → Port 5178 (Presseecho)
|
||||
assets.businessportal24.test → Port 5178 (Businessportal24)
|
||||
```
|
||||
|
|
@ -61,7 +61,7 @@ assets.businessportal24.test → Port 5178 (Businessportal24)
|
|||
|
||||
```yaml
|
||||
# Portal Assets (Backend)
|
||||
- "traefik.http.routers.assets-portal.rule=Host(`assets.presseportale.test`)"
|
||||
- "traefik.http.routers.assets-portal.rule=Host(`assets.pressekonto.test`)"
|
||||
- "traefik.http.routers.assets-portal.entrypoints=websecure"
|
||||
- "traefik.http.routers.assets-portal.tls=true"
|
||||
- "traefik.http.routers.assets-portal.service=assets-portal-service"
|
||||
|
|
@ -69,7 +69,7 @@ assets.businessportal24.test → Port 5178 (Businessportal24)
|
|||
- "traefik.http.services.assets-portal-service.loadbalancer.server.scheme=http"
|
||||
|
||||
# Web Assets (Frontend)
|
||||
- "traefik.http.routers.assets-web.rule=Host(`assets-web.presseportale.test`)"
|
||||
- "traefik.http.routers.assets-web.rule=Host(`assets-web.pressekonto.test`)"
|
||||
- "traefik.http.routers.assets-web.entrypoints=websecure"
|
||||
- "traefik.http.routers.assets-web.tls=true"
|
||||
- "traefik.http.routers.assets-web.service=assets-web-service"
|
||||
|
|
@ -89,8 +89,8 @@ ports:
|
|||
|
||||
```env
|
||||
# Vite Asset Domains
|
||||
ASSET_URL_PORTAL=https://assets.presseportale.test
|
||||
ASSET_URL_WEB=https://assets-web.presseportale.test
|
||||
ASSET_URL_PORTAL=https://assets.pressekonto.test
|
||||
ASSET_URL_WEB=https://assets-web.pressekonto.test
|
||||
|
||||
# Vite Development Ports
|
||||
VITE_PORT_PORTAL=5177
|
||||
|
|
@ -102,11 +102,11 @@ VITE_PORT_WEB=5178
|
|||
Füge folgende Einträge zu deiner `/etc/hosts` (Linux/Mac) oder `C:\Windows\System32\drivers\etc\hosts` (Windows) hinzu:
|
||||
|
||||
```
|
||||
127.0.0.1 presseportale.test
|
||||
127.0.0.1 pressekonto.test
|
||||
127.0.0.1 presseecho.test
|
||||
127.0.0.1 businessportal24.test
|
||||
127.0.0.1 assets.presseportale.test
|
||||
127.0.0.1 assets-web.presseportale.test
|
||||
127.0.0.1 assets.pressekonto.test
|
||||
127.0.0.1 assets-web.pressekonto.test
|
||||
```
|
||||
|
||||
## Vite-Konfigurationen
|
||||
|
|
@ -121,7 +121,7 @@ export default defineConfig({
|
|||
host: "0.0.0.0",
|
||||
port: 5177,
|
||||
hmr: {
|
||||
host: "assets.presseportale.test", // ← Asset-URL
|
||||
host: "assets.pressekonto.test", // ← Asset-URL
|
||||
protocol: "wss",
|
||||
},
|
||||
},
|
||||
|
|
@ -139,7 +139,7 @@ export default defineConfig({
|
|||
host: "0.0.0.0",
|
||||
port: 5178,
|
||||
hmr: {
|
||||
host: "assets-web.presseportale.test", // ← Asset-URL
|
||||
host: "assets-web.pressekonto.test", // ← Asset-URL
|
||||
protocol: "wss",
|
||||
},
|
||||
},
|
||||
|
|
@ -156,16 +156,16 @@ Browser-Request
|
|||
↓
|
||||
2. Laravel lädt View mit: @vite(['resources/css/web/theme-presseecho.css', ...])
|
||||
↓
|
||||
3. Vite-Helper generiert: <script src="https://assets-web.presseportale.test/@vite/client"></script>
|
||||
<link href="https://assets-web.presseportale.test/resources/css/web/theme-presseecho.css">
|
||||
3. Vite-Helper generiert: <script src="https://assets-web.pressekonto.test/@vite/client"></script>
|
||||
<link href="https://assets-web.pressekonto.test/resources/css/web/theme-presseecho.css">
|
||||
↓
|
||||
4. Browser requested: assets-web.presseportale.test
|
||||
4. Browser requested: assets-web.pressekonto.test
|
||||
↓
|
||||
5. Traefik routet zu: Container Port 5178
|
||||
↓
|
||||
6. Vite Web Server antwortet
|
||||
↓
|
||||
7. HMR WebSocket öffnet: wss://assets-web.presseportale.test
|
||||
7. HMR WebSocket öffnet: wss://assets-web.pressekonto.test
|
||||
↓
|
||||
8. ✅ Hot Module Replacement funktioniert!
|
||||
```
|
||||
|
|
@ -175,8 +175,8 @@ Browser-Request
|
|||
### 1. DNS-Auflösung testen
|
||||
```bash
|
||||
# Sollte zu 127.0.0.1 auflösen
|
||||
ping assets.presseportale.test
|
||||
ping assets-web.presseportale.test
|
||||
ping assets.pressekonto.test
|
||||
ping assets-web.pressekonto.test
|
||||
```
|
||||
|
||||
### 2. Vite-Server starten
|
||||
|
|
@ -195,15 +195,15 @@ Du solltest sehen:
|
|||
|
||||
### 3. Browser-Test
|
||||
Öffne:
|
||||
- https://presseportale.test (sollte Assets von assets.presseportale.test laden)
|
||||
- https://presseecho.test (sollte Assets von assets-web.presseportale.test laden)
|
||||
- https://businessportal24.test (sollte Assets von assets-web.presseportale.test laden)
|
||||
- https://pressekonto.test (sollte Assets von assets.pressekonto.test laden)
|
||||
- https://presseecho.test (sollte Assets von assets-web.pressekonto.test laden)
|
||||
- https://businessportal24.test (sollte Assets von assets-web.pressekonto.test laden)
|
||||
|
||||
### 4. HMR-Test
|
||||
1. Öffne Browser DevTools (F12)
|
||||
2. Gehe zu "Network" Tab
|
||||
3. Filter auf "WS" (WebSocket)
|
||||
4. Du solltest Verbindungen zu `wss://assets.*.presseportale.test` sehen
|
||||
4. Du solltest Verbindungen zu `wss://assets.*.pressekonto.test` sehen
|
||||
5. Ändere eine CSS-Datei
|
||||
6. Browser sollte automatisch neu laden (ohne vollständigen Page-Refresh)
|
||||
|
||||
|
|
@ -256,8 +256,8 @@ docker compose logs laravel.test | grep traefik
|
|||
### ✅ Verwende diese Asset-URLs:
|
||||
|
||||
```
|
||||
assets.presseportale.test → Port 5177 (Portal/Backend)
|
||||
assets-web.presseportale.test → Port 5178 (Web/Frontend)
|
||||
assets.pressekonto.test → Port 5177 (Portal/Backend)
|
||||
assets-web.pressekonto.test → Port 5178 (Web/Frontend)
|
||||
```
|
||||
|
||||
### ✅ Vorteile:
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@ Füge die folgenden Variablen zu deiner `.env`-Datei hinzu:
|
|||
|
||||
```env
|
||||
# Domain-Konfigurationen
|
||||
APP_NAME=presseportale
|
||||
APP_URL=https://presseportale.test
|
||||
APP_NAME=pressekonto
|
||||
APP_URL=https://pressekonto.test
|
||||
APP_PRIMARY="#3ea3dc"
|
||||
APP_ACCENT="#5c5c60"
|
||||
|
||||
# Entwicklungseinstellungen für Domains
|
||||
DEV_SIMULATE_DOMAIN=false
|
||||
DEV_SIMULATED_DOMAIN=presseportale.test
|
||||
DEV_SIMULATED_DOMAIN=pressekonto.test
|
||||
```
|
||||
|
||||
## Entwicklungsmodus
|
||||
|
|
@ -34,10 +34,10 @@ unabhängig von der tatsächlichen URL.
|
|||
|
||||
Jede Domain kann eigene Einstellungen haben:
|
||||
|
||||
### Haupt-Website (presseportale.test)
|
||||
### Haupt-Website (pressekonto.test)
|
||||
|
||||
- `APP_URL`: Die Domain für die Haupt-Website (https://presseportale.test)
|
||||
- `APP_NAME`: Der Name der Haupt-Website (presseportale)
|
||||
- `APP_URL`: Die Domain für die Haupt-Website (https://pressekonto.test)
|
||||
- `APP_NAME`: Der Name der Haupt-Website (pressekonto)
|
||||
- `APP_PRIMARY`: Die primäre Farbe im HEX-Format (#3ea3dc)
|
||||
- `APP_ACCENT`: Die Akzentfarbe im HEX-Format (#5c5c60)
|
||||
|
||||
|
|
@ -62,7 +62,7 @@ Jede Domain kann eigene Einstellungen haben:
|
|||
Um die verschiedenen Domains lokal zu testen, füge folgende Zeilen zu deiner Hosts-Datei hinzu:
|
||||
|
||||
```
|
||||
127.0.0.1 presseportale.test
|
||||
127.0.0.1 pressekonto.test
|
||||
127.0.0.1 presseecho.test
|
||||
127.0.0.1 businessportal24.test
|
||||
```
|
||||
|
|
@ -101,7 +101,7 @@ Im Code kannst du auf die Domain-Konfiguration zugreifen:
|
|||
|
||||
Das Projekt verwendet bereits eine Multi-Domain-Architektur mit:
|
||||
|
||||
- **Hauptwebsite:** `presseportale.test` - Hauptwebsite mit blauem Theme (#3ea3dc)
|
||||
- **Hauptwebsite:** `pressekonto.test` - Hauptwebsite mit blauem Theme (#3ea3dc)
|
||||
- **Presseecho:** `presseecho.test` - Presseecho-Website mit rotem Theme (#e94a3c)
|
||||
- **Business Portal:** `businessportal24.test` - Business Portal mit orangem Theme (#f69f0f)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## 🔍 Ursprüngliches Problem
|
||||
|
||||
Auf https://presseportale.test erschien der Fehler:
|
||||
Auf https://pressekonto.test erschien der Fehler:
|
||||
```
|
||||
[Error] Not allowed to use restricted network host "0.0.0.0":
|
||||
https://0.0.0.0:5178/@vite/client
|
||||
|
|
@ -53,7 +53,7 @@ npm run dev:all
|
|||
|
||||
```bash
|
||||
# Portal CSS wird korrekt geladen:
|
||||
curl -Iks https://assets.presseportale.test/resources/css/portal.css
|
||||
curl -Iks https://assets.pressekonto.test/resources/css/portal.css
|
||||
# → HTTP/2 200 ✅
|
||||
|
||||
# Web Assets funktionieren:
|
||||
|
|
@ -65,13 +65,13 @@ curl -Iks https://assets.businessportal24.test/resources/css/web/theme-businessp
|
|||
|
||||
| Domain | Asset-Domain | Port | Build-Dir | CSS-Datei |
|
||||
|--------|-------------|------|-----------|-----------|
|
||||
| presseportale.test | assets.presseportale.test | 5177 | build/portal | portal.css |
|
||||
| pressekonto.test | assets.pressekonto.test | 5177 | build/portal | portal.css |
|
||||
| presseecho.test | assets.presseecho.test | 5178 | build/web | theme-presseecho.css |
|
||||
| businessportal24.test | assets.businessportal24.test | 5178 | build/web | theme-businessportal24.css |
|
||||
|
||||
## 🚀 Nächste Schritte
|
||||
|
||||
1. **Browser testen**: Öffne https://presseportale.test und mache einen Hard-Refresh (`Ctrl+Shift+R`)
|
||||
1. **Browser testen**: Öffne https://pressekonto.test und mache einen Hard-Refresh (`Ctrl+Shift+R`)
|
||||
2. **Keine Fehler mehr**: Die "0.0.0.0" Fehler sollten verschwunden sein
|
||||
3. **Assets laden über HTTPS**: Alle CSS/JS-Dateien werden über die korrekten Asset-Subdomains geladen
|
||||
|
||||
|
|
@ -87,7 +87,7 @@ Falls du die Docker Container neu gestartet hast, stelle sicher dass:
|
|||
|
||||
1. ✅ DNS-Einträge in `/etc/hosts` vorhanden sind:
|
||||
```
|
||||
127.0.0.1 assets.presseportale.test
|
||||
127.0.0.1 assets.pressekonto.test
|
||||
127.0.0.1 assets.presseecho.test
|
||||
127.0.0.1 assets.businessportal24.test
|
||||
```
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
### 1. `config/domains.php`
|
||||
Jede Domain hat jetzt eine dedizierte `asset_url`:
|
||||
- `portal`: `https://assets.presseportale.test`
|
||||
- `portal`: `https://assets.pressekonto.test`
|
||||
- `presseecho`: `https://assets.presseecho.test`
|
||||
- `businessportal24`: `https://assets.businessportal24.test`
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ sleep 5 && tail -30 /tmp/vite-server.log
|
|||
### 2. Im Browser testen
|
||||
|
||||
Öffne mit Hard-Refresh (`Ctrl+Shift+R`):
|
||||
- ✅ https://presseportale.test
|
||||
- ✅ https://pressekonto.test
|
||||
- ✅ https://presseecho.test
|
||||
- ✅ https://businessportal24.test
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ https://0.0.0.0:5178/@vite/client
|
|||
|
||||
**NACHHER (✅)**:
|
||||
```
|
||||
https://assets.presseportale.test/@vite/client
|
||||
https://assets.pressekonto.test/@vite/client
|
||||
https://assets.presseecho.test/@vite/client
|
||||
https://assets.businessportal24.test/@vite/client
|
||||
```
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ composer require laravel/fortify laravel/sanctum
|
|||
|
||||
## Routen
|
||||
|
||||
### Web-Authentifizierung (presseportale.test)
|
||||
### Web-Authentifizierung (pressekonto.test)
|
||||
|
||||
- `GET /login` - Anmeldeseite (Livewire)
|
||||
- `POST /login` - Anmeldung (Livewire)
|
||||
|
|
@ -68,7 +68,7 @@ composer require laravel/fortify laravel/sanctum
|
|||
- `GET /verify-email` - E-Mail-Verifizierung (Livewire)
|
||||
- `GET /confirm-password` - Passwort bestätigen (Livewire)
|
||||
|
||||
### API-Routen (api.presseportale.test)
|
||||
### API-Routen (api.pressekonto.test)
|
||||
|
||||
- `GET /api/user` - Aktueller Benutzer (geschützt)
|
||||
- `GET /api/profile` - Benutzerprofil (geschützt)
|
||||
|
|
@ -78,7 +78,7 @@ composer require laravel/fortify laravel/sanctum
|
|||
|
||||
### Web-Authentifizierung
|
||||
|
||||
1. Besuchen Sie `http://portal.presseportale.test/login`
|
||||
1. Besuchen Sie `http://portal.pressekonto.test/login`
|
||||
2. Registrieren Sie sich oder melden Sie sich an
|
||||
3. Nutzen Sie die verschiedenen Authentifizierungsfeatures
|
||||
|
||||
|
|
@ -87,7 +87,7 @@ composer require laravel/fortify laravel/sanctum
|
|||
1. **Token erstellen**:
|
||||
|
||||
```bash
|
||||
curl -X POST http://api.presseportale.test/login \
|
||||
curl -X POST http://api.pressekonto.test/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"user@example.com","password":"password"}'
|
||||
```
|
||||
|
|
@ -95,7 +95,7 @@ curl -X POST http://api.presseportale.test/login \
|
|||
2. **Geschützte Route aufrufen**:
|
||||
|
||||
```bash
|
||||
curl -X GET http://api.presseportale.test/api/user \
|
||||
curl -X GET http://api.pressekonto.test/api/user \
|
||||
-H "Authorization: Bearer YOUR_TOKEN_HERE"
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Das Projekt nutzt ein dynamisches Theme-System mit 3 Domains:
|
|||
|
||||
| Domain | Theme | Primary Color | Secondary Color | CSS-Datei |
|
||||
|--------|-------|---------------|-----------------|-----------|
|
||||
| **presseportale.test** | portal | #526266 | #82a0a7 | `resources/css/portal.css` |
|
||||
| **pressekonto.test** | portal | #526266 | #82a0a7 | `resources/css/portal.css` |
|
||||
| **presseecho.test** | presseecho | #345636 (Grün) | #6b8f71 | `resources/css/web/theme-presseecho.css` |
|
||||
| **businessportal24.test** | businessportal24 | #cf3628 (Rot) | #f0834a | `resources/css/web/theme-businessportal24.css` |
|
||||
|
||||
|
|
@ -276,8 +276,8 @@ npm run build:web
|
|||
Der `ThemeServiceProvider` unterstützt einen `?theme=` URL-Parameter zum Testen:
|
||||
|
||||
```
|
||||
https://presseportale.test?theme=presseecho
|
||||
https://presseportale.test?theme=businessportal24
|
||||
https://pressekonto.test?theme=presseecho
|
||||
https://pressekonto.test?theme=businessportal24
|
||||
```
|
||||
|
||||
### Via Host
|
||||
|
|
@ -285,7 +285,7 @@ https://presseportale.test?theme=businessportal24
|
|||
Einfach die entsprechende Domain aufrufen:
|
||||
|
||||
```
|
||||
https://presseportale.test → Portal Theme
|
||||
https://pressekonto.test → Portal Theme
|
||||
https://presseecho.test → Presseecho Theme
|
||||
https://businessportal24.test → Businessportal24 Theme
|
||||
```
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Jede Domain hat jetzt ihre eigene dedizierte Asset-Subdomain:
|
|||
|
||||
| Domain | Asset-Subdomain | Port | Vite-Config |
|
||||
|--------|----------------|------|-------------|
|
||||
| `presseportale.test` | `assets.presseportale.test` | 5177 | `vite.portal.config.js` |
|
||||
| `pressekonto.test` | `assets.pressekonto.test` | 5177 | `vite.portal.config.js` |
|
||||
| `presseecho.test` | `assets.presseecho.test` | 5178 | `vite.web.config.js` |
|
||||
| `businessportal24.test` | `assets.businessportal24.test` | 5178 | `vite.web.config.js` |
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ Füge folgende Einträge zu deiner Hosts-Datei hinzu (lokal auf deinem Host-Syst
|
|||
**Windows**: `C:\Windows\System32\drivers\etc\hosts`
|
||||
|
||||
```
|
||||
127.0.0.1 assets.presseportale.test
|
||||
127.0.0.1 assets.pressekonto.test
|
||||
127.0.0.1 assets.presseecho.test
|
||||
127.0.0.1 assets.businessportal24.test
|
||||
```
|
||||
|
|
@ -47,7 +47,7 @@ npm run dev:all
|
|||
|
||||
### 1. `docker-compose.yml`
|
||||
Neue Traefik-Routen hinzugefügt:
|
||||
- `assets.presseportale.test` → Port 5177 (Portal)
|
||||
- `assets.pressekonto.test` → Port 5177 (Portal)
|
||||
- `assets.presseecho.test` → Port 5178 (Presseecho)
|
||||
- `assets.businessportal24.test` → Port 5178 (Businessportal24)
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ Nach dem Neustart kannst du testen:
|
|||
|
||||
```bash
|
||||
# Im DevContainer:
|
||||
curl -Ik https://assets.presseportale.test/@vite/client
|
||||
curl -Ik https://assets.pressekonto.test/@vite/client
|
||||
curl -Ik https://assets.presseecho.test/@vite/client
|
||||
curl -Ik https://assets.businessportal24.test/@vite/client
|
||||
```
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Dieses Projekt verwendet **2 separate Vite-Ports** für unterschiedliche Domain-
|
|||
|
||||
| Bereich | Port | Vite Config | Tailwind Config | Domains | FluxUI |
|
||||
|---------|------|-------------|-----------------|---------|--------|
|
||||
| **Backend (Portal)** | 5177 | `vite.portal.config.js` | `tailwind.portal.config.js` | `presseportale.test` | ✅ Ja |
|
||||
| **Backend (Portal)** | 5177 | `vite.portal.config.js` | `tailwind.portal.config.js` | `pressekonto.test` | ✅ Ja |
|
||||
| **Frontend (Web)** | 5178 | `vite.web.config.js` | `tailwind.web.config.js` | `presseecho.test`, `businessportal24.test` | ❌ Nein |
|
||||
|
||||
## Warum 2 Ports?
|
||||
|
|
@ -35,15 +35,15 @@ Startet beide Vite-Server parallel mit `concurrently`
|
|||
npm run dev:portal
|
||||
```
|
||||
- Port: 5177
|
||||
- HMR-Host: assets.presseportale.test
|
||||
- Domain: presseportale.test
|
||||
- HMR-Host: assets.pressekonto.test
|
||||
- Domain: pressekonto.test
|
||||
|
||||
### Option 3: Nur Frontend (Web)
|
||||
```bash
|
||||
npm run dev:web
|
||||
```
|
||||
- Port: 5178
|
||||
- HMR-Host: assets-web.presseportale.test
|
||||
- HMR-Host: assets-web.pressekonto.test
|
||||
- Domains: presseecho.test, businessportal24.test
|
||||
|
||||
## Production Build
|
||||
|
|
@ -86,7 +86,7 @@ public/
|
|||
## Theme-System
|
||||
|
||||
### Backend (Portal)
|
||||
- **Domain:** presseportale.test
|
||||
- **Domain:** pressekonto.test
|
||||
- **Theme:** `portal`
|
||||
- **CSS:** `resources/css/portal.css`
|
||||
- **Views:** `resources/views/portal/**`
|
||||
|
|
@ -110,8 +110,8 @@ public/
|
|||
|
||||
Beide Vite-Server laufen intern auf HTTP (`https: false`), Traefik übernimmt SSL-Terminierung:
|
||||
|
||||
- **Portal HMR:** `wss://assets.presseportale.test` → Port 5177
|
||||
- **Web HMR:** `wss://assets-web.presseportale.test` → Port 5178
|
||||
- **Portal HMR:** `wss://assets.pressekonto.test` → Port 5177
|
||||
- **Web HMR:** `wss://assets-web.pressekonto.test` → Port 5178
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ sudo nano /etc/hosts
|
|||
|
||||
**Einträge:**
|
||||
```
|
||||
127.0.0.1 assets.presseportale.test
|
||||
127.0.0.1 assets.pressekonto.test
|
||||
127.0.0.1 assets.presseecho.test
|
||||
127.0.0.1 assets.businessportal24.test
|
||||
```
|
||||
|
|
@ -51,7 +51,7 @@ Zurück im DevContainer:
|
|||
tail -20 /tmp/vite-server.log
|
||||
|
||||
# Teste die Asset-URLs:
|
||||
curl -Ik https://assets.presseportale.test/@vite/client
|
||||
curl -Ik https://assets.pressekonto.test/@vite/client
|
||||
curl -Ik https://assets.presseecho.test/@vite/client
|
||||
curl -Ik https://assets.businessportal24.test/@vite/client
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ curl -Ik https://assets.businessportal24.test/@vite/client
|
|||
Öffne:
|
||||
- https://businessportal24.test
|
||||
- https://presseecho.test
|
||||
- https://presseportale.test
|
||||
- https://pressekonto.test
|
||||
|
||||
Die Assets sollten nun korrekt über HTTPS von den jeweiligen Asset-Subdomains geladen werden!
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ Die Assets sollten nun korrekt über HTTPS von den jeweiligen Asset-Subdomains g
|
|||
|
||||
| Hauptdomain | Asset-Domain | Port | Build-Dir |
|
||||
|------------|-------------|------|-----------|
|
||||
| presseportale.test | assets.presseportale.test | 5177 | public/build/portal |
|
||||
| pressekonto.test | assets.pressekonto.test | 5177 | public/build/portal |
|
||||
| presseecho.test | assets.presseecho.test | 5178 | public/build/web |
|
||||
| businessportal24.test | assets.businessportal24.test | 5178 | public/build/web |
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ Du benötigst **mindestens 2 Vite-Ports**:
|
|||
│ └──────────────────┘ └──────────────────┘ │
|
||||
│ ↓ ↓ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ presseportale.test │ │ presseecho.test │ │
|
||||
│ │ pressekonto.test │ │ presseecho.test │ │
|
||||
│ │ │ │ businessp24.test│ │
|
||||
│ └──────────────────┘ └──────────────────┘ │
|
||||
│ │
|
||||
|
|
@ -80,14 +80,14 @@ Du benötigst **mindestens 2 Vite-Ports**:
|
|||
- Port: **5177**
|
||||
- Input: `resources/css/portal.css`
|
||||
- Build: `public/build/portal`
|
||||
- HMR: `assets.presseportale.test`
|
||||
- HMR: `assets.pressekonto.test`
|
||||
- FluxUI: ✅ Ja
|
||||
|
||||
#### ✅ `vite.web.config.js`
|
||||
- Port: **5178** (geändert von 5177!)
|
||||
- Input: `theme-presseecho.css`, `theme-businessportal24.css`
|
||||
- Build: `public/build/web`
|
||||
- HMR: `assets-web.presseportale.test`
|
||||
- HMR: `assets-web.pressekonto.test`
|
||||
- FluxUI: ❌ Nein
|
||||
|
||||
#### ❌ `vite.config.js` (deprecated)
|
||||
|
|
@ -190,14 +190,14 @@ npm run build:web
|
|||
|
||||
| URL | Vite-Port | Theme | FluxUI |
|
||||
|-----|-----------|-------|--------|
|
||||
| https://presseportale.test | 5177 | portal | ✅ |
|
||||
| https://pressekonto.test | 5177 | portal | ✅ |
|
||||
| https://presseecho.test | 5178 | presseecho | ❌ |
|
||||
| https://businessportal24.test | 5178 | businessportal24 | ❌ |
|
||||
|
||||
## HMR (Hot Module Replacement)
|
||||
|
||||
- **Portal:** `wss://assets.presseportale.test` → Port 5177
|
||||
- **Web:** `wss://assets-web.presseportale.test` → Port 5178
|
||||
- **Portal:** `wss://assets.pressekonto.test` → Port 5177
|
||||
- **Web:** `wss://assets-web.pressekonto.test` → Port 5178
|
||||
|
||||
⚠️ **Wichtig:** Traefik muss beide HMR-Hosts routen!
|
||||
|
||||
|
|
@ -210,12 +210,12 @@ Stelle sicher, dass Traefik beide Vite-Ports routet:
|
|||
# docker-compose.yml oder traefik.yml
|
||||
labels:
|
||||
# Portal Assets
|
||||
- "traefik.http.routers.vite-portal.rule=Host(`assets.presseportale.test`)"
|
||||
- "traefik.http.routers.vite-portal.rule=Host(`assets.pressekonto.test`)"
|
||||
- "traefik.http.routers.vite-portal.service=vite-portal"
|
||||
- "traefik.http.services.vite-portal.loadbalancer.server.port=5177"
|
||||
|
||||
# Web Assets
|
||||
- "traefik.http.routers.vite-web.rule=Host(`assets-web.presseportale.test`)"
|
||||
- "traefik.http.routers.vite-web.rule=Host(`assets-web.pressekonto.test`)"
|
||||
- "traefik.http.routers.vite-web.service=vite-web"
|
||||
- "traefik.http.services.vite-web.loadbalancer.server.port=5178"
|
||||
```
|
||||
|
|
@ -223,11 +223,11 @@ labels:
|
|||
### 2. DNS/Hosts-Datei aktualisieren
|
||||
|
||||
```
|
||||
127.0.0.1 presseportale.test
|
||||
127.0.0.1 pressekonto.test
|
||||
127.0.0.1 presseecho.test
|
||||
127.0.0.1 businessportal24.test
|
||||
127.0.0.1 assets.presseportale.test
|
||||
127.0.0.1 assets-web.presseportale.test
|
||||
127.0.0.1 assets.pressekonto.test
|
||||
127.0.0.1 assets-web.pressekonto.test
|
||||
```
|
||||
|
||||
### 3. Blade-Templates aktualisieren
|
||||
|
|
@ -258,7 +258,7 @@ Diese Dateien werden nicht mehr benötigt:
|
|||
npm run dev:all
|
||||
|
||||
# 2. Browser öffnen
|
||||
# - https://presseportale.test (Backend)
|
||||
# - https://pressekonto.test (Backend)
|
||||
# - https://presseecho.test (Frontend Grün)
|
||||
# - https://businessportal24.test (Frontend Rot)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
openapi: 3.1.0
|
||||
info:
|
||||
title: Presseportale API
|
||||
title: Pressekonto API
|
||||
version: 1.0.0
|
||||
description: >
|
||||
REST API for customer integrations after the 2026 migration. Legacy
|
||||
|
|
|
|||
472
app/Console/Commands/MigrateLegacyMedia.php
Normal file
472
app/Console/Commands/MigrateLegacyMedia.php
Normal file
|
|
@ -0,0 +1,472 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\Portal;
|
||||
use App\Models\Company;
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\PressReleaseImage;
|
||||
use App\Services\Image\ImageService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class MigrateLegacyMedia extends Command
|
||||
{
|
||||
protected $signature = 'legacy:migrate-media
|
||||
{--portal=all : Portal (presseecho|businessportal24|all)}
|
||||
{--type=all : Medientyp (company-logos|press-release-images|all)}
|
||||
{--base-path=dev/migration : Lokaler Ordner mit den Legacy-Dateien}
|
||||
{--dry-run : Nur prüfen, nichts kopieren oder aktualisieren}
|
||||
{--force : Bereits migrierte Dateien erneut überschreiben}
|
||||
{--limit=0 : Maximal N Legacy-Datensätze pro Typ und Portal verarbeiten (0 = alle)}';
|
||||
|
||||
protected $description = 'Migriert Legacy-Firmenlogos und Pressemitteilungsbilder anhand der Legacy-DB in den finalen Storage.';
|
||||
|
||||
private const PORTAL_CONNECTIONS = [
|
||||
'presseecho' => 'mysql_presseecho',
|
||||
'businessportal24' => 'mysql_businessportal',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, list<string>>
|
||||
*/
|
||||
private const SOURCE_DIRECTORIES = [
|
||||
'company-logos' => [
|
||||
'uploads/company',
|
||||
'thumbnails/company',
|
||||
],
|
||||
'press-release-images' => [
|
||||
'uploads/pressreleaseimage',
|
||||
'uploads/pressreleaseimage_',
|
||||
'uploads/_pressreleaseimage',
|
||||
'thumbnails/pressreleaseimage',
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(private readonly ImageService $imageService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$portals = $this->selectedPortals();
|
||||
$types = $this->selectedTypes();
|
||||
|
||||
if ($portals === []) {
|
||||
$this->error('Ungültiges Portal. Erlaubt: presseecho, businessportal24, all.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($types === []) {
|
||||
$this->error('Ungültiger Medientyp. Erlaubt: company-logos, press-release-images, all.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$configuredBasePath = (string) $this->option('base-path');
|
||||
$basePath = Str::startsWith($configuredBasePath, '/')
|
||||
? rtrim($configuredBasePath, '/')
|
||||
: base_path(trim($configuredBasePath, '/'));
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
$force = (bool) $this->option('force');
|
||||
$limit = max(0, (int) $this->option('limit'));
|
||||
$totals = $this->emptyStats();
|
||||
|
||||
foreach ($portals as $portal) {
|
||||
foreach ($types as $type) {
|
||||
$stats = match ($type) {
|
||||
'company-logos' => $this->migrateCompanyLogos($portal, $basePath, $dryRun, $force, $limit),
|
||||
'press-release-images' => $this->migratePressReleaseImages($portal, $basePath, $dryRun, $force, $limit),
|
||||
};
|
||||
|
||||
foreach ($totals as $key => $value) {
|
||||
$totals[$key] = $value + $stats[$key];
|
||||
}
|
||||
|
||||
$this->line(sprintf(
|
||||
'%s/%s: Legacy %d, migriert %d, Thumbnail-Fallback %d, DB-Updates %d, bereits synchron %d, Ziel fehlt %d, Datei fehlt %d',
|
||||
$portal,
|
||||
$type,
|
||||
$stats['legacy_rows'],
|
||||
$stats['migrated'],
|
||||
$stats['thumbnail_fallback'],
|
||||
$stats['updated'],
|
||||
$stats['already_synced'],
|
||||
$stats['missing_target'],
|
||||
$stats['missing_file'],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info(sprintf(
|
||||
'Gesamt: Legacy %d, migriert %d, Thumbnail-Fallback %d, DB-Updates %d, bereits synchron %d, Ziel fehlt %d, Datei fehlt %d%s',
|
||||
$totals['legacy_rows'],
|
||||
$totals['migrated'],
|
||||
$totals['thumbnail_fallback'],
|
||||
$totals['updated'],
|
||||
$totals['already_synced'],
|
||||
$totals['missing_target'],
|
||||
$totals['missing_file'],
|
||||
$dryRun ? ' (Dry-Run)' : '',
|
||||
));
|
||||
|
||||
return ($totals['missing_target'] + $totals['missing_file']) > 0
|
||||
? self::FAILURE
|
||||
: self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{legacy_rows:int,migrated:int,thumbnail_fallback:int,updated:int,already_synced:int,missing_target:int,missing_file:int}
|
||||
*/
|
||||
private function migrateCompanyLogos(string $portal, string $basePath, bool $dryRun, bool $force, int $limit): array
|
||||
{
|
||||
$stats = $this->emptyStats();
|
||||
$sourceIndex = $this->sourceIndex($basePath, $portal, 'company-logos');
|
||||
$processed = 0;
|
||||
|
||||
DB::connection(self::PORTAL_CONNECTIONS[$portal])
|
||||
->table('company')
|
||||
->whereNotNull('logo')
|
||||
->where('logo', '!=', '')
|
||||
->orderBy('id')
|
||||
->chunk(500, function ($rows) use ($portal, $sourceIndex, $dryRun, $force, $limit, &$processed, &$stats): bool {
|
||||
foreach ($rows as $row) {
|
||||
if ($limit > 0 && $processed >= $limit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$processed++;
|
||||
$stats['legacy_rows']++;
|
||||
|
||||
$company = Company::withoutGlobalScopes()
|
||||
->where('legacy_portal', $portal)
|
||||
->where('legacy_id', $row->id)
|
||||
->first(['id', 'logo_path', 'logo_variants']);
|
||||
|
||||
if (! $company) {
|
||||
$stats['missing_target']++;
|
||||
$this->warn("Ziel fehlt: {$portal}/company/{$row->id}");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$sourceFilename = $this->legacyFilename((string) $row->logo);
|
||||
$sourcePath = $sourceIndex[$sourceFilename] ?? $sourceIndex[Str::lower($sourceFilename)] ?? null;
|
||||
$destinationPath = "company-logos/{$portal}/{$company->id}/{$sourceFilename}";
|
||||
|
||||
if ($this->isAlreadySynced($company->logo_path, $destinationPath, $force)) {
|
||||
$stats['already_synced']++;
|
||||
|
||||
if (! $dryRun && (! is_array($company->logo_variants) || $company->logo_variants === [])) {
|
||||
$variants = $this->imageService->generateMissingCompanyLogoVariants($destinationPath);
|
||||
|
||||
if ($variants !== []) {
|
||||
$company->forceFill(['logo_variants' => $variants])->save();
|
||||
$stats['updated']++;
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $sourcePath) {
|
||||
$stats['missing_file']++;
|
||||
$this->warn("Datei fehlt: {$portal}/company/{$sourceFilename} (Company #{$company->id})");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $dryRun) {
|
||||
$this->copyToPublicStorage($sourcePath, $destinationPath, $force);
|
||||
$company->forceFill([
|
||||
'logo_path' => $destinationPath,
|
||||
'logo_variants' => $this->imageService->generateMissingCompanyLogoVariants($destinationPath),
|
||||
])->save();
|
||||
}
|
||||
|
||||
$stats['migrated']++;
|
||||
$stats['updated']++;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{legacy_rows:int,migrated:int,thumbnail_fallback:int,updated:int,already_synced:int,missing_target:int,missing_file:int}
|
||||
*/
|
||||
private function migratePressReleaseImages(string $portal, string $basePath, bool $dryRun, bool $force, int $limit): array
|
||||
{
|
||||
$stats = $this->emptyStats();
|
||||
$sourceIndex = $this->sourceIndex($basePath, $portal, 'press-release-images');
|
||||
$thumbnailIndex = $this->thumbnailIndex($basePath, $portal);
|
||||
$processed = 0;
|
||||
|
||||
DB::connection(self::PORTAL_CONNECTIONS[$portal])
|
||||
->table('press_release_image')
|
||||
->orderBy('id')
|
||||
->chunk(500, function ($rows) use ($portal, $sourceIndex, $thumbnailIndex, $dryRun, $force, $limit, &$processed, &$stats): bool {
|
||||
foreach ($rows as $row) {
|
||||
if ($limit > 0 && $processed >= $limit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (blank($row->image)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$processed++;
|
||||
$stats['legacy_rows']++;
|
||||
|
||||
$pressRelease = PressRelease::withoutGlobalScopes()
|
||||
->where('legacy_portal', $portal)
|
||||
->where('legacy_id', $row->press_release_id)
|
||||
->first(['id']);
|
||||
|
||||
if (! $pressRelease) {
|
||||
$stats['missing_target']++;
|
||||
$this->warn("Ziel fehlt: {$portal}/press_release/{$row->press_release_id}");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$sourceFilename = $this->legacyFilename((string) $row->image);
|
||||
$sourcePath = $sourceIndex[$sourceFilename] ?? $sourceIndex[Str::lower($sourceFilename)] ?? null;
|
||||
$usedThumbnailFallback = false;
|
||||
|
||||
if (! $sourcePath) {
|
||||
$sourcePath = $thumbnailIndex[(int) $row->id] ?? null;
|
||||
$usedThumbnailFallback = $sourcePath !== null;
|
||||
}
|
||||
|
||||
$destinationFilename = $usedThumbnailFallback ? basename($sourcePath) : $sourceFilename;
|
||||
$destinationPath = "press-releases/{$pressRelease->id}/images/{$destinationFilename}";
|
||||
$image = PressReleaseImage::withTrashed()
|
||||
->where('legacy_portal', $portal)
|
||||
->where('legacy_id', $row->id)
|
||||
->first();
|
||||
|
||||
if ($image && $this->isAlreadySynced($image->path, $destinationPath, $force)) {
|
||||
$stats['already_synced']++;
|
||||
|
||||
if (! $dryRun && (! is_array($image->variants) || $image->variants === [])) {
|
||||
$variants = $this->imageService->generateMissingPressReleaseVariants($destinationPath);
|
||||
|
||||
if ($variants !== []) {
|
||||
$image->forceFill(['variants' => $variants])->save();
|
||||
$stats['updated']++;
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $sourcePath) {
|
||||
$stats['missing_file']++;
|
||||
$this->warn("Datei fehlt: {$portal}/press_release_image/{$sourceFilename} (Image #{$row->id})");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($usedThumbnailFallback) {
|
||||
$stats['thumbnail_fallback']++;
|
||||
}
|
||||
|
||||
$variants = [];
|
||||
$size = [null, null];
|
||||
$mime = null;
|
||||
|
||||
if (! $dryRun) {
|
||||
$this->copyToPublicStorage($sourcePath, $destinationPath, $force);
|
||||
$variants = $this->imageService->generateMissingPressReleaseVariants($destinationPath);
|
||||
$size = @getimagesize(Storage::disk('public')->path($destinationPath)) ?: [null, null];
|
||||
$mime = File::mimeType($sourcePath) ?: null;
|
||||
|
||||
$image = $image ?: new PressReleaseImage;
|
||||
$image->forceFill([
|
||||
'press_release_id' => $pressRelease->id,
|
||||
'disk' => 'public',
|
||||
'path' => $destinationPath,
|
||||
'variants' => $variants,
|
||||
'title' => $row->title ?: null,
|
||||
'description' => $row->description ?: null,
|
||||
'copyright' => $row->copyright ?: null,
|
||||
'is_preview' => (bool) $row->is_preview_image,
|
||||
'sort_order' => 0,
|
||||
'width' => is_int($size[0] ?? null) ? $size[0] : null,
|
||||
'height' => is_int($size[1] ?? null) ? $size[1] : null,
|
||||
'mime' => $mime,
|
||||
'legacy_portal' => $portal,
|
||||
'legacy_id' => $row->id,
|
||||
]);
|
||||
|
||||
if ($image->trashed()) {
|
||||
$image->restore();
|
||||
}
|
||||
|
||||
$image->save();
|
||||
}
|
||||
|
||||
$stats['migrated']++;
|
||||
$stats['updated']++;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function selectedPortals(): array
|
||||
{
|
||||
$portal = (string) $this->option('portal');
|
||||
|
||||
return match ($portal) {
|
||||
'all' => [Portal::Presseecho->value, Portal::Businessportal24->value],
|
||||
Portal::Presseecho->value, Portal::Businessportal24->value => [$portal],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function selectedTypes(): array
|
||||
{
|
||||
$type = (string) $this->option('type');
|
||||
|
||||
return match ($type) {
|
||||
'all' => ['company-logos', 'press-release-images'],
|
||||
'company-logos', 'press-release-images' => [$type],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{legacy_rows:int,migrated:int,thumbnail_fallback:int,updated:int,already_synced:int,missing_target:int,missing_file:int}
|
||||
*/
|
||||
private function emptyStats(): array
|
||||
{
|
||||
return [
|
||||
'legacy_rows' => 0,
|
||||
'migrated' => 0,
|
||||
'thumbnail_fallback' => 0,
|
||||
'updated' => 0,
|
||||
'already_synced' => 0,
|
||||
'missing_target' => 0,
|
||||
'missing_file' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function sourceIndex(string $basePath, string $portal, string $type): array
|
||||
{
|
||||
$index = [];
|
||||
|
||||
foreach (self::SOURCE_DIRECTORIES[$type] as $relativeDirectory) {
|
||||
$directory = "{$basePath}/{$portal}/{$relativeDirectory}";
|
||||
|
||||
if (! File::isDirectory($directory)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (File::allFiles($directory) as $file) {
|
||||
$index[$file->getFilename()] ??= $file->getPathname();
|
||||
$index[Str::lower($file->getFilename())] ??= $file->getPathname();
|
||||
}
|
||||
}
|
||||
|
||||
return $index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function thumbnailIndex(string $basePath, string $portal): array
|
||||
{
|
||||
$directory = "{$basePath}/{$portal}/thumbnails/pressreleaseimage";
|
||||
|
||||
if (! File::isDirectory($directory)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$index = [];
|
||||
$priorities = [
|
||||
'press_image_preview' => 50,
|
||||
'pressrelease_form' => 40,
|
||||
'backend_list' => 30,
|
||||
'press_image_list' => 20,
|
||||
'press_list_image' => 10,
|
||||
];
|
||||
|
||||
foreach (File::allFiles($directory) as $file) {
|
||||
if (! preg_match('/-(\d+)\.[^.]+$/', $file->getFilename(), $matches)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$legacyImageId = (int) $matches[1];
|
||||
$thumbnailType = Str::after($file->getPathname(), "{$directory}/");
|
||||
$thumbnailType = Str::before($thumbnailType, DIRECTORY_SEPARATOR);
|
||||
$priority = $priorities[$thumbnailType] ?? 0;
|
||||
$current = $index[$legacyImageId] ?? null;
|
||||
|
||||
if (! $current || $priority > ($current['priority'] ?? 0)) {
|
||||
$index[$legacyImageId] = [
|
||||
'path' => $file->getPathname(),
|
||||
'priority' => $priority,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return collect($index)
|
||||
->map(fn (array $entry): string => $entry['path'])
|
||||
->all();
|
||||
}
|
||||
|
||||
private function legacyFilename(string $path): string
|
||||
{
|
||||
$urlPath = parse_url($path, PHP_URL_PATH);
|
||||
$filename = basename(rawurldecode((string) ($urlPath ?: $path)));
|
||||
|
||||
return Str::of($filename)->trim()->toString();
|
||||
}
|
||||
|
||||
private function isAlreadySynced(?string $currentPath, string $destinationPath, bool $force): bool
|
||||
{
|
||||
return ! $force
|
||||
&& $currentPath === $destinationPath
|
||||
&& Storage::disk('public')->exists($destinationPath);
|
||||
}
|
||||
|
||||
private function copyToPublicStorage(string $sourcePath, string $destinationPath, bool $force): void
|
||||
{
|
||||
if (! $force && Storage::disk('public')->exists($destinationPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$stream = fopen($sourcePath, 'rb');
|
||||
|
||||
if ($stream === false) {
|
||||
throw new \RuntimeException("Quelldatei kann nicht gelesen werden: {$sourcePath}");
|
||||
}
|
||||
|
||||
try {
|
||||
Storage::disk('public')->put($destinationPath, $stream, 'public');
|
||||
} finally {
|
||||
fclose($stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ class ThemeHelper
|
|||
'positive' => 'img/logos/portal-logo-positive.svg',
|
||||
'negative' => 'img/logos/portal-logo-negative.svg',
|
||||
],
|
||||
'presseportale' => [
|
||||
'pressekonto' => [
|
||||
'positive' => 'img/logos/portal-logo-positive.svg',
|
||||
'negative' => 'img/logos/portal-logo-negative.svg',
|
||||
],
|
||||
|
|
@ -100,7 +100,7 @@ class ThemeHelper
|
|||
{
|
||||
$config = self::getDomainConfig();
|
||||
|
||||
return $config['domain_name'] ?? 'presseportale.test';
|
||||
return $config['domain_name'] ?? 'pressekonto.test';
|
||||
}
|
||||
|
||||
public static function getDomainUrl(): string
|
||||
|
|
@ -118,12 +118,12 @@ class ThemeHelper
|
|||
$theme = config('app.theme', 'portal');
|
||||
|
||||
$assetUrlMap = [
|
||||
'portal' => 'https://assets.presseportale.test',
|
||||
'presseportale' => 'https://assets.presseportale.test',
|
||||
'portal' => 'https://assets.pressekonto.test',
|
||||
'pressekonto' => 'https://assets.pressekonto.test',
|
||||
'presseecho' => 'https://assets.presseecho.test',
|
||||
'businessportal24' => 'https://assets.businessportal24.test',
|
||||
];
|
||||
|
||||
return $assetUrlMap[$theme] ?? 'https://assets.presseportale.test';
|
||||
return $assetUrlMap[$theme] ?? 'https://assets.pressekonto.test';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class MagicLoginLink extends Mailable
|
|||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Ihr Login-Link fuer presseportale'
|
||||
subject: 'Ihr Login-Link fuer pressekonto'
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ class AppServiceProvider extends ServiceProvider
|
|||
config(['app.asset_url' => $assetUrl]);
|
||||
} catch (\Exception $e) {
|
||||
// Fallback to default if theme detection fails
|
||||
config(['app.asset_url' => 'https://assets.presseportale.test']);
|
||||
config(['app.asset_url' => 'https://assets.pressekonto.test']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ class ThemeServiceProvider extends ServiceProvider
|
|||
|
||||
if (app()->environment('local')) {
|
||||
// Entwicklung: Vite Dev Server mit HMR
|
||||
$viteDevServerUrl = env('VITE_DEV_SERVER_URL', 'https://assets.presseportale.test');
|
||||
$viteDevServerUrl = env('VITE_DEV_SERVER_URL', 'https://assets.pressekonto.test');
|
||||
Vite::useHotFile(public_path('hot'));
|
||||
config(['app.vite_dev_server_url' => $viteDevServerUrl]);
|
||||
View::share('viteDevServerUrl', $viteDevServerUrl);
|
||||
|
|
|
|||
|
|
@ -177,6 +177,24 @@ class ImageService
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates and persists missing variants for an existing company logo.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function generateMissingCompanyLogoVariants(string $relativePath): array
|
||||
{
|
||||
$disk = $this->disk();
|
||||
|
||||
if (! $disk->exists($relativePath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$extension = strtolower(pathinfo($relativePath, PATHINFO_EXTENSION) ?: 'jpg');
|
||||
|
||||
return $this->generateLogoVariants($disk, $relativePath, $extension);
|
||||
}
|
||||
|
||||
private function deleteWithVariants(?string $relativePath, ?array $variants, ?string $diskName = null): void
|
||||
{
|
||||
$disk = $diskName ? Storage::disk($diskName) : $this->disk();
|
||||
|
|
|
|||
|
|
@ -13,23 +13,23 @@ return [
|
|||
*/
|
||||
'protocol' => env('APP_PROTOCOL', 'https://'),
|
||||
|
||||
'domain_portal' => env('APP_PORTAL_NAME', 'presseportale.test'),
|
||||
'domain_portal' => env('APP_PORTAL_NAME', 'pressekonto.test'),
|
||||
'domain_presseecho' => env('APP_PRESSEECHO_NAME', 'presseecho.test'),
|
||||
'domain_businessportal' => env('APP_BUSINESSPORTAL_NAME', 'businessportal24.test'),
|
||||
|
||||
'domain_portal_url' => env('APP_PORTAL_URL', 'https://presseportale.test'),
|
||||
'domain_portal_url' => env('APP_PORTAL_URL', 'https://pressekonto.test'),
|
||||
'domain_presseecho_url' => env('APP_PRESSEECHO_URL', 'https://presseecho.test'),
|
||||
'domain_businessportal_url' => env('APP_BUSINESSPORTAL_URL', 'https://businessportal24.test'),
|
||||
|
||||
'domains' => [
|
||||
'portal' => [
|
||||
'domain_name' => env('APP_PORTAL_NAME', 'presseportale.test'),
|
||||
'url' => env('APP_PORTAL_URL', 'https://presseportale.test'),
|
||||
'asset_url' => env('APP_PORTAL_ASSET_URL', 'https://assets.presseportale.test'),
|
||||
'domain_name' => env('APP_PORTAL_NAME', 'pressekonto.test'),
|
||||
'url' => env('APP_PORTAL_URL', 'https://pressekonto.test'),
|
||||
'asset_url' => env('APP_PORTAL_ASSET_URL', 'https://assets.pressekonto.test'),
|
||||
'theme' => 'main',
|
||||
'view_prefix' => 'portal',
|
||||
'assets_dir' => 'build/portal',
|
||||
'description' => 'Backend Presseportale',
|
||||
'description' => 'Backend Pressekonto',
|
||||
'color_scheme' => [
|
||||
'primary' => env('APP_PORTAL_PRIMARY', '#526266'), //
|
||||
'secondary' => env('APP_PORTAL_SECONDARY', '#82a0a7'), //
|
||||
|
|
@ -37,14 +37,14 @@ return [
|
|||
'font' => 'Montserrat',
|
||||
],
|
||||
|
||||
'presseportale' => [
|
||||
'domain_name' => env('APP_PORTAL_NAME', 'presseportale.test'),
|
||||
'url' => env('APP_PORTAL_URL', 'https://presseportale.test'),
|
||||
'asset_url' => env('APP_PORTAL_ASSET_URL', 'https://assets.presseportale.test'),
|
||||
'theme' => 'presseportale',
|
||||
'pressekonto' => [
|
||||
'domain_name' => env('APP_PORTAL_NAME', 'pressekonto.test'),
|
||||
'url' => env('APP_PORTAL_URL', 'https://pressekonto.test'),
|
||||
'asset_url' => env('APP_PORTAL_ASSET_URL', 'https://assets.pressekonto.test'),
|
||||
'theme' => 'pressekonto',
|
||||
'view_prefix' => 'web',
|
||||
'assets_dir' => 'build/web',
|
||||
'description' => 'Hub-Landing presseportale.com (öffentlicher Publisher-Bereich)',
|
||||
'description' => 'Hub-Landing pressekonto.de (öffentlicher Publisher-Bereich)',
|
||||
'color_scheme' => [
|
||||
'primary' => '#1A2540',
|
||||
'secondary' => '#B07A3A',
|
||||
|
|
@ -52,12 +52,12 @@ return [
|
|||
'font' => 'Inter Tight',
|
||||
'brand' => [
|
||||
'name' => 'presse',
|
||||
'accent' => 'portale',
|
||||
'accent' => 'konto',
|
||||
'tagline_short' => 'Publisher · Hub',
|
||||
'tagline_long' => 'Der gemeinsame Publisher-Bereich für presseecho und businessportal24 – Pressemitteilungen schreiben, redaktionell prüfen lassen, auf beiden Reichweiten veröffentlichen.',
|
||||
'footer_legal' => '© :year presseportale · Alle Rechte vorbehalten',
|
||||
'about_label' => 'Über presseportale',
|
||||
'meta_title' => 'presseportale – Publisher-Hub für presseecho und businessportal24',
|
||||
'footer_legal' => '© :year pressekonto · Alle Rechte vorbehalten',
|
||||
'about_label' => 'Über pressekonto',
|
||||
'meta_title' => 'pressekonto – Publisher-Hub für presseecho und businessportal24',
|
||||
'meta_description' => 'Ein Konto, zwei Reichweiten: Pressemitteilungen redaktionell geprüft auf presseecho und businessportal24 gleichzeitig veröffentlichen.',
|
||||
],
|
||||
],
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'dev_url' => env('ASSET_URL', env('VITE_DEV_SERVER_URL', 'https://assets.presseportale.test')),
|
||||
'dev_url' => env('ASSET_URL', env('VITE_DEV_SERVER_URL', 'https://assets.pressekonto.test')),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -20,11 +20,11 @@ class DatabaseSeeder extends Seeder
|
|||
RolesAndPermissionsSeeder::class,
|
||||
AdminPresetSeeder::class,
|
||||
PaymentOptionSeeder::class,
|
||||
CategorySeeder::class,
|
||||
// CategorySeeder::class,
|
||||
]);
|
||||
|
||||
$adminUser = User::firstOrCreate([
|
||||
'email' => 'admin@presseportale.test',
|
||||
'email' => 'admin@pressekonto.test',
|
||||
], [
|
||||
'name' => 'Portal Admin',
|
||||
'password' => Hash::make('password'),
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
# Entwicklungskonzept – BusinessPortal24, Presseecho & presseportale-Hub Frontend
|
||||
# Entwicklungskonzept – BusinessPortal24, Presseecho & pressekonto-Hub Frontend
|
||||
|
||||
> **Stand:** 13. Mai 2026
|
||||
> **Domains:** `businessportal24.test` / `.com` · `presseecho.test` / `.de` · `presseportale.test` / `.com` (Hub)
|
||||
> **Theme-Slugs:** `businessportal24` (warm-rotes Editorial) · `presseecho` (grünes Editorial) · `presseportale` (Hub-Blau · Publisher-Landing)
|
||||
> **Domains:** `businessportal24.test` / `.com` · `presseecho.test` / `.de` · `pressekonto.test` / `.de` (Hub)
|
||||
> **Theme-Slugs:** `businessportal24` (warm-rotes Editorial) · `presseecho` (grünes Editorial) · `pressekonto` (Hub-Blau · Publisher-Landing)
|
||||
> **Assets-Dir (geteilt):** `public/build/web/`
|
||||
> **Ziel:** Editoriales DACH-Pressemitteilungs-Ökosystem mit 1:1-Mockup-Umsetzung. Presseecho nutzt die **gleiche Komponenten-Architektur** wie BP24, der Hub `presseportale.com` ist eine **eigenständige Publisher-Landing** mit klar abgegrenztem Charakter (Hub-Blau + Bernstein, kein Editorial-Feed).
|
||||
> **Ziel:** Editoriales DACH-Pressemitteilungs-Ökosystem mit 1:1-Mockup-Umsetzung. Presseecho nutzt die **gleiche Komponenten-Architektur** wie BP24, der Hub `pressekonto.de` ist eine **eigenständige Publisher-Landing** mit klar abgegrenztem Charakter (Hub-Blau + Bernstein, kein Editorial-Feed).
|
||||
|
||||
Dieses Dokument beschreibt den aktuellen Stand und die wichtigsten Architekturentscheidungen der BusinessPortal24-, Presseecho- und presseportale-Hub-Frontend-Entwicklung. Es ist die zentrale Anlaufstelle für alle, die im Frontend weiterarbeiten oder neue Seiten ergänzen.
|
||||
Dieses Dokument beschreibt den aktuellen Stand und die wichtigsten Architekturentscheidungen der BusinessPortal24-, Presseecho- und pressekonto-Hub-Frontend-Entwicklung. Es ist die zentrale Anlaufstelle für alle, die im Frontend weiterarbeiten oder neue Seiten ergänzen.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -67,9 +67,9 @@ Request (Host: presseecho.test)
|
|||
|
||||
Lokale Domain-Simulation:
|
||||
- `.env`: `DEV_SIMULATE_DOMAIN=true`, `DEV_SIMULATED_DOMAIN=businessportal24.test|presseecho.test`
|
||||
- Alternativ: `?theme=businessportal24|presseecho|presseportale` als Query-Parameter
|
||||
- Alternativ: `?theme=businessportal24|presseecho|pressekonto` als Query-Parameter
|
||||
|
||||
> **Hub-Sonderfall (`presseportale.test`):** Diese Domain ist gleichzeitig **Admin-Backend** (Auth/Admin/Customer-Routen, theme = `main`, Build-Dir `build/portal/`) **und** öffentliche **Hub-Landing** (theme = `presseportale`, Build-Dir `build/web/`). In `config/domains.php` existieren beide Einträge (`portal` und `presseportale`) für dieselbe `domain_name`. Der `ThemeServiceProvider` matcht zuerst `portal` (Backend-Standard); für die öffentliche Landing schaltet **`routes/web.php` per `$applyWebDomainConfig('presseportale')` explizit auf das Hub-Theme** um. Auth- und Admin-Routen bleiben unbeeinflusst.
|
||||
> **Hub-Sonderfall (`pressekonto.test`):** Diese Domain ist gleichzeitig **Admin-Backend** (Auth/Admin/Customer-Routen, theme = `main`, Build-Dir `build/portal/`) **und** öffentliche **Hub-Landing** (theme = `pressekonto`, Build-Dir `build/web/`). In `config/domains.php` existieren beide Einträge (`portal` und `pressekonto`) für dieselbe `domain_name`. Der `ThemeServiceProvider` matcht zuerst `portal` (Backend-Standard); für die öffentliche Landing schaltet **`routes/web.php` per `$applyWebDomainConfig('pressekonto')` explizit auf das Hub-Theme** um. Auth- und Admin-Routen bleiben unbeeinflusst.
|
||||
|
||||
### 3.1 Generischer Daten-Provider
|
||||
|
||||
|
|
@ -138,7 +138,7 @@ Damit das Theme für Presseecho dokumentiert ist, hier der **verbindliche Token-
|
|||
| Erste Variante | `#1f4d3a → #163a2c` | **zu hell** |
|
||||
| **Final** | **`#1a3d2e → #122d22`** | **abgenommen** ✅ |
|
||||
|
||||
### 3.1.2 presseportale-Hub-Palette (Stand 13.05.2026)
|
||||
### 3.1.2 pressekonto-Hub-Palette (Stand 13.05.2026)
|
||||
|
||||
Der **Hub** ist bewusst eigenständig positioniert: er ist **kein Editorial-Feed**, sondern eine reine Publisher-Landing („Ein Konto – zwei Reichweiten"). Er bekommt daher einen ganz eigenen Charakter:
|
||||
|
||||
|
|
@ -147,7 +147,7 @@ Der **Hub** ist bewusst eigenständig positioniert: er ist **kein Editorial-Feed
|
|||
* **Akzent:** gedecktes Bernstein `#B07A3A` – **bewusst gewählt**, weil weder Orange (BP24) noch Grün (Presseecho). Der Hub steht visuell „zwischen" den beiden Brands.
|
||||
* **Schrift:** Inter Tight (Standardtext) + JetBrains Mono (Mono) + Source Serif 4 (**nur für Marken-Mentions** der Tochter-Portale, damit typografische Konsistenz zur jeweiligen Brand-Landing erhalten bleibt; im Hub-Fließtext nicht verwendet).
|
||||
|
||||
Token-Snapshot aus `resources/css/web/theme-presseportale.css`:
|
||||
Token-Snapshot aus `resources/css/web/theme-pressekonto.css`:
|
||||
|
||||
```css
|
||||
@theme {
|
||||
|
|
@ -225,12 +225,12 @@ In `config/domains.php` liegt pro Domain ein **`brand`-Block**, der Komponenten
|
|||
...
|
||||
],
|
||||
],
|
||||
'presseportale' => [ // Hub-Variante (web)
|
||||
'theme' => 'presseportale',
|
||||
'pressekonto' => [ // Hub-Variante (web)
|
||||
'theme' => 'pressekonto',
|
||||
'brand' => [
|
||||
'name' => 'presse', // hub-blau
|
||||
'accent' => 'portale', // bernstein
|
||||
'footer_legal' => '© :year presseportale · Alle Rechte vorbehalten',
|
||||
'accent' => 'konto', // bernstein
|
||||
'footer_legal' => '© :year pressekonto · Alle Rechte vorbehalten',
|
||||
...
|
||||
],
|
||||
],
|
||||
|
|
@ -246,12 +246,12 @@ Die Schreibweise der drei Marken folgt einer einheitlichen Regel: **keine TLD-En
|
|||
| ------------------ | ------------------------------------------ | ---------------------- | --------------------- |
|
||||
| `presseecho` | **presse**·*echo* | `#345636` (Forest) | `#9BD5B2` |
|
||||
| `businessportal24` | **businessportal**·*24* | `#C84A1E` (Orange) | `#F4B098` |
|
||||
| `presseportale` | **presse**·*portale* | `#B07A3A` (Bernstein) | `#B07A3A` |
|
||||
| `pressekonto` | **presse**·*konto* | `#B07A3A` (Bernstein) | `#B07A3A` |
|
||||
|
||||
**Single Source of Truth:** Die Komponente `<x-web.brand-mark brand="…" />` rendert die Markenschreibung zentral inkl. Span-Splitting, Schriftart und Akzentfarbe. Sie wird überall verwendet, wo eine Marke als Fließtext-Mention erscheint:
|
||||
|
||||
* Hub-Komponenten (`hub/top-utility-bar`, `hub/site-header`, `hub/site-footer`, `hub/brand-context-banner`)
|
||||
* Hub-View `presseportale.blade.php` (Hero-Headline, Architektur-Diagramm, Tarif-Subline, Plattform-Familie, FAQ)
|
||||
* Hub-View `pressekonto.blade.php` (Hero-Headline, Architektur-Diagramm, Tarif-Subline, Plattform-Familie, FAQ)
|
||||
* Cross-Brand-Mentions auf BP24-/Presseecho-Landings, falls ergänzt
|
||||
|
||||
```blade
|
||||
|
|
@ -417,22 +417,22 @@ Alle Komponenten haben **konsistente Konventionen**:
|
|||
|
||||
### 5.4 Hub-Komponenten (`components/web/hub/`)
|
||||
|
||||
Der Hub `presseportale.com` hat einen **eigenen, deutlich anderen Charakter** als die beiden Brand-Portale (kein Editorial-Feed, sondern Publisher-Landing) und bekommt daher einen eigenen Komponenten-Namespace. Die Sektionen selbst (Hero, Features, How-it-works, Tarife, Plattform-Familie, Social-Proof, FAQ, CTA) sind als **inline-Markup** in `resources/views/web/presseportale.blade.php` umgesetzt, weil sie page-spezifisch sind.
|
||||
Der Hub `pressekonto.de` hat einen **eigenen, deutlich anderen Charakter** als die beiden Brand-Portale (kein Editorial-Feed, sondern Publisher-Landing) und bekommt daher einen eigenen Komponenten-Namespace. Die Sektionen selbst (Hero, Features, How-it-works, Tarife, Plattform-Familie, Social-Proof, FAQ, CTA) sind als **inline-Markup** in `resources/views/web/pressekonto.blade.php` umgesetzt, weil sie page-spezifisch sind.
|
||||
|
||||
| Datei | Rolle |
|
||||
| --- | --- |
|
||||
| `hub/top-utility-bar.blade.php` | Schmale Hub-Blau-Topbar mit Datum, „Publisher-Hub für …"-Brand-Family-Links (rendert `<x-web.brand-mark variant="on-dark" :serif="false">`), Status/Doku/Kontakt. Props: `date`, `siblingPortals` (jetzt Liste mit `brand`-Key statt fixer Strings). |
|
||||
| `hub/site-header.blade.php` | Wortmark `presse`·`portale` (über `<x-web.brand-mark brand="presseportale" :serif="false">`) + Untertitel „Publisher · Hub", zentrale Primary-Nav (Tarife, So funktioniert es, …), Anmelden + Konto erstellen CTAs. Routes: `login`, `register`. |
|
||||
| `hub/site-header.blade.php` | Wortmark `presse`·`konto` (über `<x-web.brand-mark brand="pressekonto" :serif="false">`) + Untertitel „Publisher · Hub", zentrale Primary-Nav (Tarife, So funktioniert es, …), Anmelden + Konto erstellen CTAs. Routes: `login`, `register`. |
|
||||
| `hub/brand-context-banner.blade.php` | **Conditional Banner** unter dem Header – greift nur bei `?from=presseecho` oder `?from=businessportal24` und zeigt: „Sie kommen von … – Ihr Konto hier funktioniert für beide Portale". Markenname über Brand-Mark (font-serif), „Zurück zu …"-Link nutzt sans-Variante. |
|
||||
| `hub/site-footer.blade.php` | 4-Spaltiger Hub-Footer (Konto / Plattform / Rechtliches + Brand-Spalte mit Plattform-Familie-Links über Brand-Mark `variant="on-dark"`), Hub-Gradient `linear-gradient(180deg,#1A2540,#0F1729)`. Brand-Block aus `config/domains.php`. |
|
||||
|
||||
**Hub-Sektionen als inline-Blade** (in `presseportale.blade.php`):
|
||||
**Hub-Sektionen als inline-Blade** (in `pressekonto.blade.php`):
|
||||
|
||||
1. **Hero** mit Architektur-Diagramm rechts (zentraler Hub-Knoten + Brand-Portal-Karten + Output-Boxen, alles SVG-only).
|
||||
2. **Was Sie hier können** – 3-Karten-Grid (Veröffentlichen / Newsrooms / Reichweite).
|
||||
3. **So funktioniert es** – 4-Step-Ol mit Differenzierungs-Highlight in Schritt 3 (Bernstein-Akzent für „Unsere Qualitätssicherung").
|
||||
4. **Tarife** – 3 Karten (Starter / Standard / Pro mit `.ribbon-recommend`) + breiter Enterprise-Streifen in Hub-Blau.
|
||||
5. **Hinter presseportale.com** – 2-Spalten-Plattform-Familie mit den **Original-Brand-Gradients** der Tochter-Portale (`#1F4D3A→#163A2C` für Presseecho, `#1A1F26→#232A33` für BP24).
|
||||
5. **Hinter pressekonto.de** – 2-Spalten-Plattform-Familie mit den **Original-Brand-Gradients** der Tochter-Portale (`#1F4D3A→#163A2C` für Presseecho, `#1A1F26→#232A33` für BP24).
|
||||
6. **Aktive Newsrooms** – Prose-Auflistung statt Logo-Wall + kompakte Stats-Sidebar.
|
||||
7. **FAQ** – CSS-only-Accordion (`<details>` + `.faq-chev`) mit 8 Fragen, eine offen by default.
|
||||
8. **CTA-Wiederholung** + Footer.
|
||||
|
|
@ -496,12 +496,12 @@ Drei spiegelbildliche Test-Szenarien:
|
|||
2. **`feed only shows published presseecho content`** – Portal-Trennung gespiegelt: Presseecho + Both sichtbar, BP24-Only und Drafts nicht.
|
||||
3. **`shows most read releases in the sidebar`** – Hits-Sortierung.
|
||||
|
||||
### Datei: `tests/Feature/Web/PresseportaleHubHomeTest.php`
|
||||
### Datei: `tests/Feature/Web/PressekontoHubHomeTest.php`
|
||||
|
||||
Fünf Test-Szenarien rund um die Hub-Landing:
|
||||
|
||||
1. **`renders the publisher landing shell`** – prüft alle Hauptsektionen sind sichtbar (Publisher-Hub, Was Sie hier können, So funktioniert es, Vier Schritte, Tarife, Starter, Standard, Pro, Enterprise, Hinter presseportale.com, Plattform im Überblick, Häufige Fragen, Loslegen, Alle Systeme betriebsbereit).
|
||||
2. **`loads the hub theme assets, not portal admin`** – stellt sicher, dass `theme-presseportale` aus dem Manifest geladen wird (nicht `theme-businessportal24` oder `theme-presseecho`).
|
||||
1. **`renders the publisher landing shell`** – prüft alle Hauptsektionen sind sichtbar (Publisher-Hub, Was Sie hier können, So funktioniert es, Vier Schritte, Tarife, Starter, Standard, Pro, Enterprise, Hinter pressekonto.de, Plattform im Überblick, Häufige Fragen, Loslegen, Alle Systeme betriebsbereit).
|
||||
2. **`loads the hub theme assets, not portal admin`** – stellt sicher, dass `theme-pressekonto` aus dem Manifest geladen wird (nicht `theme-businessportal24` oder `theme-presseecho`).
|
||||
3. **`hides the brand-context banner without a from parameter`** – Default-Aufruf zeigt keinen „Sie kommen von …"-Banner.
|
||||
4. **`shows the brand-context banner when arriving from presseecho`** – `?from=presseecho` triggert den Banner inkl. Link „Zurück zu presseecho.de".
|
||||
5. **`shows the brand-context banner when arriving from businessportal24`** – `?from=businessportal24` triggert den Banner für BP24.
|
||||
|
|
@ -563,9 +563,9 @@ vendor/bin/pint --dirty --format agent
|
|||
| 13 | 12.05.2026 | Brand-Konfiguration in `config/domains.php` pro Domain (`brand.name`, `brand.accent`, `brand.tagline_*`, `brand.newsletter_topics`, `brand.footer_legal`, `brand.about_label`) – `site-header`, `site-footer`, `newsletter-strip` lesen daraus | ✅ |
|
||||
| 14 | 12.05.2026 | `press-release-feed`-Volt-Component portal-agnostisch (Prop `:portal`); Aufruf vom View-Layer aus | ✅ |
|
||||
| 15 | 12.05.2026 | `PresseechoHomeTest` analog zu `Businessportal24HomeTest` (3 Szenarien, RefreshDatabase) | ✅ |
|
||||
| 16 | 13.05.2026 | **presseportale-Hub-Landing live**: neues Web-Theme `presseportale` (Hub-Blau + Bernstein, `theme-presseportale.css`), eigener Komponenten-Namespace `components/web/hub/` (Top-Bar, Site-Header, Brand-Context-Banner, Site-Footer), Hub-View `web/presseportale.blade.php` mit Hero/Architektur-Diagramm, Features, How-it-works, Tarife (Starter/Standard/Pro+Ribbon, Enterprise-Streifen), Plattform-Familie, Aktive-Newsrooms, FAQ-Accordion, CTA. `routes/web.php` schaltet für `presseportale.test` auf das Hub-Theme um. Root-Route in `routes/admin.php` entfernt (Layout referenziert jetzt `route('dashboard')`). | ✅ |
|
||||
| 17 | 13.05.2026 | `PresseportaleHubHomeTest` (5 Szenarien inkl. Brand-Context-Banner-Conditional). Vite-Config + ThemeHelper + `web-master`-Fonts (Inter Tight + JetBrains Mono ohne Serif) für `presseportale` ergänzt. | ✅ |
|
||||
| 18 | 13.05.2026 | **Brand-Mark-Konvention etabliert** (Feintuning Marken-Schreibweise): keine TLD am Markenschriftzug, Akzent farblich vom Basis-Wort abgesetzt. Single Source of Truth `<x-web.brand-mark>` (Marken-Tabelle inkl. Standard- und On-Dark-Akzentfarben, Serif/Sans-Switch). `config/domains.php` umgestellt (`presseecho`: `name=presse`/`accent=echo`; `presseportale`: `name=presse`/`accent=portale`; Footer-Legal & Meta-Texte ohne TLD). Hub-Komponenten und Hub-View durchgehend auf Brand-Mark migriert (Top-Utility-Bar, Site-Header, Brand-Context-Banner, Site-Footer, Hero-Headline, Architektur-Diagramm, Tarif-Subline, Plattform-Familie, FAQ). Hub-Theme bekommt Source Serif 4 als `--font-serif` (für Marken-Mentions) – Bunny-Font-Loader erweitert. **+1 neuer Test `uses the brand-mark splitting without TLDs`**; alle 12 Web-Tests grün. | ✅ |
|
||||
| 16 | 13.05.2026 | **pressekonto-Hub-Landing live**: neues Web-Theme `pressekonto` (Hub-Blau + Bernstein, `theme-pressekonto.css`), eigener Komponenten-Namespace `components/web/hub/` (Top-Bar, Site-Header, Brand-Context-Banner, Site-Footer), Hub-View `web/pressekonto.blade.php` mit Hero/Architektur-Diagramm, Features, How-it-works, Tarife (Starter/Standard/Pro+Ribbon, Enterprise-Streifen), Plattform-Familie, Aktive-Newsrooms, FAQ-Accordion, CTA. `routes/web.php` schaltet für `pressekonto.test` auf das Hub-Theme um. Root-Route in `routes/admin.php` entfernt (Layout referenziert jetzt `route('dashboard')`). | ✅ |
|
||||
| 17 | 13.05.2026 | `PressekontoHubHomeTest` (5 Szenarien inkl. Brand-Context-Banner-Conditional). Vite-Config + ThemeHelper + `web-master`-Fonts (Inter Tight + JetBrains Mono ohne Serif) für `pressekonto` ergänzt. | ✅ |
|
||||
| 18 | 13.05.2026 | **Brand-Mark-Konvention etabliert** (Feintuning Marken-Schreibweise): keine TLD am Markenschriftzug, Akzent farblich vom Basis-Wort abgesetzt. Single Source of Truth `<x-web.brand-mark>` (Marken-Tabelle inkl. Standard- und On-Dark-Akzentfarben, Serif/Sans-Switch). `config/domains.php` umgestellt (`presseecho`: `name=presse`/`accent=echo`; `pressekonto`: `name=presse`/`accent=konto`; Footer-Legal & Meta-Texte ohne TLD). Hub-Komponenten und Hub-View durchgehend auf Brand-Mark migriert (Top-Utility-Bar, Site-Header, Brand-Context-Banner, Site-Footer, Hero-Headline, Architektur-Diagramm, Tarif-Subline, Plattform-Familie, FAQ). Hub-Theme bekommt Source Serif 4 als `--font-serif` (für Marken-Mentions) – Bunny-Font-Loader erweitert. **+1 neuer Test `uses the brand-mark splitting without TLDs`**; alle 12 Web-Tests grün. | ✅ |
|
||||
| 19 | 12.05.2026 | **Aktuell offen:** Detailseite, Branchenseite, Veröffentlichen-Landing für BP24 + Presseecho. Hub-Folgeseiten (Konto-Erstellen-Flow als Landing, Tarif-Detail, Doku-Hub) ebenfalls offen. | 🟡 |
|
||||
|
||||
---
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
216
dev/frontend/hub-flux/01-PHASE-0-TOKENS.md
Normal file
216
dev/frontend/hub-flux/01-PHASE-0-TOKENS.md
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
# Phase 0 — Design-Tokens vereinheitlichen
|
||||
|
||||
> **Ziel**: Single Source of Truth für alle Design-Tokens (Farben, Fonts,
|
||||
> Radii, Schatten). Sowohl Hub-Build als auch Portal-Build beziehen ihre
|
||||
> Werte aus derselben Datei. **Visuell ändert sich noch nichts.**
|
||||
|
||||
**Status**: ✅ abgeschlossen am 2026-05-19
|
||||
**Risiko**: sehr niedrig
|
||||
**Aufwand (tatsächlich)**: ~½ Tag
|
||||
|
||||
## Ergebnis-Check (2026-05-19)
|
||||
|
||||
- Single Source of Truth liegt in `resources/css/shared/design-tokens.css`.
|
||||
- Web-Build und Portal-Build importieren sie beide.
|
||||
- **Visuelle Unverändertheit verifiziert**:
|
||||
- Hub (`pressekonto.test/`, `/login`, `/register`) — unverändert.
|
||||
- Portal (`/dashboard`) — FluxUI-Defaults bleiben dominant
|
||||
(`--font-sans: "Instrument Sans"`, Zinc-Palette, `#3ea3dc`-Akzent).
|
||||
- Build-Sizes:
|
||||
- `theme-pressekonto: 193 kB` (vorher 189 kB · +4 kB für neue Tokens)
|
||||
- `theme-presseecho`, `theme-businessportal24`: praktisch unverändert
|
||||
- `portal: 408 kB` (vorher 397 kB · +12 kB für zusätzlich bereitgestellte Token-Vars im `:root`)
|
||||
- Details: `PROGRESS.md` (Eintrag vom 2026-05-19).
|
||||
|
||||
## Warum
|
||||
|
||||
Heute leben Tokens an zwei Orten:
|
||||
|
||||
- `resources/css/web/theme-pressekonto.css` — Hub-Tokens (Hub-Blau,
|
||||
Bernstein, Buchpapier, Inter Tight)
|
||||
- `resources/css/portal.css` — Portal-Tokens (Zinc, `#3ea3dc`, Instrument
|
||||
Sans)
|
||||
|
||||
Solange diese parallel gepflegt werden, **driften** sie auseinander. Wir
|
||||
ziehen die gemeinsame Wahrheit in eine eigene Datei und referenzieren
|
||||
sie aus beiden Welten.
|
||||
|
||||
## Liefergegenstand
|
||||
|
||||
```
|
||||
resources/css/shared/
|
||||
└── design-tokens.css ← NEU
|
||||
```
|
||||
|
||||
Inhalt: alle `--color-*`, `--font-*`, `--radius-*`, `--shadow-*` Tokens,
|
||||
die in Hub und Portal gleichermaßen gelten. Strukturiert als
|
||||
`@theme`-Block, sodass Tailwind v4 die Variablen sowohl als
|
||||
CSS-Custom-Properties als auch als Tailwind-Utility-Klassen erkennt.
|
||||
|
||||
## Schritte
|
||||
|
||||
### 1. Token-Inventur aus `theme-pressekonto.css`
|
||||
|
||||
Die folgenden Tokens werden aus `theme-pressekonto.css` extrahiert und
|
||||
nach `shared/design-tokens.css` verschoben:
|
||||
|
||||
#### Surfaces
|
||||
- `--color-bg`, `--color-bg-elev`, `--color-bg-rule`, `--color-bg-rule-strong`
|
||||
- `--color-bg-dark`, `--color-bg-card`, `--color-bg-card-warm`,
|
||||
`--color-bg-card-warm-border`, `--color-bg-card-warm-hover`,
|
||||
`--color-bg-card-warm-rule`
|
||||
|
||||
#### Hub-Palette
|
||||
- `--color-hub`, `--color-hub-2`, `--color-hub-3`
|
||||
- `--color-hub-soft`, `--color-hub-soft-2`, `--color-hub-line`
|
||||
- `--color-topbar`, `--color-topbar2`, `--color-topbar-deep`
|
||||
|
||||
#### Akzent (Bernstein)
|
||||
- `--color-accent`, `--color-accent-deep`, `--color-accent-soft`,
|
||||
`--color-accent-warm`
|
||||
|
||||
#### Ink (Anthrazit)
|
||||
- `--color-ink`, `--color-ink-2`, `--color-ink-3`, `--color-ink-4`
|
||||
- `--color-ink-on-dark`, `--color-ink-on-dark-2`, `--color-ink-on-dark-3`,
|
||||
`--color-ink-on-dark-muted`, `--color-ink-on-dark-rule`
|
||||
|
||||
#### Brand-Aliase + Status
|
||||
- `--color-brand`, `--color-brand-deep`, `--color-brand-soft`
|
||||
- `--color-live`, `--color-gain`, `--color-loss`, `--color-ok`
|
||||
|
||||
#### Editorial / Cards
|
||||
- `--color-card-warm-cat`, `--color-card-warm-title`,
|
||||
`--color-feature-line`, `--color-feature-dot`
|
||||
|
||||
#### Status (für KPI-Cards / Badges — laut Mockup ergänzen)
|
||||
- `--color-warn` `#A87A1F`, `--color-warn-soft` `#F6EAC8`
|
||||
- `--color-err` `#A8331F`, `--color-err-soft` `#F4DAD2`
|
||||
- `--color-ok-soft` `#E2F1E5`
|
||||
|
||||
#### Fonts
|
||||
- `--font-sans` (Inter Tight)
|
||||
- `--font-serif` (Source Serif 4 — nur für Brand-Mark)
|
||||
- `--font-mono` (JetBrains Mono)
|
||||
|
||||
#### Layout
|
||||
- `--container-layout: 1280px`
|
||||
|
||||
#### Radii (laut Mockup)
|
||||
- `--radius-xs: 3px`, `--radius-sm: 4px`, `--radius-md: 6px`,
|
||||
`--radius-lg: 8px`
|
||||
|
||||
#### Schatten (laut Mockup + Hub-Login)
|
||||
- `--shadow-soft`: leicht warm, für Cards
|
||||
- `--shadow-card`: Standard-Card-Schatten
|
||||
- `--shadow-card-hover`: Hover-Stufe
|
||||
- `--shadow-auth`: weiche Glocke unter Auth-Card
|
||||
|
||||
### 2. Datei `resources/css/shared/design-tokens.css` anlegen
|
||||
|
||||
Aufbau:
|
||||
|
||||
```css
|
||||
/**
|
||||
* Hub × FluxUI — Gemeinsame Design-Tokens
|
||||
*
|
||||
* Single Source of Truth für Hub-Frontend (build/web) und Portal-Backend
|
||||
* (build/portal). Beide CSS-Builds @import diese Datei.
|
||||
*
|
||||
* Token-Names sind STABIL — Werte können sich ändern (z.B. Dark Mode in
|
||||
* Phase 5), Namen nicht.
|
||||
*/
|
||||
|
||||
@theme {
|
||||
/* Surfaces */
|
||||
--color-bg: #f6f4ef;
|
||||
--color-bg-elev: #fbfaf6;
|
||||
/* … alle Tokens aus der Inventur … */
|
||||
}
|
||||
|
||||
/* Dark Mode (vorbereitet, in Phase 5 finalisiert) */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@theme {
|
||||
/* Spätere Dark-Werte */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. `theme-pressekonto.css` refactoren
|
||||
|
||||
Die `@theme {}`-Definitionen werden durch einen
|
||||
`@import "../shared/design-tokens.css";` ersetzt. Nur die `@layer
|
||||
components {}`-Klassen (`.eyebrow`, `.auth-card`, `.field-input`, etc.)
|
||||
bleiben in `theme-pressekonto.css`.
|
||||
|
||||
`:root { … --background, --primary … }`-HSL-Variablen für Legacy-Komponenten
|
||||
bleiben ebenfalls hier (sind Portal-unspezifisch).
|
||||
|
||||
### 4. `portal.css` minimal vorbereiten (noch keine Werte übernehmen)
|
||||
|
||||
In Phase 0 importieren wir die Token-Datei in `portal.css`, **aber lassen
|
||||
das alte `@theme`-Setup mit Zinc/Accent zunächst stehen**. Damit:
|
||||
|
||||
- Beide Welten greifen technisch auf die gleiche Datei zu
|
||||
- Aber Portal bleibt visuell unverändert (Zinc-Palette gewinnt durch
|
||||
Reihenfolge im `@theme`)
|
||||
|
||||
In Phase 1 wird das Zinc-Setup dann **gezielt durch Hub-Werte ersetzt**.
|
||||
|
||||
### 5. Build & Verifikation
|
||||
|
||||
```bash
|
||||
npm run build:web # → erzeugt theme-pressekonto.css ohne Drift
|
||||
npm run build:portal # → erzeugt portal.css unverändert
|
||||
```
|
||||
|
||||
Erwartung:
|
||||
- Hub-Landing rendert visuell **identisch** wie vorher
|
||||
- Hub-Auth-Pages rendern visuell **identisch** wie vorher
|
||||
- Portal rendert visuell **identisch** wie vorher
|
||||
|
||||
Smoke-Test (kein neues Test-Schreiben nötig):
|
||||
|
||||
```bash
|
||||
php artisan tinker --execute '
|
||||
$urls = [
|
||||
"https://pressekonto.test/",
|
||||
"https://pressekonto.test/login",
|
||||
"https://pressekonto.test/dashboard",
|
||||
];
|
||||
foreach ($urls as $u) {
|
||||
echo $u . " => " .
|
||||
app(\Illuminate\Contracts\Http\Kernel::class)
|
||||
->handle(\Illuminate\Http\Request::create($u, "GET"))
|
||||
->getStatusCode() . "\n";
|
||||
}'
|
||||
```
|
||||
|
||||
Alle 3 URLs müssen weiterhin `200` liefern (für `/dashboard` ggf.
|
||||
`302`-Redirect, je nach Auth-Status — beides ist okay, solange kein `500`).
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] `resources/css/shared/design-tokens.css` existiert mit allen Hub-Tokens
|
||||
- [ ] `theme-pressekonto.css` importiert die Token-Datei und enthält
|
||||
keine doppelten `--color-*`-Definitionen mehr
|
||||
- [ ] `portal.css` importiert die Token-Datei (Werte werden in Phase 1 genutzt)
|
||||
- [ ] `npm run build:web` und `npm run build:portal` laufen ohne Fehler durch
|
||||
- [ ] Hub-Landing, Hub-Auth und Portal-Login visuell **unverändert**
|
||||
- [ ] Pint passed (`vendor/bin/sail bin pint --dirty --format agent`)
|
||||
|
||||
## Risiken & Fallstricke
|
||||
|
||||
- **Tailwind v4 + `@theme`**: Mehrere `@theme {}`-Blöcke in importierten
|
||||
Dateien werden zusammengeführt. Das funktioniert, solange die
|
||||
Token-Namen eindeutig sind.
|
||||
- **Reihenfolge der Imports**: Tokens müssen **vor** den
|
||||
`@layer components {}`-Definitionen importiert werden, sonst
|
||||
greifen die Variablen in den Komponenten nicht.
|
||||
- **Portal-Tailwind-Config**: `tailwind.portal.config.js` darf die
|
||||
Token-Datei nicht ausschließen. `@source`-Direktiven prüfen.
|
||||
|
||||
## Was Phase 0 NICHT macht
|
||||
|
||||
- Portal sieht **noch nicht** wie der Hub aus — das ist Phase 1
|
||||
- Keine Änderung am Sidebar-Layout, am Logo oder am Dashboard
|
||||
- Keine Dark-Mode-Aktivierung (nur vorbereitet)
|
||||
290
dev/frontend/hub-flux/02-PHASE-1-PORTAL-SHELL.md
Normal file
290
dev/frontend/hub-flux/02-PHASE-1-PORTAL-SHELL.md
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
# Phase 1 — Portal-Shell ans Hub-Design angleichen
|
||||
|
||||
> **Ziel**: Sidebar, Topbar und Layout-Container des Portals sehen aus
|
||||
> wie das Mockup `User Dashboard presseportale.html`. Inhalt der
|
||||
> einzelnen Pages bleibt unverändert — wir tauschen nur die Shell.
|
||||
|
||||
**Status**: ✅ abgeschlossen am 2026-05-19
|
||||
**Risiko (tatsächlich)**: niedrig — keine Test-Regressionen
|
||||
**Aufwand (tatsächlich)**: ~½ Tag (kleiner als geschätzt, weil Topbar
|
||||
auf Phase 2 verschoben wurde)
|
||||
|
||||
## Ergebnis-Check (2026-05-19)
|
||||
|
||||
**Umgesetzt**:
|
||||
- `portal.css` mit Hub-Token-Bridge, Inter-Tight-Font, Zinc→Hub-Mapping,
|
||||
FluxUI-Overrides für Sidebar / Navlist / Buttons / Cards.
|
||||
- Sidebar mit Brand-Mark + Eyebrow „Publisher · Hub", neuem Hub-Stil-
|
||||
Testmodus-Block, ohne Starter-Kit-Resources-Block.
|
||||
- Customer-Banner (`app.blade.php`) im Hub-Soft-Look mit Hub-Pille.
|
||||
- `class="dark"` aus Sidebar-Layout entfernt — Light Mode ist Default.
|
||||
- Font-Wechsel auf Bunny: `inter-tight + jetbrains-mono + source-serif-4`.
|
||||
|
||||
**Verschoben auf Phase 2**:
|
||||
- Eigene Topbar mit Breadcrumb + Bridge-Row + Search + „Neue Mitteilung"-CTA
|
||||
(lebt sinnvoller im Customer-Dashboard-Kontext).
|
||||
- Konto-Switcher als Sidebar-Header oben (statt User-Menü unten).
|
||||
|
||||
**Build-Sizes**:
|
||||
- `portal.css: 409.03 kB` (vorher 408.89 kB · +0.14 kB · weit unter dem
|
||||
10 %-Limit aus den Akzeptanzkriterien).
|
||||
|
||||
**Tests**:
|
||||
- Auth-Test-Suite Vergleich (Stash vs Phase 1): `8 failed, 15 passed`
|
||||
in beiden Ständen — 0 zusätzliche Regressionen durch Phase 1.
|
||||
- Zwei Tests im Zuge des Login-Fixes angepasst (Admin-Rolle bzw.
|
||||
`terms_accepted: true`), siehe `PROGRESS.md` Eintrag „Phase 1".
|
||||
|
||||
**Details**: `PROGRESS.md` (Eintrag vom 2026-05-19, Abschnitt „Phase 1").
|
||||
|
||||
## Sichtbarer Mehrwert
|
||||
|
||||
Nach Phase 1 sieht der eingeloggte User:
|
||||
- Eine **warme Sidebar** im Hub-Stil mit Brand-Wortmarke statt
|
||||
Starter-Kit-Logo
|
||||
- Eine **schlanke Topbar** mit Breadcrumb, Bridge-Row, Search, Notification,
|
||||
"Neue Mitteilung"-Button
|
||||
- Den **Testmodus-Block** (Impersonation) als Hub-Karten-Element
|
||||
- Den **Konto-Switcher** als oberen Sidebar-Header
|
||||
|
||||
Innenleben (Tabellen, Formulare, Cards) bleiben FluxUI — wirken aber
|
||||
durch Token-Anpassung **automatisch passender**.
|
||||
|
||||
## Liefergegenstand
|
||||
|
||||
### Geänderte Dateien
|
||||
|
||||
| Datei | Änderung |
|
||||
|-------|----------|
|
||||
| `resources/css/portal.css` | Zinc → Hub-Palette, Font auf Inter Tight, `--color-accent` auf Hub-Blau, FluxUI-Overrides |
|
||||
| `resources/views/components/layouts/app/sidebar.blade.php` | Brand-Mark statt App-Logo, Eyebrow „Publisher · Hub", Sidebar-Design am Mockup orientiert, Testmodus-Block neu |
|
||||
| `resources/views/partials/head.blade.php` | Font-Wechsel (Bunny: inter-tight + jetbrains-mono statt instrument-sans) |
|
||||
| `resources/views/components/layouts/app.blade.php` | Customer-Banner ggf. an neues Design anpassen |
|
||||
|
||||
### Vermutlich gelöscht
|
||||
|
||||
| Datei | Begründung |
|
||||
|-------|------------|
|
||||
| `resources/views/components/layouts/app/header.blade.php` | Wird laut Inventur nirgends referenziert |
|
||||
| `resources/views/partials/admin-head.blade.php` | Legacy, im Code nicht eingebunden |
|
||||
| `resources/views/components/app-logo.blade.php` | Wird durch `brand-mark` ersetzt |
|
||||
| `resources/views/components/app-logo-icon.blade.php` | Wird durch `brand-mark` ersetzt |
|
||||
|
||||
Vor Löschung: per `rg "x-app-logo"` und `rg "auth.split|auth.card"`
|
||||
prüfen, dass nichts mehr referenziert wird.
|
||||
|
||||
## Schritte
|
||||
|
||||
### 1. `portal.css` — Token-Bridge zu FluxUI
|
||||
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
@import "../../vendor/livewire/flux/dist/flux.css";
|
||||
@import "./shared/design-tokens.css"; /* aus Phase 0 */
|
||||
|
||||
@source '../views';
|
||||
@source '../../vendor/livewire/flux-pro/stubs/**/*.blade.php';
|
||||
@source '../../vendor/livewire/flux/stubs/**/*.blade.php';
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
/* FluxUI-Akzent auf Hub-Blau */
|
||||
--color-accent: var(--color-hub);
|
||||
--color-accent-content: var(--color-hub);
|
||||
--color-accent-foreground: #ffffff;
|
||||
|
||||
/* Zinc auf warmes Buchpapier mappen */
|
||||
--color-zinc-50: var(--color-bg-elev); /* #FBFAF6 */
|
||||
--color-zinc-100: var(--color-bg); /* #F6F4EF */
|
||||
--color-zinc-200: var(--color-bg-rule); /* #E2DDD0 */
|
||||
--color-zinc-300: #cfc8b5;
|
||||
--color-zinc-400: var(--color-ink-4); /* #8A918D */
|
||||
--color-zinc-500: var(--color-ink-3); /* #5A6360 */
|
||||
--color-zinc-600: var(--color-ink-2); /* #3A413D */
|
||||
--color-zinc-700: var(--color-ink); /* #1A1F1C */
|
||||
--color-zinc-800: var(--color-hub-2); /* #243152 */
|
||||
--color-zinc-900: var(--color-hub); /* #1A2540 */
|
||||
--color-zinc-950: var(--color-topbar-deep); /* #0F1729 */
|
||||
}
|
||||
```
|
||||
|
||||
#### FluxUI-spezifische Overrides
|
||||
|
||||
```css
|
||||
/* Navlist — Hub-Stil mit Active-Strip links */
|
||||
[data-flux-navlist-item][data-active="true"] {
|
||||
background: var(--color-hub-soft);
|
||||
color: var(--color-hub);
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
}
|
||||
[data-flux-navlist-item][data-active="true"]::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 6px;
|
||||
bottom: 6px;
|
||||
width: 2px;
|
||||
background: var(--color-hub);
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
/* Buttons — Hub-Primär */
|
||||
[data-flux-button][data-variant="primary"] {
|
||||
background: var(--color-hub);
|
||||
color: #ffffff;
|
||||
}
|
||||
[data-flux-button][data-variant="primary"]:hover {
|
||||
background: var(--color-hub-2);
|
||||
}
|
||||
|
||||
/* Sidebar-Section-Headings */
|
||||
[data-flux-navlist] [data-flux-navlist-group-heading] {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-ink-4);
|
||||
}
|
||||
|
||||
/* Cards bekommen Hub-Buchpapier statt Zinc */
|
||||
[data-flux-card] {
|
||||
background: var(--color-bg-card);
|
||||
border-color: var(--color-bg-rule);
|
||||
}
|
||||
```
|
||||
|
||||
> **Hinweis**: Die exakten `[data-flux-*]`-Attribute werden beim Bauen
|
||||
> per Dev-Tools verifiziert. Die hier gezeigten sind die wahrscheinlichsten
|
||||
> laut FluxUI-Doku.
|
||||
|
||||
### 2. `partials/head.blade.php` — Font wechseln
|
||||
|
||||
```diff
|
||||
- <link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600"
|
||||
+ <link href="https://fonts.bunny.net/css?family=inter-tight:400,500,600,700|jetbrains-mono:400,500,600|source-serif-4:400,500,600,700"
|
||||
rel="stylesheet" />
|
||||
```
|
||||
|
||||
### 3. Sidebar neu — `components/layouts/app/sidebar.blade.php`
|
||||
|
||||
Aufbau **strikt am Mockup** orientiert (s. `dev/frontend/tailwind_v3/User Dashboard presseportale.html`):
|
||||
|
||||
#### Sidebar-Aufbau (oben → unten)
|
||||
|
||||
1. **Brand-Block** (`px-5 pt-6 pb-5`):
|
||||
- `<x-web.brand-mark brand="pressekonto" :serif="false" />` (19 px, bold)
|
||||
- Eyebrow „Publisher · Hub" darunter
|
||||
- **Konto-Switcher-Button** mit Avatar (Initialen), Name, Firma — als
|
||||
`<flux:dropdown>`-Trigger mit Custom-Stil
|
||||
|
||||
2. **Navigation** (`px-3 flex-1`):
|
||||
- Section „Mein Bereich": Übersicht, Meine Pressemitteilungen (mit Counter-Badge),
|
||||
Firmen, Buchungen & Add-ons, Statistiken (disabled, „bald")
|
||||
- Section „Finanzen": Credits & Tarif (bald), Rechnungen, Zahlungsarten (bald)
|
||||
- Section „Konto": Profil, Sicherheit, API & Integrationen, Benachrichtigungen (bald)
|
||||
- Section „Administration" (nur für Admins/Editoren): Press-Releases, Companies, Users, Roles, etc.
|
||||
|
||||
3. **Testmodus-Block** (`px-4 pb-4`) — wenn Impersonation aktiv:
|
||||
- Hub-blaues Panel mit Bernstein-Eyebrow „Testmodus aktiv"
|
||||
- „Zurück zum Admin"-Button (weiß auf Hub-Blau)
|
||||
|
||||
4. **Resources-Block** (`px-3 pb-5 border-t`):
|
||||
- Optional: Tailwind CSS, Hero Icons, Flux UI, Repository
|
||||
- Im Live-Portal vermutlich weglassen oder durch eigene Hilfe-Links ersetzen
|
||||
|
||||
#### Komponenten-Strategie
|
||||
|
||||
Wo immer möglich **FluxUI-Komponenten** verwenden:
|
||||
- `<flux:sidebar>` als Wrapper
|
||||
- `<flux:navlist>` für Section-Gruppen
|
||||
- `<flux:navlist.item>` für Einträge, `data-active`-Markierung übernimmt CSS-Override
|
||||
- `<flux:dropdown>` für Konto-Switcher
|
||||
- `<flux:badge>` für Counter
|
||||
|
||||
Wo FluxUI nicht passt (z.B. Konto-Switcher-Header mit Avatar+Name+Firma+Chevron):
|
||||
**Custom Blade** in `<x-portal.account-switcher>` als wiederverwendbare Komponente.
|
||||
|
||||
### 4. `app.blade.php` — Customer-Banner Hub-Stil
|
||||
|
||||
Der Banner „User Backend" (für Customer-Rolle) wird visuell ans Hub-Design
|
||||
angeglichen — Hub-Soft-Hintergrund, Hub-Blau-Eyebrow, Firma-Switcher als
|
||||
Pille.
|
||||
|
||||
### 5. `class="dark"` entfernen
|
||||
|
||||
In allen Auth- und App-Layouts:
|
||||
|
||||
```diff
|
||||
- <html lang="..." class="dark">
|
||||
+ <html lang="...">
|
||||
```
|
||||
|
||||
FluxUI Appearance-Switcher in den Settings übernimmt die Steuerung.
|
||||
Dark-Mode-Werte landen in Phase 5 in `design-tokens.css`.
|
||||
|
||||
### 6. Build & Visual-Check
|
||||
|
||||
```bash
|
||||
npm run build:portal
|
||||
```
|
||||
|
||||
Öffnen und visuell prüfen:
|
||||
- `https://pressekonto.test/dashboard` (Admin-Dashboard)
|
||||
- `https://pressekonto.test/admin/me` (Customer-Dashboard)
|
||||
- `https://pressekonto.test/settings/profile`
|
||||
- `https://pressekonto.test/admin/companies`
|
||||
- `https://pressekonto.test/admin/press-releases`
|
||||
|
||||
Erwartung:
|
||||
- Sidebar wie Mockup
|
||||
- Topbar mit Breadcrumb + Aktionen
|
||||
- Inhalt unverändert, aber Tabellen/Cards/Buttons in Hub-Tonart
|
||||
- Keine kaputten Layouts
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Sidebar nutzt `<x-web.brand-mark brand="pressekonto" />` statt `x-app-logo`
|
||||
- [ ] Sidebar-Sections und Active-Item entsprechen visuell dem Mockup
|
||||
- [ ] Topbar hat Breadcrumb, Search, Notification, „Neue Mitteilung"-CTA
|
||||
- [ ] Font im Portal ist **Inter Tight** (sichtbar im DevTools)
|
||||
- [ ] FluxUI-Buttons (Primary) sind **Hub-Blau**, nicht mehr `#3ea3dc`
|
||||
- [ ] FluxUI-Tabellen sehen sauber aus mit Buchpapier-Hintergrund
|
||||
- [ ] `class="dark"` ist aus allen Layouts entfernt
|
||||
- [ ] Alle bestehenden Routen `/dashboard`, `/admin/*`, `/admin/me/*`,
|
||||
`/settings/*` rendern Status 200
|
||||
- [ ] Pint & vorhandene Tests bleiben grün
|
||||
- [ ] Page-Last-Vergleich: `portal.css`-Größe darf um max. 10 % wachsen
|
||||
|
||||
## Risiken & Fallstricke
|
||||
|
||||
- **FluxUI-Selektoren ändern sich**: `[data-flux-*]`-Attribute sind nicht
|
||||
öffentlich dokumentierte API, sondern Implementation-Detail. Bei
|
||||
FluxUI-Update kann ein Override brechen. Mitigation: Selektoren so
|
||||
spezifisch wie nötig, so generisch wie möglich; gut kommentieren.
|
||||
- **Zinc-Remapping kann Side-Effects haben**: Stellen, die hardcoded
|
||||
`zinc-700` für Text-Farben verwenden, werden plötzlich Hub-Blau.
|
||||
Mitigation: nach dem Build kritische Pages durchklicken; gegebenenfalls
|
||||
einzelne Stellen explizit auf `text-ink` umstellen.
|
||||
- **Tailwind v4 Custom-Properties**: Reihenfolge im `@theme`-Block ist
|
||||
wichtig — Tokens müssen vor Overrides definiert sein.
|
||||
- **Mobile Sidebar**: Das Mockup zeigt nur Desktop. `flux:sidebar` hat
|
||||
einen eingebauten Mobile-Toggle — der bleibt erhalten und wird visuell
|
||||
angeglichen.
|
||||
|
||||
## Was Phase 1 NICHT macht
|
||||
|
||||
- Dashboards (`admin.dashboard`, `customer.dashboard`) bekommen noch
|
||||
**kein** Stat-Card-Strip-Redesign — das ist Phase 2 + 3
|
||||
- Listen-Pages werden nicht überarbeitet — passt automatisch durch
|
||||
Token-Anpassung „gut genug" bis Phase 4
|
||||
- Dark Mode wird nicht aktiv ausgeliefert — Token-Werte werden vorbereitet,
|
||||
aber bleiben in Phase 5
|
||||
|
||||
## Review-Punkt
|
||||
|
||||
Nach Phase 1 wird Frank/Du visuell drüberschauen und entscheiden:
|
||||
- Welche Detail-Pages priorisiert werden (Phase 4)
|
||||
- Ob Customer-Dashboard (Phase 2) direkt danach kommt
|
||||
- Ob das Auth-Layout im Portal (`auth.split`, `auth.card`) entfernt werden kann
|
||||
162
dev/frontend/hub-flux/03-WEITERE-PHASEN.md
Normal file
162
dev/frontend/hub-flux/03-WEITERE-PHASEN.md
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
# Weitere Phasen — Outline
|
||||
|
||||
> Übersicht über Phasen 2–6. Diese werden detailliert ausgearbeitet,
|
||||
> sobald Phase 0+1 abgenommen sind. Jede Phase bekommt ein eigenes
|
||||
> `0X-PHASE-N-NAME.md`, wenn sie aktiv wird.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Customer-Dashboard auf Mockup-Stil
|
||||
|
||||
**Status**: ⚪ später · **Aufwand**: ~½ Tag · **Risiko**: niedrig
|
||||
|
||||
### Ziel
|
||||
`livewire/customer/dashboard.blade.php` matched das Mockup
|
||||
`User Dashboard presseportale.html` zu ≥ 90 %.
|
||||
|
||||
### Bausteine
|
||||
- **Page-Header** mit „Mein Dashboard" + Begrüßung + Firma-zugeordnet-Pille
|
||||
- **KPI-Reihe** (4 Stat-Cards mit linkem Farb-Strip)
|
||||
- Gesamt (Hub-Blau-Strip)
|
||||
- Veröffentlicht (Grün-Strip, `is-ok`)
|
||||
- In Prüfung (Bernstein-Strip, `is-warn`)
|
||||
- Entwürfe (Grau-Strip, `is-muted`)
|
||||
- **2-Spalten-Grid**:
|
||||
- Links: Empty-State / Liste „Meine letzten Pressemitteilungen"
|
||||
- Rechts: „Datenqualität" mit Progress-Bars (Profil-Vollständigkeit,
|
||||
Rechnungsadresse)
|
||||
- **Unten**:
|
||||
- Links: Firmen-Slots
|
||||
- Rechts: Brand-Bridge-Dark-Card (presseecho + businessportal24)
|
||||
|
||||
### Neue Blade-Components
|
||||
- `<x-portal.stat-card>` — Stat-Card mit Slot + Strip-Variante
|
||||
- `<x-portal.hint-card>` — Datenqualitäts-Hint mit Icon + Progress-Bar
|
||||
- `<x-portal.bridge-card>` — Dunkle Brand-Bridge-Card
|
||||
|
||||
→ Diese landen in `resources/views/components/portal/`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Admin-Dashboard konsistent
|
||||
|
||||
**Status**: ⚪ später · **Aufwand**: ~½ Tag · **Risiko**: niedrig
|
||||
|
||||
### Ziel
|
||||
`resources/views/admin/dashboard.blade.php` nutzt dasselbe Vokabular wie
|
||||
Customer-Dashboard.
|
||||
|
||||
### Heutiger Stand
|
||||
Reines Tailwind mit `zinc-*`-Klassen, **keine** FluxUI-Komponenten,
|
||||
visuell aus der Zeit gefallen.
|
||||
|
||||
### Schritte
|
||||
- KPI-Karten als `<x-portal.stat-card>`
|
||||
- Wenn Charts vorhanden: über `flux:chart` umsetzen (FluxUI Pro)
|
||||
- Recent-Activity-Liste in `flux:card` mit Hub-Akzenten
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Listen-/Detail-Pages durchgehen
|
||||
|
||||
**Status**: 🚧 iterativ · **Aufwand**: ~3–5 Tage gesamt · **Risiko**: niedrig
|
||||
|
||||
### Ziel
|
||||
Alle Volt-Pages im Admin- und Customer-Bereich nutzen denselben Hub-Stil.
|
||||
|
||||
### Vorgehen
|
||||
- Pro Page: 15–30 min
|
||||
- In Päckchen geteilt:
|
||||
- **4A** = Press-Releases Listen (Admin + Customer) — ✅ **abgeschlossen**
|
||||
siehe `07-PHASE-4A-PRESS-RELEASES-LISTEN.md`
|
||||
- **4B** = Press-Releases Detail/Show (Admin + Customer) — ✅ **abgeschlossen**
|
||||
siehe `08-PHASE-4B-PRESS-RELEASES-DETAIL.md`
|
||||
- **4C** = Press-Releases Forms (create/edit, Admin + Customer) — ✅ **abgeschlossen**
|
||||
siehe `09-PHASE-4C-PRESS-RELEASES-FORMS.md`
|
||||
- **4D** = Companies (`admin.companies.*`) — ⚪ pending
|
||||
- **4E** = Profile/Settings (`settings.*`, `me.profile`, `me.security`) — ⚪ pending
|
||||
- **4F** = Restliche Admin-Bereiche — ⚪ pending
|
||||
|
||||
### Was geändert wird
|
||||
- Page-Header-Struktur (Eyebrow + H1 + Subtitle + Aktions-Bar)
|
||||
- Filter-Bars werden Hub-konform gestylt
|
||||
- `flux:table`-Header bekommen Eyebrow-Optik
|
||||
- Form-Felder bleiben FluxUI, aber mit Hub-Tokens
|
||||
|
||||
### Was nicht geändert wird
|
||||
- Logik / Volt-Methoden
|
||||
- Datenbank / Models
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Dark Mode konsistent
|
||||
|
||||
**Status**: ⚪ später · **Aufwand**: ~½ Tag · **Risiko**: niedrig
|
||||
|
||||
### Ziel
|
||||
Dark Mode funktioniert sauber im Portal, **ohne doppelte UI-Pflege**.
|
||||
|
||||
### Quelle
|
||||
`dev/frontend/tailwind_v3/User Dashboard presseportale Dark.html` hat
|
||||
alle Dark-Tokens bereits vorgegeben.
|
||||
|
||||
### Schritte
|
||||
1. In `shared/design-tokens.css` Dark-Werte ergänzen:
|
||||
```css
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@theme {
|
||||
--color-bg: #0E1218;
|
||||
--color-bg-elev: #14181F;
|
||||
--color-hub: #5A78C2;
|
||||
--color-accent: #D9A560;
|
||||
/* … */
|
||||
}
|
||||
}
|
||||
.dark {
|
||||
/* gleiche Werte für expliziten Switch */
|
||||
}
|
||||
```
|
||||
2. `class="dark"` wird **nicht** mehr hardcoded gesetzt
|
||||
3. Flux Appearance-Switcher (`settings/appearance.blade.php`) steuert
|
||||
`.dark` auf `<html>`
|
||||
4. Hub-Frontend bleibt erstmal Light-Only (so wie heute)
|
||||
|
||||
### Erwartung
|
||||
- Settings → Appearance → „Dark" schaltet Portal um
|
||||
- Settings → Appearance → „System" folgt OS-Präferenz
|
||||
- Sidebar, Topbar, Dashboards, Listen, Forms — alles funktioniert
|
||||
- Hub-Landing und Hub-Auth bleiben Light
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — Auth-Konsolidierung (optional)
|
||||
|
||||
**Status**: ⚪ optional · **Aufwand**: 0–1 Tag · **Risiko**: mittel
|
||||
|
||||
### Frage
|
||||
Bleibt der Hub-Login (`auth/pressekonto`-Layout, Web-Build) so wie er ist?
|
||||
Oder konsolidieren wir auf den Portal-Build?
|
||||
|
||||
### Pro Konsolidierung
|
||||
- Ein Build weniger
|
||||
- Konsistente Komponenten-Sprache
|
||||
|
||||
### Pro Beibehaltung (heutiger Stand)
|
||||
- Hub-Atmosphäre der Auth-Seiten (Konzentrische Kreise, Hub-Grid) ist
|
||||
visuell sehr stark — würde verloren gehen
|
||||
- Hub-Auth ist **leichtgewichtig** (kein FluxUI im Bundle)
|
||||
- Vorlage `Login pressekonto A3 Tailwind.html` ist bewusst minimalistisch
|
||||
|
||||
### Entscheidung
|
||||
Vermutlich **beibehalten**. Aber: Die alten Auth-Layouts
|
||||
`auth/simple.blade.php`, `auth/split.blade.php`, `auth/card.blade.php` aus
|
||||
dem Starter-Kit können vermutlich gelöscht werden, sobald geprüft ist,
|
||||
dass keine Komponente sie noch verwendet.
|
||||
|
||||
### Schritte (wenn konsolidiert)
|
||||
- Hub-Auth-CSS in `portal.css` ziehen (mit `@source` für die Tokens)
|
||||
- `auth/pressekonto`-Layout auf Portal-Build umstellen
|
||||
- Hub-Auth-Klassen (`.auth-card`, `.auth-grid`, `.auth-btn-primary`)
|
||||
bleiben — laufen nur jetzt aus dem Portal-Bundle
|
||||
- `theme-pressekonto.css` aus dem Web-Build entfernen (oder behalten
|
||||
für die Landing)
|
||||
177
dev/frontend/hub-flux/04-PHASE-2-CUSTOMER-DASHBOARD.md
Normal file
177
dev/frontend/hub-flux/04-PHASE-2-CUSTOMER-DASHBOARD.md
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
# Phase 2 — Customer-Dashboard auf Mockup-Stil
|
||||
|
||||
> Detail-Plan analog zu `01-PHASE-0-TOKENS.md` und
|
||||
> `02-PHASE-1-PORTAL-SHELL.md`. Wird beim Abschluss auf Status `✅ abgeschlossen`
|
||||
> gesetzt; Lessons learned wandern in `PROGRESS.md`.
|
||||
|
||||
**Status**: ✅ abgeschlossen · **Aufwand**: ~½ Tag · **Risiko**: niedrig
|
||||
|
||||
---
|
||||
|
||||
## Ziel
|
||||
|
||||
`resources/views/livewire/customer/dashboard.blade.php` matched das Mockup
|
||||
`dev/frontend/tailwind_v3/User Dashboard presseportale.html` zu ≥ 90 %.
|
||||
Sichtbarer Wow-Moment für den Kunden, ohne neue Business-Logik.
|
||||
|
||||
## Was sich ändert
|
||||
|
||||
### Visuell
|
||||
|
||||
| Bereich | Heute | Mockup |
|
||||
| ----------------- | ------------------------------------------------ | --------------------------------------------------------------------- |
|
||||
| Page-Header | `<flux:card>` mit `<flux:heading>` „Willkommen“ | Hub-Badge + Eyebrow + großes H1 + Untertitel + rechts Status-Pille |
|
||||
| KPI-Reihe | 4 schmale `<flux:card>` mit `<flux:text>`-Zahl | 4 `stat-card`s mit Strip links, Mono-Zahl groß, Sub-Info, Sparkline |
|
||||
| Pressemitteilungen-Block | `<flux:card>` mit Liste / Empty-State | `panel` mit `section-eyebrow`, Empty-State mit Schritt-Karten 01/02/03 |
|
||||
| Datenqualität | Grid aus `<flux:badge>`-Karten | `panel` rechts mit 2 `hint-card`s (Progress-Bar + Action-Link) |
|
||||
| Firmen-Slot | `<flux:card>` mit Firmen-Liste | `panel` mit gestrichelten Empty-Slot-Karten + Hinweis-Box |
|
||||
| Brand-Bridge | (nicht vorhanden) | `panel-dark` rechts: presseecho + businessportal24 Status |
|
||||
| Footer | (nicht vorhanden) | Subtle-Footer mit Tastenkürzel / Changelog / Status / Support |
|
||||
|
||||
### Datenmodell
|
||||
|
||||
Heute liefert das Volt-`with()`:
|
||||
|
||||
- `stats.total | published | review | draft`
|
||||
- `qualityHints[]`
|
||||
- `recent` (PressRelease-Collection, max. 5)
|
||||
- `companies`
|
||||
|
||||
Mockup verlangt **zusätzlich**:
|
||||
|
||||
- `stats.deltaMonth` — Veränderung ggü. Vormonat (Total)
|
||||
- `profileCompleteness` — Prozentwert für Profil-Progress-Bar
|
||||
- `billingCompleteness` — Prozentwert für Rechnungsadresse
|
||||
- `bridgeStatus` — `['presseecho' => 'connected'|'pending', 'businessportal24' => …]`
|
||||
(vorerst `connected` fest verdrahtet, später aus echtem Sync-Status)
|
||||
|
||||
## Was NICHT geändert wird
|
||||
|
||||
- Logik / Volt-Methoden — Layout-only.
|
||||
- Statistik-Backend (`PressRelease::where(...)`) bleibt 1:1.
|
||||
- `qualityHints`-Logik bleibt — wird nur anders gerendert.
|
||||
- Topbar (eigene Phase, später).
|
||||
|
||||
---
|
||||
|
||||
## Implementierungs-Schritte
|
||||
|
||||
### 1. Shared Hub-Components-CSS
|
||||
|
||||
Klassen wandern in **`resources/css/shared/hub-components.css`** (neue Datei).
|
||||
Importiert in:
|
||||
|
||||
- `resources/css/portal.css` (nach `flux.css`)
|
||||
- `resources/css/web/shared-styles.css` (nach `design-tokens.css`)
|
||||
|
||||
Damit haben **Portal-Build und Web-Build** dieselbe Hub-Komponenten-Sprache —
|
||||
DRY für späteres Wiederverwenden (z. B. Admin-Dashboard in Phase 3).
|
||||
|
||||
Folgende Klassen kommen rein (alle mit `var(--color-*)`-Tokens, keine Hex-Literale):
|
||||
|
||||
- `.panel`, `.panel-warm`, `.panel-dark`, `.panel-head`
|
||||
- `.stat-card` + `.stat-strip` + `.stat-label` + `.stat-num`
|
||||
+ Varianten: `.is-primary`, `.is-ok`, `.is-warn`, `.is-muted`
|
||||
- `.hint-card` + `.hint-card .hint-ico`
|
||||
- `.badge` + `.badge.warn` + `.badge.ok` + `.badge.hub` + `.badge.dot`
|
||||
- `.bridge-row`, `.dot-pe`, `.dot-bp`
|
||||
|
||||
### 2. Blade-Components in `resources/views/components/portal/`
|
||||
|
||||
#### `stat-card.blade.php`
|
||||
|
||||
```blade
|
||||
<x-portal.stat-card variant="primary|ok|warn|muted" label="Gesamt" :value="$stats['total']">
|
||||
<x-slot:meta>2026</x-slot:meta> {{-- rechts oben --}}
|
||||
<x-slot:trend>0 ggü. Vormonat</x-slot:trend> {{-- unten --}}
|
||||
</x-portal.stat-card>
|
||||
```
|
||||
|
||||
Render: `<article class="stat-card is-{variant}">` + Strip + Label + Mono-Zahl
|
||||
+ Meta-Slot oben rechts + Trend-Slot unten. Sparkline (SVG) erstmal weggelassen
|
||||
— optionales Detail; lässt sich nachschieben, wenn Daten dafür da sind.
|
||||
|
||||
#### `hint-card.blade.php`
|
||||
|
||||
```blade
|
||||
<x-portal.hint-card :icon="$hint['icon']" :title="$hint['title']" :percent="60" :href="$hint['href']">
|
||||
{{ $hint['description'] }}
|
||||
<x-slot:action>Profil öffnen</x-slot:action>
|
||||
</x-portal.hint-card>
|
||||
```
|
||||
|
||||
Render: `.hint-card`-Grid mit Icon-Square + Progress-Bar + Text + Action-Link
|
||||
in `text-accent-deep`.
|
||||
|
||||
#### `bridge-card.blade.php` (optional, Phase 2 minimal)
|
||||
|
||||
Bleibt erstmal **inline** im Dashboard (sehr spezifisch, eine Stelle).
|
||||
Wird in Phase 3/4 extrahiert, wenn klar ist wie weit der Brand-Bridge-Pattern
|
||||
sich verbreitet.
|
||||
|
||||
### 3. `customer/dashboard.blade.php` umbauen
|
||||
|
||||
Layout-Skelett (alles im `<div class="space-y-8">`-Container):
|
||||
|
||||
1. **Page-Header** — `<header>` mit Grid `1fr auto`:
|
||||
- Links: Hub-Badge `User Backend` + Eyebrow + `<h1>Mein Dashboard</h1>`
|
||||
+ Subtitle (Name + Reichweiten-Hinweis)
|
||||
- Rechts: Status-Pille
|
||||
- Wenn `$selectedCompany`: Hub-Badge mit Firma-Name (grün/ok-Stil)
|
||||
- Wenn nicht: Warn-Pille „Keine Firma zugeordnet → zuordnen“
|
||||
2. **KPI-Reihe** — 4 `<x-portal.stat-card>` (Gesamt/Veröffentlicht/Prüfung/Entwürfe)
|
||||
3. **Zweispalten-Grid** (`2fr 1fr`):
|
||||
- Links: `<article class="panel">` mit Pressemitteilungen — Liste **oder**
|
||||
Empty-State (3 Schritt-Karten)
|
||||
- Rechts: `<article class="panel">` mit `<x-portal.hint-card>`s
|
||||
4. **Unteres Grid** (`2fr 1fr`):
|
||||
- Links: `<article class="panel">` Firmen — Liste **oder** Empty-Slot-Karten
|
||||
- Rechts: `<article class="panel-dark">` Brand-Bridge (inline)
|
||||
5. **Footer** — kleine Link-Reihe in `text-ink-3`
|
||||
|
||||
### 4. Volt `with()` ergänzen
|
||||
|
||||
- `stats.deltaMonth` via zweiter Query (Vormonats-Counts vs. Aktuell)
|
||||
- `profileCompleteness` als simpler Heuristik-Wert (firstname/lastname/phone/etc.)
|
||||
- `billingCompleteness` analog (Vorhanden = 100, sonst 0; oder Ist-Felder/Soll-Felder)
|
||||
- `bridgeStatus` vorerst hardcoded `['presseecho' => 'connected', 'businessportal24' => 'connected']`
|
||||
→ später aus echtem Sync-Service
|
||||
|
||||
### 5. Tests
|
||||
|
||||
`tests/Feature/Customer/DashboardTest.php` (oder bestehender Test, falls vorhanden):
|
||||
|
||||
- Rendert ohne Fehler bei eingeloggtem Customer
|
||||
- Stat-Zahlen werden korrekt ausgegeben
|
||||
- Empty-State wird angezeigt, wenn keine PRs existieren
|
||||
- Quality-Hints werden angezeigt, wenn `profile()` fehlt
|
||||
|
||||
---
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [x] Phase-2-Plan-Dokument geschrieben
|
||||
- [x] `shared/hub-components.css` existiert, importiert in beiden Builds
|
||||
- [x] `<x-portal.stat-card>` und `<x-portal.hint-card>` rendern wie Mockup
|
||||
- [x] `/admin/me` zeigt das neue Dashboard ohne Console-Errors
|
||||
- [x] Empty-State für Pressemitteilungen ist sichtbar, wenn keine vorhanden
|
||||
- [x] Quality-Hints rendern mit Progress-Bar
|
||||
- [x] Brand-Bridge-Dark-Card unten rechts zeigt presseecho + businessportal24
|
||||
- [x] Neuer Smoke-Test `tests/Feature/Customer/DashboardTest.php` mit 5 Cases
|
||||
(Core-Sections, Empty-State, PR-Liste, Profil-Hint, vollständiges Profil
|
||||
blendet Hints aus). Cross-Check: alle 18 verwandten Tests bleiben grün.
|
||||
- [x] Pint clean
|
||||
- [x] `PROGRESS.md`-Eintrag
|
||||
|
||||
---
|
||||
|
||||
## Risiken & Mitigation
|
||||
|
||||
- **FluxUI-Klassen vs. Custom-CSS-Kollisionen** — wir mischen `<flux:badge>`
|
||||
weiterhin **nicht** im Dashboard (für Status-Pillen nehmen wir
|
||||
`.badge.hub|.warn|.ok|.dot`). Auf den Detail-Seiten (Phase 4) bleibt
|
||||
FluxUI dominant.
|
||||
- **Sparklines weggelassen** — minimaler Stilverlust, wird in Phase 4 mit
|
||||
echten Trend-Daten nachgereicht. Mockup-Match weiterhin ≥ 90 %.
|
||||
- **`stats.deltaMonth`-Performance** — zweite Query auf gleicher Tabelle;
|
||||
bei wachsendem Datensatz später cachen. Heute irrelevant.
|
||||
159
dev/frontend/hub-flux/05-PHASE-5-DARK-MODE.md
Normal file
159
dev/frontend/hub-flux/05-PHASE-5-DARK-MODE.md
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
# Phase 5 — Dark Mode konsistent + Appearance-Switcher im User-Menü
|
||||
|
||||
> **Vorzugsphase**: Eigentlich Phase 5, vorgezogen vor Phase 3/4 nach
|
||||
> User-Wunsch. Grund: Der FluxUI-Appearance-Switcher hat in Phase 1 den
|
||||
> Dark-Mode-Bug ausgelöst (`#6d8ad3` statt Hub-Blau); statt Symptom-Fix
|
||||
> wollen wir den vollen Dark Mode jetzt richtig.
|
||||
|
||||
**Status**: ✅ abgeschlossen · **Aufwand**: ~½ Tag · **Risiko**: niedrig
|
||||
|
||||
---
|
||||
|
||||
## Ziele
|
||||
|
||||
1. Dark Mode funktioniert sauber auf allen Portal-Seiten (Customer-Dashboard,
|
||||
Sidebar, FluxUI-Komponenten, Hub-Components).
|
||||
2. Switcher (Light / Dark / System) **direkt im User-Menü** verfügbar — kein
|
||||
Umweg über `/settings/appearance` mehr nötig.
|
||||
3. Hub-Frontend (Landing + Auth) bleibt **bewusst Light-Only** — Hub-Atmosphäre
|
||||
ist Buchpapier.
|
||||
4. Der Notfall-Hack aus Phase 1 (`portal.css: .dark { --color-accent: var(--color-hub) }`)
|
||||
wird durch das echte Dark-Token-Mapping ersetzt.
|
||||
|
||||
## Quelle der Wahrheit
|
||||
|
||||
`dev/frontend/tailwind_v3/User Dashboard presseportale Dark.html` —
|
||||
liefert alle Dark-Werte. Wir spiegeln die Token-Namen aus dem Light-Mode 1:1.
|
||||
|
||||
---
|
||||
|
||||
## Was sich ändert
|
||||
|
||||
### 1. `shared/design-tokens.css`
|
||||
|
||||
Der seit Phase 0 vorbereitete `.dark`-Block wird **aktiviert** und um die seit
|
||||
Phase 2 nachgezogenen Tokens (`--color-bg-rule-2`, `--color-gain-deep`) ergänzt.
|
||||
|
||||
Wichtige Umwidmungen im Dark Mode (gleiche Namen, andere Werte):
|
||||
|
||||
| Token | Light | Dark | Bedeutung |
|
||||
| ----------------------- | ----------- | ----------- | ------------------------------------------ |
|
||||
| `--color-bg` | `#f6f4ef` | `#0e1218` | Page-Background |
|
||||
| `--color-bg-elev` | `#fbfaf6` | `#14181f` | Sidebar / leicht hervorgehoben |
|
||||
| `--color-bg-card` | `#ffffff` | `#181d27` | Card-Body |
|
||||
| `--color-bg-rule` | `#e2ddd0` | `#2a3142` | Trennlinien |
|
||||
| `--color-bg-rule-2` | `#ede7d7` | `#232838` | Progress-Track |
|
||||
| `--color-hub` | `#1a2540` | `#5a78c2` | Primary-Akzent (heller im Dark) |
|
||||
| `--color-hub-soft` | `#e5e9f1` | `#1f2a47` | Hint-Hintergrund |
|
||||
| `--color-accent` | `#b07a3a` | `#d9a560` | Bernstein-Akzent (heller im Dark) |
|
||||
| `--color-accent-warm` | `#b07a3a` | `#b07a3a` | **bleibt gleich** — für Border-Akzente |
|
||||
| `--color-accent-deep` | `#8a5e27` | `#b07a3a` | Action-Link-Color (heller im Dark) |
|
||||
| `--color-accent-soft` | `#f1e6d3` | `#2a2418` | Hint-Icon-BG |
|
||||
| `--color-ink` | `#1a1f1c` | `#ece9e0` | Text-Primary |
|
||||
| `--color-ink-2` | `#3a413d` | `#c9c5b8` | Text-Secondary |
|
||||
| `--color-ok / -soft` | `#2e8540 / #e2f1e5` | `#4dc076 / #1a2d22` | Status grün |
|
||||
|
||||
`--color-accent-warm` als **konstanter** Bernstein-Token (gleicher Wert in
|
||||
beiden Modi) ist ein bewusster Trick: Im Portal mapt `--color-accent` auf
|
||||
`var(--color-hub)` (Primary-Akzent), aber Bernstein-Borders (Hint-Card,
|
||||
Schritt-Karten-Eyebrow) brauchen weiterhin den ungeänderten Bernstein-Wert.
|
||||
`--color-accent-warm` ist der Token dafür.
|
||||
|
||||
### 2. `portal.css`
|
||||
|
||||
- Den `.dark { --color-accent: var(--color-hub); … }`-Notfall-Block aus
|
||||
Phase 1 entfernen — er ist mit echtem Dark-Mapping überflüssig.
|
||||
- Stattdessen einen kurzen Kommentar setzen, dass das Token-Mapping aus
|
||||
`design-tokens.css` greift.
|
||||
- Primary-Button-Hover-Override: heute hartcodiert auf `--color-hub-2`
|
||||
(`#243152`). Im Dark Mode ist `--color-hub-2` = `#6d8ad3` (heller),
|
||||
was richtig ist (Hover = noch heller als Default-Button). Override bleibt
|
||||
via Variable korrekt, kein Eingriff nötig.
|
||||
- Button-Shadows: `rgba(26, 37, 64, …)` ist Light-Mode-spezifisch. Im Dark
|
||||
Mode wirkt der bläuliche Shadow auf dunklem Card-BG falsch. Lösung:
|
||||
per `@media (prefers-color-scheme: dark)` ODER `.dark`-Selektor die
|
||||
Shadow-Farben auf transparenten Schwarz tönen.
|
||||
|
||||
### 3. `shared/hub-components.css`
|
||||
|
||||
- Bernstein-Stellen umstellen: `var(--color-accent)` →
|
||||
`var(--color-accent-warm)` für **Hint-Card-Border-Left** und
|
||||
**Progress-Bar-Fill** (sonst wird's im Portal Hub-Blau gemapt).
|
||||
- Wegen Tokens-Architektur (`hub-soft` etc. werden im Dark Mode dunkler)
|
||||
funktioniert der Großteil **automatisch** — keine `.dark`-Overrides nötig.
|
||||
- Eine Ausnahme: `.panel-dark` und die Brand-Bridge-Boxes — im Light Mode
|
||||
ist `panel-dark` Hub-Blau (#1a2540). Im Dark Mode bleibt das auch
|
||||
`var(--color-hub)` — und da ist `--color-hub` heller (#5a78c2). Wir
|
||||
brauchen also einen **konstanten dunklen Token** für `panel-dark` — sonst
|
||||
wird die „dunkle“ Bridge-Card im Dark Mode plötzlich hellblau.
|
||||
→ Neuer Token `--color-panel-dark` (immer #0f1729, in beiden Modi)
|
||||
oder direkt `var(--color-topbar-deep)` (existiert bereits).
|
||||
|
||||
### 4. User-Menü-Switcher
|
||||
|
||||
Beide Dropdowns (Desktop in `sidebar.blade.php`, Mobile in derselben Datei)
|
||||
bekommen vor dem Logout einen Block:
|
||||
|
||||
```blade
|
||||
<flux:menu.separator />
|
||||
<div class="px-2 py-1.5">
|
||||
<flux:radio.group x-data variant="segmented" x-model="$flux.appearance" size="sm">
|
||||
<flux:radio value="light" icon="sun" />
|
||||
<flux:radio value="dark" icon="moon" />
|
||||
<flux:radio value="system" icon="computer-desktop" />
|
||||
</flux:radio.group>
|
||||
</div>
|
||||
```
|
||||
|
||||
Das ist exakt das Pattern aus `livewire/settings/appearance.blade.php`, nur
|
||||
kompakt mit Icons-only. `$flux.appearance` ist FluxUI's Magic-Object,
|
||||
LocalStorage-persistent, von `@fluxAppearance` injiziert.
|
||||
|
||||
### 5. Hub-Auth bleibt Light
|
||||
|
||||
`auth/pressekonto.blade.php` lädt **kein** `@fluxAppearance` und **kein**
|
||||
`partials/head` — die Hub-Auth-Pipeline ist eigenständig. Damit wird `class="dark"`
|
||||
dort nie gesetzt, selbst wenn das LocalStorage `dark` enthält. Hub-Auth bleibt
|
||||
also automatisch Light. **Kein Eingriff nötig.**
|
||||
|
||||
---
|
||||
|
||||
## Implementierungs-Schritte (Reihenfolge)
|
||||
|
||||
1. `design-tokens.css`: Dark-Block aktivieren, `--color-accent-warm` definieren,
|
||||
`--color-panel-dark` einführen (oder Re-Use `--color-topbar-deep`).
|
||||
2. `portal.css`: Hack rausnehmen, Button-Shadow für Dark Mode tönen.
|
||||
3. `hub-components.css`: Bernstein-Stellen auf `--color-accent-warm`,
|
||||
`.panel-dark` auf `--color-panel-dark`.
|
||||
4. `sidebar.blade.php`: Switcher in beide Dropdowns einbauen.
|
||||
5. Build + Smoke-Test im Browser.
|
||||
6. Bestehende Tests laufen lassen (sollten alle weiter grün sein, weil
|
||||
keine Test-Assertion farbbezogen ist).
|
||||
7. Pint + PROGRESS + Plan-Status.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [x] Plan-Dokument geschrieben
|
||||
- [x] Switcher Light/Dark/System im User-Menü (Desktop + Mobile) sichtbar,
|
||||
Änderung wirkt sofort + persistent (LocalStorage)
|
||||
- [x] Dark Mode: Customer-Dashboard rendert mit allen Sektionen korrekt
|
||||
(Tokens schalten um, kein hellblauer Akzent-Bug)
|
||||
- [x] Brand-Bridge-Karte bleibt im Dark Mode dunkel (`--color-panel-dark-2`
|
||||
konstant in beiden Modi)
|
||||
- [x] Hub-Auth bleibt im Dark Mode Light (lädt kein `@fluxAppearance`)
|
||||
- [x] Tests grün (18/18)
|
||||
- [x] Pint clean
|
||||
- [x] PROGRESS-Eintrag
|
||||
|
||||
## Risiken & Mitigation
|
||||
|
||||
- **FluxUI-Default-Komponenten im Dark Mode** (z.B. Modals, Dropdowns):
|
||||
unsere Zinc→Hub-Mapping in `portal.css` bridgt die Skala. Im Dark Mode
|
||||
müssen die Zinc-Werte auch umgekippt werden — passiert automatisch über
|
||||
die Token-Vars, weil wir `var(--color-bg-*)` nutzen. Restrisiko: einzelne
|
||||
FluxUI-Komponenten könnten harte Tailwind-Klassen wie `bg-white` haben,
|
||||
die im Dark unverändert weiß bleiben. Smoke-Test deckt das auf.
|
||||
- **Hub-Components-Schatten** sind in Hub-Blau-Alpha definiert
|
||||
(`rgba(26, 37, 64, …)`). Im Dark Mode auf dunklem Bg wirken sie evtl.
|
||||
„falsch warm“. Akzeptabel im ersten Wurf; Feintuning per `prefers-color-scheme`
|
||||
Media-Query falls visuell stört.
|
||||
55
dev/frontend/hub-flux/06-PHASE-3-ADMIN-DASHBOARD.md
Normal file
55
dev/frontend/hub-flux/06-PHASE-3-ADMIN-DASHBOARD.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Phase 3 — Admin-Dashboard im Hub-Vokabular
|
||||
|
||||
**Status**: ✅ abgeschlossen · **Aufwand**: ~¼ Tag · **Risiko**: niedrig
|
||||
|
||||
---
|
||||
|
||||
## Ziel
|
||||
|
||||
`resources/views/admin/dashboard.blade.php` nutzt dieselbe Designsprache wie
|
||||
das Customer-Dashboard (Phase 2). Keine neuen Komponenten, keine neue Logik
|
||||
— nur Vokabular-Umbau auf die DRY-Schicht.
|
||||
|
||||
## Was sich ändert
|
||||
|
||||
### Layout
|
||||
|
||||
- Page-Header: Hub-Badge „Admin Backend" + Eyebrow + großes H1
|
||||
„Admin Dashboard" + Subtitle. Rechts: Status-Pille „Alle Systeme operational"
|
||||
(ok-Style mit Dot).
|
||||
- KPI-Reihe: weiter **5 Stat-Cards** (wie heute), aber als
|
||||
`<x-portal.stat-card>` mit Strip-Variante:
|
||||
- Pressemitteilungen → `primary` (mit Sub: `X pub · Y prüf · Z entwurf`)
|
||||
- In Prüfung → `warn` (eigene KPI, war vorher in der PM-Card versteckt)
|
||||
- Firmen → `muted`
|
||||
- Kontakte → `muted`
|
||||
- Benutzer → `muted`
|
||||
- 2-Spalten-Grid (`2fr 1fr`) — wie heute:
|
||||
- Links: Panel „Letzte Pressemitteilungen" (Liste + Status-Badges
|
||||
in `.badge.ok|warn|err|hub`).
|
||||
- Rechts: Panel „Zur Prüfung" mit warn-Pille (Count) + Liste +
|
||||
„+ N weitere"-Link.
|
||||
- Bottom-Reihe (`1fr 2fr`) — **neu**:
|
||||
- Links: `panel-warm` Newsletter-Block (Mono-Zahl + Subline).
|
||||
- Rechts: `panel` Quick-Actions mit Section-Eyebrow + Schnellzugriff
|
||||
auf Invoices/Payments/Coupons/Presets.
|
||||
- Footer: subtle Link-Reihe analog Customer-Dashboard.
|
||||
|
||||
### Was NICHT geändert wird
|
||||
|
||||
- Controller-Logik / Datenform.
|
||||
- Newsletter-Count bleibt erhalten, wandert nur in einen eigenen Block.
|
||||
- Bestehende Tests (`DashboardTest`) bleiben grün — alle geprüften
|
||||
Strings (`3`, `1 pub`, `1 prüf`, `1 entwurf`, `Review Dashboard PM`)
|
||||
bleiben im Output.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [x] Plan geschrieben
|
||||
- [x] Admin-Dashboard verwendet `<x-portal.stat-card>`, `.panel`,
|
||||
`.section-eyebrow`, `.badge`
|
||||
- [x] Customer-Dashboard und Admin-Dashboard sind visuell aus einem Guss
|
||||
- [x] Dark Mode greift automatisch (alle Tokens)
|
||||
- [x] `DashboardTest` bleibt grün ohne Anpassung (alle 3 Cases +
|
||||
Wortlaut „1 pub / 1 prüf / 1 entwurf / Review Dashboard PM")
|
||||
- [x] Pint clean, PROGRESS-Eintrag
|
||||
58
dev/frontend/hub-flux/07-PHASE-4A-PRESS-RELEASES-LISTEN.md
Normal file
58
dev/frontend/hub-flux/07-PHASE-4A-PRESS-RELEASES-LISTEN.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# Phase 4A — Press-Releases Listen-Pages
|
||||
|
||||
> Erstes Päckchen aus Phase 4 (Listen/Detail-Pages iterativ). Wegen
|
||||
> Umfang teilen wir Phase 4 in Päckchen — A = Listen, B = Detail/Show,
|
||||
> C = Forms (create/edit), D = Companies, E = Settings/Profile, F = Rest.
|
||||
|
||||
**Status**: ✅ abgeschlossen · **Aufwand**: ~¼ Tag · **Risiko**: niedrig
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
In diesem Päckchen NUR:
|
||||
|
||||
- `resources/views/livewire/admin/press-releases/index.blade.php`
|
||||
- `resources/views/livewire/customer/press-releases/index.blade.php`
|
||||
|
||||
NICHT in diesem Päckchen:
|
||||
- `show.blade.php`, `create.blade.php`, `edit.blade.php` (Päckchen B/C)
|
||||
- Companies, Settings, Profile (Päckchen D/E)
|
||||
|
||||
## Ziel
|
||||
|
||||
Beide Listen nutzen das Hub-Vokabular:
|
||||
|
||||
- **Page-Header** wie im Dashboard (Hub-Badge + Eyebrow + H1 + Subtitle,
|
||||
CTA rechts).
|
||||
- **KPI-Reihe** (nur Admin) als `<x-portal.stat-card>` — exakt
|
||||
wie auf dem Admin-Dashboard (DRY: vier Cards mit primary/ok/warn/muted).
|
||||
- **Filter-Bar** als `.panel` mit `.panel-head` „Filter & Suche".
|
||||
- **Tabelle** in `.panel` (kein FluxUI-Card-Wrapper mehr). FluxUI-Table
|
||||
bleibt — wir bridgen über Zinc-→Hub-Mapping.
|
||||
- **Status-Badges** auf eigene `.badge.ok|warn|err|hub`-Pillen
|
||||
(statt `<flux:badge color="…">`) — visuelle Konsistenz zum Dashboard.
|
||||
- **Empty-State** in Hub-Stil mit Icon-Box.
|
||||
|
||||
## Was NICHT angefasst wird
|
||||
|
||||
- **Confirm-Modals** für Publish/Reject/Archive — Tests assertieren
|
||||
Wortlaut („Pressemitteilung veröffentlichen?" etc.).
|
||||
- **Volt-Logik** (Sort, Filter, Methods) — Layout-only.
|
||||
- **FluxUI-Form-Inputs** (Search, Select, Combobox) — bleiben, weil
|
||||
Hub kein eigenes Combobox-Pendant hat.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [x] Plan
|
||||
- [x] Admin-Liste: Page-Header + 4 Stat-Cards + Filter-Panel + Tabelle-Panel + Hub-Badges
|
||||
- [x] Customer-Liste: Page-Header + Filter-Panel + Tabelle-Panel + Hub-Badges + Empty-State
|
||||
- [x] Visuell aus einem Guss mit Dashboard
|
||||
- [x] `PressReleaseWorkflowTest` + `AdminPressReleaseActionsTest` bleiben grün
|
||||
- [x] Build + Pint + PROGRESS
|
||||
|
||||
## Bonus-Fix
|
||||
|
||||
- `customer/dashboard.blade.php` Subtitle ergänzt um „Übersicht für {Firma}",
|
||||
wenn `$selectedCompany` gesetzt ist. Korrigiert eine Phase-2-Regression
|
||||
bei `CustomerCompanyContextTest > customer dashboard is filtered by …`.
|
||||
56
dev/frontend/hub-flux/08-PHASE-4B-PRESS-RELEASES-DETAIL.md
Normal file
56
dev/frontend/hub-flux/08-PHASE-4B-PRESS-RELEASES-DETAIL.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Phase 4B — Press-Releases Detail/Show-Pages
|
||||
|
||||
> Zweites Päckchen aus Phase 4. Folgt auf 4A (Listen).
|
||||
|
||||
**Status**: ✅ abgeschlossen · **Aufwand**: ~⅓ Tag · **Risiko**: niedrig
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
- `resources/views/livewire/admin/press-releases/show.blade.php`
|
||||
- `resources/views/livewire/customer/press-releases/show.blade.php`
|
||||
|
||||
NICHT in diesem Päckchen:
|
||||
- `create.blade.php`, `edit.blade.php` (Päckchen 4C — Forms)
|
||||
- Companies, Settings, Profile (Päckchen 4D/4E)
|
||||
|
||||
## Ziel
|
||||
|
||||
Beide Detail-Pages im Hub-Vokabular:
|
||||
|
||||
- **Page-Header** wie auf den Listen (Hub-Badge + Eyebrow + H1 +
|
||||
Subtitle), Status-Pill direkt unter dem Eyebrow oder im Header-Meta,
|
||||
Aktions-Buttons (Bearbeiten / Zurück / Vorschau-Link) rechts.
|
||||
- **Status-Workflow-Aktionsbar** als `.panel` mit klarer Optik
|
||||
je nach Status (review = warn, published = ok, draft = neutral).
|
||||
- **Content-Hauptbereich** (PM-Text) als `.panel` (kein FluxUI-Card-Wrapper).
|
||||
- **Sidebar / Side-Cards** als kleine `.panel` mit `panel-head`.
|
||||
- **Status-Verlauf-Timeline** als `.panel` mit Hub-Badges
|
||||
(`.badge.ok|warn|err|hub`).
|
||||
- **Rejection-Hinweis** (Customer) als Hub-Style-Error-Panel mit
|
||||
Linker Akzent-Border (statt `<flux:callout>`).
|
||||
- **Share-Link-Erfolgsbox** (Customer) als Hub-Style-Success-Block.
|
||||
- **Pressekontakte-Liste** (Customer) als Hub-Items in `.panel`.
|
||||
|
||||
## Was explizit NICHT angefasst wird
|
||||
|
||||
- **Confirm-Modals** (Publish / Reject / Archive) — Tests in
|
||||
`AdminPressReleaseActionsTest` assertieren Wortlaute.
|
||||
- **Wortlaute** `Werbliche Sprache wurde markiert.` und
|
||||
`Erneut einreichen` aus `PressReleaseWorkflowTest`.
|
||||
- **Volt-Logik** (publish/reject/archive/submitForReview/
|
||||
generateShareLink/with) — Layout-only.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [x] Plan
|
||||
- [x] Admin-Show: Page-Header + Status-Workflow-Bar + Text-Panel +
|
||||
Sidebar-Panels + Timeline + Hub-Badges. Modals unverändert.
|
||||
- [x] Customer-Show: Page-Header + Status-Workflow-Bar +
|
||||
Rejection-Panel + Share-Erfolgsblock + Contacts-Panel +
|
||||
Verlauf-Panel + Text-Panel + Hub-Badges. „Erneut einreichen" +
|
||||
„Werbliche Sprache wurde markiert." Wortlaute bleiben.
|
||||
- [x] `PressReleaseWorkflowTest` + `AdminPressReleaseActionsTest`
|
||||
bleiben grün (16 passed, 52 assertions).
|
||||
- [x] Build + Pint + PROGRESS.
|
||||
69
dev/frontend/hub-flux/09-PHASE-4C-PRESS-RELEASES-FORMS.md
Normal file
69
dev/frontend/hub-flux/09-PHASE-4C-PRESS-RELEASES-FORMS.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# Phase 4C — Press-Releases Forms (create / edit)
|
||||
|
||||
> Drittes Päckchen aus Phase 4. Folgt auf 4A (Listen) und 4B (Show).
|
||||
|
||||
**Status**: ✅ abgeschlossen · **Aufwand**: ~⅓–½ Tag · **Risiko**: niedrig
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
- `resources/views/livewire/admin/press-releases/create.blade.php`
|
||||
- `resources/views/livewire/admin/press-releases/edit.blade.php`
|
||||
- `resources/views/livewire/customer/press-releases/create.blade.php`
|
||||
- `resources/views/livewire/customer/press-releases/edit.blade.php`
|
||||
|
||||
NICHT in diesem Päckchen:
|
||||
- Companies (Päckchen 4D)
|
||||
- Settings / Profile (Päckchen 4E)
|
||||
- Restliche Admin-Bereiche (Päckchen 4F)
|
||||
|
||||
## Ziel
|
||||
|
||||
Alle vier Forms im Hub-Vokabular:
|
||||
|
||||
- **Page-Header** wie auf Listen und Detail-Pages: Hub-Badge +
|
||||
Eyebrow + H1 + Subtitle. Bei Edit zusätzlich Status-Pille im
|
||||
Header-Meta. Aktions-Buttons rechts.
|
||||
- **Form-Sektionen** als `.panel` mit `.panel-head` und
|
||||
`section-eyebrow` ("Inhalt", "SEO & Links", "Metadaten",
|
||||
"Status-Aktionen", "Aktionen").
|
||||
- **Form-Felder** bleiben FluxUI (`<flux:field>`, `<flux:label>`,
|
||||
`<flux:input>`, `<flux:textarea>`, `<flux:select>` inkl.
|
||||
Combobox-Variant, `<flux:checkbox>`, `<flux:error>`) — das
|
||||
Token-Bridging aus Phase 1 zieht. Required-Marker werden auf
|
||||
Hub-Error-Token umgestellt.
|
||||
- **Action-Buttons** ("Speichern", "Zur Prüfung einreichen",
|
||||
"Als Entwurf speichern", "Status wechseln") in einem `.panel`
|
||||
mit `.panel-head` "Aktionen".
|
||||
- **Flash-Boxen** auf Hub-Token-Pillen.
|
||||
|
||||
## Was explizit NICHT angefasst wird
|
||||
|
||||
- **Volt-Logik** (`save`, `submitForReview`, `publish`, `reject`,
|
||||
`backToDraft`, `archive`, `changeStatus`, `deletePressRelease`,
|
||||
`mount`, `with`, `selectedCompany`) — Layout-only.
|
||||
- **Confirm-Modals** der Admin-Edit-Page (`confirm-status-change`,
|
||||
`confirm-delete-press-release`) — Tests assertieren Wortlaute.
|
||||
- **Wortlaute** aus `AdminPressReleaseActionsTest`:
|
||||
`Neuer Status`, `Status wechseln`, `Status wirklich wechseln?`,
|
||||
`Pressemitteilung löschen?`, `Status wurde auf`.
|
||||
- **Image-Manager-Komponente** (`<livewire:components.press-release-images-manager>`).
|
||||
- **Wortlaute** für Customer-Create-Page: `Beta GmbH` (Firma
|
||||
aus Faktur via `companyId`) — Combobox/Select bleibt funktional.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [x] Plan
|
||||
- [x] Admin-Edit: Page-Header + 5 Panels + Hub-Status-Badge,
|
||||
beide Modals unverändert, alle Wortlaut-Assertions grün.
|
||||
- [x] Admin-Create: Page-Header + 3 Panels (Inhalt, SEO, Metadaten)
|
||||
+ Aktions-Panel.
|
||||
- [x] Customer-Create: Page-Header + 2 Panels (Inhalt, Metadaten)
|
||||
+ Aktions-Panel.
|
||||
- [x] Customer-Edit: Page-Header + 2 Panels (Inhalt mit Image-Manager,
|
||||
Metadaten) + Aktions-Panel.
|
||||
- [x] `AdminPressReleaseActionsTest`,
|
||||
`CustomerCompanyContextTest > customer press releases create …`,
|
||||
`CustomerProfileSecurityTest` bleiben grün (72 Tests, 310 assertions).
|
||||
- [x] Build + Pint + PROGRESS.
|
||||
812
dev/frontend/hub-flux/PROGRESS.md
Normal file
812
dev/frontend/hub-flux/PROGRESS.md
Normal file
|
|
@ -0,0 +1,812 @@
|
|||
# Fortschritts-Log — Hub × FluxUI
|
||||
|
||||
> Tagebuch der Umsetzung. Neue Einträge **oben** anfügen.
|
||||
> Format pro Eintrag: Datum · Phase · Was wurde gemacht · Wer · Notizen
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-19 · Phase 4C · Press-Releases Forms (create / edit)
|
||||
|
||||
- **Anlass**: User-Freigabe „ja" nach 4B. Drittes Päckchen aus
|
||||
Phase 4 — Forms (create + edit) für Admin und Customer.
|
||||
|
||||
- **Plan-Dokument**: `09-PHASE-4C-PRESS-RELEASES-FORMS.md`.
|
||||
|
||||
- **Was umgebaut wurde (alle 4 Forms)**:
|
||||
- **Page-Header** wie auf Listen und Detail: Hub-Badge
|
||||
(„Admin Backend" / „User Backend") + Eyebrow + großes H1 +
|
||||
Subtitle. Bei Edit zusätzlich Status-Pille im Header-Meta
|
||||
(`@class([])`-Syntax mit `ok|warn|err|hub`) und „ID"-Pill.
|
||||
Aktions-Buttons rechts (Zurück).
|
||||
- **Form-Sektionen** als `.panel` mit `.panel-head` und
|
||||
`section-eyebrow`: „Inhalt", „SEO & Links", „Metadaten",
|
||||
„Status-Aktionen" (Admin-Edit), „Aktionen". Innenraum
|
||||
immer `p-5 space-y-4`.
|
||||
- **Form-Felder** bleiben FluxUI (`<flux:field>`,
|
||||
`<flux:label>`, `<flux:input>`, `<flux:textarea>`,
|
||||
`<flux:select>` inkl. Combobox-Variant, `<flux:checkbox>`,
|
||||
`<flux:error>`) — das Token-Bridging aus Phase 1 zieht.
|
||||
- **Required-Marker** von `text-red-500` auf Token-Farbe
|
||||
(`text-[color:var(--color-err)]`) umgestellt.
|
||||
- **Save-/Submit-Buttons** in eigenem `.panel` mit Header
|
||||
„Aktionen" statt „nackten" Buttons in der Sidebar.
|
||||
- **Flash-Boxen** (Success/Error) auf Hub-Token-Pillen.
|
||||
|
||||
- **Admin-Edit-spezifisch**: 5 Sidebar-Panels (Status-Aktionen +
|
||||
Metadaten + Aktionen + Modals). „Status-Aktionen"-Panel zeigt
|
||||
rechts im Header die aktuelle Status-Pille — visuelle Doppelung
|
||||
mit dem Page-Header-Status, aber im Kontext der Sidebar
|
||||
sinnvoll als Vergleichs-Anker für die „Neuer Status"-Auswahl.
|
||||
|
||||
- **Was explizit NICHT angefasst wurde**:
|
||||
- **Volt-Logik** (alle `save`/`publish`/`reject`/`backToDraft`/
|
||||
`archive`/`changeStatus`/`deletePressRelease`/`mount`/`with`/
|
||||
`selectedCompany`-Methoden) — Layout-only.
|
||||
- **Confirm-Modals** der Admin-Edit-Page
|
||||
(`confirm-status-change`, `confirm-delete-press-release`) —
|
||||
Tests in `AdminPressReleaseActionsTest` assertieren Wortlaute.
|
||||
- **Wortlaute**: „Neuer Status", „Status wechseln",
|
||||
„Status wirklich wechseln?", „Pressemitteilung löschen?",
|
||||
„Status wurde auf" — alle erhalten.
|
||||
- **`<livewire:components.press-release-images-manager>`** —
|
||||
eigene Komponente, kommt im jeweiligen eigenen Päckchen dran.
|
||||
|
||||
- **Build/Test**:
|
||||
- `npm run build:portal` → ok (`portal-D0cNdOWP.css` 418.42 kB).
|
||||
- Linter clean (alle 4 Dateien).
|
||||
- `php artisan test --compact --filter='PressRelease|CustomerCompanyContext|CustomerProfileSecurity|PanelConsolidation'`
|
||||
→ **72 passed (310 assertions)**.
|
||||
- Volle Suite: 230 passed, 3 skipped, 1 pre-existing failure
|
||||
(`ApiDocumentationTest`).
|
||||
- Pint `--dirty --format agent` → passed.
|
||||
|
||||
- **Offene Fragen**: Keine.
|
||||
|
||||
- **Nächster Schritt**: Phase 4 für Press-Releases ist mit 4A/4B/4C
|
||||
komplett abgeschlossen. Nächstes Päckchen je nach Sichtbarkeit:
|
||||
**4D = Companies** (`admin.companies.*`) ODER
|
||||
**4E = Settings / Profile** (`me.profile`, `me.security`).
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-19 · Phase 4B · Press-Releases Detail/Show-Pages
|
||||
|
||||
- **Anlass**: User-Freigabe „weiter" nach 4A. Zweites Päckchen aus
|
||||
Phase 4 — Detail/Show-Pages für Admin und Customer.
|
||||
|
||||
- **Plan-Dokument**: `08-PHASE-4B-PRESS-RELEASES-DETAIL.md`.
|
||||
|
||||
- **Was umgebaut wurde (beide Show-Pages)**:
|
||||
- **Page-Header** mit Hub-Badge + Eyebrow + Status-Pill +
|
||||
Sprache + Portal (Admin nur), darunter großer H1 mit dem
|
||||
PM-Titel und Subtitle mit Firma/Kategorie/Autor bzw. Datum.
|
||||
Aktions-Buttons rechts (Bearbeiten/Vorschau-Link/Zurück).
|
||||
- **Status-Workflow-Aktionsbar** als `.panel` mit `.panel-head`
|
||||
und passend gefärbtem Badge je nach Status (warn=Review,
|
||||
ok=Published, err=Rejected, hub=Draft).
|
||||
- **Content-Hauptbereich** (PM-Text) als `.panel` mit eigenem
|
||||
Header „Inhalt" + `prose` darunter. Keywords/Backlink darunter
|
||||
als Footer mit Hub-Rule.
|
||||
- **Sidebar / Side-Cards** (Admin) als kompakte `.panel` mit
|
||||
`panel-head` „Details" und „Bilder" + Hub-Badges.
|
||||
- **Status-Verlauf-Timeline** als `.panel` mit Hub-Badge je
|
||||
Log-Eintrag (`.badge.ok|warn|err|hub`) statt FluxUI-`color`-Props.
|
||||
- **Status-Badges** in Header und Timelines komplett auf
|
||||
`<span class="badge …">` mit `@class([])`-Syntax.
|
||||
|
||||
- **Customer-Show-spezifisch**:
|
||||
- **Rejection-Hinweis** als roter `.panel` mit linker
|
||||
Akzent-Border (`border-left-width:3px`) und Icon-Box —
|
||||
statt `<flux:callout color="red">`.
|
||||
- **Review-Pending-Hinweis** als warner `.panel` mit
|
||||
Akzent-Border und Clock-Icon-Box — statt `<flux:callout color="yellow">`.
|
||||
- **Share-Link-Erfolgsblock** als ok-`.panel` mit
|
||||
Gültigkeits-Badge im Header und readonly-Input im Body.
|
||||
- **Contacts-Liste** als Hub-Items (`bg-bg-elev` +
|
||||
`border-bg-rule`), Empty-State mit gestrichelter Border.
|
||||
- **Status-Tile-Grid** (Aktuell/Erstellt/Veröffentlicht/Aufrufe)
|
||||
als 4-er-Grid mit kleinen Hub-Tiles statt zinc-Hintergründen.
|
||||
|
||||
- **Admin-Show-spezifisch**:
|
||||
- **Modals** (Publish/Reject/Archive) komplett unverändert —
|
||||
Tests in `AdminPressReleaseActionsTest` assertieren Wortlaute.
|
||||
|
||||
- **Was explizit NICHT angefasst wurde**:
|
||||
- **Volt-Logik** (publish/reject/archive/submitForReview/
|
||||
generateShareLink/with-Method) — Layout-only.
|
||||
- **Wortlaute** „Erneut einreichen", „Werbliche Sprache wurde
|
||||
markiert." aus `PressReleaseWorkflowTest`.
|
||||
- **Bestehende Modals** und ihre Wortlaute.
|
||||
|
||||
- **Mini-Stolperer (sofort gefixt)**:
|
||||
- Erst zwei nicht-existente Tokens (`--color-rule`,
|
||||
`--color-panel-soft`) verwendet. Korrigiert zu
|
||||
`--color-bg-rule` und `--color-bg-elev` (16 + 5 Vorkommen).
|
||||
Wäre im Browser stumm gefailed (Tailwind hätte die
|
||||
Klassen einfach nicht ausgegeben).
|
||||
|
||||
- **Build/Test**:
|
||||
- `npm run build:portal` → ok (`portal-Bq4pkLWt.css` 418.36 kB).
|
||||
- Linter clean.
|
||||
- `php artisan test --compact --filter='PressReleaseWorkflow|AdminPressReleaseActions'`
|
||||
→ 16 passed (52 assertions).
|
||||
- Volle Suite: 230 passed, 3 skipped, 1 pre-existing failure
|
||||
(`ApiDocumentationTest`, schon vor 4A bekannt).
|
||||
- Pint `--dirty --format agent` → passed.
|
||||
|
||||
- **Offene Fragen**: Keine.
|
||||
|
||||
- **Nächster Schritt**: Päckchen **4C** = Press-Releases Forms
|
||||
(`create.blade.php` + `edit.blade.php`, Admin + Customer).
|
||||
Forms sind tendenziell aufwendiger (mehr FluxUI-Felder, ggf.
|
||||
zusätzliche Logik wie Image-Uploads).
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-19 · Phase 4A · Press-Releases Listen-Pages
|
||||
|
||||
- **Anlass**: User-Freigabe „weiter" nach Phase 3 (Admin-Dashboard).
|
||||
Phase 4 ist laut Roadmap „der Marathon" über Listen-/Detail-Pages
|
||||
(~3–5 Tage iterativ). Wegen Umfang in Päckchen geteilt — A = Listen,
|
||||
B = Detail/Show, C = Forms, D = Companies, E = Settings, F = Rest.
|
||||
|
||||
- **Plan-Dokument**: `07-PHASE-4A-PRESS-RELEASES-LISTEN.md` (knapp,
|
||||
Scope hart auf zwei `index.blade.php`-Files begrenzt, mit explizitem
|
||||
„NICHT in diesem Päckchen"-Block).
|
||||
|
||||
- **Scope dieses Päckchens**:
|
||||
- `resources/views/livewire/admin/press-releases/index.blade.php`
|
||||
- `resources/views/livewire/customer/press-releases/index.blade.php`
|
||||
|
||||
- **Was umgebaut wurde (beide Listen)**:
|
||||
- **Page-Header** im Hub-Stil: `<header>` mit `1fr auto`-Grid,
|
||||
Hub-Badge („Admin Backend" / „User Backend"), Eyebrow, großes H1,
|
||||
Subtitle, rechts der CTA-Primary-Button (FluxUI bleibt für den Button).
|
||||
- **Filter-Bar** als `.panel` + `.panel-head` mit `section-eyebrow`
|
||||
„Filter & Suche". FluxUI-Inputs (Search-Input, Selects, Combobox
|
||||
für User/Company/Contact-Lookup) bleiben unverändert — Hub hat
|
||||
kein eigenes Combobox-Pendant.
|
||||
- **Tabelle** als `.panel` mit `.panel-head` „Alle Pressemitteilungen"
|
||||
+ Eintrags-Counter rechts. FluxUI-`<flux:table>` bleibt — das
|
||||
Zinc-→Hub-Mapping aus Phase 1 zieht hier perfekt.
|
||||
- **Status-Badges**: `<flux:badge color="green|yellow|red|zinc|blue">`
|
||||
ersetzt durch `<span class="badge ok|warn|err|hub">` für visuelle
|
||||
Konsistenz mit dem Dashboard (`@class([...])`-Syntax).
|
||||
- **Empty-State** mit Icon-Box (`bg-[color:var(--color-hub-soft)]`,
|
||||
Hub-Border, `flux:icon.newspaper`) + Headline + Subtext.
|
||||
- **Flash-Boxen** auf Hub-Tokens (`--color-ok-soft`, `--color-err-soft`,
|
||||
`--color-gain-deep`, `--color-loss`) statt `bg-green-50` etc.
|
||||
|
||||
- **Admin-spezifisch**: Vier `<x-portal.stat-card>` (Gesamt/Veröffentlicht/
|
||||
In Prüfung/Entwürfe) als KPI-Reihe — identisches Layout wie Admin-Dashboard.
|
||||
|
||||
- **Was explizit NICHT angefasst wurde**:
|
||||
- **Confirm-Modals** (Publish/Reject/Archive) — Tests in
|
||||
`AdminPressReleaseActionsTest` assertieren die Wortlaute
|
||||
(„Pressemitteilung veröffentlichen?" etc.) → unverändert.
|
||||
- **Volt-Logik** (Sort, Filter, alle Methods) — Layout-only.
|
||||
|
||||
- **Bonus-Fix**: `customer/dashboard.blade.php` Subtitle bekommt
|
||||
einen „Übersicht für :company —"-Einschub, wenn `$selectedCompany`
|
||||
gesetzt ist. Korrigiert eine Phase-2-Regression bei
|
||||
`CustomerCompanyContextTest > customer dashboard is filtered by …`.
|
||||
|
||||
- **Build/Test**:
|
||||
- `npm run build:portal` → ok (`portal-DaL-tXm-.css` 418.75 kB).
|
||||
- Linter clean.
|
||||
- `php artisan test --compact --filter='PressReleaseWorkflow|AdminPressReleaseActions|Dashboard|CustomerCompanyContext'`
|
||||
→ 45 passed (163 assertions).
|
||||
- Volle Suite: 230 passed, 3 skipped, 1 failed.
|
||||
Der einzelne Fail (`ApiDocumentationTest`) ist **pre-existing** —
|
||||
via `git stash` verifiziert, war auch vor diesem Päckchen rot
|
||||
(`/docs/api/v1` liefert nicht das erwartete YAML).
|
||||
- Pint `--dirty --format agent` → passed.
|
||||
|
||||
- **Offene Fragen**: Keine.
|
||||
|
||||
- **Nächster Schritt**: Päckchen **4B** = Detail/Show-Pages
|
||||
(`admin/press-releases/show.blade.php` +
|
||||
`customer/press-releases/show.blade.php`). Dort ist mit der
|
||||
„Werbliche Sprache wurde markiert."- und „Erneut einreichen"-
|
||||
Assertion aus `PressReleaseWorkflowTest` zu rechnen — Wortlaute
|
||||
bleiben unverändert.
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-19 · Phase 3 · Admin-Dashboard im Hub-Vokabular
|
||||
|
||||
- **Anlass**: User-Freigabe „weiter" nach Phase 5 (Dark Mode). Phase 3
|
||||
laut Roadmap: Admin-Dashboard (`/dashboard`) im selben Vokabular wie
|
||||
Customer-Dashboard.
|
||||
|
||||
- **Plan-Dokument**: `06-PHASE-3-ADMIN-DASHBOARD.md` (knapper als
|
||||
Phase 2 — niedrig-risiko, keine neuen Konzepte, reine
|
||||
Vokabular-Migration auf die DRY-Schicht aus Phase 2).
|
||||
|
||||
- **Ausgangslage**: `admin/dashboard.blade.php` war Controller-rendered
|
||||
(kein Volt), nutzte reines Tailwind mit `zinc-*`-Klassen, harten
|
||||
Farb-Pillen (`bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 …`)
|
||||
und keiner FluxUI-Komponente. Visuell aus der Zeit gefallen.
|
||||
|
||||
- **`admin/dashboard.blade.php` komplett umgeschrieben**:
|
||||
- **Page-Header**: Hub-Badge „Admin Backend" + Eyebrow + großes H1
|
||||
+ Subtitle mit `auth()->user()->name`. Rechts: ok-Pille
|
||||
„Alle Systeme operational".
|
||||
- **KPI-Reihe** (5 Stat-Cards via `<x-portal.stat-card>`):
|
||||
- Pressemitteilungen → `primary` mit Trend-Slot
|
||||
„X pub · Y prüf · Z entwurf" (Wortlaut für Test-Assertions
|
||||
bewusst beibehalten).
|
||||
- In Prüfung → `warn` als eigene KPI (war vorher nur in der PM-Card
|
||||
versteckt — jetzt klickbar direkt in den Filter).
|
||||
- Firmen / Kontakte / Benutzer → `muted` mit kurzen Sublines.
|
||||
- Alle Cards sind klickbar (link-wrapped) → CRM-/Content-Übersichten.
|
||||
- **2-Spalten-Grid** (`2fr 1fr`):
|
||||
- Links: Panel „Letzte Pressemitteilungen" mit Hub-Liste +
|
||||
`.badge.ok|warn|err|hub`-Pillen (statt Tailwind-Farben).
|
||||
- Rechts: Panel „Zur Prüfung" mit warn-Pille (Count) oder
|
||||
ok-Pille „leer". „+ N weitere"-Link im Footer.
|
||||
- **Newsletter + Quick-Actions** (`1fr 2fr`, NEU):
|
||||
- Links: `panel-warm` Newsletter-Block mit Mono-Zahl
|
||||
+ Subline + Sync-Link.
|
||||
- Rechts: Quick-Action-Grid mit 4 Icon-Buttons (Pressemitteilungen,
|
||||
Firmen, Rechnungen, Voreinstellungen) — Icon-Tile wechselt auf
|
||||
Hover ins gefüllte Hub-Blau.
|
||||
- **Footer**: subtle Link-Reihe (Benutzer / Rollen / Performance).
|
||||
|
||||
- **Was bewusst NICHT geändert**:
|
||||
- Controller-Logik, Datenform, Cache-Strategie (`AdminPerformanceCache`).
|
||||
- Newsletter-Count bleibt, wandert nur in eigenen Block.
|
||||
|
||||
- **Build/Verifikation**:
|
||||
- `npm run build:portal` → `portal: 417.81 kB` (Δ -0.7 kB,
|
||||
weil viele Zinc-Tailwind-Klassen weggefallen sind).
|
||||
- `php artisan test --filter='DashboardTest|AuthenticationTest|RegistrationTest|CustomerPortalTest'`
|
||||
→ **18/18 passed**, 70 Assertions. `DashboardTest` mit seinen
|
||||
Wortlaut-Assertions (`3`, `1 pub`, `1 prüf`, `1 entwurf`,
|
||||
`Review Dashboard PM`) bleibt grün ohne Anpassung.
|
||||
- `vendor/bin/pint --dirty` → `passed`.
|
||||
- Linter: 0 Errors.
|
||||
|
||||
- **Lessons learned**:
|
||||
- Vokabular-Migrations ohne Logik-Eingriff brauchen ~15 Minuten dank
|
||||
DRY-Schicht aus Phase 2 — der Wert der `shared/hub-components.css`
|
||||
+ `<x-portal.stat-card>`-Investition zahlt sich ab Phase 3 deutlich aus.
|
||||
- Der `<x-portal.stat-card>` ist auch ohne `:value`-Number-Format
|
||||
flexibel (`number_format()` direkt im Aufruf eingebettet).
|
||||
- Test-Assertions können bei Layout-Refactors überraschend stabil
|
||||
bleiben, wenn man die Original-Strings im neuen Layout an
|
||||
sinnvollen Stellen behält (hier: im Trend-Slot der KPI-Card).
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-19 · Phase 5 · Dark Mode + Switcher im User-Menü
|
||||
|
||||
- **Anlass**: User-Feedback nach Phase 2: „Switch in den Dark Mode funktioniert
|
||||
nicht. Zusätzlich hätte ich gerne einen Switcher hell/dunkel direkt im
|
||||
User-Menü." → Phase 5 vorgezogen vor Phase 3/4, weil der Switcher der
|
||||
natürliche UX-Einstiegspunkt für Dark Mode ist und der Notfall-Hack
|
||||
aus Phase 1 (`portal.css: .dark { --color-accent: var(--color-hub) }`)
|
||||
endlich verschwinden soll.
|
||||
|
||||
- **Plan-Dokument**: `05-PHASE-5-DARK-MODE.md` mit Token-Mapping-Tabelle,
|
||||
drei wichtigen Token-Konstanten-Tricks (`--color-accent-warm`,
|
||||
`--color-panel-dark`, `--color-panel-dark-2`) und Switcher-Snippet.
|
||||
|
||||
- **`design-tokens.css` — Dark-Block aktiviert**:
|
||||
- Vollständiger `.dark { … }`-Block mit Werten aus
|
||||
`User Dashboard presseportale Dark.html`.
|
||||
- Surfaces: bg/elev/card/rule schalten auf `#0e1218`-Familie.
|
||||
- Hub-Blau wird **heller** im Dark Mode (`#5a78c2` statt `#1a2540`) —
|
||||
notwendig für Lesbarkeit auf dunklem Bg.
|
||||
- Bernstein-Akzent ebenfalls heller (`#d9a560`).
|
||||
- Status-Farben (`ok/warn/err`) auf dunkle, gesättigte Variante.
|
||||
- Schatten-Tokens neutral-schwarz statt hub-blau-warm.
|
||||
- `color-scheme: dark;` als Hint für native Form-Controls.
|
||||
|
||||
- **3 neue Token-Konstanten** (gleicher Wert in beiden Modi):
|
||||
- `--color-accent-warm` (`#b07a3a`) — für Stellen, die explizit Bernstein
|
||||
bleiben müssen (Hint-Card-Border-Left, Progress-Bar-Fill); löst die
|
||||
Kollision auf, dass `--color-accent` im Portal auf Hub-Blau gemapt
|
||||
ist und im Dark Mode noch heller würde.
|
||||
- `--color-panel-dark` (`#0f1729`) und `--color-panel-dark-2` (`#1a2540`)
|
||||
— für `.panel-dark` und Brand-Bridge-Inner-Boxes. Ohne diese würde
|
||||
der dunkle Bridge-Container im Dark Mode plötzlich hellblau, weil
|
||||
`var(--color-hub)` zum hellen Wert wird.
|
||||
|
||||
- **`portal.css` — Notfall-Hack aus Phase 1 entfernt**:
|
||||
- Der `.dark { --color-accent: var(--color-hub); … }`-Block ist
|
||||
überflüssig, weil das echte Dark-Token-Mapping in `design-tokens.css`
|
||||
`--color-hub` automatisch auf `#5a78c2` schaltet — und
|
||||
`--color-accent` darüber per `var(--color-hub)`-Verweis dynamisch
|
||||
mitkommt.
|
||||
- Primary-Button-Shadows zusätzlich für `.dark` überschrieben: statt
|
||||
`rgba(26, 37, 64, …)` (warmer Hub-Blau-Alpha) jetzt `rgba(0, 0, 0, …)`
|
||||
(neutral-schwarz), weil der hub-blaue Schatten auf dunklem Card-BG
|
||||
zu sichtbar wirkt.
|
||||
|
||||
- **`hub-components.css` — Dark-tauglich gemacht**:
|
||||
- `.panel-dark` nutzt jetzt `var(--color-panel-dark-2)` und
|
||||
`var(--color-panel-dark)` (vorher `var(--color-hub)` und
|
||||
`var(--color-topbar-deep)`) → bleibt im Dark Mode dunkel.
|
||||
- `.hint-card border-left` und `.hint-bar > span` nutzen
|
||||
`var(--color-accent-warm)` (konstant Bernstein) statt `var(--color-accent)`
|
||||
(im Portal Hub-Blau-Mapping).
|
||||
- `.hint-card background` schaltet auf `var(--color-bg-card-warm)` —
|
||||
ist im Light Mode `#efeadc` (warmes Buchpapier), im Dark Mode
|
||||
`#1f1a12` (warm-dunkles Bernstein-Substrat).
|
||||
- Restliche Klassen funktionieren **automatisch**, weil alle Werte
|
||||
über Tokens laufen — DRY-Architektur zahlt sich aus.
|
||||
|
||||
- **`customer/dashboard.blade.php`**:
|
||||
- Brand-Bridge-Inner-Boxes auf `bg-[color:var(--color-panel-dark-2)]`
|
||||
umgestellt (statt `--color-hub-2`, das im Dark Mode hell würde).
|
||||
|
||||
- **Switcher im User-Menü** (`sidebar.blade.php` — Desktop + Mobile):
|
||||
- Vor dem Logout-Button: kleiner Block mit Eyebrow „Erscheinung" und
|
||||
FluxUI-Segmented-Radio-Group mit Icons-only.
|
||||
```blade
|
||||
<flux:radio.group x-data variant="segmented" size="sm" x-model="$flux.appearance">
|
||||
<flux:radio value="light" icon="sun" :title="__('Hell')" />
|
||||
<flux:radio value="dark" icon="moon" :title="__('Dunkel')" />
|
||||
<flux:radio value="system" icon="computer-desktop" :title="__('System')" />
|
||||
</flux:radio.group>
|
||||
```
|
||||
- `$flux.appearance` ist FluxUIs Magic-Object — LocalStorage-persistent,
|
||||
`@fluxAppearance`-Script setzt `class="dark"` auf `<html>`.
|
||||
- In BEIDEN Dropdowns (Desktop in der Sidebar, Mobile im Header).
|
||||
|
||||
- **Hub-Frontend bleibt Light-Only** (Plan-Vorgabe):
|
||||
- `auth/pressekonto.blade.php` lädt KEIN `@fluxAppearance` und KEIN
|
||||
`partials/head`. Damit wird `class="dark"` dort nie gesetzt — auch
|
||||
nicht, wenn LocalStorage `dark` enthält. Kein Eingriff nötig.
|
||||
- Hub-Landing analog (eigene `<head>`-Pipeline).
|
||||
|
||||
- **Build/Verifikation**:
|
||||
- `npm run build:portal` → `portal: 418.55 kB` (Δ +1.5 kB für
|
||||
Dark-Tokens + Switcher).
|
||||
- `php artisan test --filter='DashboardTest|AuthenticationTest|RegistrationTest|CustomerPortalTest'`
|
||||
→ **18/18 passed**, 70 Assertions. Keine Regressions.
|
||||
- `vendor/bin/pint --dirty` → `passed`.
|
||||
- Linter: 0 Errors.
|
||||
|
||||
- **Lessons learned**:
|
||||
- Tailwind v4's `@theme`-Variables sind **dynamisch** zur Laufzeit —
|
||||
`--color-accent: var(--color-hub)` schaltet beim `.dark`-Switch
|
||||
automatisch mit, ohne dass man `.dark { --color-accent: … }` extra
|
||||
setzen muss.
|
||||
- Wenn ein Wert in beiden Modes IDENTISCH bleiben soll (`--color-panel-dark-2`,
|
||||
`--color-accent-warm`), gehört er in den Light-Block und wird im
|
||||
`.dark`-Block bewusst NICHT überschrieben. Vererbung tut den Rest.
|
||||
- FluxUI's `flux:radio.group variant="segmented"` mit Icons-only ist
|
||||
perfekt für Dropdown-Switcher; `:title="…"` liefert Tooltips ohne
|
||||
sichtbares Label.
|
||||
|
||||
- **Bonus**: Der ursprüngliche Symptom-Fix aus Phase 1
|
||||
(Multi-Alpine + Dark-Mode-Bug) ist jetzt strukturell aufgelöst:
|
||||
- Alpine kommt ausschließlich aus `@fluxScripts`.
|
||||
- Dark Mode ist echt umgesetzt, nicht maskiert.
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-19 · Phase 2 · Customer-Dashboard auf Mockup-Stil
|
||||
|
||||
- **Anlass**: User-Freigabe „weiter“ nach Behebung des Dark-Mode/Alpine-Bugs.
|
||||
Phase 2 laut Plan: Customer-Dashboard (`/admin/me`) an
|
||||
`User Dashboard presseportale.html` angleichen.
|
||||
- **Vorab-Doku**: `04-PHASE-2-CUSTOMER-DASHBOARD.md` mit Mockup-Diff-Tabelle,
|
||||
CSS-Strategie, Component-Skizzen, Akzeptanzkriterien.
|
||||
|
||||
- **CSS-Architektur — DRY-Schritt**:
|
||||
- Neue Datei `resources/css/shared/hub-components.css` als **Single Source
|
||||
of Truth** für Hub-Layout-Bausteine: `.panel` (warm/dark/head),
|
||||
`.stat-card` (+ Varianten `is-primary|ok|warn|muted`, `.stat-strip`,
|
||||
`.stat-label`, `.stat-num`, `.stat-meta`, `.stat-trend`), `.hint-card`
|
||||
(+ `.hint-ico`, `.hint-bar`, `.hint-action`), `.badge` (+ `.warn|ok|hub|err|dot`),
|
||||
`.bridge-row`/`.dot-pe`/`.dot-bp`, portable `.eyebrow` und
|
||||
`.section-eyebrow` (für Portal — Web-Build überschreibt idempotent).
|
||||
- Importiert in **beiden** Builds:
|
||||
- `resources/css/portal.css` (nach `flux.css`)
|
||||
- `resources/css/web/shared-styles.css` (nach `design-tokens.css`)
|
||||
- Alle Werte nutzen `var(--color-*)`-Tokens — keine Hex-Literale.
|
||||
- Fehlende Tokens nachgezogen in `shared/design-tokens.css`:
|
||||
`--color-bg-rule-2` (hellere Progress-Track-Variante) und
|
||||
`--color-gain-deep` (dunkles Grün für `is-ok`-Stat-Num).
|
||||
|
||||
- **Neue Blade-Components** in `resources/views/components/portal/`:
|
||||
- `<x-portal.stat-card variant="…" label="…" :value="…">`
|
||||
mit Slots `meta` (oben rechts) und `trend` (unten).
|
||||
- `<x-portal.hint-card :icon :title :percent :href :action>`
|
||||
mit Progress-Bar bei gesetztem `$percent` und `wire:navigate`-Link.
|
||||
|
||||
- **`livewire/customer/dashboard.blade.php` komplett neu**:
|
||||
- **Page-Header**: Hub-Badge „User Backend“ + Eyebrow + großes H1
|
||||
+ Subtitle. Rechts entweder Aktive-Firma-Pille **oder** Warn-Pille
|
||||
„Keine Firma zugeordnet → zuordnen“.
|
||||
- **KPI-Reihe** (4 `stat-card`s): Gesamt (primary, mit Δ-zu-Vormonat),
|
||||
Veröffentlicht (ok), In Prüfung (warn), Entwürfe (muted).
|
||||
- **Zweispalten-Grid** (`lg:grid-cols-[2fr_1fr]`):
|
||||
- Links: Panel mit Pressemitteilungen-Liste oder Empty-State (Icon-Box
|
||||
mit Notification-Dot, Headline, Action-Button, Schritt-Karten 01/02/03,
|
||||
Footer-Tipp „4 Stunden“).
|
||||
- Rechts: Panel „Datenqualität“ mit `<x-portal.hint-card>`s oder
|
||||
Success-State („Alles im grünen Bereich“).
|
||||
- **Unteres Grid**: Firmen-Panel (Liste **oder** dashed Empty-Slot
|
||||
+ Hinweis-Box) + Brand-Bridge-Dark-Panel (presseecho + businessportal24
|
||||
mit Status, API-Status-Indikator, Tarif).
|
||||
- **Footer**: Subtle-Link-Reihe (Sicherheit, API & Tokens, Profil).
|
||||
|
||||
- **Volt-`with()` erweitert**:
|
||||
- `stats.deltaMonth` — Monatsvergleich via zweiter `whereBetween`-Query.
|
||||
- `profileCompleteness` (Heuristik auf 6 Profile-Kernfelder).
|
||||
- `billingCompleteness` (5 Address-Pflichtfelder).
|
||||
- `bridgeStatus` (vorerst hardcoded `connected` — Phase 3+).
|
||||
- `qualityHints[]` optional um `percent`-Feld erweitert — wenn gesetzt,
|
||||
rendert Progress-Bar in der Hint-Card.
|
||||
|
||||
- **Edge-Case beim Bauen**: Blade interpretierte `<flux:icon>` im PHPDoc-
|
||||
Kommentar von `hint-card.blade.php` als echten Component-Tag und produzierte
|
||||
`Undefined variable $component`. Fix: Doc-Block entkontaminiert
|
||||
(„flux:icon“ ohne Spitzklammern).
|
||||
|
||||
- **Tests neu** in `tests/Feature/Customer/DashboardTest.php` (5 Cases):
|
||||
1. Customer-Dashboard rendert Core-Sections ohne Fehler
|
||||
2. Empty-State mit 3-Schritt-Intro wird gezeigt
|
||||
3. PR-Liste + KPI-Zahlen rendern bei vorhandenen Daten
|
||||
4. Profile-Completeness-Hint mit Prozentwert erscheint bei Teildaten
|
||||
5. Vollständiges Profil + Billing → Hints werden ausgeblendet,
|
||||
„Alles im grünen Bereich“ wird gezeigt
|
||||
- **Ergebnis**: 5/5 passed, 21 Assertions.
|
||||
- Cross-Check: Verwandte Tests (`Dashboard|Authentication|Registration|CustomerPortal`)
|
||||
→ **18/18 grün**, 70 Assertions. Keine Regressions.
|
||||
|
||||
- **Build/Verifikation**:
|
||||
- `npm run build:portal` → `portal: 417.02 kB` (Δ +8 kB für neue
|
||||
Hub-Components-Klassen + Dashboard-Klassen). Web-Build unverändert.
|
||||
- `vendor/bin/pint --dirty` → `passed`.
|
||||
|
||||
- **Was bewusst weggelassen**:
|
||||
- Sparklines auf den Stat-Cards (kommen mit echten Trend-Daten in Phase 4).
|
||||
- Topbar-Anpassung (eigene Phase, später).
|
||||
- `<x-portal.bridge-card>`-Extraktion (Brand-Bridge bleibt inline, bis
|
||||
sie an mehr Stellen gebraucht wird).
|
||||
|
||||
- **Lessons learned**:
|
||||
- PHPDoc in Blade-Components darf KEINE `<flux:*>`-Tags enthalten —
|
||||
der Blade-Pre-Compiler scannt das gesamte File vor dem PHP-Parsing.
|
||||
- `shared/hub-components.css` ist der richtige Hebel für Phase 3
|
||||
(Admin-Dashboard) und Phase 4 (Listen/Detail) — Components-CSS muss
|
||||
nicht mehr pro Phase neu definiert werden.
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-19 · Phase 1 Refinement · Dark-Mode-Bug + Multi-Alpine
|
||||
|
||||
- **Anlass**: User-Review nach Kontrast-Refinement: zwei konkrete Befunde
|
||||
aus der Browser-Konsole.
|
||||
1. `[Warning] Detected multiple instances of Alpine running (me, line 114)`
|
||||
2. Button-Default-Farbe ist `rgb(109, 138, 211)` (helles Blau), nicht
|
||||
das gewünschte Hub-Blau `rgb(26, 37, 64)`. Computed Style zeigt:
|
||||
`.bg-[var(--color-accent)] { background-color: var(--color-accent); }`
|
||||
→ `var(--color-accent)` löst zu `#6d8ad3` auf.
|
||||
|
||||
- **Diagnose Bug 1 (Alpine)**:
|
||||
- `partials/head.blade.php` lädt `resources/js/app.js` über `@vite`.
|
||||
`app.js` ruft `Alpine.start()` auf.
|
||||
- Am Ende des `<body>` injiziert `@fluxScripts` (sidebar.blade.php Zeile
|
||||
nahe `</body>`) **eine eigene Alpine-Instanz** (FluxUI bringt Alpine
|
||||
selbst mit).
|
||||
- Ergebnis: zwei `window.Alpine`-Objekte, Bindings teils tot, Warning.
|
||||
- Auf der Login-Seite hatten wir das **bereits** gefixt (app.js dort
|
||||
rausgenommen), aber im Portal-Layout war der Fix noch offen.
|
||||
|
||||
- **Diagnose Bug 2 (Button-Farbe)**:
|
||||
- Der Wert `rgb(109, 138, 211)` = `#6d8ad3` ist **exakt** der
|
||||
Dark-Mode-Fallback, den ich in `portal.css` unter `.dark { --color-accent: #6d8ad3 }`
|
||||
definiert hatte.
|
||||
- FluxUI's `@fluxAppearance`-Helper schreibt `class="dark"` auf
|
||||
`<html>`, sobald der User den Appearance-Switcher mal auf "dark"
|
||||
gestellt hatte (gespeichert in `localStorage`).
|
||||
- Damit überschreibt der Dark-Block den Light-Mode-Akzent
|
||||
`var(--color-hub)` (`#1A2540`) — Buttons sehen plötzlich blassblau aus.
|
||||
- Dark Mode ist laut Plan erst Phase 5, war aber durch den
|
||||
Fallback-Block bereits halb aktiv.
|
||||
|
||||
- **Fixes**:
|
||||
- `resources/views/partials/head.blade.php`: `resources/js/app.js`
|
||||
aus dem `@vite`-Aufruf **entfernt**. Alpine kommt im Portal
|
||||
ausschließlich über `@fluxScripts`. (Hub-Landing nutzt
|
||||
`partials/head` nicht — eigene `<head>`-Pipeline.)
|
||||
- `resources/css/portal.css`: Der `.dark { --color-accent: ... }`-
|
||||
Block setzt jetzt **bewusst** alle Akzent-Tokens auf
|
||||
`var(--color-hub)` (also den Light-Mode-Wert). Damit bleibt das
|
||||
Hub-Blau auch bei eingeschaltetem `class="dark"` konsistent.
|
||||
Kommentar dokumentiert, dass Phase 5 diesen Block durch das
|
||||
echte Dark-Token-Mapping aus `shared/design-tokens.css` ersetzt.
|
||||
|
||||
- **Build/Verifikation**:
|
||||
- `npm run build:portal` → `portal: 408.97 kB` (Δ +0.02 kB).
|
||||
- `php artisan test --compact --filter='AuthenticationTest|RegistrationTest'`
|
||||
→ **6 passed, 19 assertions**, 1.93s.
|
||||
- `vendor/bin/pint --dirty --format agent` → `passed`.
|
||||
|
||||
- **User-Action zur Verifikation**:
|
||||
- Im Portal Hard-Reload (Cmd/Strg+Shift+R) — die alte `app.js`
|
||||
hängt sonst im HTTP-Cache.
|
||||
- Konsole sollte **keine** Multi-Alpine-Warning mehr werfen.
|
||||
- Primary-Buttons sind im Default-State sattes Hub-Blau (`#1A2540`),
|
||||
Hover noch dunkler (`#243152`).
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-19 · Phase 1 Refinement · Kontraste & Button-Hover
|
||||
|
||||
- **Anlass**: User-Review nach Phase 1: „Buttons als Primary auf
|
||||
`rgb(26, 37, 64)` einsetzen, die andere Farbe ist deutlich zu hell.
|
||||
Es fehlen noch deutliche Kontraste."
|
||||
- **Diagnose**:
|
||||
- FluxUI-Primary-Buttons sind eigentlich **schon** auf `var(--color-accent)`
|
||||
= `var(--color-hub)` = `#1A2540` = `rgb(26, 37, 64)` gesetzt
|
||||
(im Build verifiziert). Sollte stimmen.
|
||||
- **ABER**: Der FluxUI-Default-Hover ist
|
||||
`hover:bg-[color-mix(in_oklab,_var(--color-accent),_transparent_10%)]`
|
||||
— auf hellem Buchpapier wirkt das hell-blau statt der gewünschten
|
||||
Hub-Konvention `hover:bg-hub-2` (dunkler als Default).
|
||||
- Außerdem zu wenig Schatten/Border-Kontrast für klare Button-Kanten.
|
||||
- **Erkenntnis**: Frühere `[data-flux-button][data-variant="primary"]`-
|
||||
Overrides griffen **nie**, weil FluxUI kein `data-variant`-Attribut
|
||||
rendert. Variant-Styling kommt komplett über Tailwind-Klassen
|
||||
(z.B. `bg-[var(--color-accent)]`). Neue Selektor-Strategie nutzt
|
||||
jetzt diese Klassen direkt (mit escapeten Brackets).
|
||||
- **Fixes in `portal.css`**:
|
||||
- **Primary-Button-Hover** auf `var(--color-hub-2)` (#243152) statt
|
||||
FluxUI's color-mix-Hellung. Selektor:
|
||||
`[data-flux-button].hover\:bg-\[color-mix\(in_oklab\,_var\(--color-accent\)\,_transparent_10\%\)\]:hover`
|
||||
- **Primary-Button-Shadow**: kräftigeres Inset-Highlight + warmer
|
||||
Drop-Shadow in Hub-Blau-Alpha für klare Kanten auf Buchpapier.
|
||||
Border zusätzlich auf `var(--color-hub-2)` für definierten Rand.
|
||||
- **Hover-State**: noch stärkerer Shadow + Drop für Tiefenwirkung.
|
||||
- **Input-Focus** auf Hub-Blau-Ring (statt blassem Default-Akzent),
|
||||
mit Buchpapier-Offset für saubere Trennung.
|
||||
- Alter (defunkter) `[data-flux-button][data-variant="primary"]`-
|
||||
Block entfernt, weil Selektor nicht existiert.
|
||||
- **Build/Verifikation**:
|
||||
- `npm run build:portal` → `portal: 408.95 kB` (von 409.03 kB, -0.08 kB).
|
||||
- Im finalen CSS verifiziert:
|
||||
* Hub-2-Hover-Override:
|
||||
`[data-flux-button].hover\:bg-…:hover{background-color:var(--color-hub-2)!important}`
|
||||
* Shadow-Override:
|
||||
`[data-flux-button].bg-\[var\(--color-accent\)\]{border-color:var(--color-hub-2);box-shadow:inset 0 1px #ffffff2e,0 1px 2px #1a254040,…}`
|
||||
* Input-Focus-Ring auf Hub-Blau.
|
||||
- `vendor/bin/pint --dirty` → passed.
|
||||
- **Wirkung**:
|
||||
- Primary-Button **Default**: `#1A2540` (Hub-Blau), klarer warmer
|
||||
Schatten, definierter Rand → fühlt sich „solid" an wie auf der
|
||||
Hub-Landing.
|
||||
- Primary-Button **Hover**: `#243152` (Hub-2, dunkler statt heller)
|
||||
— Hub-Konvention, signalisiert Interaktion ohne den Eindruck einer
|
||||
schwächeren Farbe zu erzeugen.
|
||||
- Inputs zeigen jetzt einen **erkennbaren Hub-Blau-Ring** beim Fokus
|
||||
statt eines blassen Bernstein-Schimmers.
|
||||
- **Hinweis an User**: **Hard-Reload** (Cmd+Shift+R) ist nötig — das
|
||||
CSS-Bundle hat einen neuen Hash (`portal-kuU-opFv.css`).
|
||||
- **Verfeinerungen noch offen für Phase 2 / Iteration**:
|
||||
- Subtle/Filled/Ghost-Buttons (`<flux:button variant="filled">`) sind
|
||||
weiterhin transparent-zinc → bei Bedarf in Phase 2 angleichen.
|
||||
- Sidebar-Active-Item hat schon Hub-Soft + 2 px-Strip; ggf. den
|
||||
Strip noch sichtbarer machen wenn weiter zu unauffällig.
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-19 · Phase 1 abgeschlossen · Portal-Shell auf Hub-Design
|
||||
|
||||
- **Was**: Portal-Shell (Sidebar, Topbar, Layout-Container, Customer-Banner)
|
||||
visuell ans Hub-Design angeglichen. FluxUI bleibt komplett erhalten —
|
||||
Anpassung erfolgt über CSS-Token-Bridging und `[data-flux-*]`-Overrides.
|
||||
Brand-Mark ersetzt das Starter-Kit-Logo. Light Mode ist Default; Dark
|
||||
Mode für Phase 5 vorbereitet (heller Hub-Blau).
|
||||
- **Dateien (geändert)**:
|
||||
- `resources/css/portal.css` — komplett refactored:
|
||||
* `--font-sans: "Inter Tight"` statt `Instrument Sans`
|
||||
* `--color-accent: var(--color-hub)` (#1A2540) statt `#3ea3dc`
|
||||
* `--color-zinc-50..950` auf Hub-Buchpapier-Familie gemappt
|
||||
(Zinc-100 = #F6F4EF, Zinc-700 = #1A1F1C, Zinc-900 = Hub-Blau)
|
||||
* Hub-Stil-Overrides: `[data-flux-sidebar]`, `[data-flux-navlist]`,
|
||||
`[data-flux-navlist-item]` mit Active-Strip links, `[data-flux-button]`
|
||||
Primary/Filled auf Hub-Blau, `[data-flux-card]` Buchpapier
|
||||
* Dark-Mode-Block bleibt vorerst minimal (nur Accent), Vollumstellung
|
||||
in Phase 5
|
||||
- `resources/views/partials/head.blade.php` — Bunny-Font auf
|
||||
`inter-tight + jetbrains-mono + source-serif-4` (für Brand-Mark)
|
||||
- `resources/views/components/layouts/app/sidebar.blade.php`:
|
||||
* `class="dark"` aus `<html>` entfernt
|
||||
* `<body>` jetzt `bg-bg text-ink antialiased` (Hub-Buchpapier)
|
||||
* `<x-app-logo>` ersetzt durch `<x-web.brand-mark brand="pressekonto"
|
||||
:serif="false" />` + Eyebrow "Publisher · Hub" — sowohl im Desktop-
|
||||
Brand-Block als auch im Mobile-Header
|
||||
* Testmodus-Block (Impersonation) komplett im Hub-Stil: dunkles
|
||||
Hub-Blau-Panel mit Bernstein-Eyebrow „Testmodus aktiv", weiße CTA
|
||||
„Zurück zum Admin" (statt Amber-Warnfarbe wie zuvor)
|
||||
* Resources-Block (Tailwind/HeroIcons/Flux/Repository) entfernt —
|
||||
gehört nicht ins Live-Portal
|
||||
- `resources/views/components/layouts/app.blade.php`:
|
||||
* Customer-Banner („User Backend") visuell auf Hub-Stil: Hub-Soft-
|
||||
Hintergrund + Hub-Soft-2-Border, Hub-Blau-Pille mit Dot, Eyebrow
|
||||
„Firmenkontext" in Sperrschrift, Heading in Hub-Ink
|
||||
- **Dateien (Test-Anpassungen, weil rollen-basierter Redirect aus Login-Fix
|
||||
weitere Tests berührt hat)**:
|
||||
- `tests/Feature/Auth/AuthenticationTest.php` — Test-User auf
|
||||
`superAdmin()` umgestellt (sonst `canAccessAdmin() === false` →
|
||||
Redirect auf `/` statt `/dashboard`)
|
||||
- `tests/Feature/Auth/RegistrationTest.php` — `terms_accepted` im
|
||||
Formular gesetzt + Redirect-Erwartung auf `/` angepasst (frisch
|
||||
registrierte User haben keine Rolle → Fallback `/`)
|
||||
- **Build/Test**:
|
||||
- `npm run build:portal` → `portal: 409.03 kB` (vorher 408.89 kB,
|
||||
+0.14 kB). Unter dem 10 %-Akzeptanzkriterium (≤ 450 kB).
|
||||
- CSS-Inspektion bestätigt Phase-1-Tokens:
|
||||
* `--color-accent: var(--color-hub)` und `.dark { --color-accent:
|
||||
#6d8ad3 }` im `:root`
|
||||
* `--color-zinc-700: #1a1f1c` (Hub-Ink statt Zinc-Grau)
|
||||
* `--font-sans: "Inter Tight"` ohne Instrument-Sans-Vorlauf
|
||||
* Hub-Overrides im Output: `[data-flux-sidebar]{background:var(...)}`,
|
||||
`[data-flux-navlist-item][aria-current=page]{background:var(
|
||||
--color-hub-soft);...}`
|
||||
- Auth-Test-Suite: **0 zusätzliche Regressionen** verifiziert (8 fail
|
||||
/ 15 passed vor & nach Phase 1; verbleibende Failures sind pre-existing
|
||||
Domain-Mismatch- und CSRF-Issues im Test-Setup).
|
||||
- Smoke-Test: `/dashboard`, `/admin/me`, `/settings/profile`,
|
||||
`/admin/companies`, `/admin/press-releases` antworten alle HTTP 302
|
||||
→ `/login` (erwartetes Verhalten ohne Auth-Session via curl).
|
||||
- `vendor/bin/pint --dirty` → passed.
|
||||
- **Bewusst NICHT in Phase 1**:
|
||||
- **Topbar** mit Breadcrumb + Bridge-Row + Search + „Neue Mitteilung"-CTA
|
||||
aus dem Mockup → ist für Phase 2 (Customer-Dashboard) sinnvoller,
|
||||
weil die Topbar dort Page-Kontext (Breadcrumb-Titel) braucht. Aktuell
|
||||
nutzen wir FluxUI's Default-Layout ohne dedizierte Topbar.
|
||||
- Konto-Switcher als Sidebar-Header (Avatar + Name + Firma als
|
||||
Dropdown-Trigger oben statt unten) → das User-Menü unten bleibt
|
||||
vorerst FluxUI-Standard. Iterativ in Phase 2.
|
||||
- Dashboard-Inhalte (Stat-Cards, Hint-Cards) → Phase 2 & 3.
|
||||
- Listen-Pages → Phase 4 (automatisch durch Token-Bridge schon „okay").
|
||||
- **Beobachtungen**:
|
||||
- FluxUI-Navlist-Item-Selektor: `[aria-current="page"]` greift. Falls
|
||||
künftige FluxUI-Versionen `[data-current="true"]` statt
|
||||
`[aria-current="page"]` setzen, deckt der Override beides ab.
|
||||
- Vendor-Pfad in `partials/head.blade.php` bewusst nicht geändert —
|
||||
Vite-Setup mit `build/portal` bleibt wie gehabt.
|
||||
- **Nächster Schritt**: **Review-Stopp**. Bei Freigabe → entweder
|
||||
Phase 2 (Customer-Dashboard-Stat-Cards & Hint-Cards) oder Topbar-
|
||||
Iteration. Entscheidung bei dir/Frank nach Anschauen.
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-19 · Login-Funktionsfix · Doppelte Alpine-Instanz
|
||||
|
||||
- **Was**: Nach Phase 0 trat ein funktionaler Bug auf: Login-Form ging
|
||||
nicht durch, Browser-Logs zeigten „Detected multiple instances of
|
||||
Alpine running" und „Livewire: published assets out of date".
|
||||
- **Ursache**: Das Hub-Auth-Layout (`pressekonto.blade.php`) lud sowohl
|
||||
`resources/js/app.js` (das `Alpine.start()` aufruft) als auch
|
||||
`@livewireScripts` (das Alpine intern mitbringt). Zwei Alpine-Instanzen
|
||||
→ `wire:submit`, `x-data`, `wire:model` brachen.
|
||||
- **Fixes**:
|
||||
- `app.js` aus `@vite([…])` im Auth-Layout entfernt — Alpine kommt nun
|
||||
nur noch über Livewire.
|
||||
- `php artisan livewire:publish --assets` → `livewire.js` von 450 kB
|
||||
auf 552 kB aktualisiert (Versions-Mismatch behoben).
|
||||
- Login/Register-Redirect auf rollen-basierte Logik umgestellt
|
||||
(Admin → `/dashboard`, Customer → `/admin/me`, sonst `/`) und
|
||||
`navigate: true` entfernt — SPA-Navigation kann den Wechsel zwischen
|
||||
Web-Build (Hub-Auth) und Portal-Build (FluxUI-Dashboard) nicht
|
||||
handhaben.
|
||||
- **Dateien**:
|
||||
- `resources/views/components/layouts/auth/pressekonto.blade.php`
|
||||
- `resources/views/livewire/auth/login.blade.php`
|
||||
- `resources/views/livewire/auth/register.blade.php`
|
||||
- **Verifikation**: Login funktioniert (User bestätigt), Redirect zum
|
||||
Dashboard läuft, keine Browser-Warnings mehr.
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-19 · Phase 0 abgeschlossen · Token-Unifizierung
|
||||
|
||||
- **Was**: Single Source of Truth für Design-Tokens etabliert. Web- und
|
||||
Portal-Build importieren beide `resources/css/shared/design-tokens.css`.
|
||||
Visuelle Unverändertheit verifiziert — FluxUI-Defaults gewinnen im
|
||||
Portal weiterhin (Instrument Sans, Zinc, `#3ea3dc`), wird in Phase 1
|
||||
abgelöst.
|
||||
- **Dateien**:
|
||||
- `resources/css/shared/design-tokens.css` **neu** — alle Hub-Tokens
|
||||
plus Status-Reihe (`--color-warn`, `--color-warn-soft`,
|
||||
`--color-err`, `--color-err-soft`, `--color-ok-soft`), Bridge-Dots
|
||||
(`--color-bridge-presseecho`, `--color-bridge-businessportal`),
|
||||
Radii (`--radius-xs/sm/md/lg`) und Schatten (`--shadow-soft`,
|
||||
`--shadow-auth`). Dark-Mode-Block als auskommentierter Vorgriff.
|
||||
- `resources/css/web/shared-styles.css` — Token-Import nach
|
||||
`tailwindcss` ergänzt. Wirkt damit für alle Web-Themes
|
||||
(pressekonto, presseecho, businessportal24).
|
||||
- `resources/css/web/theme-pressekonto.css` — kompletter `@theme {}`-
|
||||
Block entfernt (lebt jetzt in der Token-Datei). HSL-Legacy-Variablen
|
||||
und `@layer components { … }` blieben unverändert.
|
||||
- `resources/css/portal.css` — Token-Datei importiert, FluxUI-Setup
|
||||
mit Zinc + Instrument Sans bleibt vorerst dominant. Phase 1 löst
|
||||
es ab.
|
||||
- **Build/Test**:
|
||||
- `npm run build:web` → `theme-pressekonto: 193 kB` (vorher 189 kB,
|
||||
+4 kB für neu gebridgde Status-Tokens), `theme-presseecho: 189 kB`,
|
||||
`theme-businessportal24: 189 kB`.
|
||||
- `npm run build:portal` → `portal: 408 kB` (vorher 397 kB, +12 kB
|
||||
für die zusätzlich verfügbaren Hub-Tokens als CSS-Vars im `:root`).
|
||||
Wird in Phase 1 wieder egalisiert, wenn das Zinc-Setup wegfällt.
|
||||
- Smoke-Test: `pressekonto.test/`, `/login`, `/register`,
|
||||
`/dashboard` (302→login), `presseecho.test/`, `businessportal24.test/`
|
||||
— alle HTTP 200/302 wie erwartet.
|
||||
- CSS-Inspektion: `--color-hub: #1a2540` und `--color-warn: #a87a1f`
|
||||
sind in `theme-pressekonto-*.css` enthalten. Im Portal-Build
|
||||
bleibt `--font-sans: "Instrument Sans"` — bestätigt visuelle
|
||||
Unverändertheit.
|
||||
- `vendor/bin/pint --dirty` → passed.
|
||||
- **Offene Fragen / Beobachtungen**:
|
||||
- Weil Tailwind v4 alle in `@theme {}` definierten Tokens als CSS-
|
||||
Variablen im `:root` ausgibt (auch ohne Utility-Verwendung), wird
|
||||
der Portal-CSS-Wuchs in Phase 1 nicht komplett zurückgehen —
|
||||
die Hub-Tokens bleiben als Variablen verfügbar. Der Nettoeffekt
|
||||
nach Phase 1 sollte ähnlich bleiben (+/- 5–10 kB).
|
||||
- Dark-Mode-Block in `design-tokens.css` ist auskommentiert
|
||||
vorbereitet, aber bewusst noch nicht aktiv (Phase 5).
|
||||
- **Nächster Schritt**: **Review-Stopp**. Bei Freigabe →
|
||||
Phase 1 starten: Portal-Shell (Sidebar, Topbar, Layout-Container)
|
||||
auf Hub-Style mappen, Zinc-Palette ablösen, FluxUI-Overrides via
|
||||
`[data-flux-*]`-Selektoren in `portal.css`. Details:
|
||||
`02-PHASE-1-PORTAL-SHELL.md`.
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-19 · Setup
|
||||
|
||||
- **Was**: Architektur-Entscheidung getroffen → **Plan B** (Tokens teilen,
|
||||
Komponenten getrennt lassen). Dokumentation in `dev/frontend/hub-flux/`
|
||||
angelegt:
|
||||
- `README.md` — Übersicht & Entscheidung
|
||||
- `01-PHASE-0-TOKENS.md` — Detail-Plan für Tokens-Vereinheitlichung
|
||||
- `02-PHASE-1-PORTAL-SHELL.md` — Detail-Plan für Portal-Shell-Refresh
|
||||
- `03-WEITERE-PHASEN.md` — Outline Phase 2–6
|
||||
- `PROGRESS.md` — dieses Log
|
||||
- **Bestätigt**:
|
||||
- Hub-Blau als **Primärer Akzent** im Portal (klarer Kontrast zum hellen
|
||||
Buchpapier-Hintergrund)
|
||||
- Bernstein als **Sekundärer Akzent** (Notifications, Datenqualität,
|
||||
Akzent-Ribbons)
|
||||
- Brand bleibt **pressekonto** (auch im Portal-Sidebar via
|
||||
`<x-web.brand-mark>`)
|
||||
- Reihenfolge: erst Phase 0 + 1, dann Review
|
||||
- **Nächster Schritt**: Phase 0 umsetzen
|
||||
→ `resources/css/shared/design-tokens.css` erstellen,
|
||||
`theme-pressekonto.css` refactoren, `portal.css` minimal vorbereiten.
|
||||
|
||||
---
|
||||
|
||||
## Vorlauf (zur Erinnerung, was schon steht)
|
||||
|
||||
- **Hub-Landing** (`web/pressekonto.blade.php`) — fertig, lebt im Web-Build
|
||||
mit `theme-pressekonto.css`
|
||||
- **Hub-Auth** (Login, Register, Forgot, Reset, Verify, Confirm) — fertig
|
||||
im Hub-Design, neues Layout `auth/pressekonto.blade.php` im Web-Build
|
||||
- **Register-AGB-Checkbox** mit Server-Side-Validierung ergänzt
|
||||
- **Portal-Backend** mit FluxUI funktional vorhanden (Admin-Dashboard,
|
||||
Customer-Dashboard, 77 Blade-Dateien mit FluxUI), aber **visuell noch
|
||||
Starter-Kit-Look** (Zinc + `#3ea3dc` + Instrument Sans)
|
||||
|
||||
---
|
||||
|
||||
## Template für neue Einträge
|
||||
|
||||
```markdown
|
||||
## YYYY-MM-DD · Phase N · Kurztitel
|
||||
|
||||
- **Was**: [Was wurde konkret gemacht?]
|
||||
- **Dateien**: [Pfade]
|
||||
- **Build/Test**: [Wie verifiziert?]
|
||||
- **Offene Fragen**: [Falls etwas unklar geblieben ist]
|
||||
- **Nächster Schritt**: [Was als Nächstes ansteht]
|
||||
```
|
||||
143
dev/frontend/hub-flux/README.md
Normal file
143
dev/frontend/hub-flux/README.md
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
# Hub × FluxUI — Visuelle Vereinheitlichung Portal ↔ Hub
|
||||
|
||||
> **Ziel**: Das Portal-Backend (User-Panel, Admin-Bereich) wird visuell an den
|
||||
> Hub-Stil (`pressekonto.test`-Landing + Auth-Seiten) angeglichen, **ohne
|
||||
> FluxUI aufzugeben**. Die FluxUI-Komponenten werden über Design-Tokens und
|
||||
> CSS-Overrides ans Hub-Design adaptiert.
|
||||
|
||||
## Status
|
||||
|
||||
| Phase | Beschreibung | Status |
|
||||
|-------|--------------|--------|
|
||||
| 0 | Design-Tokens vereinheitlichen | **✅ abgeschlossen** (2026-05-19) |
|
||||
| 1 | Portal-Shell (Sidebar, Layout, Brand-Mark) | **✅ abgeschlossen** (2026-05-19) |
|
||||
| 2 | Customer-Dashboard auf Mockup-Stil (inkl. Topbar) | 🟡 wartet auf Freigabe |
|
||||
| 3 | Admin-Dashboard konsistent | ⚪ später |
|
||||
| 4 | Listen-/Detail-Pages | ⚪ iterativ |
|
||||
| 5 | Dark Mode konsistent | ⚪ später |
|
||||
| 6 | Auth-Konsolidierung (optional) | ⚪ optional |
|
||||
|
||||
→ Tagesaktueller Fortschritt: [`PROGRESS.md`](./PROGRESS.md)
|
||||
|
||||
## Dokumente in diesem Verzeichnis
|
||||
|
||||
| Datei | Zweck |
|
||||
|-------|-------|
|
||||
| [`README.md`](./README.md) | Diese Übersicht: Entscheidung, Architektur, Status |
|
||||
| [`01-PHASE-0-TOKENS.md`](./01-PHASE-0-TOKENS.md) | Phase 0: Tokens vereinheitlichen (aktiv) |
|
||||
| [`02-PHASE-1-PORTAL-SHELL.md`](./02-PHASE-1-PORTAL-SHELL.md) | Phase 1: Portal-Shell ans Hub-Design angleichen |
|
||||
| [`03-WEITERE-PHASEN.md`](./03-WEITERE-PHASEN.md) | Outline für Phasen 2–6 |
|
||||
| [`PROGRESS.md`](./PROGRESS.md) | Fortschritts-Log + Notizen |
|
||||
|
||||
## Visuelle Vorlagen
|
||||
|
||||
Maßgebliche Mockups für diese Arbeit:
|
||||
|
||||
- `dev/frontend/tailwind_v3/User Dashboard presseportale.html` — Light-Variante
|
||||
- `dev/frontend/tailwind_v3/User Dashboard presseportale Dark.html` — Dark-Variante
|
||||
- `dev/frontend/tailwind_v3/Login pressekonto A3 Tailwind.html` — Hub-Auth-Vorlage (bereits umgesetzt)
|
||||
- `dev/frontend/tailwind_v3/Hub Landing pressekonto-2.html` — Hub-Landing-Vorlage (bereits umgesetzt)
|
||||
|
||||
## Architektur-Entscheidung
|
||||
|
||||
### Plan B (gewählt) — Tokens teilen, Komponenten getrennt lassen
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ shared/design-tokens.css │
|
||||
│ (Hub-Blau, Bernstein, Buchpapier, Inter Tight, JetBrains Mono) │
|
||||
└────────────────┬────────────────────────┬───────────────────────┘
|
||||
│ │
|
||||
┌────────▼─────────┐ ┌─────────▼──────────┐
|
||||
│ portal.css │ │ theme-pressekonto │
|
||||
│ + FluxUI │ │ + shared-styles │
|
||||
│ (Backend) │ │ (Hub-Landing/Auth)│
|
||||
└──────────────────┘ └────────────────────┘
|
||||
build/portal build/web
|
||||
```
|
||||
|
||||
**Warum nicht alles in FluxUI?**
|
||||
- Die Hub-Landing lebt von Atmosphäre (konzentrische Kreise, Hub-Grid, eigene
|
||||
Eyebrows, Bridge-Cards), die mit FluxUI-Komponenten nicht 1:1 abbildbar ist
|
||||
- FluxUI würde im Hub-Frontend zur unnötigen Bundle-Last
|
||||
- Der gerade gebaute Hub-Login wäre verloren
|
||||
|
||||
**Warum nicht alles Custom?**
|
||||
- 77 Blade-Dateien nutzen FluxUI – ein Rewrite wäre Aufwand ohne Mehrwert
|
||||
- FluxUI-Tabellen, -Forms, -Modals, -Date-Picker sind im Backend Gold wert
|
||||
- FluxUI ist sehr gut **customizable** über CSS-Custom-Properties
|
||||
und `[data-flux-*]`-Selectoren
|
||||
|
||||
### Verworfen — Plan A (alles FluxUI) & Plan C (alles im Web-Build)
|
||||
|
||||
Siehe Chat-Historie für die Begründung. Kurz: zu viel Aufwand, zu wenig
|
||||
Mehrwert, würde bereits gelieferte Hub-Atmosphäre verschlechtern.
|
||||
|
||||
## Branding & Tokens
|
||||
|
||||
### Brand
|
||||
- **Marke**: pressekonto (verbindlich, für Portal und Hub)
|
||||
- **Wortmarke**: `<x-web.brand-mark brand="pressekonto" />` — auch im Portal
|
||||
- **Logo-Komponente** `<x-app-logo>` wird im Portal **abgelöst**
|
||||
|
||||
### Farben (Light)
|
||||
|
||||
| Token | Wert | Rolle |
|
||||
|-------|------|-------|
|
||||
| `--color-hub` | `#1A2540` | **Primärer Akzent** (Sidebar-Active, Primary-Buttons, Eyebrows) |
|
||||
| `--color-hub-2` | `#243152` | Hover-Stufe von `--color-hub` |
|
||||
| `--color-hub-3` | `#2E3D66` | Tertiäre Stufe (Dekoration auf dunklem Grund) |
|
||||
| `--color-hub-soft` | `#E5E9F1` | Active-Pill-Hintergrund in der Sidebar |
|
||||
| `--color-accent` | `#B07A3A` | **Sekundärer Akzent** (Bernstein – Notifications, Datenqualität, Empfehlungs-Ribbon) |
|
||||
| `--color-accent-deep` | `#8A5E27` | Akzent-Hover/Text |
|
||||
| `--color-bg` | `#F6F4EF` | Warmes Buchpapier — Haupt-Hintergrund |
|
||||
| `--color-bg-elev` | `#FBFAF6` | Elevation 1 (Sidebar, Topbar, leichte Cards) |
|
||||
| `--color-bg-card` | `#FFFFFF` | Reine Cards |
|
||||
| `--color-bg-rule` | `#E2DDD0` | Standard-Trennlinien |
|
||||
| `--color-ink` | `#1A1F1C` | Primärtext |
|
||||
| `--color-ink-2` | `#3A413D` | Sekundärtext (Begleittexte) |
|
||||
| `--color-ink-3` | `#5A6360` | Meta, Labels |
|
||||
| `--color-ink-4` | `#8A918D` | Disabled, Hintergrund-Meta |
|
||||
|
||||
### Farben (Dark) — laut Mockup
|
||||
Wird in Phase 5 ausgearbeitet. Hub-Blau wird heller (`#5A78C2`), Akzent
|
||||
wärmer (`#D9A560`), Hintergrund tief Anthrazit (`#0E1218`). Wichtig: die
|
||||
Token-Namen bleiben gleich, nur die Werte tauschen — keine doppelte
|
||||
UI-Pflege.
|
||||
|
||||
### Typografie
|
||||
|
||||
| Token | Wert |
|
||||
|-------|------|
|
||||
| `--font-sans` | `"Inter Tight", Inter, system-ui, sans-serif` |
|
||||
| `--font-mono` | `"JetBrains Mono", "SF Mono", ui-monospace, monospace` |
|
||||
| `--font-serif` | `"Source Serif 4", Georgia, serif` (nur für Brand-Mark) |
|
||||
|
||||
Aktuell im Portal: **Instrument Sans** → wird in Phase 1 abgelöst.
|
||||
|
||||
## Konventionen
|
||||
|
||||
- **Light Mode** ist Default. `class="dark"` auf `<html>` wird entfernt.
|
||||
- Dark Mode wird via `prefers-color-scheme` + Flux Appearance-Switcher
|
||||
gesteuert. Token-Layer übernimmt die Umschaltung.
|
||||
- Token-Names sind **identisch** zwischen Hub-Theme und Portal-Theme.
|
||||
Sowohl `bg-bg-elev` als auch `bg-hub-soft` funktionieren in beiden Welten.
|
||||
- FluxUI-Komponenten werden **nicht überschrieben**, sondern über
|
||||
`[data-flux-*]`-Selectoren in `portal.css` ergänzt — minimaler Eingriff,
|
||||
maximale Kompatibilität mit FluxUI-Updates.
|
||||
|
||||
## Was wir bewusst NICHT machen
|
||||
|
||||
- **Kein FluxUI-Rewrite**. Nur Token-Anpassung + Selektor-Overrides.
|
||||
- **Keine zweite UI-Pflege für Dark Mode**. Tokens-only-Ansatz.
|
||||
- **Kein Verschmelzen der Builds** (Phase 6 optional).
|
||||
- **Keine Änderung an Volt-/Livewire-Logik** in dieser Arbeit.
|
||||
|
||||
## Wer ändert was
|
||||
|
||||
- **Diese Doku** wird mit jeder Phase aktualisiert. `PROGRESS.md` enthält
|
||||
die Tages-Notes.
|
||||
- Code-Änderungen sind kleinteilig und werden in eigenen Commits gebündelt
|
||||
(`hub-flux: phase 0 — tokens vereinheitlicht`, etc.).
|
||||
- Bei Unklarheiten/Entscheidungen: kurz in `PROGRESS.md` festhalten,
|
||||
damit nachvollziehbar bleibt warum etwas so und nicht anders.
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>presseportale.com — Publisher-Hub</title>
|
||||
<title>pressekonto.de — Publisher-Hub</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
|
|
@ -163,7 +163,7 @@
|
|||
|
||||
<!-- Wortmark: schlicht, kein Symbol — wirkt im Verlags-Segment seriöser -->
|
||||
<a href="#" class="flex items-baseline gap-2.5">
|
||||
<span class="text-[24px] font-bold tracking-[-0.5px] text-hub leading-none">presseportale<span class="text-accent">.com</span></span>
|
||||
<span class="text-[24px] font-bold tracking-[-0.5px] text-hub leading-none">pressekonto<span class="text-accent">.com</span></span>
|
||||
<span class="hidden md:inline-block w-px h-[18px] bg-bg-rule"></span>
|
||||
<span class="eyebrow muted text-[9.5px] tracking-[0.22em]">Publisher · Hub</span>
|
||||
</a>
|
||||
|
|
@ -230,7 +230,7 @@
|
|||
</h1>
|
||||
|
||||
<p class="text-[17px] leading-[1.55] text-ink-2 mt-7 m-0 max-w-[560px]">
|
||||
presseportale.com ist der gemeinsame Publisher-Bereich für unsere beiden Pressefachportale. Pressemitteilungen schreiben, redaktionell prüfen lassen, auf beiden Reichweiten veröffentlichen — und Reichweite, Empfänger und Abrechnung an einem Ort verwalten.
|
||||
pressekonto.de ist der gemeinsame Publisher-Bereich für unsere beiden Pressefachportale. Pressemitteilungen schreiben, redaktionell prüfen lassen, auf beiden Reichweiten veröffentlichen — und Reichweite, Empfänger und Abrechnung an einem Ort verwalten.
|
||||
</p>
|
||||
|
||||
<div class="mt-8 flex items-center gap-3">
|
||||
|
|
@ -290,7 +290,7 @@
|
|||
<div class="absolute" style="top:50%;left:50%;transform:translate(-50%,-50%);">
|
||||
<div class="w-[140px] h-[140px] rounded-full bg-hub-grad flex flex-col items-center justify-center text-white shadow-lg shadow-hub/30">
|
||||
<div class="text-[10px] font-bold tracking-[0.22em] uppercase text-hub-line">Hub</div>
|
||||
<div class="text-[15px] font-bold tracking-[-0.3px] mt-1">presseportale</div>
|
||||
<div class="text-[15px] font-bold tracking-[-0.3px] mt-1">pressekonto</div>
|
||||
<div class="text-[10px] text-white/55 tracking-[0.18em] uppercase mt-0.5">.com</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -767,15 +767,15 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============== HINTER PRESSEPORTALE — PLATTFORM-FAMILIE ============== -->
|
||||
<!-- ============== HINTER PRESSEKONTO — PLATTFORM-FAMILIE ============== -->
|
||||
<section id="familie" class="max-w-layout mx-auto px-8 pt-20 pb-16">
|
||||
<header class="mb-10">
|
||||
<div class="section-eyebrow mb-4">Hinter presseportale.com</div>
|
||||
<div class="section-eyebrow mb-4">Hinter pressekonto.de</div>
|
||||
<h2 class="text-[32px] font-bold m-0 tracking-[-0.6px] text-ink leading-[1.18] max-w-[820px]">
|
||||
Zwei eigenständige Pressefachportale. Eine kuratierte Verlags-Familie.
|
||||
</h2>
|
||||
<p class="text-[14.5px] text-ink-2 leading-[1.55] m-0 mt-4 max-w-[760px]">
|
||||
presseportale.com ist nicht „irgendein Tool" — es ist die zentrale Plattform für unsere beiden redaktionell geführten Pressefachportale. Jedes Portal hat einen eigenen Charakter, eigene Leserschaft und eigene Themen-Schwerpunkte.
|
||||
pressekonto.de ist nicht „irgendein Tool" — es ist die zentrale Plattform für unsere beiden redaktionell geführten Pressefachportale. Jedes Portal hat einen eigenen Charakter, eigene Leserschaft und eigene Themen-Schwerpunkte.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
|
|
@ -856,7 +856,7 @@
|
|||
<path d="M9 12h6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<div class="text-[13px] leading-[1.55]">
|
||||
Pressemitteilungen, die Sie über presseportale.com einreichen, erscheinen auf <strong class="text-white font-semibold">beiden Portalen</strong> — ohne Aufpreis, ohne doppelte Eingabe. Sie haben eine zentrale Mitteilungs-Verwaltung und eine zentrale Reichweiten-Statistik.
|
||||
Pressemitteilungen, die Sie über pressekonto.de einreichen, erscheinen auf <strong class="text-white font-semibold">beiden Portalen</strong> — ohne Aufpreis, ohne doppelte Eingabe. Sie haben eine zentrale Mitteilungs-Verwaltung und eine zentrale Reichweiten-Statistik.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -876,7 +876,7 @@
|
|||
<!-- Fließtext-Nennung — wie auf den Brand-Portalen, kein Logo-Grid -->
|
||||
<div>
|
||||
<p class="text-[16px] leading-[1.7] text-ink-2 m-0">
|
||||
Über presseportale.com veröffentlichen unter anderem
|
||||
Über pressekonto.de veröffentlichen unter anderem
|
||||
<a class="text-hub font-semibold underline underline-offset-[3px] decoration-hub/25 hover:decoration-hub" href="#">Siemens AG</a>,
|
||||
<a class="text-hub font-semibold underline underline-offset-[3px] decoration-hub/25 hover:decoration-hub" href="#">BASF SE</a>,
|
||||
<a class="text-hub font-semibold underline underline-offset-[3px] decoration-hub/25 hover:decoration-hub" href="#">Deutsche Bank</a>,
|
||||
|
|
@ -1069,7 +1069,7 @@
|
|||
|
||||
<div>
|
||||
<div class="text-[24px] font-bold leading-none tracking-[-0.5px] text-white">
|
||||
presseportale<span style="color:#B07A3A;">.com</span>
|
||||
pressekonto<span style="color:#B07A3A;">.com</span>
|
||||
</div>
|
||||
<div class="eyebrow on-dark mt-2 text-[9.5px] tracking-[0.22em]" style="color:#7B8FBF;">
|
||||
Publisher · Hub
|
||||
|
|
@ -1121,7 +1121,7 @@
|
|||
|
||||
<div class="border-t border-white/15">
|
||||
<div class="max-w-layout mx-auto px-8 py-5 flex items-center justify-between gap-4 text-[11.5px] text-white/55">
|
||||
<span>© 2026 presseportale.com · Alle Rechte vorbehalten</span>
|
||||
<span>© 2026 pressekonto.de · Alle Rechte vorbehalten</span>
|
||||
<span class="flex items-center gap-2 font-mono text-[10.5px]">
|
||||
<span class="inline-block w-1.5 h-1.5 rounded-full bg-ok"></span>
|
||||
Alle Systeme betriebsbereit
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>presseportale.com — Anmelden</title>
|
||||
<title>pressekonto.de — Anmelden</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
|
|
@ -160,7 +160,7 @@
|
|||
<header class="relative z-10 px-10 py-[22px] flex items-center justify-between">
|
||||
<a href="#" class="flex items-baseline gap-2.5 no-underline">
|
||||
<span class="text-[19px] font-bold tracking-[-0.4px] text-hub leading-none">
|
||||
presseportale<span class="text-accent">.com</span>
|
||||
pressekonto<span class="text-accent">.com</span>
|
||||
</span>
|
||||
<span class="inline-block w-px h-[14px] bg-bg-rule"></span>
|
||||
<span class="text-[9.5px] font-bold tracking-[0.22em] uppercase text-ink-3">
|
||||
767
dev/frontend/tailwind_v3/User Dashboard presseportale Dark.html
Normal file
767
dev/frontend/tailwind_v3/User Dashboard presseportale Dark.html
Normal file
|
|
@ -0,0 +1,767 @@
|
|||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>presseportale.com — Mein Dashboard · Dark</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
|
||||
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
/* DARK MODE — Tokens umgewidmet, gleiche Namen wie Light-Variante */
|
||||
bg: "#0E1218",
|
||||
"bg-elev": "#14181F",
|
||||
"bg-rule": "#2A3142",
|
||||
"bg-rule-2": "#232838",
|
||||
"bg-card": "#181D27",
|
||||
"bg-card-warm": "#1F1A12",
|
||||
"hub": "#5A78C2",
|
||||
"hub-2": "#6D8AD3",
|
||||
"hub-3": "#4A65A8",
|
||||
"hub-soft": "#1F2A47",
|
||||
"hub-soft-2":"#2C3A5D",
|
||||
"hub-line": "#7B8FBF",
|
||||
"accent": "#D9A560",
|
||||
"accent-deep": "#B07A3A",
|
||||
"accent-soft": "#2A2418",
|
||||
ink: "#ECE9E0",
|
||||
"ink-2":"#C9C5B8",
|
||||
"ink-3":"#8E8B82",
|
||||
"ink-4":"#5D5C57",
|
||||
"ink-on-dark": "#F6F4EF",
|
||||
"ink-on-dark-2": "#B2B9C7",
|
||||
"ink-on-dark-3": "#7B8FBF",
|
||||
ok: "#4DC076",
|
||||
"ok-soft":"#1A2D22",
|
||||
warn: "#D9A560",
|
||||
"warn-soft":"#2D2418",
|
||||
err: "#E07664",
|
||||
"err-soft":"#2E1715",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['"Inter Tight"','Inter','system-ui','sans-serif'],
|
||||
mono: ['"JetBrains Mono"','"SF Mono"','ui-monospace','monospace'],
|
||||
},
|
||||
backgroundImage: {
|
||||
"hub-grad": "linear-gradient(135deg,#1A2540 0%,#243152 100%)",
|
||||
"hub-grad-2": "linear-gradient(180deg,#1A2540 0%,#0F1729 100%)",
|
||||
"accent-grad": "linear-gradient(135deg,#B07A3A 0%,#8A5E27 100%)",
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html { color-scheme: dark; }
|
||||
html,body { margin:0; padding:0; }
|
||||
body { background:#0A0D12; font-family:"Inter Tight",system-ui,sans-serif; }
|
||||
|
||||
.eyebrow {
|
||||
font-size: 10.5px; font-weight: 700;
|
||||
letter-spacing: 0.20em; text-transform: uppercase;
|
||||
color: #B2B9C7;
|
||||
}
|
||||
.eyebrow.muted { color:#8E8B82; letter-spacing:0.16em; font-weight:600; font-size:10px; }
|
||||
.eyebrow.accent { color:#D9A560; }
|
||||
.eyebrow.on-dark { color:#7B8FBF; }
|
||||
|
||||
.section-eyebrow {
|
||||
display:inline-flex; align-items:center; gap:10px;
|
||||
font-size: 10.5px; font-weight: 700;
|
||||
letter-spacing: 0.22em; text-transform: uppercase;
|
||||
color:#B2B9C7;
|
||||
}
|
||||
.section-eyebrow::after {
|
||||
content:""; display:block; width:30px; height:1px;
|
||||
background:#B2B9C7; opacity:.35;
|
||||
}
|
||||
|
||||
.rule { height:1px; background:#2A3142; border:0; margin:0; }
|
||||
.rule-strong { height:1px; background:#ECE9E0; border:0; margin:0; }
|
||||
|
||||
/* Sidebar */
|
||||
.nav-item {
|
||||
display:flex; align-items:center; gap:11px;
|
||||
padding:8px 12px;
|
||||
border-radius:4px;
|
||||
font-size:13px; font-weight:500;
|
||||
color:#C9C5B8;
|
||||
transition: background .12s, color .12s;
|
||||
position:relative;
|
||||
}
|
||||
.nav-item:hover { background:#1F2A47; color:#ECE9E0; }
|
||||
.nav-item.active {
|
||||
background:#1F2A47;
|
||||
color:#ECE9E0;
|
||||
font-weight:600;
|
||||
}
|
||||
.nav-item.active::before {
|
||||
content:""; position:absolute; left:-1px; top:6px; bottom:6px;
|
||||
width:2px; background:#7B8FBF; border-radius:0 2px 2px 0;
|
||||
}
|
||||
.nav-item.disabled { color:#5D5C57; cursor:default; }
|
||||
.nav-item.disabled:hover { background:transparent; color:#5D5C57; }
|
||||
.nav-item .ico {
|
||||
width:16px; height:16px; flex-shrink:0;
|
||||
color: currentColor; opacity:.8;
|
||||
}
|
||||
.nav-item.active .ico { opacity:1; }
|
||||
|
||||
.nav-section {
|
||||
font-size:10px; font-weight:700; letter-spacing:0.18em;
|
||||
text-transform:uppercase; color:#5D5C57;
|
||||
padding: 0 12px 6px;
|
||||
}
|
||||
|
||||
/* Card / panel headers */
|
||||
.panel {
|
||||
background:#181D27; border:1px solid #2A3142; border-radius:6px;
|
||||
}
|
||||
.panel-warm { background:#14181F; border:1px solid #2A3142; border-radius:6px; }
|
||||
.panel-dark { background:#0F1729; border:1px solid #1A2540; border-radius:6px; color:#F6F4EF; }
|
||||
.panel-head {
|
||||
display:flex; align-items:center; justify-content:space-between;
|
||||
padding:14px 20px;
|
||||
border-bottom:1px solid #2A3142;
|
||||
}
|
||||
.panel-dark .panel-head { border-bottom-color: rgba(255,255,255,.08); }
|
||||
|
||||
/* Stat cards */
|
||||
.stat-card {
|
||||
position:relative;
|
||||
background:#181D27; border:1px solid #2A3142; border-radius:6px;
|
||||
padding:18px 20px;
|
||||
transition: border-color .12s, box-shadow .12s;
|
||||
}
|
||||
.stat-card .stat-strip {
|
||||
position:absolute; left:0; top:0; bottom:0; width:3px;
|
||||
background:#2C3A5D; border-radius:6px 0 0 6px;
|
||||
}
|
||||
.stat-card.is-primary .stat-strip { background:#5A78C2; }
|
||||
.stat-card.is-ok .stat-strip { background:#4DC076; }
|
||||
.stat-card.is-warn .stat-strip { background:#D9A560; }
|
||||
.stat-card.is-muted .stat-strip { background:#5D5C57; }
|
||||
|
||||
.stat-label {
|
||||
font-size:10.5px; font-weight:700;
|
||||
letter-spacing:0.16em; text-transform:uppercase;
|
||||
color:#8E8B82;
|
||||
}
|
||||
.stat-card.is-ok .stat-label { color:#4DC076; }
|
||||
.stat-card.is-warn .stat-label { color:#D9A560; }
|
||||
.stat-card.is-muted .stat-label { color:#8E8B82; }
|
||||
|
||||
.stat-num {
|
||||
font-family:"JetBrains Mono","SF Mono",ui-monospace,monospace;
|
||||
font-variant-numeric:tabular-nums;
|
||||
font-size:36px; font-weight:600; color:#ECE9E0;
|
||||
letter-spacing:-0.5px; line-height:1;
|
||||
margin-top:14px;
|
||||
}
|
||||
|
||||
/* Hint chips (Datenqualität) */
|
||||
.hint-card {
|
||||
display:grid; gap:14px;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items:start;
|
||||
background:#1F1A12; border:1px solid #2D2418; border-left:3px solid #B07A3A;
|
||||
border-radius:5px;
|
||||
padding:16px 18px;
|
||||
}
|
||||
.hint-card .hint-ico {
|
||||
width:36px; height:36px; border-radius:4px;
|
||||
background:#2D2418; color:#D9A560;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
flex-shrink:0;
|
||||
}
|
||||
|
||||
/* FluxUI-style buttons */
|
||||
.btn-primary {
|
||||
display:inline-flex; align-items:center; gap:8px; justify-content:center;
|
||||
padding:9px 16px;
|
||||
background:#5A78C2; color:#FFFFFF;
|
||||
border-radius:4px; font-size:13px; font-weight:600;
|
||||
transition: background .15s;
|
||||
}
|
||||
.btn-primary:hover { background:#6D8AD3; }
|
||||
.btn-secondary {
|
||||
display:inline-flex; align-items:center; gap:8px; justify-content:center;
|
||||
padding:8px 14px;
|
||||
background:#181D27; color:#ECE9E0;
|
||||
border:1px solid #2A3142; border-radius:4px;
|
||||
font-size:12.5px; font-weight:600;
|
||||
transition: border-color .15s, background .15s;
|
||||
}
|
||||
.btn-secondary:hover { border-color:#5A78C2; background:#1F2A47; }
|
||||
|
||||
.badge {
|
||||
display:inline-flex; align-items:center; gap:6px;
|
||||
padding:3px 9px; border-radius:99px;
|
||||
font-size:10.5px; font-weight:700;
|
||||
letter-spacing:0.10em; text-transform:uppercase;
|
||||
}
|
||||
.badge.warn { background:#2D2418; color:#D9A560; }
|
||||
.badge.ok { background:#1A2D22; color:#4DC076; }
|
||||
.badge.hub { background:#1F2A47; color:#B2B9C7; }
|
||||
.badge.dot::before {
|
||||
content:""; width:6px; height:6px; border-radius:99px; background:currentColor;
|
||||
}
|
||||
|
||||
/* Bridge ribbon - sehr subtil */
|
||||
.bridge-row {
|
||||
display:inline-flex; align-items:center; gap:6px;
|
||||
font-family:"JetBrains Mono","SF Mono",ui-monospace,monospace;
|
||||
font-size:10.5px; letter-spacing:0.10em; text-transform:uppercase;
|
||||
color:#8E8B82;
|
||||
}
|
||||
.dot-pe { width:6px; height:6px; border-radius:99px; background:#4DA37A; }
|
||||
.dot-bp { width:6px; height:6px; border-radius:99px; background:#E36340; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-bg text-ink font-sans antialiased">
|
||||
|
||||
<!-- ============== ARTBOARD ============== -->
|
||||
<div class="mx-auto bg-bg" style="width:1440px;">
|
||||
|
||||
<div class="flex" style="min-height:980px;">
|
||||
|
||||
<!-- =============================================
|
||||
SIDEBAR — schmal, warm, klare Hierarchie
|
||||
(FluxUI <flux:navlist> + Slot-Header-Styles)
|
||||
============================================== -->
|
||||
<aside class="bg-bg-elev border-r border-bg-rule flex flex-col" style="width:260px;">
|
||||
|
||||
<!-- Brand-Block -->
|
||||
<div class="px-5 pt-6 pb-5">
|
||||
<a href="Hub Landing presseportale.html" class="flex items-baseline gap-2">
|
||||
<span class="text-[19px] font-bold tracking-[-0.4px] text-hub leading-none">presseportale<span class="text-accent">.com</span></span>
|
||||
</a>
|
||||
<div class="eyebrow muted mt-2">Publisher · Hub</div>
|
||||
|
||||
<!-- Aktive Firma / Konto-Switcher (FluxUI-Slot: header before nav) -->
|
||||
<button class="mt-4 w-full grid items-center gap-2.5 px-3 py-2.5 bg-bg-card border border-bg-rule rounded-[4px] hover:border-hub/40 text-left"
|
||||
style="grid-template-columns:auto 1fr auto;">
|
||||
<span class="w-7 h-7 rounded-[3px] bg-hub-soft border border-hub-soft-2 flex items-center justify-center text-hub text-[11px] font-bold">TU</span>
|
||||
<span class="min-w-0">
|
||||
<span class="block text-[12.5px] font-semibold text-ink leading-tight truncate">Test User</span>
|
||||
<span class="block text-[10.5px] text-ink-3 leading-tight mt-0.5 truncate">Keine Firma zugeordnet</span>
|
||||
</span>
|
||||
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" class="text-ink-3">
|
||||
<path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 7.5l3-3 3 3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" opacity="0.4"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="px-3 flex-1">
|
||||
<div class="nav-section">Mein Bereich</div>
|
||||
<div class="space-y-0.5 mb-5">
|
||||
<a class="nav-item active" href="#">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M2 7l6-5 6 5v7H2z" stroke="currentColor" stroke-width="1.4"/><path d="M6 14V9h4v5" stroke="currentColor" stroke-width="1.4"/></svg>
|
||||
Übersicht
|
||||
</a>
|
||||
<a class="nav-item" href="#">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="2.5" width="9" height="11" stroke="currentColor" stroke-width="1.4"/><path d="M11.5 5h2v8.5H4" stroke="currentColor" stroke-width="1.4"/><path d="M5 5.5h4M5 8h4M5 10.5h2.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
|
||||
Meine Pressemitteilungen
|
||||
<span class="badge hub ml-auto" style="font-size:9.5px;padding:1px 6px;letter-spacing:0.08em;">0</span>
|
||||
</a>
|
||||
<a class="nav-item" href="#">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3.5" width="11" height="10" stroke="currentColor" stroke-width="1.4"/><path d="M2.5 6h11" stroke="currentColor" stroke-width="1.4"/><path d="M6 9h1M9 9h1M6 11.5h1M9 11.5h1" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
Firmen
|
||||
</a>
|
||||
<a class="nav-item" href="#">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 5h10l-1 9H4z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M6 5V3.5a2 2 0 014 0V5" stroke="currentColor" stroke-width="1.4"/></svg>
|
||||
Buchungen & Add-ons
|
||||
</a>
|
||||
<span class="nav-item disabled">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 13V8M7 13V5M11 13V9" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
Statistiken
|
||||
<span class="ml-auto text-[9.5px] tracking-[0.14em] uppercase font-semibold text-ink-4">bald</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">Finanzen</div>
|
||||
<div class="space-y-0.5 mb-5">
|
||||
<span class="nav-item disabled">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="5" stroke="currentColor" stroke-width="1.4"/><path d="M8 5.5v5M6 8h4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
Credits & Tarif
|
||||
<span class="ml-auto text-[9.5px] tracking-[0.14em] uppercase font-semibold text-ink-4">bald</span>
|
||||
</span>
|
||||
<a class="nav-item" href="#">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 2.5h7l3 3v8H3z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M10 2.5V5.5h3" stroke="currentColor" stroke-width="1.4"/><path d="M5.5 8h5M5.5 10.5h5M5.5 6h2" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
|
||||
Rechnungen
|
||||
</a>
|
||||
<span class="nav-item disabled">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="4.5" width="11" height="7.5" stroke="currentColor" stroke-width="1.4"/><path d="M2.5 7h11" stroke="currentColor" stroke-width="1.4"/></svg>
|
||||
Zahlungsarten
|
||||
<span class="ml-auto text-[9.5px] tracking-[0.14em] uppercase font-semibold text-ink-4">bald</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">Konto</div>
|
||||
<div class="space-y-0.5 mb-5">
|
||||
<a class="nav-item" href="#">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="6" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M3 13.5c.7-2.4 2.7-4 5-4s4.3 1.6 5 4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
Profil
|
||||
</a>
|
||||
<a class="nav-item" href="#">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M8 2l5 2v4c0 3-2 5-5 6-3-1-5-3-5-6V4z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M6 8l1.5 1.5L10.5 6" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
Sicherheit
|
||||
</a>
|
||||
<a class="nav-item" href="#">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><circle cx="6" cy="8" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M8.5 8h5M11 8v2.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
API & Integrationen
|
||||
</a>
|
||||
<span class="nav-item disabled">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3.5 7a4.5 4.5 0 119 0v3.5l1 1.5H2.5l1-1.5z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M6.5 13a1.5 1.5 0 003 0" stroke="currentColor" stroke-width="1.4"/></svg>
|
||||
Benachrichtigungen
|
||||
<span class="ml-auto text-[9.5px] tracking-[0.14em] uppercase font-semibold text-ink-4">bald</span>
|
||||
</span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Testmodus-Block (war im Original „Zurück zum Admin") -->
|
||||
<div class="px-4 pb-4">
|
||||
<div class="text-ink-on-dark rounded-[5px] p-4 relative overflow-hidden" style="background:linear-gradient(135deg,#1A2540 0%,#243152 100%); border:1px solid #2A3142;">
|
||||
<div class="absolute -top-6 -right-6 w-16 h-16 rounded-full opacity-50" style="background:#2E3D66;"></div>
|
||||
<div class="absolute -bottom-8 -left-8 w-20 h-20 rounded-full opacity-30" style="background:#2E3D66;"></div>
|
||||
<div class="relative">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="w-1.5 h-1.5 rounded-full animate-pulse" style="background:#D9A560;"></span>
|
||||
<span class="eyebrow on-dark" style="color:#F4D89C;">Testmodus aktiv</span>
|
||||
</div>
|
||||
<div class="text-[12px] leading-[1.5] text-ink-on-dark-2">
|
||||
Angemeldet als <strong class="text-white font-semibold">Test User</strong>.<br/>
|
||||
Admin: <strong class="text-white font-semibold">Portal Admin</strong>
|
||||
</div>
|
||||
<button class="mt-3 w-full px-3 py-2 text-[12px] font-semibold rounded-[3px] transition-colors flex items-center justify-center gap-1.5" style="background:#F6F4EF;color:#1A2540;">
|
||||
<svg width="11" height="11" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M9 3L3 9M3 9H8M3 9V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
Zurück zum Admin
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resources -->
|
||||
<div class="px-3 pb-5 border-t border-bg-rule pt-4">
|
||||
<div class="nav-section">Resources</div>
|
||||
<div class="space-y-0.5">
|
||||
<a class="nav-item" href="#"><svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 11l4 2 6-9-4-1z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>Tailwind CSS</a>
|
||||
<a class="nav-item" href="#"><svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M8 2l5 2v4c0 3-2 5-5 6-3-1-5-3-5-6V4z" stroke="currentColor" stroke-width="1.3"/></svg>Hero Icons</a>
|
||||
<a class="nav-item" href="#"><svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M9 2L4 9h3l-1 5 5-7H8z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>Flux UI</a>
|
||||
<a class="nav-item" href="#"><svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 4l5-2 5 2v8l-5 2-5-2z" stroke="currentColor" stroke-width="1.3"/><path d="M3 4l5 2 5-2M8 6v8" stroke="currentColor" stroke-width="1.3"/></svg>Repository</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- =============================================
|
||||
MAIN — Inhalt
|
||||
============================================== -->
|
||||
<main class="flex-1 min-w-0">
|
||||
|
||||
<!-- Topbar — sehr schlank; Brücken-Kontext + Kontoinfo rechts -->
|
||||
<div class="bg-bg-elev border-b border-bg-rule">
|
||||
<div class="px-10 py-3 flex items-center gap-6">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="flex items-center gap-2 text-[12px] text-ink-3 font-medium">
|
||||
<a href="Hub Landing presseportale.html" class="hover:text-hub">Hub</a>
|
||||
<span class="text-ink-4">/</span>
|
||||
<span class="text-ink-2">User Backend</span>
|
||||
<span class="text-ink-4">/</span>
|
||||
<span class="text-hub font-semibold">Übersicht</span>
|
||||
</div>
|
||||
|
||||
<span class="flex-1"></span>
|
||||
|
||||
<!-- Reichweiten-Kontext (Brücke zur Hub-CI) -->
|
||||
<span class="bridge-row">
|
||||
<span class="dot-pe"></span> presseecho
|
||||
<span class="text-ink-4 mx-1">·</span>
|
||||
<span class="dot-bp"></span> businessportal24
|
||||
</span>
|
||||
|
||||
<span class="w-px h-5 bg-bg-rule"></span>
|
||||
|
||||
<!-- Search (FluxUI input slot) -->
|
||||
<div class="relative">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" class="absolute left-2.5 top-1/2 -translate-y-1/2 text-ink-3">
|
||||
<circle cx="7" cy="7" r="4.5" stroke="currentColor" stroke-width="1.3"/>
|
||||
<path d="M10.5 10.5L13 13" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<input class="pl-8 pr-3 py-1.5 w-[220px] bg-bg-card border border-bg-rule rounded-[4px] text-[12.5px] text-ink placeholder:text-ink-4 focus:outline-none focus:border-hub" placeholder="Suchen…" />
|
||||
<span class="absolute right-2 top-1/2 -translate-y-1/2 text-[10px] font-mono text-ink-4 border border-bg-rule rounded px-1">⌘K</span>
|
||||
</div>
|
||||
|
||||
<button class="relative w-8 h-8 flex items-center justify-center rounded-[4px] hover:bg-bg-card border border-transparent hover:border-bg-rule">
|
||||
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" class="text-ink-2">
|
||||
<path d="M3.5 7a4.5 4.5 0 119 0v3.5l1 1.5H2.5l1-1.5z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/>
|
||||
<path d="M6.5 13a1.5 1.5 0 003 0" stroke="currentColor" stroke-width="1.3"/>
|
||||
</svg>
|
||||
<span class="absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-accent"></span>
|
||||
</button>
|
||||
|
||||
<a href="#" class="btn-primary text-[12px] py-1.5 px-3.5">
|
||||
<svg width="11" height="11" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M6 2v8M2 6h8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
Neue Mitteilung
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Inhalt -->
|
||||
<div class="px-10 py-8 space-y-6">
|
||||
|
||||
<!-- ============== PAGE HEADER ============== -->
|
||||
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
|
||||
<span class="badge hub dot">User Backend</span>
|
||||
<span class="eyebrow muted">Mein Bereich · A · 01</span>
|
||||
</div>
|
||||
<h1 class="text-[34px] font-bold tracking-[-0.7px] text-ink leading-[1.1] m-0">Mein Dashboard</h1>
|
||||
<p class="text-[13.5px] text-ink-2 leading-[1.55] mt-2 m-0 max-w-[640px]">
|
||||
Willkommen zurück, <strong class="font-semibold text-ink">Test User</strong>. Hier sehen Sie Status und Reichweite Ihres Kundenkontos für presseecho und businessportal24.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 flex-shrink-0">
|
||||
<!-- Zeitraum-Filter (FluxUI dropdown-style) -->
|
||||
<button class="btn-secondary whitespace-nowrap">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3.5" width="11" height="10" stroke="currentColor" stroke-width="1.3"/><path d="M2.5 6h11M5 2v3M11 2v3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
|
||||
Diese Woche
|
||||
<svg width="10" height="10" viewBox="0 0 12 12" fill="none"><path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
|
||||
<!-- Firma-zuordnen-Status — als Banner-Pille, statt im Karte-Header eingesperrt -->
|
||||
<span class="inline-flex items-center gap-2 px-3 py-1.5 bg-warn-soft border border-warn/30 text-accent rounded-[4px] text-[12px] font-semibold whitespace-nowrap">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" class="flex-shrink-0"><path d="M8 2l6 11H2z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M8 7v3M8 11.5v.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
Keine Firma zugeordnet
|
||||
<a href="#" class="underline underline-offset-[3px] decoration-accent/40 hover:decoration-accent">zuordnen →</a>
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ============== STAT-CARDS — KPI-Reihe ============== -->
|
||||
<section class="grid gap-4" style="grid-template-columns:repeat(4,1fr);">
|
||||
|
||||
<article class="stat-card is-primary">
|
||||
<span class="stat-strip"></span>
|
||||
<div class="flex items-baseline justify-between">
|
||||
<div class="stat-label" style="color:#7B8FBF;">Gesamt</div>
|
||||
<span class="text-[10.5px] font-mono text-ink-3 tracking-[0.14em] uppercase">2026</span>
|
||||
</div>
|
||||
<div class="stat-num">0</div>
|
||||
<div class="mt-3 flex items-center gap-2 text-[11.5px] text-ink-3">
|
||||
<span class="flex items-center gap-1">
|
||||
<svg width="9" height="9" viewBox="0 0 10 10" fill="none"><path d="M5 8V2M2 5l3-3 3 3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
0 ggü. Vormonat
|
||||
</span>
|
||||
</div>
|
||||
<!-- Mini Sparkline placeholder -->
|
||||
<svg class="absolute right-3 bottom-3 opacity-50" width="80" height="22" viewBox="0 0 80 22" fill="none">
|
||||
<path d="M0 18 L10 17 L20 18 L30 16 L40 18 L50 15 L60 17 L70 14 L80 16" stroke="#1A2540" stroke-width="1.2" fill="none"/>
|
||||
<circle cx="80" cy="16" r="1.8" fill="#1A2540"/>
|
||||
</svg>
|
||||
</article>
|
||||
|
||||
<article class="stat-card is-ok">
|
||||
<span class="stat-strip"></span>
|
||||
<div class="flex items-baseline justify-between">
|
||||
<div class="stat-label">Veröffentlicht</div>
|
||||
<span class="badge ok" style="font-size:9.5px;padding:1px 6px;">live</span>
|
||||
</div>
|
||||
<div class="stat-num" style="color:#6BD498;">0</div>
|
||||
<div class="mt-3 flex items-center gap-2 text-[11.5px] text-ink-3">
|
||||
auf beiden Portalen
|
||||
</div>
|
||||
<svg class="absolute right-3 bottom-3 opacity-50" width="80" height="22" viewBox="0 0 80 22" fill="none">
|
||||
<path d="M0 19 L10 18 L20 19 L30 17 L40 16 L50 14 L60 13 L70 11 L80 10" stroke="#2E8540" stroke-width="1.2" fill="none"/>
|
||||
<circle cx="80" cy="10" r="1.8" fill="#2E8540"/>
|
||||
</svg>
|
||||
</article>
|
||||
|
||||
<article class="stat-card is-warn">
|
||||
<span class="stat-strip"></span>
|
||||
<div class="flex items-baseline justify-between">
|
||||
<div class="stat-label">In Prüfung</div>
|
||||
<span class="badge warn" style="font-size:9.5px;padding:1px 6px;">Ø 4 h</span>
|
||||
</div>
|
||||
<div class="stat-num" style="color:#D9A560;">0</div>
|
||||
<div class="mt-3 flex items-center gap-2 text-[11.5px] text-ink-3">
|
||||
redaktionelle Prüfung
|
||||
</div>
|
||||
<svg class="absolute right-3 bottom-3 opacity-50" width="80" height="22" viewBox="0 0 80 22" fill="none">
|
||||
<path d="M0 16 L10 17 L20 14 L30 18 L40 13 L50 17 L60 12 L70 15 L80 11" stroke="#A87A1F" stroke-width="1.2" fill="none"/>
|
||||
<circle cx="80" cy="11" r="1.8" fill="#A87A1F"/>
|
||||
</svg>
|
||||
</article>
|
||||
|
||||
<article class="stat-card is-muted">
|
||||
<span class="stat-strip"></span>
|
||||
<div class="flex items-baseline justify-between">
|
||||
<div class="stat-label">Entwürfe</div>
|
||||
<span class="text-[10.5px] font-mono text-ink-3 tracking-[0.14em] uppercase">privat</span>
|
||||
</div>
|
||||
<div class="stat-num" style="color:#8E8B82;">0</div>
|
||||
<div class="mt-3 flex items-center gap-2 text-[11.5px] text-ink-3">
|
||||
gespeichert, nicht eingereicht
|
||||
</div>
|
||||
<svg class="absolute right-3 bottom-3 opacity-50" width="80" height="22" viewBox="0 0 80 22" fill="none">
|
||||
<path d="M0 12 L10 12 L20 12 L30 12 L40 12 L50 12 L60 12 L70 12 L80 12" stroke="#8A918D" stroke-width="1.2" stroke-dasharray="2 2" fill="none"/>
|
||||
</svg>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<!-- ============== ZWEISPALTEN-GRID ============== -->
|
||||
<section class="grid gap-6" style="grid-template-columns:2fr 1fr;">
|
||||
|
||||
<!-- ============== LINKS: Pressemitteilungen-Liste / Empty ============== -->
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="section-eyebrow">Meine letzten Pressemitteilungen</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-[11.5px] text-ink-3">0 von 0</span>
|
||||
<a href="#" class="text-[12px] font-semibold text-hub hover:underline underline-offset-[3px] decoration-hub/30">Alle anzeigen →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State — sauber, ohne überlappende Skelett-Zeilen -->
|
||||
<div class="px-10 py-14 flex flex-col items-center text-center">
|
||||
<!-- Dezentes Tabellen-Icon -->
|
||||
<div class="w-16 h-16 rounded-[6px] bg-hub-soft border border-hub-soft-2 flex items-center justify-center text-hub mb-5 relative">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
|
||||
<rect x="3" y="4" width="14" height="16" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M17 7h4v13H6" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M6 8h8M6 11h8M6 14h5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<span class="absolute -top-1.5 -right-1.5 w-5 h-5 rounded-full bg-accent text-white text-[10px] font-bold flex items-center justify-center font-mono">0</span>
|
||||
</div>
|
||||
<div class="text-[16px] font-semibold text-ink m-0">Noch keine Pressemitteilungen</div>
|
||||
<p class="text-[13px] text-ink-3 leading-[1.55] mt-2 m-0 max-w-[460px]">
|
||||
Starten Sie mit einer ersten Mitteilung für die aktive Firma oder Ihr Kundenkonto. Veröffentlichung erfolgt nach redaktioneller Prüfung auf beiden Portalen.
|
||||
</p>
|
||||
<div class="mt-6 flex items-center gap-2.5">
|
||||
<a href="#" class="btn-primary">
|
||||
<svg width="11" height="11" viewBox="0 0 12 12" fill="none"><path d="M6 2v8M2 6h8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
|
||||
Erste Pressemitteilung erstellen
|
||||
</a>
|
||||
<a href="#" class="btn-secondary">Vorlage öffnen</a>
|
||||
</div>
|
||||
|
||||
<!-- Vorgeschlagene Schritte -->
|
||||
<div class="mt-9 grid gap-3 w-full max-w-[560px]" style="grid-template-columns:repeat(3,1fr);">
|
||||
<div class="text-left px-3 py-2.5 bg-bg-elev border border-bg-rule rounded-[3px]">
|
||||
<div class="font-mono text-[9.5px] tracking-[0.16em] text-accent font-bold mb-1">01</div>
|
||||
<div class="text-[11.5px] font-semibold text-ink leading-tight">Firma zuordnen</div>
|
||||
</div>
|
||||
<div class="text-left px-3 py-2.5 bg-bg-elev border border-bg-rule rounded-[3px]">
|
||||
<div class="font-mono text-[9.5px] tracking-[0.16em] text-accent font-bold mb-1">02</div>
|
||||
<div class="text-[11.5px] font-semibold text-ink leading-tight">Mitteilung verfassen</div>
|
||||
</div>
|
||||
<div class="text-left px-3 py-2.5 bg-bg-elev border border-bg-rule rounded-[3px]">
|
||||
<div class="font-mono text-[9.5px] tracking-[0.16em] text-accent font-bold mb-1">03</div>
|
||||
<div class="text-[11.5px] font-semibold text-ink leading-tight">Zur Prüfung senden</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-5 py-3 bg-bg-elev border-t border-bg-rule flex items-center gap-2.5 text-[11.5px] text-ink-3">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" class="text-hub flex-shrink-0">
|
||||
<path d="M8 1.5l5.5 2.5v4c0 3.3-2.2 5.8-5.5 6.5C4.7 13.8 2.5 11.3 2.5 8V4z" stroke="currentColor" stroke-width="1.3" fill="none"/>
|
||||
<path d="M5.5 8l2 2 3.5-4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Tipp: Geprüfte Mitteilungen erscheinen i. d. R. binnen <strong class="text-ink-2 font-semibold">4 Stunden</strong> werktags auf beiden Portalen.
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- ============== RECHTS: Datenqualität ============== -->
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">Datenqualität</span>
|
||||
<span class="badge warn dot">2 offen</span>
|
||||
</div>
|
||||
<div class="px-5 py-5">
|
||||
<p class="text-[12px] text-ink-3 leading-[1.55] m-0 mb-4">
|
||||
Diese Hinweise helfen, Ihr User Backend vollständig und sauber zu halten.
|
||||
</p>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="hint-card">
|
||||
<span class="hint-ico">
|
||||
<svg width="18" height="18" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="6" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M3 13.5c.7-2.4 2.7-4 5-4s4.3 1.6 5 4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
</span>
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-baseline justify-between gap-3">
|
||||
<div class="text-[13px] font-semibold text-ink leading-tight">Profil unvollständig</div>
|
||||
<span class="font-mono text-[10px] text-ink-3 tracking-[0.10em] uppercase whitespace-nowrap flex-shrink-0">60 %</span>
|
||||
</div>
|
||||
<div class="mt-2 h-1 w-full rounded-full bg-bg-rule-2 overflow-hidden">
|
||||
<div class="h-full rounded-full bg-accent" style="width:60%;"></div>
|
||||
</div>
|
||||
<p class="text-[11.5px] text-ink-3 leading-[1.5] mt-2 m-0">
|
||||
Vorname, Telefon und Pressekontakt fehlen für saubere Kundenakte.
|
||||
</p>
|
||||
<a href="#" class="inline-flex items-center gap-1 text-[11.5px] font-semibold text-accent mt-2 hover:underline underline-offset-[3px] decoration-accent/40">Profil öffnen →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hint-card">
|
||||
<span class="hint-ico">
|
||||
<svg width="18" height="18" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3.5" width="11" height="10" stroke="currentColor" stroke-width="1.4"/><path d="M2.5 6h11" stroke="currentColor" stroke-width="1.4"/></svg>
|
||||
</span>
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-baseline justify-between gap-3">
|
||||
<div class="text-[13px] font-semibold text-ink leading-tight">Rechnungsadresse fehlt</div>
|
||||
<span class="font-mono text-[10px] text-ink-3 tracking-[0.10em] uppercase whitespace-nowrap flex-shrink-0">0 %</span>
|
||||
</div>
|
||||
<div class="mt-2 h-1 w-full rounded-full bg-bg-rule-2 overflow-hidden">
|
||||
<div class="h-full rounded-full bg-accent" style="width:5%;"></div>
|
||||
</div>
|
||||
<p class="text-[11.5px] text-ink-3 leading-[1.5] mt-2 m-0">
|
||||
Hinterlegen Sie eine Rechnungsadresse, damit spätere Buchungen sauber abgerechnet werden können.
|
||||
</p>
|
||||
<a href="#" class="inline-flex items-center gap-1 text-[11.5px] font-semibold text-accent mt-2 hover:underline underline-offset-[3px] decoration-accent/40">Rechnungsadresse ergänzen →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<!-- ============== UNTERER GRID: FIRMEN + REICHWEITE / AKTIVITÄT ============== -->
|
||||
<section class="grid gap-6" style="grid-template-columns:2fr 1fr;">
|
||||
|
||||
<!-- Firmen Card -->
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">Meine Firmen</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="badge hub" style="font-size:9.5px;padding:1px 6px;">0 zugeordnet</span>
|
||||
<a href="#" class="text-[12px] font-semibold text-hub hover:underline underline-offset-[3px] decoration-hub/30">Profil & Firma verwalten →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="grid gap-3" style="grid-template-columns:1fr 1fr;">
|
||||
|
||||
<!-- Empty-Slot Firma 1 -->
|
||||
<div class="relative border border-dashed border-bg-rule rounded-[5px] p-5 hover:border-hub/50 hover:bg-bg-elev transition-colors">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<span class="w-10 h-10 rounded-[4px] border border-bg-rule bg-bg-elev flex items-center justify-center text-ink-4">
|
||||
<svg width="18" height="18" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3.5" width="11" height="10" stroke="currentColor" stroke-width="1.3"/><path d="M2.5 6h11M6 9h1M9 9h1M6 11.5h1M9 11.5h1" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
|
||||
</span>
|
||||
<div>
|
||||
<div class="text-[13.5px] font-semibold text-ink-2">Firma hinzufügen</div>
|
||||
<div class="text-[11px] text-ink-3 mt-0.5">Slot frei · 1 von 3</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-[11.5px] text-ink-3 leading-[1.5] m-0">
|
||||
Pressestellen, für die Sie Mitteilungen erstellen — mit eigenem Logo, Kontaktperson und Themen-Tags.
|
||||
</p>
|
||||
<a href="#" class="absolute top-3 right-3 inline-flex items-center justify-center w-7 h-7 rounded-[3px] bg-bg-card border border-bg-rule text-hub hover:border-hub">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M6 2v8M2 6h8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Hinweis -->
|
||||
<div class="bg-bg-elev border border-bg-rule rounded-[5px] p-5">
|
||||
<div class="eyebrow muted mb-2">Hinweis</div>
|
||||
<div class="text-[13px] text-ink-2 leading-[1.55] m-0">
|
||||
Keine Firmen zugeordnet. Wenn hier eine Firma fehlen sollte, prüfen Sie bitte Ihr Profil oder wenden Sie sich an den Support.
|
||||
</div>
|
||||
<div class="mt-3 flex items-center gap-2">
|
||||
<a href="#" class="btn-secondary" style="padding:6px 10px;font-size:11.5px;">Profil prüfen</a>
|
||||
<a href="/cdn-cgi/l/email-protection#e5969095958a9791a5959780969680958a9791848980cb868a88" class="text-[11.5px] font-semibold text-ink-3 hover:text-hub">Support kontaktieren</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Aktivität / Bridge -->
|
||||
<article class="panel-dark">
|
||||
<div class="panel-head">
|
||||
<span class="eyebrow on-dark">Brand-Bridge</span>
|
||||
<span class="font-mono text-[10px] text-ink-on-dark-3 tracking-[0.14em] uppercase">A · B</span>
|
||||
</div>
|
||||
<div class="px-5 py-5">
|
||||
<div class="text-[12.5px] leading-[1.55] text-ink-on-dark-2 m-0 mb-4">
|
||||
Ein Konto, beide Portale — Veröffentlichungen werden parallel auf presseecho und businessportal24 ausgespielt.
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3" style="grid-template-columns:1fr 1fr;">
|
||||
<div class="rounded-[4px] px-3.5 py-3 border border-white/5" style="background:#1A2540;">
|
||||
<div class="flex items-center gap-2 mb-1.5">
|
||||
<span class="dot-pe"></span>
|
||||
<span class="text-[11px] font-bold tracking-[0.14em] uppercase text-white/85">presseecho</span>
|
||||
</div>
|
||||
<div class="font-mono text-[15px] font-semibold text-white" style="font-variant-numeric:tabular-nums;">verbunden</div>
|
||||
<div class="text-[10.5px] text-ink-on-dark-3 mt-0.5">Archiv · Branchen-Tiefe</div>
|
||||
</div>
|
||||
<div class="rounded-[4px] px-3.5 py-3 border border-white/5" style="background:#1A2540;">
|
||||
<div class="flex items-center gap-2 mb-1.5">
|
||||
<span class="dot-bp"></span>
|
||||
<span class="text-[11px] font-bold tracking-[0.14em] uppercase text-white/85">businessportal24</span>
|
||||
</div>
|
||||
<div class="font-mono text-[15px] font-semibold text-white" style="font-variant-numeric:tabular-nums;">verbunden</div>
|
||||
<div class="text-[10.5px] text-ink-on-dark-3 mt-0.5">Wirtschaft · Live</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="mt-5 mb-4" style="border:0;height:1px;background:rgba(255,255,255,.10);" />
|
||||
|
||||
<div class="space-y-2 text-[11.5px] text-ink-on-dark-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>API-Status</span>
|
||||
<span class="flex items-center gap-1.5 text-white"><span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>operational</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Letzte Synchronisation</span>
|
||||
<span class="font-mono text-white">vor 2 min</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Tarif</span>
|
||||
<span class="font-mono text-white">Starter</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="Hub Landing presseportale.html#tarife" class="mt-5 inline-flex items-center gap-1.5 text-[11.5px] font-semibold text-white hover:underline underline-offset-[3px] decoration-white/30">
|
||||
Tarife & Add-ons ansehen →
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<!-- ============== FUSSZEILE ============== -->
|
||||
<footer class="flex items-center justify-between pt-4 pb-2 text-[11px] text-ink-3 border-t border-bg-rule">
|
||||
<span>© 2026 presseportale.com · Publisher-Hub</span>
|
||||
<span class="flex items-center gap-5">
|
||||
<a href="#" class="hover:text-hub">Tastenkürzel</a>
|
||||
<a href="#" class="hover:text-hub">Changelog</a>
|
||||
<a href="#" class="hover:text-hub">Statusseite</a>
|
||||
<a href="/cdn-cgi/l/email-protection#21525451514e535561515344525244514e5355404d440f424e4c" class="hover:text-hub">Support</a>
|
||||
</span>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script></body>
|
||||
</html>
|
||||
764
dev/frontend/tailwind_v3/User Dashboard presseportale.html
Normal file
764
dev/frontend/tailwind_v3/User Dashboard presseportale.html
Normal file
|
|
@ -0,0 +1,764 @@
|
|||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>presseportale.com — Mein Dashboard</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
|
||||
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
bg: "#F6F4EF",
|
||||
"bg-elev": "#FBFAF6",
|
||||
"bg-rule": "#E2DDD0",
|
||||
"bg-rule-2": "#EDE7D7",
|
||||
"bg-card": "#FFFFFF",
|
||||
"bg-card-warm": "#EFEADC",
|
||||
"hub": "#1A2540",
|
||||
"hub-2": "#243152",
|
||||
"hub-3": "#2E3D66",
|
||||
"hub-soft": "#E5E9F1",
|
||||
"hub-soft-2":"#CFD6E4",
|
||||
"hub-line": "#7B8FBF",
|
||||
"accent": "#B07A3A",
|
||||
"accent-deep": "#8A5E27",
|
||||
"accent-soft": "#F1E6D3",
|
||||
ink: "#1A1F1C",
|
||||
"ink-2":"#3A413D",
|
||||
"ink-3":"#5A6360",
|
||||
"ink-4":"#8A918D",
|
||||
"ink-on-dark": "#F6F4EF",
|
||||
"ink-on-dark-2": "#B2B9C7",
|
||||
"ink-on-dark-3": "#7B8FBF",
|
||||
ok: "#2E8540",
|
||||
"ok-soft":"#E2F1E5",
|
||||
warn: "#A87A1F",
|
||||
"warn-soft":"#F6EAC8",
|
||||
err: "#A8331F",
|
||||
"err-soft":"#F4DAD2",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['"Inter Tight"','Inter','system-ui','sans-serif'],
|
||||
mono: ['"JetBrains Mono"','"SF Mono"','ui-monospace','monospace'],
|
||||
},
|
||||
backgroundImage: {
|
||||
"hub-grad": "linear-gradient(135deg,#1A2540 0%,#243152 100%)",
|
||||
"hub-grad-2": "linear-gradient(180deg,#1A2540 0%,#0F1729 100%)",
|
||||
"accent-grad": "linear-gradient(135deg,#B07A3A 0%,#8A5E27 100%)",
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html,body { margin:0; padding:0; }
|
||||
body { background:#E8E4DA; font-family:"Inter Tight",system-ui,sans-serif; }
|
||||
|
||||
.eyebrow {
|
||||
font-size: 10.5px; font-weight: 700;
|
||||
letter-spacing: 0.20em; text-transform: uppercase;
|
||||
color: #1A2540;
|
||||
}
|
||||
.eyebrow.muted { color:#5A6360; letter-spacing:0.16em; font-weight:600; font-size:10px; }
|
||||
.eyebrow.accent { color:#8A5E27; }
|
||||
.eyebrow.on-dark { color:#7B8FBF; }
|
||||
|
||||
.section-eyebrow {
|
||||
display:inline-flex; align-items:center; gap:10px;
|
||||
font-size: 10.5px; font-weight: 700;
|
||||
letter-spacing: 0.22em; text-transform: uppercase;
|
||||
color:#1A2540;
|
||||
}
|
||||
.section-eyebrow::after {
|
||||
content:""; display:block; width:30px; height:1px;
|
||||
background:#1A2540; opacity:.45;
|
||||
}
|
||||
|
||||
.rule { height:1px; background:#E2DDD0; border:0; margin:0; }
|
||||
.rule-strong { height:1px; background:#1A1F1C; border:0; margin:0; }
|
||||
|
||||
/* Sidebar */
|
||||
.nav-item {
|
||||
display:flex; align-items:center; gap:11px;
|
||||
padding:8px 12px;
|
||||
border-radius:4px;
|
||||
font-size:13px; font-weight:500;
|
||||
color:#3A413D;
|
||||
transition: background .12s, color .12s;
|
||||
position:relative;
|
||||
}
|
||||
.nav-item:hover { background:#F6F4EF; color:#1A2540; }
|
||||
.nav-item.active {
|
||||
background:#E5E9F1;
|
||||
color:#1A2540;
|
||||
font-weight:600;
|
||||
}
|
||||
.nav-item.active::before {
|
||||
content:""; position:absolute; left:-1px; top:6px; bottom:6px;
|
||||
width:2px; background:#1A2540; border-radius:0 2px 2px 0;
|
||||
}
|
||||
.nav-item.disabled { color:#8A918D; cursor:default; }
|
||||
.nav-item.disabled:hover { background:transparent; color:#8A918D; }
|
||||
.nav-item .ico {
|
||||
width:16px; height:16px; flex-shrink:0;
|
||||
color: currentColor; opacity:.8;
|
||||
}
|
||||
.nav-item.active .ico { opacity:1; }
|
||||
|
||||
.nav-section {
|
||||
font-size:10px; font-weight:700; letter-spacing:0.18em;
|
||||
text-transform:uppercase; color:#8A918D;
|
||||
padding: 0 12px 6px;
|
||||
}
|
||||
|
||||
/* Card / panel headers */
|
||||
.panel {
|
||||
background:#FFFFFF; border:1px solid #E2DDD0; border-radius:6px;
|
||||
}
|
||||
.panel-warm { background:#FBFAF6; border:1px solid #E2DDD0; border-radius:6px; }
|
||||
.panel-dark { background:#1A2540; border:1px solid #0F1729; border-radius:6px; color:#F6F4EF; }
|
||||
.panel-head {
|
||||
display:flex; align-items:center; justify-content:space-between;
|
||||
padding:14px 20px;
|
||||
border-bottom:1px solid #E2DDD0;
|
||||
}
|
||||
.panel-dark .panel-head { border-bottom-color: rgba(255,255,255,.10); }
|
||||
|
||||
/* Stat cards */
|
||||
.stat-card {
|
||||
position:relative;
|
||||
background:#FFFFFF; border:1px solid #E2DDD0; border-radius:6px;
|
||||
padding:18px 20px;
|
||||
transition: border-color .12s, box-shadow .12s;
|
||||
}
|
||||
.stat-card .stat-strip {
|
||||
position:absolute; left:0; top:0; bottom:0; width:3px;
|
||||
background:#CFD6E4; border-radius:6px 0 0 6px;
|
||||
}
|
||||
.stat-card.is-primary .stat-strip { background:#1A2540; }
|
||||
.stat-card.is-ok .stat-strip { background:#2E8540; }
|
||||
.stat-card.is-warn .stat-strip { background:#A87A1F; }
|
||||
.stat-card.is-muted .stat-strip { background:#8A918D; }
|
||||
|
||||
.stat-label {
|
||||
font-size:10.5px; font-weight:700;
|
||||
letter-spacing:0.16em; text-transform:uppercase;
|
||||
color:#5A6360;
|
||||
}
|
||||
.stat-card.is-ok .stat-label { color:#2E8540; }
|
||||
.stat-card.is-warn .stat-label { color:#A87A1F; }
|
||||
.stat-card.is-muted .stat-label { color:#8A918D; }
|
||||
|
||||
.stat-num {
|
||||
font-family:"JetBrains Mono","SF Mono",ui-monospace,monospace;
|
||||
font-variant-numeric:tabular-nums;
|
||||
font-size:36px; font-weight:600; color:#1A1F1C;
|
||||
letter-spacing:-0.5px; line-height:1;
|
||||
margin-top:14px;
|
||||
}
|
||||
|
||||
/* Hint chips (Datenqualität) */
|
||||
.hint-card {
|
||||
display:grid; gap:14px;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items:start;
|
||||
background:#FBFAF6; border:1px solid #E2DDD0; border-left:3px solid #B07A3A;
|
||||
border-radius:5px;
|
||||
padding:16px 18px;
|
||||
}
|
||||
.hint-card .hint-ico {
|
||||
width:36px; height:36px; border-radius:4px;
|
||||
background:#F1E6D3; color:#8A5E27;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
flex-shrink:0;
|
||||
}
|
||||
|
||||
/* FluxUI-style standard button (the "blue" one in screenshots) — kept, but harmonised */
|
||||
.btn-primary {
|
||||
display:inline-flex; align-items:center; gap:8px; justify-content:center;
|
||||
padding:9px 16px;
|
||||
background:#1A2540; color:#FFFFFF;
|
||||
border-radius:4px; font-size:13px; font-weight:600;
|
||||
transition: background .15s;
|
||||
}
|
||||
.btn-primary:hover { background:#243152; }
|
||||
.btn-secondary {
|
||||
display:inline-flex; align-items:center; gap:8px; justify-content:center;
|
||||
padding:8px 14px;
|
||||
background:#FFFFFF; color:#1A2540;
|
||||
border:1px solid #CFD6E4; border-radius:4px;
|
||||
font-size:12.5px; font-weight:600;
|
||||
transition: border-color .15s, background .15s;
|
||||
}
|
||||
.btn-secondary:hover { border-color:#1A2540; background:#F6F4EF; }
|
||||
|
||||
.badge {
|
||||
display:inline-flex; align-items:center; gap:6px;
|
||||
padding:3px 9px; border-radius:99px;
|
||||
font-size:10.5px; font-weight:700;
|
||||
letter-spacing:0.10em; text-transform:uppercase;
|
||||
}
|
||||
.badge.warn { background:#F6EAC8; color:#8A5E27; }
|
||||
.badge.ok { background:#E2F1E5; color:#1F5E2E; }
|
||||
.badge.hub { background:#E5E9F1; color:#1A2540; }
|
||||
.badge.dot::before {
|
||||
content:""; width:6px; height:6px; border-radius:99px; background:currentColor;
|
||||
}
|
||||
|
||||
/* Bridge ribbon - sehr subtil */
|
||||
.bridge-row {
|
||||
display:inline-flex; align-items:center; gap:6px;
|
||||
font-family:"JetBrains Mono","SF Mono",ui-monospace,monospace;
|
||||
font-size:10.5px; letter-spacing:0.10em; text-transform:uppercase;
|
||||
color:#5A6360;
|
||||
}
|
||||
.dot-pe { width:6px; height:6px; border-radius:99px; background:#1F4D3A; }
|
||||
.dot-bp { width:6px; height:6px; border-radius:99px; background:#C84A1E; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-bg text-ink font-sans antialiased">
|
||||
|
||||
<!-- ============== ARTBOARD ============== -->
|
||||
<div class="mx-auto bg-bg" style="width:1440px;">
|
||||
|
||||
<div class="flex" style="min-height:980px;">
|
||||
|
||||
<!-- =============================================
|
||||
SIDEBAR — schmal, warm, klare Hierarchie
|
||||
(FluxUI <flux:navlist> + Slot-Header-Styles)
|
||||
============================================== -->
|
||||
<aside class="bg-bg-elev border-r border-bg-rule flex flex-col" style="width:260px;">
|
||||
|
||||
<!-- Brand-Block -->
|
||||
<div class="px-5 pt-6 pb-5">
|
||||
<a href="Hub Landing presseportale.html" class="flex items-baseline gap-2">
|
||||
<span class="text-[19px] font-bold tracking-[-0.4px] text-hub leading-none">presseportale<span class="text-accent">.com</span></span>
|
||||
</a>
|
||||
<div class="eyebrow muted mt-2">Publisher · Hub</div>
|
||||
|
||||
<!-- Aktive Firma / Konto-Switcher (FluxUI-Slot: header before nav) -->
|
||||
<button class="mt-4 w-full grid items-center gap-2.5 px-3 py-2.5 bg-white border border-bg-rule rounded-[4px] hover:border-hub/40 text-left"
|
||||
style="grid-template-columns:auto 1fr auto;">
|
||||
<span class="w-7 h-7 rounded-[3px] bg-hub-soft border border-hub-soft-2 flex items-center justify-center text-hub text-[11px] font-bold">TU</span>
|
||||
<span class="min-w-0">
|
||||
<span class="block text-[12.5px] font-semibold text-ink leading-tight truncate">Test User</span>
|
||||
<span class="block text-[10.5px] text-ink-3 leading-tight mt-0.5 truncate">Keine Firma zugeordnet</span>
|
||||
</span>
|
||||
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" class="text-ink-3">
|
||||
<path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 7.5l3-3 3 3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" opacity="0.4"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="px-3 flex-1">
|
||||
<div class="nav-section">Mein Bereich</div>
|
||||
<div class="space-y-0.5 mb-5">
|
||||
<a class="nav-item active" href="#">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M2 7l6-5 6 5v7H2z" stroke="currentColor" stroke-width="1.4"/><path d="M6 14V9h4v5" stroke="currentColor" stroke-width="1.4"/></svg>
|
||||
Übersicht
|
||||
</a>
|
||||
<a class="nav-item" href="#">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="2.5" width="9" height="11" stroke="currentColor" stroke-width="1.4"/><path d="M11.5 5h2v8.5H4" stroke="currentColor" stroke-width="1.4"/><path d="M5 5.5h4M5 8h4M5 10.5h2.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
|
||||
Meine Pressemitteilungen
|
||||
<span class="badge hub ml-auto" style="font-size:9.5px;padding:1px 6px;letter-spacing:0.08em;">0</span>
|
||||
</a>
|
||||
<a class="nav-item" href="#">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3.5" width="11" height="10" stroke="currentColor" stroke-width="1.4"/><path d="M2.5 6h11" stroke="currentColor" stroke-width="1.4"/><path d="M6 9h1M9 9h1M6 11.5h1M9 11.5h1" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
Firmen
|
||||
</a>
|
||||
<a class="nav-item" href="#">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 5h10l-1 9H4z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M6 5V3.5a2 2 0 014 0V5" stroke="currentColor" stroke-width="1.4"/></svg>
|
||||
Buchungen & Add-ons
|
||||
</a>
|
||||
<span class="nav-item disabled">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 13V8M7 13V5M11 13V9" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
Statistiken
|
||||
<span class="ml-auto text-[9.5px] tracking-[0.14em] uppercase font-semibold text-ink-4">bald</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">Finanzen</div>
|
||||
<div class="space-y-0.5 mb-5">
|
||||
<span class="nav-item disabled">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="5" stroke="currentColor" stroke-width="1.4"/><path d="M8 5.5v5M6 8h4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
Credits & Tarif
|
||||
<span class="ml-auto text-[9.5px] tracking-[0.14em] uppercase font-semibold text-ink-4">bald</span>
|
||||
</span>
|
||||
<a class="nav-item" href="#">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 2.5h7l3 3v8H3z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M10 2.5V5.5h3" stroke="currentColor" stroke-width="1.4"/><path d="M5.5 8h5M5.5 10.5h5M5.5 6h2" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
|
||||
Rechnungen
|
||||
</a>
|
||||
<span class="nav-item disabled">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="4.5" width="11" height="7.5" stroke="currentColor" stroke-width="1.4"/><path d="M2.5 7h11" stroke="currentColor" stroke-width="1.4"/></svg>
|
||||
Zahlungsarten
|
||||
<span class="ml-auto text-[9.5px] tracking-[0.14em] uppercase font-semibold text-ink-4">bald</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">Konto</div>
|
||||
<div class="space-y-0.5 mb-5">
|
||||
<a class="nav-item" href="#">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="6" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M3 13.5c.7-2.4 2.7-4 5-4s4.3 1.6 5 4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
Profil
|
||||
</a>
|
||||
<a class="nav-item" href="#">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M8 2l5 2v4c0 3-2 5-5 6-3-1-5-3-5-6V4z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M6 8l1.5 1.5L10.5 6" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
Sicherheit
|
||||
</a>
|
||||
<a class="nav-item" href="#">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><circle cx="6" cy="8" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M8.5 8h5M11 8v2.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
API & Integrationen
|
||||
</a>
|
||||
<span class="nav-item disabled">
|
||||
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3.5 7a4.5 4.5 0 119 0v3.5l1 1.5H2.5l1-1.5z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M6.5 13a1.5 1.5 0 003 0" stroke="currentColor" stroke-width="1.4"/></svg>
|
||||
Benachrichtigungen
|
||||
<span class="ml-auto text-[9.5px] tracking-[0.14em] uppercase font-semibold text-ink-4">bald</span>
|
||||
</span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Testmodus-Block (war im Original „Zurück zum Admin") -->
|
||||
<div class="px-4 pb-4">
|
||||
<div class="bg-hub text-ink-on-dark rounded-[5px] p-4 relative overflow-hidden">
|
||||
<div class="absolute -top-6 -right-6 w-16 h-16 rounded-full bg-hub-3 opacity-50"></div>
|
||||
<div class="absolute -bottom-8 -left-8 w-20 h-20 rounded-full bg-hub-3 opacity-30"></div>
|
||||
<div class="relative">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
|
||||
<span class="eyebrow on-dark" style="color:#F4D89C;">Testmodus aktiv</span>
|
||||
</div>
|
||||
<div class="text-[12px] leading-[1.5] text-ink-on-dark-2">
|
||||
Angemeldet als <strong class="text-white font-semibold">Test User</strong>.<br/>
|
||||
Admin: <strong class="text-white font-semibold">Portal Admin</strong>
|
||||
</div>
|
||||
<button class="mt-3 w-full px-3 py-2 bg-white text-hub text-[12px] font-semibold rounded-[3px] hover:bg-bg transition-colors flex items-center justify-center gap-1.5">
|
||||
<svg width="11" height="11" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M9 3L3 9M3 9H8M3 9V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
Zurück zum Admin
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resources -->
|
||||
<div class="px-3 pb-5 border-t border-bg-rule pt-4">
|
||||
<div class="nav-section">Resources</div>
|
||||
<div class="space-y-0.5">
|
||||
<a class="nav-item" href="#"><svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 11l4 2 6-9-4-1z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>Tailwind CSS</a>
|
||||
<a class="nav-item" href="#"><svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M8 2l5 2v4c0 3-2 5-5 6-3-1-5-3-5-6V4z" stroke="currentColor" stroke-width="1.3"/></svg>Hero Icons</a>
|
||||
<a class="nav-item" href="#"><svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M9 2L4 9h3l-1 5 5-7H8z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>Flux UI</a>
|
||||
<a class="nav-item" href="#"><svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 4l5-2 5 2v8l-5 2-5-2z" stroke="currentColor" stroke-width="1.3"/><path d="M3 4l5 2 5-2M8 6v8" stroke="currentColor" stroke-width="1.3"/></svg>Repository</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- =============================================
|
||||
MAIN — Inhalt
|
||||
============================================== -->
|
||||
<main class="flex-1 min-w-0">
|
||||
|
||||
<!-- Topbar — sehr schlank; Brücken-Kontext + Kontoinfo rechts -->
|
||||
<div class="bg-bg-elev border-b border-bg-rule">
|
||||
<div class="px-10 py-3 flex items-center gap-6">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="flex items-center gap-2 text-[12px] text-ink-3 font-medium">
|
||||
<a href="Hub Landing presseportale.html" class="hover:text-hub">Hub</a>
|
||||
<span class="text-ink-4">/</span>
|
||||
<span class="text-ink-2">User Backend</span>
|
||||
<span class="text-ink-4">/</span>
|
||||
<span class="text-hub font-semibold">Übersicht</span>
|
||||
</div>
|
||||
|
||||
<span class="flex-1"></span>
|
||||
|
||||
<!-- Reichweiten-Kontext (Brücke zur Hub-CI) -->
|
||||
<span class="bridge-row">
|
||||
<span class="dot-pe"></span> presseecho
|
||||
<span class="text-ink-4 mx-1">·</span>
|
||||
<span class="dot-bp"></span> businessportal24
|
||||
</span>
|
||||
|
||||
<span class="w-px h-5 bg-bg-rule"></span>
|
||||
|
||||
<!-- Search (FluxUI input slot) -->
|
||||
<div class="relative">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" class="absolute left-2.5 top-1/2 -translate-y-1/2 text-ink-3">
|
||||
<circle cx="7" cy="7" r="4.5" stroke="currentColor" stroke-width="1.3"/>
|
||||
<path d="M10.5 10.5L13 13" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<input class="pl-8 pr-3 py-1.5 w-[220px] bg-white border border-bg-rule rounded-[4px] text-[12.5px] placeholder:text-ink-4 focus:outline-none focus:border-hub" placeholder="Suchen…" />
|
||||
<span class="absolute right-2 top-1/2 -translate-y-1/2 text-[10px] font-mono text-ink-4 border border-bg-rule rounded px-1">⌘K</span>
|
||||
</div>
|
||||
|
||||
<button class="relative w-8 h-8 flex items-center justify-center rounded-[4px] hover:bg-bg border border-transparent hover:border-bg-rule">
|
||||
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" class="text-ink-2">
|
||||
<path d="M3.5 7a4.5 4.5 0 119 0v3.5l1 1.5H2.5l1-1.5z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/>
|
||||
<path d="M6.5 13a1.5 1.5 0 003 0" stroke="currentColor" stroke-width="1.3"/>
|
||||
</svg>
|
||||
<span class="absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-accent"></span>
|
||||
</button>
|
||||
|
||||
<a href="#" class="btn-primary text-[12px] py-1.5 px-3.5">
|
||||
<svg width="11" height="11" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M6 2v8M2 6h8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
Neue Mitteilung
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Inhalt -->
|
||||
<div class="px-10 py-8 space-y-6">
|
||||
|
||||
<!-- ============== PAGE HEADER ============== -->
|
||||
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
|
||||
<span class="badge hub dot">User Backend</span>
|
||||
<span class="eyebrow muted">Mein Bereich · A · 01</span>
|
||||
</div>
|
||||
<h1 class="text-[34px] font-bold tracking-[-0.7px] text-ink leading-[1.1] m-0">Mein Dashboard</h1>
|
||||
<p class="text-[13.5px] text-ink-2 leading-[1.55] mt-2 m-0 max-w-[640px]">
|
||||
Willkommen zurück, <strong class="font-semibold text-ink">Test User</strong>. Hier sehen Sie Status und Reichweite Ihres Kundenkontos für presseecho und businessportal24.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 flex-shrink-0">
|
||||
<!-- Zeitraum-Filter (FluxUI dropdown-style) -->
|
||||
<button class="btn-secondary whitespace-nowrap">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3.5" width="11" height="10" stroke="currentColor" stroke-width="1.3"/><path d="M2.5 6h11M5 2v3M11 2v3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
|
||||
Diese Woche
|
||||
<svg width="10" height="10" viewBox="0 0 12 12" fill="none"><path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
|
||||
<!-- Firma-zuordnen-Status — als Banner-Pille, statt im Karte-Header eingesperrt -->
|
||||
<span class="inline-flex items-center gap-2 px-3 py-1.5 bg-warn-soft border border-warn/30 text-accent-deep rounded-[4px] text-[12px] font-semibold whitespace-nowrap">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" class="flex-shrink-0"><path d="M8 2l6 11H2z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M8 7v3M8 11.5v.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
Keine Firma zugeordnet
|
||||
<a href="#" class="underline underline-offset-[3px] decoration-accent-deep/40 hover:decoration-accent-deep">zuordnen →</a>
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ============== STAT-CARDS — KPI-Reihe ============== -->
|
||||
<section class="grid gap-4" style="grid-template-columns:repeat(4,1fr);">
|
||||
|
||||
<article class="stat-card is-primary">
|
||||
<span class="stat-strip"></span>
|
||||
<div class="flex items-baseline justify-between">
|
||||
<div class="stat-label" style="color:#1A2540;">Gesamt</div>
|
||||
<span class="text-[10.5px] font-mono text-ink-3 tracking-[0.14em] uppercase">2026</span>
|
||||
</div>
|
||||
<div class="stat-num">0</div>
|
||||
<div class="mt-3 flex items-center gap-2 text-[11.5px] text-ink-3">
|
||||
<span class="flex items-center gap-1">
|
||||
<svg width="9" height="9" viewBox="0 0 10 10" fill="none"><path d="M5 8V2M2 5l3-3 3 3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
0 ggü. Vormonat
|
||||
</span>
|
||||
</div>
|
||||
<!-- Mini Sparkline placeholder -->
|
||||
<svg class="absolute right-3 bottom-3 opacity-50" width="80" height="22" viewBox="0 0 80 22" fill="none">
|
||||
<path d="M0 18 L10 17 L20 18 L30 16 L40 18 L50 15 L60 17 L70 14 L80 16" stroke="#1A2540" stroke-width="1.2" fill="none"/>
|
||||
<circle cx="80" cy="16" r="1.8" fill="#1A2540"/>
|
||||
</svg>
|
||||
</article>
|
||||
|
||||
<article class="stat-card is-ok">
|
||||
<span class="stat-strip"></span>
|
||||
<div class="flex items-baseline justify-between">
|
||||
<div class="stat-label">Veröffentlicht</div>
|
||||
<span class="badge ok" style="font-size:9.5px;padding:1px 6px;">live</span>
|
||||
</div>
|
||||
<div class="stat-num" style="color:#1F5E2E;">0</div>
|
||||
<div class="mt-3 flex items-center gap-2 text-[11.5px] text-ink-3">
|
||||
auf beiden Portalen
|
||||
</div>
|
||||
<svg class="absolute right-3 bottom-3 opacity-50" width="80" height="22" viewBox="0 0 80 22" fill="none">
|
||||
<path d="M0 19 L10 18 L20 19 L30 17 L40 16 L50 14 L60 13 L70 11 L80 10" stroke="#2E8540" stroke-width="1.2" fill="none"/>
|
||||
<circle cx="80" cy="10" r="1.8" fill="#2E8540"/>
|
||||
</svg>
|
||||
</article>
|
||||
|
||||
<article class="stat-card is-warn">
|
||||
<span class="stat-strip"></span>
|
||||
<div class="flex items-baseline justify-between">
|
||||
<div class="stat-label">In Prüfung</div>
|
||||
<span class="badge warn" style="font-size:9.5px;padding:1px 6px;">Ø 4 h</span>
|
||||
</div>
|
||||
<div class="stat-num" style="color:#8A5E27;">0</div>
|
||||
<div class="mt-3 flex items-center gap-2 text-[11.5px] text-ink-3">
|
||||
redaktionelle Prüfung
|
||||
</div>
|
||||
<svg class="absolute right-3 bottom-3 opacity-50" width="80" height="22" viewBox="0 0 80 22" fill="none">
|
||||
<path d="M0 16 L10 17 L20 14 L30 18 L40 13 L50 17 L60 12 L70 15 L80 11" stroke="#A87A1F" stroke-width="1.2" fill="none"/>
|
||||
<circle cx="80" cy="11" r="1.8" fill="#A87A1F"/>
|
||||
</svg>
|
||||
</article>
|
||||
|
||||
<article class="stat-card is-muted">
|
||||
<span class="stat-strip"></span>
|
||||
<div class="flex items-baseline justify-between">
|
||||
<div class="stat-label">Entwürfe</div>
|
||||
<span class="text-[10.5px] font-mono text-ink-3 tracking-[0.14em] uppercase">privat</span>
|
||||
</div>
|
||||
<div class="stat-num" style="color:#5A6360;">0</div>
|
||||
<div class="mt-3 flex items-center gap-2 text-[11.5px] text-ink-3">
|
||||
gespeichert, nicht eingereicht
|
||||
</div>
|
||||
<svg class="absolute right-3 bottom-3 opacity-50" width="80" height="22" viewBox="0 0 80 22" fill="none">
|
||||
<path d="M0 12 L10 12 L20 12 L30 12 L40 12 L50 12 L60 12 L70 12 L80 12" stroke="#8A918D" stroke-width="1.2" stroke-dasharray="2 2" fill="none"/>
|
||||
</svg>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<!-- ============== ZWEISPALTEN-GRID ============== -->
|
||||
<section class="grid gap-6" style="grid-template-columns:2fr 1fr;">
|
||||
|
||||
<!-- ============== LINKS: Pressemitteilungen-Liste / Empty ============== -->
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="section-eyebrow">Meine letzten Pressemitteilungen</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-[11.5px] text-ink-3">0 von 0</span>
|
||||
<a href="#" class="text-[12px] font-semibold text-hub hover:underline underline-offset-[3px] decoration-hub/30">Alle anzeigen →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State — sauber, ohne überlappende Skelett-Zeilen -->
|
||||
<div class="px-10 py-14 flex flex-col items-center text-center">
|
||||
<!-- Dezentes Tabellen-Icon -->
|
||||
<div class="w-16 h-16 rounded-[6px] bg-hub-soft border border-hub-soft-2 flex items-center justify-center text-hub mb-5 relative">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
|
||||
<rect x="3" y="4" width="14" height="16" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M17 7h4v13H6" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M6 8h8M6 11h8M6 14h5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<span class="absolute -top-1.5 -right-1.5 w-5 h-5 rounded-full bg-accent text-white text-[10px] font-bold flex items-center justify-center font-mono">0</span>
|
||||
</div>
|
||||
<div class="text-[16px] font-semibold text-ink m-0">Noch keine Pressemitteilungen</div>
|
||||
<p class="text-[13px] text-ink-3 leading-[1.55] mt-2 m-0 max-w-[460px]">
|
||||
Starten Sie mit einer ersten Mitteilung für die aktive Firma oder Ihr Kundenkonto. Veröffentlichung erfolgt nach redaktioneller Prüfung auf beiden Portalen.
|
||||
</p>
|
||||
<div class="mt-6 flex items-center gap-2.5">
|
||||
<a href="#" class="btn-primary">
|
||||
<svg width="11" height="11" viewBox="0 0 12 12" fill="none"><path d="M6 2v8M2 6h8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
|
||||
Erste Pressemitteilung erstellen
|
||||
</a>
|
||||
<a href="#" class="btn-secondary">Vorlage öffnen</a>
|
||||
</div>
|
||||
|
||||
<!-- Vorgeschlagene Schritte -->
|
||||
<div class="mt-9 grid gap-3 w-full max-w-[560px]" style="grid-template-columns:repeat(3,1fr);">
|
||||
<div class="text-left px-3 py-2.5 bg-bg-elev border border-bg-rule rounded-[3px]">
|
||||
<div class="font-mono text-[9.5px] tracking-[0.16em] text-accent font-bold mb-1">01</div>
|
||||
<div class="text-[11.5px] font-semibold text-ink leading-tight">Firma zuordnen</div>
|
||||
</div>
|
||||
<div class="text-left px-3 py-2.5 bg-bg-elev border border-bg-rule rounded-[3px]">
|
||||
<div class="font-mono text-[9.5px] tracking-[0.16em] text-accent font-bold mb-1">02</div>
|
||||
<div class="text-[11.5px] font-semibold text-ink leading-tight">Mitteilung verfassen</div>
|
||||
</div>
|
||||
<div class="text-left px-3 py-2.5 bg-bg-elev border border-bg-rule rounded-[3px]">
|
||||
<div class="font-mono text-[9.5px] tracking-[0.16em] text-accent font-bold mb-1">03</div>
|
||||
<div class="text-[11.5px] font-semibold text-ink leading-tight">Zur Prüfung senden</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-5 py-3 bg-bg-elev border-t border-bg-rule flex items-center gap-2.5 text-[11.5px] text-ink-3">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" class="text-hub flex-shrink-0">
|
||||
<path d="M8 1.5l5.5 2.5v4c0 3.3-2.2 5.8-5.5 6.5C4.7 13.8 2.5 11.3 2.5 8V4z" stroke="currentColor" stroke-width="1.3" fill="none"/>
|
||||
<path d="M5.5 8l2 2 3.5-4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Tipp: Geprüfte Mitteilungen erscheinen i. d. R. binnen <strong class="text-ink-2 font-semibold">4 Stunden</strong> werktags auf beiden Portalen.
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- ============== RECHTS: Datenqualität ============== -->
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">Datenqualität</span>
|
||||
<span class="badge warn dot">2 offen</span>
|
||||
</div>
|
||||
<div class="px-5 py-5">
|
||||
<p class="text-[12px] text-ink-3 leading-[1.55] m-0 mb-4">
|
||||
Diese Hinweise helfen, Ihr User Backend vollständig und sauber zu halten.
|
||||
</p>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="hint-card">
|
||||
<span class="hint-ico">
|
||||
<svg width="18" height="18" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="6" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M3 13.5c.7-2.4 2.7-4 5-4s4.3 1.6 5 4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
</span>
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-baseline justify-between gap-3">
|
||||
<div class="text-[13px] font-semibold text-ink leading-tight">Profil unvollständig</div>
|
||||
<span class="font-mono text-[10px] text-ink-3 tracking-[0.10em] uppercase whitespace-nowrap flex-shrink-0">60 %</span>
|
||||
</div>
|
||||
<div class="mt-2 h-1 w-full rounded-full bg-bg-rule-2 overflow-hidden">
|
||||
<div class="h-full rounded-full bg-accent" style="width:60%;"></div>
|
||||
</div>
|
||||
<p class="text-[11.5px] text-ink-3 leading-[1.5] mt-2 m-0">
|
||||
Vorname, Telefon und Pressekontakt fehlen für saubere Kundenakte.
|
||||
</p>
|
||||
<a href="#" class="inline-flex items-center gap-1 text-[11.5px] font-semibold text-accent-deep mt-2 hover:underline underline-offset-[3px] decoration-accent-deep/40">Profil öffnen →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hint-card">
|
||||
<span class="hint-ico">
|
||||
<svg width="18" height="18" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3.5" width="11" height="10" stroke="currentColor" stroke-width="1.4"/><path d="M2.5 6h11" stroke="currentColor" stroke-width="1.4"/></svg>
|
||||
</span>
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-baseline justify-between gap-3">
|
||||
<div class="text-[13px] font-semibold text-ink leading-tight">Rechnungsadresse fehlt</div>
|
||||
<span class="font-mono text-[10px] text-ink-3 tracking-[0.10em] uppercase whitespace-nowrap flex-shrink-0">0 %</span>
|
||||
</div>
|
||||
<div class="mt-2 h-1 w-full rounded-full bg-bg-rule-2 overflow-hidden">
|
||||
<div class="h-full rounded-full bg-accent" style="width:5%;"></div>
|
||||
</div>
|
||||
<p class="text-[11.5px] text-ink-3 leading-[1.5] mt-2 m-0">
|
||||
Hinterlegen Sie eine Rechnungsadresse, damit spätere Buchungen sauber abgerechnet werden können.
|
||||
</p>
|
||||
<a href="#" class="inline-flex items-center gap-1 text-[11.5px] font-semibold text-accent-deep mt-2 hover:underline underline-offset-[3px] decoration-accent-deep/40">Rechnungsadresse ergänzen →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<!-- ============== UNTERER GRID: FIRMEN + REICHWEITE / AKTIVITÄT ============== -->
|
||||
<section class="grid gap-6" style="grid-template-columns:2fr 1fr;">
|
||||
|
||||
<!-- Firmen Card -->
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">Meine Firmen</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="badge hub" style="font-size:9.5px;padding:1px 6px;">0 zugeordnet</span>
|
||||
<a href="#" class="text-[12px] font-semibold text-hub hover:underline underline-offset-[3px] decoration-hub/30">Profil & Firma verwalten →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="grid gap-3" style="grid-template-columns:1fr 1fr;">
|
||||
|
||||
<!-- Empty-Slot Firma 1 -->
|
||||
<div class="relative border border-dashed border-bg-rule rounded-[5px] p-5 hover:border-hub/50 hover:bg-bg-elev transition-colors">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<span class="w-10 h-10 rounded-[4px] border border-bg-rule bg-bg-elev flex items-center justify-center text-ink-4">
|
||||
<svg width="18" height="18" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3.5" width="11" height="10" stroke="currentColor" stroke-width="1.3"/><path d="M2.5 6h11M6 9h1M9 9h1M6 11.5h1M9 11.5h1" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
|
||||
</span>
|
||||
<div>
|
||||
<div class="text-[13.5px] font-semibold text-ink-2">Firma hinzufügen</div>
|
||||
<div class="text-[11px] text-ink-3 mt-0.5">Slot frei · 1 von 3</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-[11.5px] text-ink-3 leading-[1.5] m-0">
|
||||
Pressestellen, für die Sie Mitteilungen erstellen — mit eigenem Logo, Kontaktperson und Themen-Tags.
|
||||
</p>
|
||||
<a href="#" class="absolute top-3 right-3 inline-flex items-center justify-center w-7 h-7 rounded-[3px] bg-white border border-bg-rule text-hub hover:border-hub">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M6 2v8M2 6h8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Hinweis -->
|
||||
<div class="bg-bg-elev border border-bg-rule rounded-[5px] p-5">
|
||||
<div class="eyebrow muted mb-2">Hinweis</div>
|
||||
<div class="text-[13px] text-ink-2 leading-[1.55] m-0">
|
||||
Keine Firmen zugeordnet. Wenn hier eine Firma fehlen sollte, prüfen Sie bitte Ihr Profil oder wenden Sie sich an den Support.
|
||||
</div>
|
||||
<div class="mt-3 flex items-center gap-2">
|
||||
<a href="#" class="btn-secondary" style="padding:6px 10px;font-size:11.5px;">Profil prüfen</a>
|
||||
<a href="/cdn-cgi/l/email-protection#d1a2a4a1a1bea3a591a1a3b4a2a2b4a1bea3a5b0bdb4ffb2bebc" class="text-[11.5px] font-semibold text-ink-3 hover:text-hub">Support kontaktieren</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Aktivität / Bridge -->
|
||||
<article class="panel-dark">
|
||||
<div class="panel-head">
|
||||
<span class="eyebrow on-dark">Brand-Bridge</span>
|
||||
<span class="font-mono text-[10px] text-ink-on-dark-3 tracking-[0.14em] uppercase">A · B</span>
|
||||
</div>
|
||||
<div class="px-5 py-5">
|
||||
<div class="text-[12.5px] leading-[1.55] text-ink-on-dark-2 m-0 mb-4">
|
||||
Ein Konto, beide Portale — Veröffentlichungen werden parallel auf presseecho und businessportal24 ausgespielt.
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3" style="grid-template-columns:1fr 1fr;">
|
||||
<div class="bg-hub-2 rounded-[4px] px-3.5 py-3 border border-white/5">
|
||||
<div class="flex items-center gap-2 mb-1.5">
|
||||
<span class="dot-pe"></span>
|
||||
<span class="text-[11px] font-bold tracking-[0.14em] uppercase text-white/85">presseecho</span>
|
||||
</div>
|
||||
<div class="font-mono text-[15px] font-semibold text-white" style="font-variant-numeric:tabular-nums;">verbunden</div>
|
||||
<div class="text-[10.5px] text-ink-on-dark-3 mt-0.5">Archiv · Branchen-Tiefe</div>
|
||||
</div>
|
||||
<div class="bg-hub-2 rounded-[4px] px-3.5 py-3 border border-white/5">
|
||||
<div class="flex items-center gap-2 mb-1.5">
|
||||
<span class="dot-bp"></span>
|
||||
<span class="text-[11px] font-bold tracking-[0.14em] uppercase text-white/85">businessportal24</span>
|
||||
</div>
|
||||
<div class="font-mono text-[15px] font-semibold text-white" style="font-variant-numeric:tabular-nums;">verbunden</div>
|
||||
<div class="text-[10.5px] text-ink-on-dark-3 mt-0.5">Wirtschaft · Live</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="mt-5 mb-4" style="border:0;height:1px;background:rgba(255,255,255,.10);" />
|
||||
|
||||
<div class="space-y-2 text-[11.5px] text-ink-on-dark-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>API-Status</span>
|
||||
<span class="flex items-center gap-1.5 text-white"><span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>operational</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Letzte Synchronisation</span>
|
||||
<span class="font-mono text-white">vor 2 min</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Tarif</span>
|
||||
<span class="font-mono text-white">Starter</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="Hub Landing presseportale.html#tarife" class="mt-5 inline-flex items-center gap-1.5 text-[11.5px] font-semibold text-white hover:underline underline-offset-[3px] decoration-white/30">
|
||||
Tarife & Add-ons ansehen →
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<!-- ============== FUSSZEILE ============== -->
|
||||
<footer class="flex items-center justify-between pt-4 pb-2 text-[11px] text-ink-3 border-t border-bg-rule">
|
||||
<span>© 2026 presseportale.com · Publisher-Hub</span>
|
||||
<span class="flex items-center gap-5">
|
||||
<a href="#" class="hover:text-hub">Tastenkürzel</a>
|
||||
<a href="#" class="hover:text-hub">Changelog</a>
|
||||
<a href="#" class="hover:text-hub">Statusseite</a>
|
||||
<a href="/cdn-cgi/l/email-protection#04777174746b767044747661777761746b76706568612a676b69" class="hover:text-hub">Support</a>
|
||||
</span>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script></body>
|
||||
</html>
|
||||
|
|
@ -165,7 +165,7 @@
|
|||
<span class="absolute left-0 top-0 bottom-0 w-[3px] bg-brand"></span>
|
||||
<div>
|
||||
<div class="eyebrow mb-2.5 text-[10.5px]" style="color:#FF8B6F;">Einreichen im Publisher-Bereich</div>
|
||||
<h3 class="font-serif m-0 text-[23px] font-semibold leading-[1.25] tracking-[-0.3px] text-ink-on-dark">Die Einreichung läuft über presseportale.com.</h3>
|
||||
<h3 class="font-serif m-0 text-[23px] font-semibold leading-[1.25] tracking-[-0.3px] text-ink-on-dark">Die Einreichung läuft über pressekonto.de.</h3>
|
||||
<p class="mt-2.5 mb-0 text-[13.5px] leading-[1.55] text-ink-on-dark-2 max-w-[540px]">Dort verwalten Sie Mitteilungen, Credits und Newsroom — einmaliges Konto, beide Portale nutzbar (businessportal24 & presseecho.de).</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-end gap-2.5">
|
||||
|
|
@ -342,7 +342,7 @@
|
|||
|
||||
<p class="mt-4.5 mb-0 text-[12px] text-ink-3" style="margin-top:18px;">
|
||||
Die Veröffentlichung erfolgt über den zentralen Publisher-Bereich auf
|
||||
<a href="#" class="text-brand font-medium border-b border-brand">presseportale.com</a>.
|
||||
<a href="#" class="text-brand font-medium border-b border-brand">pressekonto.de</a>.
|
||||
Cross-Publishing nach presseecho.de ist optional verfügbar.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -583,7 +583,7 @@
|
|||
Zum Publisher-Bereich
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" class="ml-0.5"><path d="M4 8L8.5 3.5M8.5 3.5H5M8.5 3.5V7" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
<span class="text-[12px] text-ink-on-dark-2">Einreichung läuft über presseportale.com · Login per Magic-Link</span>
|
||||
<span class="text-[12px] text-ink-on-dark-2">Einreichung läuft über pressekonto.de · Login per Magic-Link</span>
|
||||
<a href="#" class="mt-1.5 text-[13px] text-ink-on-dark border-b pb-0.5" style="border-color:rgba(255,255,255,0.35);">Oder zuerst Beispiele ansehen →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -597,7 +597,7 @@
|
|||
<div class="grid pb-7" style="grid-template-columns:1.5fr 1fr 1fr 1fr;gap:56px;border-bottom:1px solid rgba(255,255,255,0.08);">
|
||||
<div>
|
||||
<div class="font-serif text-[22px] font-semibold" style="letter-spacing:-0.4px;">businessportal<span class="text-brand">24</span></div>
|
||||
<p class="mt-3 text-[12.5px] text-ink-on-dark-2 leading-[1.6] max-w-[380px]">businessportal24 ist ein Service der Presseportale-Gruppe. Plattform für Pressemitteilungen mittelständischer Unternehmen, Selbstständiger und PR-Agenturen im deutschsprachigen Raum.</p>
|
||||
<p class="mt-3 text-[12.5px] text-ink-on-dark-2 leading-[1.6] max-w-[380px]">businessportal24 ist ein Service der Pressekonto-Gruppe. Plattform für Pressemitteilungen mittelständischer Unternehmen, Selbstständiger und PR-Agenturen im deutschsprachigen Raum.</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="eyebrow mb-3.5 text-[10px]" style="color:rgba(255,255,255,0.5);">Einreichen</div>
|
||||
|
|
@ -628,7 +628,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="mt-5.5 flex justify-between items-center flex-wrap gap-4 text-[12px] text-ink-on-dark-2" style="margin-top:22px;">
|
||||
<div>© 2026 Presseportale-Gruppe · Alle Rechte vorbehalten</div>
|
||||
<div>© 2026 Pressekonto-Gruppe · Alle Rechte vorbehalten</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-ink-on-dark-2">Für fachlich-spezifische Themen:</span>
|
||||
<a href="#" class="text-ink-on-dark border-b pb-px" style="border-color:rgba(255,255,255,0.3);">presseecho.de →</a>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ Diese Frontends sollen künftig **gegen das neue Backend arbeiten** – via dire
|
|||
|
||||
### 1.3 Neues Backend (Aufgabe dieses Projekts)
|
||||
|
||||
- Läuft unter `presseportale.test` / `presseportale.com`
|
||||
- Läuft unter `pressekonto.test` / `pressekonto.de`
|
||||
- Stack: **Laravel 12, PHP 8.4, Livewire 4, Volt, Flux UI 2, MySQL 8, Tailwind 4**
|
||||
- **Vorarbeiten bereits vorhanden**: Admin-UI-Gerüst, Routes, Auth-Stack
|
||||
- **Noch zu tun**: Eloquent-Models, Migrations, Services, Daten-Migration, API, Payment (Stripe), Cron-Jobs
|
||||
|
|
@ -58,7 +58,7 @@ Diese Frontends sollen künftig **gegen das neue Backend arbeiten** – via dire
|
|||
|
||||
| Kriterium | Messbar an |
|
||||
|---|---|
|
||||
| Backend unter `presseportale.test` erreichbar | HTTP 200 auf Login-Seite |
|
||||
| Backend unter `pressekonto.test` erreichbar | HTTP 200 auf Login-Seite |
|
||||
| Admin-Login (Fortify + 2FA) funktioniert | Feature-Test `AuthTest` |
|
||||
| Customer-Portal via Magic-Link erreichbar | Feature-Test `MagicLinkLoginTest` |
|
||||
| Pressemitteilungen CRUD (Admin + Customer) | Livewire-Tests `PressReleaseIndex/Create/Edit/Show` |
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Presseportale Backend (presseportale.test) │
|
||||
│ Pressekonto Backend (pressekonto.test) │
|
||||
│ Laravel 12 · PHP 8.4 · Livewire 4 · Volt · Flux UI 2 │
|
||||
│ │
|
||||
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌─────────┐ │
|
||||
|
|
@ -268,7 +268,7 @@ app/
|
|||
|
||||
## 6. Multi-Domain & Portal-Scope
|
||||
|
||||
- Pro Domain ein Frontend, aber alle Admin-Screens auf `presseportale.test`.
|
||||
- Pro Domain ein Frontend, aber alle Admin-Screens auf `pressekonto.test`.
|
||||
- Middleware `SetCurrentPortal`:
|
||||
- Web (presseecho.test / businessportal24.test) → `app()->instance('current_portal', Portal::Presseecho|Businessportal24)`
|
||||
- Admin → Portal-Auswahl via Dropdown, gespeichert in Session (`current_portal`)
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ Die Views existieren bereits als Blade/Livewire-Stubs (`resources/views/admin/*`
|
|||
| 11.3 | Produktiv-Import-**Rehearsal** gegen aktuellen Legacy-Snapshot | ⬜ |
|
||||
| 11.4 | **Go-Live-Mailing**: Passwort-Reset + Sicherheitshinweis an alle User | 🔄 Command/Mailable fertig; Versand und finale Texte offen |
|
||||
| 11.5 | **API-Kunden-Kommunikation** (Token-Migration) | 🔄 Kundenreport fertig; Freigabeliste und operative Mailtexte offen |
|
||||
| 11.6 | DNS-Cutover-Plan: `presseportale.com` auf neuen Server | ⬜ |
|
||||
| 11.6 | DNS-Cutover-Plan: `pressekonto.de` auf neuen Server | ⬜ |
|
||||
| 11.7 | Read-Only-Modus auf Legacy-Systemen während Final-Import | ⬜ |
|
||||
| 11.8 | Abschalten der alten Symfony-Server (nach Review-Periode) | ⬜ |
|
||||
|
||||
|
|
|
|||
|
|
@ -175,8 +175,8 @@ Content-Type: application/json
|
|||
|
||||
{
|
||||
"message": "Legacy API keys are no longer supported.",
|
||||
"migration_url": "https://presseportale.com/account/tokens",
|
||||
"docs_url": "https://presseportale.com/docs/api/v1"
|
||||
"migration_url": "https://pressekonto.de/account/tokens",
|
||||
"docs_url": "https://pressekonto.de/docs/api/v1"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ php artisan test --compact tests/Feature/LegacyInvoiceArchiveCommandTest.php tes
|
|||
|
||||
## 2026-05-04 – Architektur-Entscheidung: ein gemeinsames Admin-Panel mit rollenbasierter Sichtbarkeit
|
||||
|
||||
Der Auftraggeber hat festgelegt: **es gibt nur ein gemeinsames Admin-Panel** unter dem Presseportale-Backend. Admins, Editoren und Customer arbeiten im selben UI – Sichtbarkeit von Menüpunkten und Aktionen entscheidet die Rolle/Permission.
|
||||
Der Auftraggeber hat festgelegt: **es gibt nur ein gemeinsames Admin-Panel** unter dem Pressekonto-Backend. Admins, Editoren und Customer arbeiten im selben UI – Sichtbarkeit von Menüpunkten und Aktionen entscheidet die Rolle/Permission.
|
||||
|
||||
- Aktuell laufen Admin-/Editor-Funktionen unter `/admin/*` und Customer-Funktionen unter `/customer/*`. Diese Trennung wird **vor dem Pressemitteilungs-Veröffentlichungs-Block** in eine **gemeinsame Panel-Architektur** überführt.
|
||||
- Sidebar wird rollenbasiert gefiltert (Admin/Editor sehen alle PMs, Customer nur seine etc.). Routen-Konsolidierung Customer → Admin-Panel.
|
||||
|
|
@ -318,7 +318,7 @@ Status in `03-MIGRATION-PLAN.md` ist auf `⏸️ Vertagt 2026-05-04` gesetzt.
|
|||
### Was wurde gemacht
|
||||
|
||||
#### Paket A – Test-Suite stabilisiert
|
||||
- `phpunit.xml`: `APP_URL=https://presseportale.test` ergänzt, damit `route('login')` etc. nicht mehr auf eine fremde Domain ohne Auth-Routen zeigen.
|
||||
- `phpunit.xml`: `APP_URL=https://pressekonto.test` ergänzt, damit `route('login')` etc. nicht mehr auf eine fremde Domain ohne Auth-Routen zeigen.
|
||||
- `tests/Feature/Auth/EmailVerificationTest.php`: Tests werden jetzt sauber per `markTestSkipped()` übersprungen, solange `Features::emailVerification()` in `config/fortify.php` deaktiviert ist. Sobald das Feature aktiviert wird, laufen die Tests automatisch wieder.
|
||||
- `tests/Feature/ExampleTest.php`: testet jetzt den Health-Endpoint `/up` statt der von Vite/Theme-Layout abhängigen Startseite.
|
||||
- Leerer `tests/Feature/Feature/` / `tests/Feature/Feature/Billing/`-Doppelpfad entfernt.
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ Hängt komplett an §2 (Stripe-Account + Produktliste).
|
|||
| 11.3 | Produktiv-Import-**Rehearsal** gegen aktuellen Legacy-Snapshot | 🔴 | ⬜ |
|
||||
| 11.4 | Go-Live-Mailing (Passwort-Reset + Sicherheitshinweis) versenden | 🔴 | 🔄 (Code fertig, Texte/Versand offen) |
|
||||
| 11.5 | API-Kunden-Kommunikation (Token-Migration) versenden | 🔴 | 🔄 (Report fertig, Texte/Versand offen) |
|
||||
| 11.6 | DNS-Cutover-Plan: `presseportale.com` auf neuen Server | 🔴 | ⬜ |
|
||||
| 11.6 | DNS-Cutover-Plan: `pressekonto.de` auf neuen Server | 🔴 | ⬜ |
|
||||
| 11.7 | Read-Only-Modus auf Legacy-Systemen während Final-Import | 🔴 | ⬜ |
|
||||
| 11.8 | Abschalten der alten Symfony-Server (nach Review-Periode) | 🟡 | ⬜ |
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ Stand: 2026-05-04. Dieses Kurz-Runbook spiegelt den aktuell implementierten Comm
|
|||
php artisan legacy:import --source=all --dry-run
|
||||
php artisan legacy:archive-invoices --dry-run
|
||||
php artisan legacy:verify --no-report
|
||||
php artisan legacy:migrate-media --portal=all --type=all --base-path=dev/migration --dry-run
|
||||
|
||||
```
|
||||
|
||||
Hinweis: `legacy:archive-invoices` importiert die Legacy-Rechnungen vollständig in `legacy_invoices`, inkl. Status/User-Zuordnung, `raw_snapshot`, `pdf_payload` und Report. Die PDF-Erzeugung erfolgt im Customer-Bereich bei Abruf aus diesen Archivdaten.
|
||||
|
|
@ -38,6 +40,7 @@ php artisan legacy:import --source=all --force
|
|||
php artisan legacy:archive-invoices
|
||||
php artisan legacy:fix-timestamps
|
||||
php artisan legacy:verify
|
||||
php artisan legacy:migrate-media --portal=all --type=all --base-path=dev/migration
|
||||
```
|
||||
|
||||
## Noch nicht im Runbook finalisiert
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Migration 2026 – Presseportale Backend
|
||||
# Migration 2026 – Pressekonto Backend
|
||||
|
||||
> **Migration des Backends** der beiden Legacy-Portale `presseecho` und `businessportal24` (Symfony 1.4, PHP 5.6) in ein **gemeinsames, modernes Laravel 12 Backend** unter der Domain `presseportale.test` / `presseportale.com`.
|
||||
> **Migration des Backends** der beiden Legacy-Portale `presseecho` und `businessportal24` (Symfony 1.4, PHP 5.6) in ein **gemeinsames, modernes Laravel 12 Backend** unter der Domain `pressekonto.test` / `pressekonto.de`.
|
||||
|
||||
**Start:** 23.04.2026
|
||||
**Lead-Technologie:** Laravel 12 · PHP 8.4 · Livewire 4 · Volt · Flux UI 2 · Tailwind CSS 4 · MySQL 8
|
||||
|
|
@ -43,7 +43,7 @@ Historisch existieren zwei technisch identische Symfony-1.4-Installationen (`pre
|
|||
- **DB-Dumps** (2026-04-23): `businessportal24` 42 Tabellen / 578 MB, `presseecho` 43 Tabellen / 369 MB – 41 Tabellen sind strukturell identisch, 3 abweichend (`press_release_image_old` nur BP24, `category_pe_data` + `press_release_pe_data` nur PE)
|
||||
|
||||
Aktueller Implementierungsstand (Code-Abgleich 2026-04-29):
|
||||
- ✅ **Domain-basiertes Theme-System** (`presseportale` / `presseecho` / `businessportal24`)
|
||||
- ✅ **Domain-basiertes Theme-System** (`pressekonto` / `presseecho` / `businessportal24`)
|
||||
- ✅ **Admin-UI** mit echten Daten für Dashboard, Users/Roles, Companies, Contacts, Categories-Index und PressRelease-CRUD/-Workflow
|
||||
- ✅ **Auth-Stack**: Fortify, Sanctum, Spatie/Permission
|
||||
- ✅ **Admin-Schutz und Portal-Scoping**: `EnsureUserIsAdmin`, `SetCurrentPortal`, `PortalScope`, Sidebar-Portal-Switcher
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ services:
|
|||
DB_PORT: 3306
|
||||
# Hier definieren wir nur die Haupt-Datenbank für .env
|
||||
# Die anderen beiden richtest du in Laravel ein
|
||||
DB_DATABASE: presseportale
|
||||
DB_DATABASE: pressekonto
|
||||
DB_USERNAME: root
|
||||
DB_PASSWORD: password
|
||||
MAIL_HOST: global-mailpit
|
||||
|
|
@ -40,23 +40,23 @@ services:
|
|||
labels:
|
||||
- "traefik.enable=true"
|
||||
# Portal Domain
|
||||
- "traefik.http.routers.presseportale.rule=Host(`presseportale.test`)"
|
||||
- "traefik.http.routers.presseportale.entrypoints=websecure"
|
||||
- "traefik.http.routers.presseportale.tls=true"
|
||||
- "traefik.http.routers.presseportale.service=presseportale-service-prc"
|
||||
- "traefik.http.routers.pressekonto.rule=Host(`pressekonto.test`)"
|
||||
- "traefik.http.routers.pressekonto.entrypoints=websecure"
|
||||
- "traefik.http.routers.pressekonto.tls=true"
|
||||
- "traefik.http.routers.pressekonto.service=pressekonto-service-prc"
|
||||
# Presseecho Domain
|
||||
- "traefik.http.routers.presseecho.rule=Host(`presseecho.test`)"
|
||||
- "traefik.http.routers.presseecho.entrypoints=websecure"
|
||||
- "traefik.http.routers.presseecho.tls=true"
|
||||
- "traefik.http.routers.presseecho.service=presseportale-service-prc"
|
||||
- "traefik.http.routers.presseecho.service=pressekonto-service-prc"
|
||||
# Business Portal Domain
|
||||
- "traefik.http.routers.businessportal.rule=Host(`businessportal24.test`)"
|
||||
- "traefik.http.routers.businessportal.entrypoints=websecure"
|
||||
- "traefik.http.routers.businessportal.tls=true"
|
||||
- "traefik.http.routers.businessportal.service=presseportale-service-prc"
|
||||
- "traefik.http.routers.businessportal.service=pressekonto-service-prc"
|
||||
|
||||
# Asset Domain für Vite-Server Portal (Port 5177)
|
||||
- "traefik.http.routers.assets-portal.rule=Host(`assets.presseportale.test`)"
|
||||
- "traefik.http.routers.assets-portal.rule=Host(`assets.pressekonto.test`)"
|
||||
- "traefik.http.routers.assets-portal.entrypoints=websecure"
|
||||
- "traefik.http.routers.assets-portal.tls=true"
|
||||
- "traefik.http.routers.assets-portal.service=assets-portal-service-prc"
|
||||
|
|
@ -74,7 +74,7 @@ services:
|
|||
- "traefik.http.routers.assets-businessportal.service=assets-web-service-prc"
|
||||
|
||||
# Service Definition - NUR EINMAL!
|
||||
- "traefik.http.services.presseportale-service-prc.loadbalancer.server.port=80"
|
||||
- "traefik.http.services.pressekonto-service-prc.loadbalancer.server.port=80"
|
||||
- "traefik.http.services.assets-portal-service-prc.loadbalancer.server.port=5177"
|
||||
- "traefik.http.services.assets-portal-service-prc.loadbalancer.server.scheme=http"
|
||||
- "traefik.http.services.assets-web-service-prc.loadbalancer.server.port=5178"
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
</source>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="APP_URL" value="https://presseportale.test"/>
|
||||
<env name="APP_URL" value="https://pressekonto.test"/>
|
||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,23 @@
|
|||
@import "tailwindcss";
|
||||
@import "../../vendor/livewire/flux/dist/flux.css";
|
||||
|
||||
/**
|
||||
* Hub × FluxUI — Phase 1: Portal-Shell auf Hub-Design.
|
||||
*
|
||||
* Tokens leben in shared/design-tokens.css (Single Source of Truth aus Phase 0).
|
||||
* Hier:
|
||||
* 1. Tokens importieren
|
||||
* 2. Zinc-Skala auf Hub-Buchpapier-Familie mappen (für FluxUI-Komponenten,
|
||||
* die ihre Skala-Defaults aus Zinc beziehen)
|
||||
* 3. FluxUI-Akzent (--color-accent) auf Hub-Blau umstellen
|
||||
* 4. FluxUI-Komponenten via [data-flux-*]-Selektoren ans Hub-Design angleichen
|
||||
* (Sidebar, Navlist-Active, Primary-Buttons, Cards)
|
||||
*
|
||||
* Dokumentation: dev/frontend/hub-flux/02-PHASE-1-PORTAL-SHELL.md
|
||||
*/
|
||||
@import "./shared/design-tokens.css";
|
||||
@import "./shared/hub-components.css";
|
||||
|
||||
@source '../views';
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../vendor/livewire/flux-pro/stubs/**/*.blade.php';
|
||||
|
|
@ -9,34 +26,46 @@
|
|||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
--font-sans: "Instrument Sans", ui-sans-serif, system-ui, sans-serif,
|
||||
/* Font: Inter Tight statt Instrument Sans
|
||||
(Token --font-sans aus design-tokens.css wird hier nochmal explizit
|
||||
gesetzt, weil FluxUI zuvor seinen eigenen Wert setzen würde) */
|
||||
--font-sans:
|
||||
"Inter Tight", Inter, ui-sans-serif, system-ui, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
||||
"Noto Color Emoji";
|
||||
|
||||
--color-zinc-50: var(--color-neutral-50);
|
||||
--color-zinc-100: var(--color-neutral-100);
|
||||
--color-zinc-200: var(--color-neutral-200);
|
||||
--color-zinc-300: var(--color-neutral-300);
|
||||
--color-zinc-400: var(--color-neutral-400);
|
||||
--color-zinc-500: var(--color-neutral-500);
|
||||
--color-zinc-600: var(--color-neutral-600);
|
||||
--color-zinc-700: var(--color-neutral-700);
|
||||
--color-zinc-800: var(--color-neutral-800);
|
||||
--color-zinc-900: var(--color-neutral-900);
|
||||
--color-zinc-950: var(--color-neutral-950);
|
||||
/* FluxUI-Akzent (für Buttons, Focus-Rings, Active-States) auf Hub-Blau.
|
||||
Vorher: #3ea3dc (Starter-Kit-Türkis). */
|
||||
--color-accent: var(--color-hub);
|
||||
--color-accent-content: var(--color-hub);
|
||||
--color-accent-foreground: #ffffff;
|
||||
|
||||
--color-accent: #3ea3dc;
|
||||
--color-accent-content: #3ea3dc;
|
||||
--color-accent-foreground: var(--color-white);
|
||||
/* Zinc-Skala auf warmes Buchpapier mappen.
|
||||
FluxUI nutzt Zinc als neutrale Skala für Sidebars, Borders, Text.
|
||||
Wir bridgen die ganze Skala, damit der Look automatisch auf Hub-Stil
|
||||
umschwenkt — ohne dass wir hunderte FluxUI-Klassen einzeln umbiegen
|
||||
müssen. */
|
||||
--color-zinc-50: #fbfaf6;
|
||||
--color-zinc-100: #f6f4ef;
|
||||
--color-zinc-200: #e2ddd0;
|
||||
--color-zinc-300: #cfc8b5;
|
||||
--color-zinc-400: #8a918d;
|
||||
--color-zinc-500: #5a6360;
|
||||
--color-zinc-600: #3a413d;
|
||||
--color-zinc-700: #1a1f1c;
|
||||
--color-zinc-800: #243152;
|
||||
--color-zinc-900: #1a2540;
|
||||
--color-zinc-950: #0f1729;
|
||||
}
|
||||
|
||||
@layer theme {
|
||||
.dark {
|
||||
--color-accent: #3ea3dc;
|
||||
--color-accent-content: #5bb8e6;
|
||||
--color-accent-foreground: var(--color-white);
|
||||
}
|
||||
}
|
||||
/* Phase 5: Dark-Mode-Mapping liegt jetzt vollständig in
|
||||
shared/design-tokens.css (`.dark { … }`). FluxUI Appearance-Switcher
|
||||
setzt `class="dark"` auf <html>, alle `--color-*`-Vars schalten
|
||||
automatisch um — inklusive `--color-accent`, weil das oben im @theme
|
||||
per `var(--color-hub)`-Verweis dynamisch ist.
|
||||
|
||||
Der Notfall-Hack aus Phase 1 (`.dark { --color-accent: var(--color-hub) }`)
|
||||
ist damit gegenstandslos und entfernt. */
|
||||
|
||||
@layer base {
|
||||
*,
|
||||
|
|
@ -48,6 +77,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* FluxUI Form-Felder — Layout vom Starter-Kit übernommen
|
||||
* Input-Focus wird weiter unten im Kontrast-Tuning auf Hub-Blau gesetzt.
|
||||
* ============================================================ */
|
||||
[data-flux-field]:not(ui-radio, ui-checkbox) {
|
||||
@apply grid gap-2;
|
||||
}
|
||||
|
|
@ -56,12 +89,165 @@
|
|||
@apply !mb-0 !leading-tight;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Phase 1 — Hub-Style-Overrides für FluxUI-Komponenten
|
||||
* ============================================================
|
||||
*
|
||||
* Strategie: minimal-invasive Overrides via [data-flux-*]-Attribute und
|
||||
* der Tailwind-Klassen, die FluxUI auf seinen Komponenten setzt. Wir
|
||||
* ändern KEINE Vendor-Dateien, sondern legen unsere Styles mit höherer
|
||||
* Spezifität darüber.
|
||||
*
|
||||
* Wichtig: FluxUI nutzt für variant-spezifisches Styling KEINE
|
||||
* `data-variant`-Attribute, sondern direkt Tailwind-Klassen wie
|
||||
* `bg-[var(--color-accent)]` (für variant="primary"). Wir greifen
|
||||
* deshalb über diese Klassen-Selektoren.
|
||||
*
|
||||
* Bei FluxUI-Updates können sich diese Klassen ändern — Selektoren bewusst
|
||||
* konservativ, gut kommentiert. Visueller Smoke-Test pro Release-Bump.
|
||||
* ============================================================ */
|
||||
|
||||
/* Sidebar — warmes Buchpapier statt Zinc-Grau, klare Trennlinie */
|
||||
[data-flux-sidebar] {
|
||||
background: var(--color-bg-elev);
|
||||
border-color: var(--color-bg-rule);
|
||||
}
|
||||
|
||||
/* Sidebar-Section-Headings — Mockup-Konvention:
|
||||
10 px, fett, gesperrt, anthrazit */
|
||||
[data-flux-navlist] [data-flux-navlist-group-heading],
|
||||
[data-flux-navlist] [data-flux-group-heading] {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-ink-4);
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
/* Navlist-Item — Hub-Stil: kompakter, mit Active-Strip links */
|
||||
[data-flux-navlist-item] {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--color-ink-2);
|
||||
border-radius: var(--radius-sm);
|
||||
transition:
|
||||
background 0.12s,
|
||||
color 0.12s;
|
||||
}
|
||||
|
||||
[data-flux-navlist-item]:hover {
|
||||
background: var(--color-bg);
|
||||
color: var(--color-hub);
|
||||
}
|
||||
|
||||
[data-flux-navlist-item][data-current="true"],
|
||||
[data-flux-navlist-item][aria-current="page"],
|
||||
[data-flux-navlist-item].active {
|
||||
background: var(--color-hub-soft);
|
||||
color: var(--color-hub);
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-flux-navlist-item][data-current="true"]::before,
|
||||
[data-flux-navlist-item][aria-current="page"]::before,
|
||||
[data-flux-navlist-item].active::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -1px;
|
||||
top: 6px;
|
||||
bottom: 6px;
|
||||
width: 2px;
|
||||
background: var(--color-hub);
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* FluxUI Buttons — Hub-Stil
|
||||
* ============================================================
|
||||
* FluxUI rendert `<flux:button variant="primary">` als
|
||||
* class="… bg-[var(--color-accent)]
|
||||
* hover:bg-[color-mix(in_oklab,_var(--color-accent),_transparent_10%)]
|
||||
* shadow-[inset_0px_1px_--theme(--color-white/.2)] …"
|
||||
*
|
||||
* `--color-accent` haben wir auf `var(--color-hub)` (#1A2540) gesetzt →
|
||||
* Default-Background passt schon. ABER:
|
||||
*
|
||||
* 1. Der Hover ("10 % transparent") wirkt auf hellem Buchpapier
|
||||
* hellblau — wir wollen statt dessen Hub-2 (#243152, dunkler) wie
|
||||
* auf der Hub-Landing (`hover:bg-hub-2`).
|
||||
* 2. FluxUI's Default-Shadow (1 px Weiß-Inset) braucht auf dem warmen
|
||||
* Hintergrund mehr Kontrast für klare Button-Kanten.
|
||||
*
|
||||
* Wir greifen über `data-flux-button` (Attribut auf dem gerenderten
|
||||
* <button>) plus dem Tailwind-Klassen-Selektor mit escapten Brackets.
|
||||
* !important ist nötig, weil FluxUI's Tailwind-Hover normal höhere
|
||||
* Spezifität hätte (escapter Klassenname). */
|
||||
|
||||
/* Primary-Button-Hover: dunkler statt heller. */
|
||||
[data-flux-button].hover\:bg-\[color-mix\(in_oklab\,_var\(--color-accent\)\,_transparent_10\%\)\]:hover {
|
||||
background-color: var(--color-hub-2) !important;
|
||||
}
|
||||
|
||||
/* Primary-Button: kräftigerer Shadow für klare Button-Kanten auf hellem Bg.
|
||||
Schatten-Farbton hängt vom Mode ab: Light = warmer Hub-Blau-Alpha,
|
||||
Dark = neutraler Schwarz-Alpha (sonst wirkt der hub-blaue Schatten
|
||||
auf dunklem Card-BG zu sichtbar). */
|
||||
[data-flux-button].bg-\[var\(--color-accent\)\] {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.18),
|
||||
0 1px 2px rgba(26, 37, 64, 0.25),
|
||||
0 2px 6px -2px rgba(26, 37, 64, 0.18);
|
||||
border-color: var(--color-hub-2);
|
||||
}
|
||||
|
||||
[data-flux-button].bg-\[var\(--color-accent\)\]:hover {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.15),
|
||||
0 2px 4px rgba(26, 37, 64, 0.35),
|
||||
0 3px 10px -2px rgba(26, 37, 64, 0.25);
|
||||
}
|
||||
|
||||
.dark [data-flux-button].bg-\[var\(--color-accent\)\] {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.12),
|
||||
0 1px 2px rgba(0, 0, 0, 0.5),
|
||||
0 2px 6px -2px rgba(0, 0, 0, 0.4);
|
||||
border-color: var(--color-hub);
|
||||
}
|
||||
|
||||
.dark [data-flux-button].bg-\[var\(--color-accent\)\]:hover {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
||||
0 2px 4px rgba(0, 0, 0, 0.6),
|
||||
0 3px 10px -2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Cards — Buchpapier statt Zinc */
|
||||
[data-flux-card] {
|
||||
background: var(--color-bg-card);
|
||||
border-color: var(--color-bg-rule);
|
||||
}
|
||||
|
||||
/* Headings & Heading-Defaults — Hub-Ink-Farbe */
|
||||
[data-flux-heading] {
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Kontrast-Tuning für Inputs, Borders, Trennlinien
|
||||
* ============================================================
|
||||
* Unser Zinc → Buchpapier-Mapping macht `border-zinc-200` zu `#e2ddd0`
|
||||
* (warmes Rule). Auf hellem Buchpapier sind Inputs/Cards damit korrekt
|
||||
* vom Bg trennend, aber für Fokus-States braucht's klar Hub-Blau. */
|
||||
|
||||
/* Input-Focus: Hub-Blau-Ring statt blassen Default */
|
||||
input:focus[data-flux-control],
|
||||
textarea:focus[data-flux-control],
|
||||
select:focus[data-flux-control] {
|
||||
@apply outline-hidden ring-2 ring-accent ring-offset-2 ring-offset-accent-foreground;
|
||||
@apply ring-2 ring-offset-1;
|
||||
--tw-ring-color: var(--color-hub);
|
||||
--tw-ring-offset-color: var(--color-bg-elev);
|
||||
border-color: var(--color-hub) !important;
|
||||
}
|
||||
|
||||
/* \[:where(&)\]:size-4 {
|
||||
@apply size-4;
|
||||
} */
|
||||
|
|
|
|||
248
resources/css/shared/design-tokens.css
Normal file
248
resources/css/shared/design-tokens.css
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
/**
|
||||
* Hub × FluxUI — Gemeinsame Design-Tokens (Single Source of Truth)
|
||||
*
|
||||
* Diese Datei ist die EINZIGE Quelle für Hub-Farben, Fonts, Radii und
|
||||
* Schatten. Sowohl der Web-Build (Hub-Landing, Hub-Auth) als auch der
|
||||
* Portal-Build (User-Panel, Admin-Bereich) importieren diese Datei.
|
||||
*
|
||||
* Konventionen:
|
||||
* - Token-Names sind STABIL. Werte können sich ändern (z.B. Dark Mode),
|
||||
* Namen nicht.
|
||||
* - Light Mode ist Default. Dark Mode kommt in Phase 5.
|
||||
* - FluxUI-Akzent-Variablen (--color-accent, --color-accent-*) werden
|
||||
* in portal.css mit unseren Hub-Werten gebridged, nicht hier.
|
||||
*
|
||||
* Dokumentation: dev/frontend/hub-flux/README.md
|
||||
* dev/frontend/hub-flux/01-PHASE-0-TOKENS.md
|
||||
*/
|
||||
|
||||
@theme {
|
||||
/* =========================================================
|
||||
* SURFACES — warmes Buchpapier, gemeinsame Familie
|
||||
* ========================================================= */
|
||||
--color-bg: #f6f4ef;
|
||||
--color-bg-elev: #fbfaf6;
|
||||
--color-bg-rule: #e2ddd0;
|
||||
--color-bg-rule-2: #ede7d7; /* leicht hellere Variante für Progress-Tracks */
|
||||
--color-bg-rule-strong: #1a1f1c;
|
||||
--color-bg-dark: #15201a;
|
||||
--color-bg-card: #ffffff;
|
||||
--color-bg-card-warm: #efeadc;
|
||||
--color-bg-card-warm-border: #d6cfbb;
|
||||
--color-bg-card-warm-hover: #e6deca;
|
||||
--color-bg-card-warm-rule: #c8bda3;
|
||||
|
||||
/* =========================================================
|
||||
* HUB-BLAU — primärer Akzent, plattform-neutral
|
||||
* Klarer Kontrast zum hellen Buchpapier-Hintergrund.
|
||||
* ========================================================= */
|
||||
--color-hub: #1a2540;
|
||||
--color-hub-2: #243152;
|
||||
--color-hub-3: #2e3d66;
|
||||
--color-hub-soft: #e5e9f1;
|
||||
--color-hub-soft-2: #cfd6e4;
|
||||
--color-hub-line: #7b8fbf;
|
||||
|
||||
/* Topbar-Aliase für Hub-Gradient */
|
||||
--color-topbar: #1a2540;
|
||||
--color-topbar2: #243152;
|
||||
--color-topbar-deep: #0f1729;
|
||||
|
||||
/* =========================================================
|
||||
* AKZENT — gedecktes Bernstein (sekundärer Akzent)
|
||||
* Bewusst NICHT Orange (BP24) und NICHT Grün (Presseecho).
|
||||
* Im Portal für: Notifications, Datenqualität, Empfehlungs-Ribbons.
|
||||
*
|
||||
* Wichtige Trennung (Phase 5 / Dark Mode):
|
||||
* --color-accent — heller Akzent (Light: Bernstein, Dark: heller Bernstein).
|
||||
* In portal.css wird das auf var(--color-hub) umgebogen,
|
||||
* weil FluxUI-Primary-Buttons das nutzen.
|
||||
* --color-accent-warm — KONSTANTER Bernstein (gleicher Wert in beiden Modi).
|
||||
* Für Hint-Card-Border, Schritt-Karten-Eyebrows
|
||||
* und alle Stellen, die explizit Bernstein sein müssen.
|
||||
* --color-accent-deep — gedämpfter Bernstein für Action-Links.
|
||||
* ========================================================= */
|
||||
--color-accent: #b07a3a;
|
||||
--color-accent-deep: #8a5e27;
|
||||
--color-accent-soft: #f1e6d3;
|
||||
--color-accent-warm: #b07a3a;
|
||||
|
||||
/* =========================================================
|
||||
* PANEL-DARK — KONSTANTES dunkles Hub-Blau (in beiden Modi).
|
||||
* Wird von `.panel-dark` und der Brand-Bridge genutzt; soll im
|
||||
* Dark Mode NICHT hell werden, sondern immer dunkel bleiben.
|
||||
* Im Hub-Frontend (Light-Only) zeigt's die gleiche Atmosphäre.
|
||||
* ========================================================= */
|
||||
--color-panel-dark: #0f1729;
|
||||
--color-panel-dark-2: #1a2540;
|
||||
|
||||
/* =========================================================
|
||||
* INK — Anthrazit-Reihe für Text & Linien
|
||||
* ========================================================= */
|
||||
--color-ink: #1a1f1c;
|
||||
--color-ink-2: #3a413d;
|
||||
--color-ink-3: #5a6360;
|
||||
--color-ink-4: #8a918d;
|
||||
--color-ink-on-dark: #f6f4ef;
|
||||
--color-ink-on-dark-2: #b2b9c7;
|
||||
--color-ink-on-dark-3: #7b8fbf;
|
||||
--color-ink-on-dark-muted: #7b8fbf;
|
||||
--color-ink-on-dark-rule: #2a3550;
|
||||
|
||||
/* =========================================================
|
||||
* BRAND-ALIASE für Komponenten, die brand-Tokens nutzen
|
||||
* ========================================================= */
|
||||
--color-brand: #1a2540;
|
||||
--color-brand-deep: #0f1729;
|
||||
--color-brand-soft: #e5e9f1;
|
||||
|
||||
/* =========================================================
|
||||
* STATUS — Erfolg, Warnung, Fehler, Live
|
||||
* Identische Namen wie das User-Dashboard-Mockup verwendet.
|
||||
* ========================================================= */
|
||||
--color-ok: #2e8540;
|
||||
--color-ok-soft: #e2f1e5;
|
||||
--color-warn: #a87a1f;
|
||||
--color-warn-soft: #f6eac8;
|
||||
--color-err: #a8331f;
|
||||
--color-err-soft: #f4dad2;
|
||||
--color-live: #c84a1e;
|
||||
--color-gain: #2e8540;
|
||||
--color-gain-deep: #1f5e2e;
|
||||
--color-loss: #c8341e;
|
||||
|
||||
/* =========================================================
|
||||
* EDITORIAL — für Card-Warm-Sektionen
|
||||
* ========================================================= */
|
||||
--color-card-warm-cat: #5a6360;
|
||||
--color-card-warm-title: #2a302d;
|
||||
--color-feature-line: #c0c8db;
|
||||
--color-feature-dot: #d8dde9;
|
||||
|
||||
/* =========================================================
|
||||
* BRIDGE-DOTS — kleine farbige Punkte für presseecho /
|
||||
* businessportal24 in Brücken-Anzeigen (Sidebar, Topbar,
|
||||
* Bridge-Cards). Werte aus dem User-Dashboard-Mockup.
|
||||
* ========================================================= */
|
||||
--color-bridge-presseecho: #1f4d3a;
|
||||
--color-bridge-businessportal: #c84a1e;
|
||||
|
||||
/* =========================================================
|
||||
* FONTS — Hub: Inter Tight + JetBrains Mono.
|
||||
* Source Serif 4 wird im Hub für die Markennennungen der
|
||||
* Tochter-Portale (presseecho, businessportal24) verwendet;
|
||||
* im Portal nur für Brand-Mark, falls überhaupt.
|
||||
* ========================================================= */
|
||||
--font-sans:
|
||||
"Inter Tight", Inter, system-ui, -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", sans-serif;
|
||||
--font-serif:
|
||||
"Source Serif 4", "Source Serif Pro", Charter, "Iowan Old Style",
|
||||
Georgia, serif;
|
||||
--font-mono:
|
||||
"JetBrains Mono", "SF Mono", ui-monospace, SFMono-Regular, Menlo,
|
||||
Consolas, monospace;
|
||||
|
||||
/* =========================================================
|
||||
* LAYOUT
|
||||
* ========================================================= */
|
||||
--container-layout: 1280px;
|
||||
|
||||
/* =========================================================
|
||||
* RADII — kleines Set, am Mockup orientiert
|
||||
* ========================================================= */
|
||||
--radius-xs: 3px;
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 6px;
|
||||
--radius-lg: 8px;
|
||||
|
||||
/* =========================================================
|
||||
* SCHATTEN — weich, warm-getönt
|
||||
* ========================================================= */
|
||||
--shadow-soft: 0 1px 0 rgba(26, 31, 28, 0.03);
|
||||
--shadow-card: 0 1px 2px 0 rgb(26 37 64 / 0.06);
|
||||
--shadow-card-hover: 0 4px 12px -2px rgb(26 37 64 / 0.12);
|
||||
--shadow-auth:
|
||||
0 1px 0 rgba(26, 31, 28, 0.03),
|
||||
0 20px 50px -32px rgba(26, 37, 64, 0.28);
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* DARK MODE — Phase 5 AKTIV.
|
||||
* Werte aus dev/frontend/tailwind_v3/User Dashboard presseportale Dark.html.
|
||||
* Aktivierung über `.dark` (FluxUI Appearance-Switcher setzt das auf <html>).
|
||||
*
|
||||
* Konventionen:
|
||||
* - Gleiche Token-Namen wie Light Mode (Single Source of Truth).
|
||||
* - `--color-accent-warm`, `--color-panel-dark*` bleiben KONSTANT
|
||||
* (siehe Begründungen oben im Light-Block).
|
||||
* - Hub-Frontend (Landing + Auth) lädt KEIN `@fluxAppearance` und
|
||||
* bleibt damit immer Light. Nur Portal-Seiten schalten um.
|
||||
* ========================================================= */
|
||||
.dark {
|
||||
/* Surfaces */
|
||||
--color-bg: #0e1218;
|
||||
--color-bg-elev: #14181f;
|
||||
--color-bg-rule: #2a3142;
|
||||
--color-bg-rule-2: #232838;
|
||||
--color-bg-rule-strong: #ece9e0;
|
||||
--color-bg-card: #181d27;
|
||||
--color-bg-card-warm: #1f1a12;
|
||||
|
||||
/* Hub-Blau (im Dark Mode HELLER für Lesbarkeit auf dunklem Bg) */
|
||||
--color-hub: #5a78c2;
|
||||
--color-hub-2: #6d8ad3;
|
||||
--color-hub-3: #4a65a8;
|
||||
--color-hub-soft: #1f2a47;
|
||||
--color-hub-soft-2: #2c3a5d;
|
||||
--color-hub-line: #7b8fbf;
|
||||
|
||||
/* Bernstein — heller Akzent (warm-Token bleibt konstant) */
|
||||
--color-accent: #d9a560;
|
||||
--color-accent-deep: #b07a3a;
|
||||
--color-accent-soft: #2a2418;
|
||||
/* --color-accent-warm: konstant #b07a3a (aus Light geerbt) */
|
||||
|
||||
/* Ink */
|
||||
--color-ink: #ece9e0;
|
||||
--color-ink-2: #c9c5b8;
|
||||
--color-ink-3: #8e8b82;
|
||||
--color-ink-4: #5d5c57;
|
||||
|
||||
/* Brand-Aliase */
|
||||
--color-brand: #5a78c2;
|
||||
--color-brand-deep: #4a65a8;
|
||||
--color-brand-soft: #1f2a47;
|
||||
|
||||
/* Status */
|
||||
--color-ok: #4dc076;
|
||||
--color-ok-soft: #1a2d22;
|
||||
--color-warn: #d9a560;
|
||||
--color-warn-soft: #2d2418;
|
||||
--color-err: #e07664;
|
||||
--color-err-soft: #2e1715;
|
||||
--color-gain: #4dc076;
|
||||
--color-gain-deep: #3aa060;
|
||||
--color-loss: #e07664;
|
||||
|
||||
/* Bridge-Dots heller für Dark-Bg */
|
||||
--color-bridge-presseecho: #4da37a;
|
||||
--color-bridge-businessportal: #e36340;
|
||||
|
||||
/* Editorial */
|
||||
--color-card-warm-cat: #8e8b82;
|
||||
--color-card-warm-title: #ece9e0;
|
||||
--color-feature-line: #2c3a5d;
|
||||
--color-feature-dot: #2a3142;
|
||||
|
||||
/* Schatten — im Dark Mode neutral schwarz statt hub-blau-warm */
|
||||
--shadow-soft: 0 1px 0 rgba(0, 0, 0, 0.4);
|
||||
--shadow-card: 0 1px 2px 0 rgba(0, 0, 0, 0.4);
|
||||
--shadow-card-hover: 0 4px 12px -2px rgba(0, 0, 0, 0.55);
|
||||
--shadow-auth:
|
||||
0 1px 0 rgba(0, 0, 0, 0.5),
|
||||
0 20px 50px -32px rgba(0, 0, 0, 0.7);
|
||||
|
||||
/* color-scheme-Hint für native Form-Controls (Scrollbars, Inputs) */
|
||||
color-scheme: dark;
|
||||
}
|
||||
340
resources/css/shared/hub-components.css
Normal file
340
resources/css/shared/hub-components.css
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
/**
|
||||
* Hub-Components — Single Source of Truth für Hub-typische Layout-Bausteine
|
||||
*
|
||||
* Wird von BEIDEN Builds importiert:
|
||||
* - resources/css/portal.css (FluxUI-Portal)
|
||||
* - resources/css/web/shared-styles.css (Web/Hub-Frontend)
|
||||
*
|
||||
* Dadurch DRY für Customer-Dashboard (Phase 2), Admin-Dashboard (Phase 3),
|
||||
* Listen/Detail-Pages (Phase 4) und perspektivisch auch die Hub-Landing.
|
||||
*
|
||||
* Tokens kommen aus shared/design-tokens.css — diese Datei darf KEINE
|
||||
* Hex-Literale enthalten (außer wo bewusst alpha-overlays via `rgba`
|
||||
* gegen White/Black gerechnet werden).
|
||||
*
|
||||
* Vorlage: dev/frontend/tailwind_v3/User Dashboard presseportale.html
|
||||
* Dokumentation: dev/frontend/hub-flux/04-PHASE-2-CUSTOMER-DASHBOARD.md
|
||||
*/
|
||||
|
||||
@layer components {
|
||||
/* ============================================================
|
||||
* Panels (Karten-Container mit Hub-Charakter)
|
||||
* ============================================================ */
|
||||
|
||||
.panel {
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-bg-rule);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.panel-warm {
|
||||
background: var(--color-bg-elev);
|
||||
border: 1px solid var(--color-bg-rule);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.panel-dark {
|
||||
/* KONSTANTES Dark-Hub-Blau — bleibt auch im Dark Mode dunkel
|
||||
(sonst würde `var(--color-hub)` zum hellen #5a78c2 werden).
|
||||
Tokens `--color-panel-dark` und `--color-panel-dark-2` sind
|
||||
in beiden Modi identisch. */
|
||||
background: var(--color-panel-dark-2);
|
||||
border: 1px solid var(--color-panel-dark);
|
||||
border-radius: 6px;
|
||||
color: var(--color-ink-on-dark);
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 20px;
|
||||
border-bottom: 1px solid var(--color-bg-rule);
|
||||
}
|
||||
.panel-dark .panel-head {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Stat-Cards (KPI-Karten mit farbigem Strip links)
|
||||
* ============================================================ */
|
||||
|
||||
.stat-card {
|
||||
position: relative;
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-bg-rule);
|
||||
border-radius: 6px;
|
||||
padding: 18px 20px;
|
||||
transition:
|
||||
border-color 0.12s,
|
||||
box-shadow 0.12s;
|
||||
}
|
||||
.stat-card .stat-strip {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: var(--color-hub-soft-2);
|
||||
border-radius: 6px 0 0 6px;
|
||||
}
|
||||
.stat-card.is-primary .stat-strip {
|
||||
background: var(--color-hub);
|
||||
}
|
||||
.stat-card.is-ok .stat-strip {
|
||||
background: var(--color-ok);
|
||||
}
|
||||
.stat-card.is-warn .stat-strip {
|
||||
background: var(--color-warn);
|
||||
}
|
||||
.stat-card.is-muted .stat-strip {
|
||||
background: var(--color-ink-4);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-ink-3);
|
||||
}
|
||||
.stat-card.is-primary .stat-label {
|
||||
color: var(--color-hub);
|
||||
}
|
||||
.stat-card.is-ok .stat-label {
|
||||
color: var(--color-ok);
|
||||
}
|
||||
.stat-card.is-warn .stat-label {
|
||||
color: var(--color-warn);
|
||||
}
|
||||
.stat-card.is-muted .stat-label {
|
||||
color: var(--color-ink-4);
|
||||
}
|
||||
|
||||
.stat-num {
|
||||
font-family: var(--font-mono);
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: 36px;
|
||||
font-weight: 600;
|
||||
color: var(--color-ink);
|
||||
letter-spacing: -0.5px;
|
||||
line-height: 1;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.stat-card.is-ok .stat-num {
|
||||
color: var(--color-ok);
|
||||
}
|
||||
.stat-card.is-warn .stat-num {
|
||||
color: var(--color-warn);
|
||||
}
|
||||
.stat-card.is-muted .stat-num {
|
||||
color: var(--color-ink-3);
|
||||
}
|
||||
|
||||
.stat-meta {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10.5px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-ink-3);
|
||||
}
|
||||
|
||||
.stat-trend {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 11.5px;
|
||||
color: var(--color-ink-3);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Hint-Cards (Datenqualitäts-Hinweise mit Progress-Bar)
|
||||
* ============================================================ */
|
||||
|
||||
.hint-card {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: start;
|
||||
/* Im Light Mode warmes Buchpapier-elev; im Dark Mode der wärmere
|
||||
Card-Ton (`--color-bg-card-warm` schaltet automatisch um). */
|
||||
background: var(--color-bg-card-warm);
|
||||
border: 1px solid var(--color-bg-rule);
|
||||
/* `--color-accent-warm` bleibt KONSTANT Bernstein (Phase 5):
|
||||
Im Portal mappt --color-accent auf Hub-Blau, hier wollen wir
|
||||
aber den klassischen Bernstein-Border behalten. */
|
||||
border-left: 3px solid var(--color-accent-warm);
|
||||
border-radius: 5px;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
.hint-card .hint-ico {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 4px;
|
||||
background: var(--color-accent-soft);
|
||||
color: var(--color-accent-deep);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.hint-card .hint-bar {
|
||||
margin-top: 8px;
|
||||
height: 4px;
|
||||
width: 100%;
|
||||
border-radius: 999px;
|
||||
background: var(--color-bg-rule-2);
|
||||
overflow: hidden;
|
||||
}
|
||||
.hint-card .hint-bar > span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
background: var(--color-accent-warm);
|
||||
}
|
||||
.hint-card .hint-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-top: 8px;
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
color: var(--color-accent-deep);
|
||||
text-decoration: none;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.hint-card .hint-action:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration-color: color-mix(
|
||||
in oklab,
|
||||
var(--color-accent-deep),
|
||||
transparent 60%
|
||||
);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Badges (Status-Pillen)
|
||||
* ============================================================ */
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 3px 9px;
|
||||
border-radius: 999px;
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.badge.warn {
|
||||
background: var(--color-warn-soft);
|
||||
color: var(--color-accent-deep);
|
||||
}
|
||||
.badge.ok {
|
||||
background: var(--color-ok-soft);
|
||||
color: var(--color-gain-deep);
|
||||
}
|
||||
.badge.hub {
|
||||
background: var(--color-hub-soft);
|
||||
color: var(--color-hub);
|
||||
}
|
||||
.badge.err {
|
||||
background: var(--color-err-soft);
|
||||
color: var(--color-loss);
|
||||
}
|
||||
.badge.dot::before {
|
||||
content: "";
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Brand-Bridge (presseecho + businessportal24 Indikatoren)
|
||||
* ============================================================ */
|
||||
|
||||
.bridge-row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10.5px;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-ink-3);
|
||||
}
|
||||
.dot-pe {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-bridge-presseecho);
|
||||
}
|
||||
.dot-bp {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-bridge-businessportal);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Section-Eyebrow VARIANTE (für panel-head)
|
||||
* ============================================================
|
||||
* Die Basis-`.section-eyebrow` liegt in theme-pressekonto.css.
|
||||
* Im Portal-Bundle haben wir die Klasse heute NOCH NICHT — hier
|
||||
* eine portable Definition, die im Web-Build vom Original-Layer
|
||||
* überschrieben wird (gleiche Werte → idempotent).
|
||||
*/
|
||||
.section-eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-hub);
|
||||
}
|
||||
.section-eyebrow::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 30px;
|
||||
height: 1px;
|
||||
background: var(--color-hub);
|
||||
opacity: 0.45;
|
||||
}
|
||||
.section-eyebrow.on-dark {
|
||||
color: var(--color-hub-line);
|
||||
}
|
||||
.section-eyebrow.on-dark::after {
|
||||
background: var(--color-hub-line);
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Eyebrow VARIANTE (für Portal)
|
||||
* ============================================================
|
||||
* Basis liegt in theme-pressekonto.css. Hier portabel für Portal-Build.
|
||||
*/
|
||||
.eyebrow {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-hub);
|
||||
}
|
||||
.eyebrow.muted {
|
||||
color: var(--color-ink-3);
|
||||
letter-spacing: 0.16em;
|
||||
font-weight: 600;
|
||||
font-size: 10.5px;
|
||||
}
|
||||
.eyebrow.accent {
|
||||
color: var(--color-accent-deep);
|
||||
}
|
||||
.eyebrow.on-dark {
|
||||
color: var(--color-hub-line);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,24 @@
|
|||
/**
|
||||
* Gemeinsame Styles für alle Frontend-Themes
|
||||
* Diese Datei wird von allen Web-Themes importiert
|
||||
*
|
||||
* Hub × FluxUI: Seit Phase 0 leben die Design-Tokens (Farben, Fonts,
|
||||
* Radii, Schatten) zentral in resources/css/shared/design-tokens.css.
|
||||
* Wir importieren sie hier — damit kommen sie automatisch in jedes
|
||||
* Web-Theme (theme-pressekonto.css, theme-presseecho.css,
|
||||
* theme-businessportal24.css).
|
||||
*/
|
||||
@import "tailwindcss";
|
||||
@import "../shared/design-tokens.css";
|
||||
@import "../shared/hub-components.css";
|
||||
|
||||
/* Definiere wo Tailwind nach Klassen suchen soll */
|
||||
@source "../../views/web";
|
||||
@source "../../views/layouts/web";
|
||||
@source "../../views/livewire/web";
|
||||
@source "../../views/components/web";
|
||||
@source "../../views/livewire/auth";
|
||||
@source "../../views/components/layouts/auth";
|
||||
|
||||
/* Tailwind Base Layer für gemeinsame Elemente */
|
||||
@layer base {
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@
|
|||
--color-bg-dark: #15201a;
|
||||
--color-bg-card-warm: #ecefe3;
|
||||
--color-bg-card-warm-border: #c7cfb6;
|
||||
--color-topbar: #1a3d2e; /* moderat heller als #1b2a1f, aber dunkler als #1f4d3a */
|
||||
--color-topbar2: #122d22; /* analog moderat heller als #25342a */
|
||||
--color-topbar: #112d20; /* Brand-Manual: satter Forest-Verlauf für die Topbar */
|
||||
--color-topbar2: #081b13; /* Brand-Manual: tieferer Forest-Endpunkt */
|
||||
|
||||
/* Ink */
|
||||
--color-ink: #1b2417;
|
||||
|
|
|
|||
484
resources/css/web/theme-pressekonto.css
Normal file
484
resources/css/web/theme-pressekonto.css
Normal file
|
|
@ -0,0 +1,484 @@
|
|||
/**
|
||||
* Theme für den Publisher-Hub pressekonto.de (pressekonto.test)
|
||||
*
|
||||
* Charakter:
|
||||
* - Surface: warmes Buchpapier (gleiche Familie wie BP24/Presseecho)
|
||||
* - Primary: Hub-Blau (#1A2540) – seriös, plattform-neutral
|
||||
* - Accent: gedecktes Bernstein (#B07A3A) – bewusst NICHT Orange (BP24) und NICHT Grün (Presseecho)
|
||||
* - Schrift: Inter Tight + JetBrains Mono
|
||||
*
|
||||
* Tokens (Farben, Fonts, Radii, Schatten) leben seit Phase 0 in
|
||||
* resources/css/shared/design-tokens.css als Single Source of Truth
|
||||
* — sie werden via shared-styles.css → design-tokens.css importiert.
|
||||
*
|
||||
* Hier nur noch:
|
||||
* - HSL-Variablen für Legacy-Komponenten (shared-styles)
|
||||
* - @layer components { … } mit Hub-spezifischen Klassen
|
||||
* (Eyebrows, Hero-Grid, Auth-Felder, FAQ-Accordion, …)
|
||||
*
|
||||
* Dokumentation: dev/frontend/hub-flux/01-PHASE-0-TOKENS.md
|
||||
*/
|
||||
@import "./shared-styles.css";
|
||||
|
||||
/* HSL-Variablen für Legacy-Komponenten (shared-styles) */
|
||||
:root {
|
||||
--font-primary: var(--font-sans);
|
||||
--font-secondary: var(--font-sans);
|
||||
|
||||
--background: 40 30% 95%; /* #f6f4ef */
|
||||
--foreground: 144 8% 11%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 144 8% 11%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 144 8% 11%;
|
||||
|
||||
/* Primary: #1A2540 -> hsl(222, 43%, 18%) */
|
||||
--primary: 222 43% 18%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--primary-50: 222 43% 96%;
|
||||
--primary-100: 222 43% 92%;
|
||||
--primary-200: 222 43% 85%;
|
||||
--primary-300: 222 43% 75%;
|
||||
--primary-400: 222 43% 60%;
|
||||
--primary-500: 222 43% 45%;
|
||||
--primary-600: 222 43% 36%;
|
||||
--primary-700: 222 43% 28%;
|
||||
--primary-800: 222 43% 22%;
|
||||
--primary-900: 222 43% 18%;
|
||||
--primary-950: 222 43% 12%;
|
||||
|
||||
--secondary: 224 30% 30%;
|
||||
--secondary-foreground: 0 0% 100%;
|
||||
|
||||
--muted: 40 18% 90%;
|
||||
--muted-foreground: 144 6% 38%;
|
||||
--accent: 31 51% 46%;
|
||||
--accent-foreground: 0 0% 100%;
|
||||
--destructive: 0 84% 60%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--border: 38 23% 85%;
|
||||
--input: 38 23% 85%;
|
||||
--ring: 222 43% 18%;
|
||||
--radius: 4px;
|
||||
|
||||
--shadow-card: 0 1px 2px 0 rgb(26 37 64 / 0.06);
|
||||
--shadow-card-hover: 0 4px 12px -2px rgb(26 37 64 / 0.12);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #e8e4da;
|
||||
color: var(--color-ink);
|
||||
font-family: var(--font-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-feature-settings: "ss01", "cv11";
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
font-family: var(--font-sans);
|
||||
color: inherit;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: var(--font-mono);
|
||||
font-feature-settings: "tnum";
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Hub-Eyebrow — sperrgesetzt, in Hub-Blau */
|
||||
.eyebrow {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-hub);
|
||||
}
|
||||
.eyebrow.muted {
|
||||
color: var(--color-ink-3);
|
||||
letter-spacing: 0.16em;
|
||||
font-weight: 600;
|
||||
font-size: 10.5px;
|
||||
}
|
||||
.eyebrow.accent {
|
||||
color: var(--color-accent-deep);
|
||||
}
|
||||
.eyebrow.on-dark {
|
||||
color: var(--color-hub-line);
|
||||
}
|
||||
|
||||
/* Section-Eyebrow mit kurzem Linien-Schwanz */
|
||||
.section-eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-hub);
|
||||
}
|
||||
.section-eyebrow::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 42px;
|
||||
height: 1px;
|
||||
background: var(--color-hub);
|
||||
opacity: 0.45;
|
||||
}
|
||||
.section-eyebrow.on-dark {
|
||||
color: var(--color-hub-line);
|
||||
}
|
||||
.section-eyebrow.on-dark::after {
|
||||
background: var(--color-hub-line);
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.rule {
|
||||
height: 1px;
|
||||
background: var(--color-bg-rule);
|
||||
border: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.rule-strong {
|
||||
height: 1px;
|
||||
background: var(--color-bg-rule-strong);
|
||||
border: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.rule-hub {
|
||||
height: 2px;
|
||||
background: var(--color-hub);
|
||||
border: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Subtile geometrische Hintergrundlinien fürs Hero */
|
||||
.hero-grid {
|
||||
background-image:
|
||||
linear-gradient(
|
||||
to right,
|
||||
rgba(26, 37, 64, 0.04) 1px,
|
||||
transparent 1px
|
||||
),
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
rgba(26, 37, 64, 0.04) 1px,
|
||||
transparent 1px
|
||||
);
|
||||
background-size: 48px 48px;
|
||||
background-position: -1px -1px;
|
||||
}
|
||||
|
||||
/* Hub-Gradient-Klassen */
|
||||
.bg-hub-grad {
|
||||
background-image: linear-gradient(
|
||||
135deg,
|
||||
var(--color-hub) 0%,
|
||||
var(--color-hub-2) 100%
|
||||
);
|
||||
}
|
||||
.bg-hub-grad-2 {
|
||||
background-image: linear-gradient(
|
||||
180deg,
|
||||
var(--color-hub) 0%,
|
||||
var(--color-topbar-deep) 100%
|
||||
);
|
||||
}
|
||||
.bg-accent-grad {
|
||||
background-image: linear-gradient(
|
||||
135deg,
|
||||
var(--color-accent) 0%,
|
||||
var(--color-accent-deep) 100%
|
||||
);
|
||||
}
|
||||
.bg-topbar-grad {
|
||||
background-image: linear-gradient(
|
||||
135deg,
|
||||
var(--color-topbar) 0%,
|
||||
var(--color-topbar2) 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* Empfohlen-Ribbon auf Tarif-Karten */
|
||||
.ribbon-recommend {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
right: -1px;
|
||||
background: var(--color-hub);
|
||||
color: #fff;
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* Accordion-Pfeil (FAQ) */
|
||||
details > summary {
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
details > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
details[open] .faq-chev {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.faq-chev {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
/*
|
||||
* Auth-Seiten (Anmelden, Registrieren, Passwort zurücksetzen, …)
|
||||
* — Konsistente Felder, Buttons und Atmosphäre für den Publisher-Hub
|
||||
*/
|
||||
|
||||
/* Hintergrund-Raster (Atmosphäre) */
|
||||
.auth-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(
|
||||
to right,
|
||||
rgba(26, 37, 64, 0.045) 1px,
|
||||
transparent 1px
|
||||
),
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
rgba(26, 37, 64, 0.045) 1px,
|
||||
transparent 1px
|
||||
);
|
||||
background-size: 56px 56px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* From=portal Banner über der Auth-Card */
|
||||
.from-banner {
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--color-bg-rule);
|
||||
border-left: 2px solid var(--color-hub);
|
||||
border-radius: 4px;
|
||||
padding: 8px 14px;
|
||||
font-size: 12px;
|
||||
color: var(--color-ink-2);
|
||||
}
|
||||
|
||||
/* Auth-Card mit weicher, fokussierender Schatten-Glocke */
|
||||
.auth-card {
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-bg-rule);
|
||||
border-radius: 8px;
|
||||
padding: 34px 36px 32px;
|
||||
box-shadow:
|
||||
0 1px 0 rgba(26, 31, 28, 0.03),
|
||||
0 20px 50px -32px rgba(26, 37, 64, 0.28);
|
||||
}
|
||||
|
||||
/* Eyebrow & Link in Hub-Blau für Auth-Kontext */
|
||||
.eyebrow-hub {
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-hub);
|
||||
}
|
||||
.link-hub {
|
||||
color: var(--color-hub);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.link-hub:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Input-Felder */
|
||||
.field-label {
|
||||
display: block;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.field-input {
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--color-bg-rule);
|
||||
border-radius: 6px;
|
||||
padding: 11px 13px;
|
||||
font-size: 14px;
|
||||
color: var(--color-ink);
|
||||
outline: none;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
box-shadow 0.15s,
|
||||
background 0.15s;
|
||||
font-family: inherit;
|
||||
}
|
||||
.field-input::placeholder {
|
||||
color: var(--color-ink-4);
|
||||
}
|
||||
.field-input:hover {
|
||||
border-color: #cfc8b5;
|
||||
}
|
||||
.field-input:focus {
|
||||
border-color: var(--color-hub);
|
||||
box-shadow: 0 0 0 3px rgba(26, 37, 64, 0.1);
|
||||
}
|
||||
.field-input[aria-invalid="true"],
|
||||
.field-input.is-invalid {
|
||||
border-color: var(--color-loss);
|
||||
}
|
||||
|
||||
/* Passwort-Feld mit „Anzeigen“-Button */
|
||||
.field-pw-wrap {
|
||||
position: relative;
|
||||
}
|
||||
.field-affix {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 10px;
|
||||
transform: translateY(-50%);
|
||||
color: var(--color-ink-3);
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
padding: 4px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: inherit;
|
||||
}
|
||||
.field-affix:hover {
|
||||
color: var(--color-hub);
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
/* Checkbox */
|
||||
.auth-check {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1.5px solid #cfc8b5;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
transition:
|
||||
border-color 0.12s,
|
||||
background 0.12s;
|
||||
margin: 0;
|
||||
}
|
||||
.auth-check:hover {
|
||||
border-color: var(--color-hub);
|
||||
}
|
||||
.auth-check:checked {
|
||||
background: var(--color-hub);
|
||||
border-color: var(--color-hub);
|
||||
}
|
||||
.auth-check:checked::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 1px;
|
||||
width: 5px;
|
||||
height: 9px;
|
||||
border: solid #fff;
|
||||
border-width: 0 1.8px 1.8px 0;
|
||||
transform: rotate(42deg);
|
||||
}
|
||||
|
||||
/* Buttons (auth-spezifisch, um den allgemeinen .btn-primary nicht zu überschreiben) */
|
||||
.auth-btn-primary {
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 11px 16px;
|
||||
background: var(--color-hub);
|
||||
color: #ffffff;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s;
|
||||
font-family: inherit;
|
||||
}
|
||||
.auth-btn-primary:hover {
|
||||
background: var(--color-hub-2);
|
||||
}
|
||||
.auth-btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.auth-btn-outline {
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
background: #ffffff;
|
||||
color: var(--color-ink);
|
||||
border: 1px solid var(--color-bg-rule);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.15s,
|
||||
border-color 0.15s;
|
||||
font-family: inherit;
|
||||
}
|
||||
.auth-btn-outline:hover {
|
||||
background: var(--color-bg-elev);
|
||||
border-color: #cfc8b5;
|
||||
}
|
||||
|
||||
/* Validierungs-Fehler-Text */
|
||||
.field-error {
|
||||
margin-top: 6px;
|
||||
color: var(--color-loss);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Erfolgs-/Status-Mitteilung */
|
||||
.field-status {
|
||||
background: #f1f6ef;
|
||||
border: 1px solid #c8dec0;
|
||||
color: #2e5b32;
|
||||
padding: 10px 14px;
|
||||
border-radius: 4px;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.tabular-nums {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
}
|
||||
|
||||
[x-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
|
|
@ -1,319 +0,0 @@
|
|||
/**
|
||||
* Theme für den Publisher-Hub presseportale.com (presseportale.test)
|
||||
*
|
||||
* Eigener Charakter zwischen den beiden Brand-Portalen:
|
||||
* - Surface: warmes Buchpapier (gleiche Familie wie BP24/Presseecho)
|
||||
* - Primary: Hub-Blau (#1A2540) – seriös, plattform-neutral
|
||||
* - Accent: gedecktes Bernstein (#B07A3A) – bewusst NICHT Orange (BP24) und NICHT Grün (Presseecho)
|
||||
* - Schrift: Inter Tight + JetBrains Mono, OHNE Source Serif (Hub-Bezug, weniger editorial-redaktionell als die Brand-Portale)
|
||||
*
|
||||
* Token-Namen folgen wo möglich der BP24/Presseecho-Konvention (bg, bg-elev, bg-rule, ink-*),
|
||||
* sodass viele Utility-Klassen identisch greifen. Hub-spezifische Tokens
|
||||
* (hub, hub-2, hub-soft, accent-soft, hub-line) sind zusätzlich definiert.
|
||||
*/
|
||||
@import "./shared-styles.css";
|
||||
|
||||
@theme {
|
||||
/* Surfaces — warmes Buchpapier, gleiche Familie wie die Brand-Portale */
|
||||
--color-bg: #f6f4ef;
|
||||
--color-bg-elev: #fbfaf6;
|
||||
--color-bg-rule: #e2ddd0;
|
||||
--color-bg-rule-strong: #1a1f1c;
|
||||
--color-bg-dark: #15201a;
|
||||
--color-bg-card: #ffffff;
|
||||
--color-bg-card-warm: #efeadc;
|
||||
--color-bg-card-warm-border: #d6cfbb;
|
||||
|
||||
/* Hub-Blau — Primary, plattform-neutral */
|
||||
--color-hub: #1a2540;
|
||||
--color-hub-2: #243152;
|
||||
--color-hub-3: #2e3d66;
|
||||
--color-hub-soft: #e5e9f1;
|
||||
--color-hub-soft-2: #cfd6e4;
|
||||
--color-hub-line: #7b8fbf;
|
||||
|
||||
/* Topbar (Alias für Hub-Gradient, damit shared base-Klassen passen) */
|
||||
--color-topbar: #1a2540;
|
||||
--color-topbar2: #243152;
|
||||
--color-topbar-deep: #0f1729;
|
||||
|
||||
/* Akzent — gedecktes Bernstein (zwischen Orange und Beige) */
|
||||
--color-accent: #b07a3a;
|
||||
--color-accent-deep: #8a5e27;
|
||||
--color-accent-soft: #f1e6d3;
|
||||
--color-accent-warm: #b07a3a;
|
||||
|
||||
/* Ink — Anthrazit-Reihe */
|
||||
--color-ink: #1a1f1c;
|
||||
--color-ink-2: #3a413d;
|
||||
--color-ink-3: #5a6360;
|
||||
--color-ink-4: #8a918d;
|
||||
--color-ink-on-dark: #f6f4ef;
|
||||
--color-ink-on-dark-2: #b2b9c7;
|
||||
--color-ink-on-dark-3: #7b8fbf;
|
||||
--color-ink-on-dark-muted: #7b8fbf;
|
||||
--color-ink-on-dark-rule: #2a3550;
|
||||
|
||||
/* Brand-Aliase, damit Komponenten, die brand-Tokens verwenden, funktionieren */
|
||||
--color-brand: #1a2540;
|
||||
--color-brand-deep: #0f1729;
|
||||
--color-brand-soft: #e5e9f1;
|
||||
--color-live: #c84a1e;
|
||||
--color-gain: #2e8540;
|
||||
--color-loss: #c8341e;
|
||||
--color-ok: #2e8540;
|
||||
|
||||
/* Editorial-Akzente (für card-warm-Sektionen) */
|
||||
--color-bg-card-warm-hover: #e6deca;
|
||||
--color-bg-card-warm-rule: #c8bda3;
|
||||
--color-card-warm-cat: #5a6360;
|
||||
--color-card-warm-title: #2a302d;
|
||||
--color-feature-line: #c0c8db;
|
||||
--color-feature-dot: #d8dde9;
|
||||
|
||||
/* Fonts — Hub: Inter Tight + JetBrains Mono.
|
||||
Source Serif 4 wird zusätzlich geladen, damit Markennennungen der
|
||||
Tochter-Portale (presseecho, businessportal24) typografisch konsistent
|
||||
erscheinen – im Hub-Standardtext kommt sie aber nicht zum Einsatz. */
|
||||
--font-sans: "Inter Tight", Inter, system-ui, -apple-system,
|
||||
BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
--font-serif: "Source Serif 4", "Source Serif Pro", Charter,
|
||||
"Iowan Old Style", Georgia, serif;
|
||||
--font-mono: "JetBrains Mono", "SF Mono", ui-monospace, SFMono-Regular,
|
||||
Menlo, Consolas, monospace;
|
||||
|
||||
/* Layout */
|
||||
--container-layout: 1280px;
|
||||
}
|
||||
|
||||
/* HSL-Variablen für Legacy-Komponenten (shared-styles) */
|
||||
:root {
|
||||
--font-primary: var(--font-sans);
|
||||
--font-secondary: var(--font-sans);
|
||||
|
||||
--background: 40 30% 95%; /* #f6f4ef */
|
||||
--foreground: 144 8% 11%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 144 8% 11%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 144 8% 11%;
|
||||
|
||||
/* Primary: #1A2540 -> hsl(222, 43%, 18%) */
|
||||
--primary: 222 43% 18%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--primary-50: 222 43% 96%;
|
||||
--primary-100: 222 43% 92%;
|
||||
--primary-200: 222 43% 85%;
|
||||
--primary-300: 222 43% 75%;
|
||||
--primary-400: 222 43% 60%;
|
||||
--primary-500: 222 43% 45%;
|
||||
--primary-600: 222 43% 36%;
|
||||
--primary-700: 222 43% 28%;
|
||||
--primary-800: 222 43% 22%;
|
||||
--primary-900: 222 43% 18%;
|
||||
--primary-950: 222 43% 12%;
|
||||
|
||||
--secondary: 224 30% 30%;
|
||||
--secondary-foreground: 0 0% 100%;
|
||||
|
||||
--muted: 40 18% 90%;
|
||||
--muted-foreground: 144 6% 38%;
|
||||
--accent: 31 51% 46%;
|
||||
--accent-foreground: 0 0% 100%;
|
||||
--destructive: 0 84% 60%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--border: 38 23% 85%;
|
||||
--input: 38 23% 85%;
|
||||
--ring: 222 43% 18%;
|
||||
--radius: 4px;
|
||||
|
||||
--shadow-card: 0 1px 2px 0 rgb(26 37 64 / 0.06);
|
||||
--shadow-card-hover: 0 4px 12px -2px rgb(26 37 64 / 0.12);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #e8e4da;
|
||||
color: var(--color-ink);
|
||||
font-family: var(--font-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-feature-settings: "ss01", "cv11";
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
font-family: var(--font-sans);
|
||||
color: inherit;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: var(--font-mono);
|
||||
font-feature-settings: "tnum";
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Hub-Eyebrow — sperrgesetzt, in Hub-Blau */
|
||||
.eyebrow {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-hub);
|
||||
}
|
||||
.eyebrow.muted {
|
||||
color: var(--color-ink-3);
|
||||
letter-spacing: 0.16em;
|
||||
font-weight: 600;
|
||||
font-size: 10.5px;
|
||||
}
|
||||
.eyebrow.accent {
|
||||
color: var(--color-accent-deep);
|
||||
}
|
||||
.eyebrow.on-dark {
|
||||
color: var(--color-hub-line);
|
||||
}
|
||||
|
||||
/* Section-Eyebrow mit kurzem Linien-Schwanz */
|
||||
.section-eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-hub);
|
||||
}
|
||||
.section-eyebrow::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 42px;
|
||||
height: 1px;
|
||||
background: var(--color-hub);
|
||||
opacity: 0.45;
|
||||
}
|
||||
.section-eyebrow.on-dark {
|
||||
color: var(--color-hub-line);
|
||||
}
|
||||
.section-eyebrow.on-dark::after {
|
||||
background: var(--color-hub-line);
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.rule {
|
||||
height: 1px;
|
||||
background: var(--color-bg-rule);
|
||||
border: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.rule-strong {
|
||||
height: 1px;
|
||||
background: var(--color-bg-rule-strong);
|
||||
border: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.rule-hub {
|
||||
height: 2px;
|
||||
background: var(--color-hub);
|
||||
border: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Subtile geometrische Hintergrundlinien fürs Hero */
|
||||
.hero-grid {
|
||||
background-image:
|
||||
linear-gradient(
|
||||
to right,
|
||||
rgba(26, 37, 64, 0.04) 1px,
|
||||
transparent 1px
|
||||
),
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
rgba(26, 37, 64, 0.04) 1px,
|
||||
transparent 1px
|
||||
);
|
||||
background-size: 48px 48px;
|
||||
background-position: -1px -1px;
|
||||
}
|
||||
|
||||
/* Hub-Gradient-Klassen */
|
||||
.bg-hub-grad {
|
||||
background-image: linear-gradient(
|
||||
135deg,
|
||||
var(--color-hub) 0%,
|
||||
var(--color-hub-2) 100%
|
||||
);
|
||||
}
|
||||
.bg-hub-grad-2 {
|
||||
background-image: linear-gradient(
|
||||
180deg,
|
||||
var(--color-hub) 0%,
|
||||
var(--color-topbar-deep) 100%
|
||||
);
|
||||
}
|
||||
.bg-accent-grad {
|
||||
background-image: linear-gradient(
|
||||
135deg,
|
||||
var(--color-accent) 0%,
|
||||
var(--color-accent-deep) 100%
|
||||
);
|
||||
}
|
||||
.bg-topbar-grad {
|
||||
background-image: linear-gradient(
|
||||
135deg,
|
||||
var(--color-topbar) 0%,
|
||||
var(--color-topbar2) 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* Empfohlen-Ribbon auf Tarif-Karten */
|
||||
.ribbon-recommend {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
right: -1px;
|
||||
background: var(--color-hub);
|
||||
color: #fff;
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* Accordion-Pfeil (FAQ) */
|
||||
details > summary {
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
details > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
details[open] .faq-chev {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.faq-chev {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.tabular-nums {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
}
|
||||
|
||||
[x-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
# Backend-Status: presseportale.test
|
||||
# Backend-Status: pressekonto.test
|
||||
|
||||
**Projekt:** BusinessPortal24 → Laravel 12 Migration
|
||||
**Domain:** presseportale.test
|
||||
**Stand:** 23. Januar 2026 nach Server-Neustart
|
||||
**Projekt:** BusinessPortal24 → Laravel 12 Migration
|
||||
**Domain:** pressekonto.test
|
||||
**Stand:** 23. Januar 2026 nach Server-Neustart
|
||||
**Status:** 🟡 Admin-UI-Gerüst vorhanden, Routing auf bestehende Volt-Komponenten konsolidiert
|
||||
|
||||
---
|
||||
|
|
@ -274,7 +274,7 @@ Beginne mit der eigentlichen Migration gemäß:
|
|||
|
||||
### Option 3: Backend testen 🟢
|
||||
- Dev-Server starten (`npm run dev`)
|
||||
- Backend öffnen (`http://presseportale.test/admin/press-releases`)
|
||||
- Backend öffnen (`http://pressekonto.test/admin/press-releases`)
|
||||
- UI und Navigation prüfen
|
||||
|
||||
### Option 4: Dummy-Daten verfeinern 🟢
|
||||
|
|
@ -294,14 +294,14 @@ npm run dev:portal
|
|||
php artisan serve
|
||||
|
||||
# Backend öffnen
|
||||
http://presseportale.test/admin/press-releases
|
||||
http://presseportale.test/admin/companies
|
||||
http://presseportale.test/admin/invoices
|
||||
http://presseportale.test/admin/contacts
|
||||
http://presseportale.test/admin/payments
|
||||
http://presseportale.test/admin/categories
|
||||
http://presseportale.test/admin/coupons
|
||||
http://presseportale.test/admin/roles
|
||||
http://pressekonto.test/admin/press-releases
|
||||
http://pressekonto.test/admin/companies
|
||||
http://pressekonto.test/admin/invoices
|
||||
http://pressekonto.test/admin/contacts
|
||||
http://pressekonto.test/admin/payments
|
||||
http://pressekonto.test/admin/categories
|
||||
http://pressekonto.test/admin/coupons
|
||||
http://pressekonto.test/admin/roles
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -327,7 +327,7 @@ http://presseportale.test/admin/roles
|
|||
|
||||
## 🎉 Meilenstein erreicht!
|
||||
|
||||
**Backend-Struktur für presseportale.test ist als Gerüst weitgehend vorbereitet.**
|
||||
**Backend-Struktur für pressekonto.test ist als Gerüst weitgehend vorbereitet.**
|
||||
|
||||
- ✅ 7 Hauptbereiche mit Navigation
|
||||
- ✅ 24 Routes definiert (konsistent gemappt)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Flux UI v2 - Komponenten-Referenz
|
||||
|
||||
**Projekt:** presseportale.test Backend
|
||||
**Flux Version:** 2.x
|
||||
**Projekt:** pressekonto.test Backend
|
||||
**Flux Version:** 2.x
|
||||
**Stand:** 23. Januar 2026
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ php artisan serve
|
|||
npm run dev
|
||||
|
||||
# Backend öffnen:
|
||||
http://presseportale.test/admin/press-releases
|
||||
http://pressekonto.test/admin/press-releases
|
||||
```
|
||||
|
||||
## 📚 Weitere Ressourcen
|
||||
|
|
|
|||
|
|
@ -1,110 +1,217 @@
|
|||
<x-layouts.app title="Dashboard">
|
||||
<div class="space-y-6">
|
||||
{{-- Statistik-Karten --}}
|
||||
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-5">
|
||||
<a href="{{ route('admin.press-releases.index') }}" wire:navigate class="block">
|
||||
<div class="rounded-xl border border-zinc-200 bg-white p-4 transition hover:border-zinc-300 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<p class="text-xs text-zinc-500">{{ __('PMs gesamt') }}</p>
|
||||
<p class="mt-1 text-2xl font-bold">{{ number_format($stats['press_releases']['total']) }}</p>
|
||||
<div class="mt-2 flex gap-2 text-xs text-zinc-400">
|
||||
<span class="text-green-600">{{ $stats['press_releases']['published'] }} pub</span>
|
||||
<span class="text-yellow-600">{{ $stats['press_releases']['review'] }} prüf</span>
|
||||
<span>{{ $stats['press_releases']['draft'] }} entwurf</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="space-y-8">
|
||||
|
||||
<a href="{{ route('admin.companies.index') }}" wire:navigate class="block">
|
||||
<div class="rounded-xl border border-zinc-200 bg-white p-4 transition hover:border-zinc-300 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<p class="text-xs text-zinc-500">{{ __('Firmen') }}</p>
|
||||
<p class="mt-1 text-2xl font-bold">{{ number_format($stats['companies']) }}</p>
|
||||
{{-- ============== PAGE HEADER ============== --}}
|
||||
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
|
||||
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
|
||||
<span class="eyebrow muted">{{ __('Operations · A · 01') }}</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('admin.contacts.index') }}" wire:navigate class="block">
|
||||
<div class="rounded-xl border border-zinc-200 bg-white p-4 transition hover:border-zinc-300 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<p class="text-xs text-zinc-500">{{ __('Kontakte') }}</p>
|
||||
<p class="mt-1 text-2xl font-bold">{{ number_format($stats['contacts']) }}</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('admin.users.index') }}" wire:navigate class="block">
|
||||
<div class="rounded-xl border border-zinc-200 bg-white p-4 transition hover:border-zinc-300 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<p class="text-xs text-zinc-500">{{ __('Benutzer') }}</p>
|
||||
<p class="mt-1 text-2xl font-bold">{{ number_format($stats['users']) }}</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="rounded-xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<p class="text-xs text-zinc-500">{{ __('Newsletter') }}</p>
|
||||
<p class="mt-1 text-2xl font-bold">{{ number_format($stats['newsletter']) }}</p>
|
||||
<p class="mt-2 text-xs text-zinc-400">{{ __('bestätigt') }}</p>
|
||||
<h1 class="text-[34px] font-bold tracking-[-0.7px] leading-[1.1] m-0 text-[color:var(--color-ink)]">
|
||||
{{ __('Admin Dashboard') }}
|
||||
</h1>
|
||||
<p class="text-[13.5px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
|
||||
{{ __('Willkommen zurück, ') }}<strong class="font-semibold text-[color:var(--color-ink)]">{{ auth()->user()->name }}</strong>.
|
||||
{{ __('Operations-Übersicht über Pressemitteilungen, Firmen und Konten beider Portale.') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-[1fr,360px]">
|
||||
<div class="flex items-center gap-3 flex-shrink-0">
|
||||
<span class="badge ok dot">{{ __('Alle Systeme operational') }}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{{-- ============== KPI-Reihe (5 Stat-Cards) ============== --}}
|
||||
<section class="grid gap-4 grid-cols-2 sm:grid-cols-3 lg:grid-cols-5">
|
||||
<a href="{{ route('admin.press-releases.index') }}" wire:navigate class="block focus:outline-none">
|
||||
<x-portal.stat-card variant="primary" :label="__('Pressemitteilungen')" :value="number_format($stats['press_releases']['total'])">
|
||||
<x-slot:meta>{{ now()->format('Y') }}</x-slot:meta>
|
||||
{{-- WICHTIG: Wortlaut „X pub · Y prüf · Z entwurf" exakt beibehalten,
|
||||
weil DashboardTest darauf assertet (1 pub, 1 prüf, 1 entwurf). --}}
|
||||
<x-slot:trend>
|
||||
<span class="flex items-center gap-3">
|
||||
<span class="text-[color:var(--color-ok)]">{{ $stats['press_releases']['published'] }} pub</span>
|
||||
<span class="text-[color:var(--color-warn)]">{{ $stats['press_releases']['review'] }} prüf</span>
|
||||
<span>{{ $stats['press_releases']['draft'] }} entwurf</span>
|
||||
</span>
|
||||
</x-slot:trend>
|
||||
</x-portal.stat-card>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('admin.press-releases.index', ['status' => 'review']) }}" wire:navigate class="block focus:outline-none">
|
||||
<x-portal.stat-card variant="warn" :label="__('In Prüfung')" :value="$stats['press_releases']['review']">
|
||||
<x-slot:meta>{{ __('queue') }}</x-slot:meta>
|
||||
<x-slot:trend>{{ __('warten auf Review') }}</x-slot:trend>
|
||||
</x-portal.stat-card>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('admin.companies.index') }}" wire:navigate class="block focus:outline-none">
|
||||
<x-portal.stat-card variant="muted" :label="__('Firmen')" :value="number_format($stats['companies'])">
|
||||
<x-slot:trend>{{ __('aktiv im CRM') }}</x-slot:trend>
|
||||
</x-portal.stat-card>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('admin.contacts.index') }}" wire:navigate class="block focus:outline-none">
|
||||
<x-portal.stat-card variant="muted" :label="__('Kontakte')" :value="number_format($stats['contacts'])">
|
||||
<x-slot:trend>{{ __('Pressekontakte') }}</x-slot:trend>
|
||||
</x-portal.stat-card>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('admin.users.index') }}" wire:navigate class="block focus:outline-none">
|
||||
<x-portal.stat-card variant="muted" :label="__('Benutzer')" :value="number_format($stats['users'])">
|
||||
<x-slot:trend>{{ __('Portal-Konten') }}</x-slot:trend>
|
||||
</x-portal.stat-card>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
{{-- ============== ZWEISPALTEN-GRID ============== --}}
|
||||
<section class="grid gap-6 lg:grid-cols-[2fr_1fr]">
|
||||
|
||||
{{-- Letzte Pressemitteilungen --}}
|
||||
<div class="overflow-hidden rounded-xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="flex items-center justify-between border-b border-zinc-200 px-4 py-3 dark:border-zinc-700">
|
||||
<h2 class="font-semibold">{{ __('Letzte Pressemitteilungen') }}</h2>
|
||||
<a href="{{ route('admin.press-releases.index') }}" wire:navigate class="text-sm text-blue-600 hover:underline dark:text-blue-400">{{ __('Alle anzeigen') }}</a>
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Letzte Pressemitteilungen') }}</span>
|
||||
<a href="{{ route('admin.press-releases.index') }}" wire:navigate
|
||||
class="text-[12px] font-semibold text-[color:var(--color-hub)] hover:underline underline-offset-[3px] decoration-[color:var(--color-hub)]/30">
|
||||
{{ __('Alle anzeigen') }} →
|
||||
</a>
|
||||
</div>
|
||||
<div class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
@forelse($recentPRs as $pr)
|
||||
<a href="{{ route('admin.press-releases.show', $pr->id) }}" wire:navigate
|
||||
class="flex items-center justify-between gap-3 px-4 py-3 transition hover:bg-zinc-50 dark:hover:bg-zinc-800">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-medium">{{ $pr->title }}</p>
|
||||
<p class="text-xs text-zinc-500">{{ $pr->company?->name ?? '–' }} · {{ $pr->created_at->format('d.m.Y') }}</p>
|
||||
</div>
|
||||
<span class="shrink-0 rounded-full px-2 py-0.5 text-xs font-medium {{ match($pr->status->value) {
|
||||
'published' => 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
|
||||
'review' => 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400',
|
||||
'rejected' => 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400',
|
||||
'archived' => 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
|
||||
default => 'bg-zinc-100 text-zinc-600 dark:bg-zinc-700 dark:text-zinc-300',
|
||||
} }}">
|
||||
{{ $pr->status->label() }}
|
||||
</span>
|
||||
</a>
|
||||
@empty
|
||||
<p class="px-4 py-6 text-center text-sm text-zinc-500">{{ __('Noch keine Pressemitteilungen.') }}</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@forelse ($recentPRs as $pr)
|
||||
<a href="{{ route('admin.press-releases.show', $pr->id) }}" wire:navigate
|
||||
class="flex items-center justify-between gap-3 px-5 py-3 border-b border-[color:var(--color-bg-rule)] last:border-b-0 hover:bg-[color:var(--color-bg)] transition-colors">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-[13px] font-medium text-[color:var(--color-ink)] m-0">{{ $pr->title }}</p>
|
||||
<p class="text-[11px] text-[color:var(--color-ink-3)] mt-0.5 m-0 truncate">
|
||||
{{ $pr->company?->name ?? '–' }}
|
||||
<span class="text-[color:var(--color-ink-4)] mx-1">·</span>
|
||||
{{ $pr->user?->name ?? '–' }}
|
||||
<span class="text-[color:var(--color-ink-4)] mx-1">·</span>
|
||||
{{ $pr->created_at->format('d.m.Y') }}
|
||||
</p>
|
||||
</div>
|
||||
<span @class([
|
||||
'badge shrink-0',
|
||||
'ok' => $pr->status->value === 'published',
|
||||
'warn' => $pr->status->value === 'review',
|
||||
'err' => $pr->status->value === 'rejected',
|
||||
'hub' => in_array($pr->status->value, ['archived', 'draft'], true),
|
||||
])>
|
||||
{{ $pr->status->label() }}
|
||||
</span>
|
||||
</a>
|
||||
@empty
|
||||
<p class="px-5 py-8 text-center text-[12.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ __('Noch keine Pressemitteilungen.') }}
|
||||
</p>
|
||||
@endforelse
|
||||
</article>
|
||||
|
||||
{{-- Warteschlange Prüfung --}}
|
||||
<div class="overflow-hidden rounded-xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="flex items-center justify-between border-b border-zinc-200 px-4 py-3 dark:border-zinc-700">
|
||||
<h2 class="font-semibold">{{ __('Zur Prüfung') }}</h2>
|
||||
@if($stats['press_releases']['review'] > 0)
|
||||
<span class="rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400">
|
||||
{{ $stats['press_releases']['review'] }}
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Zur Prüfung') }}</span>
|
||||
@if ($stats['press_releases']['review'] > 0)
|
||||
<span class="badge warn dot">
|
||||
{{ $stats['press_releases']['review'] }} {{ __('offen') }}
|
||||
</span>
|
||||
@else
|
||||
<span class="badge ok dot">{{ __('leer') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
@forelse($pendingReviews as $pr)
|
||||
<a href="{{ route('admin.press-releases.show', $pr->id) }}" wire:navigate
|
||||
class="block px-4 py-3 transition hover:bg-zinc-50 dark:hover:bg-zinc-800">
|
||||
<p class="truncate text-sm font-medium">{{ $pr->title }}</p>
|
||||
<p class="text-xs text-zinc-500">
|
||||
{{ $pr->company?->name ?? '–' }} · {{ $pr->portal->label() }} · {{ $pr->created_at->format('d.m.Y') }}
|
||||
</p>
|
||||
</a>
|
||||
@empty
|
||||
<p class="px-4 py-6 text-center text-sm text-zinc-500">{{ __('Keine PMs in der Prüfwarteschlange.') }}</p>
|
||||
@endforelse
|
||||
</div>
|
||||
@if($stats['press_releases']['review'] > count($pendingReviews))
|
||||
<div class="border-t border-zinc-100 px-4 py-2 dark:border-zinc-800">
|
||||
<a href="{{ route('admin.press-releases.index', ['statusFilter' => 'review']) }}" wire:navigate
|
||||
class="text-xs text-blue-600 hover:underline dark:text-blue-400">
|
||||
+ {{ $stats['press_releases']['review'] - count($pendingReviews) }} {{ __('weitere') }}
|
||||
|
||||
@forelse ($pendingReviews as $pr)
|
||||
<a href="{{ route('admin.press-releases.show', $pr->id) }}" wire:navigate
|
||||
class="block px-5 py-3 border-b border-[color:var(--color-bg-rule)] last:border-b-0 hover:bg-[color:var(--color-bg)] transition-colors">
|
||||
<p class="truncate text-[13px] font-medium text-[color:var(--color-ink)] m-0">{{ $pr->title }}</p>
|
||||
<p class="text-[11px] text-[color:var(--color-ink-3)] mt-0.5 m-0 truncate">
|
||||
{{ $pr->company?->name ?? '–' }}
|
||||
<span class="text-[color:var(--color-ink-4)] mx-1">·</span>
|
||||
{{ $pr->portal->label() }}
|
||||
<span class="text-[color:var(--color-ink-4)] mx-1">·</span>
|
||||
{{ $pr->created_at->format('d.m.Y') }}
|
||||
</p>
|
||||
</a>
|
||||
@empty
|
||||
<p class="px-5 py-8 text-center text-[12.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ __('Keine PMs in der Prüfwarteschlange.') }}
|
||||
</p>
|
||||
@endforelse
|
||||
|
||||
@if ($stats['press_releases']['review'] > count($pendingReviews))
|
||||
<div class="px-5 py-3 border-t border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)]">
|
||||
<a href="{{ route('admin.press-releases.index', ['status' => 'review']) }}" wire:navigate
|
||||
class="text-[12px] font-semibold text-[color:var(--color-hub)] hover:underline underline-offset-[3px] decoration-[color:var(--color-hub)]/30">
|
||||
+ {{ $stats['press_releases']['review'] - count($pendingReviews) }} {{ __('weitere') }} →
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
{{-- ============== NEWSLETTER + QUICK ACTIONS ============== --}}
|
||||
<section class="grid gap-6 lg:grid-cols-[1fr_2fr]">
|
||||
|
||||
{{-- Newsletter-Stat als panel-warm Block --}}
|
||||
<article class="panel-warm relative overflow-hidden">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Newsletter') }}</span>
|
||||
<span class="badge hub">{{ __('bestätigt') }}</span>
|
||||
</div>
|
||||
<div class="px-5 py-5">
|
||||
<div class="stat-num text-[color:var(--color-ink)]">
|
||||
{{ number_format($stats['newsletter']) }}
|
||||
</div>
|
||||
<p class="text-[12px] text-[color:var(--color-ink-3)] mt-2 m-0">
|
||||
{{ __('Aktive Newsletter-Abonnenten über beide Portale.') }}
|
||||
</p>
|
||||
<a href="{{ route('admin.newsletter.sync') }}" wire:navigate
|
||||
class="inline-flex items-center gap-1 mt-3 text-[12px] font-semibold text-[color:var(--color-hub)] hover:underline underline-offset-[3px] decoration-[color:var(--color-hub)]/30">
|
||||
{{ __('Sync verwalten') }} →
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{{-- Quick-Actions Panel --}}
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Quick Actions') }}</span>
|
||||
</div>
|
||||
<div class="grid gap-3 p-5 grid-cols-2 md:grid-cols-4">
|
||||
@foreach ([
|
||||
['icon' => 'newspaper', 'label' => __('Pressemitteilungen'), 'route' => 'admin.press-releases.index'],
|
||||
['icon' => 'building-office', 'label' => __('Firmen'), 'route' => 'admin.companies.index'],
|
||||
['icon' => 'document-text', 'label' => __('Rechnungen'), 'route' => 'admin.invoices.index'],
|
||||
['icon' => 'cog', 'label' => __('Voreinstellungen'), 'route' => 'admin.presets.index'],
|
||||
] as $action)
|
||||
<a href="{{ route($action['route']) }}" wire:navigate
|
||||
class="group flex flex-col items-start gap-2 p-4 rounded-[5px] transition-colors
|
||||
bg-[color:var(--color-bg-elev)] border border-[color:var(--color-bg-rule)]
|
||||
hover:border-[color:var(--color-hub)]/40 hover:bg-[color:var(--color-bg)]">
|
||||
<span class="w-9 h-9 rounded-[4px] flex items-center justify-center
|
||||
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)]
|
||||
text-[color:var(--color-hub)] group-hover:bg-[color:var(--color-hub)] group-hover:text-white transition-colors">
|
||||
<flux:icon :name="$action['icon']" class="size-[18px]" />
|
||||
</span>
|
||||
<span class="text-[12px] font-semibold text-[color:var(--color-ink-2)] leading-tight">
|
||||
{{ $action['label'] }}
|
||||
</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
{{-- ============== FOOTER ============== --}}
|
||||
<footer class="flex items-center justify-between pt-4 pb-2 text-[11px]
|
||||
border-t border-[color:var(--color-bg-rule)] text-[color:var(--color-ink-3)]">
|
||||
<span>© {{ now()->format('Y') }} pressekonto.de · Admin Backend</span>
|
||||
<span class="flex items-center gap-5">
|
||||
<a href="{{ route('admin.users.index') }}" wire:navigate class="hover:text-[color:var(--color-hub)]">{{ __('Benutzer') }}</a>
|
||||
<a href="{{ route('admin.roles.index') }}" wire:navigate class="hover:text-[color:var(--color-hub)]">{{ __('Rollen & Rechte') }}</a>
|
||||
<a href="{{ route('admin.reports.slow-requests') }}" wire:navigate class="hover:text-[color:var(--color-hub)]">{{ __('Performance') }}</a>
|
||||
</span>
|
||||
</footer>
|
||||
</div>
|
||||
</x-layouts.app>
|
||||
|
|
|
|||
|
|
@ -6,18 +6,23 @@
|
|||
@endphp
|
||||
|
||||
@if($canCustomer)
|
||||
<div class="mb-6 rounded-xl border border-zinc-200 bg-zinc-50/80 px-4 py-3 shadow-sm ring-1 ring-zinc-950/5 dark:border-zinc-700 dark:bg-zinc-900/60 dark:ring-white/10">
|
||||
{{-- Hub-Stil-Banner: Hub-Soft-Hintergrund, Hub-Blau-Badge,
|
||||
dezente Buchpapier-Rule. Ersetzt das Zinc-Starter-Kit-Card. --}}
|
||||
<div class="mb-6 rounded-md border border-hub-soft-2 bg-hub-soft/50 px-4 py-3">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:badge color="zinc" size="sm">{{ __('User Backend') }}</flux:badge>
|
||||
<flux:text class="hidden text-xs text-zinc-500 dark:text-zinc-400 sm:block">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-hub-soft text-hub text-[10.5px] font-bold tracking-[0.10em] uppercase">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-hub"></span>
|
||||
{{ __('User Backend') }}
|
||||
</span>
|
||||
<span class="hidden sm:inline-block text-[10.5px] font-semibold tracking-[0.16em] uppercase text-ink-3">
|
||||
{{ __('Firmenkontext') }}
|
||||
</flux:text>
|
||||
</span>
|
||||
</div>
|
||||
<flux:heading size="sm" class="mt-1 truncate">
|
||||
<h2 class="mt-1 text-[15px] font-semibold tracking-[-0.2px] truncate text-ink m-0">
|
||||
{{ $title ?? __('Mein Bereich') }}
|
||||
</flux:heading>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="w-full sm:w-auto">
|
||||
|
|
|
|||
|
|
@ -1,13 +1,25 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
{{--
|
||||
Hub × FluxUI Phase 1 — Portal-Shell im Hub-Design.
|
||||
class="dark" wurde entfernt; Light Mode ist Default, Dark kommt mit
|
||||
FluxUI Appearance-Switcher in Phase 5.
|
||||
--}}
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
@include('partials.head')
|
||||
</head>
|
||||
<body class="min-h-screen bg-white dark:bg-zinc-800">
|
||||
<flux:sidebar sticky stashable class="border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<body class="min-h-screen bg-bg text-ink antialiased">
|
||||
<flux:sidebar sticky stashable class="border-e border-bg-rule">
|
||||
<flux:sidebar.toggle class="lg:hidden" icon="x-mark" />
|
||||
<a href="{{ config('domains.domain_main_url') }}" class="me-5 flex items-center space-x-2 rtl:space-x-reverse">
|
||||
<x-app-logo />
|
||||
|
||||
{{-- Brand-Block: Wortmarke + Hub-Eyebrow --}}
|
||||
<a href="{{ config('domains.domain_main_url') }}" class="block px-2 pt-1 pb-3 no-underline">
|
||||
<span class="text-[19px] font-bold tracking-[-0.4px] leading-none">
|
||||
<x-web.brand-mark brand="pressekonto" :serif="false" />
|
||||
</span>
|
||||
<div class="mt-1.5 text-[10px] font-semibold tracking-[0.18em] uppercase text-ink-3">
|
||||
Publisher · Hub
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@php
|
||||
|
|
@ -158,43 +170,42 @@
|
|||
@endauth
|
||||
|
||||
@if($impersonator)
|
||||
<div class="mt-3 rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-950 dark:border-amber-700/60 dark:bg-amber-950/40 dark:text-amber-100">
|
||||
<flux:text weight="semibold">{{ __('Testmodus aktiv') }}</flux:text>
|
||||
<flux:text class="mt-1 text-xs">
|
||||
{{ __('Angemeldet als :user. Admin: :admin.', ['user' => $user?->name, 'admin' => $impersonator->name]) }}
|
||||
</flux:text>
|
||||
|
||||
<form method="POST" action="{{ route('admin.impersonate.leave') }}" class="mt-3">
|
||||
@csrf
|
||||
<flux:button type="submit" size="sm" variant="primary" class="w-full">
|
||||
{{ __('Zurück zum Admin') }}
|
||||
</flux:button>
|
||||
</form>
|
||||
{{-- Testmodus-Block im Hub-Stil (statt Amber-Warnfarbe).
|
||||
Dunkles Hub-Blau-Panel mit Bernstein-Eyebrow, klare
|
||||
CTA „Zurück zum Admin" als helle Pille. --}}
|
||||
<div class="mt-3 relative overflow-hidden rounded-[5px] bg-hub p-4 text-ink-on-dark">
|
||||
<div class="absolute -top-6 -right-6 w-16 h-16 rounded-full bg-hub-3 opacity-50"></div>
|
||||
<div class="absolute -bottom-8 -left-8 w-20 h-20 rounded-full bg-hub-3 opacity-30"></div>
|
||||
<div class="relative">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
|
||||
<span class="text-[10.5px] font-bold tracking-[0.20em] uppercase text-accent-soft">
|
||||
{{ __('Testmodus aktiv') }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-[12px] leading-[1.5] text-ink-on-dark-2 m-0">
|
||||
{{ __('Angemeldet als') }}
|
||||
<strong class="text-white font-semibold">{{ $user?->name }}</strong>.<br/>
|
||||
{{ __('Admin:') }}
|
||||
<strong class="text-white font-semibold">{{ $impersonator->name }}</strong>
|
||||
</p>
|
||||
<form method="POST" action="{{ route('admin.impersonate.leave') }}" class="mt-3">
|
||||
@csrf
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full px-3 py-2 bg-white text-hub text-[12px] font-semibold rounded-[3px] hover:bg-bg transition-colors flex items-center justify-center gap-1.5"
|
||||
>
|
||||
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<path d="M9 3L3 9M3 9H8M3 9V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
{{ __('Zurück zum Admin') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<flux:spacer />
|
||||
|
||||
<flux:navlist variant="outline">
|
||||
<flux:navlist.group :heading="__('Resources')">
|
||||
<flux:navlist.item icon="pencil" href="https://tailwindcss.com/docs/installation/using-vite" target="_blank">
|
||||
{{ __('Tailwind CSS') }}
|
||||
</flux:navlist.item>
|
||||
<flux:navlist.item icon="shield-check" href="https://heroicons.com" target="_blank">
|
||||
{{ __('Hero Icons') }}
|
||||
</flux:navlist.item>
|
||||
<flux:navlist.item icon="bolt" href="https://fluxui.dev/docs/installation" target="_blank">
|
||||
{{ __('Flux UI') }}
|
||||
</flux:navlist.item>
|
||||
<flux:navlist.item icon="folder-git-2" href="https://github.com/laravel/livewire-starter-kit" target="_blank">
|
||||
{{ __('Repository') }}
|
||||
</flux:navlist.item>
|
||||
|
||||
<flux:navlist.item icon="book-open-text" href="https://laravel.com/docs/starter-kits" target="_blank">
|
||||
{{ __('Documentation') }}
|
||||
</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
</flux:navlist>
|
||||
<!-- Desktop User Menu -->
|
||||
<flux:dropdown position="bottom" align="start">
|
||||
<flux:profile
|
||||
|
|
@ -231,6 +242,22 @@
|
|||
|
||||
<flux:menu.separator />
|
||||
|
||||
{{-- Phase 5: Appearance-Switcher direkt im User-Menü.
|
||||
`$flux.appearance` ist FluxUIs Magic-Object, persistent
|
||||
über LocalStorage. Werte: 'light' | 'dark' | 'system'. --}}
|
||||
<div class="px-3 py-2">
|
||||
<div class="mb-1.5 text-[10px] font-semibold tracking-[0.16em] uppercase text-[color:var(--color-ink-3)]">
|
||||
{{ __('Erscheinung') }}
|
||||
</div>
|
||||
<flux:radio.group x-data variant="segmented" size="sm" x-model="$flux.appearance" class="w-full">
|
||||
<flux:radio value="light" icon="sun" :title="__('Hell')" />
|
||||
<flux:radio value="dark" icon="moon" :title="__('Dunkel')" />
|
||||
<flux:radio value="system" icon="computer-desktop" :title="__('System')" />
|
||||
</flux:radio.group>
|
||||
</div>
|
||||
|
||||
<flux:menu.separator />
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}" class="w-full">
|
||||
@csrf
|
||||
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle" class="w-full">
|
||||
|
|
@ -245,8 +272,10 @@
|
|||
<flux:header class="lg:hidden">
|
||||
<flux:sidebar.toggle class="lg:hidden" icon="bars-2" inset="left" />
|
||||
|
||||
<a href="{{ config('domains.domain_main_url') }}" class="me-5 ml-2 flex items-center space-x-2 rtl:space-x-reverse">
|
||||
<x-app-logo />
|
||||
<a href="{{ config('domains.domain_main_url') }}" class="me-5 ml-2 flex items-baseline no-underline">
|
||||
<span class="text-[16px] font-bold tracking-[-0.3px] leading-none">
|
||||
<x-web.brand-mark brand="pressekonto" :serif="false" />
|
||||
</span>
|
||||
</a>
|
||||
<flux:spacer />
|
||||
|
||||
|
|
@ -284,6 +313,20 @@
|
|||
|
||||
<flux:menu.separator />
|
||||
|
||||
{{-- Phase 5: Appearance-Switcher (Mobile-Dropdown). --}}
|
||||
<div class="px-3 py-2">
|
||||
<div class="mb-1.5 text-[10px] font-semibold tracking-[0.16em] uppercase text-[color:var(--color-ink-3)]">
|
||||
{{ __('Erscheinung') }}
|
||||
</div>
|
||||
<flux:radio.group x-data variant="segmented" size="sm" x-model="$flux.appearance" class="w-full">
|
||||
<flux:radio value="light" icon="sun" :title="__('Hell')" />
|
||||
<flux:radio value="dark" icon="moon" :title="__('Dunkel')" />
|
||||
<flux:radio value="system" icon="computer-desktop" :title="__('System')" />
|
||||
</flux:radio.group>
|
||||
</div>
|
||||
|
||||
<flux:menu.separator />
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}" class="w-full">
|
||||
@csrf
|
||||
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle" class="w-full">
|
||||
|
|
|
|||
154
resources/views/components/layouts/auth/pressekonto.blade.php
Normal file
154
resources/views/components/layouts/auth/pressekonto.blade.php
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
@props([
|
||||
'title' => null,
|
||||
'eyebrow' => 'Publisher-Hub',
|
||||
'heading' => null,
|
||||
'topRightLabel' => null,
|
||||
'topRightLinkText' => null,
|
||||
'topRightLinkHref' => null,
|
||||
'showFromBanner' => true,
|
||||
])
|
||||
|
||||
@php
|
||||
$brand = config('domains.domains.pressekonto.brand', []);
|
||||
$from = request()->query('from');
|
||||
|
||||
$brandLabelMap = [
|
||||
'presseecho' => 'presseecho.de',
|
||||
'businessportal24' => 'businessportal24.com',
|
||||
];
|
||||
$fromBrandLabel = $brandLabelMap[$from] ?? null;
|
||||
|
||||
$pageTitle = $title ?? ($brand['meta_title'] ?? 'pressekonto – Publisher-Hub');
|
||||
|
||||
config([
|
||||
'app.theme' => 'pressekonto',
|
||||
'app.view_prefix' => 'web',
|
||||
]);
|
||||
$themeCssPath = \App\Helpers\ThemeHelper::getThemeCssPath();
|
||||
$assetsDir = config('domains.domains.pressekonto.assets_dir', 'build/web');
|
||||
\Illuminate\Support\Facades\Vite::useBuildDirectory($assetsDir);
|
||||
@endphp
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
||||
|
||||
<title>{{ $pageTitle }}</title>
|
||||
|
||||
<link rel="icon" href="{{ asset(\App\Helpers\ThemeHelper::getFaviconPath()) }}" />
|
||||
|
||||
<link rel="preconnect" href="https://fonts.bunny.net" />
|
||||
<link rel="preconnect" href="https://fonts.bunny.net" crossorigin />
|
||||
<link href="https://fonts.bunny.net/css?family=inter-tight:400,500,600,700|source-serif-4:400,500,600,700|jetbrains-mono:400,500,600" rel="stylesheet" />
|
||||
|
||||
{{-- Nur CSS aus dem Web-Build laden. Alpine bringt @livewireScripts mit;
|
||||
würden wir hier zusätzlich resources/js/app.js mit Alpine.start()
|
||||
laden, gäbe es zwei Alpine-Instanzen und wire:submit, x-data,
|
||||
wire:model würden brechen (siehe Browser-Logs vor diesem Fix). --}}
|
||||
@vite([$themeCssPath], $assetsDir)
|
||||
@livewireStyles
|
||||
</head>
|
||||
|
||||
<body class="font-sans text-ink antialiased" style="background-color: #f6f4ef;">
|
||||
|
||||
<div class="relative min-h-screen flex flex-col overflow-hidden bg-bg">
|
||||
|
||||
{{-- Atmosphäre: subtiles Raster --}}
|
||||
<div class="auth-grid" aria-hidden="true"></div>
|
||||
|
||||
{{-- Atmosphäre: konzentrische Kreise um die Bildmitte --}}
|
||||
<svg class="absolute inset-0 w-full h-full pointer-events-none" preserveAspectRatio="xMidYMid slice" viewBox="0 0 1280 880" aria-hidden="true">
|
||||
<g opacity="0.09" stroke="#1A2540" fill="none" stroke-width="1">
|
||||
<circle cx="640" cy="470" r="160" />
|
||||
<circle cx="640" cy="470" r="260" />
|
||||
<circle cx="640" cy="470" r="380" />
|
||||
<circle cx="640" cy="470" r="510" />
|
||||
<circle cx="640" cy="470" r="660" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
{{-- 3px Hub-Blau-Streifen --}}
|
||||
<div class="relative h-[3px] bg-hub z-10"></div>
|
||||
|
||||
{{-- Header --}}
|
||||
<header class="relative z-10 px-6 sm:px-10 py-[22px] flex items-center justify-between gap-4">
|
||||
<a href="{{ route('home') }}" class="flex items-baseline gap-2.5 no-underline" wire:navigate>
|
||||
<span class="text-[19px] font-bold tracking-[-0.4px] leading-none">
|
||||
<x-web.brand-mark brand="pressekonto" :serif="false" />
|
||||
</span>
|
||||
<span class="hidden sm:inline-block w-px h-[14px] bg-bg-rule"></span>
|
||||
<span class="hidden sm:inline-block text-[9.5px] font-bold tracking-[0.22em] uppercase text-ink-3">
|
||||
{{ $brand['tagline_short'] ?? 'Publisher · Hub' }}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@if ($topRightLinkText && $topRightLinkHref)
|
||||
<span class="text-[13px] text-ink-3">
|
||||
@if ($topRightLabel)
|
||||
{{ $topRightLabel }}
|
||||
@endif
|
||||
<a href="{{ $topRightLinkHref }}" class="link-hub" wire:navigate>{{ $topRightLinkText }}</a>
|
||||
</span>
|
||||
@endif
|
||||
</header>
|
||||
|
||||
{{-- Auth-Card --}}
|
||||
<main class="relative z-10 flex-1 flex items-center justify-center px-6 sm:px-10 py-5">
|
||||
<div class="w-full max-w-[440px]">
|
||||
|
||||
@if ($showFromBanner && $fromBrandLabel)
|
||||
<div class="from-banner mb-3.5">
|
||||
Sie kommen von <strong class="font-semibold text-ink">{{ $fromBrandLabel }}</strong>.
|
||||
Ihr Konto funktioniert für <strong class="font-semibold text-ink">beide Portale</strong> –
|
||||
presseecho.de und businessportal24.com.
|
||||
</div>
|
||||
@elseif ($showFromBanner)
|
||||
<div class="from-banner mb-3.5">
|
||||
Ihr Konto funktioniert auch für
|
||||
<strong class="font-semibold text-ink">presseecho.de</strong>
|
||||
und
|
||||
<strong class="font-semibold text-ink">businessportal24.com</strong>.
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="auth-card">
|
||||
@if ($eyebrow)
|
||||
<div class="eyebrow-hub mb-2.5">{{ $eyebrow }}</div>
|
||||
@endif
|
||||
|
||||
@if ($heading)
|
||||
<h1 class="text-[26px] font-bold tracking-[-0.5px] leading-[1.2] m-0 mb-7 text-ink">
|
||||
{{ $heading }}
|
||||
</h1>
|
||||
@endif
|
||||
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{-- Micro-Footer --}}
|
||||
<footer class="relative z-10 px-6 sm:px-10 pt-[18px] pb-[26px] flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 text-[11.5px] text-ink-3">
|
||||
<span>
|
||||
SSL <span class="text-ink-4 mx-2">·</span>
|
||||
Daten in DE <span class="text-ink-4 mx-2">·</span>
|
||||
2-Faktor verfügbar
|
||||
</span>
|
||||
<span class="flex flex-wrap items-center gap-x-1 gap-y-1">
|
||||
<a href="{{ route('impressum') }}" class="text-ink-3 no-underline hover:text-ink">Impressum</a>
|
||||
<span class="text-ink-4 mx-2">·</span>
|
||||
<a href="{{ route('datenschutz') }}" class="text-ink-3 no-underline hover:text-ink">Datenschutz</a>
|
||||
<span class="text-ink-4 mx-2">·</span>
|
||||
<a href="{{ route('agb') }}" class="text-ink-3 no-underline hover:text-ink">AGB</a>
|
||||
<span class="text-ink-4 mx-2">·</span>
|
||||
<a href="{{ route('hilfe') }}" class="text-ink-3 no-underline hover:text-ink">Support</a>
|
||||
</span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
||||
64
resources/views/components/portal/hint-card.blade.php
Normal file
64
resources/views/components/portal/hint-card.blade.php
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
@props([
|
||||
/**
|
||||
* Heroicon-Name (ohne flux:icon Prefix), z.B. user, archive-box, newspaper.
|
||||
* Wird unten via flux:icon gerendert.
|
||||
*/
|
||||
'icon' => 'information-circle',
|
||||
|
||||
/** Titel (fett, dunkel) */
|
||||
'title' => '',
|
||||
|
||||
/** Vollständigkeitswert in Prozent (0–100) für die Progress-Bar */
|
||||
'percent' => null,
|
||||
|
||||
/** Ziel-URL der Aktion (Pflicht für ein klickbares Hint) */
|
||||
'href' => null,
|
||||
|
||||
/** Optionaler Action-Text — wenn null wird `action`-Slot oder „Öffnen“ verwendet */
|
||||
'action' => null,
|
||||
|
||||
/** Wenn `true`, wird das Hint als Volt-`wire:navigate`-Link gerendert */
|
||||
'navigate' => true,
|
||||
])
|
||||
|
||||
@php
|
||||
$percent = $percent !== null ? max(0, min(100, (int) $percent)) : null;
|
||||
@endphp
|
||||
|
||||
<div class="hint-card">
|
||||
<span class="hint-ico">
|
||||
<flux:icon :name="$icon" class="size-[18px]" />
|
||||
</span>
|
||||
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-baseline justify-between gap-3">
|
||||
<div class="text-[13px] font-semibold leading-tight text-[color:var(--color-ink)]">
|
||||
{{ $title }}
|
||||
</div>
|
||||
|
||||
@if ($percent !== null)
|
||||
<span class="stat-meta whitespace-nowrap flex-shrink-0 text-[10px]">{{ $percent }} %</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($percent !== null)
|
||||
<div class="hint-bar">
|
||||
<span style="width: {{ max(2, $percent) }}%;"></span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<p class="text-[11.5px] leading-[1.5] mt-2 m-0 text-[color:var(--color-ink-3)]">
|
||||
{{ $slot }}
|
||||
</p>
|
||||
|
||||
@if ($href)
|
||||
<a
|
||||
href="{{ $href }}"
|
||||
@if ($navigate) wire:navigate @endif
|
||||
class="hint-action"
|
||||
>
|
||||
{{ $action ?? (isset($actionSlot) ? $actionSlot : __('Öffnen')) }} →
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
37
resources/views/components/portal/stat-card.blade.php
Normal file
37
resources/views/components/portal/stat-card.blade.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
@props([
|
||||
/**
|
||||
* Strip- und Label-Farbvariante.
|
||||
* Erlaubte Werte: primary | ok | warn | muted.
|
||||
* Mockup: dev/frontend/tailwind_v3/User Dashboard presseportale.html
|
||||
*/
|
||||
'variant' => 'muted',
|
||||
|
||||
/** Eyebrow-Label oben links (UPPERCASE, sperrgesetzt) */
|
||||
'label' => '',
|
||||
|
||||
/** Hauptzahl in JetBrains Mono */
|
||||
'value' => 0,
|
||||
])
|
||||
|
||||
@php
|
||||
$allowedVariants = ['primary', 'ok', 'warn', 'muted'];
|
||||
$variant = in_array($variant, $allowedVariants, true) ? $variant : 'muted';
|
||||
@endphp
|
||||
|
||||
<article {{ $attributes->class(['stat-card', "is-{$variant}"]) }}>
|
||||
<span class="stat-strip"></span>
|
||||
|
||||
<div class="flex items-baseline justify-between gap-3">
|
||||
<div class="stat-label">{{ $label }}</div>
|
||||
|
||||
@isset($meta)
|
||||
<span class="stat-meta">{{ $meta }}</span>
|
||||
@endisset
|
||||
</div>
|
||||
|
||||
<div class="stat-num">{{ $value }}</div>
|
||||
|
||||
@isset($trend)
|
||||
<div class="stat-trend">{{ $trend }}</div>
|
||||
@endisset
|
||||
</article>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
@props([
|
||||
'brand' => 'presseportale',
|
||||
'brand' => 'pressekonto',
|
||||
'variant' => 'auto',
|
||||
'serif' => true,
|
||||
])
|
||||
|
|
@ -9,47 +9,66 @@
|
|||
* Zentrale Brand-Wortmarke für alle drei Marken der Verlags-Familie.
|
||||
*
|
||||
* Schreibweise (verbindlich):
|
||||
* - presseecho → "presse" + "echo" (echo ist die Akzentfarbe)
|
||||
* - businessportal24 → "businessportal" + "24" (24 ist orange)
|
||||
* - presseportale → "presse" + "portale" (portale ist Bernstein)
|
||||
* - presseecho → "presse" + "echo" (echo grün)
|
||||
* - businessportal24 → "businessportal" + "24" (24 orange)
|
||||
* - pressekonto → "presse" + "konto" (konto bernstein)
|
||||
*
|
||||
* Keine TLD-Endung (".de", ".com") direkt am Markennamen. Diese gehören
|
||||
* – falls überhaupt – getrennt in den juristischen Bereich (Impressum, Kontakt).
|
||||
*
|
||||
* Schriftart:
|
||||
* - Standard `font-serif` (Source Serif 4) – passt zum Editorial-Charakter
|
||||
* von Presseecho und BusinessPortal24. Der Hub lädt Source Serif 4
|
||||
* ebenfalls mit, damit Markennennungen typografisch konsistent bleiben.
|
||||
* aller drei Portale.
|
||||
* - `:serif="false"` schaltet auf font-sans (Inter Tight) – etwa für die
|
||||
* Top-Utility-Bar, in der die Marken sehr klein erscheinen.
|
||||
*
|
||||
* Variant:
|
||||
* - `auto` → Akzentfarbe = Theme-Default (Orange / Grün / Bernstein)
|
||||
* - `on-dark` → hellere/wärmere Akzentfarbe (für dunkle Hub-Panels)
|
||||
* - `mono` → Akzent identisch zum Basis-Ton (z. B. wenn beides weiß sein soll)
|
||||
* - `auto` → Name + Akzent in den Marken-Standardfarben (auf hellem Grund)
|
||||
* - `on-dark` → Name in Weiß (Negativ), Akzent in der dunkel-tauglichen Variante
|
||||
* - `mono` → Name + Akzent erben vom Parent (z. B. wenn beides weiß sein soll)
|
||||
*
|
||||
* Logo-Farben (siehe Brand-Manual):
|
||||
* - businessportal: #232A33 (auf hell) / #FFFFFF (negativ)
|
||||
* - presse (Echo): #1B2417 (auf hell) / #FFFFFF (negativ)
|
||||
* - presse (Konto): #1A2540 (auf hell) / #FFFFFF (negativ)
|
||||
* - 24: #C84A1E immer
|
||||
* - echo: #345636 (auf hell) / #9BD5B2 (negativ)
|
||||
* - konto: #B07A3A immer
|
||||
*/
|
||||
$marks = [
|
||||
'presseecho' => [
|
||||
'name' => 'presse',
|
||||
'accent' => 'echo',
|
||||
'name_color_auto' => '#1B2417',
|
||||
'name_color_on_dark' => '#FFFFFF',
|
||||
'accent_color_auto' => '#345636',
|
||||
'accent_color_on_dark' => '#9BD5B2',
|
||||
],
|
||||
'businessportal24' => [
|
||||
'name' => 'businessportal',
|
||||
'accent' => '24',
|
||||
'name_color_auto' => '#232A33',
|
||||
'name_color_on_dark' => '#FFFFFF',
|
||||
'accent_color_auto' => '#C84A1E',
|
||||
'accent_color_on_dark' => '#C84A1E',
|
||||
],
|
||||
'presseportale' => [
|
||||
'pressekonto' => [
|
||||
'name' => 'presse',
|
||||
'accent' => 'portale',
|
||||
'accent' => 'konto',
|
||||
'name_color_auto' => '#1A2540',
|
||||
'name_color_on_dark' => '#FFFFFF',
|
||||
'accent_color_auto' => '#B07A3A',
|
||||
'accent_color_on_dark' => '#B07A3A',
|
||||
],
|
||||
];
|
||||
|
||||
$mark = $marks[$brand] ?? $marks['presseportale'];
|
||||
$mark = $marks[$brand] ?? $marks['pressekonto'];
|
||||
|
||||
$nameColor = match ($variant) {
|
||||
'on-dark' => $mark['name_color_on_dark'],
|
||||
'mono' => 'inherit',
|
||||
default => $mark['name_color_auto'],
|
||||
};
|
||||
|
||||
$accentColor = match ($variant) {
|
||||
'on-dark' => $mark['accent_color_on_dark'],
|
||||
|
|
@ -62,5 +81,6 @@
|
|||
$baseAttributes = $attributes->merge(['class' => $fontClass]);
|
||||
@endphp
|
||||
|
||||
<span {{ $baseAttributes }}>{{ $mark['name'] }}<span
|
||||
<span {{ $baseAttributes }}><span
|
||||
style="color: {{ $nameColor }};">{{ $mark['name'] }}</span><span
|
||||
style="color: {{ $accentColor }};">{{ $mark['accent'] }}</span></span>
|
||||
|
|
|
|||
|
|
@ -3,19 +3,19 @@
|
|||
])
|
||||
|
||||
@php
|
||||
$themeKey = config('app.theme', 'presseportale');
|
||||
$themeKey = config('app.theme', 'pressekonto');
|
||||
$brand = $brand ?? config('domains.domains.' . $themeKey . '.brand', []);
|
||||
$brandTagline = $brand['tagline_short'] ?? 'Publisher · Hub';
|
||||
$brandTaglineLong = $brand['tagline_long'] ?? 'Der gemeinsame Publisher-Bereich für presseecho und businessportal24.';
|
||||
$footerLegal = str_replace(':year', (string) now()->year, $brand['footer_legal'] ?? '© ' . now()->year . ' presseportale');
|
||||
$footerLegal = str_replace(':year', (string) now()->year, $brand['footer_legal'] ?? '© ' . now()->year . ' pressekonto');
|
||||
@endphp
|
||||
|
||||
<footer class="bg-hub-grad-2 text-ink-on-dark">
|
||||
<div class="max-w-layout mx-auto px-8 py-14 grid gap-10" style="grid-template-columns:1.5fr 1fr 1fr 1fr;">
|
||||
|
||||
<div>
|
||||
<div class="text-[24px] font-bold leading-none tracking-[-0.5px] text-white">
|
||||
<x-web.brand-mark brand="presseportale" :serif="false" />
|
||||
<div class="text-[24px] font-bold leading-none tracking-[-0.5px]">
|
||||
<x-web.brand-mark brand="pressekonto" variant="on-dark" :serif="false" />
|
||||
</div>
|
||||
<div class="eyebrow on-dark mt-2 text-[9.5px] tracking-[0.22em]">
|
||||
{{ $brandTagline }}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@
|
|||
])
|
||||
|
||||
@php
|
||||
$themeKey = config('app.theme', 'presseportale');
|
||||
$themeKey = config('app.theme', 'pressekonto');
|
||||
$brand =
|
||||
$brand ??
|
||||
config('domains.domains.' . $themeKey . '.brand', [
|
||||
'name' => 'presse',
|
||||
'accent' => 'portale',
|
||||
'accent' => 'konto',
|
||||
'tagline_short' => 'Publisher · Hub',
|
||||
]);
|
||||
$brandTagline = $brand['tagline_short'] ?? 'Publisher · Hub';
|
||||
|
|
@ -27,9 +27,9 @@
|
|||
<div class="max-w-layout mx-auto px-8 py-[18px] grid items-center gap-6" style="grid-template-columns:auto 1fr auto;">
|
||||
|
||||
<a href="{{ route('home') }}" class="flex items-baseline gap-2.5 cursor-pointer group"
|
||||
aria-label="presseportale Startseite">
|
||||
aria-label="pressekonto Startseite">
|
||||
<span class="text-[24px] font-bold tracking-[-0.5px] leading-none text-hub">
|
||||
<x-web.brand-mark brand="presseportale" :serif="false" />
|
||||
<x-web.brand-mark brand="pressekonto" :serif="false" />
|
||||
</span>
|
||||
<span class="hidden md:inline-block w-px h-[18px] bg-bg-rule"></span>
|
||||
<span class="eyebrow muted text-[9.5px] tracking-[0.22em]">{{ $brandTagline }}</span>
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@
|
|||
<div class="max-w-layout mx-auto px-8 py-12 grid gap-10 grid-cols-1 md:grid-cols-2 lg:grid-cols-[1.4fr_1fr_1fr_1fr]">
|
||||
<div>
|
||||
<a href="{{ route('home') }}" class="block cursor-pointer group" aria-label="{{ $brandName }}{{ $brandAccent }} Startseite">
|
||||
<div class="font-serif text-[24px] font-semibold leading-none tracking-[-0.5px] text-white group-hover:text-white/80 transition-colors">
|
||||
{{ $brandName }}@if ($brandAccent)<span class="text-brand">{{ $brandAccent }}</span>@endif
|
||||
<div class="text-[24px] font-semibold leading-none tracking-[-0.5px]">
|
||||
<x-web.brand-mark :brand="$themeKey" variant="on-dark" />
|
||||
</div>
|
||||
<div class="eyebrow mt-2 text-[9.5px] tracking-[0.18em] text-ink-on-dark-muted">
|
||||
{{ $brandTagline }}
|
||||
|
|
|
|||
|
|
@ -102,8 +102,8 @@
|
|||
@keydown.escape.window="searchOpen = false">
|
||||
<div class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 py-3 lg:py-[18px] flex items-center gap-3 sm:gap-4 lg:gap-6">
|
||||
<a href="{{ route('home') }}" class="block cursor-pointer group flex-shrink-0" aria-label="{{ $brandName }}{{ $brandAccent }} Startseite">
|
||||
<div class="font-serif text-[22px] sm:text-[24px] lg:text-[28px] font-semibold leading-none tracking-[-0.5px] text-ink group-hover:text-brand transition-colors">
|
||||
{{ $brandName }}@if ($brandAccent)<span class="text-brand">{{ $brandAccent }}</span>@endif
|
||||
<div class="text-[22px] sm:text-[24px] lg:text-[28px] font-semibold leading-none tracking-[-0.5px]">
|
||||
<x-web.brand-mark :brand="$themeKey" />
|
||||
</div>
|
||||
<div class="eyebrow muted mt-1 text-[9.5px] tracking-[0.18em] hidden sm:block">
|
||||
{{ $brandTagline }}
|
||||
|
|
|
|||
|
|
@ -137,44 +137,56 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
|
|||
|
||||
}; ?>
|
||||
|
||||
<div class="space-y-6">
|
||||
<flux:card>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Neue Pressemitteilung') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Entwurf erstellen oder direkt zur Prüfung einreichen.') }}</flux:subheading>
|
||||
<div class="space-y-8">
|
||||
{{-- ============== PAGE HEADER ============== --}}
|
||||
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
|
||||
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
|
||||
<span class="eyebrow muted">{{ __('Content · Neu anlegen') }}</span>
|
||||
</div>
|
||||
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
|
||||
{{ __('Neue Pressemitteilung') }}
|
||||
</h1>
|
||||
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
|
||||
{{ __('Entwurf erstellen oder direkt zur Prüfung einreichen.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.press-releases.index') }}" wire:navigate>
|
||||
{{ __('Zurück') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</flux:card>
|
||||
</header>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-[1fr,320px]">
|
||||
{{-- Hauptinhalt --}}
|
||||
{{-- ============== HAUPTINHALT ============== --}}
|
||||
<div class="space-y-6">
|
||||
<flux:card>
|
||||
<flux:heading size="md" class="mb-4">{{ __('Inhalt') }}</flux:heading>
|
||||
|
||||
<div class="space-y-4">
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Inhalt') }}</span>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Titel') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:label>{{ __('Titel') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
|
||||
<flux:input wire:model.live.debounce.500ms="title" placeholder="{{ __('Aussagekräftiger Titel…') }}" />
|
||||
<flux:error name="title" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Text') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:label>{{ __('Text') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
|
||||
<flux:textarea wire:model="text" rows="16" placeholder="{{ __('Vollständiger Text der Pressemitteilung…') }}" />
|
||||
<flux:error name="text" />
|
||||
</flux:field>
|
||||
</div>
|
||||
</flux:card>
|
||||
</article>
|
||||
|
||||
<flux:card>
|
||||
<flux:heading size="md" class="mb-4">{{ __('SEO & Links') }}</flux:heading>
|
||||
|
||||
<div class="space-y-4">
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('SEO & Links') }}</span>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Stichwörter') }}</flux:label>
|
||||
<flux:input wire:model="keywords" placeholder="{{ __('Kommagetrennte Stichwörter…') }}" />
|
||||
|
|
@ -187,19 +199,20 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
|
|||
<flux:error name="backlinkUrl" />
|
||||
</flux:field>
|
||||
</div>
|
||||
</flux:card>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
{{-- Sidebar --}}
|
||||
<div class="space-y-4">
|
||||
<flux:card>
|
||||
<flux:heading size="md" class="mb-4">{{ __('Metadaten') }}</flux:heading>
|
||||
|
||||
<div class="space-y-4">
|
||||
{{-- ============== SIDEBAR ============== --}}
|
||||
<aside class="space-y-4">
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Metadaten') }}</span>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Portal') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:label>{{ __('Portal') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
|
||||
<flux:select wire:model="portal">
|
||||
@foreach($portalOptions as $p)
|
||||
@foreach ($portalOptions as $p)
|
||||
<option value="{{ $p->value }}">{{ $p->label() }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
|
|
@ -215,7 +228,7 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
|
|||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Firma') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:label>{{ __('Firma') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
|
||||
<flux:select
|
||||
wire:model.live="companyId"
|
||||
variant="combobox"
|
||||
|
|
@ -229,14 +242,14 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
|
|||
placeholder="{{ __('Name eingeben…') }}"
|
||||
/>
|
||||
</x-slot>
|
||||
@foreach($companies as $company)
|
||||
@foreach ($companies as $company)
|
||||
<flux:select.option :value="$company->id" wire:key="{{ $company->id }}">
|
||||
{{ $company->name }}
|
||||
</flux:select.option>
|
||||
@endforeach
|
||||
<x-slot name="empty">
|
||||
<flux:select.option.empty>
|
||||
@if(blank(trim($companySearch)))
|
||||
@if (blank(trim($companySearch)))
|
||||
{{ __('Mindestens 1 Zeichen eingeben…') }}
|
||||
@else
|
||||
{{ __('Keine Firma gefunden.') }}
|
||||
|
|
@ -248,11 +261,11 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
|
|||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Kategorie') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:label>{{ __('Kategorie') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
|
||||
<flux:select wire:model="categoryId">
|
||||
<option value="">{{ __('Bitte wählen…') }}</option>
|
||||
@foreach($categories as $cat)
|
||||
@php $catName = $cat->translations->firstWhere('locale', 'de')?->name ?? $cat->id; @endphp
|
||||
@foreach ($categories as $cat)
|
||||
@php($catName = $cat->translations->firstWhere('locale', 'de')?->name ?? $cat->id)
|
||||
<option value="{{ $cat->id }}">{{ $catName }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
|
|
@ -261,10 +274,13 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
|
|||
|
||||
<flux:checkbox wire:model="noExport" label="{{ __('Kein Export') }}" />
|
||||
</div>
|
||||
</flux:card>
|
||||
</article>
|
||||
|
||||
<flux:card>
|
||||
<div class="space-y-2">
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Aktionen') }}</span>
|
||||
</div>
|
||||
<div class="p-5 space-y-2">
|
||||
<flux:button
|
||||
type="button"
|
||||
variant="primary"
|
||||
|
|
@ -284,7 +300,7 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
|
|||
{{ __('Als Entwurf speichern') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
</article>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -264,53 +264,80 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
|
|||
|
||||
}; ?>
|
||||
|
||||
<div class="space-y-6">
|
||||
@if(session('success'))
|
||||
<div class="rounded-md border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800 dark:border-green-800 dark:bg-green-900/20 dark:text-green-300">
|
||||
<div class="space-y-8">
|
||||
@php
|
||||
$statusClass = match ($currentStatus) {
|
||||
'published' => 'ok',
|
||||
'review' => 'warn',
|
||||
'rejected' => 'err',
|
||||
default => 'hub',
|
||||
};
|
||||
@endphp
|
||||
|
||||
@if (session('success'))
|
||||
<div class="px-4 py-3 rounded-[5px] border text-[12.5px]
|
||||
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<flux:card>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Pressemitteilung bearbeiten') }}</flux:heading>
|
||||
<flux:subheading>ID: {{ $id }}</flux:subheading>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:badge :color="$statusColor" size="lg">{{ $statusEnum?->label() ?? $currentStatus }}</flux:badge>
|
||||
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.press-releases.index') }}" wire:navigate>
|
||||
{{ __('Zurück') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
@if (session('error'))
|
||||
<div class="px-4 py-3 rounded-[5px] border text-[12.5px]
|
||||
bg-[color:var(--color-err-soft)] border-[color:var(--color-err)]/30 text-[color:var(--color-loss)]">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
</flux:card>
|
||||
@endif
|
||||
|
||||
{{-- ============== PAGE HEADER ============== --}}
|
||||
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-3 mb-3 flex-wrap">
|
||||
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
|
||||
<span class="eyebrow muted">{{ __('Content · Bearbeiten') }}</span>
|
||||
<span @class(['badge', $statusClass])>{{ $statusEnum?->label() ?? $currentStatus }}</span>
|
||||
<span class="badge hub">ID {{ $id }}</span>
|
||||
</div>
|
||||
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
|
||||
{{ __('Pressemitteilung bearbeiten') }}
|
||||
</h1>
|
||||
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
|
||||
{{ __('Inhalt, Metadaten und Status der PM aktualisieren. Änderungen werden sofort wirksam.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.press-releases.index') }}" wire:navigate>
|
||||
{{ __('Zurück') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-[1fr,320px]">
|
||||
{{-- Hauptinhalt --}}
|
||||
{{-- ============== HAUPTINHALT ============== --}}
|
||||
<div class="space-y-6">
|
||||
<flux:card>
|
||||
<flux:heading size="md" class="mb-4">{{ __('Inhalt') }}</flux:heading>
|
||||
|
||||
<div class="space-y-4">
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Inhalt') }}</span>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Titel') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:label>{{ __('Titel') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
|
||||
<flux:input wire:model="title" />
|
||||
<flux:error name="title" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Text') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:label>{{ __('Text') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
|
||||
<flux:textarea wire:model="text" rows="20" />
|
||||
<flux:error name="text" />
|
||||
</flux:field>
|
||||
</div>
|
||||
</flux:card>
|
||||
</article>
|
||||
|
||||
<flux:card>
|
||||
<flux:heading size="md" class="mb-4">{{ __('SEO & Links') }}</flux:heading>
|
||||
|
||||
<div class="space-y-4">
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('SEO & Links') }}</span>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Stichwörter') }}</flux:label>
|
||||
<flux:input wire:model="keywords" placeholder="{{ __('Kommagetrennt…') }}" />
|
||||
|
|
@ -322,22 +349,23 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
|
|||
<flux:error name="backlinkUrl" />
|
||||
</flux:field>
|
||||
</div>
|
||||
</flux:card>
|
||||
</article>
|
||||
|
||||
<livewire:components.press-release-images-manager :press-release-id="$id" :wire:key="'pr-images-'.$id" />
|
||||
</div>
|
||||
|
||||
{{-- Sidebar --}}
|
||||
<div class="space-y-4">
|
||||
{{-- Status-Aktionen --}}
|
||||
<flux:card>
|
||||
<flux:heading size="sm" class="mb-3">{{ __('Status-Aktionen') }}</flux:heading>
|
||||
|
||||
<div class="space-y-3">
|
||||
{{-- ============== SIDEBAR ============== --}}
|
||||
<aside class="space-y-4">
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Status-Aktionen') }}</span>
|
||||
<span @class(['badge', $statusClass])>{{ $statusEnum?->label() ?? $currentStatus }}</span>
|
||||
</div>
|
||||
<div class="p-5 space-y-3">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Neuer Status') }}</flux:label>
|
||||
<flux:select wire:model.live="targetStatus">
|
||||
@foreach($statusOptions as $statusOption)
|
||||
@foreach ($statusOptions as $statusOption)
|
||||
<option value="{{ $statusOption->value }}">
|
||||
{{ $statusOption->label() }}{{ $statusOption->value === $currentStatus ? ' (aktuell)' : '' }}
|
||||
</option>
|
||||
|
|
@ -352,17 +380,17 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
|
|||
</flux:button>
|
||||
</flux:modal.trigger>
|
||||
</div>
|
||||
</flux:card>
|
||||
</article>
|
||||
|
||||
{{-- Metadaten --}}
|
||||
<flux:card>
|
||||
<flux:heading size="sm" class="mb-3">{{ __('Metadaten') }}</flux:heading>
|
||||
|
||||
<div class="space-y-4">
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Metadaten') }}</span>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Portal') }}</flux:label>
|
||||
<flux:select wire:model="portal">
|
||||
@foreach($portalOptions as $p)
|
||||
@foreach ($portalOptions as $p)
|
||||
<option value="{{ $p->value }}">{{ $p->label() }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
|
|
@ -391,14 +419,14 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
|
|||
placeholder="{{ __('Name eingeben…') }}"
|
||||
/>
|
||||
</x-slot>
|
||||
@foreach($companies as $company)
|
||||
@foreach ($companies as $company)
|
||||
<flux:select.option :value="$company->id" wire:key="{{ $company->id }}">
|
||||
{{ $company->name }}
|
||||
</flux:select.option>
|
||||
@endforeach
|
||||
<x-slot name="empty">
|
||||
<flux:select.option.empty>
|
||||
@if(blank(trim($companySearch)))
|
||||
@if (blank(trim($companySearch)))
|
||||
{{ __('Mindestens 1 Zeichen eingeben…') }}
|
||||
@else
|
||||
{{ __('Keine Firma gefunden.') }}
|
||||
|
|
@ -413,8 +441,8 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
|
|||
<flux:label>{{ __('Kategorie') }}</flux:label>
|
||||
<flux:select wire:model="categoryId">
|
||||
<option value="">{{ __('Bitte wählen…') }}</option>
|
||||
@foreach($categories as $cat)
|
||||
@php $catName = $cat->translations->firstWhere('locale', 'de')?->name ?? $cat->id; @endphp
|
||||
@foreach ($categories as $cat)
|
||||
@php($catName = $cat->translations->firstWhere('locale', 'de')?->name ?? $cat->id)
|
||||
<option value="{{ $cat->id }}">{{ $catName }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
|
|
@ -423,18 +451,24 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
|
|||
|
||||
<flux:checkbox wire:model="noExport" label="{{ __('Kein Export') }}" />
|
||||
</div>
|
||||
</flux:card>
|
||||
</article>
|
||||
|
||||
<flux:button type="button" variant="primary" class="w-full" wire:click="save">
|
||||
{{ __('Änderungen speichern') }}
|
||||
</flux:button>
|
||||
|
||||
<flux:modal.trigger name="confirm-delete-press-release">
|
||||
<flux:button type="button" variant="danger" icon="trash" class="w-full">
|
||||
{{ __('Pressemitteilung löschen') }}
|
||||
</flux:button>
|
||||
</flux:modal.trigger>
|
||||
</div>
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Aktionen') }}</span>
|
||||
</div>
|
||||
<div class="p-5 space-y-2">
|
||||
<flux:button type="button" variant="primary" class="w-full" wire:click="save">
|
||||
{{ __('Änderungen speichern') }}
|
||||
</flux:button>
|
||||
<flux:modal.trigger name="confirm-delete-press-release">
|
||||
<flux:button type="button" variant="danger" icon="trash" class="w-full">
|
||||
{{ __('Pressemitteilung löschen') }}
|
||||
</flux:button>
|
||||
</flux:modal.trigger>
|
||||
</div>
|
||||
</article>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<flux:modal name="confirm-status-change" class="max-w-lg">
|
||||
|
|
|
|||
|
|
@ -349,43 +349,68 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
|
|||
}
|
||||
}; ?>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-8">
|
||||
@if (session('success'))
|
||||
<div
|
||||
class="rounded-md border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800 dark:border-green-800 dark:bg-green-900/20 dark:text-green-300">
|
||||
<div class="px-4 py-3 rounded-[5px] border text-[12.5px]
|
||||
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
@if (session('error'))
|
||||
<div class="px-4 py-3 rounded-[5px] border text-[12.5px]
|
||||
bg-[color:var(--color-err-soft)] border-[color:var(--color-err)]/30 text-[color:var(--color-loss)]">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Statistiken --}}
|
||||
<div class="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||
<flux:card>
|
||||
<flux:text class="text-xs text-zinc-500">{{ __('Gesamt') }}</flux:text>
|
||||
<flux:text size="xl" weight="bold">{{ $stats['total'] }}</flux:text>
|
||||
</flux:card>
|
||||
<flux:card>
|
||||
<flux:text class="text-xs text-green-600">{{ __('Veröffentlicht') }}</flux:text>
|
||||
<flux:text size="xl" weight="bold">{{ $stats['published'] }}</flux:text>
|
||||
</flux:card>
|
||||
<flux:card>
|
||||
<flux:text class="text-xs text-yellow-600">{{ __('In Prüfung') }}</flux:text>
|
||||
<flux:text size="xl" weight="bold">{{ $stats['review'] }}</flux:text>
|
||||
</flux:card>
|
||||
<flux:card>
|
||||
<flux:text class="text-xs text-zinc-500">{{ __('Entwürfe') }}</flux:text>
|
||||
<flux:text size="xl" weight="bold">{{ $stats['draft'] }}</flux:text>
|
||||
</flux:card>
|
||||
</div>
|
||||
{{-- ============== PAGE HEADER ============== --}}
|
||||
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
|
||||
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
|
||||
<span class="eyebrow muted">{{ __('Content · Pressemitteilungen') }}</span>
|
||||
</div>
|
||||
<h1 class="text-[34px] font-bold tracking-[-0.7px] leading-[1.1] m-0 text-[color:var(--color-ink)]">
|
||||
{{ __('Pressemitteilungen') }}
|
||||
</h1>
|
||||
<p class="text-[13.5px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
|
||||
{{ __('Übersicht aller PMs beider Portale, mit Filter, Status-Workflow und Schnellaktionen.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<flux:button icon="plus" variant="primary" href="{{ route('admin.press-releases.create') }}" wire:navigate>
|
||||
{{ __('Neue PM') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 flex-shrink-0">
|
||||
<flux:button icon="plus" variant="primary" href="{{ route('admin.press-releases.create') }}" wire:navigate>
|
||||
{{ __('Neue PM') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{{-- Filter --}}
|
||||
<flux:card>
|
||||
<div class="flex flex-col gap-3">
|
||||
{{-- ============== KPI-Reihe ============== --}}
|
||||
<section class="grid gap-4 grid-cols-2 sm:grid-cols-4">
|
||||
<x-portal.stat-card variant="primary" :label="__('Gesamt')" :value="number_format($stats['total'])">
|
||||
<x-slot:meta>{{ now()->format('Y') }}</x-slot:meta>
|
||||
<x-slot:trend>{{ __('über beide Portale') }}</x-slot:trend>
|
||||
</x-portal.stat-card>
|
||||
<x-portal.stat-card variant="ok" :label="__('Veröffentlicht')" :value="number_format($stats['published'])">
|
||||
<x-slot:meta>{{ __('live') }}</x-slot:meta>
|
||||
<x-slot:trend>{{ __('öffentlich sichtbar') }}</x-slot:trend>
|
||||
</x-portal.stat-card>
|
||||
<x-portal.stat-card variant="warn" :label="__('In Prüfung')" :value="number_format($stats['review'])">
|
||||
<x-slot:meta>{{ __('queue') }}</x-slot:meta>
|
||||
<x-slot:trend>{{ __('redaktionelle Prüfung') }}</x-slot:trend>
|
||||
</x-portal.stat-card>
|
||||
<x-portal.stat-card variant="muted" :label="__('Entwürfe')" :value="number_format($stats['draft'])">
|
||||
<x-slot:meta>{{ __('privat') }}</x-slot:meta>
|
||||
<x-slot:trend>{{ __('nicht eingereicht') }}</x-slot:trend>
|
||||
</x-portal.stat-card>
|
||||
</section>
|
||||
|
||||
{{-- ============== FILTER-PANEL ============== --}}
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Filter & Suche') }}</span>
|
||||
</div>
|
||||
<div class="p-5 flex flex-col gap-3">
|
||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-6">
|
||||
<flux:input
|
||||
wire:model.live.debounce.300ms="search"
|
||||
|
|
@ -551,10 +576,16 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
</article>
|
||||
|
||||
{{-- Tabelle --}}
|
||||
<flux:card class="overflow-hidden">
|
||||
{{-- ============== TABELLE-PANEL ============== --}}
|
||||
<article class="panel overflow-hidden">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Alle Pressemitteilungen') }}</span>
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ __(':count Einträge', ['count' => $pressReleases->count()]) }}
|
||||
</span>
|
||||
</div>
|
||||
<flux:table>
|
||||
<flux:table.columns>
|
||||
<flux:table.column>{{ __('Aktionen') }}</flux:table.column>
|
||||
|
|
@ -604,16 +635,15 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
|
|||
</div>
|
||||
</flux:table.cell>
|
||||
<flux:table.cell>
|
||||
<flux:badge
|
||||
color="{{ match ($pr->status->value) {
|
||||
'published' => 'green',
|
||||
'review' => 'yellow',
|
||||
'draft' => 'zinc',
|
||||
'rejected' => 'red',
|
||||
'archived' => 'blue',
|
||||
} }}">
|
||||
<span @class([
|
||||
'badge',
|
||||
'ok' => $pr->status->value === 'published',
|
||||
'warn' => $pr->status->value === 'review',
|
||||
'err' => $pr->status->value === 'rejected',
|
||||
'hub' => in_array($pr->status->value, ['archived', 'draft'], true),
|
||||
])>
|
||||
{{ $pr->status->label() }}
|
||||
</flux:badge>
|
||||
</span>
|
||||
</flux:table.cell>
|
||||
<flux:table.cell>
|
||||
<flux:text class="text-sm">{{ $pr->portal->label() }}</flux:text>
|
||||
|
|
@ -704,16 +734,23 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
|
|||
@empty
|
||||
<flux:table.row>
|
||||
<flux:table.cell colspan="8">
|
||||
<div class="flex flex-col items-center justify-center py-10">
|
||||
<flux:icon.newspaper class="size-10 text-zinc-300" />
|
||||
<flux:text class="mt-3 text-zinc-500">{{ __('Keine Pressemitteilungen gefunden.') }}
|
||||
</flux:text>
|
||||
<div class="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-4
|
||||
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
|
||||
<flux:icon.newspaper class="size-6" />
|
||||
</div>
|
||||
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
|
||||
{{ __('Keine Pressemitteilungen gefunden') }}
|
||||
</div>
|
||||
<p class="text-[12px] text-[color:var(--color-ink-3)] max-w-md m-0">
|
||||
{{ __('Passen Sie die Filter an oder erstellen Sie eine neue Pressemitteilung.') }}
|
||||
</p>
|
||||
</div>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@endforelse
|
||||
</flux:table>
|
||||
</flux:card>
|
||||
</article>
|
||||
|
||||
{{ $pressReleases->links() }}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -90,46 +90,75 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
}
|
||||
}; ?>
|
||||
|
||||
<div class="space-y-6">
|
||||
@if(session('success'))
|
||||
<div class="rounded-md border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800 dark:border-green-800 dark:bg-green-900/20 dark:text-green-300">
|
||||
<div class="space-y-8">
|
||||
@php
|
||||
$statusClass = match ($pr->status->value) {
|
||||
'published' => 'ok',
|
||||
'review' => 'warn',
|
||||
'rejected' => 'err',
|
||||
default => 'hub',
|
||||
};
|
||||
@endphp
|
||||
|
||||
@if (session('success'))
|
||||
<div class="px-4 py-3 rounded-[5px] border text-[12.5px]
|
||||
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<flux:card>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:badge :color="$statusColor">{{ $pr->status->label() }}</flux:badge>
|
||||
<flux:badge color="zinc" size="sm">{{ strtoupper($pr->language) }}</flux:badge>
|
||||
<flux:badge color="zinc" size="sm">{{ $pr->portal->label() }}</flux:badge>
|
||||
</div>
|
||||
<flux:heading size="xl" class="mt-2">{{ $pr->title }}</flux:heading>
|
||||
<flux:text class="mt-1 text-sm text-zinc-500">
|
||||
{{ __('Firma') }}: {{ $pr->company?->name ?? '–' }} ·
|
||||
{{ __('Kategorie') }}: {{ $categoryName }} ·
|
||||
{{ __('Autor') }}: {{ $pr->user?->name ?? '–' }}
|
||||
</flux:text>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<flux:button variant="ghost" icon="pencil" href="{{ route('admin.press-releases.edit', $pr->id) }}" wire:navigate>
|
||||
{{ __('Bearbeiten') }}
|
||||
</flux:button>
|
||||
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.press-releases.index') }}" wire:navigate>
|
||||
{{ __('Zurück') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
@if (session('error'))
|
||||
<div class="px-4 py-3 rounded-[5px] border text-[12.5px]
|
||||
bg-[color:var(--color-err-soft)] border-[color:var(--color-err)]/30 text-[color:var(--color-loss)]">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
</flux:card>
|
||||
@endif
|
||||
|
||||
{{-- Status-Aktionen --}}
|
||||
@if($pr->status === \App\Enums\PressReleaseStatus::Review)
|
||||
<flux:card>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<flux:text weight="medium" class="text-yellow-700 dark:text-yellow-400">
|
||||
{{-- ============== PAGE HEADER ============== --}}
|
||||
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-3 mb-3 flex-wrap">
|
||||
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
|
||||
<span class="eyebrow muted">{{ __('Content · Pressemitteilung') }}</span>
|
||||
<span @class(['badge', $statusClass])>{{ $pr->status->label() }}</span>
|
||||
<span class="badge hub">{{ strtoupper($pr->language) }}</span>
|
||||
<span class="badge hub">{{ $pr->portal->label() }}</span>
|
||||
</div>
|
||||
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
|
||||
{{ $pr->title }}
|
||||
</h1>
|
||||
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[720px] text-[color:var(--color-ink-2)]">
|
||||
<strong class="font-semibold text-[color:var(--color-ink)]">{{ __('Firma') }}:</strong>
|
||||
{{ $pr->company?->name ?? '–' }}
|
||||
<span class="text-[color:var(--color-bg-rule)] mx-1">·</span>
|
||||
<strong class="font-semibold text-[color:var(--color-ink)]">{{ __('Kategorie') }}:</strong>
|
||||
{{ $categoryName }}
|
||||
<span class="text-[color:var(--color-bg-rule)] mx-1">·</span>
|
||||
<strong class="font-semibold text-[color:var(--color-ink)]">{{ __('Autor') }}:</strong>
|
||||
{{ $pr->user?->name ?? '–' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<flux:button variant="ghost" icon="pencil" href="{{ route('admin.press-releases.edit', $pr->id) }}" wire:navigate>
|
||||
{{ __('Bearbeiten') }}
|
||||
</flux:button>
|
||||
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.press-releases.index') }}" wire:navigate>
|
||||
{{ __('Zurück') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{{-- ============== STATUS-WORKFLOW ============== --}}
|
||||
@if ($pr->status === \App\Enums\PressReleaseStatus::Review)
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Status-Workflow') }}</span>
|
||||
<span class="badge warn dot">{{ __('Wartet auf Prüfung') }}</span>
|
||||
</div>
|
||||
<div class="p-5 flex flex-wrap items-center gap-3">
|
||||
<p class="text-[13px] text-[color:var(--color-ink-2)] m-0 flex-1 min-w-[200px]">
|
||||
{{ __('Diese PM wartet auf Prüfung.') }}
|
||||
</flux:text>
|
||||
</p>
|
||||
<flux:modal.trigger name="confirm-show-publish">
|
||||
<flux:button type="button" variant="primary">{{ __('Veröffentlichen') }}</flux:button>
|
||||
</flux:modal.trigger>
|
||||
|
|
@ -137,136 +166,162 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
<flux:button type="button" variant="danger">{{ __('Ablehnen') }}</flux:button>
|
||||
</flux:modal.trigger>
|
||||
</div>
|
||||
</flux:card>
|
||||
</article>
|
||||
@endif
|
||||
@if($pr->status === \App\Enums\PressReleaseStatus::Published)
|
||||
<flux:card>
|
||||
<div class="flex items-center gap-3">
|
||||
@if ($pr->status === \App\Enums\PressReleaseStatus::Published)
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Status-Workflow') }}</span>
|
||||
<span class="badge ok dot">{{ __('Live') }}</span>
|
||||
</div>
|
||||
<div class="p-5 flex flex-wrap items-center gap-3">
|
||||
@if ($pr->hits > 0)
|
||||
<p class="text-[13px] text-[color:var(--color-ink-2)] m-0 flex-1 min-w-[200px]">
|
||||
<strong class="text-[color:var(--color-ink)] font-semibold">{{ number_format($pr->hits) }}</strong>
|
||||
{{ __('Aufrufe seit Veröffentlichung') }}
|
||||
</p>
|
||||
@endif
|
||||
<flux:modal.trigger name="confirm-show-archive">
|
||||
<flux:button type="button" variant="ghost">{{ __('Archivieren') }}</flux:button>
|
||||
</flux:modal.trigger>
|
||||
@if($pr->hits > 0)
|
||||
<flux:text class="text-sm text-zinc-500">{{ number_format($pr->hits) }} {{ __('Aufrufe') }}</flux:text>
|
||||
@endif
|
||||
</div>
|
||||
</flux:card>
|
||||
</article>
|
||||
@endif
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-[1fr,280px]">
|
||||
{{-- Text --}}
|
||||
<flux:card>
|
||||
<div class="prose prose-zinc dark:prose-invert max-w-none">
|
||||
{!! nl2br(e($pr->text)) !!}
|
||||
{{-- ============== TEXT + SIDEBAR ============== --}}
|
||||
<div class="grid gap-6 lg:grid-cols-[1fr,300px]">
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Inhalt') }}</span>
|
||||
</div>
|
||||
</flux:card>
|
||||
<div class="p-5">
|
||||
<div class="prose prose-zinc dark:prose-invert max-w-none text-[color:var(--color-ink)]">
|
||||
{!! nl2br(e($pr->text)) !!}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{{-- Details --}}
|
||||
<div class="space-y-4">
|
||||
<flux:card>
|
||||
<flux:heading size="sm" class="mb-3">{{ __('Details') }}</flux:heading>
|
||||
<dl class="space-y-2 text-sm">
|
||||
<aside class="space-y-4">
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Details') }}</span>
|
||||
</div>
|
||||
<dl class="p-5 space-y-2.5 text-[12.5px]">
|
||||
<div class="flex justify-between gap-2">
|
||||
<dt class="text-zinc-500">{{ __('Status') }}</dt>
|
||||
<dd class="font-medium">{{ $pr->status->label() }}</dd>
|
||||
<dt class="text-[color:var(--color-ink-3)]">{{ __('Status') }}</dt>
|
||||
<dd class="font-semibold text-[color:var(--color-ink)]">{{ $pr->status->label() }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-2">
|
||||
<dt class="text-zinc-500">{{ __('Erstellt') }}</dt>
|
||||
<dd>{{ $pr->created_at->format('d.m.Y H:i') }}</dd>
|
||||
<dt class="text-[color:var(--color-ink-3)]">{{ __('Erstellt') }}</dt>
|
||||
<dd class="text-[color:var(--color-ink)]">{{ $pr->created_at->format('d.m.Y H:i') }}</dd>
|
||||
</div>
|
||||
@if($pr->published_at)
|
||||
@if ($pr->published_at)
|
||||
<div class="flex justify-between gap-2">
|
||||
<dt class="text-zinc-500">{{ __('Veröffentlicht') }}</dt>
|
||||
<dd>{{ $pr->published_at->format('d.m.Y H:i') }}</dd>
|
||||
<dt class="text-[color:var(--color-ink-3)]">{{ __('Veröffentlicht') }}</dt>
|
||||
<dd class="text-[color:var(--color-ink)]">{{ $pr->published_at->format('d.m.Y H:i') }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex justify-between gap-2">
|
||||
<dt class="text-zinc-500">{{ __('Aufrufe') }}</dt>
|
||||
<dd>{{ number_format($pr->hits) }}</dd>
|
||||
<dt class="text-[color:var(--color-ink-3)]">{{ __('Aufrufe') }}</dt>
|
||||
<dd class="text-[color:var(--color-ink)] font-semibold">{{ number_format($pr->hits) }}</dd>
|
||||
</div>
|
||||
@if($pr->keywords)
|
||||
<div>
|
||||
<dt class="text-zinc-500">{{ __('Stichwörter') }}</dt>
|
||||
<dd class="mt-1">{{ $pr->keywords }}</dd>
|
||||
@if ($pr->keywords)
|
||||
<div class="pt-2 border-t border-[color:var(--color-bg-rule)]">
|
||||
<dt class="text-[color:var(--color-ink-3)] mb-1">{{ __('Stichwörter') }}</dt>
|
||||
<dd class="text-[color:var(--color-ink)]">{{ $pr->keywords }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
@if($pr->backlink_url)
|
||||
<div>
|
||||
<dt class="text-zinc-500">{{ __('Backlink') }}</dt>
|
||||
<dd class="mt-1 break-all">
|
||||
<a href="{{ $pr->backlink_url }}" target="_blank" class="text-blue-600 underline dark:text-blue-400">
|
||||
@if ($pr->backlink_url)
|
||||
<div class="pt-2 border-t border-[color:var(--color-bg-rule)]">
|
||||
<dt class="text-[color:var(--color-ink-3)] mb-1">{{ __('Backlink') }}</dt>
|
||||
<dd class="break-all">
|
||||
<a href="{{ $pr->backlink_url }}" target="_blank"
|
||||
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
|
||||
{{ $pr->backlink_url }}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
@endif
|
||||
@if($pr->no_export)
|
||||
<div class="rounded bg-zinc-100 px-2 py-1 text-xs text-zinc-600 dark:bg-zinc-700 dark:text-zinc-300">
|
||||
{{ __('Kein Export') }}
|
||||
@if ($pr->no_export)
|
||||
<div class="mt-2 pt-2 border-t border-[color:var(--color-bg-rule)]">
|
||||
<span class="badge hub">{{ __('Kein Export') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</dl>
|
||||
</flux:card>
|
||||
</article>
|
||||
|
||||
@if($pr->images->isNotEmpty())
|
||||
<flux:card>
|
||||
<flux:heading size="sm" class="mb-3">{{ __('Bilder') }}</flux:heading>
|
||||
<div class="space-y-2">
|
||||
@foreach($pr->images as $image)
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<flux:icon.photo class="size-4 text-zinc-400" />
|
||||
<span class="truncate text-zinc-600 dark:text-zinc-400">{{ basename($image->path) }}</span>
|
||||
@if($image->is_preview)
|
||||
<flux:badge size="sm" color="blue">{{ __('Preview') }}</flux:badge>
|
||||
@if ($pr->images->isNotEmpty())
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Bilder') }}</span>
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ $pr->images->count() }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="p-5 space-y-2">
|
||||
@foreach ($pr->images as $image)
|
||||
<div class="flex items-center gap-2 text-[12.5px]">
|
||||
<flux:icon.photo class="size-4 flex-shrink-0 text-[color:var(--color-ink-3)]" />
|
||||
<span class="truncate text-[color:var(--color-ink-2)]">{{ basename($image->path) }}</span>
|
||||
@if ($image->is_preview)
|
||||
<span class="badge hub">{{ __('Preview') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</flux:card>
|
||||
</article>
|
||||
@endif
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@if($statusLogs->isNotEmpty())
|
||||
<flux:card>
|
||||
<flux:heading size="sm" class="mb-3">{{ __('Status-Verlauf') }}</flux:heading>
|
||||
<ol class="space-y-3 border-s border-zinc-200 ps-4 dark:border-zinc-700">
|
||||
@foreach($statusLogs as $log)
|
||||
<li class="text-sm">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@php
|
||||
$color = match($log->to_status?->value) {
|
||||
'published' => 'green',
|
||||
'review' => 'yellow',
|
||||
'rejected' => 'red',
|
||||
'archived' => 'blue',
|
||||
default => 'zinc',
|
||||
};
|
||||
@endphp
|
||||
<flux:badge size="sm" :color="$color">{{ $log->to_status?->label() ?? $log->to_status }}</flux:badge>
|
||||
@if($log->from_status)
|
||||
<span class="text-xs text-zinc-500">
|
||||
{{ __('von') }} {{ $log->from_status->label() }}
|
||||
{{-- ============== STATUS-VERLAUF ============== --}}
|
||||
@if ($statusLogs->isNotEmpty())
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Status-Verlauf') }}</span>
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ $statusLogs->count() }} {{ __('Einträge') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<ol class="space-y-3 border-s border-[color:var(--color-bg-rule)] ps-4">
|
||||
@foreach ($statusLogs as $log)
|
||||
<li class="text-[12.5px]">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@php
|
||||
$logClass = match ($log->to_status?->value) {
|
||||
'published' => 'ok',
|
||||
'review' => 'warn',
|
||||
'rejected' => 'err',
|
||||
default => 'hub',
|
||||
};
|
||||
@endphp
|
||||
<span @class(['badge', $logClass])>{{ $log->to_status?->label() ?? $log->to_status }}</span>
|
||||
@if ($log->from_status)
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ __('von') }} {{ $log->from_status->label() }}
|
||||
</span>
|
||||
@endif
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">·</span>
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ $log->created_at->format('d.m.Y H:i') }}
|
||||
</span>
|
||||
@if ($log->changedBy)
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">·</span>
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">{{ $log->changedBy->name }}</span>
|
||||
@endif
|
||||
@if ($log->source !== 'admin')
|
||||
<span class="badge hub">{{ $log->source }}</span>
|
||||
@endif
|
||||
</div>
|
||||
@if ($log->reason)
|
||||
<p class="mt-1.5 text-[color:var(--color-ink-2)] m-0">{{ $log->reason }}</p>
|
||||
@endif
|
||||
<span class="text-xs text-zinc-500">·</span>
|
||||
<span class="text-xs text-zinc-500">
|
||||
{{ $log->created_at->format('d.m.Y H:i') }}
|
||||
</span>
|
||||
@if($log->changedBy)
|
||||
<span class="text-xs text-zinc-500">·</span>
|
||||
<span class="text-xs text-zinc-500">{{ $log->changedBy->name }}</span>
|
||||
@endif
|
||||
@if($log->source !== 'admin')
|
||||
<flux:badge size="xs" color="zinc">{{ $log->source }}</flux:badge>
|
||||
@endif
|
||||
</div>
|
||||
@if($log->reason)
|
||||
<p class="mt-1 text-zinc-600 dark:text-zinc-400">{{ $log->reason }}</p>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ol>
|
||||
</flux:card>
|
||||
</li>
|
||||
@endforeach
|
||||
</ol>
|
||||
</div>
|
||||
</article>
|
||||
@endif
|
||||
|
||||
@if($pr->status === \App\Enums\PressReleaseStatus::Review)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use Illuminate\Validation\ValidationException;
|
|||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new #[Layout('components.layouts.auth')] class extends Component {
|
||||
new #[Layout('components.layouts.auth.pressekonto', ['heading' => 'Passwort bestätigen', 'eyebrow' => 'Sicherheitsbereich', 'showFromBanner' => false])] class extends Component {
|
||||
public string $password = '';
|
||||
|
||||
/**
|
||||
|
|
@ -32,26 +32,43 @@ new #[Layout('components.layouts.auth')] class extends Component {
|
|||
}
|
||||
}; ?>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-auth-header
|
||||
:title="__('Confirm password')"
|
||||
:description="__('This is a secure area of the application. Please confirm your password before continuing.')"
|
||||
/>
|
||||
<div>
|
||||
<p class="text-[13.5px] text-ink-2 leading-[1.65] !-mt-4 mb-6">
|
||||
Dieser Bereich des Publisher-Hubs ist besonders geschützt. Bitte bestätigen Sie zur
|
||||
Sicherheit erneut Ihr Passwort, bevor Sie fortfahren.
|
||||
</p>
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="text-center" :status="session('status')" />
|
||||
<form wire:submit="confirmPassword" class="space-y-[18px]" x-data="{ showPassword: false }" novalidate>
|
||||
|
||||
<form wire:submit="confirmPassword" class="flex flex-col gap-6">
|
||||
<!-- Password -->
|
||||
<flux:input
|
||||
wire:model="password"
|
||||
:label="__('Password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
:placeholder="__('Password')"
|
||||
/>
|
||||
<div>
|
||||
<label class="field-label" for="auth-password">Passwort</label>
|
||||
<div class="field-pw-wrap">
|
||||
<input
|
||||
id="auth-password"
|
||||
wire:model="password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="current-password"
|
||||
class="field-input pr-[72px]"
|
||||
placeholder="••••••••••"
|
||||
@error('password') aria-invalid="true" @enderror
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="field-affix"
|
||||
@click="showPassword = !showPassword"
|
||||
x-text="showPassword ? 'Verbergen' : 'Anzeigen'"
|
||||
>Anzeigen</button>
|
||||
</div>
|
||||
@error('password')
|
||||
<p class="field-error">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<flux:button variant="primary" type="submit" class="w-full">{{ __('Confirm') }}</flux:button>
|
||||
<button type="submit" class="auth-btn-primary !mt-[22px]" wire:loading.attr="disabled" wire:target="confirmPassword">
|
||||
<span wire:loading.remove wire:target="confirmPassword">Bestätigen</span>
|
||||
<span wire:loading wire:target="confirmPassword">Wird geprüft …</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use Illuminate\Support\Facades\Password;
|
|||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new #[Layout('components.layouts.auth')] class extends Component {
|
||||
new #[Layout('components.layouts.auth.pressekonto', ['heading' => 'Passwort vergessen', 'eyebrow' => 'Passwort zurücksetzen', 'topRightLabel' => 'Doch erinnert?', 'topRightLinkText' => 'Zur Anmeldung', 'topRightLinkHref' => '/login'])] class extends Component {
|
||||
public string $email = '';
|
||||
|
||||
/**
|
||||
|
|
@ -22,28 +22,45 @@ new #[Layout('components.layouts.auth')] class extends Component {
|
|||
}
|
||||
}; ?>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-auth-header :title="__('Forgot password')" :description="__('Enter your email to receive a password reset link')" />
|
||||
<div>
|
||||
<p class="text-[13.5px] text-ink-2 leading-[1.6] !-mt-4 mb-6">
|
||||
Geben Sie die E-Mail-Adresse Ihres Kontos ein. Sie erhalten innerhalb weniger Minuten
|
||||
einen Link, mit dem Sie ein neues Passwort vergeben können.
|
||||
</p>
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="text-center" :status="session('status')" />
|
||||
@if (session('status'))
|
||||
<div class="field-status mb-4" role="status">
|
||||
{{ session('status') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form wire:submit="sendPasswordResetLink" class="flex flex-col gap-6">
|
||||
<!-- Email Address -->
|
||||
<flux:input
|
||||
wire:model="email"
|
||||
:label="__('Email Address')"
|
||||
type="email"
|
||||
required
|
||||
autofocus
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
<form wire:submit="sendPasswordResetLink" class="space-y-[18px]" novalidate>
|
||||
|
||||
<flux:button variant="primary" type="submit" class="w-full">{{ __('Email password reset link') }}</flux:button>
|
||||
<div>
|
||||
<label class="field-label" for="auth-email">E-Mail-Adresse</label>
|
||||
<input
|
||||
id="auth-email"
|
||||
type="email"
|
||||
wire:model="email"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="username"
|
||||
class="field-input"
|
||||
placeholder="redaktion@ihr-unternehmen.de"
|
||||
@error('email') aria-invalid="true" @enderror
|
||||
/>
|
||||
@error('email')
|
||||
<p class="field-error">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<button type="submit" class="auth-btn-primary !mt-[22px]" wire:loading.attr="disabled" wire:target="sendPasswordResetLink">
|
||||
<span wire:loading.remove wire:target="sendPasswordResetLink">Reset-Link senden</span>
|
||||
<span wire:loading wire:target="sendPasswordResetLink">Link wird gesendet …</span>
|
||||
</button>
|
||||
|
||||
<div class="flex items-center justify-center pt-2 text-[12.5px] text-ink-3">
|
||||
<a href="{{ route('login') }}" class="link-hub" wire:navigate>← Zurück zur Anmeldung</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="space-x-1 text-center text-sm text-zinc-400">
|
||||
{{ __('Or, return to') }}
|
||||
<flux:link :href="route('login')" wire:navigate>{{ __('log in') }}</flux:link>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Auth\Events\Lockout;
|
||||
use App\Mail\MagicLoginLink;
|
||||
use App\Services\Auth\MagicLinkGenerator;
|
||||
use App\Models\User;
|
||||
use App\Services\Auth\MagicLinkGenerator;
|
||||
use Illuminate\Auth\Events\Lockout;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
|
|
@ -15,7 +15,7 @@ use Livewire\Attributes\Layout;
|
|||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new #[Layout('components.layouts.auth')] class extends Component {
|
||||
new #[Layout('components.layouts.auth.pressekonto', ['heading' => 'Willkommen zurück', 'eyebrow' => 'Anmeldung im Publisher-Hub', 'topRightLabel' => 'Noch kein Konto?', 'topRightLinkText' => 'Konto erstellen', 'topRightLinkHref' => '/register'])] class extends Component {
|
||||
#[Validate('required|string|email')]
|
||||
public string $email = '';
|
||||
|
||||
|
|
@ -52,7 +52,18 @@ new #[Layout('components.layouts.auth')] class extends Component {
|
|||
RateLimiter::clear($this->throttleKey());
|
||||
Session::regenerate();
|
||||
|
||||
$this->redirectIntended(default: route('dashboard', absolute: false), navigate: true);
|
||||
// Rollen-basierter Default-Redirect:
|
||||
// Admin/Editor → /dashboard, Customer → /admin/me.
|
||||
// Ohne navigate:true, weil das Portal ein anderes Vite-Bundle nutzt
|
||||
// (build/portal mit FluxUI) als das Hub-Auth-Layout (build/web).
|
||||
// SPA-Navigation kann den Bundle-Wechsel nicht handhaben.
|
||||
$defaultRoute = $authenticatedUser?->canAccessAdmin()
|
||||
? route('dashboard', absolute: false)
|
||||
: ($authenticatedUser?->canAccessCustomer()
|
||||
? route('me.dashboard', absolute: false)
|
||||
: '/');
|
||||
|
||||
$this->redirectIntended(default: $defaultRoute);
|
||||
}
|
||||
|
||||
public function sendMagicLink(): void
|
||||
|
|
@ -107,63 +118,94 @@ new #[Layout('components.layouts.auth')] class extends Component {
|
|||
}
|
||||
}; ?>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-auth-header :title="__('Log in to your account')" :description="__('Enter your email and password below to log in')" />
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="text-center" :status="session('status')" />
|
||||
|
||||
<form wire:submit="login" class="flex flex-col gap-6">
|
||||
<!-- Email Address -->
|
||||
<flux:input
|
||||
wire:model="email"
|
||||
:label="__('Email address')"
|
||||
type="email"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="email"
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="relative">
|
||||
<flux:input
|
||||
wire:model="password"
|
||||
:label="__('Password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
:placeholder="__('Password')"
|
||||
/>
|
||||
|
||||
@if (Route::has('password.request'))
|
||||
<flux:link class="absolute right-0 top-0 text-sm" :href="route('password.request')" wire:navigate>
|
||||
{{ __('Forgot your password?') }}
|
||||
</flux:link>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Remember Me -->
|
||||
<flux:checkbox wire:model="remember" :label="__('Remember me')" />
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<flux:button variant="primary" type="submit" class="w-full">{{ __('Log in') }}</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="rounded-lg border border-zinc-200 p-4 dark:border-zinc-700">
|
||||
<p class="mb-3 text-sm text-zinc-600 dark:text-zinc-300">
|
||||
{{ __('Login without password? Request a one-time email link.') }}
|
||||
</p>
|
||||
<flux:button variant="subtle" wire:click="sendMagicLink" class="w-full">
|
||||
{{ __('Send magic login link') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
@if (Route::has('register'))
|
||||
<div class="space-x-1 text-center text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Don\'t have an account?') }}
|
||||
<flux:link :href="route('register')" wire:navigate>{{ __('Sign up') }}</flux:link>
|
||||
<div>
|
||||
@if (session('status'))
|
||||
<div class="field-status mb-4" role="status">
|
||||
{{ session('status') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form wire:submit="login" class="space-y-[18px]" x-data="{ showPassword: false }" novalidate>
|
||||
|
||||
<div>
|
||||
<label class="field-label" for="auth-email">E-Mail-Adresse</label>
|
||||
<input
|
||||
id="auth-email"
|
||||
type="email"
|
||||
wire:model="email"
|
||||
autocomplete="username"
|
||||
required
|
||||
autofocus
|
||||
class="field-input"
|
||||
placeholder="redaktion@ihr-unternehmen.de"
|
||||
@error('email') aria-invalid="true" @enderror
|
||||
/>
|
||||
@error('email')
|
||||
<p class="field-error">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-baseline justify-between mb-1.5">
|
||||
<label class="field-label !mb-0" for="auth-password">Passwort</label>
|
||||
@if (\Illuminate\Support\Facades\Route::has('password.request'))
|
||||
<a href="{{ route('password.request') }}" class="link-hub text-[12px]" wire:navigate>
|
||||
Passwort vergessen?
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
<div class="field-pw-wrap">
|
||||
<input
|
||||
id="auth-password"
|
||||
wire:model="password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
class="field-input pr-[72px]"
|
||||
placeholder="••••••••••"
|
||||
@error('password') aria-invalid="true" @enderror
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="field-affix"
|
||||
@click="showPassword = !showPassword"
|
||||
x-text="showPassword ? 'Verbergen' : 'Anzeigen'"
|
||||
>Anzeigen</button>
|
||||
</div>
|
||||
@error('password')
|
||||
<p class="field-error">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<label class="flex items-center gap-2.5 text-[12.5px] text-ink-2 cursor-pointer select-none">
|
||||
<input type="checkbox" wire:model="remember" class="auth-check" />
|
||||
Angemeldet bleiben
|
||||
</label>
|
||||
|
||||
<button type="submit" class="auth-btn-primary !mt-[22px]" wire:loading.attr="disabled" wire:target="login">
|
||||
<span wire:loading.remove wire:target="login">Anmelden</span>
|
||||
<span wire:loading wire:target="login">Anmelden …</span>
|
||||
</button>
|
||||
|
||||
<div class="flex items-center gap-3 !mt-[22px] !mb-[14px]">
|
||||
<span class="flex-1 h-px bg-bg-rule"></span>
|
||||
<span class="text-[11px] font-semibold tracking-[0.18em] uppercase text-ink-3">oder</span>
|
||||
<span class="flex-1 h-px bg-bg-rule"></span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
wire:click="sendMagicLink"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="sendMagicLink"
|
||||
class="auth-btn-outline !mt-0"
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<rect x="2" y="3" width="12" height="10" stroke="currentColor" stroke-width="1.4" />
|
||||
<path d="M2.5 4l5.5 5 5.5-5" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<span wire:loading.remove wire:target="sendMagicLink">Magic-Link senden</span>
|
||||
<span wire:loading wire:target="sendMagicLink">Magic-Link wird gesendet …</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,12 +8,17 @@ use Illuminate\Validation\Rules;
|
|||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new #[Layout('components.layouts.auth')] class extends Component {
|
||||
new #[Layout('components.layouts.auth.pressekonto', ['heading' => 'Konto erstellen', 'eyebrow' => 'Registrierung im Publisher-Hub', 'topRightLabel' => 'Bereits Konto?', 'topRightLinkText' => 'Anmelden', 'topRightLinkHref' => '/login'])] class extends Component {
|
||||
public string $name = '';
|
||||
|
||||
public string $email = '';
|
||||
|
||||
public string $password = '';
|
||||
|
||||
public string $password_confirmation = '';
|
||||
|
||||
public bool $terms_accepted = false;
|
||||
|
||||
/**
|
||||
* Handle an incoming registration request.
|
||||
*/
|
||||
|
|
@ -21,77 +26,150 @@ new #[Layout('components.layouts.auth')] class extends Component {
|
|||
{
|
||||
$validated = $this->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:' . User::class],
|
||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
||||
'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()],
|
||||
'terms_accepted' => ['accepted'],
|
||||
], [
|
||||
'terms_accepted.accepted' => 'Bitte bestätigen Sie unsere AGB und die Datenschutzerklärung.',
|
||||
]);
|
||||
|
||||
unset($validated['terms_accepted']);
|
||||
|
||||
$validated['password'] = Hash::make($validated['password']);
|
||||
|
||||
event(new Registered(($user = User::create($validated))));
|
||||
event(new Registered($user = User::create($validated)));
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
$this->redirectIntended(route('dashboard', absolute: false), navigate: true);
|
||||
// Frisch registrierte User sind in der Regel Customer ohne Admin-
|
||||
// Rollen → /admin/me. Ohne navigate:true, weil das Panel ein
|
||||
// anderes Vite-Bundle nutzt als das Hub-Auth-Layout.
|
||||
$defaultRoute = $user->canAccessAdmin()
|
||||
? route('dashboard', absolute: false)
|
||||
: ($user->canAccessCustomer()
|
||||
? route('me.dashboard', absolute: false)
|
||||
: '/');
|
||||
|
||||
$this->redirectIntended($defaultRoute);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-auth-header :title="__('Create an account')" :description="__('Enter your details below to create your account')" />
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="text-center" :status="session('status')" />
|
||||
|
||||
<form wire:submit="register" class="flex flex-col gap-6">
|
||||
<!-- Name -->
|
||||
<flux:input
|
||||
wire:model="name"
|
||||
:label="__('Name')"
|
||||
type="text"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="name"
|
||||
:placeholder="__('Full name')"
|
||||
/>
|
||||
|
||||
<!-- Email Address -->
|
||||
<flux:input
|
||||
wire:model="email"
|
||||
:label="__('Email address')"
|
||||
type="email"
|
||||
required
|
||||
autocomplete="email"
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
|
||||
<!-- Password -->
|
||||
<flux:input
|
||||
wire:model="password"
|
||||
:label="__('Password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
:placeholder="__('Password')"
|
||||
/>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<flux:input
|
||||
wire:model="password_confirmation"
|
||||
:label="__('Confirm password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
:placeholder="__('Confirm password')"
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<flux:button type="submit" variant="primary" class="w-full">
|
||||
{{ __('Create account') }}
|
||||
</flux:button>
|
||||
<div>
|
||||
@if (session('status'))
|
||||
<div class="field-status mb-4" role="status">
|
||||
{{ session('status') }}
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
<div class="space-x-1 text-center text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Already have an account?') }}
|
||||
<flux:link :href="route('login')" wire:navigate>{{ __('Log in') }}</flux:link>
|
||||
</div>
|
||||
<form wire:submit="register" class="space-y-[18px]" x-data="{ showPassword: false, showPasswordConfirmation: false }" novalidate>
|
||||
|
||||
<div>
|
||||
<label class="field-label" for="auth-name">Name</label>
|
||||
<input
|
||||
id="auth-name"
|
||||
type="text"
|
||||
wire:model="name"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="name"
|
||||
class="field-input"
|
||||
placeholder="Vor- und Nachname"
|
||||
@error('name') aria-invalid="true" @enderror
|
||||
/>
|
||||
@error('name')
|
||||
<p class="field-error">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="field-label" for="auth-email">E-Mail-Adresse</label>
|
||||
<input
|
||||
id="auth-email"
|
||||
type="email"
|
||||
wire:model="email"
|
||||
required
|
||||
autocomplete="email"
|
||||
class="field-input"
|
||||
placeholder="redaktion@ihr-unternehmen.de"
|
||||
@error('email') aria-invalid="true" @enderror
|
||||
/>
|
||||
@error('email')
|
||||
<p class="field-error">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="field-label" for="auth-password">Passwort</label>
|
||||
<div class="field-pw-wrap">
|
||||
<input
|
||||
id="auth-password"
|
||||
wire:model="password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
class="field-input pr-[72px]"
|
||||
placeholder="Mindestens 8 Zeichen"
|
||||
@error('password') aria-invalid="true" @enderror
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="field-affix"
|
||||
@click="showPassword = !showPassword"
|
||||
x-text="showPassword ? 'Verbergen' : 'Anzeigen'"
|
||||
>Anzeigen</button>
|
||||
</div>
|
||||
@error('password')
|
||||
<p class="field-error">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="field-label" for="auth-password-confirmation">Passwort bestätigen</label>
|
||||
<div class="field-pw-wrap">
|
||||
<input
|
||||
id="auth-password-confirmation"
|
||||
wire:model="password_confirmation"
|
||||
:type="showPasswordConfirmation ? 'text' : 'password'"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
class="field-input pr-[72px]"
|
||||
placeholder="Passwort wiederholen"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="field-affix"
|
||||
@click="showPasswordConfirmation = !showPasswordConfirmation"
|
||||
x-text="showPasswordConfirmation ? 'Verbergen' : 'Anzeigen'"
|
||||
>Anzeigen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="!mt-5">
|
||||
<label for="auth-terms" class="flex items-start gap-3 cursor-pointer select-none">
|
||||
<input
|
||||
id="auth-terms"
|
||||
type="checkbox"
|
||||
wire:model="terms_accepted"
|
||||
required
|
||||
class="auth-check !mt-[3px]"
|
||||
@error('terms_accepted') aria-invalid="true" @enderror
|
||||
/>
|
||||
<span class="text-[12.5px] text-ink-2 leading-[1.55]">
|
||||
Ich habe die
|
||||
<a href="{{ route('agb') }}" target="_blank" rel="noopener" class="link-hub">AGB</a>
|
||||
und die
|
||||
<a href="{{ route('datenschutz') }}" target="_blank" rel="noopener" class="link-hub">Datenschutzerklärung</a>
|
||||
gelesen und stimme der Verarbeitung meiner Daten zur Konto-Erstellung ausdrücklich zu.
|
||||
</span>
|
||||
</label>
|
||||
@error('terms_accepted')
|
||||
<p class="field-error !ml-7">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<button type="submit" class="auth-btn-primary !mt-[18px]" wire:loading.attr="disabled" wire:target="register">
|
||||
<span wire:loading.remove wire:target="register">Konto erstellen</span>
|
||||
<span wire:loading wire:target="register">Konto wird angelegt …</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,11 +10,14 @@ use Livewire\Attributes\Layout;
|
|||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new #[Layout('components.layouts.auth')] class extends Component {
|
||||
new #[Layout('components.layouts.auth.pressekonto', ['heading' => 'Neues Passwort vergeben', 'eyebrow' => 'Passwort zurücksetzen'])] class extends Component {
|
||||
#[Locked]
|
||||
public string $token = '';
|
||||
|
||||
public string $email = '';
|
||||
|
||||
public string $password = '';
|
||||
|
||||
public string $password_confirmation = '';
|
||||
|
||||
/**
|
||||
|
|
@ -38,9 +41,6 @@ new #[Layout('components.layouts.auth')] class extends Component {
|
|||
'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
// Here we will attempt to reset the user's password. If it is successful we
|
||||
// will update the password on an actual user model and persist it to the
|
||||
// database. Otherwise we will parse the error and return the response.
|
||||
$status = Password::reset(
|
||||
$this->only('email', 'password', 'password_confirmation', 'token'),
|
||||
function ($user) {
|
||||
|
|
@ -53,9 +53,6 @@ new #[Layout('components.layouts.auth')] class extends Component {
|
|||
}
|
||||
);
|
||||
|
||||
// If the password was successfully reset, we will redirect the user back to
|
||||
// the application's home authenticated view. If there is an error we can
|
||||
// redirect them back to where they came from with their error message.
|
||||
if ($status != Password::PasswordReset) {
|
||||
$this->addError('email', __($status));
|
||||
|
||||
|
|
@ -68,46 +65,79 @@ new #[Layout('components.layouts.auth')] class extends Component {
|
|||
}
|
||||
}; ?>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-auth-header :title="__('Reset password')" :description="__('Please enter your new password below')" />
|
||||
<div>
|
||||
<p class="text-[13.5px] text-ink-2 leading-[1.6] !-mt-4 mb-6">
|
||||
Vergeben Sie ein neues Passwort für Ihr Konto. Mindestens 8 Zeichen, idealerweise eine
|
||||
Kombination aus Buchstaben, Zahlen und Sonderzeichen.
|
||||
</p>
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="text-center" :status="session('status')" />
|
||||
<form wire:submit="resetPassword" class="space-y-[18px]" x-data="{ showPassword: false, showPasswordConfirmation: false }" novalidate>
|
||||
|
||||
<form wire:submit="resetPassword" class="flex flex-col gap-6">
|
||||
<!-- Email Address -->
|
||||
<flux:input
|
||||
wire:model="email"
|
||||
:label="__('Email')"
|
||||
type="email"
|
||||
required
|
||||
autocomplete="email"
|
||||
/>
|
||||
|
||||
<!-- Password -->
|
||||
<flux:input
|
||||
wire:model="password"
|
||||
:label="__('Password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
:placeholder="__('Password')"
|
||||
/>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<flux:input
|
||||
wire:model="password_confirmation"
|
||||
:label="__('Confirm password')"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
:placeholder="__('Confirm password')"
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<flux:button type="submit" variant="primary" class="w-full">
|
||||
{{ __('Reset password') }}
|
||||
</flux:button>
|
||||
<div>
|
||||
<label class="field-label" for="auth-email">E-Mail-Adresse</label>
|
||||
<input
|
||||
id="auth-email"
|
||||
type="email"
|
||||
wire:model="email"
|
||||
required
|
||||
autocomplete="email"
|
||||
class="field-input"
|
||||
@error('email') aria-invalid="true" @enderror
|
||||
/>
|
||||
@error('email')
|
||||
<p class="field-error">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="field-label" for="auth-password">Neues Passwort</label>
|
||||
<div class="field-pw-wrap">
|
||||
<input
|
||||
id="auth-password"
|
||||
wire:model="password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
class="field-input pr-[72px]"
|
||||
placeholder="Mindestens 8 Zeichen"
|
||||
@error('password') aria-invalid="true" @enderror
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="field-affix"
|
||||
@click="showPassword = !showPassword"
|
||||
x-text="showPassword ? 'Verbergen' : 'Anzeigen'"
|
||||
>Anzeigen</button>
|
||||
</div>
|
||||
@error('password')
|
||||
<p class="field-error">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="field-label" for="auth-password-confirmation">Passwort bestätigen</label>
|
||||
<div class="field-pw-wrap">
|
||||
<input
|
||||
id="auth-password-confirmation"
|
||||
wire:model="password_confirmation"
|
||||
:type="showPasswordConfirmation ? 'text' : 'password'"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
class="field-input pr-[72px]"
|
||||
placeholder="Neues Passwort wiederholen"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="field-affix"
|
||||
@click="showPasswordConfirmation = !showPasswordConfirmation"
|
||||
x-text="showPasswordConfirmation ? 'Verbergen' : 'Anzeigen'"
|
||||
>Anzeigen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="auth-btn-primary !mt-[22px]" wire:loading.attr="disabled" wire:target="resetPassword">
|
||||
<span wire:loading.remove wire:target="resetPassword">Passwort zurücksetzen</span>
|
||||
<span wire:loading wire:target="resetPassword">Passwort wird gespeichert …</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use Illuminate\Support\Facades\Session;
|
|||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new #[Layout('components.layouts.auth')] class extends Component {
|
||||
new #[Layout('components.layouts.auth.pressekonto', ['heading' => 'E-Mail-Adresse bestätigen', 'eyebrow' => 'Konto-Verifizierung', 'showFromBanner' => false])] class extends Component {
|
||||
/**
|
||||
* Send an email verification notification to the user.
|
||||
*/
|
||||
|
|
@ -34,24 +34,37 @@ new #[Layout('components.layouts.auth')] class extends Component {
|
|||
}
|
||||
}; ?>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-6">
|
||||
<flux:text class="text-center">
|
||||
{{ __('Please verify your email address by clicking on the link we just emailed to you.') }}
|
||||
</flux:text>
|
||||
<div>
|
||||
<p class="text-[13.5px] text-ink-2 leading-[1.65] !-mt-4 mb-6">
|
||||
Wir haben Ihnen einen Bestätigungslink an
|
||||
<strong class="text-ink font-semibold">{{ Auth::user()?->email }}</strong>
|
||||
gesendet. Bitte öffnen Sie die Mail und klicken Sie auf den Link, um Ihre E-Mail-Adresse zu bestätigen.
|
||||
</p>
|
||||
|
||||
@if (session('status') == 'verification-link-sent')
|
||||
<flux:text class="text-center font-medium !dark:text-green-400 !text-green-600">
|
||||
{{ __('A new verification link has been sent to the email address you provided during registration.') }}
|
||||
</flux:text>
|
||||
@if (session('status') === 'verification-link-sent')
|
||||
<div class="field-status mb-6" role="status">
|
||||
Ein neuer Bestätigungslink wurde an Ihre E-Mail-Adresse versendet.
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-col items-center justify-between space-y-3">
|
||||
<flux:button wire:click="sendVerification" variant="primary" class="w-full">
|
||||
{{ __('Resend verification email') }}
|
||||
</flux:button>
|
||||
<div class="space-y-3">
|
||||
<button
|
||||
type="button"
|
||||
wire:click="sendVerification"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="sendVerification"
|
||||
class="auth-btn-primary"
|
||||
>
|
||||
<span wire:loading.remove wire:target="sendVerification">Bestätigungs-Mail erneut senden</span>
|
||||
<span wire:loading wire:target="sendVerification">Mail wird gesendet …</span>
|
||||
</button>
|
||||
|
||||
<flux:link class="text-sm cursor-pointer" wire:click="logout">
|
||||
{{ __('Log out') }}
|
||||
</flux:link>
|
||||
<button
|
||||
type="button"
|
||||
wire:click="logout"
|
||||
class="w-full text-center text-[12.5px] text-ink-3 hover:text-hub transition-colors py-2"
|
||||
>
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\PressReleaseStatus;
|
||||
use App\Models\BillingAddress;
|
||||
use App\Models\Company;
|
||||
use App\Models\Contact;
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\Profile;
|
||||
use App\Models\User;
|
||||
use App\Services\Customer\CustomerCompanyContext;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Volt\Component;
|
||||
|
|
@ -35,6 +38,9 @@ new #[Layout('components.layouts.app'), Title('Mein Dashboard')] class extends C
|
|||
->limit(5)
|
||||
->get(['id', 'title', 'status', 'company_id', 'created_at']);
|
||||
|
||||
$profile = $user->profile;
|
||||
$billingAddress = $user->billingAddress;
|
||||
|
||||
return [
|
||||
'user' => $user,
|
||||
'selectedCompany' => $selectedCompany,
|
||||
|
|
@ -43,36 +49,123 @@ new #[Layout('components.layouts.app'), Title('Mein Dashboard')] class extends C
|
|||
'published' => $myPRs->get('published', 0),
|
||||
'review' => $myPRs->get('review', 0),
|
||||
'draft' => $myPRs->get('draft', 0),
|
||||
'deltaMonth' => $this->totalDeltaToPreviousMonth(clone $pressReleaseQuery),
|
||||
],
|
||||
'qualityHints' => $this->qualityHints($user, $selectedCompany, $pressReleaseQuery),
|
||||
'profileCompleteness' => $this->profileCompleteness($profile),
|
||||
'billingCompleteness' => $this->billingCompleteness($billingAddress),
|
||||
'qualityHints' => $this->qualityHints(
|
||||
$user,
|
||||
$profile,
|
||||
$billingAddress,
|
||||
$selectedCompany,
|
||||
$pressReleaseQuery,
|
||||
),
|
||||
'recent' => $recent,
|
||||
'companies' => $context->companiesFor($user),
|
||||
'bridgeStatus' => [
|
||||
/* Heute hardcoded — perspektivisch aus echtem Sync-Service. */
|
||||
'presseecho' => ['state' => 'connected', 'subline' => __('Archiv · Branchen-Tiefe')],
|
||||
'businessportal24' => ['state' => 'connected', 'subline' => __('Wirtschaft · Live')],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function qualityHints(User $user, ?Company $selectedCompany, Builder $pressReleaseQuery): array
|
||||
/**
|
||||
* Heuristische Profil-Vollständigkeit in %.
|
||||
* 6 Kernfelder, jedes ~17 %.
|
||||
*/
|
||||
private function profileCompleteness(?Profile $profile): int
|
||||
{
|
||||
if (! $profile) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$fields = [
|
||||
'salutation_key',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'phone',
|
||||
'address',
|
||||
'country_code',
|
||||
];
|
||||
|
||||
$filled = collect($fields)->filter(fn (string $field) => filled($profile->{$field}))->count();
|
||||
|
||||
return (int) round(($filled / count($fields)) * 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rechnungsadressen-Vollständigkeit in %.
|
||||
* Ohne hinterlegte Adresse: 0; sonst je nach gefüllten Pflichtfeldern.
|
||||
*/
|
||||
private function billingCompleteness(?BillingAddress $address): int
|
||||
{
|
||||
if (! $address) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$fields = ['name', 'address1', 'postal_code', 'city', 'country_code'];
|
||||
$filled = collect($fields)->filter(fn (string $field) => filled($address->{$field}))->count();
|
||||
|
||||
return (int) round(($filled / count($fields)) * 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vergleicht PRs im aktuellen Monat mit dem Vormonat (Differenz, Vorzeichen mit Pfeil im View).
|
||||
*/
|
||||
private function totalDeltaToPreviousMonth(Builder $pressReleaseQuery): int
|
||||
{
|
||||
$now = Carbon::now();
|
||||
$currentStart = $now->copy()->startOfMonth();
|
||||
$previousStart = $now->copy()->subMonthNoOverflow()->startOfMonth();
|
||||
$previousEnd = $now->copy()->subMonthNoOverflow()->endOfMonth();
|
||||
|
||||
$currentCount = (clone $pressReleaseQuery)
|
||||
->where('created_at', '>=', $currentStart)
|
||||
->count();
|
||||
|
||||
$previousCount = (clone $pressReleaseQuery)
|
||||
->whereBetween('created_at', [$previousStart, $previousEnd])
|
||||
->count();
|
||||
|
||||
return $currentCount - $previousCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{icon: string, title: string, description: string, href: string, action: string, percent?: int}>
|
||||
*/
|
||||
private function qualityHints(
|
||||
User $user,
|
||||
?Profile $profile,
|
||||
?BillingAddress $billingAddress,
|
||||
?Company $selectedCompany,
|
||||
Builder $pressReleaseQuery,
|
||||
): array {
|
||||
$hints = [];
|
||||
|
||||
if (! $user->profile()->exists()) {
|
||||
$profilePercent = $this->profileCompleteness($profile);
|
||||
if ($profilePercent < 100) {
|
||||
$hints[] = [
|
||||
'color' => 'amber',
|
||||
'icon' => 'user',
|
||||
'title' => __('Profil unvollständig'),
|
||||
'description' => __('Ergänzen Sie Ihre Profildaten für eine sauberere Kundenakte.'),
|
||||
'description' => __('Ergänzen Sie Salutation, Telefon und Adresse für eine saubere Kundenakte.'),
|
||||
'href' => route('me.profile').'#profil',
|
||||
'action' => __('Profil öffnen'),
|
||||
'percent' => $profilePercent,
|
||||
];
|
||||
}
|
||||
|
||||
if (! $user->billingAddress()->exists()) {
|
||||
$billingPercent = $this->billingCompleteness($billingAddress);
|
||||
if ($billingPercent < 100) {
|
||||
$hints[] = [
|
||||
'color' => 'amber',
|
||||
'icon' => 'archive-box',
|
||||
'title' => __('Rechnungsadresse fehlt'),
|
||||
'title' => $billingAddress
|
||||
? __('Rechnungsadresse unvollständig')
|
||||
: __('Rechnungsadresse fehlt'),
|
||||
'description' => __('Hinterlegen Sie eine Rechnungsadresse, damit spätere Buchungen sauber abgerechnet werden können.'),
|
||||
'href' => route('me.profile').'#rechnungsadresse',
|
||||
'action' => __('Rechnungsadresse ergänzen'),
|
||||
'percent' => $billingPercent,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -83,7 +176,6 @@ new #[Layout('components.layouts.app'), Title('Mein Dashboard')] class extends C
|
|||
|
||||
if ($contactsCount === 0) {
|
||||
$hints[] = [
|
||||
'color' => 'blue',
|
||||
'icon' => 'user-group',
|
||||
'title' => __('Keine Pressekontakte hinterlegt'),
|
||||
'description' => __('Ergänzen Sie Pressekontakte für diese Firma.'),
|
||||
|
|
@ -98,9 +190,12 @@ new #[Layout('components.layouts.app'), Title('Mein Dashboard')] class extends C
|
|||
|
||||
if ($unassignedPressReleasesCount > 0) {
|
||||
$hints[] = [
|
||||
'color' => 'amber',
|
||||
'icon' => 'newspaper',
|
||||
'title' => trans_choice(':count Pressemitteilung ohne Firma|:count Pressemitteilungen ohne Firma', $unassignedPressReleasesCount, ['count' => $unassignedPressReleasesCount]),
|
||||
'title' => trans_choice(
|
||||
':count Pressemitteilung ohne Firma|:count Pressemitteilungen ohne Firma',
|
||||
$unassignedPressReleasesCount,
|
||||
['count' => $unassignedPressReleasesCount],
|
||||
),
|
||||
'description' => __('Ordnen Sie Legacy-Pressemitteilungen einer Firma zu, damit Portal und Pressekontakte eindeutig sind.'),
|
||||
'href' => route('me.press-releases.index', ['company' => 'unassigned']),
|
||||
'action' => __('Pressemitteilungen prüfen'),
|
||||
|
|
@ -112,126 +207,371 @@ new #[Layout('components.layouts.app'), Title('Mein Dashboard')] class extends C
|
|||
}
|
||||
}; ?>
|
||||
|
||||
<div class="space-y-6">
|
||||
<flux:card>
|
||||
<flux:heading size="lg">{{ __('Willkommen, :name', ['name' => $user->name]) }}</flux:heading>
|
||||
<flux:subheading>
|
||||
{{ $selectedCompany
|
||||
? __('Übersicht für :company', ['company' => $selectedCompany->name])
|
||||
: __('Übersicht Ihres Kundenkontos') }}
|
||||
</flux:subheading>
|
||||
</flux:card>
|
||||
<div class="space-y-8">
|
||||
|
||||
{{-- Statistiken --}}
|
||||
<div class="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||
<flux:card>
|
||||
<flux:text class="text-xs text-zinc-500">{{ __('Gesamt') }}</flux:text>
|
||||
<flux:text size="xl" weight="bold">{{ $stats['total'] }}</flux:text>
|
||||
</flux:card>
|
||||
<flux:card>
|
||||
<flux:text class="text-xs text-green-600">{{ __('Veröffentlicht') }}</flux:text>
|
||||
<flux:text size="xl" weight="bold">{{ $stats['published'] }}</flux:text>
|
||||
</flux:card>
|
||||
<flux:card>
|
||||
<flux:text class="text-xs text-yellow-600">{{ __('In Prüfung') }}</flux:text>
|
||||
<flux:text size="xl" weight="bold">{{ $stats['review'] }}</flux:text>
|
||||
</flux:card>
|
||||
<flux:card>
|
||||
<flux:text class="text-xs text-zinc-500">{{ __('Entwürfe') }}</flux:text>
|
||||
<flux:text size="xl" weight="bold">{{ $stats['draft'] }}</flux:text>
|
||||
</flux:card>
|
||||
</div>
|
||||
|
||||
@if($qualityHints)
|
||||
<flux:card>
|
||||
<div class="mb-4">
|
||||
<flux:heading size="sm">{{ __('Datenqualität') }}</flux:heading>
|
||||
<flux:text class="text-sm text-zinc-500">{{ __('Diese Hinweise helfen, Ihr User Backend vollständig und sauber zu halten.') }}</flux:text>
|
||||
{{-- ============== PAGE HEADER ============== --}}
|
||||
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
|
||||
<span class="badge hub dot">{{ __('User Backend') }}</span>
|
||||
<span class="eyebrow muted">{{ __('Mein Bereich · A · 01') }}</span>
|
||||
</div>
|
||||
<h1 class="text-[34px] font-bold tracking-[-0.7px] leading-[1.1] m-0 text-[color:var(--color-ink)]">
|
||||
{{ __('Mein Dashboard') }}
|
||||
</h1>
|
||||
<p class="text-[13.5px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
|
||||
{{ __('Willkommen zurück, ') }}<strong class="font-semibold text-[color:var(--color-ink)]">{{ $user->name }}</strong>.
|
||||
@if ($selectedCompany)
|
||||
{{ __('Übersicht für :company', ['company' => $selectedCompany->name]) }} —
|
||||
@endif
|
||||
{{ __('Hier sehen Sie Status und Reichweite Ihres Kundenkontos für presseecho und businessportal24.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 lg:grid-cols-3">
|
||||
@foreach($qualityHints as $hint)
|
||||
<a href="{{ $hint['href'] }}" wire:navigate class="rounded-lg border border-zinc-200 p-4 transition hover:bg-zinc-50 dark:border-zinc-700 dark:hover:bg-zinc-900">
|
||||
<div class="flex items-start gap-3">
|
||||
<flux:badge color="{{ $hint['color'] }}" size="sm" icon="{{ $hint['icon'] }}" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<flux:text weight="semibold">{{ $hint['title'] }}</flux:text>
|
||||
<flux:text class="mt-1 text-sm text-zinc-500">{{ $hint['description'] }}</flux:text>
|
||||
<flux:text class="mt-3 text-xs font-medium text-zinc-700 dark:text-zinc-300">
|
||||
{{ $hint['action'] ?? __('Öffnen') }} →
|
||||
</flux:text>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 flex-shrink-0">
|
||||
@if ($selectedCompany)
|
||||
<span class="badge hub dot">
|
||||
{{ __('Aktive Firma:') }} <strong class="font-semibold">{{ $selectedCompany->name }}</strong>
|
||||
</span>
|
||||
@else
|
||||
<a href="{{ route('me.profile') }}#firmen" wire:navigate
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-[4px] text-[12px] font-semibold whitespace-nowrap bg-[color:var(--color-warn-soft)] border border-[color:var(--color-warn)]/30 text-[color:var(--color-accent-deep)] hover:bg-[color:var(--color-warn-soft)]/80 transition">
|
||||
<flux:icon.exclamation-triangle class="size-[13px] flex-shrink-0" />
|
||||
{{ __('Keine Firma zugeordnet') }}
|
||||
<span class="underline underline-offset-[3px] decoration-[color:var(--color-accent-deep)]/40">{{ __('zuordnen') }} →</span>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{{-- ============== STAT-CARDS — KPI-Reihe ============== --}}
|
||||
<section class="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||
<x-portal.stat-card variant="primary" :label="__('Gesamt')" :value="$stats['total']">
|
||||
<x-slot:meta>{{ now()->format('Y') }}</x-slot:meta>
|
||||
<x-slot:trend>
|
||||
@php
|
||||
$delta = $stats['deltaMonth'];
|
||||
$deltaLabel = $delta === 0
|
||||
? __('keine Veränderung ggü. Vormonat')
|
||||
: trans_choice(':sign:abs ggü. Vormonat', abs($delta), [
|
||||
'sign' => $delta > 0 ? '+' : '−',
|
||||
'abs' => abs($delta),
|
||||
]);
|
||||
@endphp
|
||||
<span class="flex items-center gap-1">
|
||||
@if ($delta > 0)
|
||||
<flux:icon.arrow-trending-up class="size-[11px] text-[color:var(--color-ok)]" />
|
||||
@elseif ($delta < 0)
|
||||
<flux:icon.arrow-trending-down class="size-[11px] text-[color:var(--color-loss)]" />
|
||||
@else
|
||||
<flux:icon.minus class="size-[11px] text-[color:var(--color-ink-4)]" />
|
||||
@endif
|
||||
{{ $deltaLabel }}
|
||||
</span>
|
||||
</x-slot:trend>
|
||||
</x-portal.stat-card>
|
||||
|
||||
<x-portal.stat-card variant="ok" :label="__('Veröffentlicht')" :value="$stats['published']">
|
||||
<x-slot:meta>
|
||||
<span class="badge ok" style="font-size:9.5px;padding:1px 6px;">{{ __('live') }}</span>
|
||||
</x-slot:meta>
|
||||
<x-slot:trend>{{ __('auf beiden Portalen') }}</x-slot:trend>
|
||||
</x-portal.stat-card>
|
||||
|
||||
<x-portal.stat-card variant="warn" :label="__('In Prüfung')" :value="$stats['review']">
|
||||
<x-slot:meta>{{ __('Ø 4 h') }}</x-slot:meta>
|
||||
<x-slot:trend>{{ __('redaktionelle Prüfung') }}</x-slot:trend>
|
||||
</x-portal.stat-card>
|
||||
|
||||
<x-portal.stat-card variant="muted" :label="__('Entwürfe')" :value="$stats['draft']">
|
||||
<x-slot:meta>{{ __('privat') }}</x-slot:meta>
|
||||
<x-slot:trend>{{ __('gespeichert, nicht eingereicht') }}</x-slot:trend>
|
||||
</x-portal.stat-card>
|
||||
</section>
|
||||
|
||||
{{-- ============== ZWEISPALTEN-GRID ============== --}}
|
||||
<section class="grid gap-6 lg:grid-cols-[2fr_1fr]">
|
||||
|
||||
{{-- LINKS: Pressemitteilungen-Liste / Empty-State --}}
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Meine letzten Pressemitteilungen') }}</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ $recent->count() }} {{ __('von') }} {{ $stats['total'] }}
|
||||
</span>
|
||||
<a href="{{ route('me.press-releases.index') }}" wire:navigate
|
||||
class="text-[12px] font-semibold text-[color:var(--color-hub)] hover:underline underline-offset-[3px] decoration-[color:var(--color-hub)]/30">
|
||||
{{ __('Alle anzeigen') }} →
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
@endif
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-[1fr,280px]">
|
||||
{{-- Letzte Pressemitteilungen --}}
|
||||
<flux:card class="p-0">
|
||||
<div class="flex items-center justify-between border-b border-zinc-200 px-4 py-3 dark:border-zinc-700">
|
||||
<flux:heading size="sm">{{ __('Meine letzten Pressemitteilungen') }}</flux:heading>
|
||||
<flux:button size="sm" variant="ghost" href="{{ route('me.press-releases.index') }}" wire:navigate>
|
||||
{{ __('Alle anzeigen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
<div class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
@forelse($recent as $pr)
|
||||
<a href="{{ route('me.press-releases.show', $pr->id) }}" wire:navigate
|
||||
class="flex items-center justify-between gap-3 px-4 py-3 transition hover:bg-zinc-50 dark:hover:bg-zinc-800">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-medium">{{ $pr->title }}</p>
|
||||
<p class="text-xs text-zinc-500">{{ $pr->company?->name ?? '–' }} · {{ $pr->created_at->format('d.m.Y') }}</p>
|
||||
</div>
|
||||
<flux:badge color="{{ match($pr->status->value) {
|
||||
'published' => 'green',
|
||||
'review' => 'yellow',
|
||||
'rejected' => 'red',
|
||||
'archived' => 'blue',
|
||||
default => 'zinc',
|
||||
} }}" size="sm">
|
||||
{{ $pr->status->label() }}
|
||||
</flux:badge>
|
||||
</a>
|
||||
@empty
|
||||
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
|
||||
<flux:icon.newspaper class="size-10 text-zinc-300" />
|
||||
<flux:text weight="semibold" class="mt-3">{{ __('Noch keine Pressemitteilungen') }}</flux:text>
|
||||
<flux:text class="mt-1 max-w-md text-sm text-zinc-500">
|
||||
{{ __('Starten Sie mit einer ersten Pressemitteilung für die aktive Firma oder für Ihr Kundenkonto.') }}
|
||||
</flux:text>
|
||||
<flux:button class="mt-4" variant="primary" href="{{ route('me.press-releases.create') }}" wire:navigate>
|
||||
@forelse ($recent as $pr)
|
||||
<a href="{{ route('me.press-releases.show', $pr->id) }}" wire:navigate
|
||||
class="flex items-center justify-between gap-3 px-5 py-3 border-b border-[color:var(--color-bg-rule)] last:border-b-0 hover:bg-[color:var(--color-bg)] transition-colors">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-[13px] font-medium text-[color:var(--color-ink)] m-0">{{ $pr->title }}</p>
|
||||
<p class="text-[11px] text-[color:var(--color-ink-3)] mt-0.5 m-0">
|
||||
{{ $pr->company?->name ?? __('Ohne Firma') }} · {{ $pr->created_at->format('d.m.Y') }}
|
||||
</p>
|
||||
</div>
|
||||
<span @class([
|
||||
'badge',
|
||||
'ok' => $pr->status === PressReleaseStatus::Published,
|
||||
'warn' => $pr->status === PressReleaseStatus::Review,
|
||||
'err' => $pr->status === PressReleaseStatus::Rejected,
|
||||
'hub' => ! in_array($pr->status, [PressReleaseStatus::Published, PressReleaseStatus::Review, PressReleaseStatus::Rejected], true),
|
||||
])>
|
||||
{{ $pr->status->label() }}
|
||||
</span>
|
||||
</a>
|
||||
@empty
|
||||
<div class="px-10 py-14 flex flex-col items-center text-center">
|
||||
<div class="w-16 h-16 rounded-[6px] flex items-center justify-center mb-5 relative
|
||||
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
|
||||
<flux:icon.newspaper class="size-7" />
|
||||
<span class="absolute -top-1.5 -right-1.5 w-5 h-5 rounded-full text-[10px] font-bold flex items-center justify-center font-mono
|
||||
bg-[color:var(--color-accent)] text-white">0</span>
|
||||
</div>
|
||||
<div class="text-[16px] font-semibold m-0 text-[color:var(--color-ink)]">
|
||||
{{ __('Noch keine Pressemitteilungen') }}
|
||||
</div>
|
||||
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[460px] text-[color:var(--color-ink-3)]">
|
||||
{{ __('Starten Sie mit einer ersten Mitteilung für die aktive Firma oder Ihr Kundenkonto. Veröffentlichung erfolgt nach redaktioneller Prüfung auf beiden Portalen.') }}
|
||||
</p>
|
||||
<div class="mt-6 flex items-center gap-2.5">
|
||||
<flux:button variant="primary" icon="plus" href="{{ route('me.press-releases.create') }}" wire:navigate>
|
||||
{{ __('Erste Pressemitteilung erstellen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Zugeordnete Firmen --}}
|
||||
<flux:card>
|
||||
<flux:heading size="sm" class="mb-3">{{ __('Meine Firmen') }}</flux:heading>
|
||||
@forelse($companies as $company)
|
||||
<div class="py-2 text-sm">
|
||||
<p class="font-medium">{{ $company->name }}</p>
|
||||
</div>
|
||||
@empty
|
||||
<div class="rounded-lg border border-dashed border-zinc-200 p-4 text-sm text-zinc-500 dark:border-zinc-700">
|
||||
{{ __('Keine Firmen zugeordnet. Wenn hier eine Firma fehlen sollte, prüfen Sie bitte Ihr Profil oder wenden Sie sich an den Support.') }}
|
||||
<div class="mt-9 grid gap-3 w-full max-w-[560px] grid-cols-1 sm:grid-cols-3">
|
||||
@foreach ([
|
||||
['num' => '01', 'label' => __('Firma zuordnen')],
|
||||
['num' => '02', 'label' => __('Mitteilung verfassen')],
|
||||
['num' => '03', 'label' => __('Zur Prüfung senden')],
|
||||
] as $step)
|
||||
<div class="text-left px-3 py-2.5 rounded-[3px]
|
||||
bg-[color:var(--color-bg-elev)] border border-[color:var(--color-bg-rule)]">
|
||||
<div class="font-mono text-[9.5px] tracking-[0.16em] font-bold mb-1 text-[color:var(--color-accent-deep)]">
|
||||
{{ $step['num'] }}
|
||||
</div>
|
||||
<div class="text-[11.5px] font-semibold leading-tight text-[color:var(--color-ink)]">
|
||||
{{ $step['label'] }}
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endforelse
|
||||
|
||||
<div class="mt-4 border-t border-zinc-100 pt-4 dark:border-zinc-800">
|
||||
<flux:button size="sm" variant="ghost" href="{{ route('me.profile') }}" wire:navigate>
|
||||
{{ __('Profil & Firma verwalten') }}
|
||||
</flux:button>
|
||||
<div class="px-5 py-3 border-t border-[color:var(--color-bg-rule)] flex items-center gap-2.5 text-[11.5px]
|
||||
bg-[color:var(--color-bg-elev)] text-[color:var(--color-ink-3)]">
|
||||
<flux:icon.shield-check class="size-[12px] text-[color:var(--color-hub)] flex-shrink-0" />
|
||||
{{ __('Tipp: Geprüfte Mitteilungen erscheinen i. d. R. binnen') }}
|
||||
<strong class="font-semibold text-[color:var(--color-ink-2)]">{{ __('4 Stunden') }}</strong>
|
||||
{{ __('werktags auf beiden Portalen.') }}
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<flux:button variant="primary" icon="plus" href="{{ route('me.press-releases.create') }}" wire:navigate>
|
||||
{{ __('Neue Pressemitteilung') }}
|
||||
</flux:button>
|
||||
{{-- RECHTS: Datenqualität --}}
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Datenqualität') }}</span>
|
||||
@if (count($qualityHints) > 0)
|
||||
<span class="badge warn dot">{{ count($qualityHints) }} {{ __('offen') }}</span>
|
||||
@else
|
||||
<span class="badge ok dot">{{ __('vollständig') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="px-5 py-5">
|
||||
<p class="text-[12px] leading-[1.55] m-0 mb-4 text-[color:var(--color-ink-3)]">
|
||||
{{ __('Diese Hinweise helfen, Ihr User Backend vollständig und sauber zu halten.') }}
|
||||
</p>
|
||||
|
||||
@if (count($qualityHints) > 0)
|
||||
<div class="space-y-3">
|
||||
@foreach ($qualityHints as $hint)
|
||||
<x-portal.hint-card
|
||||
:icon="$hint['icon']"
|
||||
:title="$hint['title']"
|
||||
:percent="$hint['percent'] ?? null"
|
||||
:href="$hint['href']"
|
||||
:action="$hint['action']"
|
||||
>
|
||||
{{ $hint['description'] }}
|
||||
</x-portal.hint-card>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="flex flex-col items-center text-center py-6">
|
||||
<flux:icon.check-badge class="size-8 text-[color:var(--color-ok)] mb-2" />
|
||||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)]">
|
||||
{{ __('Alles im grünen Bereich') }}
|
||||
</div>
|
||||
<p class="text-[11.5px] text-[color:var(--color-ink-3)] mt-1 m-0">
|
||||
{{ __('Profil, Rechnungsadresse und Firmen-Daten sind vollständig.') }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
{{-- ============== UNTERER GRID: FIRMEN + BRAND-BRIDGE ============== --}}
|
||||
<section class="grid gap-6 lg:grid-cols-[2fr_1fr]">
|
||||
|
||||
{{-- Firmen --}}
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Meine Firmen') }}</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="badge hub" style="font-size:9.5px;padding:1px 6px;">
|
||||
{{ $companies->count() }} {{ __('zugeordnet') }}
|
||||
</span>
|
||||
<a href="{{ route('me.profile') }}" wire:navigate
|
||||
class="text-[12px] font-semibold text-[color:var(--color-hub)] hover:underline underline-offset-[3px] decoration-[color:var(--color-hub)]/30">
|
||||
{{ __('Profil & Firma verwalten') }} →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
@if ($companies->isNotEmpty())
|
||||
<div class="grid gap-3 grid-cols-1 md:grid-cols-2">
|
||||
@foreach ($companies as $company)
|
||||
<a href="{{ route('me.press-kits.show', $company->id) }}" wire:navigate
|
||||
class="block relative rounded-[5px] p-5 transition-colors
|
||||
bg-[color:var(--color-bg-elev)] border border-[color:var(--color-bg-rule)]
|
||||
hover:border-[color:var(--color-hub)]/50">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<span class="w-10 h-10 rounded-[4px] flex items-center justify-center
|
||||
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)]
|
||||
text-[color:var(--color-hub)] text-[12px] font-bold">
|
||||
{{ \Illuminate\Support\Str::of($company->name)->take(2)->upper() }}
|
||||
</span>
|
||||
<div class="min-w-0">
|
||||
<div class="text-[13.5px] font-semibold truncate text-[color:var(--color-ink)]">
|
||||
{{ $company->name }}
|
||||
</div>
|
||||
<div class="text-[11px] mt-0.5 text-[color:var(--color-ink-3)]">
|
||||
{{ __('Presse-Kit öffnen') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="grid gap-3 grid-cols-1 md:grid-cols-2">
|
||||
<div class="relative rounded-[5px] p-5 transition-colors
|
||||
border border-dashed border-[color:var(--color-bg-rule)] hover:bg-[color:var(--color-bg-elev)]">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<span class="w-10 h-10 rounded-[4px] flex items-center justify-center
|
||||
border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)]
|
||||
text-[color:var(--color-ink-4)]">
|
||||
<flux:icon.building-office class="size-[18px]" />
|
||||
</span>
|
||||
<div>
|
||||
<div class="text-[13.5px] font-semibold text-[color:var(--color-ink-2)]">
|
||||
{{ __('Firma hinzufügen') }}
|
||||
</div>
|
||||
<div class="text-[11px] mt-0.5 text-[color:var(--color-ink-3)]">
|
||||
{{ __('Slot frei') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-[11.5px] leading-[1.5] m-0 text-[color:var(--color-ink-3)]">
|
||||
{{ __('Pressestellen, für die Sie Mitteilungen erstellen — mit eigenem Logo, Kontaktperson und Themen-Tags.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-5 rounded-[5px]
|
||||
bg-[color:var(--color-bg-elev)] border border-[color:var(--color-bg-rule)]">
|
||||
<div class="eyebrow muted mb-2">{{ __('Hinweis') }}</div>
|
||||
<div class="text-[13px] leading-[1.55] m-0 text-[color:var(--color-ink-2)]">
|
||||
{{ __('Keine Firmen zugeordnet. Wenn hier eine Firma fehlen sollte, prüfen Sie bitte Ihr Profil oder wenden Sie sich an den Support.') }}
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<flux:button size="sm" variant="ghost" href="{{ route('me.profile') }}" wire:navigate>
|
||||
{{ __('Profil prüfen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{{-- Brand-Bridge (dark) --}}
|
||||
<article class="panel-dark">
|
||||
<div class="panel-head">
|
||||
<span class="eyebrow on-dark">{{ __('Brand-Bridge') }}</span>
|
||||
<span class="font-mono text-[10px] tracking-[0.14em] uppercase text-[color:var(--color-ink-on-dark-3)]">A · B</span>
|
||||
</div>
|
||||
|
||||
<div class="px-5 py-5">
|
||||
<div class="text-[12.5px] leading-[1.55] m-0 mb-4 text-[color:var(--color-ink-on-dark-2)]">
|
||||
{{ __('Ein Konto, beide Portale — Veröffentlichungen werden parallel auf presseecho und businessportal24 ausgespielt.') }}
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 grid-cols-2">
|
||||
{{-- Konstantes dunkles Hub-Blau (Phase 5): bleibt in beiden Modi gleich. --}}
|
||||
<div class="rounded-[4px] px-3.5 py-3 border bg-[color:var(--color-panel-dark-2)] border-white/5">
|
||||
<div class="flex items-center gap-2 mb-1.5">
|
||||
<span class="dot-pe"></span>
|
||||
<span class="text-[11px] font-bold tracking-[0.14em] uppercase text-white/85">presseecho</span>
|
||||
</div>
|
||||
<div class="font-mono text-[15px] font-semibold text-white tabular-nums">
|
||||
{{ __($bridgeStatus['presseecho']['state']) }}
|
||||
</div>
|
||||
<div class="text-[10.5px] mt-0.5 text-[color:var(--color-ink-on-dark-3)]">
|
||||
{{ $bridgeStatus['presseecho']['subline'] }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-[4px] px-3.5 py-3 border bg-[color:var(--color-panel-dark-2)] border-white/5">
|
||||
<div class="flex items-center gap-2 mb-1.5">
|
||||
<span class="dot-bp"></span>
|
||||
<span class="text-[11px] font-bold tracking-[0.14em] uppercase text-white/85">businessportal24</span>
|
||||
</div>
|
||||
<div class="font-mono text-[15px] font-semibold text-white tabular-nums">
|
||||
{{ __($bridgeStatus['businessportal24']['state']) }}
|
||||
</div>
|
||||
<div class="text-[10.5px] mt-0.5 text-[color:var(--color-ink-on-dark-3)]">
|
||||
{{ $bridgeStatus['businessportal24']['subline'] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="mt-5 mb-4 border-0 h-px bg-white/10" />
|
||||
|
||||
<div class="space-y-2 text-[11.5px] text-[color:var(--color-ink-on-dark-2)]">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>{{ __('API-Status') }}</span>
|
||||
<span class="flex items-center gap-1.5 text-white">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
|
||||
{{ __('operational') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>{{ __('Tarif') }}</span>
|
||||
<span class="font-mono text-white">{{ __('Starter') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
{{-- ============== FOOTER ============== --}}
|
||||
<footer class="flex items-center justify-between pt-4 pb-2 text-[11px]
|
||||
border-t border-[color:var(--color-bg-rule)] text-[color:var(--color-ink-3)]">
|
||||
<span>© {{ now()->format('Y') }} pressekonto.de · Publisher-Hub</span>
|
||||
<span class="flex items-center gap-5">
|
||||
<a href="{{ route('me.security') }}" wire:navigate class="hover:text-[color:var(--color-hub)]">{{ __('Sicherheit') }}</a>
|
||||
<a href="{{ route('me.tokens.index') }}" wire:navigate class="hover:text-[color:var(--color-hub)]">{{ __('API & Tokens') }}</a>
|
||||
<a href="{{ route('me.profile') }}" wire:navigate class="hover:text-[color:var(--color-hub)]">{{ __('Profil') }}</a>
|
||||
</span>
|
||||
</footer>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -129,32 +129,44 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
|
|||
}
|
||||
}; ?>
|
||||
|
||||
<div class="space-y-6">
|
||||
<flux:card>
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Neue Pressemitteilung') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Entwurf erstellen oder direkt zur Prüfung einreichen.') }}</flux:subheading>
|
||||
<div class="space-y-8">
|
||||
{{-- ============== PAGE HEADER ============== --}}
|
||||
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
|
||||
<span class="badge hub dot">{{ __('User Backend') }}</span>
|
||||
<span class="eyebrow muted">{{ __('Mein Bereich · Neue PM') }}</span>
|
||||
</div>
|
||||
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
|
||||
{{ __('Neue Pressemitteilung') }}
|
||||
</h1>
|
||||
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
|
||||
{{ __('Entwurf erstellen oder direkt zur Prüfung einreichen.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<flux:button variant="ghost" icon="arrow-left" href="{{ route('me.press-releases.index') }}" wire:navigate>
|
||||
{{ __('Zurück') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</flux:card>
|
||||
</header>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-[1fr,300px]">
|
||||
<div class="space-y-6">
|
||||
<flux:card>
|
||||
<flux:heading size="md" class="mb-4">{{ __('Inhalt') }}</flux:heading>
|
||||
<div class="space-y-4">
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Inhalt') }}</span>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Titel') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:label>{{ __('Titel') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
|
||||
<flux:input wire:model="title" placeholder="{{ __('Aussagekräftiger Titel…') }}" />
|
||||
<flux:error name="title" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Text') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:label>{{ __('Text') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
|
||||
<flux:textarea wire:model="text" rows="16" placeholder="{{ __('Vollständiger Text…') }}" />
|
||||
<flux:error name="text" />
|
||||
</flux:field>
|
||||
|
|
@ -170,18 +182,20 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
|
|||
<flux:error name="backlinkUrl" />
|
||||
</flux:field>
|
||||
</div>
|
||||
</flux:card>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<flux:card>
|
||||
<flux:heading size="sm" class="mb-3">{{ __('Metadaten') }}</flux:heading>
|
||||
<div class="space-y-4">
|
||||
<aside class="space-y-4">
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Metadaten') }}</span>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Firma') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:label>{{ __('Firma') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
|
||||
<flux:select wire:model="companyId">
|
||||
<option value="">{{ __('Bitte wählen…') }}</option>
|
||||
@foreach($myCompanies as $c)
|
||||
@foreach ($myCompanies as $c)
|
||||
<option value="{{ $c->id }}">{{ $c->name }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
|
|
@ -189,11 +203,11 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
|
|||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Kategorie') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:label>{{ __('Kategorie') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
|
||||
<flux:select wire:model="categoryId">
|
||||
<option value="">{{ __('Bitte wählen…') }}</option>
|
||||
@foreach($categories as $cat)
|
||||
@php $catName = $cat->translations->firstWhere('locale', 'de')?->name ?? $cat->id; @endphp
|
||||
@foreach ($categories as $cat)
|
||||
@php($catName = $cat->translations->firstWhere('locale', 'de')?->name ?? $cat->id)
|
||||
<option value="{{ $cat->id }}">{{ $catName }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
|
|
@ -210,10 +224,13 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
|
|||
</flux:select>
|
||||
</flux:field>
|
||||
</div>
|
||||
</flux:card>
|
||||
</article>
|
||||
|
||||
<flux:card>
|
||||
<div class="space-y-2">
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Aktionen') }}</span>
|
||||
</div>
|
||||
<div class="p-5 space-y-2">
|
||||
<flux:button type="button" variant="primary" class="w-full" wire:click="save('review')">
|
||||
{{ __('Zur Prüfung einreichen') }}
|
||||
</flux:button>
|
||||
|
|
@ -221,7 +238,7 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
|
|||
{{ __('Als Entwurf speichern') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
</article>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -139,32 +139,45 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
|
|||
}
|
||||
}; ?>
|
||||
|
||||
<div class="space-y-6">
|
||||
<flux:card>
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Pressemitteilung bearbeiten') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Inhalt und Metadaten der Pressemitteilung aktualisieren.') }}</flux:subheading>
|
||||
<div class="space-y-8">
|
||||
{{-- ============== PAGE HEADER ============== --}}
|
||||
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
|
||||
<span class="badge hub dot">{{ __('User Backend') }}</span>
|
||||
<span class="eyebrow muted">{{ __('Mein Bereich · Bearbeiten') }}</span>
|
||||
<span class="badge hub">ID {{ $id }}</span>
|
||||
</div>
|
||||
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
|
||||
{{ __('Pressemitteilung bearbeiten') }}
|
||||
</h1>
|
||||
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
|
||||
{{ __('Inhalt und Metadaten der Pressemitteilung aktualisieren.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<flux:button variant="ghost" icon="arrow-left" href="{{ route('me.press-releases.show', $id) }}" wire:navigate>
|
||||
{{ __('Zurück') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</flux:card>
|
||||
</header>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-[1fr,300px]">
|
||||
<div class="space-y-6">
|
||||
<flux:card>
|
||||
<flux:heading size="md" class="mb-4">{{ __('Inhalt') }}</flux:heading>
|
||||
<div class="space-y-4">
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Inhalt') }}</span>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Titel') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:label>{{ __('Titel') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
|
||||
<flux:input wire:model="title" />
|
||||
<flux:error name="title" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Text') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:label>{{ __('Text') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
|
||||
<flux:textarea wire:model="text" rows="20" />
|
||||
<flux:error name="text" />
|
||||
</flux:field>
|
||||
|
|
@ -180,19 +193,21 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
|
|||
<flux:error name="backlinkUrl" />
|
||||
</flux:field>
|
||||
</div>
|
||||
</flux:card>
|
||||
</article>
|
||||
|
||||
<livewire:components.press-release-images-manager :press-release-id="$id" :wire:key="'pr-images-'.$id" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<flux:card>
|
||||
<flux:heading size="sm" class="mb-3">{{ __('Metadaten') }}</flux:heading>
|
||||
<div class="space-y-4">
|
||||
<aside class="space-y-4">
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Metadaten') }}</span>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Firma') }}</flux:label>
|
||||
<flux:select wire:model="companyId">
|
||||
@foreach($myCompanies as $c)
|
||||
@foreach ($myCompanies as $c)
|
||||
<option value="{{ $c->id }}">{{ $c->name }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
|
|
@ -203,8 +218,8 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
|
|||
<flux:label>{{ __('Kategorie') }}</flux:label>
|
||||
<flux:select wire:model="categoryId">
|
||||
<option value="">{{ __('Bitte wählen…') }}</option>
|
||||
@foreach($categories as $cat)
|
||||
@php $catName = $cat->translations->firstWhere('locale', 'de')?->name ?? $cat->id; @endphp
|
||||
@foreach ($categories as $cat)
|
||||
@php($catName = $cat->translations->firstWhere('locale', 'de')?->name ?? $cat->id)
|
||||
<option value="{{ $cat->id }}">{{ $catName }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
|
|
@ -221,11 +236,18 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
|
|||
</flux:select>
|
||||
</flux:field>
|
||||
</div>
|
||||
</flux:card>
|
||||
</article>
|
||||
|
||||
<flux:button type="button" variant="primary" class="w-full" wire:click="save">
|
||||
{{ __('Speichern') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Aktionen') }}</span>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<flux:button type="button" variant="primary" class="w-full" wire:click="save">
|
||||
{{ __('Speichern') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</article>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -95,29 +95,52 @@ new #[Layout('components.layouts.app'), Title('Meine Pressemitteilungen')] class
|
|||
}
|
||||
}; ?>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-8">
|
||||
@if(session('success'))
|
||||
<div class="rounded-md border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800 dark:border-green-800 dark:bg-green-900/20 dark:text-green-300">
|
||||
<div class="px-4 py-3 rounded-[5px] border text-[12.5px]
|
||||
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<div class="px-4 py-3 rounded-[5px] border text-[12.5px]
|
||||
bg-[color:var(--color-err-soft)] border-[color:var(--color-err)]/30 text-[color:var(--color-loss)]">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<flux:card>
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Meine Pressemitteilungen') }}</flux:heading>
|
||||
@if($selectedCompany)
|
||||
<flux:subheading>{{ __('Gefiltert auf :company', ['company' => $selectedCompany->name]) }}</flux:subheading>
|
||||
@endif
|
||||
{{-- ============== PAGE HEADER ============== --}}
|
||||
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
|
||||
<span class="badge hub dot">{{ __('User Backend') }}</span>
|
||||
<span class="eyebrow muted">{{ __('Mein Bereich · Pressemitteilungen') }}</span>
|
||||
</div>
|
||||
<h1 class="text-[34px] font-bold tracking-[-0.7px] leading-[1.1] m-0 text-[color:var(--color-ink)]">
|
||||
{{ __('Meine Pressemitteilungen') }}
|
||||
</h1>
|
||||
<p class="text-[13.5px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
|
||||
@if ($selectedCompany)
|
||||
{{ __('Gefiltert auf :company', ['company' => $selectedCompany->name]) }}
|
||||
@else
|
||||
{{ __('Übersicht aller PMs Ihres Kundenkontos, mit Filter und Schnellaktionen.') }}
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 flex-shrink-0">
|
||||
<flux:button variant="primary" icon="plus" href="{{ route('me.press-releases.create') }}" wire:navigate>
|
||||
{{ __('Neue Pressemitteilung') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</flux:card>
|
||||
</header>
|
||||
|
||||
<flux:card>
|
||||
<div class="flex flex-col gap-3 sm:flex-row">
|
||||
{{-- ============== FILTER-PANEL ============== --}}
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Filter & Suche') }}</span>
|
||||
</div>
|
||||
<div class="p-5 flex flex-col gap-3 sm:flex-row">
|
||||
<flux:input wire:model.live.debounce.300ms="search" placeholder="{{ __('Titel suchen…') }}" icon="magnifying-glass" class="flex-1" />
|
||||
<flux:select wire:model.live="statusFilter" class="sm:w-44">
|
||||
<option value="all">{{ __('Alle Status') }}</option>
|
||||
|
|
@ -133,9 +156,16 @@ new #[Layout('components.layouts.app'), Title('Meine Pressemitteilungen')] class
|
|||
</flux:select>
|
||||
@endif
|
||||
</div>
|
||||
</flux:card>
|
||||
</article>
|
||||
|
||||
<flux:card class="p-0">
|
||||
{{-- ============== TABELLE-PANEL ============== --}}
|
||||
<article class="panel overflow-hidden">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Alle Pressemitteilungen') }}</span>
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ __(':count Einträge', ['count' => $pressReleases->count()]) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<flux:table>
|
||||
<flux:table.columns>
|
||||
|
|
@ -155,13 +185,13 @@ new #[Layout('components.layouts.app'), Title('Meine Pressemitteilungen')] class
|
|||
<flux:text class="text-sm">{{ $pr->company?->name ?? '–' }}</flux:text>
|
||||
</flux:table.cell>
|
||||
<flux:table.cell>
|
||||
<flux:badge color="{{ match($pr->status->value) {
|
||||
'published' => 'green',
|
||||
'review' => 'yellow',
|
||||
'rejected' => 'red',
|
||||
'archived' => 'blue',
|
||||
default => 'zinc',
|
||||
} }}">{{ $pr->status->label() }}</flux:badge>
|
||||
<span @class([
|
||||
'badge',
|
||||
'ok' => $pr->status->value === 'published',
|
||||
'warn' => $pr->status->value === 'review',
|
||||
'err' => $pr->status->value === 'rejected',
|
||||
'hub' => in_array($pr->status->value, ['archived', 'draft'], true),
|
||||
])>{{ $pr->status->label() }}</span>
|
||||
</flux:table.cell>
|
||||
<flux:table.cell>
|
||||
<flux:text class="text-sm text-zinc-500">{{ $pr->created_at->format('d.m.Y') }}</flux:text>
|
||||
|
|
@ -180,13 +210,18 @@ new #[Layout('components.layouts.app'), Title('Meine Pressemitteilungen')] class
|
|||
@empty
|
||||
<flux:table.row>
|
||||
<flux:table.cell colspan="5">
|
||||
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
|
||||
<flux:icon.newspaper class="size-10 text-zinc-300" />
|
||||
<flux:text weight="semibold" class="mt-3">{{ __('Keine Pressemitteilungen gefunden') }}</flux:text>
|
||||
<flux:text class="mt-1 max-w-md text-sm text-zinc-500">
|
||||
<div class="flex flex-col items-center justify-center px-4 py-12 text-center">
|
||||
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-4
|
||||
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
|
||||
<flux:icon.newspaper class="size-6" />
|
||||
</div>
|
||||
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
|
||||
{{ __('Keine Pressemitteilungen gefunden') }}
|
||||
</div>
|
||||
<p class="text-[12px] text-[color:var(--color-ink-3)] max-w-md m-0 mb-4">
|
||||
{{ __('Passen Sie die Filter an oder erstellen Sie eine neue Pressemitteilung.') }}
|
||||
</flux:text>
|
||||
<flux:button class="mt-4" size="sm" variant="primary" icon="plus" href="{{ route('me.press-releases.create') }}" wire:navigate>
|
||||
</p>
|
||||
<flux:button size="sm" variant="primary" icon="plus" href="{{ route('me.press-releases.create') }}" wire:navigate>
|
||||
{{ __('Neue Pressemitteilung') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
|
@ -195,7 +230,7 @@ new #[Layout('components.layouts.app'), Title('Meine Pressemitteilungen')] class
|
|||
@endforelse
|
||||
</flux:table>
|
||||
</div>
|
||||
</flux:card>
|
||||
</article>
|
||||
|
||||
{{ $pressReleases->links() }}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -104,76 +104,122 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
}
|
||||
}; ?>
|
||||
|
||||
<div class="space-y-6">
|
||||
@if(session('success'))
|
||||
<div class="rounded-md border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800 dark:border-green-800 dark:bg-green-900/20 dark:text-green-300">
|
||||
<div class="space-y-8">
|
||||
@php
|
||||
$statusClass = match ($pr->status->value) {
|
||||
'published' => 'ok',
|
||||
'review' => 'warn',
|
||||
'rejected' => 'err',
|
||||
default => 'hub',
|
||||
};
|
||||
@endphp
|
||||
|
||||
@if (session('success'))
|
||||
<div class="px-4 py-3 rounded-[5px] border text-[12.5px]
|
||||
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<flux:card>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:badge :color="$statusColor">{{ $pr->status->label() }}</flux:badge>
|
||||
<flux:badge color="zinc" size="sm">{{ strtoupper($pr->language) }}</flux:badge>
|
||||
</div>
|
||||
<flux:heading size="xl" class="mt-2">{{ $pr->title }}</flux:heading>
|
||||
<flux:text class="mt-1 text-sm text-zinc-500">
|
||||
{{ $pr->company?->name ?? '–' }} · {{ $categoryName }} · {{ $pr->created_at->format('d.m.Y') }}
|
||||
</flux:text>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@if($canEdit)
|
||||
<flux:button variant="ghost" icon="pencil" href="{{ route('me.press-releases.edit', $pr->id) }}" wire:navigate>
|
||||
{{ __('Bearbeiten') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
<flux:button variant="ghost" icon="link" wire:click="generateShareLink">
|
||||
{{ __('Vorschau-Link') }}
|
||||
</flux:button>
|
||||
<flux:button variant="ghost" icon="arrow-left" href="{{ route('me.press-releases.index') }}" wire:navigate>
|
||||
{{ __('Zurück') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
@if (session('error'))
|
||||
<div class="px-4 py-3 rounded-[5px] border text-[12.5px]
|
||||
bg-[color:var(--color-err-soft)] border-[color:var(--color-err)]/30 text-[color:var(--color-loss)]">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
|
||||
@if($shareUrl)
|
||||
<div class="mt-4 rounded-md border border-emerald-300 bg-emerald-50 p-4 dark:border-emerald-700 dark:bg-emerald-900/20">
|
||||
<flux:heading size="sm" class="mb-2">{{ __('Öffentlicher Vorschau-Link erstellt') }}</flux:heading>
|
||||
<flux:text class="mb-2 text-xs text-zinc-500">{{ __('Gültig bis :date.', ['date' => $shareExpiresAt]) }}</flux:text>
|
||||
<flux:input readonly :value="$shareUrl" />
|
||||
</div>
|
||||
@endif
|
||||
</flux:card>
|
||||
|
||||
@if($pr->status === PressReleaseStatus::Rejected && $latestRejection)
|
||||
<flux:callout color="red" icon="exclamation-triangle">
|
||||
<flux:callout.heading>{{ __('Diese Pressemitteilung wurde abgelehnt') }}</flux:callout.heading>
|
||||
<flux:callout.text>
|
||||
@if($latestRejection->reason)
|
||||
<strong>{{ __('Begründung') }}:</strong>
|
||||
<span class="block mt-1 whitespace-pre-line">{{ $latestRejection->reason }}</span>
|
||||
@else
|
||||
{{ __('Bitte überarbeiten Sie den Inhalt und reichen Sie die Pressemitteilung erneut ein.') }}
|
||||
@endif
|
||||
<span class="mt-2 block text-xs text-red-700/70 dark:text-red-300/70">
|
||||
{{ __('Abgelehnt am') }} {{ $latestRejection->created_at->format('d.m.Y H:i') }}
|
||||
</span>
|
||||
</flux:callout.text>
|
||||
</flux:callout>
|
||||
@endif
|
||||
|
||||
@if($pr->status === PressReleaseStatus::Draft || $pr->status === PressReleaseStatus::Rejected)
|
||||
<flux:card>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<flux:text class="text-sm text-zinc-500">
|
||||
{{-- ============== PAGE HEADER ============== --}}
|
||||
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-3 mb-3 flex-wrap">
|
||||
<span class="badge hub dot">{{ __('User Backend') }}</span>
|
||||
<span class="eyebrow muted">{{ __('Mein Bereich · Pressemitteilung') }}</span>
|
||||
<span @class(['badge', $statusClass])>{{ $pr->status->label() }}</span>
|
||||
<span class="badge hub">{{ strtoupper($pr->language) }}</span>
|
||||
</div>
|
||||
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
|
||||
{{ $pr->title }}
|
||||
</h1>
|
||||
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[720px] text-[color:var(--color-ink-2)]">
|
||||
{{ $pr->company?->name ?? '–' }}
|
||||
<span class="text-[color:var(--color-bg-rule)] mx-1">·</span>
|
||||
{{ $categoryName }}
|
||||
<span class="text-[color:var(--color-bg-rule)] mx-1">·</span>
|
||||
{{ $pr->created_at->format('d.m.Y') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
@if ($canEdit)
|
||||
<flux:button variant="ghost" icon="pencil" href="{{ route('me.press-releases.edit', $pr->id) }}" wire:navigate>
|
||||
{{ __('Bearbeiten') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
<flux:button variant="ghost" icon="link" wire:click="generateShareLink">
|
||||
{{ __('Vorschau-Link') }}
|
||||
</flux:button>
|
||||
<flux:button variant="ghost" icon="arrow-left" href="{{ route('me.press-releases.index') }}" wire:navigate>
|
||||
{{ __('Zurück') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{{-- ============== SHARE-LINK ERFOLG ============== --}}
|
||||
@if ($shareUrl)
|
||||
<article class="panel" style="border-color:var(--color-ok);">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Öffentlicher Vorschau-Link erstellt') }}</span>
|
||||
<span class="badge ok dot">{{ __('gültig bis :date', ['date' => $shareExpiresAt]) }}</span>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<flux:input readonly :value="$shareUrl" />
|
||||
</div>
|
||||
</article>
|
||||
@endif
|
||||
|
||||
{{-- ============== REJECTION-HINWEIS ============== --}}
|
||||
@if ($pr->status === PressReleaseStatus::Rejected && $latestRejection)
|
||||
<article class="panel" style="border-color:var(--color-err); border-left-width:3px;">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Diese Pressemitteilung wurde abgelehnt') }}</span>
|
||||
<span class="badge err dot">{{ __('Handlung erforderlich') }}</span>
|
||||
</div>
|
||||
<div class="p-5 flex items-start gap-3">
|
||||
<div class="w-9 h-9 rounded-[5px] flex items-center justify-center flex-shrink-0
|
||||
bg-[color:var(--color-err-soft)] border border-[color:var(--color-err)]/30 text-[color:var(--color-loss)]">
|
||||
<flux:icon.exclamation-triangle class="size-[18px]" />
|
||||
</div>
|
||||
<div class="flex-1 text-[13px] text-[color:var(--color-ink-2)]">
|
||||
@if ($latestRejection->reason)
|
||||
<strong class="text-[color:var(--color-ink)] font-semibold">{{ __('Begründung') }}:</strong>
|
||||
<span class="block mt-1 whitespace-pre-line">{{ $latestRejection->reason }}</span>
|
||||
@else
|
||||
{{ __('Bitte überarbeiten Sie den Inhalt und reichen Sie die Pressemitteilung erneut ein.') }}
|
||||
@endif
|
||||
<span class="mt-2 block text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ __('Abgelehnt am') }} {{ $latestRejection->created_at->format('d.m.Y H:i') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@endif
|
||||
|
||||
{{-- ============== STATUS-WORKFLOW ============== --}}
|
||||
@if ($pr->status === PressReleaseStatus::Draft || $pr->status === PressReleaseStatus::Rejected)
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Status-Workflow') }}</span>
|
||||
<span @class(['badge', 'dot', $pr->status === PressReleaseStatus::Rejected ? 'err' : 'hub'])>
|
||||
{{ $pr->status === PressReleaseStatus::Rejected ? __('Überarbeiten') : __('Entwurf') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="p-5 flex flex-wrap items-center gap-3">
|
||||
<p class="text-[13px] text-[color:var(--color-ink-2)] m-0 flex-1 min-w-[220px]">
|
||||
{{ $pr->status === PressReleaseStatus::Rejected
|
||||
? __('Sie können den Text bearbeiten und erneut zur Prüfung einreichen.')
|
||||
: __('Reichen Sie den Entwurf ein, sobald er vollständig ist.') }}
|
||||
</flux:text>
|
||||
<div class="flex items-center gap-2">
|
||||
@if($canEdit)
|
||||
</p>
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
@if ($canEdit)
|
||||
<flux:button variant="ghost" icon="pencil" href="{{ route('me.press-releases.edit', $pr->id) }}" wire:navigate>
|
||||
{{ __('Bearbeiten') }}
|
||||
</flux:button>
|
||||
|
|
@ -184,140 +230,180 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
</article>
|
||||
@endif
|
||||
|
||||
@if($pr->status === PressReleaseStatus::Review)
|
||||
<flux:callout color="yellow" icon="clock">
|
||||
{{ __('Ihre Pressemitteilung wird gerade geprüft. Sie werden benachrichtigt, sobald eine Entscheidung vorliegt.') }}
|
||||
</flux:callout>
|
||||
@endif
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-2">
|
||||
<flux:card>
|
||||
<div class="mb-4 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Zugeordnete Pressekontakte') }}</flux:heading>
|
||||
<flux:text class="text-sm text-zinc-500">
|
||||
{{ __('Kontakte, die dieser Pressemitteilung zugeordnet sind.') }}
|
||||
</flux:text>
|
||||
@if ($pr->status === PressReleaseStatus::Review)
|
||||
<article class="panel" style="border-color:var(--color-warn); border-left-width:3px;">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('In Prüfung') }}</span>
|
||||
<span class="badge warn dot">{{ __('Geduld bitte') }}</span>
|
||||
</div>
|
||||
<div class="p-5 flex items-start gap-3">
|
||||
<div class="w-9 h-9 rounded-[5px] flex items-center justify-center flex-shrink-0
|
||||
bg-[color:var(--color-warn-soft)] border border-[color:var(--color-warn)]/30 text-[color:var(--color-accent-deep)]">
|
||||
<flux:icon.clock class="size-[18px]" />
|
||||
</div>
|
||||
<p class="flex-1 text-[13px] text-[color:var(--color-ink-2)] m-0">
|
||||
{{ __('Ihre Pressemitteilung wird gerade geprüft. Sie werden benachrichtigt, sobald eine Entscheidung vorliegt.') }}
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
@endif
|
||||
|
||||
@if($pr->company)
|
||||
{{-- ============== KONTAKTE + STATUS/VERLAUF ============== --}}
|
||||
<div class="grid gap-6 xl:grid-cols-2">
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Zugeordnete Pressekontakte') }}</span>
|
||||
@if ($pr->company)
|
||||
<flux:button size="sm" variant="ghost" icon="building-office" href="{{ route('me.press-kits.show', $pr->company->id) }}" wire:navigate>
|
||||
{{ __('Firma') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
@forelse($contacts as $contact)
|
||||
<div class="rounded-lg bg-zinc-50 p-3 dark:bg-zinc-900">
|
||||
<flux:text weight="semibold">
|
||||
{{ trim(($contact->first_name ?? '').' '.($contact->last_name ?? '')) ?: __('Kontakt ohne Name') }}
|
||||
</flux:text>
|
||||
<flux:text class="text-sm text-zinc-500">{{ $contact->responsibility ?: __('Keine Rolle hinterlegt') }}</flux:text>
|
||||
<div class="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs text-zinc-500">
|
||||
@if($contact->email)
|
||||
<a href="mailto:{{ $contact->email }}" class="text-blue-600 hover:underline dark:text-blue-400">{{ $contact->email }}</a>
|
||||
@endif
|
||||
@if($contact->phone)
|
||||
<span>{{ $contact->phone }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="rounded-lg border border-dashed border-zinc-200 p-4 text-sm text-zinc-500 dark:border-zinc-700">
|
||||
{{ __('Dieser Pressemitteilung ist noch kein Pressekontakt zugeordnet.') }}
|
||||
@if($pr->company)
|
||||
<a href="{{ route('me.press-kits.show', $pr->company->id) }}" wire:navigate class="font-medium text-blue-600 hover:underline dark:text-blue-400">
|
||||
{{ __('Kontakte in der Firma prüfen.') }}
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<flux:card>
|
||||
<flux:heading size="lg" class="mb-4">{{ __('Status & Verlauf') }}</flux:heading>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div class="rounded-lg bg-zinc-50 p-3 dark:bg-zinc-900">
|
||||
<flux:text class="text-xs text-zinc-500">{{ __('Aktueller Status') }}</flux:text>
|
||||
<flux:badge class="mt-1" :color="$statusColor">{{ $pr->status->label() }}</flux:badge>
|
||||
</div>
|
||||
<div class="rounded-lg bg-zinc-50 p-3 dark:bg-zinc-900">
|
||||
<flux:text class="text-xs text-zinc-500">{{ __('Erstellt') }}</flux:text>
|
||||
<flux:text weight="semibold">{{ $pr->created_at?->format('d.m.Y H:i') ?? '–' }}</flux:text>
|
||||
</div>
|
||||
<div class="rounded-lg bg-zinc-50 p-3 dark:bg-zinc-900">
|
||||
<flux:text class="text-xs text-zinc-500">{{ __('Veröffentlicht') }}</flux:text>
|
||||
<flux:text weight="semibold">{{ $pr->published_at?->format('d.m.Y H:i') ?? '–' }}</flux:text>
|
||||
</div>
|
||||
<div class="rounded-lg bg-zinc-50 p-3 dark:bg-zinc-900">
|
||||
<flux:text class="text-xs text-zinc-500">{{ __('Aufrufe') }}</flux:text>
|
||||
<flux:text weight="semibold">{{ number_format($pr->hits, 0, ',', '.') }}</flux:text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<flux:separator class="my-4" />
|
||||
|
||||
@if($statusLogs->isNotEmpty())
|
||||
<ol class="space-y-3 border-s border-zinc-200 ps-4 dark:border-zinc-700">
|
||||
@foreach($statusLogs as $log)
|
||||
<li class="text-sm">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@php
|
||||
$color = match($log->to_status?->value) {
|
||||
'published' => 'green',
|
||||
'review' => 'yellow',
|
||||
'rejected' => 'red',
|
||||
'archived' => 'blue',
|
||||
default => 'zinc',
|
||||
};
|
||||
@endphp
|
||||
<flux:badge size="sm" :color="$color">{{ $log->to_status?->label() }}</flux:badge>
|
||||
<span class="text-xs text-zinc-500">
|
||||
{{ $log->created_at->format('d.m.Y H:i') }}
|
||||
</span>
|
||||
@if($log->changedBy)
|
||||
<span class="text-xs text-zinc-500">
|
||||
{{ __('durch :name', ['name' => $log->changedBy->name]) }}
|
||||
</span>
|
||||
<div class="p-5">
|
||||
<p class="text-[12px] text-[color:var(--color-ink-3)] mt-0 mb-4">
|
||||
{{ __('Kontakte, die dieser Pressemitteilung zugeordnet sind.') }}
|
||||
</p>
|
||||
<div class="space-y-2">
|
||||
@forelse ($contacts as $contact)
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)]">
|
||||
{{ trim(($contact->first_name ?? '').' '.($contact->last_name ?? '')) ?: __('Kontakt ohne Name') }}
|
||||
</div>
|
||||
<div class="text-[12px] text-[color:var(--color-ink-3)]">
|
||||
{{ $contact->responsibility ?: __('Keine Rolle hinterlegt') }}
|
||||
</div>
|
||||
<div class="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
@if ($contact->email)
|
||||
<a href="mailto:{{ $contact->email }}"
|
||||
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
|
||||
{{ $contact->email }}
|
||||
</a>
|
||||
@endif
|
||||
@if ($contact->phone)
|
||||
<span>{{ $contact->phone }}</span>
|
||||
@endif
|
||||
</div>
|
||||
@if($log->reason)
|
||||
<p class="mt-1 text-zinc-600 dark:text-zinc-400">{{ $log->reason }}</p>
|
||||
</div>
|
||||
@empty
|
||||
<div class="rounded-[5px] border border-dashed border-[color:var(--color-bg-rule)] p-4 text-[12.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ __('Dieser Pressemitteilung ist noch kein Pressekontakt zugeordnet.') }}
|
||||
@if ($pr->company)
|
||||
<a href="{{ route('me.press-kits.show', $pr->company->id) }}" wire:navigate
|
||||
class="font-medium text-[color:var(--color-hub)] hover:underline">
|
||||
{{ __('Kontakte in der Firma prüfen.') }}
|
||||
</a>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ol>
|
||||
@else
|
||||
<flux:text class="text-sm text-zinc-500">
|
||||
{{ __('Noch keine Statusänderungen protokolliert.') }}
|
||||
</flux:text>
|
||||
@endif
|
||||
</flux:card>
|
||||
</div>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<flux:card>
|
||||
<div class="prose prose-zinc dark:prose-invert max-w-none">
|
||||
{!! nl2br(e($pr->text)) !!}
|
||||
</div>
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Status & Verlauf') }}</span>
|
||||
<span @class(['badge', $statusClass])>{{ $pr->status->label() }}</span>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<div class="grid gap-2 sm:grid-cols-2">
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Aktueller Status') }}</div>
|
||||
<div class="mt-1.5">
|
||||
<span @class(['badge', $statusClass])>{{ $pr->status->label() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Erstellt') }}</div>
|
||||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
|
||||
{{ $pr->created_at?->format('d.m.Y H:i') ?? '–' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Veröffentlicht') }}</div>
|
||||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
|
||||
{{ $pr->published_at?->format('d.m.Y H:i') ?? '–' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Aufrufe') }}</div>
|
||||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
|
||||
{{ number_format($pr->hits, 0, ',', '.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($pr->keywords || $pr->backlink_url)
|
||||
<div class="mt-6 space-y-2 border-t border-zinc-200 pt-4 text-sm text-zinc-500 dark:border-zinc-700">
|
||||
@if($pr->keywords)
|
||||
<p><strong>{{ __('Stichwörter') }}:</strong> {{ $pr->keywords }}</p>
|
||||
@endif
|
||||
@if($pr->backlink_url)
|
||||
<p><strong>{{ __('Backlink') }}:</strong>
|
||||
<a href="{{ $pr->backlink_url }}" target="_blank" class="text-blue-600 underline">{{ $pr->backlink_url }}</a>
|
||||
<div class="my-4 border-t border-[color:var(--color-bg-rule)]"></div>
|
||||
|
||||
@if ($statusLogs->isNotEmpty())
|
||||
<ol class="space-y-3 border-s border-[color:var(--color-bg-rule)] ps-4">
|
||||
@foreach ($statusLogs as $log)
|
||||
<li class="text-[12.5px]">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@php
|
||||
$logClass = match ($log->to_status?->value) {
|
||||
'published' => 'ok',
|
||||
'review' => 'warn',
|
||||
'rejected' => 'err',
|
||||
default => 'hub',
|
||||
};
|
||||
@endphp
|
||||
<span @class(['badge', $logClass])>{{ $log->to_status?->label() }}</span>
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ $log->created_at->format('d.m.Y H:i') }}
|
||||
</span>
|
||||
@if ($log->changedBy)
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ __('durch :name', ['name' => $log->changedBy->name]) }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
@if ($log->reason)
|
||||
<p class="mt-1.5 text-[color:var(--color-ink-2)] m-0">{{ $log->reason }}</p>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ol>
|
||||
@else
|
||||
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
|
||||
{{ __('Noch keine Statusänderungen protokolliert.') }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</flux:card>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
{{-- ============== INHALT ============== --}}
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Inhalt') }}</span>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<div class="prose prose-zinc dark:prose-invert max-w-none text-[color:var(--color-ink)]">
|
||||
{!! nl2br(e($pr->text)) !!}
|
||||
</div>
|
||||
|
||||
@if ($pr->keywords || $pr->backlink_url)
|
||||
<div class="mt-6 space-y-2 border-t border-[color:var(--color-bg-rule)] pt-4 text-[12.5px] text-[color:var(--color-ink-2)]">
|
||||
@if ($pr->keywords)
|
||||
<p class="m-0">
|
||||
<strong class="text-[color:var(--color-ink)] font-semibold">{{ __('Stichwörter') }}:</strong>
|
||||
{{ $pr->keywords }}
|
||||
</p>
|
||||
@endif
|
||||
@if ($pr->backlink_url)
|
||||
<p class="m-0">
|
||||
<strong class="text-[color:var(--color-ink)] font-semibold">{{ __('Backlink') }}:</strong>
|
||||
<a href="{{ $pr->backlink_url }}" target="_blank"
|
||||
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
|
||||
{{ $pr->backlink_url }}
|
||||
</a>
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,15 @@
|
|||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600" rel="stylesheet" />
|
||||
{{-- Hub × FluxUI Phase 1: Inter Tight + JetBrains Mono + Source Serif 4
|
||||
(Source Serif 4 nur für brand-mark in Headern, deshalb mitgeladen). --}}
|
||||
<link href="https://fonts.bunny.net/css?family=inter-tight:400,500,600,700|jetbrains-mono:400,500,600|source-serif-4:400,500,600,700" rel="stylesheet" />
|
||||
|
||||
@vite(['resources/css/portal.css', 'resources/js/app.js'], 'build/portal')
|
||||
{{-- Phase 1 Refinement: NUR portal.css einbinden — KEIN resources/js/app.js.
|
||||
app.js startet Alpine via `Alpine.start()`, aber @fluxScripts (am Ende
|
||||
des <body>) bringt bereits eine eigene Alpine-Instanz mit. Geladen
|
||||
wir beides, läuft Alpine doppelt — Browser-Warning
|
||||
"Detected multiple instances of Alpine running" und kaputte Bindings.
|
||||
Für x-data im Portal greift Alpine aus @fluxScripts. --}}
|
||||
@vite(['resources/css/portal.css'], 'build/portal')
|
||||
@fluxAppearance
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
@vite([\App\Helpers\ThemeHelper::getThemeCssPath(), 'resources/js/app.js'], $domainConfig['assets_dir'] ?? 'build/web')
|
||||
|
||||
@if (in_array(($theme ?? null), ['businessportal24', 'presseecho', 'presseportale'], true))
|
||||
@if (in_array(($theme ?? null), ['businessportal24', 'presseecho', 'pressekonto'], true))
|
||||
<link rel="preconnect" href="https://fonts.bunny.net" crossorigin>
|
||||
<link href="https://fonts.bunny.net/css?family=inter-tight:400,500,600,700|source-serif-4:400,500,600,700|jetbrains-mono:400,500,600" rel="stylesheet" />
|
||||
@endif
|
||||
|
|
@ -72,7 +72,7 @@
|
|||
@stack('styles')
|
||||
|
||||
<!-- Domain-spezifische Fonts -->
|
||||
@if (! in_array(($theme ?? null), ['businessportal24', 'presseecho', 'presseportale'], true))
|
||||
@if (! in_array(($theme ?? null), ['businessportal24', 'presseecho', 'pressekonto'], true))
|
||||
<link href="https://fonts.bunny.net/css?family=montserrat:400,500,600,700" rel="stylesheet" />
|
||||
@endif
|
||||
</head>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
@extends('web.layouts.web-master')
|
||||
|
||||
@php
|
||||
$brand = config('domains.domains.presseportale.brand', []);
|
||||
$brand = config('domains.domains.pressekonto.brand', []);
|
||||
$from = request()->query('from');
|
||||
$todayDate = now()->locale('de')->isoFormat('dddd, D. MMMM YYYY');
|
||||
@endphp
|
||||
|
||||
@section('title', $brand['meta_title'] ?? 'presseportale – Publisher-Hub')
|
||||
@section('title', $brand['meta_title'] ?? 'pressekonto – Publisher-Hub')
|
||||
@section('meta_description',
|
||||
$brand['meta_description'] ??
|
||||
'Der gemeinsame Publisher-Hub für presseecho und
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
</h1>
|
||||
|
||||
<p class="text-[16px] lg:text-[17px] leading-[1.55] text-ink-2 mt-7 m-0 max-w-[560px]">
|
||||
<x-web.brand-mark brand="presseportale" :serif="false" /> ist der gemeinsame Publisher-Bereich für
|
||||
<x-web.brand-mark brand="pressekonto" :serif="false" /> ist der gemeinsame Publisher-Bereich für
|
||||
unsere beiden Pressefachportale. Pressemitteilungen schreiben, redaktionell prüfen lassen, auf
|
||||
beiden Reichweiten veröffentlichen — und Reichweite, Empfänger und Abrechnung an einem Ort
|
||||
verwalten.
|
||||
|
|
@ -106,7 +106,7 @@
|
|||
class="w-[140px] h-[140px] rounded-full bg-hub-grad flex flex-col items-center justify-center text-white shadow-lg shadow-hub/30 text-center">
|
||||
<div class="text-[10px] font-bold tracking-[0.22em] uppercase text-hub-line">Hub</div>
|
||||
<div class="text-[15px] font-bold tracking-[-0.3px] mt-1">
|
||||
<x-web.brand-mark brand="presseportale" :serif="false" />
|
||||
<x-web.brand-mark brand="pressekonto" variant="on-dark" :serif="false" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -692,8 +692,8 @@
|
|||
</a>
|
||||
<div class="text-[11px] text-ink-on-dark-2 text-center mt-3 leading-[1.5]">
|
||||
Rückruf werktags innerhalb von 4 h<br />
|
||||
<a href="mailto:vertrieb@presseportale.com"
|
||||
class="text-white/85 underline underline-offset-[3px] decoration-white/30">vertrieb@presseportale.com</a>
|
||||
<a href="mailto:vertrieb@pressekonto.de"
|
||||
class="text-white/85 underline underline-offset-[3px] decoration-white/30">vertrieb@pressekonto.de</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
|
@ -723,13 +723,13 @@
|
|||
{{-- ============== PLATTFORM-FAMILIE ============== --}}
|
||||
<section id="familie" class="max-w-layout mx-auto px-8 pt-16 lg:pt-20 pb-12 lg:pb-16">
|
||||
<header class="mb-10">
|
||||
<div class="section-eyebrow mb-4">Hinter presseportale</div>
|
||||
<div class="section-eyebrow mb-4">Hinter pressekonto</div>
|
||||
<h2
|
||||
class="text-[26px] lg:text-[32px] font-bold m-0 tracking-[-0.6px] text-ink leading-[1.18] max-w-[820px]">
|
||||
Zwei eigenständige Pressefachportale. Eine kuratierte Verlags-Familie.
|
||||
</h2>
|
||||
<p class="text-[14.5px] text-ink-2 leading-[1.55] m-0 mt-4 max-w-[760px]">
|
||||
<x-web.brand-mark brand="presseportale" :serif="false" /> ist nicht „irgendein Tool" — es ist die
|
||||
<x-web.brand-mark brand="pressekonto" :serif="false" /> ist nicht „irgendein Tool" — es ist die
|
||||
zentrale Plattform für unsere beiden redaktionell geführten Pressefachportale. Jedes Portal hat einen
|
||||
eigenen Charakter, eigene Leserschaft und eigene Themen-Schwerpunkte.
|
||||
</p>
|
||||
|
|
@ -840,7 +840,7 @@
|
|||
<path d="M9 12h6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||
</svg>
|
||||
<div class="text-[13px] leading-[1.55]">
|
||||
Pressemitteilungen, die Sie über <x-web.brand-mark brand="presseportale" variant="on-dark"
|
||||
Pressemitteilungen, die Sie über <x-web.brand-mark brand="pressekonto" variant="on-dark"
|
||||
:serif="false" /> einreichen, erscheinen auf <strong class="text-white font-semibold">beiden
|
||||
Portalen</strong> — ohne Aufpreis, ohne doppelte Eingabe. Sie haben eine zentrale
|
||||
Mitteilungs-Verwaltung und eine zentrale Reichweiten-Statistik.
|
||||
|
|
@ -863,7 +863,7 @@
|
|||
|
||||
<div>
|
||||
<p class="text-[15px] lg:text-[16px] leading-[1.7] text-ink-2 m-0">
|
||||
Über <x-web.brand-mark brand="presseportale" :serif="false" /> veröffentlichen unter anderem
|
||||
Über <x-web.brand-mark brand="pressekonto" :serif="false" /> veröffentlichen unter anderem
|
||||
<a class="text-hub font-semibold underline underline-offset-[3px] decoration-hub/25 hover:decoration-hub"
|
||||
href="#">Siemens AG</a>,
|
||||
<a class="text-hub font-semibold underline underline-offset-[3px] decoration-hub/25 hover:decoration-hub"
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
Styling</h3>
|
||||
<p>Die Styles werden basierend auf der Domain automatisch geladen:</p>
|
||||
<ul class="list-disc pl-5 mt-2 space-y-1">
|
||||
<li>presseportale.test – Hauptstil (Portal)</li>
|
||||
<li>pressekonto.test – Hauptstil (Portal)</li>
|
||||
<li>landing1.local - Landingpage 1</li>
|
||||
<li>landing2.local - Landingpage 2</li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
## Aktive Routen
|
||||
|
||||
### Basis
|
||||
- `GET /` → öffentliche Publisher-Landing (`web/presseportale.blade.php`, Hub-Theme); eingeloggte Admins werden nach Login direkt nach `/dashboard` geleitet.
|
||||
- `GET /` → öffentliche Publisher-Landing (`web/pressekonto.blade.php`, Hub-Theme); eingeloggte Admins werden nach Login direkt nach `/dashboard` geleitet.
|
||||
- `GET /dashboard` → `dashboard`
|
||||
- `GET /settings` (Redirect) → `settings/profile`
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use Illuminate\Support\Facades\Route;
|
|||
use Livewire\Volt\Volt;
|
||||
|
||||
// Hinweis: Die Root-Route (`/`) auf der Portal-Domain rendert seit dem
|
||||
// Hub-Launch die öffentliche Publisher-Landing (web/presseportale.blade.php).
|
||||
// Hub-Launch die öffentliche Publisher-Landing (web/pressekonto.blade.php).
|
||||
// Eingeloggte Admins werden über die Auth-Pipeline direkt nach /dashboard
|
||||
// geleitet (siehe FORTIFY_HOME / LoginResponseContract).
|
||||
Route::get('dashboard', DashboardController::class)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ $domainPortal = config('domains.domain_portal');
|
|||
$domainPresseecho = config('domains.domain_presseecho');
|
||||
$domainBusinessportal = config('domains.domain_businessportal');
|
||||
|
||||
// Portal-Bereich (lokal: presseportale.test, live: presseportale.com – via .env)
|
||||
// Portal-Bereich (lokal: pressekonto.test, live: pressekonto.de – via .env)
|
||||
Route::domain($domainPortal)->group(function () {
|
||||
// Auth-Routen laden
|
||||
require __DIR__.'/auth.php';
|
||||
|
|
@ -32,7 +32,7 @@ Route::domain($domainPortal)->group(function () {
|
|||
});
|
||||
|
||||
// API-Routen für alle Domains
|
||||
/*Route::domain('api.presseportale.test')->group(function () {
|
||||
/*Route::domain('api.pressekonto.test')->group(function () {
|
||||
require __DIR__ . '/api.php';
|
||||
});*/
|
||||
|
||||
|
|
|
|||
|
|
@ -188,10 +188,10 @@ Route::get('/', function () use ($applyWebDomainConfig, $webHomeData) {
|
|||
$applyWebDomainConfig('businessportal24');
|
||||
|
||||
return view('web.businessportal24', $webHomeData(Portal::Businessportal24));
|
||||
} elseif (str_contains($domain, 'presseportale')) {
|
||||
$applyWebDomainConfig('presseportale');
|
||||
} elseif (str_contains($domain, 'pressekonto')) {
|
||||
$applyWebDomainConfig('pressekonto');
|
||||
|
||||
return view('web.presseportale');
|
||||
return view('web.pressekonto');
|
||||
}
|
||||
|
||||
$applyWebDomainConfig('businessportal24');
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ echo "📋 Schritt 1: DNS-Einträge prüfen"
|
|||
echo ""
|
||||
echo "Bitte stelle sicher, dass folgende Einträge in deiner Hosts-Datei existieren:"
|
||||
echo ""
|
||||
echo "127.0.0.1 presseportale.test"
|
||||
echo "127.0.0.1 assets.presseportale.test"
|
||||
echo "127.0.0.1 pressekonto.test"
|
||||
echo "127.0.0.1 assets.pressekonto.test"
|
||||
echo "127.0.0.1 assets.presseecho.test"
|
||||
echo "127.0.0.1 assets.businessportal24.test"
|
||||
echo ""
|
||||
|
|
@ -43,8 +43,8 @@ echo "🔍 Schritt 3: Traefik-Routen testen"
|
|||
echo ""
|
||||
|
||||
# Test mit curl (SSL-Verifikation ignorieren für self-signed certs)
|
||||
echo "Teste assets.presseportale.test..."
|
||||
if curl -Iks https://assets.presseportale.test | head -1 | grep -q "404\|200"; then
|
||||
echo "Teste assets.pressekonto.test..."
|
||||
if curl -Iks https://assets.pressekonto.test | head -1 | grep -q "404\|200"; then
|
||||
echo " ✅ Erreichbar über HTTPS"
|
||||
else
|
||||
echo " ⚠️ Nicht erreichbar - Docker Container neu starten!"
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue