Skip to content

feat(trustlab): add image lightbox preview to HorizontalGallery#1432

Open
koechkevin wants to merge 8 commits intomainfrom
bugfix/date_description_images_trustlab
Open

feat(trustlab): add image lightbox preview to HorizontalGallery#1432
koechkevin wants to merge 8 commits intomainfrom
bugfix/date_description_images_trustlab

Conversation

@koechkevin
Copy link
Copy Markdown
Contributor

@koechkevin koechkevin commented Mar 26, 2026

Description

This PR covers three related bug fixes and feature enhancements to the TrustLab app's opportunities and gallery
UX.

  1. Gallery image lightbox preview
  • Added a new ImageLightbox component that opens a full-screen modal when a gallery image is clicked from
    HorizontalGallery
  • Supports previous/next navigation via on-screen arrow buttons or keyboard (← / → / Escape)
  • Shows an image counter (n / total) when the gallery has more than one image
  • Clicking the backdrop closes the lightbox
  • Gallery thumbnails now show a subtle scale + shadow on hover and a pointer cursor to signal interactivity
  • Full unit test suite covers: open/close state, navigation boundaries, counter visibility, keyboard events,
    backdrop click, and fallback to src when url is absent
  1. Opportunity date fix
  • Opportunity cards were displaying createdAt (the Payload record creation timestamp) instead of an explicit
    event date
  • Added a dedicated date field to the Opportunities collection (sidebar date picker, day-only format)
  • getOpportunities now reads doc.date instead of doc.createdAt, so cards show the correct event date
  • Also removed a stray console.log left in getOpportunities
  1. OpportunityList title & description
  • Added optional title and description (Lexical rich text) props to the OpportunityList component, rendered above
    the filter bar
  • Wired both fields through the OpportunityList Payload block definition so editors can populate them from the
    CMS
  • Reduced section top padding (py: 8 → py: 4) to tighten the layout when a header is present

Test plan

  • Open a page containing HorizontalGallery with images — clicking a thumbnail should open the lightbox at the
    correct image
  • Verify keyboard navigation (arrow keys) and Escape close work in the lightbox
  • Confirm the image counter shows n / total for multi-image galleries and is hidden for single-image galleries
  • In the CMS, create/edit an Opportunity and set the new date field — confirm opportunity cards render the
    correct date
  • In the CMS, add a title and description to an OpportunityList block — confirm they render above the filter bar
    on the front end
  • Run unit tests: pnpm jest --filter=trustlab

Fixes # (issue)

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

Screenshots

image

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation

Kevin Koech added 2 commits March 26, 2026 15:52
  Adds a full-screen lightbox preview when clicking images in the HorizontalGallery component.

  - New ImageLightbox component built on MUI Modal with previous/next navigation buttons, image counter, and
  keyboard support (Arrow keys + Escape)
  - Clicking any gallery image opens the lightbox at the correct index, calculated from chunk position
  - Gallery thumbnails now show a subtle scale + shadow hover effect and a pointer cursor to signal interactivity
  - Full test suite for ImageLightbox covering open/close state, navigation bounds, counter visibility, keyboard
  events, and backdrop click
@koechkevin koechkevin self-assigned this Mar 26, 2026
@koechkevin koechkevin requested review from a team and kelvinkipruto March 26, 2026 13:32
@kilemensi
Copy link
Copy Markdown
Member

kilemensi commented Mar 26, 2026

Where are the screenshots @koechkevin ? Ignore. Seems like one screenshot is included just slow to load on my end

@kilemensi

This comment was marked as outdated.

chatgpt-codex-connector[bot]

This comment was marked as off-topic.

Copy link
Copy Markdown
Member

@kilemensi kilemensi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏽


It seems there is an invisible div around the image. The Lightbox closes only when I click outside this invisible div.

Image

Lets also make the lightbox slightly darker so the image is more easily distinguishable from the background content on the page.


From Claude:

Issues

Bug — Invalid MUI sx background value (OpportunityList.js:149)

background: "common.white" is not a valid MUI sx shorthand. MUI theme tokens work with bgcolor (shorthand) or
backgroundColor, not background. This will render as a literal CSS string "common.white" instead of the theme color.
Should be:

sx={{ bgcolor: "common.white", px: { xs: 2.5, md: 0 } }}

Accessibility — aria-labelledby has no matching element (ImageLightbox.js:65)

<Modal aria-labelledby="image-lightbox" ...>

There's no element in the component with id="image-lightbox". This breaks the ARIA association. Either add an id to a
title element (e.g. a visually-hidden heading), or switch to aria-label:

<Modal aria-label="Image lightbox" ...>

Minor — Redundant backdrop close logic

The Modal already calls onClose on outside-click via its built-in backdrop. The inner Box also has handleBackdropClick
doing the same check. This results in onClose being called twice on backdrop click (harmless since it just sets state
to false, but worth cleaning up).

@koechkevin koechkevin requested a review from kilemensi March 27, 2026 08:34
@kilemensi
Copy link
Copy Markdown
Member

As users, we must be able to see the full image when clicked. Currently, the image is cut off:

Screenshot 2026-03-27 at 11 54 20

…nd adjust styling for better responsiveness
@kilemensi kilemensi requested a review from a team March 27, 2026 12:55
@kilemensi
Copy link
Copy Markdown
Member

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b4c69a39c0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

caption: doc.caption,
location: doc.location,
date: doc.createdAt ? formatDate(doc.createdAt, "dd-MM-yyyy") : null,
date: doc.date ? formatDate(doc.date, "dd-MM-yyyy") : null,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Align opportunity filters with the new event date field

This change switches the card date source to doc.date, but the year/month predicates in getOpportunities still filter on createdAt (the where.and conditions above), so results are now inconsistent whenever event date and record creation date differ. In practice, users can select a month/year and get opportunities whose displayed dates fall outside that filter window. The filter conditions should use the same date field now used for rendering.

Useful? React with 👍 / 👎.

: {
href: orgData.website?.url ?? null,
...orgData.website,
href: orgData?.link?.href ?? null,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve legacy organization links during field rename

The mapper now reads only orgData.link?.href for non-card variants, but the schema previously stored this under website and this commit also renames that field without a migration. Existing organization records will therefore resolve to null links and render non-clickable chips/cards until every record is manually backfilled. Keep a backward-compatible fallback (e.g. legacy website value) or add a migration before removing the old path.

Useful? React with 👍 / 👎.

@kilemensi
Copy link
Copy Markdown
Member

kilemensi commented Mar 27, 2026

Can you also make the year filter on Barazas sorted descending i.e. 2026 > 2025 > 2024 etc... and I hope these years are editable in the CMS.

Screenshot 2026-03-27 at 16 05 10

@kilemensi
Copy link
Copy Markdown
Member

@claude review

@claude
Copy link
Copy Markdown

claude bot commented Mar 27, 2026

Claude finished @kilemensi's task in 2m 45s —— View job


Code Review

  • Gathering context
  • Reading changed files
  • Analyzing code
  • Posting review

Overall this is a well-structured PR with good test coverage for ImageLightbox. There are a handful of bugs and consistency issues that need addressing before merge.


Bugs

1. Image cut-off in lightbox (ImageLightbox.js:141-168)

The image container Box has no explicit width, so maxWidth: "100%" on the image has nothing to constrain against. When fill is false (images with explicit dimensions), the image renders at its natural size and the container can overflow.

// Current — no width on container:
<Box onClick={(e) => e.stopPropagation()} sx={{ display: "flex", ... }}>

// Fix — add a width constraint matching the Figure's maxWidth:
<Box
  onClick={(e) => e.stopPropagation()}
  sx={{ display: "flex", maxWidth: "75vw", maxHeight: "75vh" }}
>

Fix this →

2. Filter queries createdAt but displays date (getOpportunities.js:44-84)

The year/month filter conditions still filter on createdAt, but card rendering now shows doc.date. Users can select a year filter and get results where the displayed date falls outside the selected range.

// Lines 47-56, 74-83 — should use "date" field instead of "createdAt":
andConditions.push({ createdAt: { greater_than_equal: startOfYear } }); // ← should be "date"
andConditions.push({ createdAt: { less_than_equal: endOfYear } });       // ← should be "date"

Also, the default sort (sort = "-createdAt" on line 20) should arguably be -date now.

Fix this →

3. Missing createdAt fallback for existing records (getOpportunities.js:131)

Existing opportunities that predate the new optional date field will render with no date until manually backfilled. This is a regression.

// Current:
date: doc.date ? formatDate(doc.date, "dd-MM-yyyy") : null,

// Fix:
date: doc.date ?? doc.createdAt
  ? formatDate(doc.date ?? doc.createdAt, "dd-MM-yyyy")
  : null,

Fix this →

4. Double onClose trigger on backdrop click (ImageLightbox.js:55-66)

MUI's Modal already calls onClose when clicking its built-in backdrop. The inner Box also has handleBackdropClick calling onClose. This fires onClose twice per backdrop click. The inner backdrop Box is redundant — remove handleBackdropClick and rely on the Modal's built-in behaviour.

Fix this →


Accessibility

5. aria-labelledby references a non-existent element (ImageLightbox.js:55)

<Modal aria-labelledby="image-lightbox" ...>

No element has id="image-lightbox", breaking the ARIA association. Switch to aria-label:

<Modal ...>

Fix this →


UX / Consistency

6. Year filter not sorted descending (Filters.js:18-22)

As noted in the comments, the year filter shows oldest first (2020 → 2026). Reverse it:

const generateYearOptions = () => {
  const currentYear = new Date().getFullYear();
  const len = currentYear - 2020 + 1;
  return Array.from({ length: len }, (_, i) => currentYear - i); // descending
};

Fix this →

7. Hardcoded hex values instead of theme tokens (OpportunityList.js:147, 189, 191)

background: "#fff" is used three times. Prefer bgcolor: "common.white" to stay consistent with the rest of the file and respect any theming changes.


Potential Regression

8. Organization link path may not match schema (getParticipatingOrganizationListBlock.js:44)

The mapper now reads orgData?.link?.href, but Organisations.js uses the linkGroup() helper from @commons-ui/payload without a name override — linkGroup typically produces a links array, not a single link object. Verify the resolved field shape from Payload and confirm this path is correct, otherwise existing org records will have null links.


Positive notes

  • Test suite for ImageLightbox is comprehensive — covers open/close, navigation boundaries, keyboard events, counter visibility, and the src fallback.
  • The new date field in Opportunities.js is cleanly defined with the right admin config.
  • The OpportunityList block correctly wires title and description through the Payload block schema.
  • handleLightboxNext correctly uses images.length - 1 as the upper bound (line 61 of HorizontalGallery.js).

@koechkevin
Copy link
Copy Markdown
Contributor Author

Can you also make the year filter on Barazas sorted descending i.e. 2026 > 2025 > 2024 etc... and I hope these years are editable in the CMS.

Screenshot 2026-03-27 at 16 05 10

No, not editable on CMS. They're programatically computed from 2020 to current year @kilemensi

@kilemensi
Copy link
Copy Markdown
Member

kilemensi commented Mar 27, 2026

No, not editable on CMS. They're programatically computed from 2020 to current year @kilemensi

I think they should be editable in the CMS... if we're computing, we should compute them in such a way that they contain years with actual barazas. What is the point of showing 2020 - 2025 if there is no data for them?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants