Merge in feat/SW-415-select-room-card (pull request #663)

Feat/SW-415 select room card
This commit is contained in:
Pontus Dreij
2024-10-11 06:48:57 +00:00
38 changed files with 675 additions and 222 deletions

View File

@@ -42,3 +42,4 @@ GOOGLE_STATIC_MAP_SIGNATURE_SECRET="test"
GOOGLE_STATIC_MAP_ID="test" GOOGLE_STATIC_MAP_ID="test"
GOOGLE_DYNAMIC_MAP_ID="test" GOOGLE_DYNAMIC_MAP_ID="test"
HIDE_FOR_NEXT_RELEASE="true" HIDE_FOR_NEXT_RELEASE="true"
SALESFORCE_PREFERENCE_BASE_URL="test"

View File

@@ -1,6 +1,6 @@
.layout { .layout {
min-height: 100dvh; min-height: 100dvh;
background-color: var(--Base-Background-Primary-Normal);
max-width: var(--max-width); max-width: var(--max-width);
margin: 0 auto; margin: 0 auto;
background-color: var(--Base-Background-Primary-Normal);
} }

View File

@@ -7,13 +7,13 @@
} }
.content { .content {
max-width: 1134px; max-width: 1434px;
margin-top: var(--Spacing-x5);
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
display: flex; display: flex;
justify-content: space-between; flex-direction: column;
gap: var(--Spacing-x7); gap: var(--Spacing-x7);
padding: var(--Spacing-x2);
} }
.main { .main {

View File

@@ -15,8 +15,11 @@ export default async function SelectRatePage({
}: PageArgs<LangParams & { section: string }, SelectRateSearchParams>) { }: PageArgs<LangParams & { section: string }, SelectRateSearchParams>) {
setLang(params.lang) setLang(params.lang)
// TODO: Use real endpoint. const hotelData = await serverClient().hotel.hotelData.get({
const hotel = tempHotelData.data.attributes hotelId: searchParams.hotel,
language: params.lang,
include: ["RoomCategories"],
})
const roomConfigurations = await serverClient().hotel.availability.rooms({ const roomConfigurations = await serverClient().hotel.availability.rooms({
hotelId: parseInt(searchParams.hotel, 10), hotelId: parseInt(searchParams.hotel, 10),
@@ -24,18 +27,27 @@ export default async function SelectRatePage({
roomStayEndDate: "2024-11-03", roomStayEndDate: "2024-11-03",
adults: 1, adults: 1,
}) })
if (!roomConfigurations) { if (!roomConfigurations) {
return "No rooms found" return "No rooms found" // TODO: Add a proper error message
} }
if (!hotelData) {
return "No hotel data found" // TODO: Add a proper error message
}
const roomCategories = hotelData?.included
return ( return (
<div> <div>
{/* TODO: Add Hotel Listing Card */}
<div>Hotel Listing Card TBI</div>
<div className={styles.content}> <div className={styles.content}>
{/* TODO: Add Hotel Listing Card */}
<div>Hotel Listing Card TBI</div>
<div className={styles.main}> <div className={styles.main}>
<RoomSelection roomConfigurations={roomConfigurations} /> <RoomSelection
roomConfigurations={roomConfigurations}
roomCategories={roomCategories ?? []}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -23,20 +23,20 @@ export function Rooms({ rooms }: RoomsProps) {
const mappedRooms = rooms const mappedRooms = rooms
.map((room) => { .map((room) => {
const size = `${room.attributes.roomSize.min} - ${room.attributes.roomSize.max}` const size = `${room.roomSize.min} - ${room.roomSize.max}`
const personLabel = const personLabel =
room.attributes.occupancy.total === 1 room.occupancy.total === 1
? intl.formatMessage({ id: "hotelPages.rooms.roomCard.person" }) ? intl.formatMessage({ id: "hotelPages.rooms.roomCard.person" })
: intl.formatMessage({ id: "hotelPages.rooms.roomCard.persons" }) : intl.formatMessage({ id: "hotelPages.rooms.roomCard.persons" })
const subtitle = `${size} (${room.attributes.occupancy.total} ${personLabel})` const subtitle = `${size} (${room.occupancy.total} ${personLabel})`
return { return {
id: room.id, id: room.id,
images: room.attributes.content.images, images: room.images,
title: room.attributes.name, title: room.name,
subtitle: subtitle, subtitle: subtitle,
sortOrder: room.attributes.sortOrder, sortOrder: room.sortOrder,
popularChoice: null, popularChoice: null,
} }
}) })

View File

@@ -32,6 +32,10 @@
display: none; display: none;
} }
.infoIcon {
stroke: var(--Base-Text-Disabled);
}
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
.vouchers { .vouchers {
display: none; display: none;

View File

@@ -1,7 +1,3 @@
.infoIcon {
stroke: var(--Base-Text-Disabled);
}
.vouchersHeader { .vouchersHeader {
display: flex; display: flex;
gap: var(--Spacing-x-one-and-half); gap: var(--Spacing-x-one-and-half);

View File

@@ -0,0 +1,36 @@
import { Button, Dialog, OverlayArrow, Popover } from "react-aria-components"
import { CloseIcon } from "@/components/Icons"
import styles from "./popover.module.css"
import { PricePopoverProps } from "@/types/components/hotelReservation/selectRate/pricePopover"
export default function PricePopover({
children,
...props
}: PricePopoverProps) {
return (
<Popover {...props}>
<OverlayArrow className={styles.arrow}>
<svg
width="12"
height="12"
viewBox="0 0 12 12"
style={{ display: "block", transform: "rotate(180deg)" }}
>
<path d="M0 0L6 6L12 0" fill="white" />
</svg>
</OverlayArrow>
<Dialog>
<Button
onPress={() => props.onOpenChange?.(false)}
className={styles.closeButton}
>
<CloseIcon className={styles.closeIcon} />
</Button>
{children}
</Dialog>
</Popover>
)
}

View File

@@ -0,0 +1,12 @@
.arrow {
top: -6px;
}
.closeButton {
position: absolute;
top: 5px;
right: 5px;
background: none;
border: none;
cursor: pointer;
}

View File

@@ -0,0 +1,102 @@
import { useIntl } from "react-intl"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import styles from "./priceList.module.css"
import { PriceListProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
export default function PriceList({
publicPrice = {},
memberPrice = {},
}: PriceListProps) {
const intl = useIntl()
const { localPrice: publicLocalPrice, requestedPrice: publicRequestedPrice } =
publicPrice
const { localPrice: memberLocalPrice, requestedPrice: memberRequestedPrice } =
memberPrice
const showRequestedPrice = publicRequestedPrice && memberRequestedPrice
return (
<dl className={styles.priceList}>
<div className={styles.priceRow}>
<dt>
<Caption
textTransform="bold"
color={publicLocalPrice ? "uiTextHighContrast" : "disabled"}
>
{intl.formatMessage({ id: "Standard price" })}
</Caption>
</dt>
<dd>
{publicLocalPrice ? (
<div className={styles.price}>
<Subtitle type="two" color="uiTextHighContrast">
{publicLocalPrice.pricePerNight}
</Subtitle>
<Body color="uiTextHighContrast" textTransform="bold">
{publicLocalPrice.currency}
</Body>
</div>
) : (
<Subtitle type="two" color="disabled">
{intl.formatMessage({ id: "n/a" })}
</Subtitle>
)}
</dd>
</div>
<div className={styles.priceRow}>
<dt>
<Caption
textTransform="bold"
color={memberLocalPrice ? "red" : "disabled"}
>
{intl.formatMessage({ id: "Member price" })}
</Caption>
</dt>
<dd>
{memberLocalPrice ? (
<div className={styles.price}>
<Subtitle type="two" color="red">
{memberLocalPrice.pricePerNight}
</Subtitle>
<Body color="red" textTransform="bold">
{memberLocalPrice.currency}
</Body>
</div>
) : (
<Body textTransform="bold" color="disabled">
- {intl.formatMessage({ id: "Currency Code" })}
</Body>
)}
</dd>
</div>
<div className={styles.priceRow}>
<dt>
<Caption
color={showRequestedPrice ? "uiTextMediumContrast" : "disabled"}
>
{intl.formatMessage({ id: "Approx." })}
</Caption>
</dt>
<dd>
{showRequestedPrice ? (
<Caption color="uiTextMediumContrast">
{publicRequestedPrice.pricePerNight}/
{memberRequestedPrice.pricePerNight}{" "}
{publicRequestedPrice.currency}
</Caption>
) : (
<Caption color="disabled">- / - EUR</Caption>
)}
</dd>
</div>
</dl>
)
}

View File

@@ -0,0 +1,14 @@
.priceRow {
display: flex;
justify-content: space-between;
padding: var(--Spacing-x-quarter) 0;
}
.priceTable {
margin: 0;
}
.price {
display: flex;
gap: var(--Spacing-x-half);
}

View File

@@ -1,15 +1,80 @@
.card { .card,
font-size: 14px; .disabledCard {
border-radius: var(--Corner-radius-Medium); border-radius: var(--Corner-radius-Large);
border: 1px solid var(--Base-Border-Normal);
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2); padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
background-color: var(--Base-Surface-Secondary-light-Normal);
position: relative;
display: flex;
flex-direction: column;
gap: var(--Spacing-x-half);
} }
input[type="radio"]:checked + .card { .disabledCard {
opacity: 0.6;
}
.disabledCard:hover {
cursor: not-allowed;
}
.card:hover {
cursor: pointer;
background-color: var(--Base-Surface-Primary-light-Hover-alt); background-color: var(--Base-Surface-Primary-light-Hover-alt);
} }
.checkIcon {
display: none;
}
input[type="radio"]:checked + .card {
border: 1px solid var(--Primary-Dark-On-Surface-Divider);
background-color: var(--Base-Surface-Primary-light-Hover-alt);
}
input[type="radio"]:checked + .card .checkIcon {
display: block;
position: absolute;
top: -10px;
right: -10px;
}
.header { .header {
display: flex; display: flex;
justify-content: space-between; gap: var(--Spacing-x-half);
}
.header .infoIcon,
.header .infoIcon path {
stroke: var(--UI-Text-Medium-contrast);
fill: transparent;
}
.button {
background: none;
border: none;
cursor: pointer;
grid-area: chevron;
height: 100%;
justify-self: flex-end;
padding: 0;
}
.popover {
background-color: var(--Main-Grey-White);
border-radius: var(--Corner-radius-Medium);
left: 0px;
max-height: 400px;
padding: var(--Spacing-x2);
top: calc(55px + var(--Spacing-x1));
width: 100%;
box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1);
}
.popover section:focus-visible {
outline: none;
}
.popover .popoverText {
margin-bottom: var(--Spacing-x-half);
}
.popover .popoverHeading {
margin-bottom: var(--Spacing-x1);
font-weight: 600; /* TODO: Remove when this is updated in Design system */
} }

View File

@@ -1,9 +1,13 @@
"use client" "use client"
import { useIntl } from "react-intl" import { useState } from "react"
import { Button, DialogTrigger } from "react-aria-components"
import Body from "@/components/TempDesignSystem/Text/Body" import { CheckCircleIcon, InfoCircleIcon } from "@/components/Icons"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import PricePopover from "./Popover"
import PriceTable from "./PriceList"
import styles from "./flexibilityOption.module.css" import styles from "./flexibilityOption.module.css"
import { FlexibilityOptionProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption" import { FlexibilityOptionProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
@@ -12,59 +16,90 @@ export default function FlexibilityOption({
product, product,
name, name,
paymentTerm, paymentTerm,
priceInformation,
}: FlexibilityOptionProps) { }: FlexibilityOptionProps) {
const intl = useIntl() const [rootDiv, setRootDiv] = useState<Element | undefined>(undefined)
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
if (!product) { function setRef(node: Element | null) {
// TODO: Implement empty state when this rate can't be booked if (node) {
return <div>TBI: Rate not available</div> setRootDiv(node)
}
} }
const { productType } = product if (!product) {
const { public: publicPrice, member: memberPrice } = productType return (
const { localPrice: publicLocalPrice, requestedPrice: publicRequestedPrice } = <div className={styles.disabledCard}>
publicPrice <div className={styles.header}>
const { localPrice: memberLocalPrice, requestedPrice: memberRequestedPrice } = <InfoCircleIcon className={styles.infoIcon} />
memberPrice <Caption color="disabled">{name}</Caption>
<Caption color="disabled">({paymentTerm})</Caption>
</div>
<PriceTable />
</div>
)
}
const { public: publicPrice, member: memberPrice } = product.productType
return ( return (
<label> <label>
<input <input type="radio" name="rateCode" value={publicPrice?.rateCode} />
type="radio"
name="rateCode"
value={product.productType.public.rateCode}
/>
<div className={styles.card}> <div className={styles.card}>
<div className={styles.header}> <div className={styles.header} ref={setRef}>
<Body>{name}</Body> <DialogTrigger>
<Caption>{paymentTerm}</Caption> <Button
aria-label="Help"
className={styles.button}
onPress={() => setIsPopoverOpen(true)}
>
<InfoCircleIcon className={styles.infoIcon} />
</Button>
<PricePopover
placement="bottom"
className={styles.popover}
isNonModal
shouldFlip={false}
shouldUpdatePosition={false}
/**
* react-aria uses portals to render Popover in body
* unless otherwise specified. We need it to be contained
* by this component to both access css variables assigned
* on the container as well as to not overflow it at any time.
*/
UNSTABLE_portalContainer={rootDiv}
isOpen={isPopoverOpen}
onOpenChange={setIsPopoverOpen}
>
<Caption
color="uiTextHighContrast"
textTransform="bold"
className={styles.popoverHeading}
>
{name}
</Caption>
{priceInformation?.map((info) => (
<Caption
key={info}
color="uiTextHighContrast"
className={styles.popoverText}
>
{info}
</Caption>
))}
</PricePopover>
</DialogTrigger>
<Caption color="uiTextHighContrast">{name}</Caption>
<Caption color="uiTextPlaceholder">({paymentTerm})</Caption>
</div> </div>
<dl> <PriceTable publicPrice={publicPrice} memberPrice={memberPrice} />
<div> <CheckCircleIcon
<dt>{intl.formatMessage({ id: "Standard price" })}</dt> color="blue"
<dd> className={styles.checkIcon}
{publicLocalPrice.pricePerNight} {publicLocalPrice.currency}/ width={24}
{intl.formatMessage({ id: "night" })} height={24}
</dd> stroke="white"
</div> />
<div>
<dt>{intl.formatMessage({ id: "Member price" })}</dt>
<dd>
{memberLocalPrice.pricePerNight} {memberLocalPrice.currency}/
{intl.formatMessage({ id: "night" })}
</dd>
</div>
{publicRequestedPrice && memberRequestedPrice && (
<div>
<dt>{intl.formatMessage({ id: "Approx." })}</dt>
<dd>
{publicRequestedPrice.pricePerNight}/
{memberRequestedPrice.pricePerNight}{" "}
{publicRequestedPrice.currency}
</dd>
</div>
)}
</dl>
</div> </div>
</label> </label>
) )

View File

@@ -1,9 +1,14 @@
"use client" "use client"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { RateDefinition } from "@/server/routers/hotels/output"
import FlexibilityOption from "@/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption" import FlexibilityOption from "@/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption"
import { ChevronRightSmallIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import styles from "./roomCard.module.css" import styles from "./roomCard.module.css"
@@ -13,6 +18,7 @@ import { RoomCardProps } from "@/types/components/hotelReservation/selectRate/ro
export default function RoomCard({ export default function RoomCard({
rateDefinitions, rateDefinitions,
roomConfiguration, roomConfiguration,
roomCategories,
}: RoomCardProps) { }: RoomCardProps) {
const intl = useIntl() const intl = useIntl()
@@ -29,90 +35,115 @@ export default function RoomCard({
(rate) => rate.cancellationRule === "CancellableBefore6PM" (rate) => rate.cancellationRule === "CancellableBefore6PM"
) )
const saveProduct = saveRate function findProductForRate(rate: RateDefinition | undefined) {
? roomConfiguration.products.find( return rate
(product) => ? roomConfiguration.products.find(
product.productType.public.rateCode === saveRate.rateCode || (product) =>
product.productType.member.rateCode === saveRate.rateCode product.productType.public?.rateCode === rate.rateCode ||
) product.productType.member?.rateCode === rate.rateCode
: undefined )
const changeProduct = changeRate : undefined
? roomConfiguration.products.find( }
(product) =>
product.productType.public.rateCode === changeRate.rateCode || function getPriceForRate(
product.productType.member.rateCode === changeRate.rateCode rate: typeof saveRate | typeof changeRate | typeof flexRate
) ) {
: undefined return rateDefinitions.find((def) => def.rateCode === rate?.rateCode)
const flexProduct = flexRate ?.generalTerms
? roomConfiguration.products.find( }
(product) =>
product.productType.public.rateCode === flexRate.rateCode || const roomSize = roomCategories.find(
product.productType.member.rateCode === flexRate.rateCode (category) => category.name === roomConfiguration.roomType
) )?.roomSize
: undefined
const occupancy = roomCategories.find(
(category) => category.name === roomConfiguration.roomType
)?.occupancy.total
const roomDescription = roomCategories.find(
(room) => room.name === roomConfiguration.roomType
)?.descriptions.short
return ( return (
<div className={styles.card}> <div className={styles.card}>
<div className={styles.cardBody}> <div className={styles.cardBody}>
<div className={styles.specification}> <div className={styles.specification}>
<Subtitle className={styles.name} type="two"> <Caption color="uiTextMediumContrast" className={styles.guests}>
{roomConfiguration.roomType}
</Subtitle>
<Caption>Room size TBI</Caption>
<Button intent="text" type="button" size="small" theme="base">
{intl.formatMessage({ id: "See room details" })}
</Button>
<Caption>
{/*TODO: Handle pluralisation*/} {/*TODO: Handle pluralisation*/}
{intl.formatMessage( {intl.formatMessage(
{ {
id: "Max {nrOfGuests} guests", id: "booking.guests",
defaultMessage: "Max {nrOfGuests} guests",
}, },
// TODO: Correct number { nrOfGuests: occupancy }
{ nrOfGuests: 2 }
)} )}
</Caption>
<Caption color="uiTextMediumContrast">
{roomSize?.min}-{roomSize?.max} m²
</Caption>
<Button
intent="text"
type="button"
size="small"
theme="base"
className={styles.button}
>
{intl.formatMessage({ id: "See room details" })}
<ChevronRightSmallIcon color="burgundy" width={20} height={20} />
</Button>
</div>
<div className={styles.container}>
<div className={styles.roomDetails}>
<Subtitle className={styles.name} type="two">
{roomConfiguration.roomType}
</Subtitle>
<Body>{roomDescription}</Body>
</div>
<Caption color="uiTextHighContrast">
{intl.formatMessage({ {intl.formatMessage({
id: "Breakfast included", id: "Breakfast selection in next step.",
})} })}
</Caption> </Caption>
<div>
<FlexibilityOption
name={intl.formatMessage({ id: "Non-refundable" })}
value="non-refundable"
paymentTerm={intl.formatMessage({ id: "Pay now" })}
product={findProductForRate(saveRate)}
priceInformation={getPriceForRate(saveRate)}
/>
<FlexibilityOption
name={intl.formatMessage({ id: "Free rebooking" })}
value="free-rebooking"
paymentTerm={intl.formatMessage({ id: "Pay now" })}
product={findProductForRate(changeRate)}
priceInformation={getPriceForRate(changeRate)}
/>
<FlexibilityOption
name={intl.formatMessage({ id: "Free cancellation" })}
value="free-cancellation"
paymentTerm={intl.formatMessage({ id: "Pay later" })}
product={findProductForRate(flexRate)}
priceInformation={getPriceForRate(flexRate)}
/>
</div>
</div> </div>
<FlexibilityOption
name={intl.formatMessage({ id: "Non-refundable" })}
value="non-refundable"
paymentTerm={intl.formatMessage({ id: "Pay now" })}
product={saveProduct}
/>
<FlexibilityOption
name={intl.formatMessage({ id: "Free rebooking" })}
value="free-rebooking"
paymentTerm={intl.formatMessage({ id: "Pay now" })}
product={changeProduct}
/>
<FlexibilityOption
name={intl.formatMessage({ id: "Free cancellation" })}
value="free-cancellation"
paymentTerm={intl.formatMessage({ id: "Pay later" })}
product={flexProduct}
/>
<Button
type="submit"
size="small"
theme="primaryDark"
className={styles.button}
>
{intl.formatMessage({ id: "Choose room" })}
</Button>
</div> </div>
{/* TODO: maybe use the `Image` component instead of the `img` tag. Waiting until we know how to get the image */}
{/* eslint-disable-next-line @next/next/no-img-element */} <div className={styles.imageContainer}>
<img <span className={styles.roomsLeft}>
alt={intl.formatMessage({ id: "A photo of the room" })} <Footnote
// TODO: Correct image URL color="burgundy"
src="https://www.scandichotels.se/imageVault/publishedmedia/xnmqnmz6mz0uhuat0917/scandic-helsinki-hub-room-standard-KR-7.jpg" textTransform="uppercase"
/> >{`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`}</Footnote>
</span>
{/* TODO: maybe use the `Image` component instead of the `img` tag. Waiting until we know how to get the image */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
alt={intl.formatMessage({ id: "A photo of the room" })}
// TODO: Correct image URL
src="https://www.scandichotels.se/imageVault/publishedmedia/xnmqnmz6mz0uhuat0917/scandic-helsinki-hub-room-standard-KR-7.jpg"
/>
</div>
</div> </div>
) )
} }

View File

@@ -3,19 +3,44 @@
display: flex; display: flex;
flex-direction: column-reverse; flex-direction: column-reverse;
background-color: #fff; background-color: #fff;
border-radius: var(--Corner-radius-Small); border-radius: var(--Corner-radius-Large);
border: 1px solid rgba(77, 0, 27, 0.1); border: 1px solid var(--Base-Border-Subtle);
position: relative;
} }
.cardBody { .cardBody {
padding: var(--Spacing-x1);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--Spacing-x1);
} }
.specification { .specification {
padding: var(--Spacing-x1); display: flex;
flex-direction: row;
align-items: center;
gap: var(--Spacing-x1);
padding: 0 var(--Spacing-x1) 0 var(--Spacing-x-one-and-half);
height: 40px;
}
.specification .guests {
border-right: 1px solid var(--Base-Border-Subtle);
padding-right: var(--Spacing-x1);
}
.specification .button {
margin-left: auto;
padding: 0 0 0 var(--Spacing-x-half);
text-decoration: none;
}
.container {
padding: var(--Spacing-x1) var(--Spacing-x2) var(--Spacing-x2);
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
}
.roomDetails {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--Spacing-x1); gap: var(--Spacing-x1);
@@ -25,12 +50,18 @@
display: inline-block; display: inline-block;
} }
.card .button {
display: inline;
}
.card img { .card img {
max-width: 100%; max-width: 100%;
aspect-ratio: 1.5; aspect-ratio: 16/9;
object-fit: cover; object-fit: cover;
border-radius: var(--Corner-radius-Medium) var(--Corner-radius-Medium) 0 0;
}
.roomsLeft {
position: absolute;
top: 12px;
left: 12px;
background-color: var(--Main-Grey-White);
padding: var(--Spacing-x-half) var(--Spacing-x1);
border-radius: var(--Corner-radius-Small);
} }

View File

@@ -12,6 +12,7 @@ import { RoomSelectionProps } from "@/types/components/hotelReservation/selectRa
export default function RoomSelection({ export default function RoomSelection({
roomConfigurations, roomConfigurations,
roomCategories,
}: RoomSelectionProps) { }: RoomSelectionProps) {
const router = useRouter() const router = useRouter()
const searchParams = useSearchParams() const searchParams = useSearchParams()
@@ -38,6 +39,7 @@ export default function RoomSelection({
<RoomCard <RoomCard
rateDefinitions={roomConfigurations.rateDefinitions} rateDefinitions={roomConfigurations.rateDefinitions}
roomConfiguration={roomConfiguration} roomConfiguration={roomConfiguration}
roomCategories={roomCategories}
/> />
</li> </li>
))} ))}

View File

@@ -1,5 +1,4 @@
.wrapper { .wrapper {
border-bottom: 1px solid rgba(17, 17, 17, 0.2);
padding-bottom: var(--Spacing-x3); padding-bottom: var(--Spacing-x3);
} }
@@ -7,9 +6,8 @@
margin-top: var(--Spacing-x4); margin-top: var(--Spacing-x4);
list-style: none; list-style: none;
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr; grid-template-columns: 1fr;
column-gap: var(--Spacing-x2); gap: var(--Spacing-x3);
row-gap: var(--Spacing-x4);
} }
.roomList > li { .roomList > li {
@@ -30,3 +28,15 @@
background-color: white; background-color: white;
padding: var(--Spacing-x3) var(--Spacing-x7) var(--Spacing-x5); padding: var(--Spacing-x3) var(--Spacing-x7) var(--Spacing-x5);
} }
@media (min-width: 767px) {
.roomList {
grid-template-columns: repeat(3, minmax(240px, 1fr));
}
}
@media (min-width: 1367px) {
.roomList {
grid-template-columns: repeat(4, 1fr);
}
}

View File

@@ -99,3 +99,7 @@
.uiTextPlaceholder { .uiTextPlaceholder {
color: var(--UI-Text-Placeholder); color: var(--UI-Text-Placeholder);
} }
.disabled {
color: var(--Base-Text-Disabled);
}

View File

@@ -7,6 +7,7 @@ const config = {
color: { color: {
black: styles.black, black: styles.black,
burgundy: styles.burgundy, burgundy: styles.burgundy,
disabled: styles.disabled,
grey: styles.grey, grey: styles.grey,
pale: styles.pale, pale: styles.pale,
red: styles.red, red: styles.red,

View File

@@ -75,6 +75,10 @@ p.caption {
color: var(--UI-Text-High-contrast); color: var(--UI-Text-High-contrast);
} }
.uiTextPlaceholder {
color: var(--UI-Text-Placeholder);
}
.disabled { .disabled {
color: var(--Base-Text-Disabled); color: var(--Base-Text-Disabled);
} }

View File

@@ -15,6 +15,7 @@ const config = {
uiTextHighContrast: styles.uiTextHighContrast, uiTextHighContrast: styles.uiTextHighContrast,
uiTextActive: styles.uiTextActive, uiTextActive: styles.uiTextActive,
uiTextMediumContrast: styles.uiTextMediumContrast, uiTextMediumContrast: styles.uiTextMediumContrast,
uiTextPlaceholder: styles.uiTextPlaceholder,
disabled: styles.disabled, disabled: styles.disabled,
}, },
textTransform: { textTransform: {

View File

@@ -66,3 +66,11 @@
.uiTextMediumContrast { .uiTextMediumContrast {
color: var(--UI-Text-Medium-contrast); color: var(--UI-Text-Medium-contrast);
} }
.red {
color: var(--Scandic-Brand-Scandic-Red);
}
.disabled {
color: var(--Base-Text-Disabled);
}

View File

@@ -7,9 +7,11 @@ const config = {
color: { color: {
black: styles.black, black: styles.black,
burgundy: styles.burgundy, burgundy: styles.burgundy,
disabled: styles.disabled,
pale: styles.pale, pale: styles.pale,
uiTextHighContrast: styles.uiTextHighContrast, uiTextHighContrast: styles.uiTextHighContrast,
uiTextMediumContrast: styles.uiTextMediumContrast, uiTextMediumContrast: styles.uiTextMediumContrast,
red: styles.red,
}, },
textAlign: { textAlign: {
center: styles.center, center: styles.center,

View File

@@ -18,6 +18,16 @@ export const selectHotel = {
de: `${hotelReservation.de}/select-hotel`, de: `${hotelReservation.de}/select-hotel`,
} }
// TODO: Translate paths
export const selectRate = {
en: `${hotelReservation.en}/select-rate`,
sv: `${hotelReservation.sv}/select-rate`,
no: `${hotelReservation.no}/select-rate`,
fi: `${hotelReservation.fi}/select-rate`,
da: `${hotelReservation.da}/select-rate`,
de: `${hotelReservation.de}/select-rate`,
}
// TODO: Translate paths // TODO: Translate paths
export const selectBed = { export const selectBed = {
en: `${hotelReservation.en}/select-bed`, en: `${hotelReservation.en}/select-bed`,
@@ -86,4 +96,5 @@ export const bookingFlow = [
...Object.values(payment), ...Object.values(payment),
...Object.values(selectHotelMap), ...Object.values(selectHotelMap),
...Object.values(bookingConfirmation), ...Object.values(bookingConfirmation),
...Object.values(selectRate),
] ]

View File

@@ -34,6 +34,7 @@
"Book reward night": "Book bonusnat", "Book reward night": "Book bonusnat",
"Booking number": "Bookingnummer", "Booking number": "Bookingnummer",
"booking.adults": "{totalAdults, plural, one {# voksen} other {# voksne}}", "booking.adults": "{totalAdults, plural, one {# voksen} other {# voksne}}",
"booking.guests": "Maks {nrOfGuests, plural, one {# gæst} other {# gæster}}",
"booking.nights": "{totalNights, plural, one {# nat} other {# nætter}}", "booking.nights": "{totalNights, plural, one {# nat} other {# nætter}}",
"booking.rooms": "{totalRooms, plural, one {# værelse} other {# værelser}}", "booking.rooms": "{totalRooms, plural, one {# værelse} other {# værelser}}",
"booking.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle <termsLink>Vilkår og betingelser</termsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyLink>Scandics Privatlivspolitik</privacyLink>. Jeg accepterer, at Scandic kræver et gyldigt kreditkort under min besøg i tilfælde af, at noget er tilbagebetalt.", "booking.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle <termsLink>Vilkår og betingelser</termsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyLink>Scandics Privatlivspolitik</privacyLink>. Jeg accepterer, at Scandic kræver et gyldigt kreditkort under min besøg i tilfælde af, at noget er tilbagebetalt.",
@@ -42,6 +43,7 @@
"Breakfast excluded": "Morgenmad ikke inkluderet", "Breakfast excluded": "Morgenmad ikke inkluderet",
"Breakfast included": "Morgenmad inkluderet", "Breakfast included": "Morgenmad inkluderet",
"Breakfast restaurant": "Breakfast restaurant", "Breakfast restaurant": "Breakfast restaurant",
"Breakfast selection in next step.": "Valg af morgenmad i næste trin.",
"Bus terminal": "Busstation", "Bus terminal": "Busstation",
"Business": "Forretning", "Business": "Forretning",
"by": "inden", "by": "inden",
@@ -74,6 +76,7 @@
"Country code": "Landekode", "Country code": "Landekode",
"Credit card": "Kreditkort", "Credit card": "Kreditkort",
"Credit card deleted successfully": "Kreditkort blev slettet", "Credit card deleted successfully": "Kreditkort blev slettet",
"Currency Code": "DKK",
"Current password": "Nuværende kodeord", "Current password": "Nuværende kodeord",
"Customer service": "Kundeservice", "Customer service": "Kundeservice",
"Date of Birth": "Fødselsdato", "Date of Birth": "Fødselsdato",
@@ -140,6 +143,7 @@
"Language": "Sprog", "Language": "Sprog",
"Lastname": "Efternavn", "Lastname": "Efternavn",
"Latest searches": "Seneste søgninger", "Latest searches": "Seneste søgninger",
"Left": "tilbage",
"Level": "Niveau", "Level": "Niveau",
"Level 1": "Niveau 1", "Level 1": "Niveau 1",
"Level 2": "Niveau 2", "Level 2": "Niveau 2",
@@ -197,6 +201,7 @@
"Not found": "Ikke fundet", "Not found": "Ikke fundet",
"Nr night, nr adult": "{nights, number} nat, {adults, number} voksen", "Nr night, nr adult": "{nights, number} nat, {adults, number} voksen",
"number": "nummer", "number": "nummer",
"n/a": "n/a",
"On your journey": "På din rejse", "On your journey": "På din rejse",
"Open": "Åben", "Open": "Åben",
"Open language menu": "Åbn sprogmenuen", "Open language menu": "Åbn sprogmenuen",
@@ -240,6 +245,8 @@
"Room & Terms": "Værelse & Vilkår", "Room & Terms": "Værelse & Vilkår",
"Room facilities": "Værelsesfaciliteter", "Room facilities": "Værelsesfaciliteter",
"Rooms": "Værelser", "Rooms": "Værelser",
"guest": "gæst",
"guests": "gæster",
"Rooms & Guests": "Værelser & gæster", "Rooms & Guests": "Værelser & gæster",
"Sauna and gym": "Sauna and gym", "Sauna and gym": "Sauna and gym",
"Save": "Gemme", "Save": "Gemme",
@@ -285,7 +292,6 @@
"There are no transactions to display": "Der er ingen transaktioner at vise", "There are no transactions to display": "Der er ingen transaktioner at vise",
"Things nearby HOTEL_NAME": "Ting i nærheden af {hotelName}", "Things nearby HOTEL_NAME": "Ting i nærheden af {hotelName}",
"to": "til", "to": "til",
"Total incl VAT": "Inkl. moms",
"Total Points": "Samlet antal point", "Total Points": "Samlet antal point",
"Tourist": "Turist", "Tourist": "Turist",
"Transaction date": "Overførselsdato", "Transaction date": "Overførselsdato",

View File

@@ -33,13 +33,17 @@
"Book": "Buchen", "Book": "Buchen",
"Book reward night": "Bonusnacht buchen", "Book reward night": "Bonusnacht buchen",
"Booking number": "Buchungsnummer", "Booking number": "Buchungsnummer",
"booking.adults": "{totalAdults, plural, one {# erwachsene} other {# erwachsene}}",
"booking.guests": "Max {nrOfGuests, plural, one {# gast} other {# gäste}}",
"booking.nights": "{totalNights, plural, one {# nacht} other {# Nächte}}", "booking.nights": "{totalNights, plural, one {# nacht} other {# Nächte}}",
"booking.rooms": "{totalRooms, plural, one {# zimmer} other {# räume}}",
"booking.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle <termsLink>Vilkår og betingelser</termsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyLink>Scandics Privatlivspolitik</privacyLink>. Jeg accepterer, at Scandic kræver et gyldigt kreditkort under min besøg i tilfælde af, at noget er tilbagebetalt.", "booking.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle <termsLink>Vilkår og betingelser</termsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyLink>Scandics Privatlivspolitik</privacyLink>. Jeg accepterer, at Scandic kræver et gyldigt kreditkort under min besøg i tilfælde af, at noget er tilbagebetalt.",
"Breakfast": "Frühstück", "Breakfast": "Frühstück",
"Breakfast buffet": "Frühstücksbuffet", "Breakfast buffet": "Frühstücksbuffet",
"Breakfast excluded": "Frühstück nicht inbegriffen", "Breakfast excluded": "Frühstück nicht inbegriffen",
"Breakfast included": "Frühstück inbegriffen", "Breakfast included": "Frühstück inbegriffen",
"Breakfast restaurant": "Breakfast restaurant", "Breakfast restaurant": "Breakfast restaurant",
"Breakfast selection in next step.": "Frühstücksauswahl in nächsten Schritt.",
"Bus terminal": "Busbahnhof", "Bus terminal": "Busbahnhof",
"Business": "Geschäft", "Business": "Geschäft",
"by": "bis", "by": "bis",
@@ -72,6 +76,7 @@
"Country code": "Landesvorwahl", "Country code": "Landesvorwahl",
"Credit card": "Kreditkarte", "Credit card": "Kreditkarte",
"Credit card deleted successfully": "Kreditkarte erfolgreich gelöscht", "Credit card deleted successfully": "Kreditkarte erfolgreich gelöscht",
"Currency Code": "EUR",
"Current password": "Aktuelles Passwort", "Current password": "Aktuelles Passwort",
"Customer service": "Kundendienst", "Customer service": "Kundendienst",
"Date of Birth": "Geburtsdatum", "Date of Birth": "Geburtsdatum",
@@ -113,6 +118,8 @@
"Get member benefits & offers": "Holen Sie sich Vorteile und Angebote für Mitglieder", "Get member benefits & offers": "Holen Sie sich Vorteile und Angebote für Mitglieder",
"Go back to edit": "Zurück zum Bearbeiten", "Go back to edit": "Zurück zum Bearbeiten",
"Go back to overview": "Zurück zur Übersicht", "Go back to overview": "Zurück zur Übersicht",
"guest": "gast",
"guests": "gäste",
"Guest information": "Informationen für Gäste", "Guest information": "Informationen für Gäste",
"Guests & Rooms": "Gäste & Zimmer", "Guests & Rooms": "Gäste & Zimmer",
"Hi": "Hallo", "Hi": "Hallo",
@@ -131,12 +138,14 @@
"Image gallery": "Bildergalerie", "Image gallery": "Bildergalerie",
"Included": "Iinklusive", "Included": "Iinklusive",
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Es ist derzeit nicht möglich, Ihre Kommunikationseinstellungen zu verwalten. Bitte versuchen Sie es später erneut oder wenden Sie sich an den Support, wenn das Problem weiterhin besteht.", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Es ist derzeit nicht möglich, Ihre Kommunikationseinstellungen zu verwalten. Bitte versuchen Sie es später erneut oder wenden Sie sich an den Support, wenn das Problem weiterhin besteht.",
"Join Scandic Friends": "Treten Sie Scandic Friends bei",
"Join at no cost": "Kostenlos beitreten", "Join at no cost": "Kostenlos beitreten",
"Join Scandic Friends": "Treten Sie Scandic Friends bei",
"King bed": "Kingsize-Bett", "King bed": "Kingsize-Bett",
"km to city center": "km bis zum Stadtzentrum",
"Language": "Sprache", "Language": "Sprache",
"Lastname": "Nachname", "Lastname": "Nachname",
"Latest searches": "Letzte Suchanfragen", "Latest searches": "Letzte Suchanfragen",
"Left": "übrig",
"Level": "Level", "Level": "Level",
"Level 1": "Level 1", "Level 1": "Level 1",
"Level 2": "Level 2", "Level 2": "Level 2",
@@ -194,6 +203,7 @@
"Not found": "Nicht gefunden", "Not found": "Nicht gefunden",
"Nr night, nr adult": "{nights, number} Nacht, {adults, number} Erwachsener", "Nr night, nr adult": "{nights, number} Nacht, {adults, number} Erwachsener",
"number": "nummer", "number": "nummer",
"n/a": "n/a",
"On your journey": "Auf deiner Reise", "On your journey": "Auf deiner Reise",
"Open": "Offen", "Open": "Offen",
"Open language menu": "Sprachmenü öffnen", "Open language menu": "Sprachmenü öffnen",

View File

@@ -34,6 +34,7 @@
"Book reward night": "Book reward night", "Book reward night": "Book reward night",
"Booking number": "Booking number", "Booking number": "Booking number",
"booking.adults": "{totalAdults, plural, one {# adult} other {# adults}}", "booking.adults": "{totalAdults, plural, one {# adult} other {# adults}}",
"booking.guests": "Max {nrOfGuests, plural, one {# guest} other {# guests}}",
"booking.nights": "{totalNights, plural, one {# night} other {# nights}}", "booking.nights": "{totalNights, plural, one {# night} other {# nights}}",
"booking.rooms": "{totalRooms, plural, one {# room} other {# rooms}}", "booking.rooms": "{totalRooms, plural, one {# room} other {# rooms}}",
"booking.terms": "By paying with any of the payment methods available, I accept the terms for this booking and the general <termsLink>Terms & Conditions</termsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyLink>Scandic's Privacy policy</privacyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.", "booking.terms": "By paying with any of the payment methods available, I accept the terms for this booking and the general <termsLink>Terms & Conditions</termsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyLink>Scandic's Privacy policy</privacyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.",
@@ -42,6 +43,7 @@
"Breakfast excluded": "Breakfast excluded", "Breakfast excluded": "Breakfast excluded",
"Breakfast included": "Breakfast included", "Breakfast included": "Breakfast included",
"Breakfast restaurant": "Breakfast restaurant", "Breakfast restaurant": "Breakfast restaurant",
"Breakfast selection in next step.": "Breakfast selection in next step.",
"Bus terminal": "Bus terminal", "Bus terminal": "Bus terminal",
"Business": "Business", "Business": "Business",
"by": "by", "by": "by",
@@ -74,6 +76,7 @@
"Country code": "Country code", "Country code": "Country code",
"Credit card": "Credit card", "Credit card": "Credit card",
"Credit card deleted successfully": "Credit card deleted successfully", "Credit card deleted successfully": "Credit card deleted successfully",
"Currency Code": "EUR",
"Current password": "Current password", "Current password": "Current password",
"Customer service": "Customer service", "Customer service": "Customer service",
"Date of Birth": "Date of Birth", "Date of Birth": "Date of Birth",
@@ -115,6 +118,8 @@
"Get member benefits & offers": "Get member benefits & offers", "Get member benefits & offers": "Get member benefits & offers",
"Go back to edit": "Go back to edit", "Go back to edit": "Go back to edit",
"Go back to overview": "Go back to overview", "Go back to overview": "Go back to overview",
"guest": "guest",
"guests": "guests",
"Guest information": "Guest information", "Guest information": "Guest information",
"Guests & Rooms": "Guests & Rooms", "Guests & Rooms": "Guests & Rooms",
"Hi": "Hi", "Hi": "Hi",
@@ -133,11 +138,12 @@
"Image gallery": "Image gallery", "Image gallery": "Image gallery",
"Included": "Included", "Included": "Included",
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.",
"Join Scandic Friends": "Join Scandic Friends",
"Join at no cost": "Join at no cost", "Join at no cost": "Join at no cost",
"Join Scandic Friends": "Join Scandic Friends",
"King bed": "King bed", "King bed": "King bed",
"Language": "Language", "Language": "Language",
"Lastname": "Lastname", "Lastname": "Lastname",
"Left": "left",
"Latest searches": "Latest searches", "Latest searches": "Latest searches",
"Level": "Level", "Level": "Level",
"Level 1": "Level 1", "Level 1": "Level 1",
@@ -196,6 +202,7 @@
"Not found": "Not found", "Not found": "Not found",
"Nr night, nr adult": "{nights, number} night, {adults, number} adult", "Nr night, nr adult": "{nights, number} night, {adults, number} adult",
"number": "number", "number": "number",
"n/a": "n/a",
"On your journey": "On your journey", "On your journey": "On your journey",
"Open": "Open", "Open": "Open",
"Open language menu": "Open language menu", "Open language menu": "Open language menu",
@@ -213,6 +220,7 @@
"Phone is required": "Phone is required", "Phone is required": "Phone is required",
"Phone number": "Phone number", "Phone number": "Phone number",
"Please enter a valid phone number": "Please enter a valid phone number", "Please enter a valid phone number": "Please enter a valid phone number",
"points": "Points",
"Points": "Points", "Points": "Points",
"points": "Points", "points": "Points",
"Points being calculated": "Points being calculated", "Points being calculated": "Points being calculated",

View File

@@ -33,15 +33,20 @@
"Book": "Varaa", "Book": "Varaa",
"Book reward night": "Kirjapalkinto-ilta", "Book reward night": "Kirjapalkinto-ilta",
"Booking number": "Varausnumero", "Booking number": "Varausnumero",
"booking.adults": "{totalAdults, plural, one {# aikuinen} other {# aikuiset}}",
"booking.guests": "Max {nrOfGuests, plural, one {# vieras} other {# vieraita}}",
"booking.nights": "{totalNights, plural, one {# yö} other {# yötä}}", "booking.nights": "{totalNights, plural, one {# yö} other {# yötä}}",
"booking.rooms": "{totalRooms, plural, one {# huone} other {# sviitti}}",
"booking.terms": "Maksamalla minkä tahansa saatavilla olevan maksutavan avulla hyväksyn tämän varauksen ehdot ja yleiset <termsLink>ehdot ja ehtoja</termsLink>, ja ymmärrän, että Scandic käsittelee minun henkilötietoni tässä varauksessa mukaisesti <privacyLink>Scandicin tietosuojavaltuuden</privacyLink> mukaisesti. Hyväksyn myös, että Scandic vaatii validin luottokortin majoituksen ajan, jos jokin jää maksamatta.", "booking.terms": "Maksamalla minkä tahansa saatavilla olevan maksutavan avulla hyväksyn tämän varauksen ehdot ja yleiset <termsLink>ehdot ja ehtoja</termsLink>, ja ymmärrän, että Scandic käsittelee minun henkilötietoni tässä varauksessa mukaisesti <privacyLink>Scandicin tietosuojavaltuuden</privacyLink> mukaisesti. Hyväksyn myös, että Scandic vaatii validin luottokortin majoituksen ajan, jos jokin jää maksamatta.",
"Breakfast": "Aamiainen", "Breakfast": "Aamiainen",
"Breakfast buffet": "Aamiaisbuffet", "Breakfast buffet": "Aamiaisbuffet",
"Breakfast excluded": "Aamiainen ei sisälly", "Breakfast excluded": "Aamiainen ei sisälly",
"Breakfast included": "Aamiainen sisältyy", "Breakfast included": "Aamiainen sisältyy",
"Breakfast restaurant": "Breakfast restaurant", "Breakfast restaurant": "Breakfast restaurant",
"Breakfast selection in next step.": "Aamiaisvalinta seuraavassa vaiheessa.",
"Bus terminal": "Bussiasema", "Bus terminal": "Bussiasema",
"Business": "Business", "Business": "Business",
"by": "mennessä",
"Cancel": "Peruuttaa", "Cancel": "Peruuttaa",
"characters": "hahmoja", "characters": "hahmoja",
"Check in": "Sisäänkirjautuminen", "Check in": "Sisäänkirjautuminen",
@@ -71,6 +76,7 @@
"Country code": "Maatunnus", "Country code": "Maatunnus",
"Credit card": "Luottokortti", "Credit card": "Luottokortti",
"Credit card deleted successfully": "Luottokortti poistettu onnistuneesti", "Credit card deleted successfully": "Luottokortti poistettu onnistuneesti",
"Currency Code": "EUR",
"Current password": "Nykyinen salasana", "Current password": "Nykyinen salasana",
"Customer service": "Asiakaspalvelu", "Customer service": "Asiakaspalvelu",
"Date of Birth": "Syntymäaika", "Date of Birth": "Syntymäaika",
@@ -112,6 +118,8 @@
"Get member benefits & offers": "Hanki jäsenetuja ja -tarjouksia", "Get member benefits & offers": "Hanki jäsenetuja ja -tarjouksia",
"Go back to edit": "Palaa muokkaamaan", "Go back to edit": "Palaa muokkaamaan",
"Go back to overview": "Palaa yleiskatsaukseen", "Go back to overview": "Palaa yleiskatsaukseen",
"guest": "Vieras",
"guests": "Vieraita",
"Guest information": "Vieraan tiedot", "Guest information": "Vieraan tiedot",
"Guests & Rooms": "Vieraat & Huoneet", "Guests & Rooms": "Vieraat & Huoneet",
"Hi": "Hi", "Hi": "Hi",
@@ -130,12 +138,13 @@
"Image gallery": "Kuvagalleria", "Image gallery": "Kuvagalleria",
"Included": "Sisälly hintaan", "Included": "Sisälly hintaan",
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Viestintäasetuksiasi ei voi hallita juuri nyt. Yritä myöhemmin uudelleen tai ota yhteyttä tukeen, jos ongelma jatkuu.", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Viestintäasetuksiasi ei voi hallita juuri nyt. Yritä myöhemmin uudelleen tai ota yhteyttä tukeen, jos ongelma jatkuu.",
"Join Scandic Friends": "Liity jäseneksi",
"Join at no cost": "Liity maksutta", "Join at no cost": "Liity maksutta",
"Join Scandic Friends": "Liity jäseneksi",
"King bed": "King-vuode", "King bed": "King-vuode",
"Language": "Kieli", "Language": "Kieli",
"Lastname": "Sukunimi", "Lastname": "Sukunimi",
"Latest searches": "Viimeisimmät haut", "Latest searches": "Viimeisimmät haut",
"Left": "jäljellä",
"Level": "Level", "Level": "Level",
"Level 1": "Taso 1", "Level 1": "Taso 1",
"Level 2": "Taso 2", "Level 2": "Taso 2",
@@ -193,6 +202,7 @@
"Not found": "Ei löydetty", "Not found": "Ei löydetty",
"Nr night, nr adult": "{nights, number} yö, {adults, number} aikuinen", "Nr night, nr adult": "{nights, number} yö, {adults, number} aikuinen",
"number": "määrä", "number": "määrä",
"n/a": "n/a",
"On your journey": "Matkallasi", "On your journey": "Matkallasi",
"Open": "Avata", "Open": "Avata",
"Open language menu": "Avaa kielivalikko", "Open language menu": "Avaa kielivalikko",

View File

@@ -32,12 +32,16 @@
"Book": "Bestill", "Book": "Bestill",
"Book reward night": "Bestill belønningskveld", "Book reward night": "Bestill belønningskveld",
"Booking number": "Bestillingsnummer", "Booking number": "Bestillingsnummer",
"booking.adults": "{totalAdults, plural, one {# voksen} other {# voksne}}",
"booking.guests": "Maks {nrOfGuests, plural, one {# gjest} other {# gjester}}",
"booking.nights": "{totalNights, plural, one {# natt} other {# netter}}", "booking.nights": "{totalNights, plural, one {# natt} other {# netter}}",
"booking.terms": "Ved å betale med en av de tilgjengelige betalingsmetodene, accepterer jeg vilkårene for denne booking og de generelle <termsLink>Vilkår og betingelser</termsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyLink>Scandic's Privatlivspolitik</privacyLink>. Jeg accepterer, at Scandic krever et gyldig kredittkort under min besøk i tilfelle at noe er tilbakebetalt.", "booking.rooms": "{totalRooms, plural, one {# rom} other {# rom}}",
"Breakfast": "Frokost", "Breakfast": "Frokost",
"Breakfast buffet": "Breakfast buffet", "Breakfast buffet": "Breakfast buffet",
"Breakfast excluded": "Frokost ekskludert", "Breakfast excluded": "Frokost ekskludert",
"Breakfast included": "Frokost inkludert", "Breakfast included": "Frokost inkludert",
"Breakfast restaurant": "Breakfast restaurant",
"Breakfast selection in next step.": "Frokostvalg i neste steg.",
"Bus terminal": "Bussterminal", "Bus terminal": "Bussterminal",
"Business": "Forretnings", "Business": "Forretnings",
"by": "innen", "by": "innen",
@@ -68,8 +72,8 @@
"Could not find requested resource": "Kunne ikke finne den forespurte ressursen", "Could not find requested resource": "Kunne ikke finne den forespurte ressursen",
"Country": "Land", "Country": "Land",
"Country code": "Landskode", "Country code": "Landskode",
"Credit card": "Kredittkort",
"Credit card deleted successfully": "Kredittkort slettet", "Credit card deleted successfully": "Kredittkort slettet",
"Currency Code": "NOK",
"Current password": "Nåværende passord", "Current password": "Nåværende passord",
"Customer service": "Kundeservice", "Customer service": "Kundeservice",
"Date of Birth": "Fødselsdato", "Date of Birth": "Fødselsdato",
@@ -111,6 +115,8 @@
"Get member benefits & offers": "Få medlemsfordeler og tilbud", "Get member benefits & offers": "Få medlemsfordeler og tilbud",
"Go back to edit": "Gå tilbake til redigering", "Go back to edit": "Gå tilbake til redigering",
"Go back to overview": "Gå tilbake til oversikten", "Go back to overview": "Gå tilbake til oversikten",
"guest": "gjest",
"guests": "gjester",
"Guest information": "Informasjon til gjester", "Guest information": "Informasjon til gjester",
"Guests & Rooms": "Gjester & rom", "Guests & Rooms": "Gjester & rom",
"Hi": "Hei", "Hi": "Hei",
@@ -125,16 +131,16 @@
"Hotels": "Hoteller", "Hotels": "Hoteller",
"How do you want to sleep?": "Hvordan vil du sove?", "How do you want to sleep?": "Hvordan vil du sove?",
"How it works": "Hvordan det fungerer", "How it works": "Hvordan det fungerer",
"I would like to get my booking confirmation via sms": "Jeg vil gerne få min booking bekræftelse via SMS",
"Image gallery": "Bildegalleri", "Image gallery": "Bildegalleri",
"Included": "Inkludert", "Included": "Inkludert",
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke mulig å administrere kommunikasjonspreferansene dine akkurat nå, prøv igjen senere eller kontakt support hvis problemet vedvarer.", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke mulig å administrere kommunikasjonspreferansene dine akkurat nå, prøv igjen senere eller kontakt support hvis problemet vedvarer.",
"Join Scandic Friends": "Bli med i Scandic Friends",
"Join at no cost": "Bli med uten kostnad", "Join at no cost": "Bli med uten kostnad",
"Join Scandic Friends": "Bli med i Scandic Friends",
"King bed": "King-size-seng", "King bed": "King-size-seng",
"Language": "Språk", "Language": "Språk",
"Lastname": "Etternavn", "Lastname": "Etternavn",
"Latest searches": "Siste søk", "Latest searches": "Siste søk",
"Left": "igjen",
"Level": "Nivå", "Level": "Nivå",
"Level 1": "Nivå 1", "Level 1": "Nivå 1",
"Level 2": "Nivå 2", "Level 2": "Nivå 2",
@@ -192,6 +198,7 @@
"Not found": "Ikke funnet", "Not found": "Ikke funnet",
"Nr night, nr adult": "{nights, number} natt, {adults, number} voksen", "Nr night, nr adult": "{nights, number} natt, {adults, number} voksen",
"number": "antall", "number": "antall",
"n/a": "n/a",
"On your journey": "På reisen din", "On your journey": "På reisen din",
"Open": "Åpen", "Open": "Åpen",
"Open language menu": "Åpne språkmenyen", "Open language menu": "Åpne språkmenyen",
@@ -330,5 +337,8 @@
"Zip code": "Post kode", "Zip code": "Post kode",
"Zoo": "Dyrehage", "Zoo": "Dyrehage",
"Zoom in": "Zoom inn", "Zoom in": "Zoom inn",
"Zoom out": "Zoom ut" "Zoom out": "Zoom ut",
"{amount} {currency}": "{amount} {currency}",
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
"{width} cm × {length} cm": "{width} cm × {length} cm"
} }

View File

@@ -32,15 +32,20 @@
"Book": "Boka", "Book": "Boka",
"Book reward night": "Boka frinatt", "Book reward night": "Boka frinatt",
"Booking number": "Bokningsnummer", "Booking number": "Bokningsnummer",
"booking.adults": "{totalAdults, plural, one {# vuxen} other {# vuxna}}",
"booking.guests": "Max {nrOfGuests, plural, one {# gäst} other {# gäster}}",
"booking.nights": "{totalNights, plural, one {# natt} other {# nätter}}", "booking.nights": "{totalNights, plural, one {# natt} other {# nätter}}",
"booking.rooms": "{totalRooms, plural, one {# rum} other {# rum}}",
"booking.terms": "Genom att betala med någon av de tillgängliga betalningsmetoderna accepterar jag villkoren för denna bokning och de generella <termsLink>Villkoren och villkoren</termsLink>, och förstår att Scandic kommer att behandla min personliga data i samband med denna bokning i enlighet med <privacyLink>Scandics integritetspolicy</privacyLink>. Jag accepterar att Scandic kräver ett giltigt kreditkort under min besök i fall att något är tillbaka betalt.", "booking.terms": "Genom att betala med någon av de tillgängliga betalningsmetoderna accepterar jag villkoren för denna bokning och de generella <termsLink>Villkoren och villkoren</termsLink>, och förstår att Scandic kommer att behandla min personliga data i samband med denna bokning i enlighet med <privacyLink>Scandics integritetspolicy</privacyLink>. Jag accepterar att Scandic kräver ett giltigt kreditkort under min besök i fall att något är tillbaka betalt.",
"Breakfast": "Frukost", "Breakfast": "Frukost",
"Breakfast buffet": "Frukostbuffé", "Breakfast buffet": "Frukostbuffé",
"Breakfast excluded": "Frukost ingår ej", "Breakfast excluded": "Frukost ingår ej",
"Breakfast included": "Frukost ingår", "Breakfast included": "Frukost ingår",
"Breakfast restaurant": "Breakfast restaurant", "Breakfast restaurant": "Breakfast restaurant",
"Breakfast selection in next step.": "Frukostval i nästa steg.",
"Bus terminal": "Bussterminal", "Bus terminal": "Bussterminal",
"Business": "Business", "Business": "Business",
"by": "innan",
"Cancel": "Avbryt", "Cancel": "Avbryt",
"characters": "tecken", "characters": "tecken",
"Check in": "Checka in", "Check in": "Checka in",
@@ -68,8 +73,8 @@
"Could not find requested resource": "Det gick inte att hitta den begärda resursen", "Could not find requested resource": "Det gick inte att hitta den begärda resursen",
"Country": "Land", "Country": "Land",
"Country code": "Landskod", "Country code": "Landskod",
"Credit card": "Kreditkort",
"Credit card deleted successfully": "Kreditkort har tagits bort", "Credit card deleted successfully": "Kreditkort har tagits bort",
"Currency Code": "SEK",
"Current password": "Nuvarande lösenord", "Current password": "Nuvarande lösenord",
"Customer service": "Kundservice", "Customer service": "Kundservice",
"Date of Birth": "Födelsedatum", "Date of Birth": "Födelsedatum",
@@ -111,6 +116,8 @@
"Get member benefits & offers": "Ta del av medlemsförmåner och erbjudanden", "Get member benefits & offers": "Ta del av medlemsförmåner och erbjudanden",
"Go back to edit": "Gå tillbaka till redigeringen", "Go back to edit": "Gå tillbaka till redigeringen",
"Go back to overview": "Gå tillbaka till översikten", "Go back to overview": "Gå tillbaka till översikten",
"guest": "gäst",
"guests": "gäster",
"Guest information": "Information till gästerna", "Guest information": "Information till gästerna",
"Guests & Rooms": "Gäster & rum", "Guests & Rooms": "Gäster & rum",
"Hi": "Hej", "Hi": "Hej",
@@ -125,16 +132,16 @@
"Hotels": "Hotell", "Hotels": "Hotell",
"How do you want to sleep?": "Hur vill du sova?", "How do you want to sleep?": "Hur vill du sova?",
"How it works": "Hur det fungerar", "How it works": "Hur det fungerar",
"I would like to get my booking confirmation via sms": "Jag vill få min bokningsbekräftelse via SMS",
"Image gallery": "Bildgalleri", "Image gallery": "Bildgalleri",
"Included": "Inkluderad", "Included": "Inkluderad",
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det gick inte att hantera dina kommunikationsinställningar just nu, försök igen senare eller kontakta supporten om problemet kvarstår.", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det gick inte att hantera dina kommunikationsinställningar just nu, försök igen senare eller kontakta supporten om problemet kvarstår.",
"Join Scandic Friends": "Gå med i Scandic Friends",
"Join at no cost": "Gå med utan kostnad", "Join at no cost": "Gå med utan kostnad",
"Join Scandic Friends": "Gå med i Scandic Friends",
"King bed": "King size-säng", "King bed": "King size-säng",
"Language": "Språk", "Language": "Språk",
"Lastname": "Efternamn", "Lastname": "Efternamn",
"Latest searches": "Senaste sökningarna", "Latest searches": "Senaste sökningarna",
"Left": "kvar",
"Level": "Nivå", "Level": "Nivå",
"Level 1": "Nivå 1", "Level 1": "Nivå 1",
"Level 2": "Nivå 2", "Level 2": "Nivå 2",
@@ -192,6 +199,7 @@
"Not found": "Hittades inte", "Not found": "Hittades inte",
"Nr night, nr adult": "{nights, number} natt, {adults, number} vuxen", "Nr night, nr adult": "{nights, number} natt, {adults, number} vuxen",
"number": "nummer", "number": "nummer",
"n/a": "n/a",
"On your journey": "På din resa", "On your journey": "På din resa",
"Open": "Öppna", "Open": "Öppna",
"Open language menu": "Öppna språkmenyn", "Open language menu": "Öppna språkmenyn",
@@ -209,8 +217,8 @@
"Phone is required": "Telefonnummer är obligatorisk", "Phone is required": "Telefonnummer är obligatorisk",
"Phone number": "Telefonnummer", "Phone number": "Telefonnummer",
"Please enter a valid phone number": "Var vänlig och ange ett giltigt telefonnummer", "Please enter a valid phone number": "Var vänlig och ange ett giltigt telefonnummer",
"Points": "Poäng",
"points": "poäng", "points": "poäng",
"Points": "Poäng",
"Points being calculated": "Poäng beräknas", "Points being calculated": "Poäng beräknas",
"Points earned prior to May 1, 2021": "Intjänade poäng före den 1 maj 2021", "Points earned prior to May 1, 2021": "Intjänade poäng före den 1 maj 2021",
"Points may take up to 10 days to be displayed.": "Det kan ta upp till 10 dagar innan poäng visas.", "Points may take up to 10 days to be displayed.": "Det kan ta upp till 10 dagar innan poäng visas.",

View File

@@ -17,6 +17,7 @@ import { findLang } from "@/utils/languages"
export const middleware: NextMiddleware = async (request, event) => { export const middleware: NextMiddleware = async (request, event) => {
const headers = getDefaultRequestHeaders(request) const headers = getDefaultRequestHeaders(request)
const lang = findLang(request.nextUrl.pathname) const lang = findLang(request.nextUrl.pathname)
if (!lang) { if (!lang) {
// Lang is required for all our middleware. // Lang is required for all our middleware.
// Without it we shortcircuit early. // Without it we shortcircuit early.

View File

@@ -425,26 +425,39 @@ const roomFacilitiesSchema = z.object({
sortOrder: z.number(), sortOrder: z.number(),
}) })
export const roomSchema = z.object({ export const roomSchema = z
attributes: z.object({ .object({
name: z.string(), attributes: z.object({
sortOrder: z.number(), name: z.string(),
content: roomContentSchema, sortOrder: z.number(),
roomTypes: z.array(roomTypesSchema), content: roomContentSchema,
roomFacilities: z.array(roomFacilitiesSchema), roomTypes: z.array(roomTypesSchema),
occupancy: z.object({ roomFacilities: z.array(roomFacilitiesSchema),
total: z.number(), occupancy: z.object({
adults: z.number(), total: z.number(),
children: z.number(), adults: z.number(),
children: z.number(),
}),
roomSize: z.object({
min: z.number(),
max: z.number(),
}),
}), }),
roomSize: z.object({ id: z.string(),
min: z.number(), type: z.enum(["roomcategories"]),
max: z.number(), })
}), .transform((data) => {
}), return {
id: z.string(), descriptions: data.attributes.content.texts.descriptions,
type: z.enum(["roomcategories"]), id: data.id,
}) images: data.attributes.content.images,
name: data.attributes.name,
occupancy: data.attributes.occupancy,
roomSize: data.attributes.roomSize,
sortOrder: data.attributes.sortOrder,
type: data.type,
}
})
const merchantInformationSchema = z.object({ const merchantInformationSchema = z.object({
webMerchantId: z.string(), webMerchantId: z.string(),
@@ -572,40 +585,23 @@ export type HotelsAvailability = z.infer<typeof hotelsAvailabilitySchema>
export type HotelsAvailabilityPrices = export type HotelsAvailabilityPrices =
HotelsAvailability["data"][number]["attributes"]["bestPricePerNight"] HotelsAvailability["data"][number]["attributes"]["bestPricePerNight"]
const priceSchema = z.object({
pricePerNight: z.string(),
pricePerStay: z.string(),
currency: z.string(),
})
export const productTypePriceSchema = z.object({
rateCode: z.string(),
rateType: z.string().optional(),
localPrice: priceSchema,
requestedPrice: priceSchema.optional(),
})
const productSchema = z.object({ const productSchema = z.object({
productType: z.object({ productType: z.object({
public: z.object({ public: productTypePriceSchema.optional(),
rateCode: z.string(), member: productTypePriceSchema.optional(),
rateType: z.string().optional(),
localPrice: z.object({
pricePerNight: z.string(),
pricePerStay: z.string(),
currency: z.string(),
}),
requestedPrice: z
.object({
pricePerNight: z.string(),
pricePerStay: z.string(),
currency: z.string(),
})
.optional(),
}),
member: z.object({
rateCode: z.string(),
rateType: z.string().optional(),
localPrice: z.object({
pricePerNight: z.string(),
pricePerStay: z.string(),
currency: z.string(),
}),
requestedPrice: z
.object({
pricePerNight: z.string(),
pricePerStay: z.string(),
currency: z.string(),
})
.optional(),
}),
}), }),
}) })

View File

@@ -635,6 +635,7 @@ export const hotelQueryRouter = router({
query: { hotelId, params: params }, query: { hotelId, params: params },
}) })
) )
return validateHotelData.data return validateHotelData.data
}), }),
}), }),

View File

@@ -2,7 +2,7 @@ import type { RoomData } from "@/types/hotel"
export interface RoomCardProps { export interface RoomCardProps {
id: string id: string
images: RoomData["attributes"]["content"]["images"] images: RoomData["images"]
title: string title: string
subtitle: string subtitle: string
badgeTextTransKey: string | null badgeTextTransKey: string | null

View File

@@ -1,8 +1,18 @@
import { Product, RateDefinition } from "@/server/routers/hotels/output" import { z } from "zod"
import { Product, productTypePriceSchema } from "@/server/routers/hotels/output"
type ProductPrice = z.output<typeof productTypePriceSchema>
export type FlexibilityOptionProps = { export type FlexibilityOptionProps = {
product: Product | undefined product: Product | undefined
name: string name: string
value: string value: string
paymentTerm: string paymentTerm: string
priceInformation?: Array<string>
}
export interface PriceListProps {
publicPrice?: ProductPrice | Record<string, never>
memberPrice?: ProductPrice | Record<string, never>
} }

View File

@@ -0,0 +1,5 @@
import type { PopoverProps } from "react-aria-components"
export interface PricePopoverProps extends Omit<PopoverProps, "children"> {
children: React.ReactNode
}

View File

@@ -3,7 +3,10 @@ import {
RoomConfiguration, RoomConfiguration,
} from "@/server/routers/hotels/output" } from "@/server/routers/hotels/output"
import { RoomData } from "@/types/hotel"
export type RoomCardProps = { export type RoomCardProps = {
roomConfiguration: RoomConfiguration roomConfiguration: RoomConfiguration
rateDefinitions: RateDefinition[] rateDefinitions: RateDefinition[]
roomCategories: RoomData[]
} }

View File

@@ -1,5 +1,8 @@
import { RoomsAvailability } from "@/server/routers/hotels/output" import { RoomsAvailability } from "@/server/routers/hotels/output"
import { RoomData } from "@/types/hotel"
export interface RoomSelectionProps { export interface RoomSelectionProps {
roomConfigurations: RoomsAvailability roomConfigurations: RoomsAvailability
roomCategories: RoomData[]
} }