Browse Source

bug-fixes

imwald
Silberengel 3 days ago
parent
commit
956ee1dbbc
  1. 174
      assets/styles/app.css
  2. 4
      src/Controller/DefaultController.php
  3. 137
      src/Service/MagazineContentService.php
  4. 71
      templates/components/Organisms/FeaturedList.html.twig
  5. 41
      templates/components/Organisms/FeaturedWall.html.twig
  6. 6
      templates/home.html.twig

174
assets/styles/app.css

@ -164,53 +164,96 @@ svg.icon { @@ -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 { @@ -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 { @@ -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 { @@ -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;

4
src/Controller/DefaultController.php

@ -22,8 +22,10 @@ class DefaultController extends AbstractController @@ -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),
]);
}

137
src/Service/MagazineContentService.php

@ -4,6 +4,7 @@ declare(strict_types=1); @@ -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 @@ -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<array<int, string>> $categoryATags
*
* @return list<array{article: FeaturedArticleCard, categoryTitle: string}>
*/
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<FeaturedArticleCard>}
*/
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;
}
}

71
templates/components/Organisms/FeaturedList.html.twig

@ -1,54 +1,39 @@ @@ -1,54 +1,39 @@
<div>
{% if list %}
<div class="featured-cat">
<small>{{ title }}</small>
</div>
<div {{ attributes }}>
<div>
{% set feature = list[0] %}
<div class="card">
<a href="{{ (feature.pubkey and npub_from_hex(feature.pubkey) != '') ? path('article', { npub: npub_from_hex(feature.pubkey), slug: feature.slug }) : path('article-legacy-redirect', { slug: feature.slug }) }}">
<div class="card-header">
<img src="{{ article_card_cover(feature.image, feature.pubkey) }}" alt="{{ ('Illustration for ' ~ feature.title)|e('html_attr') }}" width="1200" height="675" loading="lazy" decoding="async">
{% set _hue = (title|default('x')|length * 47) % 360 %}
<div
{{ attributes.defaults({ class: 'featured-list featured-list--wall' }) }}
role="region"
aria-label="{{ title|e('html_attr') }}"
>
{% for item in list %}
<article class="featured-tile" style="--tile-hue: {{ _hue }};">
<a
class="featured-tile__link"
href="{{ (item.pubkey and npub_from_hex(item.pubkey) != '') ? path('article', { npub: npub_from_hex(item.pubkey), slug: item.slug }) : path('article-legacy-redirect', { slug: item.slug }) }}"
>
<div class="featured-tile__head">
<span class="featured-tile__cat">{{ title }}</span>
</div>
<div class="featured-tile__media featured-tile__media--ar{{ loop.index0 % 4 }}">
<img
src="{{ article_card_cover(item.image, item.pubkey) }}"
alt="{{ ('Illustration for ' ~ item.title)|e('html_attr') }}"
width="1200"
height="675"
loading="lazy"
decoding="async"
>
</div>
<div class="card-body">
<h2 class="card-title">{{ feature.title }}</h2>
{% if feature.displayAt %}
<p class="featured-list__meta">
<time datetime="{{ feature.displayAt|date('c') }}">{{ feature.displayAt|date('F j, Y') }}</time>
</p>
{% endif %}
<h2 class="card-title">{{ item.title }}</h2>
<p class="lede truncate">
{{ feature.summary }}
{{ item.summary }}
</p>
</div>
</a>
</div>
</div>
<div>
{% for item in list %}
{% if item != feature %}
<div class="card">
<a href="{{ (item.pubkey and npub_from_hex(item.pubkey) != '') ? path('article', { npub: npub_from_hex(item.pubkey), slug: item.slug }) : path('article-legacy-redirect', { slug: item.slug }) }}">
<div class="card-header">
<img src="{{ article_card_cover(item.image, item.pubkey) }}" alt="{{ ('Illustration for ' ~ item.title)|e('html_attr') }}" width="1200" height="675" loading="lazy" decoding="async">
</div>
<div class="card-body">
<h2 class="card-title">{{ item.title }}</h2>
{% if item.displayAt %}
<p class="featured-list__meta">
<time datetime="{{ item.displayAt|date('c') }}">{{ item.displayAt|date('F j, Y') }}</time>
</p>
{% endif %}
<p class="lede truncate">
{{ item.summary }}
</p>
</div>
</a>
</div>
{% endif %}
{% endfor %}
</div>
</article>
{% endfor %}
</div>
{% endif %}
</div>

41
templates/components/Organisms/FeaturedWall.html.twig

@ -0,0 +1,41 @@ @@ -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 %}
<div
class="featured-list featured-list--wall"
role="region"
aria-label="{{ (website_name ~ ' — featured articles')|e('html_attr') }}"
>
{% for tile in tiles %}
{% set item = tile.article %}
{% set _hue = (tile.categoryTitle|default('x')|length * 47) % 360 %}
<article class="featured-tile" style="--tile-hue: {{ _hue }};">
<a
class="featured-tile__link"
href="{{ (item.pubkey and npub_from_hex(item.pubkey) != '') ? path('article', { npub: npub_from_hex(item.pubkey), slug: item.slug }) : path('article-legacy-redirect', { slug: item.slug }) }}"
>
<div class="featured-tile__head">
<span class="featured-tile__cat">{{ tile.categoryTitle }}</span>
</div>
<div class="featured-tile__media featured-tile__media--ar{{ loop.index0 % 4 }}">
<img
src="{{ article_card_cover(item.image, item.pubkey) }}"
alt="{{ ('Illustration for ' ~ item.title)|e('html_attr') }}"
width="1200"
height="675"
loading="lazy"
decoding="async"
>
</div>
<div class="card-body">
<h2 class="card-title">{{ item.title }}</h2>
<p class="lede truncate">
{{ item.summary }}
</p>
</div>
</a>
</article>
{% endfor %}
</div>
{% endif %}

6
templates/home.html.twig

@ -26,10 +26,8 @@ @@ -26,10 +26,8 @@
{% endblock %}
{% block body %}
<div class="home-body">
{% for item in indices %}
<twig:Organisms:FeaturedList :category="item" class="featured-list"/>
{% endfor %}
<div class="home-body home-body--wall">
{% include 'components/Organisms/FeaturedWall.html.twig' with { tiles: home_featured_tiles|default([]) } only %}
</div>
{% endblock %}

Loading…
Cancel
Save