R4.3: Karten-Metadaten-Logik als PressRelease-Praesentations-Helper
localizedCategoryName/cardImageUrl/teaserText/wordCount/readTimeMinutes/ compactPublishedLabel ersetzen die 5x kopierte @php-Logik der Kartenkomponenten; Lesezeit-Formel vereinheitlicht (ceil -> round in HomePageData-Spotlight). Tests: Web/PressReleaseCardHelpersTest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
459b339a9b
commit
01e175e138
4 changed files with 137 additions and 3 deletions
|
|
@ -26,6 +26,7 @@ use Illuminate\Pagination\Paginator;
|
|||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PressRelease extends Model
|
||||
{
|
||||
|
|
@ -148,6 +149,77 @@ class PressRelease extends Model
|
|||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kategorie-Name in der Anzeigesprache, Fallback auf die erste
|
||||
* vorhandene Übersetzung. Zentrale Ablöse der zuvor in jeder
|
||||
* Kartenkomponente kopierten Auflösung (R4.3).
|
||||
*/
|
||||
public function localizedCategoryName(?string $locale = null): ?string
|
||||
{
|
||||
$locale ??= app()->getLocale();
|
||||
|
||||
$translation = $this->category?->translations?->firstWhere('locale', $locale)
|
||||
?? $this->category?->translations?->first();
|
||||
|
||||
return $translation?->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bild-URL für Kartenansichten: card-Variante des ersten Bildes,
|
||||
* Fallback Original. Null bei bildlosen PMs (dann greift
|
||||
* chosenPlaceholderUrl() bzw. der generische Fallback der Komponente).
|
||||
*/
|
||||
public function cardImageUrl(): ?string
|
||||
{
|
||||
$image = $this->images?->first();
|
||||
|
||||
return $image?->variantUrl('card') ?? $image?->url();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag-freier, whitespace-normalisierter Anriss des PM-Textes.
|
||||
*/
|
||||
public function teaserText(int $limit = 180): string
|
||||
{
|
||||
return (string) Str::of(strip_tags((string) $this->text))->squish()->limit($limit);
|
||||
}
|
||||
|
||||
public function wordCount(): int
|
||||
{
|
||||
return str_word_count(strip_tags((string) $this->text));
|
||||
}
|
||||
|
||||
/**
|
||||
* Lesezeit in Minuten (200 Wörter/Minute, mindestens 1).
|
||||
*/
|
||||
public function readTimeMinutes(): int
|
||||
{
|
||||
return max(1, (int) round($this->wordCount() / 200));
|
||||
}
|
||||
|
||||
/**
|
||||
* Kompakter Zeitstempel der Kartenkomponenten: heute „HH:MM",
|
||||
* gestern lokalisiert, sonst Kurzdatum. Null ohne published_at.
|
||||
*/
|
||||
public function compactPublishedLabel(): ?string
|
||||
{
|
||||
$publishedAt = $this->published_at;
|
||||
|
||||
if (! $publishedAt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($publishedAt->isToday()) {
|
||||
return $publishedAt->format('H:i');
|
||||
}
|
||||
|
||||
if ($publishedAt->isYesterday()) {
|
||||
return __('web.shared.yesterday');
|
||||
}
|
||||
|
||||
return $publishedAt->isoFormat('D. MMM');
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
|
|
|
|||
|
|
@ -206,7 +206,6 @@ class HomePageData
|
|||
->limit(3)
|
||||
->get()
|
||||
->map(static function (PressRelease $release) use ($spotlightTranslation): array {
|
||||
$words = str_word_count(strip_tags((string) $release->text));
|
||||
$publishedAt = $release->published_at;
|
||||
|
||||
return [
|
||||
|
|
@ -216,7 +215,7 @@ class HomePageData
|
|||
'title' => $release->title,
|
||||
'company' => $release->company?->name ?? '',
|
||||
'city' => null,
|
||||
'minutes' => max(1, (int) ceil($words / 200)),
|
||||
'minutes' => $release->readTimeMinutes(),
|
||||
'href' => $release->slug ? route('release.detail', ['slug' => $release->slug]) : '#',
|
||||
];
|
||||
});
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ R2.2+R2.3 erledigt: Hub-Landing komplett ueberarbeitet (datengetriebenes Tarif-G
|
|||
|
||||
- [x] **R4.1 Tote Dateien entfernen** — Erledigt 03.07. (User-Freigabe): `web/examples/` (4 Demo-Views), `welcome`, `home` („B2IN"-Fragment), `brand-mark`-View (die gleichnamige, viel genutzte *Komponente* bleibt), `layouts/web-master-test`, altes engl. `layouts/header`+`footer` — alle per grep verifiziert (inkl. dynamischer Auflösung).
|
||||
- [x] **R4.2 Tote Komponenten entfernt** — Erledigt 03.07.: die 12 gelisteten `x-web`-Komponenten + 7 zusätzlich gefundene Chain-Dead-Komponenten (`main-navigation`, `breadcrumb`, `page-header`, `content-layout`, `article-card`, `section-header`, `sidebar-widget` — nur von examples/ bzw. anderen toten Kandidaten referenziert). Verwaiste `.highlights-slider`-CSS-Regeln mit entfernt, Web-Build gegengeprüft. Suite grün.
|
||||
- [ ] **R4.3 Dedup (optional, nice-to-have)** — Release-Card-Vereinheitlichung, Industrie-Icon-Map → Config, Inline-SVGs → Icon-Set, `<x-web.container>` (alt 6.2/6.3). Nur wenn Zeit; kein Release-Blocker.
|
||||
- [x] **R4.3 Dedup** — Erledigt 10.07. (angepasster Zuschnitt nach dem Frontend-Umbau): Die 5× kopierte Karten-Metadaten-Logik (Kategorie-Übersetzung mit Locale-Fallback, card-Bild-URL, Teaser, Lesezeit, Kompakt-Zeitstempel) in Präsentations-Helper am `PressRelease`-Model gezogen (`localizedCategoryName`, `cardImageUrl`, `teaserText`, `wordCount`, `readTimeMinutes`, `compactPublishedLabel`); feed-item, feed-top-item, focus-side-item, focus-hero, kategorie, newsroom, release-detail, veroeffentlichen + `HomePageData` umgestellt — Markup unverändert. Lesezeit-Formel vereinheitlicht (2 Stellen nutzten `ceil` statt `round`). Tests: `Web/PressReleaseCardHelpersTest`. **Bewusst gelassen:** Inline-SVGs → Icon-Set (max. 2–3 echte Dubletten in 13 Komponenten, hoher Churn im frischen User-Frontend, kein funktionaler Gewinn); Industrie-Icon-Map hatte der Umbau bereits erledigt.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
63
tests/Feature/Web/PressReleaseCardHelpersTest.php
Normal file
63
tests/Feature/Web/PressReleaseCardHelpersTest.php
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\Portal;
|
||||
use App\Models\Category;
|
||||
use App\Models\PressRelease;
|
||||
|
||||
test('localizedCategoryName resolves the current locale with fallback to the first translation', function () {
|
||||
$category = Category::factory()->create(['portal' => Portal::Businessportal24]);
|
||||
$category->translations()->create(['locale' => 'de', 'name' => 'Energie & Klima', 'slug' => 'energie-klima']);
|
||||
$category->translations()->create(['locale' => 'en', 'name' => 'Energy & Climate', 'slug' => 'energy-climate']);
|
||||
|
||||
$release = PressRelease::factory()->for($category)->create();
|
||||
$release->load('category.translations');
|
||||
|
||||
app()->setLocale('de');
|
||||
expect($release->localizedCategoryName())->toBe('Energie & Klima');
|
||||
|
||||
app()->setLocale('en');
|
||||
expect($release->localizedCategoryName())->toBe('Energy & Climate');
|
||||
|
||||
// Fallback: Sprache ohne Übersetzung → erste vorhandene.
|
||||
app()->setLocale('fr');
|
||||
expect($release->localizedCategoryName())->toBe('Energie & Klima');
|
||||
|
||||
app()->setLocale('de');
|
||||
});
|
||||
|
||||
test('teaserText strips tags, squishes whitespace and limits length', function () {
|
||||
$release = PressRelease::factory()->create([
|
||||
'text' => "<p>Erster Absatz.</p>\n<p>Zweiter Absatz mit <strong>Auszeichnung</strong>.</p>",
|
||||
]);
|
||||
|
||||
expect($release->teaserText())->toBe('Erster Absatz. Zweiter Absatz mit Auszeichnung.')
|
||||
->and($release->teaserText(13))->toBe('Erster Absatz...');
|
||||
});
|
||||
|
||||
test('readTimeMinutes rounds at 200 words per minute with a minimum of one', function () {
|
||||
$short = PressRelease::factory()->create(['text' => 'Nur wenige Worte.']);
|
||||
$long = PressRelease::factory()->create(['text' => implode(' ', array_fill(0, 450, 'Wort'))]);
|
||||
|
||||
expect($short->wordCount())->toBe(3)
|
||||
->and($short->readTimeMinutes())->toBe(1)
|
||||
->and($long->readTimeMinutes())->toBe(2);
|
||||
});
|
||||
|
||||
test('compactPublishedLabel formats today, yesterday and older dates', function () {
|
||||
$today = PressRelease::factory()->create(['published_at' => now()->setTime(9, 30)]);
|
||||
$yesterday = PressRelease::factory()->create(['published_at' => now()->subDay()]);
|
||||
$older = PressRelease::factory()->create(['published_at' => now()->parse('2026-03-05 08:00')]);
|
||||
$unpublished = PressRelease::factory()->create(['published_at' => null]);
|
||||
|
||||
expect($today->compactPublishedLabel())->toBe('09:30')
|
||||
->and($yesterday->compactPublishedLabel())->toBe(__('web.shared.yesterday'))
|
||||
->and($older->compactPublishedLabel())->toBe($older->published_at->isoFormat('D. MMM'))
|
||||
->and($unpublished->compactPublishedLabel())->toBeNull();
|
||||
});
|
||||
|
||||
test('cardImageUrl is null for releases without images', function () {
|
||||
$release = PressRelease::factory()->create();
|
||||
$release->load('images');
|
||||
|
||||
expect($release->cardImageUrl())->toBeNull();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue