Merged master into fix/SW-1107-alert-on-current-header

This commit is contained in:
Linus Flood
2024-12-05 13:25:18 +00:00
92 changed files with 940 additions and 442 deletions

View File

@@ -20,7 +20,7 @@ export default function ActivitiesCardGrid(activitiesCard: ActivityCard) {
theme: hasImage ? "image" : "primaryDark",
primaryButton: hasImage
? {
href: activitiesCard.contentPage.href,
href: `?s=${activities[lang]}`,
title: activitiesCard.ctaText,
isExternal: false,
}
@@ -28,7 +28,7 @@ export default function ActivitiesCardGrid(activitiesCard: ActivityCard) {
secondaryButton: hasImage
? undefined
: {
href: activitiesCard.contentPage.href,
href: `?s=${activities[lang]}`,
title: activitiesCard.ctaText,
isExternal: false,
},

View File

@@ -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;
}

View File

@@ -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>
)
}

View File

@@ -9,7 +9,7 @@ import {
} from "@/types/components/hotelPage/sidepeek/parking"
export default async function ParkingPrices({
data,
pricing,
currency,
freeParking,
}: 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 (
<div className={styles.wrapper}>

View File

@@ -1,5 +1,8 @@
import { ExternalLinkIcon } from "@/components/Icons"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
import Button from "@/components/TempDesignSystem/Button"
import Divider from "@/components/TempDesignSystem/Divider"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { getIntl } from "@/i18n"
@@ -12,7 +15,10 @@ import styles from "./parkingAmenity.module.css"
import type { ParkingAmenityProps } from "@/types/components/hotelPage/sidepeek/parking"
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()
return (
@@ -43,7 +49,7 @@ export default async function ParkingAmenity({ parking }: ParkingAmenityProps) {
</Caption>
<Divider color="baseSurfaceSubtleHover" />
<ParkingPrices
data={data.pricing.localCurrency.ordinary}
pricing={data.pricing.localCurrency.ordinary}
currency={data.pricing.localCurrency.currency}
freeParking={data.pricing.freeParking}
/>
@@ -54,15 +60,41 @@ export default async function ParkingAmenity({ parking }: ParkingAmenityProps) {
</Caption>
<Divider color="baseSurfaceSubtleHover" />
<ParkingPrices
data={data.pricing.localCurrency.weekend}
pricing={data.pricing.localCurrency.weekend}
currency={data.pricing.localCurrency.currency}
freeParking={data.pricing.freeParking}
/>
</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>
{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>
)
}

View File

@@ -1,9 +1,9 @@
.wrapper {
.wrapper,
.information {
display: grid;
gap: var(--Spacing-x3);
}
.information,
.list,
.prices {
display: grid;
@@ -18,3 +18,7 @@
display: grid;
gap: var(--Spacing-x1);
}
.parkingPageLink {
margin-top: var(--Spacing-x2);
}

View File

@@ -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>
)
}

View File

@@ -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;
}
}

View File

@@ -1,4 +1,6 @@
export { default as AboutTheHotelSidePeek } from "./AboutTheHotel"
export { default as ActivitiesSidePeek } from "./Activities"
export { default as AmenitiesSidePeek } from "./Amenities"
export { default as MeetingsAndConferencesSidePeek } from "./MeetingsAndConferences"
export { default as RoomSidePeek } from "./Room"
export { default as WellnessAndExerciseSidePeek } from "./WellnessAndExercise"

View File

@@ -1,10 +1,6 @@
import { notFound } from "next/navigation"
import {
activities,
meetingsAndConferences,
restaurantAndBar,
} from "@/constants/routes/hotelPageParams"
import { restaurantAndBar } from "@/constants/routes/hotelPageParams"
import { env } from "@/env/server"
import { getHotelData, getHotelPage } from "@/lib/trpc/memoizedRequests"
@@ -29,7 +25,9 @@ import PreviewImages from "./PreviewImages"
import { Rooms } from "./Rooms"
import {
AboutTheHotelSidePeek,
ActivitiesSidePeek,
AmenitiesSidePeek,
MeetingsAndConferencesSidePeek,
RoomSidePeek,
WellnessAndExerciseSidePeek,
} from "./SidePeeks"
@@ -200,20 +198,13 @@ export default async function HotelPage({ hotelId }: HotelPageProps) {
Restaurant & Bar
</SidePeek>
<WellnessAndExerciseSidePeek healthFacilities={healthFacilities} />
<SidePeek
contentKey={activities[lang]}
title={intl.formatMessage({ id: "Activities" })}
>
{/* TODO */}
Activities
</SidePeek>
<SidePeek
contentKey={meetingsAndConferences[lang]}
title={intl.formatMessage({ id: "Meetings & Conferences" })}
>
{/* TODO */}
Meetings & Conferences
</SidePeek>
{activitiesCard && (
<ActivitiesSidePeek contentPage={activitiesCard.contentPage} />
)}
<MeetingsAndConferencesSidePeek
meetingFacilities={conferencesAndMeetings}
descriptions={hotelContent.texts.meetingDescription}
/>
{roomCategories.map((room) => (
<RoomSidePeek key={room.name} room={room} />
))}

View File

@@ -6,7 +6,7 @@
line-height: 20px;
border: 1px solid transparent;
border-radius: 50px;
height: 32px;
height: 38px;
line-height: 20px;
font-size: 14px;
font-family: Helvetica, Arial, sans-serif;
@@ -17,6 +17,7 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
align-content: center;
}
.button:hover {

View File

@@ -1,49 +1,13 @@
import { homeHrefs } from "@/constants/homeHrefs"
import { env } from "@/env/server"
import { getCurrentHeader } from "@/lib/trpc/memoizedRequests"
import { getLang } from "@/i18n/serverContext"
import { MainMenu } from "../MainMenu"
import OfflineBanner from "../OfflineBanner"
import TopMenu from "../TopMenu"
import { MainMenuSkeleton } from "../MainMenu"
import { TopMenuSkeleton } from "../TopMenu"
import styles from "../header.module.css"
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 (
<header className={styles.header} role="banner">
<OfflineBanner />
<TopMenu
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}
/>
<TopMenuSkeleton />
<MainMenuSkeleton />
</header>
)
}

View File

@@ -9,6 +9,7 @@ import useDropdownStore from "@/stores/main-menu"
import Image from "@/components/Image"
import LoginButton from "@/components/LoginButton"
import Avatar from "@/components/MyPages/Avatar"
import SkeletonShimmer from "@/components/SkeletonShimmer"
import Link from "@/components/TempDesignSystem/Link"
import useLang from "@/hooks/useLang"
import { trackClick } from "@/utils/tracking"
@@ -227,3 +228,56 @@ export function MainMenu({
</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>
)
}

View File

@@ -24,11 +24,9 @@
.navBar {
display: grid;
grid-template-columns: 1fr 80px 1fr;
grid-template-columns: auto auto 1fr auto;
grid-template-areas: "expanderBtn logoLink . buttonContainer";
grid-template-rows: 100%;
height: 100%;
padding: 0 var(--Spacing-x2);
}
.expanderBtn {
@@ -47,7 +45,7 @@
background: #757575;
border-radius: 2.3px;
display: inline-block;
height: 3px;
height: 5px;
position: relative;
transition: 0.3s;
width: 32px;
@@ -104,7 +102,6 @@
align-items: center;
height: 100%;
width: 80px;
padding-left: var(--Spacing-x1);
}
.logo {
@@ -238,6 +235,12 @@
display: none;
}
.skeletonWrapper {
padding: 4px 10px;
height: 100%;
align-content: center;
}
@media (min-width: 1367px) {
.navBar {
grid-template-columns: 140px auto 1fr;

View File

@@ -3,6 +3,7 @@ import { overview } from "@/constants/routes/myPages"
import { getName } from "@/lib/trpc/memoizedRequests"
import LoginButton from "@/components/LoginButton"
import SkeletonShimmer from "@/components/SkeletonShimmer"
import Link from "@/components/TempDesignSystem/Link"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
@@ -68,6 +69,8 @@ export default async function TopMenu({
position="hamburger menu"
trackingId="loginStartTopMenu"
className={`${styles.sessionLink} ${styles.loginLink}`}
variant="default"
size="small"
>
{intl.formatMessage({ id: "Log in" })}
</LoginButton>
@@ -78,3 +81,32 @@ export default async function TopMenu({
</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>
)
}

View File

@@ -49,6 +49,12 @@
display: block;
}
.skeletonWrapper {
padding: 4px 10px;
height: 30px;
align-content: center;
}
@media screen and (min-width: 768px) {
.container {
padding: 0 30px;

View File

@@ -159,13 +159,13 @@ td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton,
}
.weekDay {
color: var(--Base-Text-Medium-contrast);
font-family: var(--typography-Footnote-Labels-fontFamily);
font-size: var(--typography-Footnote-Labels-fontSize);
font-weight: var(--typography-Footnote-Labels-fontWeight);
letter-spacing: var(--typography-Footnote-Labels-letterSpacing);
line-height: var(--typography-Footnote-Labels-lineHeight);
text-decoration: var(--typography-Footnote-Labels-textDecoration);
color: var(--UI-Text-Placeholder);
font-family: var(--typography-Caption-Labels-fontFamily);
font-size: var(--typography-Caption-Labels-fontSize);
font-weight: var(--typography-Caption-Labels-fontWeight);
letter-spacing: var(--typography-Caption-Labels-letterSpacing);
line-height: var(--typography-Caption-Labels-lineHeight);
text-decoration: var(--typography-Caption-Labels-textDecoration);
text-transform: uppercase;
}

View File

@@ -56,6 +56,7 @@ function Logo({ alt }: { alt: string }) {
className={styles.logo}
height={22}
src="/_static/img/scandic-logotype.svg"
priority
width={103}
/>
</NextLink>

View File

@@ -1,14 +1,18 @@
"use client"
import { useIntl } from "react-intl"
import { useReactToPrint } from "react-to-print"
import { DownloadIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
export default function DownloadInvoice() {
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() {
window.print()
reactToPrintFn()
}
return (

View File

@@ -1,9 +1,9 @@
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
"use client"
import { useIntl } from "react-intl"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Title from "@/components/TempDesignSystem/Text/Title"
import { getIntl } from "@/i18n"
import AddToCalendar from "./Actions/AddToCalendar"
import DownloadInvoice from "./Actions/DownloadInvoice"
@@ -14,13 +14,14 @@ import styles from "./header.module.css"
import type { EventAttributes } from "ics"
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
import type { BookingConfirmationHeaderProps } from "@/types/components/hotelReservation/bookingConfirmation/header"
export default async function Header({
confirmationNumber,
}: BookingConfirmationProps) {
const intl = await getIntl()
const { booking, hotel } = await getBookingConfirmation(confirmationNumber)
export default function Header({
booking,
hotel,
mainRef,
}: BookingConfirmationHeaderProps) {
const intl = useIntl()
const text = intl.formatMessage<React.ReactNode>(
{ id: "booking.confirmation.text" },
@@ -70,7 +71,7 @@ export default async function Header({
hotelName={hotel.name}
/>
<ManageBooking />
<DownloadInvoice />
<DownloadInvoice mainRef={mainRef} />
</div>
</header>
)

View File

@@ -1,20 +1,19 @@
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
"use client"
import { useIntl } from "react-intl"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { Toast } from "@/components/TempDesignSystem/Toasts"
import { getIntl } from "@/i18n"
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({
confirmationNumber,
}: BookingConfirmationProps) {
const intl = await getIntl()
const { hotel } = await getBookingConfirmation(confirmationNumber)
export default function HotelDetails({
hotel,
}: BookingConfirmationHotelDetailsProps) {
const intl = useIntl()
return (
<div className={styles.container}>
<div className={styles.details}>

View File

@@ -1,23 +1,23 @@
"use client"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
import { CreditCardAddIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import Body from "@/components/TempDesignSystem/Text/Body"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import useLang from "@/hooks/useLang"
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({
confirmationNumber,
}: BookingConfirmationProps) {
const intl = await getIntl()
const lang = getLang()
const { booking } = await getBookingConfirmation(confirmationNumber)
export default function PaymentDetails({
booking,
}: BookingConfirmationPaymentDetailsProps) {
const intl = useIntl()
const lang = useLang()
return (
<div className={styles.details}>
<Subtitle color="uiTextHighContrast" type="two">

View File

@@ -1,11 +1,12 @@
import { getIntl } from "@/i18n"
"use client"
import { useIntl } from "react-intl"
import Promo from "./Promo"
import styles from "./promos.module.css"
export default async function Promos() {
const intl = await getIntl()
export default function Promos() {
const intl = useIntl()
return (
<div className={styles.promos}>
<Promo

View File

@@ -1,6 +1,6 @@
"use client"
import { notFound } from "next/navigation"
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
import { useIntl } from "react-intl"
import { ChevronRightSmallIcon, InfoCircleIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
@@ -9,21 +9,20 @@ import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { getIntl } from "@/i18n"
import { getBookedHotelRoom } from "@/utils/getBookedHotelRoom"
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"
export default async function Receipt({
confirmationNumber,
}: BookingConfirmationProps) {
const intl = await getIntl()
const { booking, hotel } = await getBookingConfirmation(confirmationNumber)
const roomAndBed = getBookedHotelRoom(hotel, booking.roomTypeCode ?? "")
if (!roomAndBed) {
export default function Receipt({
booking,
hotel,
room,
}: BookingConfirmationReceiptProps) {
const intl = useIntl()
if (!room) {
return notFound()
}
@@ -38,7 +37,7 @@ export default async function Receipt({
<Subtitle type="two">{intl.formatMessage({ id: "Summary" })}</Subtitle>
<article className={styles.room}>
<header className={styles.roomHeader}>
<Body color="uiTextHighContrast">{roomAndBed.name}</Body>
<Body color="uiTextHighContrast">{room.name}</Body>
{booking.rateDefinition.isMemberRate ? (
<div className={styles.memberPrice}>
<Body color="uiTextPlaceholder">
@@ -82,9 +81,7 @@ export default async function Receipt({
</Link>
</header>
<div className={styles.entry}>
<Body color="uiTextHighContrast">
{roomAndBed.bedType.description}
</Body>
<Body color="uiTextHighContrast">{room.bedType.description}</Body>
<Body color="uiTextHighContrast">
{intl.formatNumber(0, {
currency: booking.currencyCode,

View File

@@ -1,3 +1,6 @@
"use client"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import {
@@ -10,16 +13,15 @@ import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import useLang from "@/hooks/useLang"
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) {
const intl = await getIntl()
const lang = getLang()
export default function Room({ booking, img, roomName }: RoomProps) {
const intl = useIntl()
const lang = useLang()
const fromDate = dt(booking.checkInDate).locale(lang)
const toDate = dt(booking.checkOutDate).locale(lang)

View File

@@ -1,30 +1,23 @@
"use client"
import { notFound } from "next/navigation"
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
import { getBookedHotelRoom } from "@/utils/getBookedHotelRoom"
import Room from "./Room"
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({
confirmationNumber,
}: BookingConfirmationProps) {
const { booking, hotel } = await getBookingConfirmation(confirmationNumber)
const roomAndBed = getBookedHotelRoom(hotel, booking.roomTypeCode ?? "")
if (!roomAndBed) {
export default function Rooms({
booking,
room,
}: BookingConfirmationRoomsProps) {
if (!room) {
return notFound()
}
return (
<section className={styles.rooms}>
<Room
booking={booking}
img={roomAndBed.images[0]}
roomName={roomAndBed.name}
/>
<Room booking={booking} img={room.images[0]} roomName={room.name} />
</section>
)
}

View File

@@ -0,0 +1,42 @@
.main {
background-color: var(--Base-Surface-Primary-light-Normal);
display: grid;
gap: var(--Spacing-x5);
grid-template-areas: "header" "booking";
margin: 0 auto;
min-height: 100dvh;
padding-top: var(--Spacing-x5);
width: var(--max-width-page);
}
.booking {
display: flex;
flex-direction: column;
gap: var(--Spacing-x5);
grid-area: booking;
padding-bottom: var(--Spacing-x9);
}
.aside {
display: none;
}
@media screen and (min-width: 1367px) {
.main {
grid-template-areas:
"header receipt"
"booking receipt";
grid-template-columns: 1fr 340px;
grid-template-rows: auto 1fr;
padding-top: var(--Spacing-x9);
}
.mobileReceipt {
display: none;
}
.aside {
display: grid;
grid-area: receipt;
}
}

View 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>
)
}

View File

@@ -4,6 +4,7 @@
grid-template-rows: auto;
gap: var(--Spacing-x2);
font-family: var(--typography-Body-Regular-fontFamily);
margin-bottom: var(--Spacing-x3);
}
.address,
@@ -20,6 +21,7 @@
list-style-type: none;
display: flex;
flex-direction: column;
min-width: 0;
}
.soMeIcons {
@@ -28,6 +30,19 @@
}
.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;
align-items: center;
column-gap: var(--Spacing-x-one-and-half);
@@ -38,10 +53,6 @@
margin-bottom: var(--Spacing-x1);
}
.ecoLabel img {
flex-shrink: 0;
}
.ecoLabelText {
display: flex;
color: var(--UI-Text-Medium-contrast);
@@ -49,8 +60,8 @@
justify-content: center;
}
.googleMaps {
text-decoration: none;
.link {
text-decoration: underline;
font-family: var(--typography-Body-Regular-fontFamily);
color: var(--Base-Text-Medium-contrast);
color: var(--Base-Text-High-contrast);
}

View File

@@ -24,31 +24,27 @@ export default function Contact({ hotel }: ContactProps) {
<Body textTransform="bold">
{intl.formatMessage({ id: "Address" })}
</Body>
<Body>
{`${hotel.address.streetAddress}, ${hotel.address.city}`}
</Body>
<Body>{`${hotel.address.streetAddress}, `}</Body>
<Body>{hotel.address.city}</Body>
</li>
<li>
<Body textTransform="bold">
{intl.formatMessage({ id: "Driving directions" })}
</Body>
<a
<Link
href={`https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}`}
className={styles.googleMaps}
target="_blank"
>
Google Maps
</a>
<span className={styles.link}>Google Maps</span>
</Link>
</li>
<li>
<Body textTransform="bold">
{intl.formatMessage({ id: "Contact us" })}
</Body>
<Link
href={`tel:${hotel.contactInformation.phoneNumber}`}
color="peach80"
>
{hotel.contactInformation.phoneNumber}
<Link href={`tel:${hotel.contactInformation.phoneNumber}`}>
<span className={styles.link}>
{hotel.contactInformation.phoneNumber}
</span>
</Link>
</li>
<li>
@@ -76,23 +72,24 @@ export default function Contact({ hotel }: ContactProps) {
<Body textTransform="bold">
{intl.formatMessage({ id: "Email" })}
</Body>
<Link
href={`mailto:${hotel.contactInformation.email}`}
color="peach80"
>
{hotel.contactInformation.email}
<Link href={`mailto:${hotel.contactInformation.email}`}>
<span className={styles.link}>
{hotel.contactInformation.email}
</span>
</Link>
</li>
</ul>
</address>
{hotel.hotelFacts.ecoLabels?.nordicEcoLabel ? (
<div className={styles.ecoLabel}>
<Image
height={38}
width={43}
alt={intl.formatMessage({ id: "Nordic Swan Ecolabel" })}
src={`/_static/img/icons/swan-eco/swan_eco_dark_${lang}.png`}
/>
<div className={styles.ecoContainer}>
<div className={styles.ecoLabel}>
<Image
height={38}
width={38}
alt={intl.formatMessage({ id: "Nordic Swan Ecolabel" })}
src={`/_static/img/icons/swan-eco/swan_eco_dark_${lang}.png`}
/>
</div>
<div className={styles.ecoLabelText}>
<span>{intl.formatMessage({ id: "Nordic Swan Ecolabel" })}</span>
<span>

View File

@@ -1,7 +1,7 @@
"use client"
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 { useIntl } from "react-intl"
import { useMediaQuery } from "usehooks-ts"
@@ -27,7 +27,8 @@ export default function RoomFilter({
onFilter,
filterOptions,
}: RoomFilterProps) {
const isAboveMobile = useMediaQuery("(min-width: 768px)")
const isTabletAndUp = useMediaQuery("(min-width: 768px)")
const [isAboveMobile, setIsAboveMobile] = useState(false)
const initialFilterValues = useMemo(
() =>
@@ -71,6 +72,10 @@ export default function RoomFilter({
return () => subscription.unsubscribe()
}, [handleSubmit, watch, submitFilter])
useEffect(() => {
setIsAboveMobile(isTabletAndUp)
}, [isTabletAndUp])
return (
<div className={styles.container}>
<div className={styles.infoDesktop}>

View File

@@ -18,9 +18,7 @@ export default function FlexibilityOption({
name,
paymentTerm,
priceInformation,
roomType,
roomTypeCode,
features,
petRoomPackage,
handleSelectRate,
}: FlexibilityOptionProps) {
@@ -45,10 +43,22 @@ export default function FlexibilityOption({
const { public: publicPrice, member: memberPrice } = product.productType
function onChange() {
handleSelectRate({
publicRateCode: publicPrice.rateCode,
roomTypeCode: roomTypeCode,
const onClick: React.MouseEventHandler<HTMLInputElement> = (e) => {
handleSelectRate((prev) => {
if (
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"
name="rateCode"
value={publicPrice?.rateCode}
onChange={onChange}
onClick={onClick}
/>
<div className={styles.card}>
<div className={styles.header}>

View File

@@ -34,6 +34,7 @@ export default function RateSummary({
features,
roomType,
priceName,
priceTerm,
} = rateSummary
const priceToShow = isUserLoggedIn && member ? member : publicRate
@@ -80,87 +81,93 @@ export default function RateSummary({
</Footnote>
</div>
)}
<div className={styles.summaryText}>
<Subtitle color="uiTextHighContrast">{roomType}</Subtitle>
<Body color="uiTextMediumContrast">{priceName}</Body>
</div>
<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 className={styles.content}>
<div className={styles.summaryText}>
<Subtitle color="uiTextHighContrast">{roomType}</Subtitle>
<Body color="uiTextMediumContrast">{`${priceName}, ${priceTerm}`}</Body>
</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 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>
)}
<Button type="submit" theme="base" className={styles.continueButton}>
{intl.formatMessage({ id: "Continue" })}
</Button>
<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 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>

View File

@@ -6,12 +6,19 @@
right: 0;
background-color: var(--Base-Surface-Primary-light-Normal);
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;
flex-direction: column;
justify-content: space-between;
align-items: center;
border-top: 1px solid var(--Base-Border-Subtle);
transition: bottom 300ms ease-in-out;
}
.summary[data-visible="true"] {
@@ -80,7 +87,9 @@
@media (min-width: 768px) {
.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;
}
.petInfo,
@@ -102,5 +111,6 @@
.summaryPriceContainer {
width: auto;
padding: 0;
align-items: center;
}
}

View File

@@ -210,9 +210,7 @@ export default function RoomCard({
product={findProductForRate(rate)}
priceInformation={getRateDefinitionForRate(rate)?.generalTerms}
handleSelectRate={handleSelectRate}
roomType={roomConfiguration.roomType}
roomTypeCode={roomConfiguration.roomTypeCode}
features={roomConfiguration.features}
petRoomPackage={petRoomPackage}
/>
))}

View File

@@ -14,7 +14,10 @@ import {
type RoomPackageCodes,
} from "@/types/components/hotelReservation/selectRate/roomFilter"
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"
export default function Rooms({
@@ -25,9 +28,9 @@ export default function Rooms({
}: SelectRateProps) {
const visibleRooms: RoomConfiguration[] =
filterDuplicateRoomTypesByLowestPrice(roomsAvailability.roomConfigurations)
const [selectedRate, setSelectedRate] = useState<
{ publicRateCode: string; roomTypeCode: string } | undefined
>(undefined)
const [selectedRate, setSelectedRate] = useState<RateCode | undefined>(
undefined
)
const [selectedPackages, setSelectedPackages] = useState<RoomPackageCodes[]>(
[]
)
@@ -115,17 +118,30 @@ export default function Rooms({
)
)?.features
const roomType = roomCategories.find((roomCategory) =>
roomCategory.roomTypes.some(
(roomType) => roomType.code === room.roomTypeCode
)
)
const rateSummary: Rate = {
features: petRoomPackage && features ? features : [],
priceName: room.roomType,
priceName: selectedRate?.name,
priceTerm: selectedRate?.paymentTerm,
public: product.productType.public,
member: product.productType.member,
roomType: room.roomType,
roomType: roomType?.name || room.roomType,
roomTypeCode: room.roomTypeCode,
}
return rateSummary
}, [filteredRooms, availablePackages, selectedPackages, selectedRate])
}, [
filteredRooms,
availablePackages,
selectedPackages,
selectedRate,
roomCategories,
])
useEffect(() => {
if (rateSummary) return

View 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>
)
}

View File

@@ -36,6 +36,7 @@ import {
ElectricBikeIcon,
ElectricCarIcon,
EmailIcon,
ExternalLinkIcon,
EyeHideIcon,
EyeShowIcon,
FacebookIcon,
@@ -176,6 +177,8 @@ export function getIconByIconName(
return ElectricCarIcon
case IconName.Email:
return EmailIcon
case IconName.ExternalLink:
return ExternalLinkIcon
case IconName.EyeHide:
return EyeHideIcon
case IconName.EyeShow:

View File

@@ -61,6 +61,7 @@ export { default as ElectricBikeIcon } from "./ElectricBike"
export { default as ElectricCarIcon } from "./ElectricCar"
export { default as EmailIcon } from "./Email"
export { default as ErrorCircleIcon } from "./ErrorCircle"
export { default as ExternalLinkIcon } from "./ExternalLink"
export { default as EyeHideIcon } from "./EyeHide"
export { default as EyeShowIcon } from "./EyeShow"
export { default as FacebookIcon } from "./Facebook"

View File

@@ -6,7 +6,8 @@ import { useIntl } from "react-intl"
import { GalleryIcon } from "@/components/Icons"
import Image from "@/components/Image"
import Lightbox from "@/components/Lightbox"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Caption from "../TempDesignSystem/Text/Caption"
import styles from "./imageGallery.module.css"
@@ -44,9 +45,9 @@ function ImageGallery({
/>
<div className={styles.imageCount}>
<GalleryIcon color="white" />
<Footnote color="white" type="label">
<Caption color="white" type="label">
{images.length}
</Footnote>
</Caption>
</div>
</div>
<Lightbox

View File

@@ -122,6 +122,7 @@
grid-template-columns: 1fr 1fr;
gap: var(--Spacing-x1);
max-height: none;
padding: var(--Spacing-x3) 0;
}
.thumbnailContainer {

View File

@@ -14,6 +14,7 @@ export default function Accessibility({
<AccordionItem
title={intl.formatMessage({ id: "Accessibility" })}
icon={IconName.Accessibility}
variant="sidepeek"
>
<Body>{accessibilityElevatorPitchText}</Body>
</AccordionItem>

View File

@@ -13,6 +13,7 @@ export default function CheckinCheckOut({ checkin }: CheckInCheckOutProps) {
<AccordionItem
title={intl.formatMessage({ id: "Check-in/Check-out" })}
icon={IconName.Calendar}
variant="sidepeek"
>
<Body textTransform="bold">{intl.formatMessage({ id: "Hours" })}</Body>
<Body>{`${intl.formatMessage({ id: "Check in from" })}: ${checkin.checkInTime}`}</Body>

View File

@@ -14,6 +14,7 @@ export default function MeetingsAndConferences({
<AccordionItem
title={intl.formatMessage({ id: "Meetings & Conferences" })}
icon={IconName.Business}
variant="sidepeek"
>
<Body>{meetingDescription}</Body>
</AccordionItem>

View File

@@ -16,6 +16,7 @@ export default function Parking({ parking }: ParkingProps) {
title={intl.formatMessage({ id: "Parking" })}
icon={IconName.Parking}
className={styles.parking}
variant="sidepeek"
>
{parking.map((p) => (
<div key={p.name}>

View File

@@ -15,6 +15,7 @@ export default function Restaurant({
<AccordionItem
title={intl.formatMessage({ id: "Restaurant" }, { count: 1 })}
icon={IconName.Restaurant}
variant="sidepeek"
>
<Body>{restaurantsContentDescriptionMedium}</Body>
</AccordionItem>

View File

@@ -10,6 +10,7 @@
align-items: center;
gap: var(--Spacing-x1);
padding-left: var(--Spacing-x1);
justify-items: flex-start;
}
.list li svg {

View File

@@ -9,13 +9,24 @@
gap: var(--Spacing-x2);
}
.amenity {
font-family: var(--typography-Body-Regular-fontFamily);
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))
var(--Spacing-x3);
.content:last-child {
gap: 0;
}
.content > p {
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;
align-items: center;
gap: var(--Spacing-x1);
}

View File

@@ -71,24 +71,21 @@ export default function HotelSidePeek({
}
/>
)}
</Accordion>
<div className={styles.amenity}>
{amenitiesList.map((amenity) => {
const Icon = mapFacilityToIcon(amenity.id)
return (
<div key={amenity.id} className={styles.amenity}>
<Subtitle type="two" key={amenity.id} color="uiTextHighContrast">
{Icon && (
<Icon width={24} height={24} color="uiTextMediumContrast" />
<Icon width={24} height={24} color="uiTextHighContrast" />
)}
<Body
asChild
className={!Icon ? styles.noIcon : undefined}
color="uiTextMediumContrast"
>
<span>{amenity.name}</span>
</Body>
</div>
{amenity.name}
</Subtitle>
)
})}
</Accordion>
</div>
{/* TODO: handle linking to Hotel Page */}
{/* {showCTA && (
<Button theme="base" intent="secondary" size="large">

View File

@@ -19,10 +19,7 @@ export default function Sidebar({ blocks }: SidebarProps) {
switch (block.typename) {
case SidebarEnums.blocks.Content:
return (
<section
className={styles.content}
key={`${block.typename}-${idx}`}
>
<section key={`${block.typename}-${idx}`}>
<JsonToHtml
embeds={block.content.embedded_itemsConnection.edges}
nodes={block.content.json.children}

View File

@@ -6,6 +6,10 @@
padding: var(--Spacing-x1);
}
.accordionItem.sidepeek {
padding: var(--Spacing-x1) 0;
}
.summary {
position: relative;
display: flex;
@@ -18,7 +22,7 @@
font-weight: var(--typography-Body-Bold-fontWeight);
transition: background-color 0.3s;
}
.summary:hover {
.summary.card:hover {
background-color: var(--Base-Surface-Primary-light-Hover-alt);
}
.accordionItem.light .summary:hover {
@@ -33,6 +37,11 @@
border-radius: var(--Corner-radius-Medium);
}
.accordionItem.sidepeek .summary {
padding: var(--Spacing-x-one-and-half) var(--Spacing-x1);
align-items: center;
}
.title {
flex-grow: 1;
}

View File

@@ -50,16 +50,10 @@ export default function AccordionItem({
<li className={accordionItemVariants({ className, variant, theme })}>
<details ref={detailsRef} onToggle={toggleAccordion}>
<summary className={styles.summary}>
{IconComp && <IconComp className={styles.icon} color="burgundy" />}
{variant === "card" ? (
<Body
textTransform="bold"
color="baseTextHighContrast"
className={styles.title}
>
{title}
</Body>
) : (
{IconComp && (
<IconComp className={styles.icon} color="baseTextHighcontrast" />
)}
{variant === "sidepeek" ? (
<Subtitle
className={styles.title}
type="two"
@@ -67,6 +61,14 @@ export default function AccordionItem({
>
{title}
</Subtitle>
) : (
<Body
textTransform="bold"
color="baseTextHighContrast"
className={styles.title}
>
{title}
</Body>
)}
<ChevronDownIcon
className={styles.chevron}

View File

@@ -6,6 +6,7 @@ export const accordionItemVariants = cva(styles.accordionItem, {
variants: {
variant: {
card: styles.card,
sidepeek: styles.sidepeek,
},
theme: {
default: styles.default,

View File

@@ -6,6 +6,7 @@ export const accordionVariants = cva(styles.accordion, {
variants: {
variant: {
card: styles.card,
sidepeek: styles.sidepeek,
},
theme: {
default: styles.default,

View File

@@ -2,7 +2,7 @@
import { useState } from "react"
import { ChevronRightIcon } from "@/components/Icons"
import { ChevronRightSmallIcon } from "@/components/Icons"
import JsonToHtml from "@/components/JsonToHtml"
import Button from "@/components/TempDesignSystem/Button"
@@ -31,7 +31,7 @@ export default function TeaserCardSidepeek({
wrapping
>
{button.call_to_action_text}
<ChevronRightIcon height={20} width={20} />
<ChevronRightSmallIcon />
</Button>
<SidePeek
title={heading}
@@ -44,13 +44,7 @@ export default function TeaserCardSidepeek({
/>
<div className={styles.ctaContainer}>
{primary_button && (
<Button
asChild
theme="base"
intent="primary"
size="small"
className={styles.ctaButton}
>
<Button asChild theme="base" intent="primary" size="small">
<Link
href={primary_button.href}
target={primary_button.openInNewTab ? "_blank" : undefined}
@@ -61,12 +55,7 @@ export default function TeaserCardSidepeek({
</Button>
)}
{secondary_button && (
<Button
asChild
intent="secondary"
size="small"
className={styles.ctaButton}
>
<Button asChild intent="secondary" size="small">
<Link
href={secondary_button.href}
target={secondary_button.openInNewTab ? "_blank" : undefined}

View File

@@ -41,9 +41,7 @@ export default function TeaserCard({
<Subtitle textAlign="left" type="two" color="black">
{title}
</Subtitle>
<Body color="black" className={styles.body}>
{description}
</Body>
<Body color="black">{description}</Body>
{sidePeekButton && sidePeekContent ? (
<TeaserCardSidepeek
button={sidePeekButton}
@@ -77,6 +75,8 @@ export default function TeaserCard({
<Link
href={secondaryButton.href}
target={secondaryButton.openInNewTab ? "_blank" : undefined}
color="burgundy"
weight="bold"
>
{secondaryButton.title}
</Link>