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)
This commit is contained in:
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<article className={styles.container}>
|
||||
<Image
|
||||
src={firstImage.url}
|
||||
alt={firstImage.meta.alt || firstImage.meta.caption || ""}
|
||||
width={300}
|
||||
height={200}
|
||||
className={styles.image}
|
||||
/>
|
||||
<div className={styles.imageWrapper}>
|
||||
<ImageGallery
|
||||
images={galleryImages}
|
||||
fill
|
||||
title={intl.formatMessage(
|
||||
{ id: "{title} - Image gallery" },
|
||||
{ title: city.cityName }
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<section className={styles.content}>
|
||||
<Subtitle asChild>
|
||||
<h3>{city.heading}</h3>
|
||||
|
||||
@@ -48,7 +48,11 @@ export default async function DestinationCityPage() {
|
||||
<Suspense fallback={<BreadcrumbsSkeleton />}>
|
||||
<Breadcrumbs variant={PageContentTypeEnum.destinationCityPage} />
|
||||
</Suspense>
|
||||
<TopImages images={images} />
|
||||
{/* TODO: fetch translated city name from API when fetching hotel listing */}
|
||||
<TopImages
|
||||
images={images}
|
||||
destinationName={destination_settings.city}
|
||||
/>
|
||||
</header>
|
||||
<main className={styles.mainSection}>
|
||||
{/* TODO: Add hotel listing by cityIdentifier */}
|
||||
|
||||
@@ -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() {
|
||||
<Suspense fallback={<BreadcrumbsSkeleton />}>
|
||||
<Breadcrumbs variant={PageContentTypeEnum.destinationCityPage} />
|
||||
</Suspense>
|
||||
<TopImages images={images} />
|
||||
<TopImages images={images} destinationName={translatedCountry} />
|
||||
</header>
|
||||
<main className={styles.mainSection}>
|
||||
<CityListing cities={cities} />
|
||||
|
||||
@@ -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 (
|
||||
<div className={styles.imageWrapper}>
|
||||
{images.slice(0, 3).map((image, index) => (
|
||||
<Image
|
||||
key={image.url}
|
||||
src={image.url}
|
||||
alt={image.meta.alt || image.meta.caption || ""}
|
||||
width={index === 0 ? maxWidth : maxWidth / 3}
|
||||
height={Math.ceil(
|
||||
(index === 0 ? maxWidth : maxWidth / 3) /
|
||||
image.dimensions.aspectRatio
|
||||
)}
|
||||
focalPoint={image.focalPoint}
|
||||
className={styles.image}
|
||||
/>
|
||||
))}
|
||||
<div className={styles.topImages}>
|
||||
<div className={styles.imageWrapper}>
|
||||
{visibleImages.map((image, index) => (
|
||||
<Image
|
||||
key={image.url}
|
||||
src={image.url}
|
||||
alt={image.meta.alt || image.meta.caption || ""}
|
||||
width={index === 0 ? maxWidth : maxWidth / visibleImages.length}
|
||||
height={Math.ceil(
|
||||
(index === 0 ? maxWidth : maxWidth / visibleImages.length) /
|
||||
image.dimensions.aspectRatio
|
||||
)}
|
||||
focalPoint={image.focalPoint}
|
||||
className={`${styles.image} ${images.length > 1 ? styles.clickable : ""}`}
|
||||
onClick={() =>
|
||||
images.length
|
||||
? setLightboxState({ open: true, activeIndex: index })
|
||||
: null
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{images.length > 1 && (
|
||||
<>
|
||||
<Button
|
||||
theme="base"
|
||||
intent="inverted"
|
||||
size="small"
|
||||
onClick={() => setLightboxState({ open: true, activeIndex: 0 })}
|
||||
className={styles.seeAllButton}
|
||||
>
|
||||
{intl.formatMessage({ id: "See all photos" })}
|
||||
</Button>
|
||||
<Lightbox
|
||||
images={lightboxImages}
|
||||
dialogTitle={intl.formatMessage(
|
||||
{ id: "{title} - Image gallery" },
|
||||
{ title: destinationName }
|
||||
)}
|
||||
isOpen={lightboxState.open}
|
||||
activeIndex={lightboxState.activeIndex}
|
||||
onClose={() => setLightboxState({ open: false, activeIndex: 0 })}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<div className={styles.imageWrapper}>
|
||||
{images.slice(0, 3).map((image, index) => (
|
||||
@@ -44,7 +47,7 @@ export default function PreviewImages({
|
||||
{intl.formatMessage({ id: "See all photos" })}
|
||||
</Button>
|
||||
<Lightbox
|
||||
images={images}
|
||||
images={lightboxImages}
|
||||
dialogTitle={intl.formatMessage(
|
||||
{ id: "{title} - Image gallery" },
|
||||
{ title: hotelName }
|
||||
|
||||
@@ -8,6 +8,7 @@ import ImageGallery from "@/components/ImageGallery"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||
|
||||
import { getRoomNameAsParam } from "../../utils"
|
||||
|
||||
@@ -24,11 +25,13 @@ export function RoomCard({ room }: RoomCardProps) {
|
||||
? `${roomSize.min} m²`
|
||||
: `${roomSize.min} - ${roomSize.max} m²`
|
||||
|
||||
const galleryImages = mapApiImagesToGalleryImages(images)
|
||||
|
||||
return (
|
||||
<article className={styles.roomCard}>
|
||||
<div className={styles.imageContainer}>
|
||||
<ImageGallery
|
||||
images={images}
|
||||
images={galleryImages}
|
||||
title={intl.formatMessage(
|
||||
{ id: "{title} - Image gallery" },
|
||||
{ title: name }
|
||||
|
||||
@@ -8,6 +8,7 @@ import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||
|
||||
import { getRoomNameAsParam } from "../../utils"
|
||||
|
||||
@@ -22,6 +23,7 @@ export default async function RoomSidePeek({ room }: RoomSidePeekProps) {
|
||||
// TODO: Not defined where this should lead.
|
||||
const ctaUrl = ""
|
||||
|
||||
const galleryImages = mapApiImagesToGalleryImages(images)
|
||||
return (
|
||||
<SidePeek contentKey={getRoomNameAsParam(room.name)} title={room.name}>
|
||||
<div className={styles.content}>
|
||||
@@ -51,7 +53,11 @@ export default async function RoomSidePeek({ room }: RoomSidePeekProps) {
|
||||
)}
|
||||
</Body>
|
||||
<div className={styles.imageContainer}>
|
||||
<ImageGallery images={images} title={room.name} height={280} />
|
||||
<ImageGallery
|
||||
images={galleryImages}
|
||||
title={room.name}
|
||||
height={280}
|
||||
/>
|
||||
</div>
|
||||
<Body color="uiTextHighContrast">{roomDescription}</Body>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user