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:
Kevin Adametz 2026-05-19 16:36:13 +00:00
parent 092ee0e918
commit 0a3e52d603
112 changed files with 8464 additions and 1649 deletions

View file

@ -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;
} */

View 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;
}

View 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);
}
}

View file

@ -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 {

View file

@ -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;

View 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;
}

View file

@ -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;
}

View file

@ -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)

View file

@ -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
---

View file

@ -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

View file

@ -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>

View file

@ -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">

View file

@ -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">

View 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>

View 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 (0100) 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 }}&thinsp;%</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>

View 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>

View file

@ -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>

View file

@ -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 }}

View file

@ -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>

View file

@ -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 }}

View file

@ -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 }}

View file

@ -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>

View file

@ -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">

View file

@ -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>

View file

@ -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)

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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') }} &rarr;
</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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -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&nbsp;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"

View file

@ -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>