diff --git a/apps/trustlab/src/components/Content/Content.js b/apps/trustlab/src/components/Content/Content.js index 017ab139d..8580aee46 100644 --- a/apps/trustlab/src/components/Content/Content.js +++ b/apps/trustlab/src/components/Content/Content.js @@ -15,7 +15,7 @@ const Content = forwardRef((props, ref) => { return ( { color: textColor, }} TypographyProps={{ + variant: "p2", gutterBottom: true, sx: { mb: 3, diff --git a/apps/trustlab/src/components/Content/Content.snap.js b/apps/trustlab/src/components/Content/Content.snap.js index 743ee722e..80c0a7233 100644 --- a/apps/trustlab/src/components/Content/Content.snap.js +++ b/apps/trustlab/src/components/Content/Content.snap.js @@ -3,7 +3,7 @@ exports[` renders unchanged 1`] = `
-
+
renders items without labels 1`] = ` class="MuiBox-root css-1k9ek97" >
renders richtext card type 1`] = ` class="MuiBox-root css-1k9ek97" >
renders unchanged 1`] = ` class="MuiBox-root css-1k9ek97" >
renders with linked item 1`] = ` class="MuiBox-root css-1k9ek97" >
-
+
{title ? ( renders unchanged 1`] = ` class="MuiBox-root css-1k9ek97" >
renders with 4 items 1`] = ` class="MuiBox-root css-1k9ek97" >
renders with title and description 1`] = ` class="MuiBox-root css-1k9ek97" >

{ const currentYear = new Date().getFullYear(); const len = currentYear - 2020 + 1; - return Array.from({ length: len }, (_, i) => 2020 + i); + return Array.from({ length: len }, (_, i) => currentYear - i); }; // Generate month options diff --git a/apps/trustlab/src/components/HighlightList/HighlightList.js b/apps/trustlab/src/components/HighlightList/HighlightList.js index 4e927606d..6b1ea0dd8 100644 --- a/apps/trustlab/src/components/HighlightList/HighlightList.js +++ b/apps/trustlab/src/components/HighlightList/HighlightList.js @@ -12,7 +12,7 @@ const HighlightList = forwardRef(function HighlightList(props, ref) { return ( -

+
renders unchanged 1`] = ` class="MuiBox-root css-1k9ek97" >

renders with a single item 1`] = ` class="MuiBox-root css-1k9ek97" >

{ + setLightboxIndex(imageIndex); + setLightboxOpen(true); + }, []); + + const handleLightboxClose = useCallback(() => { + setLightboxOpen(false); + }, []); + + const handleLightboxPrevious = useCallback(() => { + setLightboxIndex((prev) => Math.max(0, prev - 1)); + }, []); + + const handleLightboxNext = useCallback(() => { + setLightboxIndex((prev) => Math.min(images.length - 1, prev + 1)); + }, [images?.length]); + + const flatImages = images?.map(({ image }) => image) || []; + if (!images?.length) { return null; } @@ -48,7 +71,7 @@ const HorizontalGallery = forwardRef(function HorizontalGallery(

- {chunks.map((chunk) => ( + {chunks.map((chunk, chunkIndex) => ( - {chunk.map(({ image }) => ( - -
{ + const flatIndex = chunkIndex * ITEMS_PER_PAGE + itemIndex; + return ( + handleImageClick(flatIndex)} sx={{ - m: 0, - height: { xs: 120, sm: 264 }, - borderRadius: 2, - overflow: "hidden", - position: "relative", - width: "100%", - "&:hover": { - filter: "none", + flex: { + xs: "0 0 calc(50% - 8px)", + sm: "0 0 calc(25% - 12px)", }, + minWidth: 0, + cursor: "pointer", }} - /> - - ))} + > +
+ + ); + })} ))} @@ -151,6 +182,15 @@ const HorizontalGallery = forwardRef(function HorizontalGallery( )}
+ + ); }); diff --git a/apps/trustlab/src/components/ImageLightbox/ImageLightbox.js b/apps/trustlab/src/components/ImageLightbox/ImageLightbox.js new file mode 100644 index 000000000..acc281dd5 --- /dev/null +++ b/apps/trustlab/src/components/ImageLightbox/ImageLightbox.js @@ -0,0 +1,197 @@ +import { Figure } from "@commons-ui/next"; +import { Box, IconButton, Modal, SvgIcon } from "@mui/material"; +import { forwardRef, useCallback, useEffect } from "react"; + +import ChevronRightDouble from "@/trustlab/assets/icons/Type=chevronRightDouble, Size=20, Color=currentColor.svg"; + +const ImageLightbox = forwardRef(function ImageLightbox(props, ref) { + const { + open, + onClose, + images = [], + currentIndex = 0, + onPrevious, + onNext, + } = props; + + const currentImage = images[currentIndex]; + const hasPrevious = currentIndex > 0; + const hasNext = currentIndex < images.length - 1; + + const handleKeyDown = useCallback( + (event) => { + if (event.key === "Escape") { + onClose?.(); + } else if (event.key === "ArrowLeft" && hasPrevious) { + onPrevious?.(); + } else if (event.key === "ArrowRight" && hasNext) { + onNext?.(); + } + }, + [onClose, onPrevious, onNext, hasPrevious, hasNext], + ); + + useEffect(() => { + if (open) { + window.addEventListener("keydown", handleKeyDown); + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + } + return undefined; + }, [open, handleKeyDown]); + + const handleBackdropClick = (event) => { + if (event.target === event.currentTarget) { + onClose?.(); + } + }; + + if (!currentImage) { + return null; + } + + return ( + + + {/* Previous button */} + {hasPrevious && ( + + + + )} + + {/* Next button */} + {hasNext && ( + + + + )} + + {/* Image container - stop propagation to prevent closing when clicking on image */} + e.stopPropagation()} + sx={{ + display: "flex", + alignItems: "center", + justifyContent: "center", + maxWidth: "75vw", + maxHeight: "75vh", + }} + > +
+ + + {/* Image counter */} + {images.length > 1 && ( + + {currentIndex + 1} / {images.length} + + )} + + + ); +}); + +export default ImageLightbox; diff --git a/apps/trustlab/src/components/ImageLightbox/ImageLightbox.snap.js b/apps/trustlab/src/components/ImageLightbox/ImageLightbox.snap.js new file mode 100644 index 000000000..7cbdbbdef --- /dev/null +++ b/apps/trustlab/src/components/ImageLightbox/ImageLightbox.snap.js @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ImageLightbox renders nothing when closed 1`] = `
`; diff --git a/apps/trustlab/src/components/ImageLightbox/ImageLightbox.test.js b/apps/trustlab/src/components/ImageLightbox/ImageLightbox.test.js new file mode 100644 index 000000000..d894b5de2 --- /dev/null +++ b/apps/trustlab/src/components/ImageLightbox/ImageLightbox.test.js @@ -0,0 +1,276 @@ +import { createRender } from "@commons-ui/testing-library"; +import { fireEvent } from "@testing-library/react"; +import React from "react"; + +import ImageLightbox from "./ImageLightbox"; + +import theme from "@/trustlab/theme"; + +const render = createRender({ theme }); + +const mockImages = [ + { + id: "1", + url: "https://example.com/image1.jpg", + alt: "Image 1", + height: 600, + width: 800, + }, + { + id: "2", + url: "https://example.com/image2.jpg", + alt: "Image 2", + height: 600, + width: 800, + }, + { + id: "3", + url: "https://example.com/image3.jpg", + alt: "Image 3", + height: 600, + width: 800, + }, +]; + +describe("ImageLightbox", () => { + it("renders nothing when closed", () => { + const { queryByRole, container } = render( + , + ); + expect(queryByRole("presentation")).not.toBeInTheDocument(); + expect(container).toMatchSnapshot(); + }); + + it("renders modal when open", () => { + const { getByRole } = render( + , + ); + expect(getByRole("presentation")).toBeInTheDocument(); + }); + + it("renders current image", () => { + const { getByAltText } = render( + , + ); + expect(getByAltText("Image 1")).toBeInTheDocument(); + }); + + it("renders next button when not on last image", () => { + const { getByLabelText } = render( + , + ); + expect(getByLabelText("Next image")).toBeInTheDocument(); + }); + + it("does not render next button on last image", () => { + const { queryByLabelText } = render( + , + ); + expect(queryByLabelText("Next image")).not.toBeInTheDocument(); + }); + + it("renders previous button when not on first image", () => { + const { getByLabelText } = render( + , + ); + expect(getByLabelText("Previous image")).toBeInTheDocument(); + }); + + it("does not render previous button on first image", () => { + const { queryByLabelText } = render( + , + ); + expect(queryByLabelText("Previous image")).not.toBeInTheDocument(); + }); + + it("calls onNext when next button is clicked", () => { + const onNext = jest.fn(); + const { getByLabelText } = render( + , + ); + fireEvent.click(getByLabelText("Next image")); + expect(onNext).toHaveBeenCalledTimes(1); + }); + + it("calls onPrevious when previous button is clicked", () => { + const onPrevious = jest.fn(); + const { getByLabelText } = render( + , + ); + fireEvent.click(getByLabelText("Previous image")); + expect(onPrevious).toHaveBeenCalledTimes(1); + }); + + it("renders image counter when multiple images", () => { + const { getByText } = render( + , + ); + expect(getByText("1 / 3")).toBeInTheDocument(); + }); + + it("does not render image counter for single image", () => { + const { queryByText } = render( + , + ); + expect(queryByText("1 / 1")).not.toBeInTheDocument(); + }); + + it("handles keyboard navigation - Escape closes modal", () => { + const onClose = jest.fn(); + render( + , + ); + fireEvent.keyDown(window, { key: "Escape" }); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it("handles keyboard navigation - ArrowRight calls onNext", () => { + const onNext = jest.fn(); + render( + , + ); + fireEvent.keyDown(window, { key: "ArrowRight" }); + expect(onNext).toHaveBeenCalledTimes(1); + }); + + it("handles keyboard navigation - ArrowLeft calls onPrevious", () => { + const onPrevious = jest.fn(); + render( + , + ); + fireEvent.keyDown(window, { key: "ArrowLeft" }); + expect(onPrevious).toHaveBeenCalledTimes(1); + }); + + it("does not call onPrevious on ArrowLeft when on first image", () => { + const onPrevious = jest.fn(); + render( + , + ); + fireEvent.keyDown(window, { key: "ArrowLeft" }); + expect(onPrevious).not.toHaveBeenCalled(); + }); + + it("does not call onNext on ArrowRight when on last image", () => { + const onNext = jest.fn(); + render( + , + ); + fireEvent.keyDown(window, { key: "ArrowRight" }); + expect(onNext).not.toHaveBeenCalled(); + }); + + it("renders nothing when no current image", () => { + const { queryByRole } = render( + , + ); + expect(queryByRole("presentation")).not.toBeInTheDocument(); + }); + + it("handles image with src instead of url", () => { + const imagesWithSrc = [ + { id: "1", src: "https://example.com/image.jpg", alt: "Test image" }, + ]; + const { getByAltText } = render( + , + ); + expect(getByAltText("Test image")).toBeInTheDocument(); + }); +}); diff --git a/apps/trustlab/src/components/ImageLightbox/index.js b/apps/trustlab/src/components/ImageLightbox/index.js new file mode 100644 index 000000000..973f4d227 --- /dev/null +++ b/apps/trustlab/src/components/ImageLightbox/index.js @@ -0,0 +1,3 @@ +import ImageLightbox from "./ImageLightbox"; + +export default ImageLightbox; diff --git a/apps/trustlab/src/components/OpportunityList/OpportunityList.js b/apps/trustlab/src/components/OpportunityList/OpportunityList.js index 477ea45ca..d310766b8 100644 --- a/apps/trustlab/src/components/OpportunityList/OpportunityList.js +++ b/apps/trustlab/src/components/OpportunityList/OpportunityList.js @@ -1,5 +1,6 @@ import { Section } from "@commons-ui/core"; -import { Grid2 as Grid, Box } from "@mui/material"; +import { LexicalRichText } from "@commons-ui/payload"; +import { Grid2 as Grid, Box, Typography } from "@mui/material"; import { useRouter } from "next/router"; import { forwardRef, useState, useEffect, useRef } from "react"; @@ -24,6 +25,8 @@ const OpportunityList = forwardRef(function OpportunityList(props, ref) { filterByLabel, applyFiltersLabel, clearFiltersLabel, + title, + description, ...other } = props; @@ -141,6 +144,35 @@ const OpportunityList = forwardRef(function OpportunityList(props, ref) { return ( + + {title || description ? ( +
+ {title} + {description && ( + + )} +
+ ) : null} +
+ {hasFilters ? (
{items.length ? ( -
+
-
+
renders unchanged 1`] = ` class="MuiBox-root css-14r34si" >
renders unchanged 1`] = ` class="MuiGrid2-root MuiGrid2-direction-xs-row MuiGrid2-grid-xs-12 MuiGrid2-grid-sm-6 css-hns2ug-MuiGrid2-root" >
renders with metrics 1`] = ` class="MuiBox-root css-14r34si" >
renders with metrics 1`] = ` class="MuiGrid2-root MuiGrid2-direction-xs-row MuiGrid2-grid-xs-12 MuiGrid2-grid-sm-6 css-hns2ug-MuiGrid2-root" >
-
+
{title && ( - siblingData?.type === "location" || siblingData?.type === "type", - }, fields: [ { name: "label", diff --git a/apps/trustlab/src/payload/collections/Opportunities.js b/apps/trustlab/src/payload/collections/Opportunities.js index 10998f6dc..32f4511ce 100644 --- a/apps/trustlab/src/payload/collections/Opportunities.js +++ b/apps/trustlab/src/payload/collections/Opportunities.js @@ -53,6 +53,18 @@ const Opportunities = { type: "text", localized: true, }, + { + name: "date", + type: "date", + admin: { + position: "sidebar", + date: { + pickerAppearance: "dayOnly", + displayFormat: "dd-MM-yyyy", + }, + description: "Date of the opportunity event", + }, + }, { name: "blocks", type: "blocks", diff --git a/apps/trustlab/src/payload/collections/Organisations.js b/apps/trustlab/src/payload/collections/Organisations.js index 8e66f06cd..202e78541 100644 --- a/apps/trustlab/src/payload/collections/Organisations.js +++ b/apps/trustlab/src/payload/collections/Organisations.js @@ -34,7 +34,6 @@ const Organisations = { }), linkGroup({ overrides: { - name: "website", label: "Organization Link", }, }),