feat(SW-3151): Added original to imageSchema and added transform to a more generic image type

Approved-by: Bianca Widstam
Approved-by: Chuma Mcphoy (We Ahead)
Approved-by: Matilda Landström
This commit is contained in:
Erik Tiekstra
2025-09-10 08:29:05 +00:00
parent a5790ee454
commit f04fe467da
35 changed files with 182 additions and 201 deletions

View File

@@ -45,8 +45,11 @@ function CardContent({ stay }: StayCardProps) {
<article className={styles.stay}> <article className={styles.stay}>
<Image <Image
className={styles.image} className={styles.image}
alt={hotelInformation.hotelContent.images.metaData.altText} alt={
src={hotelInformation.hotelContent.images.imageSizes.small} hotelInformation.hotelContent.images.altText ||
hotelInformation.hotelContent.images.altText_En
}
src={hotelInformation.hotelContent.images.src}
width={420} width={420}
height={240} height={240}
/> />

View File

@@ -41,8 +41,8 @@ export default async function HotelListingItem({
return ( return (
<article className={styles.container}> <article className={styles.container}>
<Image <Image
src={image.imageSizes.large} src={image.src}
alt={image.metaData.altText || image.metaData.altText_En} alt={image.altText || image.altText_En}
width={400} width={400}
height={300} height={300}
sizes="(min-width: 768px) 400px, 100vw" sizes="(min-width: 768px) 400px, 100vw"

View File

@@ -63,10 +63,12 @@ export default function HotelCardCarousel({
} }
function getImage({ hotel }: Pick<HotelListingHotelData, "hotel">) { function getImage({ hotel }: Pick<HotelListingHotelData, "hotel">) {
return { if (hotel.galleryImages?.length) {
src: hotel.galleryImages?.[0]?.imageSizes.large, const image = hotel.galleryImages[0]
alt: return {
hotel.galleryImages?.[0]?.metaData.altText || src: image.src,
hotel.galleryImages?.[0]?.metaData.altText_En, alt: image.altText || image.altText_En,
}
} }
return null
} }

View File

@@ -53,10 +53,12 @@ export function getHotelMapMarkers(hotels: HotelListingHotelData[]) {
} }
function getImage({ hotel }: Pick<HotelListingHotelData, "hotel">) { function getImage({ hotel }: Pick<HotelListingHotelData, "hotel">) {
return { if (hotel.galleryImages?.length) {
src: hotel.galleryImages?.[0]?.imageSizes.large, const image = hotel.galleryImages[0]
alt: return {
hotel.galleryImages?.[0]?.metaData.altText || src: image.src,
hotel.galleryImages?.[0]?.metaData.altText_En, alt: image.altText || image.altText_En,
}
} }
return null
} }

View File

@@ -17,11 +17,11 @@ export default function SidePeekImages({ images }: SidePeekImagesProps) {
return ( return (
<div className={styles.sidePeekImages}> <div className={styles.sidePeekImages}>
{images.map(({ metaData, imageSizes }) => ( {images.map(({ src, altText, altText_En }) => (
<Image <Image
key={imageSizes.tiny} key={src}
src={imageSizes.tiny} src={src}
alt={metaData.altText} alt={altText || altText_En}
height={240} height={240}
width={imageWidth} width={imageWidth}
sizes={sizesString} sizes={sizesString}

View File

@@ -26,8 +26,8 @@ export default async function HeroHeader({
{heroImage ? ( {heroImage ? (
<div className={styles.heroWrapper}> <div className={styles.heroWrapper}>
<Hero <Hero
src={heroImage.imageSizes.medium} src={heroImage.src}
alt={heroImage.metaData.altText || ""} alt={heroImage.altText || heroImage.altText_En}
/> />
</div> </div>
) : null} ) : null}

View File

@@ -20,8 +20,8 @@ export default async function HotelHeader({
<header className={styles.header}> <header className={styles.header}>
<Image <Image
className={styles.hero} className={styles.hero}
alt={image.metaData.altText || image.metaData.altText_En || ""} alt={image.altText || image.altText_En || ""}
src={image.imageSizes.large} src={image.src}
height={200} height={200}
width={1196} width={1196}
/> />

View File

@@ -21,8 +21,8 @@ export default function Promo({
<div className={styles.imageContainer}> <div className={styles.imageContainer}>
<Image <Image
className={styles.image} className={styles.image}
src={image.imageSizes.large} src={image.src}
alt={image.metaData.altText} alt={image.altText || image.altText_En}
fill fill
/> />
</div> </div>

View File

@@ -12,6 +12,7 @@ import { IconButton } from "@scandic-hotels/design-system/IconButton"
import IconChip from "@scandic-hotels/design-system/IconChip" import IconChip from "@scandic-hotels/design-system/IconChip"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Image from "@scandic-hotels/design-system/Image" import Image from "@scandic-hotels/design-system/Image"
import ImageFallback from "@scandic-hotels/design-system/ImageFallback"
import Modal from "@scandic-hotels/design-system/Modal" import Modal from "@scandic-hotels/design-system/Modal"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter" import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
@@ -215,11 +216,11 @@ export default function Room({ booking, roomNr, user }: RoomProps) {
</div> </div>
)} )}
<div className={styles.imageContainer}> <div className={styles.imageContainer}>
<Image {room?.images[0]?.src ? (
src={room?.images[0]?.imageSizes.small ?? ""} <Image src={room.images[0].src} alt={roomName} fill />
alt={roomName} ) : (
fill <ImageFallback />
/> )}
</div> </div>
<div className={styles.details}> <div className={styles.details}>
<div className={styles.row}> <div className={styles.row}>

View File

@@ -1,5 +1,6 @@
"use client" "use client"
import Image from "@scandic-hotels/design-system/Image" import Image from "@scandic-hotels/design-system/Image"
import ImageFallback from "@scandic-hotels/design-system/ImageFallback"
import { useMyStayStore } from "@/stores/my-stay" import { useMyStayStore } from "@/stores/my-stay"
@@ -19,13 +20,17 @@ export default function Img() {
return ( return (
<div className={styles.imageContainer}> <div className={styles.imageContainer}>
<Image {image?.src ? (
alt={roomName} <Image
className={styles.image} alt={roomName}
height={960} className={styles.image}
src={image?.imageSizes.small ?? ""} height={960}
width={640} src={image.src}
/> width={640}
/>
) : (
<ImageFallback height="640px" />
)}
</div> </div>
) )
} }

View File

@@ -168,9 +168,9 @@ export default async function MyStay(props: {
} }
const imageSrc = const imageSrc =
hotel.hotelContent.images.imageSizes.large ?? hotel.hotelContent.images.src ||
additionalData.gallery?.heroImages[0]?.imageSizes.large ?? additionalData.gallery?.heroImages[0]?.src ||
hotel.galleryImages[0]?.imageSizes.large hotel.galleryImages[0]?.src
const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com" const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com"
const promoUrl = new URL(`${baseUrl}/${lang}/`) const promoUrl = new URL(`${baseUrl}/${lang}/`)

View File

@@ -34,10 +34,10 @@ export default function MeetingRoomCard({ room }: MeetingRoomCardProps) {
return ( return (
<article className={styles.card}> <article className={styles.card}>
{image?.imageSizes.small ? ( {image?.src ? (
<Image <Image
src={image?.imageSizes.small} src={image.src}
alt={image?.metaData.altText || image?.metaData.altText_En || ""} alt={image.altText || image.altText_En || ""}
className={styles.image} className={styles.image}
width={386} width={386}
height={200} height={200}

View File

@@ -1,13 +1,8 @@
import type { imageSchema } from "@scandic-hotels/trpc/routers/hotels/schemas/image"
import type { ProductTypeCheque } from "@scandic-hotels/trpc/types/availability" import type { ProductTypeCheque } from "@scandic-hotels/trpc/types/availability"
import type { Amenities } from "@scandic-hotels/trpc/types/hotel" import type { Amenities } from "@scandic-hotels/trpc/types/hotel"
import type { z } from "zod"
import type { Coordinates } from "@/types/components/maps/coordinates" import type { Coordinates } from "@/types/components/maps/coordinates"
type ImageSizes = z.infer<typeof imageSchema>["imageSizes"]
type ImageMetaData = z.infer<typeof imageSchema>["metaData"]
export type HotelPin = { export type HotelPin = {
bookingCode?: string | null bookingCode?: string | null
name: string name: string
@@ -20,8 +15,11 @@ export type HotelPin = {
rateType: string | null rateType: string | null
currency: string currency: string
images: { images: {
imageSizes: ImageSizes src: string
metaData: ImageMetaData altText: string
altText_En: string
title: string
title_En: string
}[] }[]
amenities: Amenities amenities: Amenities
ratings: number | null ratings: number | null

View File

@@ -2,5 +2,4 @@ export interface GalleryImage {
src: string src: string
alt: string alt: string
caption?: string | null caption?: string | null
smallSrc?: string | null
} }

View File

@@ -124,16 +124,16 @@ export function setFacilityCardGrids(
// Can be a maximum 2 images per grid // Can be a maximum 2 images per grid
const img: FacilityImage = { const img: FacilityImage = {
backgroundImage: { backgroundImage: {
url: image.imageSizes.large, url: image.src,
title: image.metaData.title || image.metaData.title_En, title: image.title || image.title_En,
meta: { meta: {
alt: image.metaData.altText, alt: image.altText,
caption: image.metaData.altText_En, caption: image.altText_En,
}, },
id: image.imageSizes.large, id: image.src,
}, },
theme: "image", theme: "image",
id: image.imageSizes.large, id: image.src,
} }
return img return img
}) })

View File

@@ -5,14 +5,13 @@ import type { GalleryImage } from "@/types/components/imageGallery"
function mapApiImageToGalleryImage(apiImage: ApiImage): GalleryImage { function mapApiImageToGalleryImage(apiImage: ApiImage): GalleryImage {
return { return {
src: apiImage.imageSizes.medium, src: apiImage.src,
alt: alt:
apiImage.metaData.altText || apiImage.altText ||
apiImage.metaData.altText_En || apiImage.altText_En ||
apiImage.metaData.title || apiImage.title ||
apiImage.metaData.title_En, apiImage.title_En,
caption: apiImage.metaData.title || apiImage.metaData.title_En, caption: apiImage.title || apiImage.title_En,
smallSrc: apiImage.imageSizes.small,
} }
} }

View File

@@ -61,8 +61,8 @@ export function generateHotelSchema(hotelData: HotelData) {
if (image) { if (image) {
jsonLd.image = { jsonLd.image = {
"@type": "ImageObject", "@type": "ImageObject",
url: image.imageSizes.small, url: image.src,
caption: image.metaData.title || image.metaData.title_En, caption: image.title || image.title_En,
} }
} }

View File

@@ -24,11 +24,8 @@ export function getImage(data: RawMetadataSchema) {
const restaurantImage = restaurantSubPage?.content?.images?.[0] const restaurantImage = restaurantSubPage?.content?.images?.[0]
if (restaurantImage) { if (restaurantImage) {
subpageImage = { subpageImage = {
url: restaurantImage.imageSizes.small, url: restaurantImage.src,
alt: alt: restaurantImage.altText || restaurantImage.altText_En || "",
restaurantImage.metaData.altText ||
restaurantImage.metaData.altText_En ||
"",
} }
} }
@@ -38,11 +35,8 @@ export function getImage(data: RawMetadataSchema) {
data.additionalHotelData?.parkingImages?.heroImages[0] data.additionalHotelData?.parkingImages?.heroImages[0]
if (parkingImage) { if (parkingImage) {
subpageImage = { subpageImage = {
url: parkingImage.imageSizes.small, url: parkingImage.src,
alt: alt: parkingImage.altText || parkingImage.altText_En || "",
parkingImage.metaData.altText ||
parkingImage.metaData.altText_En ||
"",
} }
} }
break break
@@ -52,11 +46,8 @@ export function getImage(data: RawMetadataSchema) {
)?.content.images[0] )?.content.images[0]
if (wellnessImage) { if (wellnessImage) {
subpageImage = { subpageImage = {
url: wellnessImage.imageSizes.small, url: wellnessImage.src,
alt: alt: wellnessImage.altText || wellnessImage.altText_En || "",
wellnessImage.metaData.altText ||
wellnessImage.metaData.altText_En ||
"",
} }
} }
break break
@@ -65,10 +56,10 @@ export function getImage(data: RawMetadataSchema) {
data.additionalHotelData?.accessibility?.heroImages[0] data.additionalHotelData?.accessibility?.heroImages[0]
if (accessibilityImage) { if (accessibilityImage) {
subpageImage = { subpageImage = {
url: accessibilityImage.imageSizes.small, url: accessibilityImage.src,
alt: alt:
accessibilityImage.metaData.altText || accessibilityImage.altText ||
accessibilityImage.metaData.altText_En || accessibilityImage.altText_En ||
"", "",
} }
} }
@@ -78,11 +69,8 @@ export function getImage(data: RawMetadataSchema) {
data.additionalHotelData?.conferencesAndMeetings?.heroImages[0] data.additionalHotelData?.conferencesAndMeetings?.heroImages[0]
if (meetingImage) { if (meetingImage) {
subpageImage = { subpageImage = {
url: meetingImage.imageSizes.small, url: meetingImage.src,
alt: alt: meetingImage.altText || meetingImage.altText_En || "",
meetingImage.metaData.altText ||
meetingImage.metaData.altText_En ||
"",
} }
} }
break break
@@ -100,8 +88,8 @@ export function getImage(data: RawMetadataSchema) {
data.additionalHotelData?.gallery?.smallerImages?.[0] data.additionalHotelData?.gallery?.smallerImages?.[0]
if (hotelImage) { if (hotelImage) {
return { return {
url: hotelImage.imageSizes.small, url: hotelImage.src,
alt: hotelImage.metaData.altText || undefined, alt: hotelImage.altText || undefined,
} }
} }
} }

View File

@@ -10,6 +10,7 @@ import {
import { dt } from "@scandic-hotels/common/dt" import { dt } from "@scandic-hotels/common/dt"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Image from "@scandic-hotels/design-system/Image" import Image from "@scandic-hotels/design-system/Image"
import ImageFallback from "@scandic-hotels/design-system/ImageFallback"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { getHotelRoom } from "@scandic-hotels/trpc/routers/booking/helpers" import { getHotelRoom } from "@scandic-hotels/trpc/routers/booking/helpers"
@@ -114,16 +115,20 @@ export function Room({
)} )}
</header> </header>
<div className={styles.booking}> <div className={styles.booking}>
<Image {img?.src ? (
alt={img?.metaData.altText ?? ""} <Image
className={styles.img} alt={img.altText || img.altText_En || ""}
focalPoint={{ x: 50, y: 50 }} className={styles.img}
height={204} focalPoint={{ x: 50, y: 50 }}
src={img?.imageSizes.medium ?? ""} height={204}
style={{ borderRadius: "var(--Corner-radius-md)" }} src={img.src}
title={img?.metaData.title || img?.metaData.title_En || ""} style={{ borderRadius: "var(--Corner-radius-md)" }}
width={204} title={img.title || img.title_En || ""}
/> width={204}
/>
) : (
<ImageFallback height="204px" />
)}
<div className={styles.roomDetails}> <div className={styles.roomDetails}>
<div className={styles.roomName}> <div className={styles.roomName}>
<Typography variant="Title/Subtitle/md"> <Typography variant="Title/Subtitle/md">

View File

@@ -5,9 +5,7 @@ import type { z } from "zod"
import type { HotelResponse } from "../SelectHotel/helpers" import type { HotelResponse } from "../SelectHotel/helpers"
type ImageSizes = z.infer<typeof imageSchema>["imageSizes"] type ApiImage = z.infer<typeof imageSchema>
type ImageMetaData = z.infer<typeof imageSchema>["metaData"]
interface Coordinates { interface Coordinates {
lat: number lat: number
lng: number lng: number
@@ -24,10 +22,7 @@ export type HotelPin = {
voucherPrice: number | null voucherPrice: number | null
rateType: string | null rateType: string | null
currency: string currency: string
images: { images: ApiImage[]
imageSizes: ImageSizes
metaData: ImageMetaData
}[]
amenities: Amenities amenities: Amenities
ratings: number | null ratings: number | null
operaId: string operaId: string

View File

@@ -53,8 +53,8 @@ export default function ListingHotelCardDialog({
voucherPrice, voucherPrice,
hasEnoughPoints, hasEnoughPoints,
} = data } = data
const firstImage = images[0]?.imageSizes?.small const imageSrc = images[0]?.src
const altText = images[0]?.metaData?.altText const altText = images[0]?.altText || images[0]?.altText_En
const notEnoughPointsLabel = intl.formatMessage({ const notEnoughPointsLabel = intl.formatMessage({
defaultMessage: "Not enough points", defaultMessage: "Not enough points",
@@ -77,7 +77,7 @@ export default function ListingHotelCardDialog({
<div className={styles.content}> <div className={styles.content}>
<div className={styles.header}> <div className={styles.header}>
<HotelCardDialogImage <HotelCardDialogImage
firstImage={firstImage} imageSrc={imageSrc}
altText={altText} altText={altText}
rating={{ tripAdvisor: ratings }} rating={{ tripAdvisor: ratings }}
imageError={imageError} imageError={imageError}

View File

@@ -162,13 +162,12 @@ export function RoomSidePeekContent({ room }: RoomSidePeekContentProps) {
function mapApiImagesToGalleryImages(apiImages: ApiImage[]) { function mapApiImagesToGalleryImages(apiImages: ApiImage[]) {
return apiImages.map((apiImage) => ({ return apiImages.map((apiImage) => ({
src: apiImage.imageSizes.medium, src: apiImage.src,
alt: alt:
apiImage.metaData.altText || apiImage.altText ||
apiImage.metaData.altText_En || apiImage.altText_En ||
apiImage.metaData.title || apiImage.title ||
apiImage.metaData.title_En, apiImage.title_En,
caption: apiImage.metaData.title || apiImage.metaData.title_En, caption: apiImage.title || apiImage.title_En,
smallSrc: apiImage.imageSizes.small,
})) }))
} }

View File

@@ -46,6 +46,8 @@ export function SelectedRoomPanel({ roomIndex }: { roomIndex: number }) {
isMainRoom || isMainRoom ||
(!isMainRoom && selectedRates.rates.slice(0, roomNr).every((room) => room)) (!isMainRoom && selectedRates.rates.slice(0, roomNr).every((room) => room))
const image = images?.[0]
return ( return (
<div className={styles.selectedRoomPanel}> <div className={styles.selectedRoomPanel}>
<div className={styles.content}> <div className={styles.content}>
@@ -64,16 +66,17 @@ export function SelectedRoomPanel({ roomIndex }: { roomIndex: number }) {
<Body color="uiTextHighContrast">{selectedProductTitle}</Body> <Body color="uiTextHighContrast">{selectedProductTitle}</Body>
</div> </div>
<div className={styles.imageContainer}> <div className={styles.imageContainer}>
{images?.[0]?.imageSizes?.tiny ? ( {image?.src ? (
<Image <Image
alt={ alt={
selectedRate.roomInfo.roomType ?? selectedRate.roomInfo.roomType ??
images[0].metaData?.altText ?? image.altText ??
image.altText_En ??
"" ""
} }
className={styles.img} className={styles.img}
height={300} height={300}
src={images[0].imageSizes.tiny} src={image.src}
width={600} width={600}
/> />
) : null} ) : null}

View File

@@ -3,14 +3,13 @@ import type { ApiImage } from "@scandic-hotels/trpc/types/hotel"
export function mapApiImagesToGalleryImages(apiImages: ApiImage[]) { export function mapApiImagesToGalleryImages(apiImages: ApiImage[]) {
return apiImages.map((apiImage) => { return apiImages.map((apiImage) => {
return { return {
src: apiImage.imageSizes.medium, src: apiImage.src,
alt: alt:
apiImage.metaData.altText || apiImage.altText ||
apiImage.metaData.altText_En || apiImage.altText_En ||
apiImage.metaData.title || apiImage.title ||
apiImage.metaData.title_En, apiImage.title_En,
caption: apiImage.metaData.title || apiImage.metaData.title_En, caption: apiImage.title || apiImage.title_En,
smallSrc: apiImage.imageSizes.small,
} }
}) })
} }

View File

@@ -4,17 +4,8 @@ export type PromoProps = {
text: string text: string
title: string title: string
image?: { image?: {
imageSizes: { src: string
large: string altText: string
medium: string altText_En: string
small: string
tiny: string
}
metaData: {
altText: string
altText_En: string
copyRight: string
title: string
}
} }
} }

View File

@@ -1,8 +1,8 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { HotelCard } from './index' import { HotelCard } from './index'
import { fn } from 'storybook/test'
import { RateTypeEnum } from '@scandic-hotels/common/constants/rateType' import { RateTypeEnum } from '@scandic-hotels/common/constants/rateType'
import { fn } from 'storybook/test'
import { Button } from '../Button' import { Button } from '../Button'
import { MaterialIcon } from '../Icons/MaterialIcon' import { MaterialIcon } from '../Icons/MaterialIcon'
@@ -70,7 +70,6 @@ export const Default: Story = {
{ {
src: 'img/img2.jpg', src: 'img/img2.jpg',
alt: 'Alt text', alt: 'Alt text',
smallSrc: 'img/img2.jpg',
caption: 'Caption', caption: 'Caption',
}, },
], ],
@@ -97,3 +96,10 @@ export const MapListing: Story = {
type: 'mapListing', type: 'mapListing',
}, },
} }
export const WithoutImage: Story = {
args: {
...Default.args,
images: [],
},
}

View File

@@ -1,12 +1,12 @@
import Image from '../../Image' import Image from '../../Image'
import { hotelCardDialogImageVariants } from './variants'
import { TripAdvisorChip } from '../../TripAdvisorChip' import { TripAdvisorChip } from '../../TripAdvisorChip'
import { hotelCardDialogImageVariants } from './variants'
import styles from './hotelCardDialogImage.module.css' import ImageFallback from '../../ImageFallback'
export type HotelCardDialogImageProps = { export type HotelCardDialogImageProps = {
firstImage?: string imageSrc?: string
altText?: string altText?: string
rating?: { tripAdvisor?: number | null } rating?: { tripAdvisor?: number | null }
imageError: boolean imageError: boolean
@@ -15,7 +15,7 @@ export type HotelCardDialogImageProps = {
} }
export function HotelCardDialogImage({ export function HotelCardDialogImage({
firstImage, imageSrc,
altText, altText,
rating, rating,
imageError, imageError,
@@ -26,11 +26,11 @@ export function HotelCardDialogImage({
return ( return (
<div className={classNames}> <div className={classNames}>
{!firstImage || imageError ? ( {!imageSrc || imageError ? (
<div className={styles.imagePlaceholder} /> <ImageFallback />
) : ( ) : (
<Image <Image
src={firstImage} src={imageSrc}
alt={altText || ''} alt={altText || ''}
fill fill
onError={() => setImageError(true)} onError={() => setImageError(true)}

View File

@@ -14,14 +14,14 @@ import { OldDSButton as Button } from '../../../OldDSButton'
import Subtitle from '../../../Subtitle' import Subtitle from '../../../Subtitle'
import { Typography } from '../../../Typography' import { Typography } from '../../../Typography'
import { NoPriceAvailableCard } from '../../NoPriceAvailableCard'
import { HotelCardDialogImage } from '../../HotelCardDialogImage' import { HotelCardDialogImage } from '../../HotelCardDialogImage'
import { NoPriceAvailableCard } from '../../NoPriceAvailableCard'
import styles from './standaloneHotelCardDialog.module.css'
import { Lang } from '@scandic-hotels/common/constants/language' import { Lang } from '@scandic-hotels/common/constants/language'
import { HotelPin } from '../../../Map/types'
import { FacilityToIcon } from '../../../FacilityToIcon' import { FacilityToIcon } from '../../../FacilityToIcon'
import { HotelPin } from '../../../Map/types'
import { HotelPointsRow } from '../../HotelPointsRow' import { HotelPointsRow } from '../../HotelPointsRow'
import styles from './standaloneHotelCardDialog.module.css'
interface StandaloneHotelCardProps { interface StandaloneHotelCardProps {
data: HotelPin data: HotelPin
@@ -75,7 +75,7 @@ export function StandaloneHotelCardDialog({
<MaterialIcon icon="close" size={22} color="CurrentColor" /> <MaterialIcon icon="close" size={22} color="CurrentColor" />
</IconButton> </IconButton>
<HotelCardDialogImage <HotelCardDialogImage
firstImage={image?.url} imageSrc={image?.url}
altText={image?.alt} altText={image?.alt}
rating={{ tripAdvisor: ratings?.tripAdvisor ?? null }} rating={{ tripAdvisor: ratings?.tripAdvisor ?? null }}
imageError={imageError} imageError={imageError}

View File

@@ -1,11 +1,11 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { HotelInfoCard } from './index'
import { FacilityEnum } from '@scandic-hotels/common/constants/facilities'
import { AlertTypeEnum } from '@scandic-hotels/common/constants/alert' import { AlertTypeEnum } from '@scandic-hotels/common/constants/alert'
import { Button } from '../Button' import { FacilityEnum } from '@scandic-hotels/common/constants/facilities'
import { fn } from 'storybook/test' import { fn } from 'storybook/test'
import { Button } from '../Button'
import { MaterialIcon } from '../Icons/MaterialIcon' import { MaterialIcon } from '../Icons/MaterialIcon'
import { HotelInfoCard } from './index'
const meta: Meta<typeof HotelInfoCard> = { const meta: Meta<typeof HotelInfoCard> = {
title: 'Components/HotelInfoCard', title: 'Components/HotelInfoCard',
component: HotelInfoCard, component: HotelInfoCard,
@@ -104,19 +104,16 @@ export const Default: Story = {
{ {
src: './img/GrandHotelBudapest.png', src: './img/GrandHotelBudapest.png',
alt: 'Grand Hotel Budapest', alt: 'Grand Hotel Budapest',
smallSrc: './img/GrandHotelBudapest.png',
caption: 'Grand Hotel Budapest', caption: 'Grand Hotel Budapest',
}, },
{ {
src: './img/img1.png', src: './img/img1.png',
alt: 'Image 1', alt: 'Image 1',
smallSrc: './img/img1.png',
caption: 'Image 1', caption: 'Image 1',
}, },
{ {
src: './img/img2.png', src: './img/img2.png',
alt: 'Image 2', alt: 'Image 2',
smallSrc: './img/img2.png',
caption: 'Image 2', caption: 'Image 2',
}, },
], ],

View File

@@ -7,8 +7,8 @@ import { useIntl } from 'react-intl'
import { MaterialIcon } from '../Icons/MaterialIcon' import { MaterialIcon } from '../Icons/MaterialIcon'
import Image from '../Image' import Image from '../Image'
import ImageFallback from '../ImageFallback' import ImageFallback from '../ImageFallback'
import { Typography } from '../Typography'
import Lightbox from '../Lightbox' import Lightbox from '../Lightbox'
import { Typography } from '../Typography'
import styles from './imageGallery.module.css' import styles from './imageGallery.module.css'
@@ -16,7 +16,6 @@ export interface GalleryImage {
src: string src: string
alt: string alt: string
caption?: string | null caption?: string | null
smallSrc?: string | null
} }
type ImageGalleryProps = { type ImageGalleryProps = {
@@ -50,7 +49,7 @@ function ImageGallery({
} }
: { height, width: height * 1.5 } : { height, width: height * 1.5 }
if (!images || images.length === 0 || imageError) { if (!images?.length || imageError) {
return <ImageFallback /> return <ImageFallback />
} }

View File

@@ -10,8 +10,8 @@ import { Typography } from '../../Typography'
import Image from '../../Image' import Image from '../../Image'
import styles from './gallery.module.css'
import { LightboxImage } from '..' import { LightboxImage } from '..'
import styles from './gallery.module.css'
type GalleryProps = { type GalleryProps = {
images: LightboxImage[] images: LightboxImage[]
@@ -164,7 +164,7 @@ export default function Gallery({
<AnimatePresence initial={false}> <AnimatePresence initial={false}>
{getThumbImages().map((image, index) => ( {getThumbImages().map((image, index) => (
<motion.div <motion.div
key={image.smallSrc || image.src} key={image.src}
className={styles.thumbnailContainer} className={styles.thumbnailContainer}
initial={{ opacity: 0, x: 50 }} initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
@@ -179,7 +179,7 @@ export default function Gallery({
})} })}
> >
<Image <Image
src={image.smallSrc || image.src} src={image.src}
alt={image.alt} alt={image.alt}
fill fill
sizes="200px" sizes="200px"
@@ -196,7 +196,7 @@ export default function Gallery({
<div className={styles.mobileGallery}> <div className={styles.mobileGallery}>
{images.map((image, index) => ( {images.map((image, index) => (
<motion.div <motion.div
key={image.smallSrc || image.src} key={image.src}
className={`${styles.thumbnailContainer} ${index % 3 === 0 ? styles.fullWidthImage : ''}`} className={`${styles.thumbnailContainer} ${index % 3 === 0 ? styles.fullWidthImage : ''}`}
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
@@ -211,7 +211,7 @@ export default function Gallery({
}} }}
> >
<Image <Image
src={image.smallSrc || image.src} src={image.src}
alt={image.alt} alt={image.alt}
fill fill
sizes="100vw" sizes="100vw"

View File

@@ -12,7 +12,6 @@ export type LightboxImage = {
src: string src: string
alt: string alt: string
caption?: string | null caption?: string | null
smallSrc?: string | null
} }
type LightboxProps = { type LightboxProps = {

View File

@@ -526,7 +526,7 @@ export const ancillaryPackagesSchema = z
id: item.id, id: item.id,
title: item.title, title: item.title,
description: item.descriptions.html, description: item.descriptions.html,
imageUrl: item.images[0]?.imageSizes.small || undefined, imageUrl: item.images[0]?.src || undefined,
price: { price: {
total: item.variants.ancillary.price.totalPrice, total: item.variants.ancillary.price.totalPrice,
currency: item.variants.ancillary.price.currency, currency: item.variants.ancillary.price.currency,

View File

@@ -2,46 +2,37 @@ import { z } from "zod"
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator" import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
export const imageSizesSchema = z.object({
large: nullableStringValidator,
medium: nullableStringValidator,
small: nullableStringValidator,
tiny: nullableStringValidator,
})
export const imageMetaDataSchema = z.object({
altText: nullableStringValidator,
altText_En: nullableStringValidator,
copyRight: nullableStringValidator,
title: nullableStringValidator,
title_En: nullableStringValidator,
})
const DEFAULT_IMAGE_OBJ = { const DEFAULT_IMAGE_OBJ = {
metaData: { altText: "Default image",
altText: "Default image", altText_En: "Default image",
altText_En: "Default image", title: "Default image",
copyRight: "Default image", title_En: "Default image",
title: "Default image", src: "https://placehold.co/1280x720",
title_En: "Default image",
},
imageSizes: {
tiny: "https://placehold.co/1280x720",
small: "https://placehold.co/1280x720",
medium: "https://placehold.co/1280x720",
large: "https://placehold.co/1280x720",
},
} }
export const imageSchema = z export const imageSchema = z
.object({ .object({
imageSizes: imageSizesSchema, imageSizes: z.object({
metaData: imageMetaDataSchema, original: nullableStringValidator,
}),
metaData: z.object({
altText: nullableStringValidator,
altText_En: nullableStringValidator,
copyRight: nullableStringValidator,
title: nullableStringValidator,
title_En: nullableStringValidator,
}),
}) })
.nullish() .nullish()
.transform((val) => { .transform((val) => {
if (!val) { if (!val) {
return DEFAULT_IMAGE_OBJ return DEFAULT_IMAGE_OBJ
} }
return val return {
src: val.imageSizes.original,
altText: val.metaData.altText,
altText_En: val.metaData.altText_En,
title: val.metaData.title,
title_En: val.metaData.title_En,
}
}) })

View File

@@ -5,7 +5,7 @@ import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { BreakfastPackageEnum } from "../../../enums/breakfast" import { BreakfastPackageEnum } from "../../../enums/breakfast"
import { PackageTypeEnum } from "../../../enums/packages" import { PackageTypeEnum } from "../../../enums/packages"
import { RoomPackageCodeEnum } from "../../../enums/roomFilter" import { RoomPackageCodeEnum } from "../../../enums/roomFilter"
import { imageSizesSchema } from "./image" import { imageSchema } from "./image"
// TODO: Remove optional and default when the API change has been deployed // TODO: Remove optional and default when the API change has been deployed
export const packagePriceSchema = z export const packagePriceSchema = z
@@ -38,7 +38,7 @@ export const ancillaryContentSchema = z.object({
}), }),
title: z.string(), title: z.string(),
descriptions: z.object({ html: z.string() }), descriptions: z.object({ html: z.string() }),
images: z.array(z.object({ imageSizes: imageSizesSchema })), images: z.array(imageSchema),
requiresDeliveryTime: z.boolean(), requiresDeliveryTime: z.boolean(),
}) })