From b9a3e697be1bbf6aadcaf384533176573f8bf0e0 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Thu, 30 Jan 2025 13:30:58 +0000 Subject: [PATCH] Merged in feat/SW-1521-image-gallery-lightbox (pull request #1226) Feat/SW-1521 image gallery lightbox * feat(SW-1453): added city listing component * feat(SW-1521): added more generic types to ImageGallery and Lightbox components * feat(SW-1521): added lightbox functionality for top images * feat(SW-1521): added support for setting activeIndex on open inside Lightbox Approved-by: Fredrik Thorsson Approved-by: Chuma Mcphoy (We Ahead) --- .../cityListingItem.module.css | 10 ++- .../CityListing/CityListingItem/index.tsx | 22 +++--- .../DestinationCityPage/index.tsx | 6 +- .../DestinationCountryPage/index.tsx | 7 +- .../DestinationPage/TopImages/index.tsx | 78 +++++++++++++++---- .../TopImages/topImages.module.css | 14 ++++ .../HotelPage/PreviewImages/index.tsx | 5 +- .../HotelPage/Rooms/RoomCard/index.tsx | 5 +- .../HotelPage/SidePeeks/Room/index.tsx | 8 +- .../HotelReservation/HotelCard/index.tsx | 10 +-- .../SelectRate/HotelInfoCard/index.tsx | 9 +-- .../RoomTypeList/RoomCard/index.tsx | 6 +- components/ImageGallery/index.tsx | 6 +- components/Lightbox/FullView.tsx | 11 ++- components/Lightbox/Gallery.tsx | 27 +++---- components/Lightbox/Lightbox.module.css | 3 +- components/Lightbox/index.tsx | 15 +++- components/SidePeeks/RoomSidePeek/index.tsx | 9 ++- .../destinationCountryPage/query.ts | 1 + types/components/hotelPage/previewImages.ts | 4 +- .../selectRate/imageGallery.ts | 4 +- types/components/imageGallery.ts | 7 +- types/components/lightbox/lightbox.ts | 3 +- types/hotel.ts | 13 ++-- utils/imageGallery.ts | 34 ++++++++ 25 files changed, 229 insertions(+), 88 deletions(-) create mode 100644 utils/imageGallery.ts diff --git a/components/ContentType/DestinationPage/CityListing/CityListingItem/cityListingItem.module.css b/components/ContentType/DestinationPage/CityListing/CityListingItem/cityListingItem.module.css index 912b96ed3..3ed9fc905 100644 --- a/components/ContentType/DestinationPage/CityListing/CityListingItem/cityListingItem.module.css +++ b/components/ContentType/DestinationPage/CityListing/CityListingItem/cityListingItem.module.css @@ -5,9 +5,13 @@ overflow: hidden; } -.image { +.imageWrapper { + position: relative; + height: 200px; width: 100%; - max-height: 200px; +} + +.imageWrapper img { object-fit: cover; } @@ -23,7 +27,7 @@ grid-template-columns: minmax(250px, 350px) auto; } - .image { + .imageWrapper { max-height: none; height: 100%; } diff --git a/components/ContentType/DestinationPage/CityListing/CityListingItem/index.tsx b/components/ContentType/DestinationPage/CityListing/CityListingItem/index.tsx index b2ec89211..346fc9cb1 100644 --- a/components/ContentType/DestinationPage/CityListing/CityListingItem/index.tsx +++ b/components/ContentType/DestinationPage/CityListing/CityListingItem/index.tsx @@ -1,11 +1,12 @@ import Link from "next/link" -import Image from "@/components/Image" +import ImageGallery from "@/components/ImageGallery" import Button from "@/components/TempDesignSystem/Button" import Divider from "@/components/TempDesignSystem/Divider" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import { getIntl } from "@/i18n" +import { mapImageVaultImagesToGalleryImages } from "@/utils/imageGallery" import ExperienceList from "../../ExperienceList" @@ -19,17 +20,20 @@ interface CityListingItemProps { export default async function CityListingItem({ city }: CityListingItemProps) { const intl = await getIntl() - const firstImage = city.images[0] + const galleryImages = mapImageVaultImagesToGalleryImages(city.images) return (
- {firstImage.meta.alt +
+ +

{city.heading}

diff --git a/components/ContentType/DestinationPage/DestinationCityPage/index.tsx b/components/ContentType/DestinationPage/DestinationCityPage/index.tsx index c55979f70..2bc30e119 100644 --- a/components/ContentType/DestinationPage/DestinationCityPage/index.tsx +++ b/components/ContentType/DestinationPage/DestinationCityPage/index.tsx @@ -48,7 +48,11 @@ export default async function DestinationCityPage() { }> - + {/* TODO: fetch translated city name from API when fetching hotel listing */} +
{/* TODO: Add hotel listing by cityIdentifier */} diff --git a/components/ContentType/DestinationPage/DestinationCountryPage/index.tsx b/components/ContentType/DestinationPage/DestinationCountryPage/index.tsx index bd2b64692..39275069b 100644 --- a/components/ContentType/DestinationPage/DestinationCountryPage/index.tsx +++ b/components/ContentType/DestinationPage/DestinationCountryPage/index.tsx @@ -8,6 +8,7 @@ import Body from "@/components/TempDesignSystem/Text/Body" import Title from "@/components/TempDesignSystem/Text/Title" import TrackingSDK from "@/components/TrackingSDK" import { getIntl } from "@/i18n" +import { getLang } from "@/i18n/serverContext" import CityListing from "../CityListing" import ExperienceList from "../ExperienceList" @@ -21,6 +22,7 @@ import styles from "./destinationCountryPage.module.css" import { PageContentTypeEnum } from "@/types/requests/contentType" export default async function DestinationCountryPage() { + const lang = getLang() const [intl, pageData] = await Promise.all([ getIntl(), getDestinationCountryPage(), @@ -30,7 +32,8 @@ export default async function DestinationCountryPage() { return null } - const { tracking, destinationCountryPage, cities } = pageData + const { tracking, destinationCountryPage, cities, translatedCountry } = + pageData const { images, heading, @@ -49,7 +52,7 @@ export default async function DestinationCountryPage() { }> - +
diff --git a/components/ContentType/DestinationPage/TopImages/index.tsx b/components/ContentType/DestinationPage/TopImages/index.tsx index 954fb36a0..e17d2c46a 100644 --- a/components/ContentType/DestinationPage/TopImages/index.tsx +++ b/components/ContentType/DestinationPage/TopImages/index.tsx @@ -1,6 +1,12 @@ "use client" +import { useState } from "react" +import { useIntl } from "react-intl" + import Image from "@/components/Image" +import Lightbox from "@/components/Lightbox" +import Button from "@/components/TempDesignSystem/Button" +import { mapImageVaultImagesToGalleryImages } from "@/utils/imageGallery" import styles from "./topImages.module.css" @@ -8,27 +14,65 @@ import type { ImageVaultAsset } from "@/types/components/imageVault" interface TopImageProps { images: ImageVaultAsset[] + destinationName: string } -export default function TopImages({ images }: TopImageProps) { - const maxWidth = 1020 +export default function TopImages({ images, destinationName }: TopImageProps) { + const intl = useIntl() + const [lightboxState, setLightboxState] = useState({ + open: false, + activeIndex: 0, + }) + const lightboxImages = mapImageVaultImagesToGalleryImages(images) + const maxWidth = 1366 // 1366px is the max width of the image container + const visibleImages = images.slice(0, 3) return ( -
- {images.slice(0, 3).map((image, index) => ( - {image.meta.alt - ))} +
+
+ {visibleImages.map((image, index) => ( + {image.meta.alt 1 ? styles.clickable : ""}`} + onClick={() => + images.length + ? setLightboxState({ open: true, activeIndex: index }) + : null + } + /> + ))} +
+ {images.length > 1 && ( + <> + + setLightboxState({ open: false, activeIndex: 0 })} + /> + + )}
) } diff --git a/components/ContentType/DestinationPage/TopImages/topImages.module.css b/components/ContentType/DestinationPage/TopImages/topImages.module.css index b08aabfc8..b93e71563 100644 --- a/components/ContentType/DestinationPage/TopImages/topImages.module.css +++ b/components/ContentType/DestinationPage/TopImages/topImages.module.css @@ -1,3 +1,7 @@ +.topImages { + position: relative; +} + .imageWrapper { max-width: var(--max-width-page); margin: 0 auto; @@ -10,6 +14,16 @@ border-radius: var(--Corner-radius-Medium); } +.image.clickable { + cursor: pointer; +} + +.seeAllButton { + position: absolute; + bottom: var(--Spacing-x2); + right: var(--Spacing-x4); +} + @media screen and (max-width: 767px) { .image:not(:first-child) { display: none; diff --git a/components/ContentType/HotelPage/PreviewImages/index.tsx b/components/ContentType/HotelPage/PreviewImages/index.tsx index 95a8098dd..b5b567d8b 100644 --- a/components/ContentType/HotelPage/PreviewImages/index.tsx +++ b/components/ContentType/HotelPage/PreviewImages/index.tsx @@ -6,6 +6,7 @@ import { useIntl } from "react-intl" import Image from "@/components/Image" import Lightbox from "@/components/Lightbox/" import Button from "@/components/TempDesignSystem/Button" +import { mapApiImagesToGalleryImages } from "@/utils/imageGallery" import styles from "./previewImages.module.css" @@ -18,6 +19,8 @@ export default function PreviewImages({ const intl = useIntl() const [lightboxIsOpen, setLightboxIsOpen] = useState(false) + const lightboxImages = mapApiImagesToGalleryImages(images) + return (
{images.slice(0, 3).map((image, index) => ( @@ -44,7 +47,7 @@ export default function PreviewImages({ {intl.formatMessage({ id: "See all photos" })}
@@ -51,7 +53,11 @@ export default async function RoomSidePeek({ room }: RoomSidePeekProps) { )}
- +
{roomDescription}
diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 84d076060..3af9a8819 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -16,6 +16,7 @@ import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import { mapApiImagesToGalleryImages } from "@/utils/imageGallery" import { getSingleDecimal } from "@/utils/numberFormatting" import ReadMore from "../ReadMore" @@ -66,6 +67,9 @@ function HotelCard({ }) const addressStr = `${hotelData.address.streetAddress}, ${hotelData.address.city}` + const galleryImages = mapApiImagesToGalleryImages( + hotelData.galleryImages || [] + ) return (
- + {hotelData.ratings?.tripAdvisor && ( )} diff --git a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx index 7af28f4dc..bc4341a5f 100644 --- a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx +++ b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx @@ -10,6 +10,7 @@ import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" +import { mapApiImagesToGalleryImages } from "@/utils/imageGallery" import { getSingleDecimal } from "@/utils/numberFormatting" import ReadMore from "../../ReadMore" @@ -38,16 +39,14 @@ export default async function HotelInfoCard({ .sort((a, b) => b.sortOrder - a.sortOrder) .slice(0, 5) + const galleryImages = mapApiImagesToGalleryImages(hotel?.galleryImages || []) + return (
{hotel && (
- + {hotel.ratings?.tripAdvisor && ( )} diff --git a/components/HotelReservation/SelectRate/RoomTypeList/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomTypeList/RoomCard/index.tsx index 67a209056..45ce72a90 100644 --- a/components/HotelReservation/SelectRate/RoomTypeList/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomTypeList/RoomCard/index.tsx @@ -13,6 +13,7 @@ import ImageGallery from "@/components/ImageGallery" import Caption from "@/components/TempDesignSystem/Text/Caption" import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import { mapApiImagesToGalleryImages } from "@/utils/imageGallery" import FlexibilityOption from "../FlexibilityOption" import { cardVariants } from "./cardVariants" @@ -184,6 +185,7 @@ export default function RoomCard({ }) } } + const galleryImages = mapApiImagesToGalleryImages(images || []) return (
  • @@ -213,10 +215,8 @@ export default function RoomCard({ ))}
  • - {/*NOTE: images from the test API are hosted on test3.scandichotels.com, - which can't be accessed unless on Scandic's Wifi or using Citrix. */} diff --git a/components/ImageGallery/index.tsx b/components/ImageGallery/index.tsx index 007785ad8..ed765263a 100644 --- a/components/ImageGallery/index.tsx +++ b/components/ImageGallery/index.tsx @@ -28,6 +28,8 @@ function ImageGallery({ return
    } + const firstImage = images[0] + return ( <>
    {images[0].metaData.altText} setImageError(true)} {...imageProps} /> diff --git a/components/Lightbox/FullView.tsx b/components/Lightbox/FullView.tsx index fac7dcf29..714a96f86 100644 --- a/components/Lightbox/FullView.tsx +++ b/components/Lightbox/FullView.tsx @@ -77,7 +77,7 @@ export default function FullView({
    handleSwipe(info.offset.x)} > {image.metaData.altText}
    - {image.metaData.title && ( - {image.metaData.title} - )} + {image.caption && {image.caption}}
    diff --git a/components/Lightbox/Gallery.tsx b/components/Lightbox/Gallery.tsx index 686191edf..21cca522d 100644 --- a/components/Lightbox/Gallery.tsx +++ b/components/Lightbox/Gallery.tsx @@ -79,18 +79,16 @@ export default function Gallery({ {/* Desktop Gallery */}
    - {mainImage.metaData.title && ( + {mainImage.caption && (
    - - {mainImage.metaData.title} - + {mainImage.caption}
    )}
    {mainImage.metaData.altText} @@ -128,7 +127,7 @@ export default function Gallery({ {getThumbImages().map((image, index) => ( onSelectImage(image)} initial={{ opacity: 0, x: 50 }} @@ -137,9 +136,10 @@ export default function Gallery({ transition={{ duration: 0.2, delay: index * 0.05 }} > {image.metaData.altText} @@ -154,7 +154,7 @@ export default function Gallery({
    {images.map((image, index) => ( { onSelectImage(image) @@ -165,9 +165,10 @@ export default function Gallery({ transition={{ duration: 0.3, delay: index * 0.05 }} > {image.metaData.altText} diff --git a/components/Lightbox/Lightbox.module.css b/components/Lightbox/Lightbox.module.css index 8b63a3a98..b95d4fad5 100644 --- a/components/Lightbox/Lightbox.module.css +++ b/components/Lightbox/Lightbox.module.css @@ -210,7 +210,7 @@ } } -@media (min-width: 1367px) { +@media (min-width: 768px) { .mobileGallery, .thumbnailGrid { display: none; @@ -229,6 +229,7 @@ .galleryContent { width: 1090px; + width: min(var(--max-width-page), 1090px); height: min(725px, 85dvh); } diff --git a/components/Lightbox/index.tsx b/components/Lightbox/index.tsx index 3b651c907..fcc011b36 100644 --- a/components/Lightbox/index.tsx +++ b/components/Lightbox/index.tsx @@ -15,17 +15,26 @@ export default function Lightbox({ dialogTitle, onClose, isOpen, + activeIndex = 0, }: LightboxProps) { - const [selectedImageIndex, setSelectedImageIndex] = useState(0) + const [selectedImageIndex, setSelectedImageIndex] = useState(activeIndex) const [isFullView, setIsFullView] = useState(false) useEffect(() => { if (isOpen) { - setSelectedImageIndex(0) setIsFullView(false) } }, [isOpen]) + useEffect(() => { + setSelectedImageIndex(activeIndex) + }, [activeIndex]) + + function handleClose() { + setSelectedImageIndex(0) + onClose() + } + function handleNext() { setSelectedImageIndex((prevIndex) => (prevIndex + 1) % images.length) } @@ -39,7 +48,7 @@ export default function Lightbox({ return ( diff --git a/components/SidePeeks/RoomSidePeek/index.tsx b/components/SidePeeks/RoomSidePeek/index.tsx index 586e26ecf..a1fa84ea5 100644 --- a/components/SidePeeks/RoomSidePeek/index.tsx +++ b/components/SidePeeks/RoomSidePeek/index.tsx @@ -4,6 +4,7 @@ import ImageGallery from "@/components/ImageGallery" import SidePeek from "@/components/TempDesignSystem/SidePeek" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import { mapApiImagesToGalleryImages } from "@/utils/imageGallery" import { getBedIcon } from "./bedIcon" import { getFacilityIcon } from "./facilityIcon" @@ -23,7 +24,7 @@ export default function RoomSidePeek({ const roomSize = room.roomSize const totalOccupancy = room.occupancy const roomDescription = room.descriptions.medium - const images = room.images + const galleryImages = mapApiImagesToGalleryImages(room.images) return (
    - +
    {roomDescription}
    diff --git a/server/routers/contentstack/destinationCountryPage/query.ts b/server/routers/contentstack/destinationCountryPage/query.ts index 83620d0eb..19379a233 100644 --- a/server/routers/contentstack/destinationCountryPage/query.ts +++ b/server/routers/contentstack/destinationCountryPage/query.ts @@ -202,6 +202,7 @@ export const destinationCountryPageQueryRouter = router({ return { ...validatedResponse.data, + translatedCountry: apiCountry, cities: cityPages .flat() .filter((city): city is NonNullable => !!city), diff --git a/types/components/hotelPage/previewImages.ts b/types/components/hotelPage/previewImages.ts index d808fdcf7..497121f35 100644 --- a/types/components/hotelPage/previewImages.ts +++ b/types/components/hotelPage/previewImages.ts @@ -1,6 +1,6 @@ -import type { GalleryImage } from "@/types/hotel" +import type { ApiImage } from "@/types/hotel" export type PreviewImagesProps = { - images: GalleryImage[] + images: ApiImage[] hotelName: string } diff --git a/types/components/hotelReservation/selectRate/imageGallery.ts b/types/components/hotelReservation/selectRate/imageGallery.ts index 0c16c82e0..576326374 100644 --- a/types/components/hotelReservation/selectRate/imageGallery.ts +++ b/types/components/hotelReservation/selectRate/imageGallery.ts @@ -1,3 +1,3 @@ -import type { GalleryImage } from "@/types/hotel" +import type { ApiImage } from "@/types/hotel" -export type ImageGalleryProps = { images?: GalleryImage[]; title: string } +export type ImageGalleryProps = { images?: ApiImage[]; title: string } diff --git a/types/components/imageGallery.ts b/types/components/imageGallery.ts index 019fd8032..40fb70916 100644 --- a/types/components/imageGallery.ts +++ b/types/components/imageGallery.ts @@ -1,4 +1,9 @@ -import type { GalleryImage } from "@/types/hotel" +export interface GalleryImage { + src: string + alt: string + caption?: string | null + smallSrc?: string | null +} export type ImageGalleryProps = { images?: GalleryImage[] diff --git a/types/components/lightbox/lightbox.ts b/types/components/lightbox/lightbox.ts index 3c0b7db16..abfff51d4 100644 --- a/types/components/lightbox/lightbox.ts +++ b/types/components/lightbox/lightbox.ts @@ -1,10 +1,11 @@ -import type { GalleryImage } from "@/types/hotel" +import type { GalleryImage } from "../imageGallery" export interface LightboxProps { images: GalleryImage[] dialogTitle: string /* Accessible title for dialog screen readers */ onClose: () => void isOpen: boolean + activeIndex?: number } export interface GalleryProps { diff --git a/types/hotel.ts b/types/hotel.ts index 6a755e99c..fab71c413 100644 --- a/types/hotel.ts +++ b/types/hotel.ts @@ -1,6 +1,11 @@ import type { z } from "zod" import type { hotelSchema } from "@/server/routers/hotels/output" +import type { + extraPageSchema, + facilitySchema, + transformAdditionalData, +} from "@/server/routers/hotels/schemas/additionalData" import type { citySchema } from "@/server/routers/hotels/schemas/city" import type { attributesSchema } from "@/server/routers/hotels/schemas/hotel" import type { addressSchema } from "@/server/routers/hotels/schemas/hotel/address" @@ -20,11 +25,6 @@ import type { parkingSchema } from "@/server/routers/hotels/schemas/hotel/parkin import type { pointOfInterestSchema } from "@/server/routers/hotels/schemas/hotel/poi" import type { ratingsSchema } from "@/server/routers/hotels/schemas/hotel/rating" import type { imageSchema } from "@/server/routers/hotels/schemas/image" -import type { - extraPageSchema, - facilitySchema, - transformAdditionalData, -} from "@/server/routers/hotels/schemas/additionalData" export type HotelData = z.output @@ -33,7 +33,7 @@ export type CheckInData = z.output type CitySchema = z.output export type City = Pick & CitySchema["attributes"] export type Facility = z.output & { id: string } -export type GalleryImage = z.output +export type ApiImage = z.output export type HealthFacility = z.output export type HealthFacilities = HealthFacility[] export type Hotel = z.output @@ -68,4 +68,3 @@ export type HotelTripAdvisor = export type AdditionalData = ReturnType export type ExtraPageSchema = z.output - diff --git a/utils/imageGallery.ts b/utils/imageGallery.ts new file mode 100644 index 000000000..8c7097493 --- /dev/null +++ b/utils/imageGallery.ts @@ -0,0 +1,34 @@ +import type { GalleryImage } from "@/types/components/imageGallery" +import type { ImageVaultAsset } from "@/types/components/imageVault" +import type { ApiImage } from "@/types/hotel" + +function mapApiImageToGalleryImage(apiImage: ApiImage): GalleryImage { + return { + src: apiImage.imageSizes.medium, + alt: apiImage.metaData.altText || apiImage.metaData.title, + caption: apiImage.metaData.title, + smallSrc: apiImage.imageSizes.small, + } +} + +export function mapApiImagesToGalleryImages( + apiImages: ApiImage[] +): GalleryImage[] { + return apiImages.map(mapApiImageToGalleryImage) +} + +function mapImageVaultImageToGalleryImage( + imageVaultImage: ImageVaultAsset +): GalleryImage { + return { + src: imageVaultImage.url, + alt: imageVaultImage.meta.alt || imageVaultImage.meta.caption || "", + caption: imageVaultImage.meta.caption, + } +} + +export function mapImageVaultImagesToGalleryImages( + imageVaultImages: ImageVaultAsset[] +): GalleryImage[] { + return imageVaultImages.map(mapImageVaultImageToGalleryImage) +}