feat(SW-842): Added lightbox to roomcard

This commit is contained in:
Erik Tiekstra
2024-11-12 10:39:42 +01:00
parent d732138696
commit 962760ae1b
11 changed files with 150 additions and 142 deletions

View File

@@ -1,44 +1,54 @@
"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 { getIntl } from "@/i18n"
import styles from "./previewImages.module.css"
import type { PreviewImagesProps } from "@/types/components/hotelPage/previewImages"
export default async function PreviewImages({
export default function PreviewImages({
images,
hotelName,
}: PreviewImagesProps) {
const intl = await getIntl()
const imageGalleryText = intl.formatMessage({ id: "Image gallery" })
const dialogTitle = `${hotelName} - ${imageGalleryText}`
const intl = useIntl()
const [lightboxIsOpen, setLightboxIsOpen] = useState(false)
return (
<Lightbox images={images} dialogTitle={dialogTitle}>
<div className={styles.imageWrapper}>
{images.slice(0, 3).map((image, index) => (
<Image
key={index}
src={image.imageSizes.medium}
alt={image.metaData.altText}
title={image.metaData.title}
width={index === 0 ? 752 : 292}
height={index === 0 ? 540 : 266}
className={styles.image}
/>
))}
<Button
theme="base"
intent="inverted"
size="small"
id="lightboxTrigger"
className={styles.seeAllButton}
>
{intl.formatMessage({ id: "See all photos" })}
</Button>
</div>
</Lightbox>
<div className={styles.imageWrapper}>
{images.slice(0, 3).map((image, index) => (
<Image
key={index}
src={image.imageSizes.medium}
alt={image.metaData.altText}
title={image.metaData.title}
width={index === 0 ? 752 : 292}
height={index === 0 ? 540 : 266}
className={styles.image}
/>
))}
<Button
theme="base"
intent="inverted"
size="small"
onClick={() => setLightboxIsOpen(true)}
className={styles.seeAllButton}
>
{intl.formatMessage({ id: "See all photos" })}
</Button>
<Lightbox
images={images}
dialogTitle={intl.formatMessage(
{ id: "Hotel Image gallery" },
{ hotel: hotelName }
)}
isOpen={lightboxIsOpen}
onClose={() => setLightboxIsOpen(false)}
/>
</div>
)
}

View File

@@ -1,9 +1,11 @@
"use client"
import { useState } from "react"
import { useIntl } from "react-intl"
import { GalleryIcon } from "@/components/Icons"
import Image from "@/components/Image"
import Lightbox from "@/components/Lightbox"
import Body from "@/components/TempDesignSystem/Text/Body"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
@@ -16,6 +18,7 @@ import type { RoomCardProps } from "@/types/components/hotelPage/room"
export function RoomCard({ hotelId, room }: RoomCardProps) {
const { images, name, roomSize, occupancy, id } = room
const intl = useIntl()
const [lightboxIsOpen, setLightboxIsOpen] = useState(false)
const mainImage = images[0]
const size =
@@ -23,21 +26,12 @@ export function RoomCard({ hotelId, room }: RoomCardProps) {
? `${roomSize.min}`
: `${roomSize.min} - ${roomSize.max}`
const personLabel = intl.formatMessage(
{ id: "hotelPages.rooms.roomCard.persons" },
{ totalOccupancy: occupancy.total }
)
const subtitle = `${size} (${personLabel})`
function handleImageClick() {
// TODO: Implement opening of a model with carousel
console.log("Image clicked: ", id)
}
return (
<article className={styles.roomCard}>
<button className={styles.imageWrapper} onClick={handleImageClick}>
<button
className={styles.imageWrapper}
onClick={() => setLightboxIsOpen(true)}
>
{/* TODO: re-enable once we have support for badge text from API team. */}
{/* {badgeTextTransKey && ( */}
{/* <span className={styles.badge}> */}
@@ -52,12 +46,21 @@ export function RoomCard({ hotelId, room }: RoomCardProps) {
which can't be accessed unless on Scandic's Wifi or using Citrix. */}
<Image
className={styles.image}
src={mainImage.imageSizes.large}
src={mainImage.imageSizes.small}
alt={mainImage.metaData.altText}
height={200}
width={300}
/>
</button>
<Lightbox
images={images}
dialogTitle={intl.formatMessage(
{ id: "Hotel Image gallery" },
{ hotel: name }
)}
isOpen={lightboxIsOpen}
onClose={() => setLightboxIsOpen(false)}
/>
<div className={styles.content}>
<div className={styles.innerContent}>
<Subtitle
@@ -69,7 +72,12 @@ export function RoomCard({ hotelId, room }: RoomCardProps) {
>
{name}
</Subtitle>
<Body color="grey">{subtitle}</Body>
<Body color="grey">
{intl.formatMessage(
{ id: "hotelPages.rooms.roomCard.persons" },
{ size, totalOccupancy: occupancy.total }
)}
</Body>
</div>
<RoomDetailsButton
hotelId={hotelId}

View File

@@ -1,3 +1,7 @@
"use client"
import { useState } from "react"
import { GalleryIcon } from "@/components/Icons"
import Image from "@/components/Image"
import Lightbox from "@/components/Lightbox"
@@ -8,12 +12,19 @@ import styles from "./imageGallery.module.css"
import type { ImageGalleryProps } from "@/types/components/hotelReservation/selectRate/imageGallery"
export default function ImageGallery({ images, title }: ImageGalleryProps) {
if (!images || images.length === 0)
const [lightboxIsOpen, setLightboxIsOpen] = useState(false)
if (!images || images.length === 0) {
return <div className={styles.imagePlaceholder} />
}
return (
<Lightbox images={images} dialogTitle={title}>
<div className={styles.triggerArea} id="lightboxTrigger">
<>
<div
className={styles.triggerArea}
role="button"
onClick={() => setLightboxIsOpen(true)}
>
<Image
src={images[0].imageSizes.medium}
alt={images[0].metaData.altText}
@@ -26,6 +37,12 @@ export default function ImageGallery({ images, title }: ImageGalleryProps) {
</Footnote>
</div>
</div>
</Lightbox>
<Lightbox
images={images}
dialogTitle={title}
isOpen={lightboxIsOpen}
onClose={() => setLightboxIsOpen(false)}
/>
</>
)
}

View File

@@ -1,6 +1,6 @@
"use client"
import { AnimatePresence, motion } from "framer-motion"
import React, { useState } from "react"
import { useEffect, useState } from "react"
import { Dialog, Modal, ModalOverlay } from "react-aria-components"
import FullView from "./FullView"
@@ -12,24 +12,19 @@ import type { LightboxProps } from "@/types/components/lightbox/lightbox"
export default function Lightbox({
images,
children,
dialogTitle,
onClose,
isOpen,
}: LightboxProps) {
const [isOpen, setIsOpen] = useState(false)
const [selectedImageIndex, setSelectedImageIndex] = useState(0)
const [isFullView, setIsFullView] = useState(false)
function handleOpenChange(open: boolean) {
if (!open) {
setTimeout(() => {
setIsOpen(false)
setSelectedImageIndex(0)
setIsFullView(false)
}, 300) // 300ms delay
} else {
setIsOpen(true)
useEffect(() => {
if (isOpen) {
setSelectedImageIndex(0)
setIsFullView(false)
}
}
}, [isOpen])
function handleNext() {
setSelectedImageIndex((prevIndex) => (prevIndex + 1) % images.length)
@@ -41,75 +36,53 @@ export default function Lightbox({
)
}
const triggerElement = React.Children.map(
children,
function mapChild(child): React.ReactNode {
if (React.isValidElement(child)) {
if (child.props.id === "lightboxTrigger") {
return React.cloneElement(child, {
onClick: () => setIsOpen(true),
} as React.HTMLAttributes<HTMLElement>)
} else if (child.props.children) {
return React.cloneElement(child, {
children: React.Children.map(child.props.children, mapChild),
} as React.HTMLAttributes<HTMLElement>)
}
}
return child
}
)
return (
<>
{triggerElement}
<ModalOverlay
isOpen={isOpen}
onOpenChange={handleOpenChange}
className={styles.overlay}
isDismissable
>
<Modal>
<AnimatePresence>
<ModalOverlay
isOpen={isOpen}
onOpenChange={onClose}
className={styles.overlay}
isDismissable
>
<Modal>
<AnimatePresence>
<Dialog aria-label={dialogTitle}>
{isOpen && (
<Dialog>
<motion.div
className={`${styles.content} ${
isFullView ? styles.fullViewContent : styles.galleryContent
}`}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1, x: "-50%", y: "-50%" }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.2 }}
>
{isFullView ? (
<FullView
image={images[selectedImageIndex]}
onClose={() => setIsFullView(false)}
onNext={handleNext}
onPrev={handlePrev}
currentIndex={selectedImageIndex}
totalImages={images.length}
/>
) : (
<Gallery
images={images}
dialogTitle={dialogTitle}
onClose={() => setIsOpen(false)}
onSelectImage={(image) => {
setSelectedImageIndex(
images.findIndex((img) => img === image)
)
}}
onImageClick={() => setIsFullView(true)}
selectedImage={images[selectedImageIndex]}
/>
)}
</motion.div>
</Dialog>
<motion.div
className={`${styles.content} ${
isFullView ? styles.fullViewContent : styles.galleryContent
}`}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1, x: "-50%", y: "-50%" }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.2 }}
>
{isFullView ? (
<FullView
image={images[selectedImageIndex]}
onClose={() => setIsFullView(false)}
onNext={handleNext}
onPrev={handlePrev}
currentIndex={selectedImageIndex}
totalImages={images.length}
/>
) : (
<Gallery
images={images}
onClose={onClose}
onSelectImage={(image) => {
setSelectedImageIndex(
images.findIndex((img) => img === image)
)
}}
onImageClick={() => setIsFullView(true)}
selectedImage={images[selectedImageIndex]}
/>
)}
</motion.div>
)}
</AnimatePresence>
</Modal>
</ModalOverlay>
</>
</Dialog>
</AnimatePresence>
</Modal>
</ModalOverlay>
)
}

View File

@@ -144,6 +144,7 @@
"Highest level": "Højeste niveau",
"Hospital": "Hospital",
"Hotel": "Hotel",
"Hotel Image gallery": "{hotel} - Billedgalleri",
"Hotel facilities": "Hotel faciliteter",
"Hotel surroundings": "Hotel omgivelser",
"Hotels": "Hoteller",
@@ -151,7 +152,6 @@
"How it works": "Hvordan det virker",
"Hurry up and use them before they expire!": "Skynd dig og brug dem, før de udløber!",
"I would like to get my booking confirmation via sms": "Jeg vil gerne få min booking bekræftelse via SMS",
"Image gallery": "Billedgalleri",
"In adults bed": "i de voksnes seng",
"In crib": "i tremmeseng",
"In extra bed": "i ekstra seng",
@@ -409,7 +409,7 @@
"guaranteeing": "garanti",
"guest": "gæst",
"guests": "gæster",
"hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# person} other {# personer}}",
"hotelPages.rooms.roomCard.persons": "{size} ({totalOccupancy, plural, one {# person} other {# personer}})",
"hotelPages.rooms.roomCard.seeRoomDetails": "Se værelsesdetaljer",
"km to city center": "km til byens centrum",
"lowercase letter": "lille bogstav",

View File

@@ -144,6 +144,7 @@
"Highest level": "Höchstes Level",
"Hospital": "Krankenhaus",
"Hotel": "Hotel",
"Hotel Image gallery": "{hotel} - Bildergalerie",
"Hotel facilities": "Hotel-Infos",
"Hotel surroundings": "Umgebung des Hotels",
"Hotels": "Hotels",
@@ -151,7 +152,6 @@
"How it works": "Wie es funktioniert",
"Hurry up and use them before they expire!": "Beeilen Sie sich und nutzen Sie sie, bevor sie ablaufen!",
"I would like to get my booking confirmation via sms": "Ich möchte meine Buchungsbestätigung per SMS erhalten",
"Image gallery": "Bildergalerie",
"In adults bed": "Im Bett der Eltern",
"In crib": "im Kinderbett",
"In extra bed": "im zusätzlichen Bett",
@@ -408,7 +408,7 @@
"guaranteeing": "garantiert",
"guest": "gast",
"guests": "gäste",
"hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# person} other {# personen}}",
"hotelPages.rooms.roomCard.persons": "{size} ({totalOccupancy, plural, one {# person} other {# personen}})",
"hotelPages.rooms.roomCard.seeRoomDetails": "Zimmerdetails ansehen",
"km to city center": "km bis zum Stadtzentrum",
"lowercase letter": "Kleinbuchstabe",

View File

@@ -156,6 +156,7 @@
"Highest level": "Highest level",
"Hospital": "Hospital",
"Hotel": "Hotel",
"Hotel Image gallery": "{hotel} - Image gallery",
"Hotel facilities": "Hotel facilities",
"Hotel surroundings": "Hotel surroundings",
"Hotels": "Hotels",
@@ -163,7 +164,6 @@
"How it works": "How it works",
"Hurry up and use them before they expire!": "Hurry up and use them before they expire!",
"I would like to get my booking confirmation via sms": "I would like to get my booking confirmation via sms",
"Image gallery": "Image gallery",
"In adults bed": "In adults bed",
"In crib": "In crib",
"In extra bed": "In extra bed",
@@ -446,7 +446,7 @@
"guest": "guest",
"guest.paid": "{amount} {currency} has been paid",
"guests": "guests",
"hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# person} other {# persons}}",
"hotelPages.rooms.roomCard.persons": "{size} ({totalOccupancy, plural, one {# person} other {# persons}})",
"hotelPages.rooms.roomCard.seeRoomDetails": "See room details",
"km to city center": "km to city center",
"lowercase letter": "lowercase letter",

View File

@@ -144,6 +144,7 @@
"Highest level": "Korkein taso",
"Hospital": "Sairaala",
"Hotel": "Hotelli",
"Hotel Image gallery": "{hotel} - Kuvagalleria",
"Hotel facilities": "Hotellin palvelut",
"Hotel surroundings": "Hotellin ympäristö",
"Hotels": "Hotellit",
@@ -151,7 +152,6 @@
"How it works": "Kuinka se toimii",
"Hurry up and use them before they expire!": "Ole nopea ja käytä ne ennen kuin ne vanhenevat!",
"I would like to get my booking confirmation via sms": "Haluan saada varauksen vahvistuksen SMS-viestillä",
"Image gallery": "Kuvagalleria",
"In adults bed": "Aikuisten vuoteessa",
"In crib": "Pinnasängyssä",
"In extra bed": "Oma vuodepaikka",
@@ -408,7 +408,7 @@
"guaranteeing": "varmistetaan",
"guest": "Vieras",
"guests": "Vieraita",
"hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# henkilö} other {# Henkilöä}}",
"hotelPages.rooms.roomCard.persons": "{size} ({totalOccupancy, plural, one {# henkilö} other {# Henkilöä}})",
"hotelPages.rooms.roomCard.seeRoomDetails": "Katso huoneen tiedot",
"km to city center": "km keskustaan",
"lowercase letter": "pien kirjain",

View File

@@ -143,13 +143,13 @@
"Highest level": "Høyeste nivå",
"Hospital": "Sykehus",
"Hotel": "Hotel",
"Hotel Image gallery": "{hotel} - Bildegalleri",
"Hotel facilities": "Hotelfaciliteter",
"Hotel surroundings": "Hotellomgivelser",
"Hotels": "Hoteller",
"How do you want to sleep?": "Hvordan vil du sove?",
"How it works": "Hvordan det fungerer",
"Hurry up and use them before they expire!": "Skynd deg og bruk dem før de utløper!",
"Image gallery": "Bildegalleri",
"In adults bed": "i voksnes seng",
"In crib": "i sprinkelseng",
"In extra bed": "i ekstraseng",
@@ -406,7 +406,7 @@
"guaranteeing": "garantiert",
"guest": "gjest",
"guests": "gjester",
"hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# person} other {# personer}}",
"hotelPages.rooms.roomCard.persons": "{size} ({totalOccupancy, plural, one {# person} other {# personer}})",
"hotelPages.rooms.roomCard.seeRoomDetails": "Se detaljer om rommet",
"km to city center": "km til sentrum",
"lowercase letter": "liten bokstav",

View File

@@ -143,13 +143,13 @@
"Highest level": "Högsta nivå",
"Hospital": "Sjukhus",
"Hotel": "Hotell",
"Hotel Image gallery": "{hotel} - Bildgalleri",
"Hotel facilities": "Hotellfaciliteter",
"Hotel surroundings": "Hotellomgivning",
"Hotels": "Hotell",
"How do you want to sleep?": "Hur vill du sova?",
"How it works": "Hur det fungerar",
"Hurry up and use them before they expire!": "Skynda dig och använd dem innan de går ut!",
"Image gallery": "Bildgalleri",
"In adults bed": "I vuxens säng",
"In crib": "I spjälsäng",
"In extra bed": "Egen sängplats",
@@ -407,7 +407,7 @@
"guaranteeing": "garanterar",
"guest": "gäst",
"guests": "gäster",
"hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# person} other {# personer}}",
"hotelPages.rooms.roomCard.persons": "{size} ({totalOccupancy, plural, one {# person} other {# personer}})",
"hotelPages.rooms.roomCard.seeRoomDetails": "Se information om rummet",
"km to city center": "km till stadens centrum",
"lowercase letter": "liten bokstav",

View File

@@ -3,12 +3,12 @@ import type { GalleryImage } from "@/types/hotel"
export interface LightboxProps {
images: GalleryImage[]
dialogTitle: string /* Accessible title for dialog screen readers */
children: React.ReactNode
onClose: () => void
isOpen: boolean
}
export interface GalleryProps {
images: GalleryImage[]
dialogTitle: string
onClose: () => void
onSelectImage: (image: GalleryImage) => void
onImageClick: () => void