Merged master into feat/1268-mask-private-data
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { ScandicLogoIcon } from "@/components/Icons"
|
||||
import HotelLogo from "@/components/Icons/Logos"
|
||||
import Image from "@/components/Image"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
@@ -10,25 +10,27 @@ import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { getIntl } from "@/i18n"
|
||||
import getSingleDecimal from "@/utils/numberFormatting"
|
||||
|
||||
import { getTypeSpecificInformation } from "./utils"
|
||||
|
||||
import styles from "./hotelListingItem.module.css"
|
||||
|
||||
import type { HotelListingItemProps } from "@/types/components/contentPage/hotelListingItem"
|
||||
|
||||
export default async function HotelListingItem({
|
||||
imageUrl,
|
||||
altText,
|
||||
name,
|
||||
address,
|
||||
distanceToCentre,
|
||||
description,
|
||||
link,
|
||||
hotel,
|
||||
contentType = "hotel",
|
||||
url,
|
||||
}: HotelListingItemProps) {
|
||||
const intl = await getIntl()
|
||||
const { description, imageSrc, altText } = getTypeSpecificInformation(
|
||||
contentType,
|
||||
hotel
|
||||
)
|
||||
|
||||
return (
|
||||
<article className={styles.container}>
|
||||
<Image
|
||||
src={imageUrl}
|
||||
src={imageSrc}
|
||||
alt={altText}
|
||||
width={300}
|
||||
height={200}
|
||||
@@ -36,35 +38,43 @@ export default async function HotelListingItem({
|
||||
/>
|
||||
<section className={styles.content}>
|
||||
<div className={styles.intro}>
|
||||
<ScandicLogoIcon color="red" />
|
||||
<HotelLogo hotelId={hotel.operaId} hotelType={hotel.hotelType} />
|
||||
<Subtitle asChild>
|
||||
<Title as="h3">{name}</Title>
|
||||
<Title as="h3">{hotel.name}</Title>
|
||||
</Subtitle>
|
||||
<div className={styles.captions}>
|
||||
<Caption color="uiTextPlaceholder">{address}</Caption>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{hotel.address.streetAddress}
|
||||
</Caption>
|
||||
<div className={styles.dividerContainer}>
|
||||
<Divider variant="vertical" color="beige" />
|
||||
</div>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{intl.formatMessage(
|
||||
{ id: "Distance in km to city centre" },
|
||||
{ number: getSingleDecimal(distanceToCentre / 1000) }
|
||||
{
|
||||
number: getSingleDecimal(
|
||||
hotel.location.distanceToCentre / 1000
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
</div>
|
||||
<Body>{description}</Body>
|
||||
<Button
|
||||
intent="primary"
|
||||
theme="base"
|
||||
size="small"
|
||||
className={styles.button}
|
||||
asChild
|
||||
>
|
||||
<Link href={link} color="white">
|
||||
{intl.formatMessage({ id: "See hotel details" })}
|
||||
</Link>
|
||||
</Button>
|
||||
{url && (
|
||||
<Button
|
||||
intent="primary"
|
||||
theme="base"
|
||||
size="small"
|
||||
className={styles.button}
|
||||
asChild
|
||||
>
|
||||
<Link href={url} color="white">
|
||||
{intl.formatMessage({ id: "See hotel details" })}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</section>
|
||||
</article>
|
||||
)
|
||||
36
components/Blocks/HotelListing/HotelListingItem/utils.ts
Normal file
36
components/Blocks/HotelListing/HotelListingItem/utils.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
import type { HotelListing } from "@/types/trpc/routers/contentstack/blocks"
|
||||
|
||||
export function getTypeSpecificInformation(
|
||||
contentType: HotelListing["contentType"],
|
||||
hotel: Hotel
|
||||
) {
|
||||
const { restaurantsOverviewPage, images } = hotel.hotelContent
|
||||
const { descriptions, meetingDescription } = hotel.hotelContent.texts
|
||||
const hotelData = {
|
||||
description: descriptions.short,
|
||||
imageSrc: images.imageSizes.small,
|
||||
altText: images.metaData.altText,
|
||||
}
|
||||
switch (contentType) {
|
||||
case "meeting":
|
||||
const meetingImage = hotel.conferencesAndMeetings?.heroImages[0]
|
||||
return {
|
||||
description: meetingDescription?.short || hotelData.description,
|
||||
imageSrc: meetingImage?.imageSizes.small || hotelData.imageSrc,
|
||||
altText: meetingImage?.metaData.altText || hotelData.altText,
|
||||
}
|
||||
case "restaurant":
|
||||
const restaurantImage = hotel.restaurantImages?.heroImages[0]
|
||||
return {
|
||||
description:
|
||||
restaurantsOverviewPage.restaurantsContentDescriptionShort ||
|
||||
hotelData.description,
|
||||
imageSrc: restaurantImage?.imageSizes.small || hotelData.imageSrc,
|
||||
altText: restaurantImage?.metaData.altText || hotelData.altText,
|
||||
}
|
||||
case "hotel":
|
||||
default:
|
||||
return hotelData
|
||||
}
|
||||
}
|
||||
40
components/Blocks/HotelListing/index.tsx
Normal file
40
components/Blocks/HotelListing/index.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { getHotels } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import SectionContainer from "@/components/Section/Container"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import HotelListingItem from "./HotelListingItem"
|
||||
|
||||
import type { HotelListingProps } from "@/types/components/blocks/hotelListing"
|
||||
|
||||
export default async function HotelListing({
|
||||
heading,
|
||||
locationFilter,
|
||||
hotelsToInclude,
|
||||
contentType,
|
||||
}: HotelListingProps) {
|
||||
const hotels = await getHotels({
|
||||
locationFilter,
|
||||
hotelsToInclude: hotelsToInclude,
|
||||
})
|
||||
|
||||
if (!hotels.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionContainer>
|
||||
<Title level="h4" as="h3" textTransform="capitalize">
|
||||
{heading}
|
||||
</Title>
|
||||
{hotels.map(({ data, url }) => (
|
||||
<HotelListingItem
|
||||
key={data.name}
|
||||
hotel={data}
|
||||
contentType={contentType}
|
||||
url={url}
|
||||
/>
|
||||
))}
|
||||
</SectionContainer>
|
||||
)
|
||||
}
|
||||
@@ -6,13 +6,14 @@ import UspGrid from "@/components/Blocks/UspGrid"
|
||||
import JsonToHtml from "@/components/JsonToHtml"
|
||||
|
||||
import AccordionSection from "./Accordion"
|
||||
import HotelListing from "./HotelListing"
|
||||
import Table from "./Table"
|
||||
|
||||
import type { BlocksProps } from "@/types/components/blocks"
|
||||
import { BlocksEnums } from "@/types/enums/blocks"
|
||||
|
||||
export default function Blocks({ blocks }: BlocksProps) {
|
||||
return blocks.map((block, idx) => {
|
||||
return blocks.map(async (block, idx) => {
|
||||
const firstItem = idx === 0
|
||||
switch (block.typename) {
|
||||
case BlocksEnums.block.Accordion:
|
||||
@@ -48,6 +49,21 @@ export default function Blocks({ blocks }: BlocksProps) {
|
||||
key={`${block.dynamic_content.title}-${idx}`}
|
||||
/>
|
||||
)
|
||||
case BlocksEnums.block.HotelListing:
|
||||
const { heading, contentType, locationFilter, hotelsToInclude } =
|
||||
block.hotel_listing
|
||||
if (!locationFilter && !hotelsToInclude.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<HotelListing
|
||||
heading={heading}
|
||||
locationFilter={locationFilter}
|
||||
hotelsToInclude={hotelsToInclude}
|
||||
contentType={contentType}
|
||||
/>
|
||||
)
|
||||
case BlocksEnums.block.Shortcuts:
|
||||
return (
|
||||
<ShortcutsList
|
||||
|
||||
@@ -32,24 +32,28 @@ export default function PreviewImages({
|
||||
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: "Image gallery" },
|
||||
{ name: hotelName }
|
||||
)}
|
||||
isOpen={lightboxIsOpen}
|
||||
onClose={() => setLightboxIsOpen(false)}
|
||||
/>
|
||||
{images.length > 1 && (
|
||||
<>
|
||||
<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: "Image gallery" },
|
||||
{ name: hotelName }
|
||||
)}
|
||||
isOpen={lightboxIsOpen}
|
||||
onClose={() => setLightboxIsOpen(false)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||
import type { BedTypeInfoProps } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
|
||||
export default function BedTypeInfo({ hasMultipleBedTypes }: BedTypeInfoProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
const hasChildWithExtraBed = useEnterDetailsStore((state) =>
|
||||
state.booking.rooms[0].children?.some(
|
||||
(child) => Number(child.bed) === ChildBedMapEnum.IN_EXTRA_BED
|
||||
)
|
||||
)
|
||||
|
||||
const availabilityText = intl.formatMessage({
|
||||
id: "Your selected bed type will be provided based on availability",
|
||||
})
|
||||
|
||||
const extraBedText = intl.formatMessage({
|
||||
id: "Extra bed will be provided additionally",
|
||||
})
|
||||
|
||||
if (hasMultipleBedTypes && hasChildWithExtraBed) {
|
||||
return (
|
||||
<Body>
|
||||
{availabilityText}. {extraBedText}
|
||||
</Body>
|
||||
)
|
||||
}
|
||||
|
||||
if (hasMultipleBedTypes) {
|
||||
return <Body>{availabilityText}</Body>
|
||||
}
|
||||
|
||||
if (hasChildWithExtraBed) {
|
||||
return <Body>{extraBedText}</Body>
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.form {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
import { KingBedIcon } from "@/components/Icons"
|
||||
import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio"
|
||||
|
||||
import BedTypeInfo from "./BedTypeInfo"
|
||||
import { bedTypeFormSchema } from "./schema"
|
||||
|
||||
import styles from "./bedOptions.module.css"
|
||||
@@ -62,26 +63,29 @@ export default function BedType({ bedTypes }: BedTypeProps) {
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}>
|
||||
{bedTypes.map((roomType) => {
|
||||
const width =
|
||||
roomType.size.max === roomType.size.min
|
||||
? `${roomType.size.min} cm`
|
||||
: `${roomType.size.min} cm - ${roomType.size.max} cm`
|
||||
return (
|
||||
<RadioCard
|
||||
key={roomType.value}
|
||||
Icon={KingBedIcon}
|
||||
iconWidth={46}
|
||||
id={roomType.value}
|
||||
name="bedType"
|
||||
subtitle={width}
|
||||
title={roomType.description}
|
||||
value={roomType.value}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</form>
|
||||
<div className={styles.container}>
|
||||
<BedTypeInfo hasMultipleBedTypes={bedTypes.length > 1} />
|
||||
<form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}>
|
||||
{bedTypes.map((roomType) => {
|
||||
const width =
|
||||
roomType.size.max === roomType.size.min
|
||||
? `${roomType.size.min} cm`
|
||||
: `${roomType.size.min} cm - ${roomType.size.max} cm`
|
||||
return (
|
||||
<RadioCard
|
||||
key={roomType.value}
|
||||
Icon={KingBedIcon}
|
||||
iconWidth={46}
|
||||
id={roomType.value}
|
||||
name="bedType"
|
||||
subtitle={width}
|
||||
title={roomType.description}
|
||||
value={roomType.value}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</form>
|
||||
</div>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import Modal from "../../Modal"
|
||||
|
||||
import styles from "./ui.module.css"
|
||||
|
||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||
import type { SummaryProps } from "@/types/components/hotelReservation/summary"
|
||||
import type { DetailsState } from "@/types/stores/enter-details"
|
||||
|
||||
@@ -67,6 +68,25 @@ export default function SummaryUI({
|
||||
const adults = booking.rooms[0].adults
|
||||
const children = booking.rooms[0].children
|
||||
|
||||
const childrenBeds = children?.reduce(
|
||||
(acc, value) => {
|
||||
const bedType = Number(value.bed)
|
||||
if (bedType === ChildBedMapEnum.IN_ADULTS_BED) {
|
||||
return acc
|
||||
}
|
||||
const count = acc.get(bedType) ?? 0
|
||||
acc.set(bedType, count + 1)
|
||||
return acc
|
||||
},
|
||||
new Map<ChildBedMapEnum, number>([
|
||||
[ChildBedMapEnum.IN_CRIB, 0],
|
||||
[ChildBedMapEnum.IN_EXTRA_BED, 0],
|
||||
])
|
||||
)
|
||||
|
||||
const childBedCrib = childrenBeds?.get(ChildBedMapEnum.IN_CRIB)
|
||||
const childBedExtraBed = childrenBeds?.get(ChildBedMapEnum.IN_EXTRA_BED)
|
||||
|
||||
const memberPrice = roomRate.memberRate
|
||||
? {
|
||||
currency: roomRate.memberRate.localPrice.currency,
|
||||
@@ -179,12 +199,7 @@ export default function SummaryUI({
|
||||
: null}
|
||||
{bedType ? (
|
||||
<div className={styles.entry}>
|
||||
<div>
|
||||
<Body color="uiTextHighContrast">{bedType.description}</Body>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage({ id: "Based on availability" })}
|
||||
</Caption>
|
||||
</div>
|
||||
<Body color="uiTextHighContrast">{bedType.description}</Body>
|
||||
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatNumber(0, {
|
||||
@@ -194,7 +209,39 @@ export default function SummaryUI({
|
||||
</Body>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{childBedCrib ? (
|
||||
<div className={styles.entry}>
|
||||
<div>
|
||||
<Body color="uiTextHighContrast">
|
||||
{`${intl.formatMessage({ id: "Crib (child)" })} × ${childBedCrib}`}
|
||||
</Body>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage({ id: "Based on availability" })}
|
||||
</Caption>
|
||||
</div>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatNumber(0, {
|
||||
currency: roomPrice.local.currency,
|
||||
style: "currency",
|
||||
})}
|
||||
</Body>
|
||||
</div>
|
||||
) : null}
|
||||
{childBedExtraBed ? (
|
||||
<div className={styles.entry}>
|
||||
<div>
|
||||
<Body color="uiTextHighContrast">
|
||||
{`${intl.formatMessage({ id: "Extra bed (child)" })} × ${childBedExtraBed}`}
|
||||
</Body>
|
||||
</div>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatNumber(0, {
|
||||
currency: roomPrice.local.currency,
|
||||
style: "currency",
|
||||
})}
|
||||
</Body>
|
||||
</div>
|
||||
) : null}
|
||||
{breakfast === false ? (
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">
|
||||
|
||||
@@ -7,6 +7,7 @@ import { selectRate } from "@/constants/routes/hotelReservation"
|
||||
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||
|
||||
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||
import HotelLogo from "@/components/Icons/Logos"
|
||||
import ImageGallery from "@/components/ImageGallery"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
@@ -18,7 +19,6 @@ import getSingleDecimal from "@/utils/numberFormatting"
|
||||
|
||||
import ReadMore from "../ReadMore"
|
||||
import TripAdvisorChip from "../TripAdvisorChip"
|
||||
import HotelLogo from "./HotelLogo"
|
||||
import HotelPriceCard from "./HotelPriceCard"
|
||||
import NoPriceAvailableCard from "./NoPriceAvailableCard"
|
||||
import { hotelCardVariants } from "./variants"
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ScandicLogoIcon,
|
||||
} from "@/components/Icons"
|
||||
|
||||
import type { HotelLogoProps } from "@/types/components/hotelReservation/selectHotel/hotelLogoProps"
|
||||
import type { HotelLogoProps } from "@/types/components/hotelLogo"
|
||||
import { HotelTypeEnum } from "@/types/enums/hotelType"
|
||||
import { SignatureHotelEnum } from "@/types/enums/signatureHotel"
|
||||
|
||||
@@ -52,7 +52,7 @@ export const countriesMap = {
|
||||
"Congo, The Democratic Republic of the": "CD",
|
||||
"Cook Islands": "CK",
|
||||
"Costa Rica": "CR",
|
||||
'Cote D"Ivoire': "CI",
|
||||
"Côte d'Ivoire": "CI",
|
||||
Croatia: "HR",
|
||||
Cuba: "CU",
|
||||
Curacao: "CW",
|
||||
@@ -109,7 +109,6 @@ export const countriesMap = {
|
||||
"Isle of Man": "IM",
|
||||
Israel: "IL",
|
||||
Italy: "IT",
|
||||
"Ivory Coast": "CI",
|
||||
Jamaica: "JM",
|
||||
Japan: "JP",
|
||||
Jersey: "JE",
|
||||
@@ -171,7 +170,7 @@ export const countriesMap = {
|
||||
Oman: "OM",
|
||||
Pakistan: "PK",
|
||||
Palau: "PW",
|
||||
"Palestinian Territory, Occupied": "PS",
|
||||
Palestine: "PS",
|
||||
Panama: "PA",
|
||||
"Papua New Guinea": "PG",
|
||||
Paraguay: "PY",
|
||||
@@ -215,7 +214,6 @@ export const countriesMap = {
|
||||
Sudan: "SD",
|
||||
Suriname: "SR",
|
||||
"Svalbard and Jan Mayen": "SJ",
|
||||
Swaziland: "SZ",
|
||||
Sweden: "SE",
|
||||
Switzerland: "CH",
|
||||
"Syrian Arab Republic": "SY",
|
||||
|
||||
Reference in New Issue
Block a user