Skip to content

Configurable event image layout (closes #137)#139

Open
fabiodalez-dev wants to merge 5 commits into
mainfrom
feat/event-image-layout-setting
Open

Configurable event image layout (closes #137)#139
fabiodalez-dev wants to merge 5 commits into
mainfrom
feat/event-image-layout-setting

Conversation

@fabiodalez-dev
Copy link
Copy Markdown
Owner

Summary

Closes #137.

Adds an admin-controlled layout setting for the featured image on the public event detail page (/eventi/<slug>). The previous CSS rendered every uploaded image at 100% container width with no height constraint, so a 1791×927 cover (the case @HansUwe52 reported) dominated the viewport.

A new setting cms.event_image_layout lives in the existing CMS settings tab. Four presets, in increasing visual footprint:

Preset Effective size Use case
Piccola a sinistra (default) max 420px wide, left-aligned, max 320px tall Small inline poster — solves the original complaint
Miniatura affiancata al testo 240px wide, side-by-side with body text via CSS grid Side thumbnail; mobile collapses to vertical stack
Banner basso a tutta larghezza 100% wide, capped at 220px tall Low decorative strip
Originale a tutta larghezza 100% wide, natural height Legacy behaviour (opt-in)

All four labels are translated in it_IT, en_US, de_DE, fr_FR in the same commit (per i18n policy).

Architecture

  • Storage: KV row in system_settings (category=cms, key=event_image_layout). No migration needed.
  • Form: rendered inside the existing 'Eventi' card on the CMS settings tab, no new tab to discover.
  • Route: POST /admin/settings/events (CSRF + AdminAuth middleware), pattern identical to updateLabels/updateSharingSettings.
  • Frontend: controller validates the value against an allow-list before passing it to the view; the view re-narrows once more as defence in depth so a future controller refactor cannot leak an unknown layout into the DOM.

Bonus fix in the same branch

While reviewing the events admin form a separate bug was found (last commit): when the admin submits the edit form with BOTH a new featured_image upload AND remove_image=1 checked, the previous if/elseif evaluated remove_image first and silently discarded the upload. The branch flips the priority (upload wins) and adds best-effort cleanup of the orphaned file from public/uploads/events/, path-traversal-safe.

Test plan

8 cases in tests/issue-137-event-image-layout.spec.js, all passing:

  • Default fallback: when cms.event_image_layout is unset, the page renders event-cover--contained
  • Explicit fullevent-cover--full class + data-event-cover-layout='full'
  • Explicit bannerevent-cover--banner
  • Explicit containedevent-cover--contained
  • Explicit thumbevent-cover--thumb
  • Effective size regression: measures rendered bounding boxes per preset and asserts contained.width ≤ 420px AND < 70% card.width, thumb.width ≤ 245px, banner.height ≤ 225px, full.width > 85% card.width. Also asserts contained and thumb are left-aligned (figure.x within 100px of card.x).
  • Float-overflow regression (thumb layout): with very short body content, asserts figure.bottom ≤ card.bottom — the invariant the original float: right design violated.
  • Replace-while-removing regression: drives the actual admin form (login → tick remove checkbox → attach a new file → submit) and asserts the DB row points at a new upload path matching handleImageUpload()'s naming scheme, not NULL and not the original.

Run locally:

E2E_ADMIN_PASS='...' /tmp/run-e2e.sh tests/issue-137-event-image-layout.spec.js \
  --config=tests/playwright.config.js --workers=1

PHPStan: clean on the 5 PHP files touched (pre-existing storage/plugins/{archives,bibframe-linked-data,discogs}/... warnings on main remain at 11 and are out of scope here).

Issue #137 (HansUwe52): the featured image on the public event detail
page renders at 100% container width with no max-height constraint, so
sproportioned uploads (e.g. a 1791x927 book cover used as an event image)
dominate the viewport.

Adds a CMS-tab setting 'cms.event_image_layout' with four options:

  - contained  (default)  max-height: clamp(280px, 50vw, 480px) +
                          object-fit: contain. Solves the reported case
                          while preserving the previous look for
                          reasonably-sized images.
  - banner                aspect-ratio: 16/9 + object-fit: cover. Crops
                          to a uniform hero banner.
  - full                  Legacy 100%-width-no-constraint behaviour, for
                          users who depended on it.
  - thumb                 Right-floated 3:4 portrait thumbnail that wraps
                          the event body text. Collapses to full-width
                          on viewports under 640px.

The setting lives in the existing system_settings KV store (no schema
migration needed) and is wired through the existing pattern used by
'updateLabels' / 'updateSharingSettings':

  - SettingsController::updateEventSettings (validated allow-list)
  - resolveEventSettings() exposed to the settings view
  - POST /admin/settings/events route protected by CsrfMiddleware +
    AdminAuthMiddleware
  - select rendered inside the existing 'Eventi' card on the CMS tab,
    so the admin doesn't have to discover a new tab

FrontendController::event() reads and re-validates the setting against
the same allow-list before passing it to the view; event-detail.php
narrows once more before composing the CSS class, so a future
controller refactor cannot leak an unknown layout into the DOM.

i18n: 8 new keys added to all four locale files (it_IT/en_US/de_DE/fr_FR)
in the same commit, per project policy.

Tests: tests/issue-137-event-image-layout.spec.js — 5 cases covering
the four explicit layouts plus the default fallback. Each test mutates
cms.event_image_layout in the KV store directly, requests the public
event page, and asserts the figure carries the matching CSS modifier
and data-event-cover-layout attribute. All 5 pass.

PHPStan: clean (no new findings; pre-existing storage/plugins issues
unchanged at 11).
Self-review feedback on the initial layout palette:

  - 'contained' was using an arbitrary max-height: 480px that produced
    uneven silhouettes depending on the source aspect ratio. Replaced
    with aspect-ratio: 3/2 + object-fit: cover so the cover always
    lands on a predictable photographic crop.

  - 'thumb' was using CSS float: right, which caused the figure to
    escape the parent .event-card whenever the body content was short
    (e.g. a one-paragraph event) — visually the image then overlapped
    the page footer. Refactored to a CSS grid layout driven by a new
    .event-card--thumb-layout modifier on the parent article: the
    figure now lives in its own column/row and is geometrically
    constrained inside the card. On viewports below 768px the grid
    collapses to a vertical stack.

  - Bonus on desktop: the side-thumbnail sticks while the reader
    scrolls long event descriptions (position: sticky).

UI labels updated to match the new design ('Foto 3:2' instead of
'Adattato (max 480px)') in all four locale files.

Tests: added a 6th case to tests/issue-137-event-image-layout.spec.js
that creates a very short event content and asserts
figure.bottom <= card.bottom at desktop width — the exact invariant
the float bug used to violate. All 6 cases pass.
Previous iterations all rendered at 100% container width and only
differed by aspect-ratio, so the original complaint (a 1791x927 image
dominating the event page) was still visible in three of four presets.

The whole point of the setting is to LIMIT the image footprint.
Resized accordingly:

  full       100%  x  auto       legacy enormous (opt-in)
  banner     100%  x  220px max  low decorative strip
  contained  420px max, centred  small poster (DEFAULT, solves
                                  the reported case directly)
  thumb      240px wide          side thumbnail next to body text

Source aspect ratio is preserved on contained/thumb-mobile — shrinking
the footprint is the goal, forcing crops would just trade one issue
for another.

UI labels rewritten so the size is explicit ('Piccola centrata
(max 420px)', 'Miniatura affiancata al testo (240px)', etc.) and
the librarian can pick at a glance. All 4 locale files updated.

Tests: added a measurement-based regression that asserts each preset
renders at a distinct, capped size:

  contained.width  <= 420px AND < 70% of card.width
  thumb.width      <= 245px
  banner.height    <= 225px
  full.width       > 85% of card.width

Catches the whole class of regression where four CSS modifiers exist
but render identically. All 7 tests pass.
User feedback: small images should sit on the left, not centred.

  - contained:  margin-left: 0 (was: margin-left/right: auto)
  - thumb on mobile: margin: 0 0 1.5rem 0 (was: 0 auto 1.5rem auto)

Small images centred in a wide card read as a 'centred hero' which is
the same visual problem we are trying to escape: the image dominates
its own row. Left-aligned, the image becomes part of the body flow.

UI label updated: 'Piccola a sinistra (max 420px)' replaces 'Piccola
centrata (max 420px)'. All four locale files updated.

Test guard added: the effective-size test now also asserts that
figure.x is within 100px of card.x (i.e. only inner padding) for both
'contained' and 'thumb'. Catches any future refactor that
reintroduces margin: auto. All 7 cases pass.
The edit form has separate controls: a 'remove_image' checkbox and a
'featured_image' file input. When the user ticks both (replace the
old image AND clear the slot in one submit), the existing if/elseif
processed 'remove' first and skipped the upload branch — the new
file was discarded and featured_image ended up NULL.

Fix:

  - Invert the priority: handle the file upload first; only run the
    remove path when no upload was submitted. This matches user
    intent (a new file always means 'replace').

  - Look up the previous featured_image before the UPDATE and unlink
    the orphan file from disk after the row is updated. Path-
    traversal-safe via realpath() + base-directory containment, the
    same contract handleImageUpload() upholds when writing.

Test: tests/issue-137-event-image-layout.spec.js — added an
end-to-end case that drives the actual admin form (login, edit
event, tick the remove checkbox, attach a new file, submit) and
asserts the DB row points at a NEW upload path (regex matches
handleImageUpload()'s naming scheme) — not NULL, not the old
sentinel. Catches the regression at the controller layer where the
historic bug lived. All 8 cases pass.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

Warning

Rate limit exceeded

@fabiodalez-dev has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 3 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8023bc9f-5ce7-4819-bbdb-91c10e586525

📥 Commits

Reviewing files that changed from the base of the PR and between 8aaba9e and ec19d2d.

📒 Files selected for processing (11)
  • app/Controllers/EventsController.php
  • app/Controllers/FrontendController.php
  • app/Controllers/SettingsController.php
  • app/Routes/web.php
  • app/Views/frontend/event-detail.php
  • app/Views/settings/index.php
  • locale/de_DE.json
  • locale/en_US.json
  • locale/fr_FR.json
  • locale/it_IT.json
  • tests/issue-137-event-image-layout.spec.js
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/event-image-layout-setting

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

// Reset content to a long enough body so 'banner' / 'contained'
// have something below them to measure against.
const longContent = '<p>' + 'Test event description. '.repeat(40) + '</p>';
const sqlSafeLong = longContent.replace(/'/g, "\\'");

// Shrink the event content so a CSS float would expose the bug.
const shortContent = '<p>Breve.</p>';
const sqlSafe = shortContent.replace(/'/g, "\\'");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants