PM-Vorschau: echtes Portal-Design, Direkt-Button und Portal-Switch
Der Vorschau-Link (/pm-vorschau/{token}) renderte eine schlichte
Standalone-Seite, die dem spaeteren Erscheinungsbild nicht entsprach.
Jetzt rendert er die echte Detailseite (web.release-detail) im Theme
des Zielportals:
- Controller wendet Domain-/Theme-Konfig des Zielportals an (Sprache
der PM, Links zeigen auf die Portal-Domain), kein Hit-Zaehler
- Gelbes Vorschau-Banner mit Status und Link-Gueltigkeit; noindex per
Meta-Tag + X-Robots-Tag-Header, Canonical entfaellt bei noindex
(neue robots-Section im Web-Layout)
- Teilen-Buttons teilen den Vorschau-Link selbst (oeffentliche URL
existiert vor Veroeffentlichung noch nicht)
- PMs fuer beide Portale: ?portal=... waehlt die Ansicht, Umschalter
im Banner; im Kundenpanel je Portal ein Vorschau-Button, sonst ein
einzelner "Vorschau oeffnen"-Button (Link-Feld jetzt kopierbar)
- Alte press-release-preview-View entfernt
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
db65c2ed7d
commit
14994e4748
8 changed files with 306 additions and 149 deletions
|
|
@ -2,14 +2,17 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Enums\Portal;
|
||||||
|
use App\Http\Middleware\SetEdition;
|
||||||
use App\Models\MagicLink;
|
use App\Models\MagicLink;
|
||||||
use App\Models\PressRelease;
|
use App\Models\PressRelease;
|
||||||
use Illuminate\Contracts\View\View;
|
use App\Support\DomainAssetContext;
|
||||||
|
use Illuminate\Support\Facades\URL;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
class PressReleasePreviewController extends Controller
|
class PressReleasePreviewController extends Controller
|
||||||
{
|
{
|
||||||
public function __invoke(string $token): View|Response
|
public function __invoke(string $token): Response
|
||||||
{
|
{
|
||||||
$magicLink = MagicLink::query()
|
$magicLink = MagicLink::query()
|
||||||
->where('token_hash', hash('sha256', $token))
|
->where('token_hash', hash('sha256', $token))
|
||||||
|
|
@ -28,7 +31,13 @@ class PressReleasePreviewController extends Controller
|
||||||
|
|
||||||
$pressRelease = $pressReleaseId
|
$pressRelease = $pressReleaseId
|
||||||
? PressRelease::withoutGlobalScopes()
|
? PressRelease::withoutGlobalScopes()
|
||||||
->with(['company:id,name,slug', 'category.translations', 'images', 'user:id,name'])
|
->with([
|
||||||
|
'company',
|
||||||
|
'category.translations',
|
||||||
|
'category.parent.translations',
|
||||||
|
'images' => fn ($query) => $query->orderByDesc('is_preview')->orderBy('sort_order'),
|
||||||
|
'contacts',
|
||||||
|
])
|
||||||
->find($pressReleaseId)
|
->find($pressReleaseId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
|
@ -36,10 +45,81 @@ class PressReleasePreviewController extends Controller
|
||||||
return $this->renderError(__('Die Pressemitteilung wurde nicht gefunden.'), 404);
|
return $this->renderError(__('Die Pressemitteilung wurde nicht gefunden.'), 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('press-release-preview', [
|
$domainKey = $this->resolveTargetDomainKey($pressRelease);
|
||||||
'pressRelease' => $pressRelease,
|
|
||||||
'expiresAt' => $magicLink->expires_at,
|
$this->applyTargetPortalPresentation($pressRelease, $domainKey);
|
||||||
]);
|
|
||||||
|
return response()
|
||||||
|
->view('web.release-detail', [
|
||||||
|
'release' => $pressRelease,
|
||||||
|
'companyReleases' => collect(),
|
||||||
|
'relatedReleases' => collect(),
|
||||||
|
'isPreview' => true,
|
||||||
|
'previewExpiresAt' => $magicLink->expires_at,
|
||||||
|
'previewPortalOptions' => $this->previewPortalOptions($pressRelease, $domainKey),
|
||||||
|
])
|
||||||
|
->header('X-Robots-Tag', 'noindex, nofollow');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zielportal der Vorschau. Bei PMs für beide Portale wählt der optionale
|
||||||
|
* Query-Parameter `portal` die Ansicht (Default: presseecho); bei
|
||||||
|
* Einzel-Portal-PMs ist das Portal fix.
|
||||||
|
*/
|
||||||
|
private function resolveTargetDomainKey(PressRelease $pressRelease): string
|
||||||
|
{
|
||||||
|
if ($pressRelease->portal === Portal::Both) {
|
||||||
|
$requested = request()->query('portal');
|
||||||
|
|
||||||
|
return in_array($requested, [Portal::Presseecho->value, Portal::Businessportal24->value], true)
|
||||||
|
? $requested
|
||||||
|
: Portal::Presseecho->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $pressRelease->portal === Portal::Businessportal24
|
||||||
|
? Portal::Businessportal24->value
|
||||||
|
: Portal::Presseecho->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Umschalt-Optionen für den Vorschau-Banner — nur bei PMs, die auf beiden
|
||||||
|
* Portalen erscheinen.
|
||||||
|
*
|
||||||
|
* @return list<array{label: string, url: string, active: bool}>|null
|
||||||
|
*/
|
||||||
|
private function previewPortalOptions(PressRelease $pressRelease, string $activeDomainKey): ?array
|
||||||
|
{
|
||||||
|
if ($pressRelease->portal !== Portal::Both) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return collect([Portal::Presseecho->value, Portal::Businessportal24->value])
|
||||||
|
->map(fn (string $domainKey): array => [
|
||||||
|
'label' => $domainKey,
|
||||||
|
'url' => request()->url().'?portal='.$domainKey,
|
||||||
|
'active' => $domainKey === $activeDomainKey,
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rendert die Vorschau im Look des Zielportals: Theme-/Asset-Konfiguration
|
||||||
|
* der Web-Domain anwenden, Links auf das Zielportal zeigen lassen und die
|
||||||
|
* Ausgabe-Sprache der PM übernehmen — damit die Vorschau 1:1 der späteren
|
||||||
|
* öffentlichen Detailseite entspricht.
|
||||||
|
*/
|
||||||
|
private function applyTargetPortalPresentation(PressRelease $pressRelease, string $domainKey): void
|
||||||
|
{
|
||||||
|
$domainConfig = DomainAssetContext::applyWebDomainConfig($domainKey);
|
||||||
|
|
||||||
|
URL::forceRootUrl($domainConfig['url'] ?? config('app.url'));
|
||||||
|
|
||||||
|
$language = in_array($pressRelease->language, SetEdition::EDITIONS, true)
|
||||||
|
? $pressRelease->language
|
||||||
|
: SetEdition::DEFAULT_EDITION;
|
||||||
|
|
||||||
|
app()->setLocale($language);
|
||||||
|
URL::defaults(['edition' => $language]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function renderError(string $message, int $status): Response
|
private function renderError(string $message, int $status): Response
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,6 @@ class DomainAssetContext
|
||||||
'magic-links.*',
|
'magic-links.*',
|
||||||
'me.*',
|
'me.*',
|
||||||
'settings.*',
|
'settings.*',
|
||||||
'press-releases.preview',
|
|
||||||
)) {
|
)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,11 @@ return [
|
||||||
'related' => 'Verwandte Meldungen',
|
'related' => 'Verwandte Meldungen',
|
||||||
'related_sub' => 'Reaktionen, ähnliche Themen, Branchenkontext',
|
'related_sub' => 'Reaktionen, ähnliche Themen, Branchenkontext',
|
||||||
'all_from_category' => 'Alle Meldungen aus :category',
|
'all_from_category' => 'Alle Meldungen aus :category',
|
||||||
|
'preview_label' => 'Vorschau',
|
||||||
|
'preview_text' => 'So erscheint die Pressemitteilung nach der Veröffentlichung. Diese Ansicht ist nicht öffentlich.',
|
||||||
|
'preview_status' => 'Status: :status',
|
||||||
|
'preview_valid_until' => 'Link gültig bis :date',
|
||||||
|
'preview_portal_switch' => 'Ansicht',
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -297,6 +297,11 @@ return [
|
||||||
'related' => 'Related releases',
|
'related' => 'Related releases',
|
||||||
'related_sub' => 'Reactions, similar topics, industry context',
|
'related_sub' => 'Reactions, similar topics, industry context',
|
||||||
'all_from_category' => 'All releases in :category',
|
'all_from_category' => 'All releases in :category',
|
||||||
|
'preview_label' => 'Preview',
|
||||||
|
'preview_text' => 'This is how the press release will appear once published. This view is not public.',
|
||||||
|
'preview_status' => 'Status: :status',
|
||||||
|
'preview_valid_until' => 'Link valid until :date',
|
||||||
|
'preview_portal_switch' => 'View',
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -362,8 +362,26 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
||||||
<span class="section-eyebrow">{{ __('Öffentlicher Vorschau-Link erstellt') }}</span>
|
<span class="section-eyebrow">{{ __('Öffentlicher Vorschau-Link erstellt') }}</span>
|
||||||
<span class="badge ok dot">{{ __('gültig bis :date', ['date' => $shareExpiresAt]) }}</span>
|
<span class="badge ok dot">{{ __('gültig bis :date', ['date' => $shareExpiresAt]) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-5">
|
<div class="p-5 space-y-3">
|
||||||
<flux:input readonly :value="$shareUrl" />
|
<flux:input readonly copyable :value="$shareUrl" />
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
@if ($pr->portal === \App\Enums\Portal::Both)
|
||||||
|
{{-- PM erscheint auf beiden Portalen: Vorschau je Portal öffnen --}}
|
||||||
|
<flux:button size="sm" variant="primary" icon="arrow-top-right-on-square"
|
||||||
|
href="{{ $shareUrl }}?portal=presseecho" target="_blank" rel="noopener">
|
||||||
|
{{ __('Vorschau presseecho') }}
|
||||||
|
</flux:button>
|
||||||
|
<flux:button size="sm" variant="primary" icon="arrow-top-right-on-square"
|
||||||
|
href="{{ $shareUrl }}?portal=businessportal24" target="_blank" rel="noopener">
|
||||||
|
{{ __('Vorschau businessportal24') }}
|
||||||
|
</flux:button>
|
||||||
|
@else
|
||||||
|
<flux:button size="sm" variant="primary" icon="arrow-top-right-on-square"
|
||||||
|
href="{{ $shareUrl }}" target="_blank" rel="noopener">
|
||||||
|
{{ __('Vorschau öffnen') }}
|
||||||
|
</flux:button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
@endif
|
@endif
|
||||||
|
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
<!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="robots" content="noindex,nofollow">
|
|
||||||
<title>{{ $pressRelease->title }} – Vorschau</title>
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--bg: #f8fafc;
|
|
||||||
--surface: #ffffff;
|
|
||||||
--border: #e2e8f0;
|
|
||||||
--text: #0f172a;
|
|
||||||
--muted: #64748b;
|
|
||||||
--accent: #2563eb;
|
|
||||||
}
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--bg: #0f172a;
|
|
||||||
--surface: #1e293b;
|
|
||||||
--border: #334155;
|
|
||||||
--text: #f1f5f9;
|
|
||||||
--muted: #94a3b8;
|
|
||||||
--accent: #60a5fa;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
* { box-sizing: border-box; }
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
.preview-banner {
|
|
||||||
background: #fef3c7;
|
|
||||||
color: #78350f;
|
|
||||||
padding: 12px 16px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 14px;
|
|
||||||
border-bottom: 1px solid #fde68a;
|
|
||||||
}
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.preview-banner {
|
|
||||||
background: #78350f;
|
|
||||||
color: #fef3c7;
|
|
||||||
border-bottom-color: #fcd34d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 32px auto;
|
|
||||||
padding: 32px;
|
|
||||||
background: var(--surface);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
|
||||||
}
|
|
||||||
h1 { margin: 0 0 8px; font-size: 28px; line-height: 1.25; }
|
|
||||||
.meta {
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
.body { white-space: pre-wrap; }
|
|
||||||
.images {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
margin: 24px 0;
|
|
||||||
}
|
|
||||||
.images img {
|
|
||||||
width: 100%;
|
|
||||||
height: 200px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
.footer {
|
|
||||||
margin-top: 32px;
|
|
||||||
padding-top: 16px;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
.keywords { font-size: 13px; color: var(--muted); margin-top: 16px; }
|
|
||||||
a { color: var(--accent); }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="preview-banner">
|
|
||||||
{{ __('Private Vorschau – Link gültig bis :date', ['date' => $expiresAt?->format('d.m.Y H:i')]) }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<main class="container">
|
|
||||||
<h1>{{ $pressRelease->title }}</h1>
|
|
||||||
|
|
||||||
<div class="meta">
|
|
||||||
@if($pressRelease->company)
|
|
||||||
<strong>{{ $pressRelease->company->name }}</strong>
|
|
||||||
·
|
|
||||||
@endif
|
|
||||||
@if($pressRelease->category)
|
|
||||||
{{ $pressRelease->category->translations->firstWhere('locale', $pressRelease->language)?->name ?? '–' }}
|
|
||||||
·
|
|
||||||
@endif
|
|
||||||
{{ $pressRelease->created_at?->format('d.m.Y') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if($pressRelease->images->isNotEmpty())
|
|
||||||
<div class="images">
|
|
||||||
@foreach($pressRelease->images as $image)
|
|
||||||
@if($image->variantUrl('medium') ?? $image->url())
|
|
||||||
<img src="{{ $image->variantUrl('medium') ?? $image->url() }}" alt="{{ $image->title ?? $pressRelease->title }}" loading="lazy">
|
|
||||||
@endif
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<div class="body">{!! $pressRelease->renderedText() !!}</div>
|
|
||||||
|
|
||||||
@if($pressRelease->keywords)
|
|
||||||
<p class="keywords"><strong>{{ __('Stichwörter') }}:</strong> {{ $pressRelease->keywords }}</p>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<div class="footer">
|
|
||||||
{{ __('Status') }}: {{ $pressRelease->status->label() }}
|
|
||||||
@if($pressRelease->backlink_url)
|
|
||||||
· <a href="{{ $pressRelease->backlink_url }}" rel="nofollow noopener" target="_blank">{{ $pressRelease->backlink_url }}</a>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -3,6 +3,12 @@
|
||||||
@php
|
@php
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
// Private Vorschau über Magic-Link (/pm-vorschau/{token}): identisches
|
||||||
|
// Layout wie die spätere öffentliche Seite, plus Hinweis-Banner und noindex.
|
||||||
|
$isPreview = ($isPreview ?? false) === true;
|
||||||
|
$previewExpiresAt = $previewExpiresAt ?? null;
|
||||||
|
$previewPortalOptions = $previewPortalOptions ?? null;
|
||||||
|
|
||||||
$company = $release->company;
|
$company = $release->company;
|
||||||
|
|
||||||
$categoryTranslation = $release->category?->translations?->firstWhere('locale', app()->getLocale())
|
$categoryTranslation = $release->category?->translations?->firstWhere('locale', app()->getLocale())
|
||||||
|
|
@ -17,7 +23,9 @@
|
||||||
$parentSlug = $parentTranslation?->slug;
|
$parentSlug = $parentTranslation?->slug;
|
||||||
$parentHref = $parentSlug ? route('kategorie', ['slug' => $parentSlug]) : route('web.home');
|
$parentHref = $parentSlug ? route('kategorie', ['slug' => $parentSlug]) : route('web.home');
|
||||||
|
|
||||||
$publishedAt = $release->published_at;
|
// In der Vorschau ersatzweise der geplante Termin, damit die Meta-Zeile
|
||||||
|
// der späteren Veröffentlichung entspricht.
|
||||||
|
$publishedAt = $release->published_at ?? ($isPreview ? $release->scheduledAtLocal() : null);
|
||||||
$embargoAt = $release->embargoAtLocal();
|
$embargoAt = $release->embargoAtLocal();
|
||||||
|
|
||||||
$bodyHtml = $release->renderedText();
|
$bodyHtml = $release->renderedText();
|
||||||
|
|
@ -55,7 +63,9 @@
|
||||||
$publisherVerified = (bool) ($company?->is_active && filled($company?->website));
|
$publisherVerified = (bool) ($company?->is_active && filled($company?->website));
|
||||||
$isRecommended = $release->content_score !== null && $release->content_score >= 80;
|
$isRecommended = $release->content_score !== null && $release->content_score >= 80;
|
||||||
|
|
||||||
$shareUrl = route('release.detail', ['slug' => $release->slug]);
|
// In der Vorschau wird der Vorschau-Link selbst geteilt — die öffentliche
|
||||||
|
// URL existiert vor der Veröffentlichung noch nicht.
|
||||||
|
$shareUrl = $isPreview ? request()->fullUrl() : route('release.detail', ['slug' => $release->slug]);
|
||||||
$brandLabel = config('app.theme') === 'presseecho' ? 'presseecho' : 'businessportal24';
|
$brandLabel = config('app.theme') === 'presseecho' ? 'presseecho' : 'businessportal24';
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
|
|
@ -65,7 +75,11 @@
|
||||||
@if ($imageUrl)
|
@if ($imageUrl)
|
||||||
@section('og_image', $imageUrl)
|
@section('og_image', $imageUrl)
|
||||||
@endif
|
@endif
|
||||||
@section('canonical', $shareUrl)
|
@if ($isPreview)
|
||||||
|
@section('robots', 'noindex,nofollow')
|
||||||
|
@else
|
||||||
|
@section('canonical', $shareUrl)
|
||||||
|
@endif
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
{{-- Lese-Fortschrittsbalken --}}
|
{{-- Lese-Fortschrittsbalken --}}
|
||||||
|
|
@ -78,6 +92,44 @@
|
||||||
|
|
||||||
<x-web.site-header />
|
<x-web.site-header />
|
||||||
|
|
||||||
|
@if ($isPreview)
|
||||||
|
{{-- Hinweis-Banner: private Vorschau, nicht öffentlich erreichbar --}}
|
||||||
|
<div class="bg-amber-50 border-b border-amber-200 text-amber-900">
|
||||||
|
<div class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 py-2.5 flex flex-wrap items-center gap-x-4 gap-y-1 text-[12.5px]">
|
||||||
|
<span class="text-[9.5px] font-bold tracking-[0.18em] uppercase px-2.5 py-[3px] border border-amber-400 font-mono whitespace-nowrap">
|
||||||
|
● {{ __('web.detail.preview_label') }}
|
||||||
|
</span>
|
||||||
|
<span>{{ __('web.detail.preview_text') }}</span>
|
||||||
|
<span class="font-mono text-[11.5px] text-amber-800">
|
||||||
|
{{ __('web.detail.preview_status', ['status' => $release->status->label()]) }}
|
||||||
|
@if ($previewExpiresAt)
|
||||||
|
· {{ __('web.detail.preview_valid_until', ['date' => $previewExpiresAt->translatedFormat('j. F Y, H:i')]) }}
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
|
||||||
|
@if (! empty($previewPortalOptions))
|
||||||
|
{{-- PM erscheint auf beiden Portalen: Ansicht umschalten --}}
|
||||||
|
<span class="sm:ml-auto inline-flex items-center gap-2">
|
||||||
|
<span class="text-[10px] font-semibold tracking-[0.14em] uppercase text-amber-800 font-mono">{{ __('web.detail.preview_portal_switch') }}</span>
|
||||||
|
<span class="inline-flex items-stretch border border-amber-400 divide-x divide-amber-400 font-mono text-[10.5px]">
|
||||||
|
@foreach ($previewPortalOptions as $option)
|
||||||
|
<a href="{{ $option['url'] }}"
|
||||||
|
@class([
|
||||||
|
'px-2.5 py-[3px] whitespace-nowrap transition-colors cursor-pointer',
|
||||||
|
'bg-amber-400/40 font-bold text-amber-950' => $option['active'],
|
||||||
|
'hover:bg-amber-100 text-amber-900' => ! $option['active'],
|
||||||
|
])
|
||||||
|
@if ($option['active']) aria-current="true" @endif>
|
||||||
|
{{ $option['label'] }}
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<main class="bg-bg text-ink">
|
<main class="bg-bg text-ink">
|
||||||
<article>
|
<article>
|
||||||
{{-- Breadcrumbs --}}
|
{{-- Breadcrumbs --}}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,29 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Enums\Portal;
|
||||||
use App\Enums\PressReleaseStatus;
|
use App\Enums\PressReleaseStatus;
|
||||||
use App\Models\PressRelease;
|
use App\Models\PressRelease;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Auth\MagicLinkGenerator;
|
use App\Services\Auth\MagicLinkGenerator;
|
||||||
|
use Database\Seeders\RolesAndPermissionsSeeder;
|
||||||
|
use Livewire\Volt\Volt as LivewireVolt;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
function makePreviewCustomerWithPressRelease(Portal $portal): array
|
||||||
|
{
|
||||||
|
test()->seed(RolesAndPermissionsSeeder::class);
|
||||||
|
|
||||||
|
$customer = User::factory()->create(['is_active' => true]);
|
||||||
|
$customer->assignRole('customer');
|
||||||
|
|
||||||
|
$pr = PressRelease::factory()->forPortal($portal)->create([
|
||||||
|
'user_id' => $customer->id,
|
||||||
|
'status' => PressReleaseStatus::Draft->value,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [$customer, $pr];
|
||||||
|
}
|
||||||
|
|
||||||
test('preview link returns press release for valid unexpired token', function () {
|
test('preview link returns press release for valid unexpired token', function () {
|
||||||
/** @var TestCase $this */
|
/** @var TestCase $this */
|
||||||
$owner = User::factory()->create();
|
$owner = User::factory()->create();
|
||||||
|
|
@ -24,6 +42,97 @@ test('preview link returns press release for valid unexpired token', function ()
|
||||||
->assertSee('Vertraulicher Inhalt');
|
->assertSee('Vertraulicher Inhalt');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('preview renders the public release detail layout with preview banner and noindex', function () {
|
||||||
|
/** @var TestCase $this */
|
||||||
|
$owner = User::factory()->create();
|
||||||
|
$pr = PressRelease::factory()->forPortal(Portal::Presseecho)->create([
|
||||||
|
'user_id' => $owner->id,
|
||||||
|
'status' => PressReleaseStatus::Draft->value,
|
||||||
|
'title' => 'Entwurf im Portal-Design',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$share = app(MagicLinkGenerator::class)->createPressReleaseShareLink($pr, $owner);
|
||||||
|
|
||||||
|
$this->get(route('press-releases.preview', ['token' => $share['plain_token']]))
|
||||||
|
->assertOk()
|
||||||
|
->assertViewIs('web.release-detail')
|
||||||
|
->assertHeader('X-Robots-Tag', 'noindex, nofollow')
|
||||||
|
->assertSee('<meta name="robots" content="noindex,nofollow">', false)
|
||||||
|
->assertDontSee('rel="canonical"', false)
|
||||||
|
->assertSeeText('Vorschau')
|
||||||
|
->assertSeeText('So erscheint die Pressemitteilung nach der Veröffentlichung.')
|
||||||
|
->assertSeeText('Status: Entwurf')
|
||||||
|
->assertSeeText('Entwurf im Portal-Design');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preview applies the theme of the target portal', function () {
|
||||||
|
/** @var TestCase $this */
|
||||||
|
$owner = User::factory()->create();
|
||||||
|
$pr = PressRelease::factory()->forPortal(Portal::Businessportal24)->create([
|
||||||
|
'user_id' => $owner->id,
|
||||||
|
'status' => PressReleaseStatus::Draft->value,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$share = app(MagicLinkGenerator::class)->createPressReleaseShareLink($pr, $owner);
|
||||||
|
|
||||||
|
$this->get(route('press-releases.preview', ['token' => $share['plain_token']]));
|
||||||
|
|
||||||
|
expect(config('app.theme'))->toBe('businessportal24');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preview of a single portal release shows no portal switch', function () {
|
||||||
|
/** @var TestCase $this */
|
||||||
|
$owner = User::factory()->create();
|
||||||
|
$pr = PressRelease::factory()->forPortal(Portal::Presseecho)->create(['user_id' => $owner->id]);
|
||||||
|
|
||||||
|
$share = app(MagicLinkGenerator::class)->createPressReleaseShareLink($pr, $owner);
|
||||||
|
|
||||||
|
$this->get(route('press-releases.preview', ['token' => $share['plain_token']]))
|
||||||
|
->assertOk()
|
||||||
|
->assertDontSee('?portal=businessportal24', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preview of a both portals release defaults to presseecho and shows the portal switch', function () {
|
||||||
|
/** @var TestCase $this */
|
||||||
|
$owner = User::factory()->create();
|
||||||
|
$pr = PressRelease::factory()->forPortal(Portal::Both)->create(['user_id' => $owner->id]);
|
||||||
|
|
||||||
|
$share = app(MagicLinkGenerator::class)->createPressReleaseShareLink($pr, $owner);
|
||||||
|
|
||||||
|
$this->get(route('press-releases.preview', ['token' => $share['plain_token']]))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('?portal=presseecho', false)
|
||||||
|
->assertSee('?portal=businessportal24', false);
|
||||||
|
|
||||||
|
expect(config('app.theme'))->toBe('presseecho');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preview of a both portals release can be switched to businessportal24', function () {
|
||||||
|
/** @var TestCase $this */
|
||||||
|
$owner = User::factory()->create();
|
||||||
|
$pr = PressRelease::factory()->forPortal(Portal::Both)->create(['user_id' => $owner->id]);
|
||||||
|
|
||||||
|
$share = app(MagicLinkGenerator::class)->createPressReleaseShareLink($pr, $owner);
|
||||||
|
|
||||||
|
$this->get(route('press-releases.preview', ['token' => $share['plain_token'], 'portal' => 'businessportal24']))
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
expect(config('app.theme'))->toBe('businessportal24');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('portal query parameter is ignored for single portal releases', function () {
|
||||||
|
/** @var TestCase $this */
|
||||||
|
$owner = User::factory()->create();
|
||||||
|
$pr = PressRelease::factory()->forPortal(Portal::Presseecho)->create(['user_id' => $owner->id]);
|
||||||
|
|
||||||
|
$share = app(MagicLinkGenerator::class)->createPressReleaseShareLink($pr, $owner);
|
||||||
|
|
||||||
|
$this->get(route('press-releases.preview', ['token' => $share['plain_token'], 'portal' => 'businessportal24']))
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
expect(config('app.theme'))->toBe('presseecho');
|
||||||
|
});
|
||||||
|
|
||||||
test('preview link allows access to draft press release without authentication', function () {
|
test('preview link allows access to draft press release without authentication', function () {
|
||||||
/** @var TestCase $this */
|
/** @var TestCase $this */
|
||||||
$owner = User::factory()->create();
|
$owner = User::factory()->create();
|
||||||
|
|
@ -72,3 +181,28 @@ test('preview link for soft deleted press release returns 404', function () {
|
||||||
$this->get(route('press-releases.preview', ['token' => $share['plain_token']]))
|
$this->get(route('press-releases.preview', ['token' => $share['plain_token']]))
|
||||||
->assertStatus(404);
|
->assertStatus(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('share panel offers a direct open button for single portal releases', function () {
|
||||||
|
/** @var TestCase $this */
|
||||||
|
[$customer, $pr] = makePreviewCustomerWithPressRelease(Portal::Presseecho);
|
||||||
|
$this->actingAs($customer);
|
||||||
|
|
||||||
|
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
|
||||||
|
->call('generateShareLink')
|
||||||
|
->assertSee('Vorschau öffnen')
|
||||||
|
->assertDontSee('Vorschau presseecho')
|
||||||
|
->assertDontSee('?portal=', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('share panel offers one open button per portal for both portal releases', function () {
|
||||||
|
/** @var TestCase $this */
|
||||||
|
[$customer, $pr] = makePreviewCustomerWithPressRelease(Portal::Both);
|
||||||
|
$this->actingAs($customer);
|
||||||
|
|
||||||
|
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
|
||||||
|
->call('generateShareLink')
|
||||||
|
->assertSee('Vorschau presseecho')
|
||||||
|
->assertSee('Vorschau businessportal24')
|
||||||
|
->assertSee('?portal=presseecho', false)
|
||||||
|
->assertSee('?portal=businessportal24', false);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue