From 956ee1dbbc686b641bb2e97284392f3a2257c958 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 25 Apr 2026 21:29:09 +0200 Subject: [PATCH] bug-fixes --- assets/styles/app.css | 174 ++++++++++-------- src/Controller/DefaultController.php | 4 +- src/Service/MagazineContentService.php | 137 ++++++++++++++ .../Organisms/FeaturedList.html.twig | 71 +++---- .../Organisms/FeaturedWall.html.twig | 41 +++++ templates/home.html.twig | 6 +- 6 files changed, 311 insertions(+), 122 deletions(-) create mode 100644 templates/components/Organisms/FeaturedWall.html.twig diff --git a/assets/styles/app.css b/assets/styles/app.css index cfe1d80..d0a6b13 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -164,53 +164,96 @@ svg.icon { border-top: 1px solid var(--color-border); } -.featured-cat { - border-bottom: 1px solid var(--color-border); - padding: 0 0 0.75rem 10px; - margin-bottom: 1.25rem; +/* Home featured sections: column masonry (Pinterest-style wall) + per-tile category label */ +.featured-list--wall { + column-count: 1; + column-gap: 1.15rem; + column-fill: balance; } -.featured-cat small { - display: block; - font-size: 0.72rem; - font-weight: 600; - letter-spacing: 0.06em; - text-transform: uppercase; - color: color-mix(in srgb, var(--color-text-mid) 55%, var(--color-bg) 45%); - line-height: 1.35; +@media (min-width: 640px) { + .featured-list--wall { + column-count: 2; + } } -.featured-list { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - align-items: flex-start; - gap: 1.25rem; +@media (min-width: 1100px) { + .featured-list--wall { + column-count: 3; + } } -.featured-list > * { +.featured-tile { + --tile-hue: 140; + break-inside: avoid; + display: block; + width: 100%; + margin: 0 0 1.15rem; box-sizing: border-box; - margin: 0; - padding: 0; min-width: 0; + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: 0.45rem; + box-shadow: 0 1px 0 color-mix(in srgb, var(--color-text) 6%, transparent); + border-top: 3px solid hsl(var(--tile-hue) 38% 40%); + overflow: hidden; } -/* Uniform text padding for lead + side cards; image is full-bleed above. */ -.featured-list .card-body { - box-sizing: border-box; - padding: 1rem 1.125rem 1.2rem; +.featured-tile__link { + display: block; + color: inherit; + text-decoration: none; +} + +.featured-tile__link:focus-visible { + outline: 2px solid var(--color-focus-ring); + outline-offset: 2px; + border-radius: 0.35rem; +} + +.featured-tile__head { + padding: 0.4rem 0.75rem 0.45rem; + background: color-mix(in srgb, hsl(var(--tile-hue) 34% 46%) 14%, var(--color-bg) 86%); + border-bottom: 1px solid color-mix(in srgb, hsl(var(--tile-hue) 32% 38%) 24%, var(--color-border) 76%); } -/* 16:9 cover frame (lead + stacked side cards in FeaturedList). */ -.featured-list .card > a > .card-header { +.featured-tile__cat { + display: block; + font-family: var(--font-family), sans-serif; + font-size: 0.66rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + color: hsl(var(--tile-hue) 26% 32%); + line-height: 1.35; +} + +.featured-tile__media { + position: relative; margin: 0; width: 100%; - aspect-ratio: 16 / 9; overflow: hidden; background-color: var(--color-bg-light); } -.featured-list .card > a > .card-header img { +/* Vary aspect ratios for a more irregular “wall of blocks” rhythm */ +.featured-tile__media--ar0 { + aspect-ratio: 16 / 9; +} + +.featured-tile__media--ar1 { + aspect-ratio: 1 / 1; +} + +.featured-tile__media--ar2 { + aspect-ratio: 3 / 2; +} + +.featured-tile__media--ar3 { + aspect-ratio: 3 / 4; +} + +.featured-tile__media img { width: 100%; height: 100%; object-fit: cover; @@ -218,45 +261,17 @@ svg.icon { display: block; } -/* Site logo fallback (small square asset in a wide frame) */ -.featured-list .card > a > .card-header img[src*="favicon-96x96"] { +/* Site logo fallback in wide frames */ +.featured-tile__media img[src*="favicon-96x96"] { object-fit: contain; padding: 2.25rem; box-sizing: border-box; } -@media (max-width: 1024px) { - .featured-list { - flex-direction: column !important; - gap: 1.5rem; - } - - .featured-list > div:first-child, - .featured-list > div:last-child { - flex: 1 1 auto; - width: 100%; - } -} -div:nth-child(odd) .featured-list { - flex-direction: row-reverse; -} - -/* Only the two column wrappers — not every .card that happens to be :first-child/:last-child of its parent */ -.featured-list > div:first-child { - flex: 0 0 66%; - min-width: 0; -} - -.featured-list > div:last-child { - flex: 0 0 34%; - min-width: 0; -} - -/* Each column: vertical rhythm between hero / stacked cards (replaces ad-hoc card margins). */ -.featured-list > div { - display: flex; - flex-direction: column; - gap: 0.85rem; +/* Uniform text padding (home tiles + shared with list cards elsewhere) */ +.featured-list .card-body { + box-sizing: border-box; + padding: 1rem 1.125rem 1.2rem; } /* List card titles + excerpts: home (featured), category, search, author */ @@ -297,6 +312,25 @@ div:nth-child(odd) .featured-list { line-clamp: 2; } +/* Masonry wall (home): smaller titles, more title + excerpt lines (must follow generic .featured-list rules) */ +.featured-list.featured-list--wall h2.card-title { + font-size: 1.35rem; + font-weight: 600; + line-height: 1.3; + -webkit-line-clamp: 4; + line-clamp: 4; + min-height: 0; + margin-bottom: 0.35rem; +} + +.featured-list.featured-list--wall p.lede.truncate { + -webkit-line-clamp: 10; + line-clamp: 10; + font-size: 1.02rem; + line-height: 1.5; + margin-top: 0.35rem; +} + .featured-list__meta { font-family: var(--font-family), sans-serif; font-size: 0.78rem; @@ -310,25 +344,17 @@ div:nth-child(odd) .featured-list { color: inherit; } -/* Whole-card link is `color: inherit`; keep excerpt + date subdued on hover. */ -.featured-list .card a:hover p.lede, +/* Whole-card link: keep excerpt + date subdued on hover. */ +.featured-tile__link:hover p.lede, .article-list .card a:hover p.lede { color: color-mix(in srgb, var(--color-text-mid) 88%, var(--color-text) 12%); text-decoration: none; } -.featured-list .card a:hover .featured-list__meta { +.featured-tile__link:hover .featured-list__meta { color: color-mix(in srgb, var(--color-text-mid) 58%, var(--color-text) 42%); } -.featured-list .card { - margin: 0; - border: 1px solid var(--color-border); - box-sizing: border-box; - min-width: 0; - background: var(--color-bg); -} - .article-list .metadata { display: flex; flex-direction: row; diff --git a/src/Controller/DefaultController.php b/src/Controller/DefaultController.php index 951d872..aead353 100644 --- a/src/Controller/DefaultController.php +++ b/src/Controller/DefaultController.php @@ -22,8 +22,10 @@ class DefaultController extends AbstractController #[Route('/', name: 'home')] public function index(): Response { + $categoryATags = $this->magazineContent->getHomeCategoryAIndexTagsFromStoreOnly(); + return $this->render('home.html.twig', [ - 'indices' => $this->magazineContent->getHomeCategoryAIndexTagsFromStoreOnly(), + 'home_featured_tiles' => $this->magazineContent->buildHomeMixedFeaturedWallTiles($categoryATags), ]); } diff --git a/src/Service/MagazineContentService.php b/src/Service/MagazineContentService.php index 4faaa0f..7eb71b2 100644 --- a/src/Service/MagazineContentService.php +++ b/src/Service/MagazineContentService.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Service; +use App\Dto\FeaturedArticleCard; use App\Entity\Article; use App\Entity\Event; use App\Enum\EventStatusEnum; @@ -629,4 +630,140 @@ final class MagazineContentService } catch (\Throwable) { } } + + /** + * Interleaves up to four articles per home category in round-robin order (one “wall” mixing all topics). + * Duplicate slugs across categories are skipped so each article appears at most once. + * + * @param list> $categoryATags + * + * @return list + */ + public function buildHomeMixedFeaturedWallTiles(array $categoryATags): array + { + $blocks = []; + foreach ($categoryATags as $row) { + $coord = $row[1] ?? ''; + if (!\is_string($coord) || $coord === '') { + continue; + } + $b = $this->buildCategoryFeaturedBlock($coord); + if ($b !== null && $b['cards'] !== []) { + $blocks[] = $b; + } + } + if ($blocks === []) { + return []; + } + + $pointers = array_fill(0, \count($blocks), 0); + $seenSlugs = []; + $out = []; + + while (true) { + $roundAdded = false; + for ($i = 0, $n = \count($blocks); $i < $n; ++$i) { + while (isset($blocks[$i]['cards'][$pointers[$i]])) { + $card = $blocks[$i]['cards'][$pointers[$i]]; + $slug = \trim((string) $card->getSlug()); + if ($slug !== '' && isset($seenSlugs[$slug])) { + ++$pointers[$i]; + continue; + } + if ($slug !== '') { + $seenSlugs[$slug] = true; + } + $out[] = [ + 'article' => $card, + 'categoryTitle' => $blocks[$i]['title'], + ]; + ++$pointers[$i]; + $roundAdded = true; + break; + } + } + if (!$roundAdded) { + break; + } + } + + return $out; + } + + /** + * Same resolution as {@see \App\Twig\Components\Organisms\FeaturedList} (4 cards per category). + * + * @return null|array{title: string, cards: list} + */ + private function buildCategoryFeaturedBlock(string $categoryCoord): ?array + { + $parts = explode(':', $categoryCoord, 3); + if (\count($parts) < 3) { + return null; + } + $slug = $parts[2]; + $catIndex = $this->store->getCategory($slug); + if (!\is_object($catIndex) || !\method_exists($catIndex, 'getTags')) { + return null; + } + + $title = ''; + $slugs = []; + foreach ($catIndex->getTags() as $tag) { + if (($tag[0] ?? null) === 'title' && isset($tag[1])) { + $title = (string) $tag[1]; + } + if (($tag[0] ?? null) === 'a' && isset($tag[1])) { + $segs = explode(':', (string) $tag[1], 3); + $slugs[] = \trim((string) end($segs)); + if (\count($slugs) >= 5) { + break; + } + } + } + + if ($title === '') { + $title = $slug; + } + if ($slugs === []) { + return null; + } + + $articles = $this->articleRepository->findFeaturedCardsBySlugs($slugs); + $slugMap = []; + foreach ($articles as $article) { + $articleSlug = \trim((string) $article->getSlug()); + if ($articleSlug !== '') { + if (!isset($slugMap[$articleSlug])) { + $slugMap[$articleSlug] = $article; + } elseif ($this->featuredCardIsNewer($article, $slugMap[$articleSlug])) { + $slugMap[$articleSlug] = $article; + } + } + } + $orderedList = []; + foreach ($slugs as $articleSlug) { + $articleSlug = \trim((string) $articleSlug); + if ($articleSlug !== '' && isset($slugMap[$articleSlug])) { + $orderedList[] = $slugMap[$articleSlug]; + } + } + $cards = \array_slice($orderedList, 0, 4); + + return ['title' => $title, 'cards' => $cards]; + } + + private function featuredCardIsNewer(FeaturedArticleCard $a, FeaturedArticleCard $b): bool + { + $ca = $a->getDisplayAt(); + $cb = $b->getDisplayAt(); + if ($ca === null) { + return false; + } + if ($cb === null) { + return true; + } + + return $ca > $cb; + } } diff --git a/templates/components/Organisms/FeaturedList.html.twig b/templates/components/Organisms/FeaturedList.html.twig index da5f8d1..6e3c851 100644 --- a/templates/components/Organisms/FeaturedList.html.twig +++ b/templates/components/Organisms/FeaturedList.html.twig @@ -1,54 +1,39 @@
{% if list %} - -
- diff --git a/templates/components/Organisms/FeaturedWall.html.twig b/templates/components/Organisms/FeaturedWall.html.twig new file mode 100644 index 0000000..f27580d --- /dev/null +++ b/templates/components/Organisms/FeaturedWall.html.twig @@ -0,0 +1,41 @@ +{# + Single masonry wall: mixed categories (round-robin), same tile styling as the former per-section list. +#} +{% if tiles is not empty %} + +{% endif %} diff --git a/templates/home.html.twig b/templates/home.html.twig index 7952e63..adb58ba 100644 --- a/templates/home.html.twig +++ b/templates/home.html.twig @@ -26,10 +26,8 @@ {% endblock %} {% block body %} -
- {% for item in indices %} - - {% endfor %} +
+ {% include 'components/Organisms/FeaturedWall.html.twig' with { tiles: home_featured_tiles|default([]) } only %}
{% endblock %}