Merge remote-tracking branch 'origin' into feature/tracking
This commit is contained in:
@@ -1,22 +1,11 @@
|
|||||||
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
|
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
|
||||||
import { Suspense } from "react"
|
|
||||||
|
|
||||||
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
|
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
import Header from "@/components/HotelReservation/BookingConfirmation/Header"
|
import BookingConfirmation from "@/components/HotelReservation/BookingConfirmation"
|
||||||
import HotelDetails from "@/components/HotelReservation/BookingConfirmation/HotelDetails"
|
|
||||||
import PaymentDetails from "@/components/HotelReservation/BookingConfirmation/PaymentDetails"
|
|
||||||
import Promos from "@/components/HotelReservation/BookingConfirmation/Promos"
|
|
||||||
import Receipt from "@/components/HotelReservation/BookingConfirmation/Receipt"
|
|
||||||
import Rooms from "@/components/HotelReservation/BookingConfirmation/Rooms"
|
|
||||||
import SidePanel from "@/components/HotelReservation/SidePanel"
|
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
|
||||||
import TrackingSDK from "@/components/TrackingSDK"
|
import TrackingSDK from "@/components/TrackingSDK"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import styles from "./page.module.css"
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TrackingChannelEnum,
|
TrackingChannelEnum,
|
||||||
TrackingSDKHotelInfo,
|
TrackingSDKHotelInfo,
|
||||||
@@ -30,10 +19,10 @@ export default async function BookingConfirmationPage({
|
|||||||
searchParams,
|
searchParams,
|
||||||
}: PageArgs<LangParams, { confirmationNumber: string }>) {
|
}: PageArgs<LangParams, { confirmationNumber: string }>) {
|
||||||
setLang(params.lang)
|
setLang(params.lang)
|
||||||
void getBookingConfirmation(searchParams.confirmationNumber)
|
const bookingConfirmationPromise = getBookingConfirmation(
|
||||||
const { confirmationNumber } = searchParams
|
searchParams.confirmationNumber
|
||||||
|
)
|
||||||
const { hotel, booking } = await getBookingConfirmation(confirmationNumber)
|
const { hotel, booking } = await bookingConfirmationPromise
|
||||||
|
|
||||||
const arrivalDate = new Date(booking.checkInDate)
|
const arrivalDate = new Date(booking.checkInDate)
|
||||||
const departureDate = new Date(booking.checkOutDate)
|
const departureDate = new Date(booking.checkOutDate)
|
||||||
@@ -83,31 +72,14 @@ export default async function BookingConfirmationPage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={styles.main}>
|
<>
|
||||||
<Suspense fallback={<LoadingSpinner fullPage />}>
|
<TrackingSDK
|
||||||
<Header confirmationNumber={searchParams.confirmationNumber} />
|
pageData={initialPageTrackingData}
|
||||||
<div className={styles.booking}>
|
hotelInfo={initialHotelsTrackingData}
|
||||||
<Rooms confirmationNumber={searchParams.confirmationNumber} />
|
/>
|
||||||
<PaymentDetails
|
<BookingConfirmation
|
||||||
confirmationNumber={searchParams.confirmationNumber}
|
bookingConfirmationPromise={bookingConfirmationPromise}
|
||||||
/>
|
/>
|
||||||
<Divider color="primaryLightSubtle" />
|
</>
|
||||||
<HotelDetails confirmationNumber={searchParams.confirmationNumber} />
|
|
||||||
<Promos />
|
|
||||||
<div className={styles.mobileReceipt}>
|
|
||||||
<Receipt confirmationNumber={searchParams.confirmationNumber} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<aside className={styles.aside}>
|
|
||||||
<SidePanel variant="receipt">
|
|
||||||
<Receipt confirmationNumber={searchParams.confirmationNumber} />
|
|
||||||
</SidePanel>
|
|
||||||
</aside>
|
|
||||||
<TrackingSDK
|
|
||||||
pageData={initialPageTrackingData}
|
|
||||||
hotelInfo={initialHotelsTrackingData}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
|
||||||
</main>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
BOOKING_CONFIRMATION_NUMBER,
|
BOOKING_CONFIRMATION_NUMBER,
|
||||||
PaymentErrorCodeEnum,
|
PaymentErrorCodeEnum,
|
||||||
} from "@/constants/booking"
|
} from "@/constants/booking"
|
||||||
import { Lang } from "@/constants/languages"
|
|
||||||
import {
|
import {
|
||||||
bookingConfirmation,
|
bookingConfirmation,
|
||||||
payment,
|
payment,
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ export async function POST(request: NextRequest) {
|
|||||||
const entryLocale = entry.publish_details?.locale ?? entry.locale
|
const entryLocale = entry.publish_details?.locale ?? entry.locale
|
||||||
|
|
||||||
const refsTag = generateRefsResponseTag(entryLocale, entry.uid)
|
const refsTag = generateRefsResponseTag(entryLocale, entry.uid)
|
||||||
|
const contentEntryTag = generateRefsResponseTag(
|
||||||
|
entryLocale,
|
||||||
|
content_type.uid
|
||||||
|
)
|
||||||
const refTag = generateRefTag(entryLocale, content_type.uid, entry.uid)
|
const refTag = generateRefTag(entryLocale, content_type.uid, entry.uid)
|
||||||
const tag = generateTag(entryLocale, entry.uid)
|
const tag = generateTag(entryLocale, entry.uid)
|
||||||
const languageSwitcherTag = generateTag(
|
const languageSwitcherTag = generateTag(
|
||||||
@@ -97,6 +101,9 @@ export async function POST(request: NextRequest) {
|
|||||||
console.info(`Revalidating metadataTag: ${metadataTag}`)
|
console.info(`Revalidating metadataTag: ${metadataTag}`)
|
||||||
revalidateTag(metadataTag)
|
revalidateTag(metadataTag)
|
||||||
|
|
||||||
|
console.info(`Revalidating contentEntryTag: ${contentEntryTag}`)
|
||||||
|
revalidateTag(contentEntryTag)
|
||||||
|
|
||||||
if (entry.breadcrumbs) {
|
if (entry.breadcrumbs) {
|
||||||
const breadcrumbsRefsTag = generateRefsResponseTag(
|
const breadcrumbsRefsTag = generateRefsResponseTag(
|
||||||
entryLocale,
|
entryLocale,
|
||||||
|
|||||||
@@ -102,7 +102,7 @@
|
|||||||
--max-width: 94.5rem;
|
--max-width: 94.5rem;
|
||||||
--max-width-content: 74.75rem;
|
--max-width-content: 74.75rem;
|
||||||
--max-width-text-block: 49.5rem;
|
--max-width-text-block: 49.5rem;
|
||||||
--current-mobile-site-header-height: 70.047px;
|
--current-mobile-site-header-height: 52.41px;
|
||||||
--max-width-navigation: 89.5rem;
|
--max-width-navigation: 89.5rem;
|
||||||
|
|
||||||
--max-width-spacing: calc(var(--Layout-Mobile-Margin-Margin-min) * 2);
|
--max-width-spacing: calc(var(--Layout-Mobile-Margin-Margin-min) * 2);
|
||||||
|
|||||||
@@ -66,11 +66,16 @@ export default function BookingWidgetClient({
|
|||||||
const reqFromDate = bookingWidgetSearchData?.fromDate?.toString()
|
const reqFromDate = bookingWidgetSearchData?.fromDate?.toString()
|
||||||
const reqToDate = bookingWidgetSearchData?.toDate?.toString()
|
const reqToDate = bookingWidgetSearchData?.toDate?.toString()
|
||||||
|
|
||||||
|
const parsedFromDate = reqFromDate ? dt(reqFromDate) : undefined
|
||||||
|
const parsedToDate = reqToDate ? dt(reqToDate) : undefined
|
||||||
|
|
||||||
|
const now = dt()
|
||||||
|
|
||||||
const isDateParamValid =
|
const isDateParamValid =
|
||||||
reqFromDate &&
|
parsedFromDate &&
|
||||||
reqToDate &&
|
parsedToDate &&
|
||||||
dt(reqFromDate).isAfter(dt().subtract(1, "day")) &&
|
parsedFromDate.isSameOrAfter(now, "day") &&
|
||||||
dt(reqToDate).isAfter(dt(reqFromDate))
|
parsedToDate.isAfter(parsedFromDate)
|
||||||
|
|
||||||
const selectedLocation = bookingWidgetSearchData
|
const selectedLocation = bookingWidgetSearchData
|
||||||
? getLocationObj(
|
? getLocationObj(
|
||||||
@@ -97,11 +102,11 @@ export default function BookingWidgetClient({
|
|||||||
// UTC is required to handle requests from far away timezones https://scandichotels.atlassian.net/browse/SWAP-6375 & PET-507
|
// UTC is required to handle requests from far away timezones https://scandichotels.atlassian.net/browse/SWAP-6375 & PET-507
|
||||||
// This is specifically to handle timezones falling in different dates.
|
// This is specifically to handle timezones falling in different dates.
|
||||||
fromDate: isDateParamValid
|
fromDate: isDateParamValid
|
||||||
? dt(bookingWidgetSearchData?.fromDate).format("YYYY-M-D")
|
? parsedFromDate.format("YYYY-MM-DD")
|
||||||
: dt().utc().format("YYYY-M-D"),
|
: now.utc().format("YYYY-MM-DD"),
|
||||||
toDate: isDateParamValid
|
toDate: isDateParamValid
|
||||||
? dt(bookingWidgetSearchData?.toDate).format("YYYY-M-D")
|
? parsedToDate.format("YYYY-MM-DD")
|
||||||
: dt().utc().add(1, "day").format("YYYY-M-D"),
|
: now.utc().add(1, "day").format("YYYY-MM-DD"),
|
||||||
},
|
},
|
||||||
bookingCode: "",
|
bookingCode: "",
|
||||||
redemption: false,
|
redemption: false,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default function ActivitiesCardGrid(activitiesCard: ActivityCard) {
|
|||||||
theme: hasImage ? "image" : "primaryDark",
|
theme: hasImage ? "image" : "primaryDark",
|
||||||
primaryButton: hasImage
|
primaryButton: hasImage
|
||||||
? {
|
? {
|
||||||
href: activitiesCard.contentPage.href,
|
href: `?s=${activities[lang]}`,
|
||||||
title: activitiesCard.ctaText,
|
title: activitiesCard.ctaText,
|
||||||
isExternal: false,
|
isExternal: false,
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,7 @@ export default function ActivitiesCardGrid(activitiesCard: ActivityCard) {
|
|||||||
secondaryButton: hasImage
|
secondaryButton: hasImage
|
||||||
? undefined
|
? undefined
|
||||||
: {
|
: {
|
||||||
href: activitiesCard.contentPage.href,
|
href: `?s=${activities[lang]}`,
|
||||||
title: activitiesCard.ctaText,
|
title: activitiesCard.ctaText,
|
||||||
isExternal: false,
|
isExternal: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.buttonContainer {
|
||||||
|
background-color: var(--Base-Background-Primary-Normal);
|
||||||
|
border-top: 1px solid var(--Base-Border-Subtle);
|
||||||
|
padding: var(--Spacing-x4) var(--Spacing-x2);
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { activities } from "@/constants/routes/hotelPageParams"
|
||||||
|
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
|
import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
||||||
|
import Preamble from "@/components/TempDesignSystem/Text/Preamble"
|
||||||
|
import { getIntl } from "@/i18n"
|
||||||
|
import { getLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
|
import styles from "./activities.module.css"
|
||||||
|
|
||||||
|
import type { ActivitiesSidePeekProps } from "@/types/components/hotelPage/sidepeek/activities"
|
||||||
|
|
||||||
|
export default async function ActivitiesSidePeek({
|
||||||
|
contentPage,
|
||||||
|
}: ActivitiesSidePeekProps) {
|
||||||
|
const lang = getLang()
|
||||||
|
const intl = await getIntl()
|
||||||
|
const { href, preamble } = contentPage
|
||||||
|
return (
|
||||||
|
<SidePeek
|
||||||
|
contentKey={activities[lang]}
|
||||||
|
title={intl.formatMessage({ id: "Activities" })}
|
||||||
|
>
|
||||||
|
<Preamble>{preamble}</Preamble>
|
||||||
|
<div className={styles.buttonContainer}>
|
||||||
|
<Button theme="base" intent="secondary" asChild>
|
||||||
|
<Link href={href} color="burgundy" weight="bold">
|
||||||
|
{intl.formatMessage({ id: "Show activities calendar" })}
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</SidePeek>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from "@/types/components/hotelPage/sidepeek/parking"
|
} from "@/types/components/hotelPage/sidepeek/parking"
|
||||||
|
|
||||||
export default async function ParkingPrices({
|
export default async function ParkingPrices({
|
||||||
data,
|
pricing,
|
||||||
currency,
|
currency,
|
||||||
freeParking,
|
freeParking,
|
||||||
}: ParkingPricesProps) {
|
}: ParkingPricesProps) {
|
||||||
@@ -31,7 +31,7 @@ export default async function ParkingPrices({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredPeriods = data?.filter((filter) => filter.period !== "Hour")
|
const filteredPeriods = pricing?.filter((filter) => filter.period !== "Hour")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import { ExternalLinkIcon } from "@/components/Icons"
|
||||||
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
|
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
@@ -12,7 +15,10 @@ import styles from "./parkingAmenity.module.css"
|
|||||||
import type { ParkingAmenityProps } from "@/types/components/hotelPage/sidepeek/parking"
|
import type { ParkingAmenityProps } from "@/types/components/hotelPage/sidepeek/parking"
|
||||||
import { IconName } from "@/types/components/icon"
|
import { IconName } from "@/types/components/icon"
|
||||||
|
|
||||||
export default async function ParkingAmenity({ parking }: ParkingAmenityProps) {
|
export default async function ParkingAmenity({
|
||||||
|
parking,
|
||||||
|
hasParkingPage,
|
||||||
|
}: ParkingAmenityProps) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -43,7 +49,7 @@ export default async function ParkingAmenity({ parking }: ParkingAmenityProps) {
|
|||||||
</Caption>
|
</Caption>
|
||||||
<Divider color="baseSurfaceSubtleHover" />
|
<Divider color="baseSurfaceSubtleHover" />
|
||||||
<ParkingPrices
|
<ParkingPrices
|
||||||
data={data.pricing.localCurrency.ordinary}
|
pricing={data.pricing.localCurrency.ordinary}
|
||||||
currency={data.pricing.localCurrency.currency}
|
currency={data.pricing.localCurrency.currency}
|
||||||
freeParking={data.pricing.freeParking}
|
freeParking={data.pricing.freeParking}
|
||||||
/>
|
/>
|
||||||
@@ -54,15 +60,41 @@ export default async function ParkingAmenity({ parking }: ParkingAmenityProps) {
|
|||||||
</Caption>
|
</Caption>
|
||||||
<Divider color="baseSurfaceSubtleHover" />
|
<Divider color="baseSurfaceSubtleHover" />
|
||||||
<ParkingPrices
|
<ParkingPrices
|
||||||
data={data.pricing.localCurrency.weekend}
|
pricing={data.pricing.localCurrency.weekend}
|
||||||
currency={data.pricing.localCurrency.currency}
|
currency={data.pricing.localCurrency.currency}
|
||||||
freeParking={data.pricing.freeParking}
|
freeParking={data.pricing.freeParking}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{data.externalParkingUrl && (
|
||||||
|
<Button theme="base" intent="primary" asChild>
|
||||||
|
<Link
|
||||||
|
href={data.externalParkingUrl}
|
||||||
|
color="white"
|
||||||
|
weight="bold"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "Book parking" })}
|
||||||
|
<ExternalLinkIcon color="white" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{hasParkingPage && (
|
||||||
|
<Button
|
||||||
|
className={styles.parkingPageLink}
|
||||||
|
theme="base"
|
||||||
|
intent="secondary"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
{/* Not decided how to handle linking to separate parking page */}
|
||||||
|
<Link href="#" color="burgundy" weight="bold">
|
||||||
|
{intl.formatMessage({ id: "About parking" })}
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
.wrapper {
|
.wrapper,
|
||||||
|
.information {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x3);
|
gap: var(--Spacing-x3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.information,
|
|
||||||
.list,
|
.list,
|
||||||
.prices {
|
.prices {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -18,3 +18,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x1);
|
gap: var(--Spacing-x1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.parkingPageLink {
|
||||||
|
margin-top: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { meetingsAndConferences } from "@/constants/routes/hotelPageParams"
|
||||||
|
|
||||||
|
import Image from "@/components/Image"
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
|
import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
||||||
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
|
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||||
|
import { getIntl } from "@/i18n"
|
||||||
|
import { getLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
|
import styles from "./meetingsAndConferences.module.css"
|
||||||
|
|
||||||
|
import type { MeetingsAndConferencesSidePeekProps } from "@/types/components/hotelPage/sidepeek/meetingsAndConferences"
|
||||||
|
|
||||||
|
export default async function MeetingsAndConferencesSidePeek({
|
||||||
|
meetingFacilities,
|
||||||
|
descriptions,
|
||||||
|
link,
|
||||||
|
}: MeetingsAndConferencesSidePeekProps) {
|
||||||
|
const lang = getLang()
|
||||||
|
const intl = await getIntl()
|
||||||
|
const fallbackAlt = intl.formatMessage({ id: "Creative spaces for meetings" })
|
||||||
|
|
||||||
|
const primaryImage = meetingFacilities?.heroImages[0]?.imageSizes.medium
|
||||||
|
const primaryAltText =
|
||||||
|
meetingFacilities?.heroImages[0]?.metaData.altText || fallbackAlt
|
||||||
|
|
||||||
|
const secondaryImage = meetingFacilities?.heroImages[1]?.imageSizes.medium
|
||||||
|
const secondaryAltText =
|
||||||
|
meetingFacilities?.heroImages[1]?.metaData.altText || fallbackAlt
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidePeek
|
||||||
|
contentKey={meetingsAndConferences[lang]}
|
||||||
|
title={intl.formatMessage({ id: "Meetings & Conferences" })}
|
||||||
|
>
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
<Subtitle color="burgundy" asChild>
|
||||||
|
<Title level="h3">
|
||||||
|
{intl.formatMessage({ id: "Creative spaces for meetings" })}
|
||||||
|
</Title>
|
||||||
|
</Subtitle>
|
||||||
|
{primaryImage && (
|
||||||
|
<div className={secondaryImage ? styles.imageContainer : ""}>
|
||||||
|
<Image
|
||||||
|
src={primaryImage}
|
||||||
|
alt={primaryAltText}
|
||||||
|
height={300}
|
||||||
|
width={200}
|
||||||
|
className={styles.image}
|
||||||
|
/>
|
||||||
|
{secondaryImage && (
|
||||||
|
<Image
|
||||||
|
src={secondaryImage}
|
||||||
|
alt={secondaryAltText}
|
||||||
|
height={300}
|
||||||
|
width={200}
|
||||||
|
className={`${styles.image} ${styles.secondaryImage}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{descriptions?.medium && (
|
||||||
|
<Body color="uiTextHighContrast">{descriptions.medium}</Body>
|
||||||
|
)}
|
||||||
|
{link && (
|
||||||
|
<div className={styles.buttonContainer}>
|
||||||
|
<Button fullWidth theme="base" intent="secondary" asChild>
|
||||||
|
<Link href={link} weight="bold" color="burgundy">
|
||||||
|
{intl.formatMessage({ id: "About meetings & conferences" })}
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SidePeek>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
.wrapper {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x3);
|
||||||
|
margin-bottom: calc(
|
||||||
|
var(--Spacing-x4) * 2 + 80px
|
||||||
|
); /* Creates space between the wrapper and buttonContainer */
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: var(--Corner-radius-Medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondaryImage {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
background-color: var(--Base-Background-Primary-Normal);
|
||||||
|
border-top: 1px solid var(--Base-Border-Subtle);
|
||||||
|
padding: var(--Spacing-x4) var(--Spacing-x2);
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.imageContainer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
height: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondaryImage {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
export { default as AboutTheHotelSidePeek } from "./AboutTheHotel"
|
export { default as AboutTheHotelSidePeek } from "./AboutTheHotel"
|
||||||
|
export { default as ActivitiesSidePeek } from "./Activities"
|
||||||
export { default as AmenitiesSidePeek } from "./Amenities"
|
export { default as AmenitiesSidePeek } from "./Amenities"
|
||||||
|
export { default as MeetingsAndConferencesSidePeek } from "./MeetingsAndConferences"
|
||||||
export { default as RoomSidePeek } from "./Room"
|
export { default as RoomSidePeek } from "./Room"
|
||||||
export { default as WellnessAndExerciseSidePeek } from "./WellnessAndExercise"
|
export { default as WellnessAndExerciseSidePeek } from "./WellnessAndExercise"
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { notFound } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
|
|
||||||
import {
|
import { restaurantAndBar } from "@/constants/routes/hotelPageParams"
|
||||||
activities,
|
|
||||||
meetingsAndConferences,
|
|
||||||
restaurantAndBar,
|
|
||||||
} from "@/constants/routes/hotelPageParams"
|
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import { getHotelData, getHotelPage } from "@/lib/trpc/memoizedRequests"
|
import { getHotelData, getHotelPage } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
@@ -29,7 +25,9 @@ import PreviewImages from "./PreviewImages"
|
|||||||
import { Rooms } from "./Rooms"
|
import { Rooms } from "./Rooms"
|
||||||
import {
|
import {
|
||||||
AboutTheHotelSidePeek,
|
AboutTheHotelSidePeek,
|
||||||
|
ActivitiesSidePeek,
|
||||||
AmenitiesSidePeek,
|
AmenitiesSidePeek,
|
||||||
|
MeetingsAndConferencesSidePeek,
|
||||||
RoomSidePeek,
|
RoomSidePeek,
|
||||||
WellnessAndExerciseSidePeek,
|
WellnessAndExerciseSidePeek,
|
||||||
} from "./SidePeeks"
|
} from "./SidePeeks"
|
||||||
@@ -200,20 +198,13 @@ export default async function HotelPage({ hotelId }: HotelPageProps) {
|
|||||||
Restaurant & Bar
|
Restaurant & Bar
|
||||||
</SidePeek>
|
</SidePeek>
|
||||||
<WellnessAndExerciseSidePeek healthFacilities={healthFacilities} />
|
<WellnessAndExerciseSidePeek healthFacilities={healthFacilities} />
|
||||||
<SidePeek
|
{activitiesCard && (
|
||||||
contentKey={activities[lang]}
|
<ActivitiesSidePeek contentPage={activitiesCard.contentPage} />
|
||||||
title={intl.formatMessage({ id: "Activities" })}
|
)}
|
||||||
>
|
<MeetingsAndConferencesSidePeek
|
||||||
{/* TODO */}
|
meetingFacilities={conferencesAndMeetings}
|
||||||
Activities
|
descriptions={hotelContent.texts.meetingDescription}
|
||||||
</SidePeek>
|
/>
|
||||||
<SidePeek
|
|
||||||
contentKey={meetingsAndConferences[lang]}
|
|
||||||
title={intl.formatMessage({ id: "Meetings & Conferences" })}
|
|
||||||
>
|
|
||||||
{/* TODO */}
|
|
||||||
Meetings & Conferences
|
|
||||||
</SidePeek>
|
|
||||||
{roomCategories.map((room) => (
|
{roomCategories.map((room) => (
|
||||||
<RoomSidePeek key={room.name} room={room} />
|
<RoomSidePeek key={room.name} room={room} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
height: 32px;
|
height: 38px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-family: Helvetica, Arial, sans-serif;
|
font-family: Helvetica, Arial, sans-serif;
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
|
align-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:hover {
|
.button:hover {
|
||||||
|
|||||||
@@ -1,49 +1,13 @@
|
|||||||
import { homeHrefs } from "@/constants/homeHrefs"
|
import { MainMenuSkeleton } from "../MainMenu"
|
||||||
import { env } from "@/env/server"
|
import { TopMenuSkeleton } from "../TopMenu"
|
||||||
import { getCurrentHeader } from "@/lib/trpc/memoizedRequests"
|
|
||||||
|
|
||||||
import { getLang } from "@/i18n/serverContext"
|
|
||||||
|
|
||||||
import { MainMenu } from "../MainMenu"
|
|
||||||
import OfflineBanner from "../OfflineBanner"
|
|
||||||
import TopMenu from "../TopMenu"
|
|
||||||
|
|
||||||
import styles from "../header.module.css"
|
import styles from "../header.module.css"
|
||||||
|
|
||||||
export default async function HeaderFallback() {
|
export default async function HeaderFallback() {
|
||||||
const data = await getCurrentHeader(getLang())
|
|
||||||
|
|
||||||
if (!data?.header) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const homeHref = homeHrefs[env.NODE_ENV][getLang()]
|
|
||||||
const { frontpageLinkText, logo, menu, topMenu } = data.header
|
|
||||||
|
|
||||||
const topMenuMobileLinks = topMenu.links
|
|
||||||
.filter((link) => link.show_on_mobile)
|
|
||||||
.sort((a, b) => (a.sort_order_mobile < b.sort_order_mobile ? 1 : -1))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={styles.header} role="banner">
|
<header className={styles.header} role="banner">
|
||||||
<OfflineBanner />
|
<TopMenuSkeleton />
|
||||||
<TopMenu
|
<MainMenuSkeleton />
|
||||||
frontpageLinkText={frontpageLinkText}
|
|
||||||
homeHref={homeHref}
|
|
||||||
links={topMenu.links}
|
|
||||||
languageSwitcher={null}
|
|
||||||
/>
|
|
||||||
<MainMenu
|
|
||||||
frontpageLinkText={frontpageLinkText}
|
|
||||||
homeHref={homeHref}
|
|
||||||
links={menu.links}
|
|
||||||
logo={logo}
|
|
||||||
topMenuMobileLinks={topMenuMobileLinks}
|
|
||||||
languageSwitcher={null}
|
|
||||||
myPagesMobileDropdown={null}
|
|
||||||
bookingHref={homeHref}
|
|
||||||
user={null}
|
|
||||||
/>
|
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import useDropdownStore from "@/stores/main-menu"
|
|||||||
import Image from "@/components/Image"
|
import Image from "@/components/Image"
|
||||||
import LoginButton from "@/components/LoginButton"
|
import LoginButton from "@/components/LoginButton"
|
||||||
import Avatar from "@/components/MyPages/Avatar"
|
import Avatar from "@/components/MyPages/Avatar"
|
||||||
|
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
import { trackClick } from "@/utils/tracking"
|
import { trackClick } from "@/utils/tracking"
|
||||||
@@ -227,3 +228,56 @@ export function MainMenu({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function MainMenuSkeleton() {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
const links = new Array(5).fill("")
|
||||||
|
return (
|
||||||
|
<div className={styles.mainMenu}>
|
||||||
|
<div
|
||||||
|
className={styles.container}
|
||||||
|
itemScope
|
||||||
|
itemType="http://schema.org/Organization"
|
||||||
|
>
|
||||||
|
<meta itemProp="name" content="Scandic" />
|
||||||
|
<nav className={styles.navBar}>
|
||||||
|
<button
|
||||||
|
aria-pressed="false"
|
||||||
|
className={styles.expanderBtn}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span className={styles.iconBars}></span>
|
||||||
|
<span className={styles.hiddenAccessible}>Menu</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a className={styles.logoLink} href={""}>
|
||||||
|
<Image
|
||||||
|
alt="Scandic Hotels logo"
|
||||||
|
className={styles.logo}
|
||||||
|
data-js="scandiclogoimg"
|
||||||
|
itemProp="logo"
|
||||||
|
height={20}
|
||||||
|
src={"/_static/img/scandic-logotype.png"}
|
||||||
|
width={200}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<ul className={styles.listWrapper}>
|
||||||
|
{links.map((link, i) => (
|
||||||
|
<li
|
||||||
|
className={`${styles.li} ${styles.skeletonWrapper}`}
|
||||||
|
key={link.href + i}
|
||||||
|
>
|
||||||
|
<SkeletonShimmer height="22px" width="130px" />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className={styles.buttonContainer}>
|
||||||
|
<BookingButton href={""} />
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,10 +4,7 @@
|
|||||||
box-shadow: 0px 1.001px 1.001px 0px rgba(0, 0, 0, 0.05);
|
box-shadow: 0px 1.001px 1.001px 0px rgba(0, 0, 0, 0.05);
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: var(--header-z-index);
|
|
||||||
height: var(--current-mobile-site-header-height);
|
height: var(--current-mobile-site-header-height);
|
||||||
max-width: var(--max-width-navigation);
|
max-width: var(--max-width-navigation);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@@ -27,11 +24,9 @@
|
|||||||
.navBar {
|
.navBar {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 80px 1fr;
|
grid-template-columns: 1fr 80px 1fr;
|
||||||
grid-template-columns: auto auto 1fr auto;
|
|
||||||
grid-template-areas: "expanderBtn logoLink . buttonContainer";
|
grid-template-areas: "expanderBtn logoLink . buttonContainer";
|
||||||
grid-template-rows: 100%;
|
grid-template-rows: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0 var(--Spacing-x2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.expanderBtn {
|
.expanderBtn {
|
||||||
@@ -50,7 +45,7 @@
|
|||||||
background: #757575;
|
background: #757575;
|
||||||
border-radius: 2.3px;
|
border-radius: 2.3px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 3px;
|
height: 5px;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: 0.3s;
|
transition: 0.3s;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
@@ -107,7 +102,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 80px;
|
width: 80px;
|
||||||
padding-left: var(--Spacing-x1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
@@ -241,6 +235,12 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.skeletonWrapper {
|
||||||
|
padding: 4px 10px;
|
||||||
|
height: 100%;
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 1367px) {
|
@media (min-width: 1367px) {
|
||||||
.navBar {
|
.navBar {
|
||||||
grid-template-columns: 140px auto 1fr;
|
grid-template-columns: 140px auto 1fr;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { overview } from "@/constants/routes/myPages"
|
|||||||
import { getName } from "@/lib/trpc/memoizedRequests"
|
import { getName } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
import LoginButton from "@/components/LoginButton"
|
import LoginButton from "@/components/LoginButton"
|
||||||
|
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
import { getLang } from "@/i18n/serverContext"
|
||||||
@@ -68,6 +69,8 @@ export default async function TopMenu({
|
|||||||
position="hamburger menu"
|
position="hamburger menu"
|
||||||
trackingId="loginStartTopMenu"
|
trackingId="loginStartTopMenu"
|
||||||
className={`${styles.sessionLink} ${styles.loginLink}`}
|
className={`${styles.sessionLink} ${styles.loginLink}`}
|
||||||
|
variant="default"
|
||||||
|
size="small"
|
||||||
>
|
>
|
||||||
{intl.formatMessage({ id: "Log in" })}
|
{intl.formatMessage({ id: "Log in" })}
|
||||||
</LoginButton>
|
</LoginButton>
|
||||||
@@ -78,3 +81,32 @@ export default async function TopMenu({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function TopMenuSkeleton() {
|
||||||
|
const intl = await getIntl()
|
||||||
|
const links = new Array(5).fill("")
|
||||||
|
return (
|
||||||
|
<div className={styles.topMenu}>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<ul className={styles.list}>
|
||||||
|
{links.map((link, i) => (
|
||||||
|
<li key={link.href + i} className={styles.skeletonWrapper}>
|
||||||
|
<SkeletonShimmer width="100px" height="16px" />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li className={styles.sessionContainer}>
|
||||||
|
<LoginButton
|
||||||
|
position="hamburger menu"
|
||||||
|
trackingId="loginStartTopMenu"
|
||||||
|
className={`${styles.sessionLink} ${styles.loginLink}`}
|
||||||
|
variant="default"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "Log in" })}
|
||||||
|
</LoginButton>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -49,6 +49,12 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.skeletonWrapper {
|
||||||
|
padding: 4px 10px;
|
||||||
|
height: 30px;
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
.container {
|
.container {
|
||||||
padding: 0 30px;
|
padding: 0 30px;
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
.header {
|
.header {
|
||||||
display: grid;
|
display: grid;
|
||||||
background-color: var(--Main-Grey-White);
|
background-color: var(--Main-Grey-White);
|
||||||
|
position: relative;
|
||||||
|
z-index: var(--header-z-index);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 1366px) {
|
@media screen and (max-width: 950px) {
|
||||||
.header {
|
.header {
|
||||||
height: var(--current-mobile-site-header-height);
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: var(--header-z-index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,87 +35,75 @@ export default function DatePickerMobile({
|
|||||||
const endDate = dt().add(395, "day").toDate()
|
const endDate = dt().add(395, "day").toDate()
|
||||||
const endOfLastMonth = dt(endDate).endOf("month").toDate()
|
const endOfLastMonth = dt(endDate).endOf("month").toDate()
|
||||||
return (
|
return (
|
||||||
<DayPicker
|
<div className={styles.container}>
|
||||||
classNames={{
|
<header className={styles.header}>
|
||||||
...classNames,
|
<button className={styles.close} onClick={close} type="button">
|
||||||
caption_label: `${classNames.caption_label} ${styles.captionLabel}`,
|
<CloseLargeIcon />
|
||||||
day: `${classNames.day} ${styles.day}`,
|
</button>
|
||||||
day_button: `${classNames.day_button} ${styles.dayButton}`,
|
</header>
|
||||||
footer: styles.footer,
|
<DayPicker
|
||||||
month: styles.month,
|
classNames={{
|
||||||
month_caption: `${classNames.month_caption} ${styles.monthCaption}`,
|
...classNames,
|
||||||
months: styles.months,
|
caption_label: `${classNames.caption_label} ${styles.captionLabel}`,
|
||||||
range_end: styles.rangeEnd,
|
day: `${classNames.day} ${styles.day}`,
|
||||||
range_middle: styles.rangeMiddle,
|
day_button: `${classNames.day_button} ${styles.dayButton}`,
|
||||||
range_start: styles.rangeStart,
|
month: styles.month,
|
||||||
root: `${classNames.root} ${styles.container}`,
|
month_caption: `${classNames.month_caption} ${styles.monthCaption}`,
|
||||||
week: styles.week,
|
months: styles.months,
|
||||||
weekday: `${classNames.weekday} ${styles.weekDay}`,
|
range_end: styles.rangeEnd,
|
||||||
}}
|
range_middle: styles.rangeMiddle,
|
||||||
disabled={[
|
range_start: styles.rangeStart,
|
||||||
{ from: startOfCurrentMonth, to: yesterday },
|
root: `${classNames.root} ${styles.root}`,
|
||||||
{ from: endDate, to: endOfLastMonth },
|
week: styles.week,
|
||||||
]}
|
weekday: `${classNames.weekday} ${styles.weekDay}`,
|
||||||
endMonth={endDate}
|
}}
|
||||||
excludeDisabled
|
disabled={[
|
||||||
footer
|
{ from: startOfCurrentMonth, to: yesterday },
|
||||||
formatters={{
|
{ from: endDate, to: endOfLastMonth },
|
||||||
formatWeekdayName(weekday) {
|
]}
|
||||||
return dt(weekday).locale(lang).format("ddd")
|
endMonth={endDate}
|
||||||
},
|
excludeDisabled
|
||||||
}}
|
formatters={{
|
||||||
hideNavigation
|
formatWeekdayName(weekday) {
|
||||||
lang={lang}
|
return dt(weekday).locale(lang).format("ddd")
|
||||||
locale={locale}
|
},
|
||||||
mode="range"
|
}}
|
||||||
/** Showing full year or what's left of it */
|
hideNavigation
|
||||||
numberOfMonths={13}
|
lang={lang}
|
||||||
onDayClick={handleOnSelect}
|
locale={locale}
|
||||||
required
|
mode="range"
|
||||||
selected={selectedDate}
|
/** Showing full year or what's left of it */
|
||||||
startMonth={currentDate}
|
numberOfMonths={13}
|
||||||
weekStartsOn={1}
|
onDayClick={handleOnSelect}
|
||||||
components={{
|
required
|
||||||
Footer(props) {
|
selected={selectedDate}
|
||||||
return (
|
startMonth={currentDate}
|
||||||
<footer className={props.className}>
|
weekStartsOn={1}
|
||||||
<Button
|
components={{
|
||||||
className={styles.button}
|
MonthCaption(props) {
|
||||||
intent="tertiary"
|
return (
|
||||||
onPress={close}
|
<div className={props.className}>
|
||||||
size="large"
|
<Subtitle asChild type="two">
|
||||||
theme="base"
|
{props.children}
|
||||||
>
|
</Subtitle>
|
||||||
<Body color="white" textTransform="bold" asChild>
|
</div>
|
||||||
<span>{intl.formatMessage({ id: "Select dates" })}</span>
|
)
|
||||||
</Body>
|
},
|
||||||
</Button>
|
}}
|
||||||
<div className={styles.backdrop} />
|
/>
|
||||||
</footer>
|
<footer className={styles.footer}>
|
||||||
)
|
<Button
|
||||||
},
|
className={styles.button}
|
||||||
MonthCaption(props) {
|
intent="tertiary"
|
||||||
return (
|
onPress={close}
|
||||||
<div className={props.className}>
|
size="large"
|
||||||
<Subtitle asChild type="two">
|
theme="base"
|
||||||
{props.children}
|
>
|
||||||
</Subtitle>
|
<Body color="white" textTransform="bold" asChild>
|
||||||
</div>
|
<span>{intl.formatMessage({ id: "Select dates" })}</span>
|
||||||
)
|
</Body>
|
||||||
},
|
</Button>
|
||||||
Root({ children, ...props }) {
|
</footer>
|
||||||
return (
|
</div>
|
||||||
<div {...props}>
|
|
||||||
<header className={styles.header}>
|
|
||||||
<button className={styles.close} onClick={close} type="button">
|
|
||||||
<CloseLargeIcon />
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,11 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root {
|
||||||
|
display: grid;
|
||||||
|
grid-area: content;
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
background-color: var(--Main-Grey-White);
|
background-color: var(--Main-Grey-White);
|
||||||
@@ -37,7 +42,6 @@
|
|||||||
|
|
||||||
div.months {
|
div.months {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-area: content;
|
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
scroll-snap-type: y mandatory;
|
scroll-snap-type: y mandatory;
|
||||||
}
|
}
|
||||||
@@ -155,13 +159,13 @@ td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.weekDay {
|
.weekDay {
|
||||||
color: var(--Base-Text-Medium-contrast);
|
color: var(--UI-Text-Placeholder);
|
||||||
font-family: var(--typography-Footnote-Labels-fontFamily);
|
font-family: var(--typography-Caption-Labels-fontFamily);
|
||||||
font-size: var(--typography-Footnote-Labels-fontSize);
|
font-size: var(--typography-Caption-Labels-fontSize);
|
||||||
font-weight: var(--typography-Footnote-Labels-fontWeight);
|
font-weight: var(--typography-Caption-Labels-fontWeight);
|
||||||
letter-spacing: var(--typography-Footnote-Labels-letterSpacing);
|
letter-spacing: var(--typography-Caption-Labels-letterSpacing);
|
||||||
line-height: var(--typography-Footnote-Labels-lineHeight);
|
line-height: var(--typography-Caption-Labels-lineHeight);
|
||||||
text-decoration: var(--typography-Footnote-Labels-textDecoration);
|
text-decoration: var(--typography-Caption-Labels-textDecoration);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ function Logo({ alt }: { alt: string }) {
|
|||||||
className={styles.logo}
|
className={styles.logo}
|
||||||
height={22}
|
height={22}
|
||||||
src="/_static/img/scandic-logotype.svg"
|
src="/_static/img/scandic-logotype.svg"
|
||||||
|
priority
|
||||||
width={103}
|
width={103}
|
||||||
/>
|
/>
|
||||||
</NextLink>
|
</NextLink>
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"use client"
|
||||||
|
import { createEvent } from "ics"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { dt } from "@/lib/dt"
|
||||||
|
|
||||||
|
import { CalendarAddIcon } from "@/components/Icons"
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
import type { AddToCalendarProps } from "@/types/components/hotelReservation/bookingConfirmation/actions/addToCalendar"
|
||||||
|
|
||||||
|
export default function AddToCalendar({
|
||||||
|
checkInDate,
|
||||||
|
event,
|
||||||
|
hotelName,
|
||||||
|
}: AddToCalendarProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const lang = useLang()
|
||||||
|
|
||||||
|
async function downloadBooking() {
|
||||||
|
const d = dt(checkInDate).locale(lang).format("YYYY-MM-DD")
|
||||||
|
const filename = `${hotelName.toLowerCase().split(" ").join("_")}-${d}.ics`
|
||||||
|
|
||||||
|
const file: Blob = await new Promise((resolve, reject) => {
|
||||||
|
createEvent(event, (error, value) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(new File([value], filename, { type: "text/calendar" }))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(file)
|
||||||
|
|
||||||
|
const anchor = document.createElement("a")
|
||||||
|
anchor.href = url
|
||||||
|
anchor.download = filename
|
||||||
|
|
||||||
|
document.body.appendChild(anchor)
|
||||||
|
anchor.click()
|
||||||
|
document.body.removeChild(anchor)
|
||||||
|
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
intent="text"
|
||||||
|
onPress={downloadBooking}
|
||||||
|
size="small"
|
||||||
|
theme="base"
|
||||||
|
variant="icon"
|
||||||
|
wrapping
|
||||||
|
>
|
||||||
|
<CalendarAddIcon />
|
||||||
|
{intl.formatMessage({ id: "Add to calendar" })}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
import { useReactToPrint } from "react-to-print"
|
||||||
|
|
||||||
|
import { DownloadIcon } from "@/components/Icons"
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
|
||||||
|
import type { DownloadInvoiceProps } from "@/types/components/hotelReservation/bookingConfirmation/actions/downloadInvoice"
|
||||||
|
|
||||||
|
export default function DownloadInvoice({ mainRef }: DownloadInvoiceProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const reactToPrintFn = useReactToPrint({ contentRef: mainRef })
|
||||||
|
|
||||||
|
function downloadBooking() {
|
||||||
|
reactToPrintFn()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
intent="text"
|
||||||
|
onPress={downloadBooking}
|
||||||
|
size="small"
|
||||||
|
theme="base"
|
||||||
|
variant="icon"
|
||||||
|
wrapping
|
||||||
|
>
|
||||||
|
<DownloadIcon />
|
||||||
|
{intl.formatMessage({ id: "Download invoice" })}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { EditIcon } from "@/components/Icons"
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
|
||||||
|
export default function ManageBooking() {
|
||||||
|
const intl = useIntl()
|
||||||
|
return (
|
||||||
|
<Button intent="text" size="small" theme="base" variant="icon" wrapping>
|
||||||
|
<EditIcon />
|
||||||
|
{intl.formatMessage({ id: "Manage booking" })}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
.actions {
|
|
||||||
border-radius: var(--Corner-radius-Medium);
|
|
||||||
display: grid;
|
|
||||||
grid-area: actions;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
|
||||||
.actions {
|
|
||||||
gap: var(--Spacing-x3);
|
|
||||||
grid-auto-columns: auto;
|
|
||||||
grid-auto-flow: column;
|
|
||||||
grid-template-columns: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { dt } from "@/lib/dt"
|
||||||
|
|
||||||
|
import type { DateTime } from "ics"
|
||||||
|
|
||||||
|
export function generateDateTime(d: Date): DateTime {
|
||||||
|
const _d = dt(d).utc()
|
||||||
|
return [
|
||||||
|
_d.year(),
|
||||||
|
// Need to add +1 since month is 0 based
|
||||||
|
_d.month() + 1,
|
||||||
|
_d.date(),
|
||||||
|
_d.hour(),
|
||||||
|
_d.minute(),
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { CalendarAddIcon, DownloadIcon, EditIcon } from "@/components/Icons"
|
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
|
||||||
import { getIntl } from "@/i18n"
|
|
||||||
|
|
||||||
import styles from "./actions.module.css"
|
|
||||||
|
|
||||||
export default async function Actions() {
|
|
||||||
const intl = await getIntl()
|
|
||||||
return (
|
|
||||||
<div className={styles.actions}>
|
|
||||||
<Button intent="text" size="small" theme="base" variant="icon" wrapping>
|
|
||||||
<CalendarAddIcon />
|
|
||||||
{intl.formatMessage({ id: "Add to calendar" })}
|
|
||||||
</Button>
|
|
||||||
<Button intent="text" size="small" theme="base" variant="icon" wrapping>
|
|
||||||
<EditIcon />
|
|
||||||
{intl.formatMessage({ id: "Manage booking" })}
|
|
||||||
</Button>
|
|
||||||
<Button intent="text" size="small" theme="base" variant="icon" wrapping>
|
|
||||||
<DownloadIcon />
|
|
||||||
{intl.formatMessage({ id: "Download invoice" })}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -17,6 +17,22 @@
|
|||||||
max-width: 720px;
|
max-width: 720px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
border-radius: var(--Corner-radius-Medium);
|
||||||
|
display: grid;
|
||||||
|
grid-area: actions;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.actions {
|
||||||
|
gap: var(--Spacing-x3);
|
||||||
|
grid-auto-columns: auto;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
grid-template-columns: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1367px) {
|
@media screen and (min-width: 1367px) {
|
||||||
.header {
|
.header {
|
||||||
padding-bottom: var(--Spacing-x4);
|
padding-bottom: var(--Spacing-x4);
|
||||||
|
|||||||
@@ -1,21 +1,27 @@
|
|||||||
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||||
import { getIntl } from "@/i18n"
|
|
||||||
|
|
||||||
import Actions from "./Actions"
|
import AddToCalendar from "./Actions/AddToCalendar"
|
||||||
|
import DownloadInvoice from "./Actions/DownloadInvoice"
|
||||||
|
import { generateDateTime } from "./Actions/helpers"
|
||||||
|
import ManageBooking from "./Actions/ManageBooking"
|
||||||
|
|
||||||
import styles from "./header.module.css"
|
import styles from "./header.module.css"
|
||||||
|
|
||||||
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
import type { EventAttributes } from "ics"
|
||||||
|
|
||||||
export default async function Header({
|
import type { BookingConfirmationHeaderProps } from "@/types/components/hotelReservation/bookingConfirmation/header"
|
||||||
confirmationNumber,
|
|
||||||
}: BookingConfirmationProps) {
|
export default function Header({
|
||||||
const intl = await getIntl()
|
booking,
|
||||||
const { hotel } = await getBookingConfirmation(confirmationNumber)
|
hotel,
|
||||||
|
mainRef,
|
||||||
|
}: BookingConfirmationHeaderProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
const text = intl.formatMessage<React.ReactNode>(
|
const text = intl.formatMessage<React.ReactNode>(
|
||||||
{ id: "booking.confirmation.text" },
|
{ id: "booking.confirmation.text" },
|
||||||
@@ -28,6 +34,25 @@ export default async function Header({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const event: EventAttributes = {
|
||||||
|
busyStatus: "FREE",
|
||||||
|
categories: ["booking", "hotel", "stay"],
|
||||||
|
created: generateDateTime(booking.createDateTime),
|
||||||
|
description: hotel.hotelContent.texts.descriptions.medium,
|
||||||
|
end: generateDateTime(booking.checkOutDate),
|
||||||
|
endInputType: "utc",
|
||||||
|
geo: {
|
||||||
|
lat: hotel.location.latitude,
|
||||||
|
lon: hotel.location.longitude,
|
||||||
|
},
|
||||||
|
location: `${hotel.address.streetAddress}, ${hotel.address.zipCode} ${hotel.address.city} ${hotel.address.country}`,
|
||||||
|
start: generateDateTime(booking.checkInDate),
|
||||||
|
startInputType: "utc",
|
||||||
|
status: "CONFIRMED",
|
||||||
|
title: hotel.name,
|
||||||
|
url: hotel.contactInformation.websiteUrl,
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<hgroup className={styles.hgroup}>
|
<hgroup className={styles.hgroup}>
|
||||||
@@ -39,7 +64,15 @@ export default async function Header({
|
|||||||
</Title>
|
</Title>
|
||||||
</hgroup>
|
</hgroup>
|
||||||
<Body className={styles.body}>{text}</Body>
|
<Body className={styles.body}>{text}</Body>
|
||||||
<Actions />
|
<div className={styles.actions}>
|
||||||
|
<AddToCalendar
|
||||||
|
checkInDate={booking.checkInDate}
|
||||||
|
event={event}
|
||||||
|
hotelName={hotel.name}
|
||||||
|
/>
|
||||||
|
<ManageBooking />
|
||||||
|
<DownloadInvoice mainRef={mainRef} />
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import { Toast } from "@/components/TempDesignSystem/Toasts"
|
import { Toast } from "@/components/TempDesignSystem/Toasts"
|
||||||
import { getIntl } from "@/i18n"
|
|
||||||
|
|
||||||
import styles from "./hotelDetails.module.css"
|
import styles from "./hotelDetails.module.css"
|
||||||
|
|
||||||
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
import type { BookingConfirmationHotelDetailsProps } from "@/types/components/hotelReservation/bookingConfirmation/hotelDetails"
|
||||||
|
|
||||||
export default async function HotelDetails({
|
export default function HotelDetails({
|
||||||
confirmationNumber,
|
hotel,
|
||||||
}: BookingConfirmationProps) {
|
}: BookingConfirmationHotelDetailsProps) {
|
||||||
const intl = await getIntl()
|
const intl = useIntl()
|
||||||
const { hotel } = await getBookingConfirmation(confirmationNumber)
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.details}>
|
<div className={styles.details}>
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
|
|
||||||
|
|
||||||
import { CreditCardAddIcon } from "@/components/Icons"
|
import { CreditCardAddIcon } from "@/components/Icons"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import { getIntl } from "@/i18n"
|
import useLang from "@/hooks/useLang"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
|
||||||
|
|
||||||
import styles from "./paymentDetails.module.css"
|
import styles from "./paymentDetails.module.css"
|
||||||
|
|
||||||
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
import type { BookingConfirmationPaymentDetailsProps } from "@/types/components/hotelReservation/bookingConfirmation/paymentDetails"
|
||||||
|
|
||||||
export default async function PaymentDetails({
|
export default function PaymentDetails({
|
||||||
confirmationNumber,
|
booking,
|
||||||
}: BookingConfirmationProps) {
|
}: BookingConfirmationPaymentDetailsProps) {
|
||||||
const intl = await getIntl()
|
const intl = useIntl()
|
||||||
const lang = getLang()
|
const lang = useLang()
|
||||||
const { booking } = await getBookingConfirmation(confirmationNumber)
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.details}>
|
<div className={styles.details}>
|
||||||
<Subtitle color="uiTextHighContrast" type="two">
|
<Subtitle color="uiTextHighContrast" type="two">
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { getIntl } from "@/i18n"
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import Promo from "./Promo"
|
import Promo from "./Promo"
|
||||||
|
|
||||||
import styles from "./promos.module.css"
|
import styles from "./promos.module.css"
|
||||||
|
|
||||||
export default async function Promos() {
|
export default function Promos() {
|
||||||
const intl = await getIntl()
|
const intl = useIntl()
|
||||||
return (
|
return (
|
||||||
<div className={styles.promos}>
|
<div className={styles.promos}>
|
||||||
<Promo
|
<Promo
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
"use client"
|
||||||
import { notFound } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
|
|
||||||
|
|
||||||
import { ChevronRightSmallIcon, InfoCircleIcon } from "@/components/Icons"
|
import { ChevronRightSmallIcon, InfoCircleIcon } from "@/components/Icons"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
@@ -9,21 +9,20 @@ import Link from "@/components/TempDesignSystem/Link"
|
|||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import { getIntl } from "@/i18n"
|
|
||||||
import { getBookedHotelRoom } from "@/utils/getBookedHotelRoom"
|
|
||||||
|
|
||||||
import styles from "./receipt.module.css"
|
import styles from "./receipt.module.css"
|
||||||
|
|
||||||
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
import { BookingConfirmationReceiptProps } from "@/types/components/hotelReservation/bookingConfirmation/receipt"
|
||||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||||
|
|
||||||
export default async function Receipt({
|
export default function Receipt({
|
||||||
confirmationNumber,
|
booking,
|
||||||
}: BookingConfirmationProps) {
|
hotel,
|
||||||
const intl = await getIntl()
|
room,
|
||||||
const { booking, hotel } = await getBookingConfirmation(confirmationNumber)
|
}: BookingConfirmationReceiptProps) {
|
||||||
const roomAndBed = getBookedHotelRoom(hotel, booking.roomTypeCode ?? "")
|
const intl = useIntl()
|
||||||
if (!roomAndBed) {
|
|
||||||
|
if (!room) {
|
||||||
return notFound()
|
return notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +37,7 @@ export default async function Receipt({
|
|||||||
<Subtitle type="two">{intl.formatMessage({ id: "Summary" })}</Subtitle>
|
<Subtitle type="two">{intl.formatMessage({ id: "Summary" })}</Subtitle>
|
||||||
<article className={styles.room}>
|
<article className={styles.room}>
|
||||||
<header className={styles.roomHeader}>
|
<header className={styles.roomHeader}>
|
||||||
<Body color="uiTextHighContrast">{roomAndBed.name}</Body>
|
<Body color="uiTextHighContrast">{room.name}</Body>
|
||||||
{booking.rateDefinition.isMemberRate ? (
|
{booking.rateDefinition.isMemberRate ? (
|
||||||
<div className={styles.memberPrice}>
|
<div className={styles.memberPrice}>
|
||||||
<Body color="uiTextPlaceholder">
|
<Body color="uiTextPlaceholder">
|
||||||
@@ -82,9 +81,7 @@ export default async function Receipt({
|
|||||||
</Link>
|
</Link>
|
||||||
</header>
|
</header>
|
||||||
<div className={styles.entry}>
|
<div className={styles.entry}>
|
||||||
<Body color="uiTextHighContrast">
|
<Body color="uiTextHighContrast">{room.bedType.description}</Body>
|
||||||
{roomAndBed.bedType.description}
|
|
||||||
</Body>
|
|
||||||
<Body color="uiTextHighContrast">
|
<Body color="uiTextHighContrast">
|
||||||
{intl.formatNumber(0, {
|
{intl.formatNumber(0, {
|
||||||
currency: booking.currencyCode,
|
currency: booking.currencyCode,
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -10,16 +13,15 @@ import Link from "@/components/TempDesignSystem/Link"
|
|||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import { getIntl } from "@/i18n"
|
import useLang from "@/hooks/useLang"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
|
||||||
|
|
||||||
import styles from "./room.module.css"
|
import styles from "./room.module.css"
|
||||||
|
|
||||||
import type { RoomProps } from "@/types/components/hotelReservation/bookingConfirmation/room"
|
import type { RoomProps } from "@/types/components/hotelReservation/bookingConfirmation/rooms/room"
|
||||||
|
|
||||||
export default async function Room({ booking, img, roomName }: RoomProps) {
|
export default function Room({ booking, img, roomName }: RoomProps) {
|
||||||
const intl = await getIntl()
|
const intl = useIntl()
|
||||||
const lang = getLang()
|
const lang = useLang()
|
||||||
|
|
||||||
const fromDate = dt(booking.checkInDate).locale(lang)
|
const fromDate = dt(booking.checkInDate).locale(lang)
|
||||||
const toDate = dt(booking.checkOutDate).locale(lang)
|
const toDate = dt(booking.checkOutDate).locale(lang)
|
||||||
|
|||||||
@@ -1,30 +1,23 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import { notFound } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
|
|
||||||
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
|
|
||||||
|
|
||||||
import { getBookedHotelRoom } from "@/utils/getBookedHotelRoom"
|
|
||||||
|
|
||||||
import Room from "./Room"
|
import Room from "./Room"
|
||||||
|
|
||||||
import styles from "./rooms.module.css"
|
import styles from "./rooms.module.css"
|
||||||
|
|
||||||
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
import type { BookingConfirmationRoomsProps } from "@/types/components/hotelReservation/bookingConfirmation/rooms"
|
||||||
|
|
||||||
export default async function Rooms({
|
export default function Rooms({
|
||||||
confirmationNumber,
|
booking,
|
||||||
}: BookingConfirmationProps) {
|
room,
|
||||||
const { booking, hotel } = await getBookingConfirmation(confirmationNumber)
|
}: BookingConfirmationRoomsProps) {
|
||||||
const roomAndBed = getBookedHotelRoom(hotel, booking.roomTypeCode ?? "")
|
if (!room) {
|
||||||
if (!roomAndBed) {
|
|
||||||
return notFound()
|
return notFound()
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<section className={styles.rooms}>
|
<section className={styles.rooms}>
|
||||||
<Room
|
<Room booking={booking} img={room.images[0]} roomName={room.name} />
|
||||||
booking={booking}
|
|
||||||
img={roomAndBed.images[0]}
|
|
||||||
roomName={roomAndBed.name}
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
57
components/HotelReservation/BookingConfirmation/index.tsx
Normal file
57
components/HotelReservation/BookingConfirmation/index.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
"use client"
|
||||||
|
import { use, useRef } from "react"
|
||||||
|
|
||||||
|
import Header from "@/components/HotelReservation/BookingConfirmation/Header"
|
||||||
|
import HotelDetails from "@/components/HotelReservation/BookingConfirmation/HotelDetails"
|
||||||
|
import PaymentDetails from "@/components/HotelReservation/BookingConfirmation/PaymentDetails"
|
||||||
|
import Promos from "@/components/HotelReservation/BookingConfirmation/Promos"
|
||||||
|
import Receipt from "@/components/HotelReservation/BookingConfirmation/Receipt"
|
||||||
|
import Rooms from "@/components/HotelReservation/BookingConfirmation/Rooms"
|
||||||
|
import SidePanel from "@/components/HotelReservation/SidePanel"
|
||||||
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
|
||||||
|
import styles from "./confirmation.module.css"
|
||||||
|
|
||||||
|
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||||
|
|
||||||
|
export default function BookingConfirmation({
|
||||||
|
bookingConfirmationPromise,
|
||||||
|
}: BookingConfirmationProps) {
|
||||||
|
const bookingConfirmation = use(bookingConfirmationPromise)
|
||||||
|
const mainRef = useRef<HTMLElement | null>(null)
|
||||||
|
return (
|
||||||
|
<main className={styles.main} ref={mainRef}>
|
||||||
|
<Header
|
||||||
|
booking={bookingConfirmation.booking}
|
||||||
|
hotel={bookingConfirmation.hotel}
|
||||||
|
mainRef={mainRef}
|
||||||
|
/>
|
||||||
|
<div className={styles.booking}>
|
||||||
|
<Rooms
|
||||||
|
booking={bookingConfirmation.booking}
|
||||||
|
room={bookingConfirmation.room}
|
||||||
|
/>
|
||||||
|
<PaymentDetails booking={bookingConfirmation.booking} />
|
||||||
|
<Divider color="primaryLightSubtle" />
|
||||||
|
<HotelDetails hotel={bookingConfirmation.hotel} />
|
||||||
|
<Promos />
|
||||||
|
<div className={styles.mobileReceipt}>
|
||||||
|
<Receipt
|
||||||
|
booking={bookingConfirmation.booking}
|
||||||
|
hotel={bookingConfirmation.hotel}
|
||||||
|
room={bookingConfirmation.room}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<aside className={styles.aside}>
|
||||||
|
<SidePanel variant="receipt">
|
||||||
|
<Receipt
|
||||||
|
booking={bookingConfirmation.booking}
|
||||||
|
hotel={bookingConfirmation.hotel}
|
||||||
|
room={bookingConfirmation.room}
|
||||||
|
/>
|
||||||
|
</SidePanel>
|
||||||
|
</aside>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
grid-template-rows: auto;
|
grid-template-rows: auto;
|
||||||
gap: var(--Spacing-x2);
|
gap: var(--Spacing-x2);
|
||||||
font-family: var(--typography-Body-Regular-fontFamily);
|
font-family: var(--typography-Body-Regular-fontFamily);
|
||||||
|
margin-bottom: var(--Spacing-x3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.address,
|
.address,
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.soMeIcons {
|
.soMeIcons {
|
||||||
@@ -28,6 +30,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ecoLabel {
|
.ecoLabel {
|
||||||
|
width: 38px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecoLabel img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
grid-row: 4 / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecoContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
column-gap: var(--Spacing-x-one-and-half);
|
column-gap: var(--Spacing-x-one-and-half);
|
||||||
@@ -38,10 +53,6 @@
|
|||||||
margin-bottom: var(--Spacing-x1);
|
margin-bottom: var(--Spacing-x1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ecoLabel img {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ecoLabelText {
|
.ecoLabelText {
|
||||||
display: flex;
|
display: flex;
|
||||||
color: var(--UI-Text-Medium-contrast);
|
color: var(--UI-Text-Medium-contrast);
|
||||||
@@ -49,8 +60,8 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.googleMaps {
|
.link {
|
||||||
text-decoration: none;
|
text-decoration: underline;
|
||||||
font-family: var(--typography-Body-Regular-fontFamily);
|
font-family: var(--typography-Body-Regular-fontFamily);
|
||||||
color: var(--Base-Text-Medium-contrast);
|
color: var(--Base-Text-High-contrast);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,31 +24,27 @@ export default function Contact({ hotel }: ContactProps) {
|
|||||||
<Body textTransform="bold">
|
<Body textTransform="bold">
|
||||||
{intl.formatMessage({ id: "Address" })}
|
{intl.formatMessage({ id: "Address" })}
|
||||||
</Body>
|
</Body>
|
||||||
<Body>
|
<Body>{`${hotel.address.streetAddress}, `}</Body>
|
||||||
{`${hotel.address.streetAddress}, ${hotel.address.city}`}
|
<Body>{hotel.address.city}</Body>
|
||||||
</Body>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Body textTransform="bold">
|
<Body textTransform="bold">
|
||||||
{intl.formatMessage({ id: "Driving directions" })}
|
{intl.formatMessage({ id: "Driving directions" })}
|
||||||
</Body>
|
</Body>
|
||||||
<a
|
<Link
|
||||||
href={`https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}`}
|
href={`https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}`}
|
||||||
className={styles.googleMaps}
|
|
||||||
target="_blank"
|
|
||||||
>
|
>
|
||||||
Google Maps
|
<span className={styles.link}>Google Maps</span>
|
||||||
</a>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Body textTransform="bold">
|
<Body textTransform="bold">
|
||||||
{intl.formatMessage({ id: "Contact us" })}
|
{intl.formatMessage({ id: "Contact us" })}
|
||||||
</Body>
|
</Body>
|
||||||
<Link
|
<Link href={`tel:${hotel.contactInformation.phoneNumber}`}>
|
||||||
href={`tel:${hotel.contactInformation.phoneNumber}`}
|
<span className={styles.link}>
|
||||||
color="peach80"
|
{hotel.contactInformation.phoneNumber}
|
||||||
>
|
</span>
|
||||||
{hotel.contactInformation.phoneNumber}
|
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@@ -76,23 +72,24 @@ export default function Contact({ hotel }: ContactProps) {
|
|||||||
<Body textTransform="bold">
|
<Body textTransform="bold">
|
||||||
{intl.formatMessage({ id: "Email" })}
|
{intl.formatMessage({ id: "Email" })}
|
||||||
</Body>
|
</Body>
|
||||||
<Link
|
<Link href={`mailto:${hotel.contactInformation.email}`}>
|
||||||
href={`mailto:${hotel.contactInformation.email}`}
|
<span className={styles.link}>
|
||||||
color="peach80"
|
{hotel.contactInformation.email}
|
||||||
>
|
</span>
|
||||||
{hotel.contactInformation.email}
|
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</address>
|
</address>
|
||||||
{hotel.hotelFacts.ecoLabels?.nordicEcoLabel ? (
|
{hotel.hotelFacts.ecoLabels?.nordicEcoLabel ? (
|
||||||
<div className={styles.ecoLabel}>
|
<div className={styles.ecoContainer}>
|
||||||
<Image
|
<div className={styles.ecoLabel}>
|
||||||
height={38}
|
<Image
|
||||||
width={43}
|
height={38}
|
||||||
alt={intl.formatMessage({ id: "Nordic Swan Ecolabel" })}
|
width={38}
|
||||||
src={`/_static/img/icons/swan-eco/swan_eco_dark_${lang}.png`}
|
alt={intl.formatMessage({ id: "Nordic Swan Ecolabel" })}
|
||||||
/>
|
src={`/_static/img/icons/swan-eco/swan_eco_dark_${lang}.png`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className={styles.ecoLabelText}>
|
<div className={styles.ecoLabelText}>
|
||||||
<span>{intl.formatMessage({ id: "Nordic Swan Ecolabel" })}</span>
|
<span>{intl.formatMessage({ id: "Nordic Swan Ecolabel" })}</span>
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ export default function PaymentClient({
|
|||||||
const paymentRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}/${lang}/hotelreservation/payment-callback`
|
const paymentRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}/${lang}/hotelreservation/payment-callback`
|
||||||
|
|
||||||
initiateBooking.mutate({
|
initiateBooking.mutate({
|
||||||
|
language: lang,
|
||||||
hotelId: hotel,
|
hotelId: hotel,
|
||||||
checkInDate: fromDate,
|
checkInDate: fromDate,
|
||||||
checkOutDate: toDate,
|
checkOutDate: toDate,
|
||||||
|
|||||||
@@ -227,11 +227,11 @@ export default function SummaryUI({
|
|||||||
style: "currency",
|
style: "currency",
|
||||||
})}
|
})}
|
||||||
</Body>
|
</Body>
|
||||||
{totalPrice.euro && (
|
{totalPrice.requested && (
|
||||||
<Caption color="uiTextMediumContrast">
|
<Caption color="uiTextMediumContrast">
|
||||||
{intl.formatMessage({ id: "Approx." })}{" "}
|
{intl.formatMessage({ id: "Approx." })}{" "}
|
||||||
{intl.formatNumber(totalPrice.euro.price, {
|
{intl.formatNumber(totalPrice.requested.price, {
|
||||||
currency: CurrencyEnum.EUR,
|
currency: totalPrice.requested.currency,
|
||||||
style: "currency",
|
style: "currency",
|
||||||
})}
|
})}
|
||||||
</Caption>
|
</Caption>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useCallback, useEffect, useMemo } from "react"
|
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||||
import { FormProvider, useForm } from "react-hook-form"
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
import { useMediaQuery } from "usehooks-ts"
|
import { useMediaQuery } from "usehooks-ts"
|
||||||
@@ -27,7 +27,8 @@ export default function RoomFilter({
|
|||||||
onFilter,
|
onFilter,
|
||||||
filterOptions,
|
filterOptions,
|
||||||
}: RoomFilterProps) {
|
}: RoomFilterProps) {
|
||||||
const isAboveMobile = useMediaQuery("(min-width: 768px)")
|
const isTabletAndUp = useMediaQuery("(min-width: 768px)")
|
||||||
|
const [isAboveMobile, setIsAboveMobile] = useState(false)
|
||||||
|
|
||||||
const initialFilterValues = useMemo(
|
const initialFilterValues = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -71,6 +72,10 @@ export default function RoomFilter({
|
|||||||
return () => subscription.unsubscribe()
|
return () => subscription.unsubscribe()
|
||||||
}, [handleSubmit, watch, submitFilter])
|
}, [handleSubmit, watch, submitFilter])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsAboveMobile(isTabletAndUp)
|
||||||
|
}, [isTabletAndUp])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.infoDesktop}>
|
<div className={styles.infoDesktop}>
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ export default function FlexibilityOption({
|
|||||||
name,
|
name,
|
||||||
paymentTerm,
|
paymentTerm,
|
||||||
priceInformation,
|
priceInformation,
|
||||||
roomType,
|
|
||||||
roomTypeCode,
|
roomTypeCode,
|
||||||
features,
|
|
||||||
petRoomPackage,
|
petRoomPackage,
|
||||||
handleSelectRate,
|
handleSelectRate,
|
||||||
}: FlexibilityOptionProps) {
|
}: FlexibilityOptionProps) {
|
||||||
@@ -45,10 +43,22 @@ export default function FlexibilityOption({
|
|||||||
|
|
||||||
const { public: publicPrice, member: memberPrice } = product.productType
|
const { public: publicPrice, member: memberPrice } = product.productType
|
||||||
|
|
||||||
function onChange() {
|
const onClick: React.MouseEventHandler<HTMLInputElement> = (e) => {
|
||||||
handleSelectRate({
|
handleSelectRate((prev) => {
|
||||||
publicRateCode: publicPrice.rateCode,
|
if (
|
||||||
roomTypeCode: roomTypeCode,
|
prev &&
|
||||||
|
prev.publicRateCode === publicPrice.rateCode &&
|
||||||
|
prev.roomTypeCode === roomTypeCode
|
||||||
|
) {
|
||||||
|
if (e.currentTarget?.checked) e.currentTarget.checked = false
|
||||||
|
return undefined
|
||||||
|
} else
|
||||||
|
return {
|
||||||
|
publicRateCode: publicPrice.rateCode,
|
||||||
|
roomTypeCode: roomTypeCode,
|
||||||
|
name: name,
|
||||||
|
paymentTerm: paymentTerm,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +68,7 @@ export default function FlexibilityOption({
|
|||||||
type="radio"
|
type="radio"
|
||||||
name="rateCode"
|
name="rateCode"
|
||||||
value={publicPrice?.rateCode}
|
value={publicPrice?.rateCode}
|
||||||
onChange={onChange}
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
<div className={styles.card}>
|
<div className={styles.card}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export default function RateSummary({
|
|||||||
features,
|
features,
|
||||||
roomType,
|
roomType,
|
||||||
priceName,
|
priceName,
|
||||||
|
priceTerm,
|
||||||
} = rateSummary
|
} = rateSummary
|
||||||
const priceToShow = isUserLoggedIn && member ? member : publicRate
|
const priceToShow = isUserLoggedIn && member ? member : publicRate
|
||||||
|
|
||||||
@@ -80,87 +81,93 @@ export default function RateSummary({
|
|||||||
</Footnote>
|
</Footnote>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles.summaryText}>
|
<div className={styles.content}>
|
||||||
<Subtitle color="uiTextHighContrast">{roomType}</Subtitle>
|
<div className={styles.summaryText}>
|
||||||
<Body color="uiTextMediumContrast">{priceName}</Body>
|
<Subtitle color="uiTextHighContrast">{roomType}</Subtitle>
|
||||||
</div>
|
<Body color="uiTextMediumContrast">{`${priceName}, ${priceTerm}`}</Body>
|
||||||
<div className={styles.summaryPriceContainer}>
|
|
||||||
{showMemberDiscountBanner && (
|
|
||||||
<div className={styles.memberDiscountBannerDesktop}>
|
|
||||||
<Footnote color="burgundy">
|
|
||||||
{intl.formatMessage<React.ReactNode>(
|
|
||||||
{
|
|
||||||
id: "To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
span: (str) => (
|
|
||||||
<Caption color="red" type="bold" asChild>
|
|
||||||
<span>{str}</span>
|
|
||||||
</Caption>
|
|
||||||
),
|
|
||||||
amount: member.localPrice.pricePerStay,
|
|
||||||
currency: member.localPrice.currency,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</Footnote>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={styles.summaryPriceTextDesktop}>
|
|
||||||
<Body>
|
|
||||||
{intl.formatMessage<React.ReactNode>(
|
|
||||||
{ id: "<b>Total price</b> (incl VAT)" },
|
|
||||||
{ b: (str) => <b>{str}</b> }
|
|
||||||
)}
|
|
||||||
</Body>
|
|
||||||
<Caption color="uiTextMediumContrast">{summaryPriceTex}</Caption>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.summaryPrice}>
|
<div className={styles.summaryPriceContainer}>
|
||||||
<div className={styles.summaryPriceTextDesktop}>
|
{showMemberDiscountBanner && (
|
||||||
<Subtitle
|
<div className={styles.memberDiscountBannerDesktop}>
|
||||||
color={isUserLoggedIn ? "red" : "uiTextHighContrast"}
|
<Footnote color="burgundy">
|
||||||
textAlign="right"
|
{intl.formatMessage<React.ReactNode>(
|
||||||
>
|
{
|
||||||
{priceToShow?.localPrice.pricePerStay}{" "}
|
id: "To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.",
|
||||||
{priceToShow?.localPrice.currency}
|
},
|
||||||
</Subtitle>
|
{
|
||||||
<Body color="uiTextMediumContrast">
|
span: (str) => (
|
||||||
{intl.formatMessage({ id: "Approx." })}{" "}
|
<Caption color="red" type="bold" asChild>
|
||||||
{priceToShow?.requestedPrice?.pricePerStay}{" "}
|
<span>{str}</span>
|
||||||
{priceToShow?.requestedPrice?.currency}
|
</Caption>
|
||||||
</Body>
|
),
|
||||||
</div>
|
amount: member.localPrice.pricePerStay,
|
||||||
<div className={styles.summaryPriceTextMobile}>
|
currency: member.localPrice.currency,
|
||||||
<Caption color="uiTextHighContrast">
|
}
|
||||||
{intl.formatMessage({ id: "Total price" })}
|
)}
|
||||||
</Caption>
|
</Footnote>
|
||||||
<Subtitle color={isUserLoggedIn ? "red" : "uiTextHighContrast"}>
|
|
||||||
{priceToShow?.localPrice.pricePerStay}{" "}
|
|
||||||
{priceToShow?.localPrice.currency}
|
|
||||||
</Subtitle>
|
|
||||||
<Footnote
|
|
||||||
color="uiTextMediumContrast"
|
|
||||||
className={styles.summaryPriceTextMobile}
|
|
||||||
>
|
|
||||||
{summaryPriceTex}
|
|
||||||
</Footnote>
|
|
||||||
</div>
|
|
||||||
{isPetRoomSelected && (
|
|
||||||
<div className={styles.petInfo}>
|
|
||||||
<Body
|
|
||||||
color="uiTextHighContrast"
|
|
||||||
textTransform="bold"
|
|
||||||
textAlign="right"
|
|
||||||
>
|
|
||||||
+ {petRoomPrice} {petRoomCurrency}
|
|
||||||
</Body>
|
|
||||||
<Body color="uiTextMediumContrast" textAlign="right">
|
|
||||||
{intl.formatMessage({ id: "Pet charge" })}
|
|
||||||
</Body>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Button type="submit" theme="base" className={styles.continueButton}>
|
<div className={styles.summaryPriceTextDesktop}>
|
||||||
{intl.formatMessage({ id: "Continue" })}
|
<Body>
|
||||||
</Button>
|
{intl.formatMessage<React.ReactNode>(
|
||||||
|
{ id: "<b>Total price</b> (incl VAT)" },
|
||||||
|
{ b: (str) => <b>{str}</b> }
|
||||||
|
)}
|
||||||
|
</Body>
|
||||||
|
<Caption color="uiTextMediumContrast">{summaryPriceTex}</Caption>
|
||||||
|
</div>
|
||||||
|
<div className={styles.summaryPrice}>
|
||||||
|
<div className={styles.summaryPriceTextDesktop}>
|
||||||
|
<Subtitle
|
||||||
|
color={isUserLoggedIn ? "red" : "uiTextHighContrast"}
|
||||||
|
textAlign="right"
|
||||||
|
>
|
||||||
|
{priceToShow?.localPrice.pricePerStay}{" "}
|
||||||
|
{priceToShow?.localPrice.currency}
|
||||||
|
</Subtitle>
|
||||||
|
<Body color="uiTextMediumContrast">
|
||||||
|
{intl.formatMessage({ id: "Approx." })}{" "}
|
||||||
|
{priceToShow?.requestedPrice?.pricePerStay}{" "}
|
||||||
|
{priceToShow?.requestedPrice?.currency}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
<div className={styles.summaryPriceTextMobile}>
|
||||||
|
<Caption color="uiTextHighContrast">
|
||||||
|
{intl.formatMessage({ id: "Total price" })}
|
||||||
|
</Caption>
|
||||||
|
<Subtitle color={isUserLoggedIn ? "red" : "uiTextHighContrast"}>
|
||||||
|
{priceToShow?.localPrice.pricePerStay}{" "}
|
||||||
|
{priceToShow?.localPrice.currency}
|
||||||
|
</Subtitle>
|
||||||
|
<Footnote
|
||||||
|
color="uiTextMediumContrast"
|
||||||
|
className={styles.summaryPriceTextMobile}
|
||||||
|
>
|
||||||
|
{summaryPriceTex}
|
||||||
|
</Footnote>
|
||||||
|
</div>
|
||||||
|
{isPetRoomSelected && (
|
||||||
|
<div className={styles.petInfo}>
|
||||||
|
<Body
|
||||||
|
color="uiTextHighContrast"
|
||||||
|
textTransform="bold"
|
||||||
|
textAlign="right"
|
||||||
|
>
|
||||||
|
+ {petRoomPrice} {petRoomCurrency}
|
||||||
|
</Body>
|
||||||
|
<Body color="uiTextMediumContrast" textAlign="right">
|
||||||
|
{intl.formatMessage({ id: "Pet charge" })}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
theme="base"
|
||||||
|
className={styles.continueButton}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "Continue" })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,12 +6,19 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
padding: 0 0 var(--Spacing-x5);
|
padding: 0 0 var(--Spacing-x5);
|
||||||
|
align-items: center;
|
||||||
|
border-top: 1px solid var(--Base-Border-Subtle);
|
||||||
|
transition: bottom 300ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--max-width-navigation);
|
||||||
|
margin: 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-top: 1px solid var(--Base-Border-Subtle);
|
|
||||||
transition: bottom 300ms ease-in-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary[data-visible="true"] {
|
.summary[data-visible="true"] {
|
||||||
@@ -80,7 +87,9 @@
|
|||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.summary {
|
.summary {
|
||||||
padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x5);
|
padding: var(--Spacing-x3) var(--Spacing-x7) var(--Spacing-x5);
|
||||||
|
}
|
||||||
|
.content {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
.petInfo,
|
.petInfo,
|
||||||
@@ -102,5 +111,6 @@
|
|||||||
.summaryPriceContainer {
|
.summaryPriceContainer {
|
||||||
width: auto;
|
width: auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,14 +117,15 @@ export default function RoomCard({
|
|||||||
<div>
|
<div>
|
||||||
<div className={styles.imageContainer}>
|
<div className={styles.imageContainer}>
|
||||||
<div className={styles.chipContainer}>
|
<div className={styles.chipContainer}>
|
||||||
{roomConfiguration.roomsLeft < 5 && (
|
{roomConfiguration.roomsLeft > 0 &&
|
||||||
<span className={styles.chip}>
|
roomConfiguration.roomsLeft < 5 && (
|
||||||
<Footnote
|
<span className={styles.chip}>
|
||||||
color="burgundy"
|
<Footnote
|
||||||
textTransform="uppercase"
|
color="burgundy"
|
||||||
>{`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`}</Footnote>
|
textTransform="uppercase"
|
||||||
</span>
|
>{`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`}</Footnote>
|
||||||
)}
|
</span>
|
||||||
|
)}
|
||||||
{roomConfiguration.features
|
{roomConfiguration.features
|
||||||
.filter((feature) => selectedPackages.includes(feature.code))
|
.filter((feature) => selectedPackages.includes(feature.code))
|
||||||
.map((feature) => (
|
.map((feature) => (
|
||||||
@@ -209,9 +210,7 @@ export default function RoomCard({
|
|||||||
product={findProductForRate(rate)}
|
product={findProductForRate(rate)}
|
||||||
priceInformation={getRateDefinitionForRate(rate)?.generalTerms}
|
priceInformation={getRateDefinitionForRate(rate)?.generalTerms}
|
||||||
handleSelectRate={handleSelectRate}
|
handleSelectRate={handleSelectRate}
|
||||||
roomType={roomConfiguration.roomType}
|
|
||||||
roomTypeCode={roomConfiguration.roomTypeCode}
|
roomTypeCode={roomConfiguration.roomTypeCode}
|
||||||
features={roomConfiguration.features}
|
|
||||||
petRoomPackage={petRoomPackage}
|
petRoomPackage={petRoomPackage}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ import {
|
|||||||
type RoomPackageCodes,
|
type RoomPackageCodes,
|
||||||
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
import type { SelectRateProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
import type { SelectRateProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
||||||
import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate"
|
import type {
|
||||||
|
Rate,
|
||||||
|
RateCode,
|
||||||
|
} from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
import type { RoomConfiguration } from "@/server/routers/hotels/output"
|
import type { RoomConfiguration } from "@/server/routers/hotels/output"
|
||||||
|
|
||||||
export default function Rooms({
|
export default function Rooms({
|
||||||
@@ -25,9 +28,9 @@ export default function Rooms({
|
|||||||
}: SelectRateProps) {
|
}: SelectRateProps) {
|
||||||
const visibleRooms: RoomConfiguration[] =
|
const visibleRooms: RoomConfiguration[] =
|
||||||
filterDuplicateRoomTypesByLowestPrice(roomsAvailability.roomConfigurations)
|
filterDuplicateRoomTypesByLowestPrice(roomsAvailability.roomConfigurations)
|
||||||
const [selectedRate, setSelectedRate] = useState<
|
const [selectedRate, setSelectedRate] = useState<RateCode | undefined>(
|
||||||
{ publicRateCode: string; roomTypeCode: string } | undefined
|
undefined
|
||||||
>(undefined)
|
)
|
||||||
const [selectedPackages, setSelectedPackages] = useState<RoomPackageCodes[]>(
|
const [selectedPackages, setSelectedPackages] = useState<RoomPackageCodes[]>(
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
@@ -115,17 +118,30 @@ export default function Rooms({
|
|||||||
)
|
)
|
||||||
)?.features
|
)?.features
|
||||||
|
|
||||||
|
const roomType = roomCategories.find((roomCategory) =>
|
||||||
|
roomCategory.roomTypes.some(
|
||||||
|
(roomType) => roomType.code === room.roomTypeCode
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
const rateSummary: Rate = {
|
const rateSummary: Rate = {
|
||||||
features: petRoomPackage && features ? features : [],
|
features: petRoomPackage && features ? features : [],
|
||||||
priceName: room.roomType,
|
priceName: selectedRate?.name,
|
||||||
|
priceTerm: selectedRate?.paymentTerm,
|
||||||
public: product.productType.public,
|
public: product.productType.public,
|
||||||
member: product.productType.member,
|
member: product.productType.member,
|
||||||
roomType: room.roomType,
|
roomType: roomType?.name || room.roomType,
|
||||||
roomTypeCode: room.roomTypeCode,
|
roomTypeCode: room.roomTypeCode,
|
||||||
}
|
}
|
||||||
|
|
||||||
return rateSummary
|
return rateSummary
|
||||||
}, [filteredRooms, availablePackages, selectedPackages, selectedRate])
|
}, [
|
||||||
|
filteredRooms,
|
||||||
|
availablePackages,
|
||||||
|
selectedPackages,
|
||||||
|
selectedRate,
|
||||||
|
roomCategories,
|
||||||
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (rateSummary) return
|
if (rateSummary) return
|
||||||
|
|||||||
27
components/Icons/ExternalLink.tsx
Normal file
27
components/Icons/ExternalLink.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { iconVariants } from "./variants"
|
||||||
|
|
||||||
|
import type { IconProps } from "@/types/components/icon"
|
||||||
|
|
||||||
|
export default function ExternalLinkIcon({
|
||||||
|
className,
|
||||||
|
color,
|
||||||
|
...props
|
||||||
|
}: IconProps) {
|
||||||
|
const classNames = iconVariants({ className, color })
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={classNames}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="25"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 25 24"
|
||||||
|
fill="none"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5.59998 20.775C5.08434 20.775 4.64293 20.5914 4.27575 20.2242C3.90857 19.857 3.72498 19.4156 3.72498 18.9V5.09998C3.72498 4.58434 3.90857 4.14293 4.27575 3.77575C4.64293 3.40857 5.08434 3.22498 5.59998 3.22498H11.4875C11.7458 3.22498 11.9666 3.31664 12.15 3.49998C12.3333 3.68331 12.425 3.90414 12.425 4.16248C12.425 4.42081 12.3333 4.64164 12.15 4.82498C11.9666 5.00831 11.7458 5.09998 11.4875 5.09998H5.59998V18.9H19.4V13.0125C19.4 12.7541 19.4916 12.5333 19.675 12.35C19.8583 12.1666 20.0791 12.075 20.3375 12.075C20.5958 12.075 20.8166 12.1666 21 12.35C21.1833 12.5333 21.275 12.7541 21.275 13.0125V18.9C21.275 19.4156 21.0914 19.857 20.7242 20.2242C20.357 20.5914 19.9156 20.775 19.4 20.775H5.59998ZM19.4 6.41248L10.8875 14.925C10.7125 15.1 10.4979 15.1875 10.2437 15.1875C9.98956 15.1875 9.77081 15.0958 9.58748 14.9125C9.40414 14.7291 9.31248 14.5104 9.31248 14.2562C9.31248 14.0021 9.40311 13.7843 9.58438 13.6031L18.0875 5.09998H15.2375C14.9791 5.09998 14.7583 5.00831 14.575 4.82498C14.3916 4.64164 14.3 4.42081 14.3 4.16248C14.3 3.90414 14.3916 3.68331 14.575 3.49998C14.7583 3.31664 14.9791 3.22498 15.2375 3.22498H21.275V9.26248C21.275 9.52081 21.1833 9.74164 21 9.92498C20.8166 10.1083 20.5958 10.2 20.3375 10.2C20.0791 10.2 19.8583 10.1083 19.675 9.92498C19.4916 9.74164 19.4 9.52081 19.4 9.26248V6.41248Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
ElectricBikeIcon,
|
ElectricBikeIcon,
|
||||||
ElectricCarIcon,
|
ElectricCarIcon,
|
||||||
EmailIcon,
|
EmailIcon,
|
||||||
|
ExternalLinkIcon,
|
||||||
EyeHideIcon,
|
EyeHideIcon,
|
||||||
EyeShowIcon,
|
EyeShowIcon,
|
||||||
FacebookIcon,
|
FacebookIcon,
|
||||||
@@ -176,6 +177,8 @@ export function getIconByIconName(
|
|||||||
return ElectricCarIcon
|
return ElectricCarIcon
|
||||||
case IconName.Email:
|
case IconName.Email:
|
||||||
return EmailIcon
|
return EmailIcon
|
||||||
|
case IconName.ExternalLink:
|
||||||
|
return ExternalLinkIcon
|
||||||
case IconName.EyeHide:
|
case IconName.EyeHide:
|
||||||
return EyeHideIcon
|
return EyeHideIcon
|
||||||
case IconName.EyeShow:
|
case IconName.EyeShow:
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export { default as ElectricBikeIcon } from "./ElectricBike"
|
|||||||
export { default as ElectricCarIcon } from "./ElectricCar"
|
export { default as ElectricCarIcon } from "./ElectricCar"
|
||||||
export { default as EmailIcon } from "./Email"
|
export { default as EmailIcon } from "./Email"
|
||||||
export { default as ErrorCircleIcon } from "./ErrorCircle"
|
export { default as ErrorCircleIcon } from "./ErrorCircle"
|
||||||
|
export { default as ExternalLinkIcon } from "./ExternalLink"
|
||||||
export { default as EyeHideIcon } from "./EyeHide"
|
export { default as EyeHideIcon } from "./EyeHide"
|
||||||
export { default as EyeShowIcon } from "./EyeShow"
|
export { default as EyeShowIcon } from "./EyeShow"
|
||||||
export { default as FacebookIcon } from "./Facebook"
|
export { default as FacebookIcon } from "./Facebook"
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { useIntl } from "react-intl"
|
|||||||
import { GalleryIcon } from "@/components/Icons"
|
import { GalleryIcon } from "@/components/Icons"
|
||||||
import Image from "@/components/Image"
|
import Image from "@/components/Image"
|
||||||
import Lightbox from "@/components/Lightbox"
|
import Lightbox from "@/components/Lightbox"
|
||||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
|
||||||
|
import Caption from "../TempDesignSystem/Text/Caption"
|
||||||
|
|
||||||
import styles from "./imageGallery.module.css"
|
import styles from "./imageGallery.module.css"
|
||||||
|
|
||||||
@@ -44,9 +45,9 @@ function ImageGallery({
|
|||||||
/>
|
/>
|
||||||
<div className={styles.imageCount}>
|
<div className={styles.imageCount}>
|
||||||
<GalleryIcon color="white" />
|
<GalleryIcon color="white" />
|
||||||
<Footnote color="white" type="label">
|
<Caption color="white" type="label">
|
||||||
{images.length}
|
{images.length}
|
||||||
</Footnote>
|
</Caption>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Lightbox
|
<Lightbox
|
||||||
|
|||||||
@@ -122,6 +122,7 @@
|
|||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: var(--Spacing-x1);
|
gap: var(--Spacing-x1);
|
||||||
max-height: none;
|
max-height: none;
|
||||||
|
padding: var(--Spacing-x3) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnailContainer {
|
.thumbnailContainer {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export default function Accessibility({
|
|||||||
<AccordionItem
|
<AccordionItem
|
||||||
title={intl.formatMessage({ id: "Accessibility" })}
|
title={intl.formatMessage({ id: "Accessibility" })}
|
||||||
icon={IconName.Accessibility}
|
icon={IconName.Accessibility}
|
||||||
|
variant="sidepeek"
|
||||||
>
|
>
|
||||||
<Body>{accessibilityElevatorPitchText}</Body>
|
<Body>{accessibilityElevatorPitchText}</Body>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export default function CheckinCheckOut({ checkin }: CheckInCheckOutProps) {
|
|||||||
<AccordionItem
|
<AccordionItem
|
||||||
title={intl.formatMessage({ id: "Check-in/Check-out" })}
|
title={intl.formatMessage({ id: "Check-in/Check-out" })}
|
||||||
icon={IconName.Calendar}
|
icon={IconName.Calendar}
|
||||||
|
variant="sidepeek"
|
||||||
>
|
>
|
||||||
<Body textTransform="bold">{intl.formatMessage({ id: "Hours" })}</Body>
|
<Body textTransform="bold">{intl.formatMessage({ id: "Hours" })}</Body>
|
||||||
<Body>{`${intl.formatMessage({ id: "Check in from" })}: ${checkin.checkInTime}`}</Body>
|
<Body>{`${intl.formatMessage({ id: "Check in from" })}: ${checkin.checkInTime}`}</Body>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export default function MeetingsAndConferences({
|
|||||||
<AccordionItem
|
<AccordionItem
|
||||||
title={intl.formatMessage({ id: "Meetings & Conferences" })}
|
title={intl.formatMessage({ id: "Meetings & Conferences" })}
|
||||||
icon={IconName.Business}
|
icon={IconName.Business}
|
||||||
|
variant="sidepeek"
|
||||||
>
|
>
|
||||||
<Body>{meetingDescription}</Body>
|
<Body>{meetingDescription}</Body>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export default function Parking({ parking }: ParkingProps) {
|
|||||||
title={intl.formatMessage({ id: "Parking" })}
|
title={intl.formatMessage({ id: "Parking" })}
|
||||||
icon={IconName.Parking}
|
icon={IconName.Parking}
|
||||||
className={styles.parking}
|
className={styles.parking}
|
||||||
|
variant="sidepeek"
|
||||||
>
|
>
|
||||||
{parking.map((p) => (
|
{parking.map((p) => (
|
||||||
<div key={p.name}>
|
<div key={p.name}>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default function Restaurant({
|
|||||||
<AccordionItem
|
<AccordionItem
|
||||||
title={intl.formatMessage({ id: "Restaurant" }, { count: 1 })}
|
title={intl.formatMessage({ id: "Restaurant" }, { count: 1 })}
|
||||||
icon={IconName.Restaurant}
|
icon={IconName.Restaurant}
|
||||||
|
variant="sidepeek"
|
||||||
>
|
>
|
||||||
<Body>{restaurantsContentDescriptionMedium}</Body>
|
<Body>{restaurantsContentDescriptionMedium}</Body>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--Spacing-x1);
|
gap: var(--Spacing-x1);
|
||||||
padding-left: var(--Spacing-x1);
|
padding-left: var(--Spacing-x1);
|
||||||
|
justify-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list li svg {
|
.list li svg {
|
||||||
|
|||||||
@@ -9,13 +9,24 @@
|
|||||||
gap: var(--Spacing-x2);
|
gap: var(--Spacing-x2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.amenity {
|
.content:last-child {
|
||||||
font-family: var(--typography-Body-Regular-fontFamily);
|
gap: 0;
|
||||||
border-bottom: 1px solid var(--Base-Border-Subtle);
|
}
|
||||||
/* padding set to align with AccordionItem which has a different composition */
|
|
||||||
padding: calc(var(--Spacing-x1) + var(--Spacing-x-one-and-half))
|
.content > p {
|
||||||
var(--Spacing-x3);
|
margin-bottom: var(--Spacing-x-one-and-half);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content > ul > li:first-child {
|
||||||
|
border-top: 1px solid var(--Base-Border-Subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.amenity > p {
|
||||||
|
border-top: 1px solid var(--Base-Border-Subtle);
|
||||||
|
padding: calc(var(--Spacing-x-one-and-half) + var(--Spacing-x1))
|
||||||
|
var(--Spacing-x1);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: var(--Spacing-x1);
|
gap: var(--Spacing-x1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,24 +71,21 @@ export default function HotelSidePeek({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</Accordion>
|
||||||
|
<div className={styles.amenity}>
|
||||||
{amenitiesList.map((amenity) => {
|
{amenitiesList.map((amenity) => {
|
||||||
const Icon = mapFacilityToIcon(amenity.id)
|
const Icon = mapFacilityToIcon(amenity.id)
|
||||||
return (
|
return (
|
||||||
<div key={amenity.id} className={styles.amenity}>
|
<Subtitle type="two" key={amenity.id} color="uiTextHighContrast">
|
||||||
{Icon && (
|
{Icon && (
|
||||||
<Icon width={24} height={24} color="uiTextMediumContrast" />
|
<Icon width={24} height={24} color="uiTextHighContrast" />
|
||||||
)}
|
)}
|
||||||
<Body
|
{amenity.name}
|
||||||
asChild
|
</Subtitle>
|
||||||
className={!Icon ? styles.noIcon : undefined}
|
|
||||||
color="uiTextMediumContrast"
|
|
||||||
>
|
|
||||||
<span>{amenity.name}</span>
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</Accordion>
|
</div>
|
||||||
|
|
||||||
{/* TODO: handle linking to Hotel Page */}
|
{/* TODO: handle linking to Hotel Page */}
|
||||||
{/* {showCTA && (
|
{/* {showCTA && (
|
||||||
<Button theme="base" intent="secondary" size="large">
|
<Button theme="base" intent="secondary" size="large">
|
||||||
|
|||||||
@@ -19,10 +19,7 @@ export default function Sidebar({ blocks }: SidebarProps) {
|
|||||||
switch (block.typename) {
|
switch (block.typename) {
|
||||||
case SidebarEnums.blocks.Content:
|
case SidebarEnums.blocks.Content:
|
||||||
return (
|
return (
|
||||||
<section
|
<section key={`${block.typename}-${idx}`}>
|
||||||
className={styles.content}
|
|
||||||
key={`${block.typename}-${idx}`}
|
|
||||||
>
|
|
||||||
<JsonToHtml
|
<JsonToHtml
|
||||||
embeds={block.content.embedded_itemsConnection.edges}
|
embeds={block.content.embedded_itemsConnection.edges}
|
||||||
nodes={block.content.json.children}
|
nodes={block.content.json.children}
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
padding: var(--Spacing-x1);
|
padding: var(--Spacing-x1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.accordionItem.sidepeek {
|
||||||
|
padding: var(--Spacing-x1) 0;
|
||||||
|
}
|
||||||
|
|
||||||
.summary {
|
.summary {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -18,7 +22,7 @@
|
|||||||
font-weight: var(--typography-Body-Bold-fontWeight);
|
font-weight: var(--typography-Body-Bold-fontWeight);
|
||||||
transition: background-color 0.3s;
|
transition: background-color 0.3s;
|
||||||
}
|
}
|
||||||
.summary:hover {
|
.summary.card:hover {
|
||||||
background-color: var(--Base-Surface-Primary-light-Hover-alt);
|
background-color: var(--Base-Surface-Primary-light-Hover-alt);
|
||||||
}
|
}
|
||||||
.accordionItem.light .summary:hover {
|
.accordionItem.light .summary:hover {
|
||||||
@@ -33,6 +37,11 @@
|
|||||||
border-radius: var(--Corner-radius-Medium);
|
border-radius: var(--Corner-radius-Medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.accordionItem.sidepeek .summary {
|
||||||
|
padding: var(--Spacing-x-one-and-half) var(--Spacing-x1);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,16 +50,10 @@ export default function AccordionItem({
|
|||||||
<li className={accordionItemVariants({ className, variant, theme })}>
|
<li className={accordionItemVariants({ className, variant, theme })}>
|
||||||
<details ref={detailsRef} onToggle={toggleAccordion}>
|
<details ref={detailsRef} onToggle={toggleAccordion}>
|
||||||
<summary className={styles.summary}>
|
<summary className={styles.summary}>
|
||||||
{IconComp && <IconComp className={styles.icon} color="burgundy" />}
|
{IconComp && (
|
||||||
{variant === "card" ? (
|
<IconComp className={styles.icon} color="baseTextHighcontrast" />
|
||||||
<Body
|
)}
|
||||||
textTransform="bold"
|
{variant === "sidepeek" ? (
|
||||||
color="baseTextHighContrast"
|
|
||||||
className={styles.title}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Body>
|
|
||||||
) : (
|
|
||||||
<Subtitle
|
<Subtitle
|
||||||
className={styles.title}
|
className={styles.title}
|
||||||
type="two"
|
type="two"
|
||||||
@@ -67,6 +61,14 @@ export default function AccordionItem({
|
|||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Subtitle>
|
</Subtitle>
|
||||||
|
) : (
|
||||||
|
<Body
|
||||||
|
textTransform="bold"
|
||||||
|
color="baseTextHighContrast"
|
||||||
|
className={styles.title}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Body>
|
||||||
)}
|
)}
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={styles.chevron}
|
className={styles.chevron}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const accordionItemVariants = cva(styles.accordionItem, {
|
|||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
card: styles.card,
|
card: styles.card,
|
||||||
|
sidepeek: styles.sidepeek,
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
default: styles.default,
|
default: styles.default,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const accordionVariants = cva(styles.accordion, {
|
|||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
card: styles.card,
|
card: styles.card,
|
||||||
|
sidepeek: styles.sidepeek,
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
default: styles.default,
|
default: styles.default,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
|
||||||
import { ChevronRightIcon } from "@/components/Icons"
|
import { ChevronRightSmallIcon } from "@/components/Icons"
|
||||||
import JsonToHtml from "@/components/JsonToHtml"
|
import JsonToHtml from "@/components/JsonToHtml"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ export default function TeaserCardSidepeek({
|
|||||||
wrapping
|
wrapping
|
||||||
>
|
>
|
||||||
{button.call_to_action_text}
|
{button.call_to_action_text}
|
||||||
<ChevronRightIcon height={20} width={20} />
|
<ChevronRightSmallIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<SidePeek
|
<SidePeek
|
||||||
title={heading}
|
title={heading}
|
||||||
@@ -44,13 +44,7 @@ export default function TeaserCardSidepeek({
|
|||||||
/>
|
/>
|
||||||
<div className={styles.ctaContainer}>
|
<div className={styles.ctaContainer}>
|
||||||
{primary_button && (
|
{primary_button && (
|
||||||
<Button
|
<Button asChild theme="base" intent="primary" size="small">
|
||||||
asChild
|
|
||||||
theme="base"
|
|
||||||
intent="primary"
|
|
||||||
size="small"
|
|
||||||
className={styles.ctaButton}
|
|
||||||
>
|
|
||||||
<Link
|
<Link
|
||||||
href={primary_button.href}
|
href={primary_button.href}
|
||||||
target={primary_button.openInNewTab ? "_blank" : undefined}
|
target={primary_button.openInNewTab ? "_blank" : undefined}
|
||||||
@@ -61,12 +55,7 @@ export default function TeaserCardSidepeek({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{secondary_button && (
|
{secondary_button && (
|
||||||
<Button
|
<Button asChild intent="secondary" size="small">
|
||||||
asChild
|
|
||||||
intent="secondary"
|
|
||||||
size="small"
|
|
||||||
className={styles.ctaButton}
|
|
||||||
>
|
|
||||||
<Link
|
<Link
|
||||||
href={secondary_button.href}
|
href={secondary_button.href}
|
||||||
target={secondary_button.openInNewTab ? "_blank" : undefined}
|
target={secondary_button.openInNewTab ? "_blank" : undefined}
|
||||||
|
|||||||
@@ -41,9 +41,7 @@ export default function TeaserCard({
|
|||||||
<Subtitle textAlign="left" type="two" color="black">
|
<Subtitle textAlign="left" type="two" color="black">
|
||||||
{title}
|
{title}
|
||||||
</Subtitle>
|
</Subtitle>
|
||||||
<Body color="black" className={styles.body}>
|
<Body color="black">{description}</Body>
|
||||||
{description}
|
|
||||||
</Body>
|
|
||||||
{sidePeekButton && sidePeekContent ? (
|
{sidePeekButton && sidePeekContent ? (
|
||||||
<TeaserCardSidepeek
|
<TeaserCardSidepeek
|
||||||
button={sidePeekButton}
|
button={sidePeekButton}
|
||||||
@@ -77,6 +75,8 @@ export default function TeaserCard({
|
|||||||
<Link
|
<Link
|
||||||
href={secondaryButton.href}
|
href={secondaryButton.href}
|
||||||
target={secondaryButton.openInNewTab ? "_blank" : undefined}
|
target={secondaryButton.openInNewTab ? "_blank" : undefined}
|
||||||
|
color="burgundy"
|
||||||
|
weight="bold"
|
||||||
>
|
>
|
||||||
{secondaryButton.title}
|
{secondaryButton.title}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"ALLG": "Allergi",
|
"ALLG": "Allergi",
|
||||||
"About accessibility": "Om tilgængelighed",
|
"About accessibility": "Om tilgængelighed",
|
||||||
"About meetings & conferences": "About meetings & conferences",
|
"About meetings & conferences": "About meetings & conferences",
|
||||||
|
"About parking": "Om parkering",
|
||||||
"About the hotel": "Om hotellet",
|
"About the hotel": "Om hotellet",
|
||||||
"Accept new price": "Accepter ny pris",
|
"Accept new price": "Accepter ny pris",
|
||||||
"Accessibility": "Tilgængelighed",
|
"Accessibility": "Tilgængelighed",
|
||||||
@@ -47,6 +48,7 @@
|
|||||||
"Bed type": "Seng type",
|
"Bed type": "Seng type",
|
||||||
"Birth date": "Fødselsdato",
|
"Birth date": "Fødselsdato",
|
||||||
"Book": "Book",
|
"Book": "Book",
|
||||||
|
"Book parking": "Book parkering",
|
||||||
"Book reward night": "Book bonusnat",
|
"Book reward night": "Book bonusnat",
|
||||||
"Booking number": "Bookingnummer",
|
"Booking number": "Bookingnummer",
|
||||||
"Breakfast": "Morgenmad",
|
"Breakfast": "Morgenmad",
|
||||||
@@ -93,6 +95,7 @@
|
|||||||
"Could not find requested resource": "Kunne ikke finde den anmodede ressource",
|
"Could not find requested resource": "Kunne ikke finde den anmodede ressource",
|
||||||
"Country": "Land",
|
"Country": "Land",
|
||||||
"Country code": "Landekode",
|
"Country code": "Landekode",
|
||||||
|
"Creative spaces for meetings": "Kreative rum til møder",
|
||||||
"Credit card": "Kreditkort",
|
"Credit card": "Kreditkort",
|
||||||
"Credit card deleted successfully": "Kreditkort blev slettet",
|
"Credit card deleted successfully": "Kreditkort blev slettet",
|
||||||
"Currency Code": "DKK",
|
"Currency Code": "DKK",
|
||||||
@@ -349,6 +352,7 @@
|
|||||||
"Select your language": "Vælg dit sprog",
|
"Select your language": "Vælg dit sprog",
|
||||||
"Shopping": "Shopping",
|
"Shopping": "Shopping",
|
||||||
"Shopping & Dining": "Shopping & Spisning",
|
"Shopping & Dining": "Shopping & Spisning",
|
||||||
|
"Show activities calendar": "Vis aktivitetskalender",
|
||||||
"Show all amenities": "Vis alle faciliteter",
|
"Show all amenities": "Vis alle faciliteter",
|
||||||
"Show less": "Vis mindre",
|
"Show less": "Vis mindre",
|
||||||
"Show less rooms": "Vise færre rum",
|
"Show less rooms": "Vise færre rum",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"ALLG": "Allergie",
|
"ALLG": "Allergie",
|
||||||
"About accessibility": "Über Barrierefreiheit",
|
"About accessibility": "Über Barrierefreiheit",
|
||||||
"About meetings & conferences": "About meetings & conferences",
|
"About meetings & conferences": "About meetings & conferences",
|
||||||
|
"About parking": "Über das Parken",
|
||||||
"About the hotel": "Über das Hotel",
|
"About the hotel": "Über das Hotel",
|
||||||
"Accept new price": "Neuen Preis akzeptieren",
|
"Accept new price": "Neuen Preis akzeptieren",
|
||||||
"Accessibility": "Zugänglichkeit",
|
"Accessibility": "Zugänglichkeit",
|
||||||
@@ -47,6 +48,7 @@
|
|||||||
"Bed type": "Bettentyp",
|
"Bed type": "Bettentyp",
|
||||||
"Birth date": "Geburtsdatum",
|
"Birth date": "Geburtsdatum",
|
||||||
"Book": "Buchen",
|
"Book": "Buchen",
|
||||||
|
"Book parking": "Parkplatz buchen",
|
||||||
"Book reward night": "Bonusnacht buchen",
|
"Book reward night": "Bonusnacht buchen",
|
||||||
"Booking number": "Buchungsnummer",
|
"Booking number": "Buchungsnummer",
|
||||||
"Breakfast": "Frühstück",
|
"Breakfast": "Frühstück",
|
||||||
@@ -93,6 +95,7 @@
|
|||||||
"Could not find requested resource": "Die angeforderte Ressource konnte nicht gefunden werden.",
|
"Could not find requested resource": "Die angeforderte Ressource konnte nicht gefunden werden.",
|
||||||
"Country": "Land",
|
"Country": "Land",
|
||||||
"Country code": "Landesvorwahl",
|
"Country code": "Landesvorwahl",
|
||||||
|
"Creative spaces for meetings": "Kreative Räume für Meetings",
|
||||||
"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",
|
"Currency Code": "EUR",
|
||||||
@@ -349,6 +352,7 @@
|
|||||||
"Select your language": "Wählen Sie Ihre Sprache",
|
"Select your language": "Wählen Sie Ihre Sprache",
|
||||||
"Shopping": "Einkaufen",
|
"Shopping": "Einkaufen",
|
||||||
"Shopping & Dining": "Einkaufen & Essen",
|
"Shopping & Dining": "Einkaufen & Essen",
|
||||||
|
"Show activities calendar": "Aktivitätenkalender anzeigen",
|
||||||
"Show all amenities": "Alle Annehmlichkeiten anzeigen",
|
"Show all amenities": "Alle Annehmlichkeiten anzeigen",
|
||||||
"Show less": "Weniger anzeigen",
|
"Show less": "Weniger anzeigen",
|
||||||
"Show less rooms": "Weniger Zimmer anzeigen",
|
"Show less rooms": "Weniger Zimmer anzeigen",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"ALLG": "Allergy",
|
"ALLG": "Allergy",
|
||||||
"About accessibility": "About accessibility",
|
"About accessibility": "About accessibility",
|
||||||
"About meetings & conferences": "About meetings & conferences",
|
"About meetings & conferences": "About meetings & conferences",
|
||||||
|
"About parking": "About parking",
|
||||||
"About the hotel": "About the hotel",
|
"About the hotel": "About the hotel",
|
||||||
"Accept new price": "Accept new price",
|
"Accept new price": "Accept new price",
|
||||||
"Accessibility": "Accessibility",
|
"Accessibility": "Accessibility",
|
||||||
@@ -49,6 +50,7 @@
|
|||||||
"Birth date": "Birth date",
|
"Birth date": "Birth date",
|
||||||
"Book": "Book",
|
"Book": "Book",
|
||||||
"Book another stay": "Book another stay",
|
"Book another stay": "Book another stay",
|
||||||
|
"Book parking": "Book parking",
|
||||||
"Book reward night": "Book reward night",
|
"Book reward night": "Book reward night",
|
||||||
"Book your next stay": "Book your next stay",
|
"Book your next stay": "Book your next stay",
|
||||||
"Booking": "Booking",
|
"Booking": "Booking",
|
||||||
@@ -101,6 +103,7 @@
|
|||||||
"Could not find requested resource": "Could not find requested resource",
|
"Could not find requested resource": "Could not find requested resource",
|
||||||
"Country": "Country",
|
"Country": "Country",
|
||||||
"Country code": "Country code",
|
"Country code": "Country code",
|
||||||
|
"Creative spaces for meetings": "Creative spaces for meetings",
|
||||||
"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",
|
"Currency Code": "EUR",
|
||||||
@@ -388,6 +391,7 @@
|
|||||||
"Select your language": "Select your language",
|
"Select your language": "Select your language",
|
||||||
"Shopping": "Shopping",
|
"Shopping": "Shopping",
|
||||||
"Shopping & Dining": "Shopping & Dining",
|
"Shopping & Dining": "Shopping & Dining",
|
||||||
|
"Show activities calendar": "Show activities calendar",
|
||||||
"Show all amenities": "Show all amenities",
|
"Show all amenities": "Show all amenities",
|
||||||
"Show less": "Show less",
|
"Show less": "Show less",
|
||||||
"Show less rooms": "Show less rooms",
|
"Show less rooms": "Show less rooms",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"ALLG": "Allergia",
|
"ALLG": "Allergia",
|
||||||
"About accessibility": "Tietoja saavutettavuudesta",
|
"About accessibility": "Tietoja saavutettavuudesta",
|
||||||
"About meetings & conferences": "About meetings & conferences",
|
"About meetings & conferences": "About meetings & conferences",
|
||||||
|
"About parking": "Tietoja pysäköinnistä",
|
||||||
"About the hotel": "Tietoja hotellista",
|
"About the hotel": "Tietoja hotellista",
|
||||||
"Accept new price": "Hyväksy uusi hinta",
|
"Accept new price": "Hyväksy uusi hinta",
|
||||||
"Accessibility": "Saavutettavuus",
|
"Accessibility": "Saavutettavuus",
|
||||||
@@ -47,6 +48,7 @@
|
|||||||
"Bed type": "Vuodetyyppi",
|
"Bed type": "Vuodetyyppi",
|
||||||
"Birth date": "Syntymäaika",
|
"Birth date": "Syntymäaika",
|
||||||
"Book": "Varaa",
|
"Book": "Varaa",
|
||||||
|
"Book parking": "Varaa pysäköinti",
|
||||||
"Book reward night": "Kirjapalkinto-ilta",
|
"Book reward night": "Kirjapalkinto-ilta",
|
||||||
"Booking number": "Varausnumero",
|
"Booking number": "Varausnumero",
|
||||||
"Breakfast": "Aamiainen",
|
"Breakfast": "Aamiainen",
|
||||||
@@ -93,6 +95,7 @@
|
|||||||
"Could not find requested resource": "Pyydettyä resurssia ei löytynyt",
|
"Could not find requested resource": "Pyydettyä resurssia ei löytynyt",
|
||||||
"Country": "Maa",
|
"Country": "Maa",
|
||||||
"Country code": "Maatunnus",
|
"Country code": "Maatunnus",
|
||||||
|
"Creative spaces for meetings": "Luovia tiloja kokouksille",
|
||||||
"Credit card": "Luottokortti",
|
"Credit card": "Luottokortti",
|
||||||
"Credit card deleted successfully": "Luottokortti poistettu onnistuneesti",
|
"Credit card deleted successfully": "Luottokortti poistettu onnistuneesti",
|
||||||
"Currency Code": "EUR",
|
"Currency Code": "EUR",
|
||||||
@@ -350,6 +353,7 @@
|
|||||||
"Select your language": "Valitse kieli",
|
"Select your language": "Valitse kieli",
|
||||||
"Shopping": "Ostokset",
|
"Shopping": "Ostokset",
|
||||||
"Shopping & Dining": "Ostokset & Ravintolat",
|
"Shopping & Dining": "Ostokset & Ravintolat",
|
||||||
|
"Show activities calendar": "Näytä aktiviteettikalenteri",
|
||||||
"Show all amenities": "Näytä kaikki mukavuudet",
|
"Show all amenities": "Näytä kaikki mukavuudet",
|
||||||
"Show less": "Näytä vähemmän",
|
"Show less": "Näytä vähemmän",
|
||||||
"Show less rooms": "Näytä vähemmän huoneita",
|
"Show less rooms": "Näytä vähemmän huoneita",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"ALLG": "Allergi",
|
"ALLG": "Allergi",
|
||||||
"About accessibility": "Om tilgjengelighet",
|
"About accessibility": "Om tilgjengelighet",
|
||||||
"About meetings & conferences": "About meetings & conferences",
|
"About meetings & conferences": "About meetings & conferences",
|
||||||
|
"About parking": "Om parkering",
|
||||||
"About the hotel": "Om hotellet",
|
"About the hotel": "Om hotellet",
|
||||||
"Accept new price": "Aksepterer ny pris",
|
"Accept new price": "Aksepterer ny pris",
|
||||||
"Accessibility": "Tilgjengelighet",
|
"Accessibility": "Tilgjengelighet",
|
||||||
@@ -47,6 +48,7 @@
|
|||||||
"Bed type": "Seng type",
|
"Bed type": "Seng type",
|
||||||
"Birth date": "Fødselsdato",
|
"Birth date": "Fødselsdato",
|
||||||
"Book": "Bestill",
|
"Book": "Bestill",
|
||||||
|
"Book parking": "Bestill parkering",
|
||||||
"Book reward night": "Bestill belønningskveld",
|
"Book reward night": "Bestill belønningskveld",
|
||||||
"Booking number": "Bestillingsnummer",
|
"Booking number": "Bestillingsnummer",
|
||||||
"Breakfast": "Frokost",
|
"Breakfast": "Frokost",
|
||||||
@@ -93,6 +95,7 @@
|
|||||||
"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",
|
||||||
|
"Creative spaces for meetings": "Kreative rom for møter",
|
||||||
"Credit card deleted successfully": "Kredittkort slettet",
|
"Credit card deleted successfully": "Kredittkort slettet",
|
||||||
"Currency Code": "NOK",
|
"Currency Code": "NOK",
|
||||||
"Current password": "Nåværende passord",
|
"Current password": "Nåværende passord",
|
||||||
@@ -347,6 +350,7 @@
|
|||||||
"Select your language": "Velg språk",
|
"Select your language": "Velg språk",
|
||||||
"Shopping": "Shopping",
|
"Shopping": "Shopping",
|
||||||
"Shopping & Dining": "Shopping & Spisesteder",
|
"Shopping & Dining": "Shopping & Spisesteder",
|
||||||
|
"Show activities calendar": "Vis aktivitetskalender",
|
||||||
"Show all amenities": "Vis alle fasiliteter",
|
"Show all amenities": "Vis alle fasiliteter",
|
||||||
"Show less": "Vis mindre",
|
"Show less": "Vis mindre",
|
||||||
"Show less rooms": "Vise færre rom",
|
"Show less rooms": "Vise færre rom",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"ALLG": "Allergi",
|
"ALLG": "Allergi",
|
||||||
"About accessibility": "Om tillgänglighet",
|
"About accessibility": "Om tillgänglighet",
|
||||||
"About meetings & conferences": "About meetings & conferences",
|
"About meetings & conferences": "About meetings & conferences",
|
||||||
|
"About parking": "Om parkering",
|
||||||
"About the hotel": "Om hotellet",
|
"About the hotel": "Om hotellet",
|
||||||
"Accept new price": "Accepter ny pris",
|
"Accept new price": "Accepter ny pris",
|
||||||
"Accessibility": "Tillgänglighet",
|
"Accessibility": "Tillgänglighet",
|
||||||
@@ -47,6 +48,7 @@
|
|||||||
"Bed type": "Sängtyp",
|
"Bed type": "Sängtyp",
|
||||||
"Birth date": "Födelsedatum",
|
"Birth date": "Födelsedatum",
|
||||||
"Book": "Boka",
|
"Book": "Boka",
|
||||||
|
"Book parking": "Boka parkering",
|
||||||
"Book reward night": "Boka frinatt",
|
"Book reward night": "Boka frinatt",
|
||||||
"Booking number": "Bokningsnummer",
|
"Booking number": "Bokningsnummer",
|
||||||
"Breakfast": "Frukost",
|
"Breakfast": "Frukost",
|
||||||
@@ -93,6 +95,7 @@
|
|||||||
"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",
|
||||||
|
"Creative spaces for meetings": "Kreativa utrymmen för möten",
|
||||||
"Credit card deleted successfully": "Kreditkort har tagits bort",
|
"Credit card deleted successfully": "Kreditkort har tagits bort",
|
||||||
"Currency Code": "SEK",
|
"Currency Code": "SEK",
|
||||||
"Current password": "Nuvarande lösenord",
|
"Current password": "Nuvarande lösenord",
|
||||||
@@ -171,6 +174,7 @@
|
|||||||
"How it works": "Hur det fungerar",
|
"How it works": "Hur det fungerar",
|
||||||
"Hurry up and use them before they expire!": "Skynda dig och använd dem innan de går ut!",
|
"Hurry up and use them before they expire!": "Skynda dig och använd dem innan de går ut!",
|
||||||
"I accept the terms and conditions": "Jag accepterar villkoren",
|
"I accept the terms and conditions": "Jag accepterar villkoren",
|
||||||
|
"I would like to get my booking confirmation via sms": "Jag vill få min bokningsbekräftelse via sms",
|
||||||
"Image gallery": "{name} - Bildgalleri",
|
"Image gallery": "{name} - Bildgalleri",
|
||||||
"In adults bed": "I vuxens säng",
|
"In adults bed": "I vuxens säng",
|
||||||
"In crib": "I spjälsäng",
|
"In crib": "I spjälsäng",
|
||||||
@@ -347,6 +351,7 @@
|
|||||||
"Select your language": "Välj ditt språk",
|
"Select your language": "Välj ditt språk",
|
||||||
"Shopping": "Shopping",
|
"Shopping": "Shopping",
|
||||||
"Shopping & Dining": "Shopping & Mat",
|
"Shopping & Dining": "Shopping & Mat",
|
||||||
|
"Show activities calendar": "Visa aktivitetskalender",
|
||||||
"Show all amenities": "Visa alla bekvämligheter",
|
"Show all amenities": "Visa alla bekvämligheter",
|
||||||
"Show less": "Visa mindre",
|
"Show less": "Visa mindre",
|
||||||
"Show less rooms": "Visa färre rum",
|
"Show less rooms": "Visa färre rum",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import "dayjs/locale/sv"
|
|||||||
|
|
||||||
import d from "dayjs"
|
import d from "dayjs"
|
||||||
import advancedFormat from "dayjs/plugin/advancedFormat"
|
import advancedFormat from "dayjs/plugin/advancedFormat"
|
||||||
|
import isSameOrAfter from "dayjs/plugin/isSameOrAfter"
|
||||||
import isToday from "dayjs/plugin/isToday"
|
import isToday from "dayjs/plugin/isToday"
|
||||||
import relativeTime from "dayjs/plugin/relativeTime"
|
import relativeTime from "dayjs/plugin/relativeTime"
|
||||||
import timezone from "dayjs/plugin/timezone"
|
import timezone from "dayjs/plugin/timezone"
|
||||||
@@ -62,5 +63,6 @@ d.extend(isToday)
|
|||||||
d.extend(relativeTime)
|
d.extend(relativeTime)
|
||||||
d.extend(timezone)
|
d.extend(timezone)
|
||||||
d.extend(utc)
|
d.extend(utc)
|
||||||
|
d.extend(isSameOrAfter)
|
||||||
|
|
||||||
export const dt = d
|
export const dt = d
|
||||||
|
|||||||
@@ -50,13 +50,17 @@ query GetHotelPage($locale: String!, $uid: String!) {
|
|||||||
cta_text
|
cta_text
|
||||||
heading
|
heading
|
||||||
body_text
|
body_text
|
||||||
open_in_new_tab
|
|
||||||
scripted_title
|
scripted_title
|
||||||
hotel_page_activities_content_pageConnection {
|
hotel_page_activities_content_pageConnection {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
__typename
|
__typename
|
||||||
...ContentPageLink
|
... on ContentPage {
|
||||||
|
...ContentPageLink
|
||||||
|
header {
|
||||||
|
preamble
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export const middleware: NextMiddleware = async (request, event) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note that the order of middlewares is important since that is the order they are matched by.
|
||||||
const middlewares = [
|
const middlewares = [
|
||||||
currentWebLogin,
|
currentWebLogin,
|
||||||
currentWebLoginEmail,
|
currentWebLoginEmail,
|
||||||
@@ -51,9 +52,9 @@ export const middleware: NextMiddleware = async (request, event) => {
|
|||||||
handleAuth,
|
handleAuth,
|
||||||
myPages,
|
myPages,
|
||||||
webView,
|
webView,
|
||||||
|
dateFormat,
|
||||||
bookingFlow,
|
bookingFlow,
|
||||||
cmsContent,
|
cmsContent,
|
||||||
dateFormat,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { MiddlewareMatcher } from "@/types/middleware"
|
|||||||
YYYY-MM-D and YYYY-MM-DD since the current web uses YYYY-MM-D
|
YYYY-MM-D and YYYY-MM-DD since the current web uses YYYY-MM-D
|
||||||
in the URL as parameters (toDate and fromDate)
|
in the URL as parameters (toDate and fromDate)
|
||||||
*/
|
*/
|
||||||
|
const legacyDatePattern = /^([12]\d{3}-(0[1-9]|1[0-2])-([1-9]))$/
|
||||||
|
|
||||||
function normalizeDate(date: string): string {
|
function normalizeDate(date: string): string {
|
||||||
const datePattern = /^\d{4}-\d{1,2}-\d{1,2}$/
|
const datePattern = /^\d{4}-\d{1,2}-\d{1,2}$/
|
||||||
@@ -21,17 +22,23 @@ export const middleware: NextMiddleware = (request) => {
|
|||||||
const url = request.nextUrl.clone()
|
const url = request.nextUrl.clone()
|
||||||
const { searchParams } = url
|
const { searchParams } = url
|
||||||
|
|
||||||
if (searchParams.has("fromDate")) {
|
if (
|
||||||
|
legacyDatePattern.test(searchParams.get("fromDate")!) ||
|
||||||
|
legacyDatePattern.test(searchParams.get("toDate")!)
|
||||||
|
) {
|
||||||
const fromDate = searchParams.get("fromDate")!
|
const fromDate = searchParams.get("fromDate")!
|
||||||
searchParams.set("fromDate", normalizeDate(fromDate))
|
url.searchParams.set("fromDate", normalizeDate(fromDate))
|
||||||
}
|
|
||||||
|
|
||||||
if (searchParams.has("toDate")) {
|
|
||||||
const toDate = searchParams.get("toDate")!
|
const toDate = searchParams.get("toDate")!
|
||||||
searchParams.set("toDate", normalizeDate(toDate))
|
url.searchParams.set("toDate", normalizeDate(toDate))
|
||||||
|
return NextResponse.redirect(url)
|
||||||
|
} else {
|
||||||
|
const headers = new Headers(request.headers)
|
||||||
|
headers.set("x-continue", "1")
|
||||||
|
return NextResponse.next({
|
||||||
|
headers,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.rewrite(url)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const matcher: MiddlewareMatcher = (request) => {
|
export const matcher: MiddlewareMatcher = (request) => {
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ const nextConfig = {
|
|||||||
{
|
{
|
||||||
key: "fromDate",
|
key: "fromDate",
|
||||||
type: "query",
|
type: "query",
|
||||||
value: "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$",
|
value: "^([12]\\d{3}-(0[1-9]|1[0-2])-(0?[1-9]|[12]\\d|3[01]))$",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
permanent: false,
|
permanent: false,
|
||||||
@@ -168,7 +168,7 @@ const nextConfig = {
|
|||||||
{
|
{
|
||||||
key: "toDate",
|
key: "toDate",
|
||||||
type: "query",
|
type: "query",
|
||||||
value: "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$",
|
value: "^([12]\\d{3}-(0[1-9]|1[0-2])-(0?[1-9]|[12]\\d|3[01]))$",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
permanent: false,
|
permanent: false,
|
||||||
|
|||||||
79
package-lock.json
generated
79
package-lock.json
generated
@@ -41,6 +41,7 @@
|
|||||||
"graphql": "^16.8.1",
|
"graphql": "^16.8.1",
|
||||||
"graphql-request": "^6.1.0",
|
"graphql-request": "^6.1.0",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
|
"ics": "^3.8.1",
|
||||||
"immer": "10.1.1",
|
"immer": "10.1.1",
|
||||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||||
"libphonenumber-js": "^1.10.60",
|
"libphonenumber-js": "^1.10.60",
|
||||||
@@ -54,6 +55,7 @@
|
|||||||
"react-hook-form": "^7.51.2",
|
"react-hook-form": "^7.51.2",
|
||||||
"react-international-phone": "^4.2.6",
|
"react-international-phone": "^4.2.6",
|
||||||
"react-intl": "^6.6.8",
|
"react-intl": "^6.6.8",
|
||||||
|
"react-to-print": "^3.0.2",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"sonner": "^1.7.0",
|
"sonner": "^1.7.0",
|
||||||
"superjson": "^2.2.1",
|
"superjson": "^2.2.1",
|
||||||
@@ -11812,6 +11814,33 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ics": {
|
||||||
|
"version": "3.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ics/-/ics-3.8.1.tgz",
|
||||||
|
"integrity": "sha512-UqQlfkajfhrS4pUGQfGIJMYz/Jsl/ob3LqcfEhUmLbwumg+ZNkU0/6S734Vsjq3/FYNpEcZVKodLBoe+zBM69g==",
|
||||||
|
"dependencies": {
|
||||||
|
"nanoid": "^3.1.23",
|
||||||
|
"runes2": "^1.1.2",
|
||||||
|
"yup": "^1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ics/node_modules/nanoid": {
|
||||||
|
"version": "3.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||||
|
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/icss-utils": {
|
"node_modules/icss-utils": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
|
||||||
@@ -16875,6 +16904,11 @@
|
|||||||
"react-is": "^16.13.1"
|
"react-is": "^16.13.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/property-expr": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
|
||||||
|
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA=="
|
||||||
|
},
|
||||||
"node_modules/proto-list": {
|
"node_modules/proto-list": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
||||||
@@ -17436,6 +17470,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-to-print": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-to-print/-/react-to-print-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-FS/Z4LLq0bgWaxd7obygFQ8yRFdKW74iE8fIVjFFsPJWIXmuL8CIO+4me1Hj44lrlxQ00gscSNb3BRM8olbwXg==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ~19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readable-stream": {
|
"node_modules/readable-stream": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||||
@@ -17894,6 +17936,11 @@
|
|||||||
"queue-microtask": "^1.2.2"
|
"queue-microtask": "^1.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/runes2": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/runes2/-/runes2-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-LNPnEDPOOU4ehF71m5JoQyzT2yxwD6ZreFJ7MxZUAoMKNMY1XrAo60H1CUoX5ncSm0rIuKlqn9JZNRrRkNou2g=="
|
||||||
|
},
|
||||||
"node_modules/rxjs": {
|
"node_modules/rxjs": {
|
||||||
"version": "7.8.1",
|
"version": "7.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||||
@@ -19083,6 +19130,11 @@
|
|||||||
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
|
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-case": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="
|
||||||
|
},
|
||||||
"node_modules/tmp": {
|
"node_modules/tmp": {
|
||||||
"version": "0.2.3",
|
"version": "0.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
|
||||||
@@ -19138,6 +19190,11 @@
|
|||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/toposort": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="
|
||||||
|
},
|
||||||
"node_modules/tough-cookie": {
|
"node_modules/tough-cookie": {
|
||||||
"version": "4.1.3",
|
"version": "4.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
|
||||||
@@ -20393,6 +20450,28 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yup": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==",
|
||||||
|
"dependencies": {
|
||||||
|
"property-expr": "^2.0.5",
|
||||||
|
"tiny-case": "^1.0.3",
|
||||||
|
"toposort": "^2.0.2",
|
||||||
|
"type-fest": "^2.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yup/node_modules/type-fest": {
|
||||||
|
"version": "2.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
|
||||||
|
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "3.22.4",
|
"version": "3.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
"graphql": "^16.8.1",
|
"graphql": "^16.8.1",
|
||||||
"graphql-request": "^6.1.0",
|
"graphql-request": "^6.1.0",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
|
"ics": "^3.8.1",
|
||||||
"immer": "10.1.1",
|
"immer": "10.1.1",
|
||||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||||
"libphonenumber-js": "^1.10.60",
|
"libphonenumber-js": "^1.10.60",
|
||||||
@@ -69,6 +70,7 @@
|
|||||||
"react-hook-form": "^7.51.2",
|
"react-hook-form": "^7.51.2",
|
||||||
"react-international-phone": "^4.2.6",
|
"react-international-phone": "^4.2.6",
|
||||||
"react-intl": "^6.6.8",
|
"react-intl": "^6.6.8",
|
||||||
|
"react-to-print": "^3.0.2",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"sonner": "^1.7.0",
|
"sonner": "^1.7.0",
|
||||||
"superjson": "^2.2.1",
|
"superjson": "^2.2.1",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { ChildBedTypeEnum } from "@/constants/booking"
|
import { ChildBedTypeEnum } from "@/constants/booking"
|
||||||
|
import { Lang, langToApiLang } from "@/constants/languages"
|
||||||
|
|
||||||
const signupSchema = z.discriminatedUnion("becomeMember", [
|
const signupSchema = z.discriminatedUnion("becomeMember", [
|
||||||
z.object({
|
z.object({
|
||||||
@@ -81,6 +82,7 @@ export const createBookingInput = z.object({
|
|||||||
checkOutDate: z.string(),
|
checkOutDate: z.string(),
|
||||||
rooms: roomsSchema,
|
rooms: roomsSchema,
|
||||||
payment: paymentSchema,
|
payment: paymentSchema,
|
||||||
|
language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const priceChangeInput = z.object({
|
export const priceChangeInput = z.object({
|
||||||
|
|||||||
@@ -47,16 +47,18 @@ export const bookingMutationRouter = router({
|
|||||||
.input(createBookingInput)
|
.input(createBookingInput)
|
||||||
.mutation(async function ({ ctx, input }) {
|
.mutation(async function ({ ctx, input }) {
|
||||||
const accessToken = ctx.session?.token.access_token ?? ctx.serviceToken
|
const accessToken = ctx.session?.token.access_token ?? ctx.serviceToken
|
||||||
const { checkInDate, checkOutDate, hotelId } = input
|
const { language, ...inputWithoutLang } = input
|
||||||
|
const { hotelId, checkInDate, checkOutDate } = inputWithoutLang
|
||||||
|
|
||||||
const loggingAttributes = {
|
const loggingAttributes = {
|
||||||
membershipNumber: await getMembershipNumber(ctx.session),
|
membershipNumber: await getMembershipNumber(ctx.session),
|
||||||
checkInDate,
|
checkInDate,
|
||||||
checkOutDate,
|
checkOutDate,
|
||||||
hotelId,
|
hotelId,
|
||||||
|
language,
|
||||||
}
|
}
|
||||||
|
|
||||||
createBookingCounter.add(1, { hotelId, checkInDate, checkOutDate })
|
createBookingCounter.add(1, loggingAttributes)
|
||||||
|
|
||||||
console.info(
|
console.info(
|
||||||
"api.booking.create start",
|
"api.booking.create start",
|
||||||
@@ -68,10 +70,14 @@ export const bookingMutationRouter = router({
|
|||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiResponse = await api.post(api.endpoints.v1.Booking.bookings, {
|
const apiResponse = await api.post(
|
||||||
headers,
|
api.endpoints.v1.Booking.bookings,
|
||||||
body: input,
|
{
|
||||||
})
|
headers,
|
||||||
|
body: inputWithoutLang,
|
||||||
|
},
|
||||||
|
{ language }
|
||||||
|
)
|
||||||
|
|
||||||
if (!apiResponse.ok) {
|
if (!apiResponse.ok) {
|
||||||
const text = await apiResponse.text()
|
const text = await apiResponse.text()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { router, serviceProcedure } from "@/server/trpc"
|
|||||||
import { getHotelData } from "../hotels/query"
|
import { getHotelData } from "../hotels/query"
|
||||||
import { bookingConfirmationInput, getBookingStatusInput } from "./input"
|
import { bookingConfirmationInput, getBookingStatusInput } from "./input"
|
||||||
import { bookingConfirmationSchema, createBookingSchema } from "./output"
|
import { bookingConfirmationSchema, createBookingSchema } from "./output"
|
||||||
|
import { getBookedHotelRoom } from "./utils"
|
||||||
|
|
||||||
const meter = metrics.getMeter("trpc.booking")
|
const meter = metrics.getMeter("trpc.booking")
|
||||||
const getBookingConfirmationCounter = meter.createCounter(
|
const getBookingConfirmationCounter = meter.createCounter(
|
||||||
@@ -144,6 +145,7 @@ export const bookingQueryRouter = router({
|
|||||||
...hotelData.data.attributes,
|
...hotelData.data.attributes,
|
||||||
included: hotelData.included,
|
included: hotelData.included,
|
||||||
},
|
},
|
||||||
|
room: getBookedHotelRoom(hotelData.included, booking.data.roomTypeCode),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
status: serviceProcedure.input(getBookingStatusInput).query(async function ({
|
status: serviceProcedure.input(getBookingStatusInput).query(async function ({
|
||||||
|
|||||||
27
server/routers/booking/utils.ts
Normal file
27
server/routers/booking/utils.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { RoomData } from "@/types/hotel"
|
||||||
|
import { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||||
|
|
||||||
|
export function getBookedHotelRoom(
|
||||||
|
rooms: RoomData[] | undefined,
|
||||||
|
roomTypeCode: BookingConfirmation["booking"]["roomTypeCode"]
|
||||||
|
) {
|
||||||
|
if (!rooms?.length || !roomTypeCode) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const room = rooms?.find((r) => {
|
||||||
|
return r.roomTypes.find((roomType) => roomType.code === roomTypeCode)
|
||||||
|
})
|
||||||
|
if (!room) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const bedType = room.roomTypes.find(
|
||||||
|
(roomType) => roomType.code === roomTypeCode
|
||||||
|
)
|
||||||
|
if (!bedType) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...room,
|
||||||
|
bedType,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -238,15 +238,18 @@ export const rewardQueryRouter = router({
|
|||||||
const nextCursor =
|
const nextCursor =
|
||||||
limit + cursor < rewardIds.length ? limit + cursor : undefined
|
limit + cursor < rewardIds.length ? limit + cursor : undefined
|
||||||
|
|
||||||
const surprisesIds = validatedApiRewards.data
|
const wrappedSurprisesIds = validatedApiRewards.data
|
||||||
.filter(
|
.filter(
|
||||||
({ type, rewardType }) =>
|
(reward) =>
|
||||||
type === "coupon" && rewardType === "Surprise"
|
reward.type === "coupon" &&
|
||||||
|
reward.rewardType === "Surprise" &&
|
||||||
|
"coupon" in reward &&
|
||||||
|
reward.coupon?.some(({ unwrapped }) => !unwrapped)
|
||||||
)
|
)
|
||||||
.map(({ rewardId }) => rewardId)
|
.map(({ rewardId }) => rewardId)
|
||||||
|
|
||||||
const rewards = cmsRewards.filter(
|
const rewards = cmsRewards.filter(
|
||||||
(reward) => !surprisesIds.includes(reward.reward_id)
|
(reward) => !wrappedSurprisesIds.includes(reward.reward_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
getCurrentRewardSuccessCounter.add(1)
|
getCurrentRewardSuccessCounter.add(1)
|
||||||
|
|||||||
@@ -19,31 +19,33 @@ export const activitiesCardSchema = z.object({
|
|||||||
body_text: z.string(),
|
body_text: z.string(),
|
||||||
cta_text: z.string(),
|
cta_text: z.string(),
|
||||||
heading: z.string(),
|
heading: z.string(),
|
||||||
open_in_new_tab: z.boolean(),
|
|
||||||
scripted_title: z.string().optional(),
|
scripted_title: z.string().optional(),
|
||||||
hotel_page_activities_content_pageConnection: z.object({
|
hotel_page_activities_content_pageConnection: z.object({
|
||||||
edges: z.array(
|
edges: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
node: z.discriminatedUnion("__typename", [
|
node: z.discriminatedUnion("__typename", [
|
||||||
pageLinks.contentPageSchema,
|
pageLinks.contentPageSchema.extend({
|
||||||
|
header: z.object({
|
||||||
|
preamble: z.string(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.transform((data) => {
|
.transform((data) => {
|
||||||
let contentPage = { href: "" }
|
let contentPage = { href: "", preamble: "" }
|
||||||
if (data.hotel_page_activities_content_pageConnection.edges.length) {
|
if (data.hotel_page_activities_content_pageConnection.edges.length) {
|
||||||
const page =
|
const page =
|
||||||
data.hotel_page_activities_content_pageConnection.edges[0].node
|
data.hotel_page_activities_content_pageConnection.edges[0].node
|
||||||
|
contentPage.preamble = page.header.preamble
|
||||||
if (page.web.original_url) {
|
if (page.web.original_url) {
|
||||||
contentPage = {
|
contentPage.href = page.web.original_url
|
||||||
href: page.web.original_url,
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
contentPage = {
|
contentPage.href = removeMultipleSlashes(
|
||||||
href: removeMultipleSlashes(`/${page.system.locale}/${page.url}`),
|
`/${page.system.locale}/${page.url}`
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -52,7 +54,6 @@ export const activitiesCardSchema = z.object({
|
|||||||
contentPage,
|
contentPage,
|
||||||
ctaText: data.cta_text,
|
ctaText: data.cta_text,
|
||||||
heading: data.heading,
|
heading: data.heading,
|
||||||
openInNewTab: !!data.open_in_new_tab,
|
|
||||||
scriptedTopTitle: data.scripted_title,
|
scriptedTopTitle: data.scripted_title,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -298,6 +298,7 @@ export const parkingSchema = z.object({
|
|||||||
numberOfChargingSpaces: z.number().optional(),
|
numberOfChargingSpaces: z.number().optional(),
|
||||||
distanceToHotel: z.number().optional(),
|
distanceToHotel: z.number().optional(),
|
||||||
canMakeReservation: z.boolean(),
|
canMakeReservation: z.boolean(),
|
||||||
|
externalParkingUrl: z.string().optional(),
|
||||||
pricing: parkingPricingSchema,
|
pricing: parkingPricingSchema,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -77,9 +77,9 @@ export function subtract(...nums: (number | string | undefined)[]) {
|
|||||||
export function getInitialRoomPrice(roomRate: RoomRate, isMember: boolean) {
|
export function getInitialRoomPrice(roomRate: RoomRate, isMember: boolean) {
|
||||||
if (isMember && roomRate.memberRate) {
|
if (isMember && roomRate.memberRate) {
|
||||||
return {
|
return {
|
||||||
euro: {
|
requested: roomRate.memberRate.requestedPrice && {
|
||||||
currency: CurrencyEnum.EUR,
|
currency: roomRate.memberRate.requestedPrice.currency,
|
||||||
price: roomRate.memberRate.requestedPrice?.pricePerStay ?? 0,
|
price: roomRate.memberRate.requestedPrice.pricePerStay,
|
||||||
},
|
},
|
||||||
local: {
|
local: {
|
||||||
currency: roomRate.memberRate.localPrice.currency,
|
currency: roomRate.memberRate.localPrice.currency,
|
||||||
@@ -89,9 +89,9 @@ export function getInitialRoomPrice(roomRate: RoomRate, isMember: boolean) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
euro: {
|
requested: roomRate.publicRate.requestedPrice && {
|
||||||
currency: CurrencyEnum.EUR,
|
currency: roomRate.publicRate.requestedPrice.currency,
|
||||||
price: roomRate.publicRate.requestedPrice?.pricePerStay ?? 0,
|
price: roomRate.publicRate.requestedPrice.pricePerStay,
|
||||||
},
|
},
|
||||||
local: {
|
local: {
|
||||||
currency: roomRate.publicRate.localPrice.currency,
|
currency: roomRate.publicRate.localPrice.currency,
|
||||||
@@ -103,9 +103,9 @@ export function getInitialRoomPrice(roomRate: RoomRate, isMember: boolean) {
|
|||||||
export function getInitialTotalPrice(roomRate: RoomRate, isMember: boolean) {
|
export function getInitialTotalPrice(roomRate: RoomRate, isMember: boolean) {
|
||||||
if (isMember && roomRate.memberRate) {
|
if (isMember && roomRate.memberRate) {
|
||||||
return {
|
return {
|
||||||
euro: {
|
requested: roomRate.memberRate.requestedPrice && {
|
||||||
currency: CurrencyEnum.EUR,
|
currency: roomRate.memberRate.requestedPrice.currency,
|
||||||
price: roomRate.memberRate.requestedPrice?.pricePerStay ?? 0,
|
price: roomRate.memberRate.requestedPrice.pricePerStay,
|
||||||
},
|
},
|
||||||
local: {
|
local: {
|
||||||
currency: roomRate.memberRate.localPrice.currency,
|
currency: roomRate.memberRate.localPrice.currency,
|
||||||
@@ -115,9 +115,9 @@ export function getInitialTotalPrice(roomRate: RoomRate, isMember: boolean) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
euro: {
|
requested: roomRate.publicRate.requestedPrice && {
|
||||||
currency: CurrencyEnum.EUR,
|
currency: roomRate.publicRate.requestedPrice.currency,
|
||||||
price: roomRate.publicRate.requestedPrice?.pricePerStay ?? 0,
|
price: roomRate.publicRate.requestedPrice.pricePerStay,
|
||||||
},
|
},
|
||||||
local: {
|
local: {
|
||||||
currency: roomRate.publicRate.localPrice.currency,
|
currency: roomRate.publicRate.localPrice.currency,
|
||||||
@@ -165,31 +165,31 @@ export function calcTotalPrice(
|
|||||||
totalPrice: state.totalPrice,
|
totalPrice: state.totalPrice,
|
||||||
}
|
}
|
||||||
if (state.requestedPrice?.pricePerStay) {
|
if (state.requestedPrice?.pricePerStay) {
|
||||||
roomAndTotalPrice.roomPrice.euro = {
|
roomAndTotalPrice.roomPrice.requested = {
|
||||||
currency: CurrencyEnum.EUR,
|
currency: state.requestedPrice.currency,
|
||||||
price: state.requestedPrice.pricePerStay,
|
price: state.requestedPrice.pricePerStay,
|
||||||
}
|
}
|
||||||
|
|
||||||
let totalPriceEuro = state.requestedPrice.pricePerStay
|
let totalPriceRequested = state.requestedPrice.pricePerStay
|
||||||
if (state.breakfast) {
|
if (state.breakfast) {
|
||||||
totalPriceEuro = add(
|
totalPriceRequested = add(
|
||||||
totalPriceEuro,
|
totalPriceRequested,
|
||||||
state.breakfast.requestedPrice.totalPrice
|
state.breakfast.requestedPrice.totalPrice
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.packages) {
|
if (state.packages) {
|
||||||
totalPriceEuro = state.packages.reduce((total, pkg) => {
|
totalPriceRequested = state.packages.reduce((total, pkg) => {
|
||||||
if (pkg.requestedPrice.totalPrice) {
|
if (pkg.requestedPrice.totalPrice) {
|
||||||
total = add(total, pkg.requestedPrice.totalPrice)
|
total = add(total, pkg.requestedPrice.totalPrice)
|
||||||
}
|
}
|
||||||
return total
|
return total
|
||||||
}, totalPriceEuro)
|
}, totalPriceRequested)
|
||||||
}
|
}
|
||||||
|
|
||||||
roomAndTotalPrice.totalPrice.euro = {
|
roomAndTotalPrice.totalPrice.requested = {
|
||||||
currency: CurrencyEnum.EUR,
|
currency: state.requestedPrice.currency,
|
||||||
price: totalPriceEuro,
|
price: totalPriceRequested,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,10 +115,12 @@ export function createDetailsStore(
|
|||||||
|
|
||||||
if (initialState.packages) {
|
if (initialState.packages) {
|
||||||
initialState.packages.forEach((pkg) => {
|
initialState.packages.forEach((pkg) => {
|
||||||
initialTotalPrice.euro.price = add(
|
if (initialTotalPrice.requested) {
|
||||||
initialTotalPrice.euro.price,
|
initialTotalPrice.requested.price = add(
|
||||||
pkg.requestedPrice.totalPrice
|
initialTotalPrice.requested.price,
|
||||||
)
|
pkg.requestedPrice.totalPrice
|
||||||
|
)
|
||||||
|
}
|
||||||
initialTotalPrice.local.price = add(
|
initialTotalPrice.local.price = add(
|
||||||
initialTotalPrice.local.price,
|
initialTotalPrice.local.price,
|
||||||
pkg.localPrice.totalPrice
|
pkg.localPrice.totalPrice
|
||||||
@@ -165,7 +167,7 @@ export function createDetailsStore(
|
|||||||
setTotalPrice(totalPrice) {
|
setTotalPrice(totalPrice) {
|
||||||
return set(
|
return set(
|
||||||
produce((state: DetailsState) => {
|
produce((state: DetailsState) => {
|
||||||
state.totalPrice.euro = totalPrice.euro
|
state.totalPrice.requested = totalPrice.requested
|
||||||
state.totalPrice.local = totalPrice.local
|
state.totalPrice.local = totalPrice.local
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -194,7 +196,8 @@ export function createDetailsStore(
|
|||||||
return set(
|
return set(
|
||||||
produce((state: DetailsState) => {
|
produce((state: DetailsState) => {
|
||||||
state.isValid.breakfast = true
|
state.isValid.breakfast = true
|
||||||
const stateTotalEuroPrice = state.totalPrice.euro?.price || 0
|
const stateTotalRequestedPrice =
|
||||||
|
state.totalPrice.requested?.price || 0
|
||||||
const stateTotalLocalPrice = state.totalPrice.local.price
|
const stateTotalLocalPrice = state.totalPrice.local.price
|
||||||
|
|
||||||
const addToTotalPrice =
|
const addToTotalPrice =
|
||||||
@@ -206,7 +209,7 @@ export function createDetailsStore(
|
|||||||
breakfast === false
|
breakfast === false
|
||||||
|
|
||||||
if (addToTotalPrice) {
|
if (addToTotalPrice) {
|
||||||
const breakfastTotalEuroPrice = parseInt(
|
const breakfastTotalRequestedPrice = parseInt(
|
||||||
breakfast.requestedPrice.totalPrice
|
breakfast.requestedPrice.totalPrice
|
||||||
)
|
)
|
||||||
const breakfastTotalPrice = parseInt(
|
const breakfastTotalPrice = parseInt(
|
||||||
@@ -214,9 +217,10 @@ export function createDetailsStore(
|
|||||||
)
|
)
|
||||||
|
|
||||||
state.totalPrice = {
|
state.totalPrice = {
|
||||||
euro: {
|
requested: state.totalPrice.requested && {
|
||||||
currency: CurrencyEnum.EUR,
|
currency: state.totalPrice.requested.currency,
|
||||||
price: stateTotalEuroPrice + breakfastTotalEuroPrice,
|
price:
|
||||||
|
stateTotalRequestedPrice + breakfastTotalRequestedPrice,
|
||||||
},
|
},
|
||||||
local: {
|
local: {
|
||||||
currency: breakfast.localPrice.currency,
|
currency: breakfast.localPrice.currency,
|
||||||
@@ -229,21 +233,22 @@ export function createDetailsStore(
|
|||||||
let currency =
|
let currency =
|
||||||
state.totalPrice.local.currency ?? langToCurrency()
|
state.totalPrice.local.currency ?? langToCurrency()
|
||||||
let currentBreakfastTotalPrice = 0
|
let currentBreakfastTotalPrice = 0
|
||||||
let currentBreakfastTotalEuroPrice = 0
|
let currentBreakfastTotalRequestedPrice = 0
|
||||||
if (state.breakfast) {
|
if (state.breakfast) {
|
||||||
currentBreakfastTotalPrice = parseInt(
|
currentBreakfastTotalPrice = parseInt(
|
||||||
state.breakfast.localPrice.totalPrice
|
state.breakfast.localPrice.totalPrice
|
||||||
)
|
)
|
||||||
currentBreakfastTotalEuroPrice = parseInt(
|
currentBreakfastTotalRequestedPrice = parseInt(
|
||||||
state.breakfast.requestedPrice.totalPrice
|
state.breakfast.requestedPrice.totalPrice
|
||||||
)
|
)
|
||||||
currency = state.breakfast.localPrice.currency
|
currency = state.breakfast.localPrice.currency
|
||||||
}
|
}
|
||||||
|
|
||||||
let euroPrice =
|
let requestedPrice =
|
||||||
stateTotalEuroPrice - currentBreakfastTotalEuroPrice
|
stateTotalRequestedPrice -
|
||||||
if (euroPrice < 0) {
|
currentBreakfastTotalRequestedPrice
|
||||||
euroPrice = 0
|
if (requestedPrice < 0) {
|
||||||
|
requestedPrice = 0
|
||||||
}
|
}
|
||||||
let localPrice =
|
let localPrice =
|
||||||
stateTotalLocalPrice - currentBreakfastTotalPrice
|
stateTotalLocalPrice - currentBreakfastTotalPrice
|
||||||
@@ -252,9 +257,9 @@ export function createDetailsStore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.totalPrice = {
|
state.totalPrice = {
|
||||||
euro: {
|
requested: state.totalPrice.requested && {
|
||||||
currency: CurrencyEnum.EUR,
|
currency: state.totalPrice.requested.currency,
|
||||||
price: euroPrice,
|
price: requestedPrice,
|
||||||
},
|
},
|
||||||
local: {
|
local: {
|
||||||
currency,
|
currency,
|
||||||
@@ -349,8 +354,15 @@ export function createDetailsStore(
|
|||||||
persistedState.booking,
|
persistedState.booking,
|
||||||
currentState.booking
|
currentState.booking
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!isSameBooking) {
|
if (!isSameBooking) {
|
||||||
return deepmerge(persistedState, currentState, { arrayMerge })
|
// We get the booking data from query params, and the "newest" booking data should always be used.
|
||||||
|
// Merging the two states can lead to issues since some params or values in arrays might be removed.
|
||||||
|
// @ts-expect-error - persistedState cannot be typed
|
||||||
|
delete persistedState.booking
|
||||||
|
return deepmerge(persistedState, currentState, {
|
||||||
|
arrayMerge,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return deepmerge(currentState, persistedState ?? {}, { arrayMerge })
|
return deepmerge(currentState, persistedState ?? {}, { arrayMerge })
|
||||||
|
|||||||
6
types/components/hotelPage/sidepeek/activities.ts
Normal file
6
types/components/hotelPage/sidepeek/activities.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export type ActivitiesSidePeekProps = {
|
||||||
|
contentPage: {
|
||||||
|
href: string
|
||||||
|
preamble: string
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { Hotel } from "@/types/hotel"
|
||||||
|
|
||||||
|
export type MeetingsAndConferencesSidePeekProps = {
|
||||||
|
meetingFacilities: Hotel["conferencesAndMeetings"]
|
||||||
|
descriptions: Hotel["hotelContent"]["texts"]["meetingDescription"]
|
||||||
|
link?: string
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ export enum Periods {
|
|||||||
|
|
||||||
export type ParkingAmenityProps = {
|
export type ParkingAmenityProps = {
|
||||||
parking: Hotel["parking"]
|
parking: Hotel["parking"]
|
||||||
|
hasParkingPage?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ParkingListProps = {
|
export type ParkingListProps = {
|
||||||
@@ -19,7 +20,7 @@ export type ParkingListProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ParkingPricesProps = {
|
export type ParkingPricesProps = {
|
||||||
data: Hotel["parking"][number]["pricing"]["localCurrency"]["ordinary"]
|
pricing: Hotel["parking"][number]["pricing"]["localCurrency"]["ordinary"]
|
||||||
currency: Hotel["parking"][number]["pricing"]["localCurrency"]["currency"]
|
currency: Hotel["parking"][number]["pricing"]["localCurrency"]["currency"]
|
||||||
freeParking: Hotel["parking"][number]["pricing"]["freeParking"]
|
freeParking: Hotel["parking"][number]["pricing"]["freeParking"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { EventAttributes } from "ics"
|
||||||
|
|
||||||
|
import type { RouterOutput } from "@/lib/trpc/client"
|
||||||
|
|
||||||
|
export interface AddToCalendarProps {
|
||||||
|
checkInDate: RouterOutput["booking"]["confirmation"]["booking"]["checkInDate"]
|
||||||
|
event: EventAttributes
|
||||||
|
hotelName: RouterOutput["booking"]["confirmation"]["hotel"]["name"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import type { MutableRefObject } from "react"
|
||||||
|
|
||||||
|
export interface DownloadInvoiceProps {
|
||||||
|
mainRef: MutableRefObject<HTMLElement | null>
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { RouterOutput } from "@/lib/trpc/client"
|
||||||
|
|
||||||
export interface BookingConfirmationProps {
|
export interface BookingConfirmationProps {
|
||||||
confirmationNumber: string
|
bookingConfirmationPromise: Promise<RouterOutput["booking"]["confirmation"]>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import type { MutableRefObject } from "react"
|
||||||
|
|
||||||
|
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||||
|
|
||||||
|
export interface BookingConfirmationHeaderProps
|
||||||
|
extends Pick<BookingConfirmation, "booking" | "hotel"> {
|
||||||
|
mainRef: MutableRefObject<HTMLElement | null>
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user