From 5a48a118f4b3f9a887f8a044016f79e016d15838 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Mon, 11 Nov 2024 10:41:29 +0100 Subject: [PATCH 01/98] fix(SW-856): fixed issues where blocks do not have titles/preambles --- components/Blocks/Accordion/index.tsx | 2 +- components/Blocks/Table/index.tsx | 2 +- components/Section/Header/index.tsx | 4 ++++ .../TempDesignSystem/ScrollWrapper/scrollWrapper.module.css | 4 ++-- types/components/myPages/header.ts | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/components/Blocks/Accordion/index.tsx b/components/Blocks/Accordion/index.tsx index 8828555d1..edab9dec0 100644 --- a/components/Blocks/Accordion/index.tsx +++ b/components/Blocks/Accordion/index.tsx @@ -24,7 +24,7 @@ export default function AccordionSection({ accordion, title }: AccordionProps) { return ( - {title && } + - {heading ? : null} +
Date: Mon, 11 Nov 2024 15:07:28 +0100 Subject: [PATCH 02/98] fix(SW-856): fixes previous faulty fix regarding SectionHeader --- components/Section/Header/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Section/Header/index.tsx b/components/Section/Header/index.tsx index 94e9829fe..3bc5b6f01 100644 --- a/components/Section/Header/index.tsx +++ b/components/Section/Header/index.tsx @@ -14,7 +14,7 @@ export default function SectionHeader({ topTitle = false, textTransform, }: HeaderProps) { - if (!title || !preamble || !link) { + if (!title && !preamble && !link) { return null } From 66b2dc0c78c2770fc8860ed7b674eff22d1ac40b Mon Sep 17 00:00:00 2001 From: Christel Westerberg <christel.westerberg@scandichotels.com> Date: Thu, 7 Nov 2024 11:26:49 +0100 Subject: [PATCH 03/98] feat: add mobile summary --- .../[step]/@summary/page.module.css | 68 +++++++++++++++ .../(standard)/[step]/@summary/page.tsx | 50 ++++++++--- .../(standard)/[step]/enterDetailsLayout.css | 83 +++--------------- .../(standard)/[step]/layout.tsx | 6 +- .../sectionAccordion.module.css | 2 +- .../SelectedRoom/selectedRoom.module.css | 2 +- .../BottomSheet/bottomSheet.module.css | 49 +++++++++++ .../Summary/BottomSheet/index.tsx | 55 ++++++++++++ .../EnterDetails/Summary/index.tsx | 87 +++++++++++++------ .../EnterDetails/Summary/summary.module.css | 10 +++ .../Text/Caption/caption.module.css | 13 +++ .../TempDesignSystem/Text/Caption/variants.ts | 3 + i18n/dictionaries/da.json | 3 + i18n/dictionaries/de.json | 3 + i18n/dictionaries/en.json | 2 + i18n/dictionaries/fi.json | 3 + i18n/dictionaries/no.json | 3 + i18n/dictionaries/sv.json | 3 + stores/enter-details.ts | 16 ++++ 19 files changed, 345 insertions(+), 116 deletions(-) create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.module.css create mode 100644 components/HotelReservation/EnterDetails/Summary/BottomSheet/bottomSheet.module.css create mode 100644 components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.module.css new file mode 100644 index 000000000..f680a23a1 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.module.css @@ -0,0 +1,68 @@ +.mobileSummary { + display: block; +} + +.desktopSummary { + display: none; +} + +.summary { + background-color: var(--Main-Grey-White); + + border-color: var(--Primary-Light-On-Surface-Divider-subtle); + border-style: solid; + border-width: 1px; + border-bottom: none; + z-index: 10; +} + +.hider { + display: none; +} + +.shadow { + display: none; +} + +@media screen and (min-width: 1367px) { + .mobileSummary { + display: none; + } + + .desktopSummary { + display: grid; + grid-template-rows: auto auto 1fr; + margin-top: calc(0px - var(--Spacing-x9)); + } + + .summary { + position: sticky; + top: calc( + var(--booking-widget-desktop-height) + var(--Spacing-x2) + + var(--Spacing-x-half) + ); + z-index: 10; + border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; + margin-top: calc(0px - var(--Spacing-x9)); + } + + .shadow { + display: block; + background-color: var(--Main-Grey-White); + border-color: var(--Primary-Light-On-Surface-Divider-subtle); + border-style: solid; + border-left-width: 1px; + border-right-width: 1px; + border-top: none; + border-bottom: none; + } + + .hider { + display: block; + background-color: var(--Scandic-Brand-Warm-White); + position: sticky; + top: calc(var(--booking-widget-desktop-height) - 6px); + margin-top: var(--Spacing-x4); + height: 40px; + } +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx index b39c2622b..adfc99808 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx @@ -4,11 +4,14 @@ import { } from "@/lib/trpc/memoizedRequests" import Summary from "@/components/HotelReservation/EnterDetails/Summary" +import { SummaryBottomSheet } from "@/components/HotelReservation/EnterDetails/Summary/BottomSheet" import { generateChildrenString, getQueryParamsForEnterDetails, } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" +import styles from "./page.module.css" + import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import { LangParams, PageArgs, SearchParams } from "@/types/params" @@ -62,16 +65,41 @@ export default async function SummaryPage({ } return ( - <Summary - showMemberPrice={!!(user && availability.memberRate)} - room={{ - roomType: availability.selectedRoom.roomType, - localPrice: prices.local, - euroPrice: prices.euro, - adults, - children, - cancellationText: availability.cancellationText, - }} - /> + <> + <div className={styles.mobileSummary}> + <SummaryBottomSheet> + <div className={styles.summary}> + <Summary + showMemberPrice={!!(user && availability.memberRate)} + room={{ + roomType: availability.selectedRoom.roomType, + localPrice: prices.local, + euroPrice: prices.euro, + adults, + children, + cancellationText: availability.cancellationText, + }} + /> + </div> + </SummaryBottomSheet> + </div> + <div className={styles.desktopSummary}> + <div className={styles.hider} /> + <div className={styles.summary}> + <Summary + showMemberPrice={!!(user && availability.memberRate)} + room={{ + roomType: availability.selectedRoom.roomType, + localPrice: prices.local, + euroPrice: prices.euro, + adults, + children, + cancellationText: availability.cancellationText, + }} + /> + </div> + <div className={styles.shadow} /> + </div> + </> ) } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/enterDetailsLayout.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/enterDetailsLayout.css index 13c3ded9f..31054ccd4 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/enterDetailsLayout.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/enterDetailsLayout.css @@ -11,8 +11,6 @@ .enter-details-layout__content { display: grid; gap: var(--Spacing-x3) var(--Spacing-x9); - grid-template-columns: 1fr 340px; - grid-template-rows: auto 1fr; margin: var(--Spacing-x5) auto 0; /* simulates padding on viewport smaller than --max-width-navigation */ width: min( @@ -22,80 +20,23 @@ } .enter-details-layout__summaryContainer { - grid-column: 2 / 3; - grid-row: 1/-1; -} - -.enter-details-layout__summary { - background-color: var(--Main-Grey-White); - - border-color: var(--Primary-Light-On-Surface-Divider-subtle); - border-style: solid; - border-width: 1px; - border-radius: var(--Corner-radius-Large); - + position: fixed; z-index: 1; -} - -.enter-details-layout__hider { - display: none; -} - -.enter-details-layout__shadow { - display: none; -} - -@media screen and (min-width: 950px) { - .enter-details-layout__summaryContainer { - display: grid; - grid-template-rows: auto auto 1fr; - margin-top: calc(0px - var(--Spacing-x9)); - } - - .enter-details-layout__summary { - position: sticky; - top: calc( - var(--booking-widget-desktop-height) + - var(--booking-widget-desktop-height) + var(--Spacing-x-one-and-half) - ); - margin-top: calc(0px - var(--Spacing-x9)); - border-bottom: none; - border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; - } - - .enter-details-layout__hider { - display: block; - background-color: var(--Scandic-Brand-Warm-White); - position: sticky; - margin-top: var(--Spacing-x4); - top: calc( - var(--booking-widget-desktop-height) + - var(--booking-widget-desktop-height) - 6px - ); - height: 40px; - } - - .enter-details-layout__shadow { - display: block; - background-color: var(--Main-Grey-White); - border-color: var(--Primary-Light-On-Surface-Divider-subtle); - border-style: solid; - border-left-width: 1px; - border-right-width: 1px; - border-top: none; - border-bottom: none; - } + bottom: 0; + left: 0; + right: 0; } @media screen and (min-width: 1367px) { - .enter-details-layout__summary { - top: calc( - var(--booking-widget-desktop-height) + var(--Spacing-x2) + - var(--Spacing-x-half) - ); + .enter-details-layout__content { + grid-template-columns: 1fr 340px; + grid-template-rows: auto 1fr; } - .enter-details-layout__hider { - top: calc(var(--booking-widget-desktop-height) - 6px); + .enter-details-layout__summaryContainer { + position: static; + display: grid; + grid-column: 2/3; + grid-row: 1/-1; } } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx index 3a4af62ba..2b5a6241f 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx @@ -30,11 +30,7 @@ export default async function StepLayout({ {hotelHeader} <div className={"enter-details-layout__content"}> {children} - <aside className="enter-details-layout__summaryContainer"> - <div className="enter-details-layout__hider" /> - <div className="enter-details-layout__summary">{summary}</div> - <div className="enter-details-layout__shadow" /> - </aside> + <aside className={"enter-details-layout__summaryContainer"}>{summary}</aside> </div> </main> </EnterDetailsProvider> diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css index 93398378d..7b9ea37dc 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css +++ b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css @@ -45,7 +45,7 @@ .iconWrapper { position: relative; top: var(--Spacing-x1); - z-index: 2; + z-index: 1; } .circle { diff --git a/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css b/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css index 120ddc4e5..f88a56606 100644 --- a/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css +++ b/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css @@ -38,7 +38,7 @@ .iconWrapper { position: relative; top: var(--Spacing-x1); - z-index: 2; + z-index: 1; } .circle { diff --git a/components/HotelReservation/EnterDetails/Summary/BottomSheet/bottomSheet.module.css b/components/HotelReservation/EnterDetails/Summary/BottomSheet/bottomSheet.module.css new file mode 100644 index 000000000..c7e82d8af --- /dev/null +++ b/components/HotelReservation/EnterDetails/Summary/BottomSheet/bottomSheet.module.css @@ -0,0 +1,49 @@ +.wrapper { + display: grid; + grid-template-rows: 0fr 7.5em; + + transition: 0.5s ease-in-out; + border-top: 1px solid var(--Base-Border-Subtle); + background: var(--Base-Surface-Primary-light-Normal); + align-content: end; +} + +.bottomSheet { + display: grid; + grid-template-columns: 1fr auto; + padding: var(--Spacing-x2) var(--Spacing-x3) var(--Spacing-x5) + var(--Spacing-x3); + justify-content: space-between; + width: auto; + align-items: flex-start; + transition: 0.5s ease-in-out; +} + +.priceDetails { + display: block; + border: none; + background: none; + text-align: start; + opacity: 1; + transition: + opacity 0.5s ease-in-out, + padding 0.5s ease-in-out; +} + +.wrapper[data-open="true"] { + grid-template-rows: 1fr 7.5em; +} + +.wrapper[data-open="true"] .bottomSheet { + grid-template-columns: 0fr 1fr; +} + +.wrapper[data-open="true"] .priceDetails { + opacity: 0; + padding: 0; +} + +.content, +.priceDetails { + overflow: hidden; +} diff --git a/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx b/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx new file mode 100644 index 000000000..fa617b256 --- /dev/null +++ b/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx @@ -0,0 +1,55 @@ +"use client" + +import { PropsWithChildren, useState } from "react" +import { useIntl } from "react-intl" + +import { useEnterDetailsStore } from "@/stores/enter-details" + +import Button from "@/components/TempDesignSystem/Button" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import { formatNumber } from "@/utils/format" + +import styles from "./bottomSheet.module.css" + +export function SummaryBottomSheet({ children }: PropsWithChildren) { + const intl = useIntl() + + const { isSummaryOpen, toggleSummaryOpen, totalPrice } = useEnterDetailsStore( + (state) => ({ + isSummaryOpen: state.isSummaryOpen, + toggleSummaryOpen: state.toggleSummaryOpen, + totalPrice: state.totalPrice, + }) + ) + + return ( + <div className={styles.wrapper} data-open={isSummaryOpen}> + <div className={styles.content}>{children}</div> + <div className={styles.bottomSheet}> + <button + data-open={isSummaryOpen} + onClick={toggleSummaryOpen} + className={styles.priceDetails} + > + <Caption>{intl.formatMessage({ id: "Total price" })}:</Caption> + <Subtitle> + {intl.formatMessage( + { id: "{amount} {currency}" }, + { + amount: formatNumber(totalPrice.local.price), + currency: totalPrice.local.currency, + } + )} + </Subtitle> + <Caption color="baseTextHighContrast" type="underline"> + {intl.formatMessage({ id: "See details" })} + </Caption> + </button> + <Button intent="primary" size="large" type="submit"> + {intl.formatMessage({ id: "Complete booking" })} + </Button> + </div> + </div> + ) +} diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index b8d744baa..188ba94e3 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -6,7 +6,7 @@ import { useIntl } from "react-intl" import { dt } from "@/lib/dt" import { useEnterDetailsStore } from "@/stores/enter-details" -import { ArrowRightIcon } from "@/components/Icons" +import { ArrowRightIcon, CloseIcon } from "@/components/Icons" import Divider from "@/components/TempDesignSystem/Divider" import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" @@ -36,20 +36,25 @@ export default function Summary({ const [chosenBreakfast, setChosenBreakfast] = useState< BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST >() - const [totalPrice, setTotalPrice] = useState({ - local: parsePrice(room.localPrice.price), - euro: parsePrice(room.euroPrice.price), - }) const intl = useIntl() const lang = useLang() - const { fromDate, toDate, bedType, breakfast } = useEnterDetailsStore( - (state) => ({ - fromDate: state.roomData.fromDate, - toDate: state.roomData.toDate, - bedType: state.userData.bedType, - breakfast: state.userData.breakfast, - }) - ) + const { + fromDate, + toDate, + bedType, + breakfast, + setTotalPrice, + totalPrice, + toggleSummaryOpen, + } = useEnterDetailsStore((state) => ({ + fromDate: state.roomData.fromDate, + toDate: state.roomData.toDate, + bedType: state.userData.bedType, + breakfast: state.userData.breakfast, + toggleSummaryOpen: state.toggleSummaryOpen, + setTotalPrice: state.setTotalPrice, + totalPrice: state.totalPrice, + })) const diff = dt(toDate).diff(fromDate, "days") @@ -70,21 +75,48 @@ export default function Summary({ setChosenBreakfast(breakfast) if (breakfast === BreakfastPackageEnum.NO_BREAKFAST) { setTotalPrice({ - local: parsePrice(room.localPrice.price), - euro: parsePrice(room.euroPrice.price), + local: { + price: parsePrice(room.localPrice.price), + currency: room.localPrice.currency!, + }, + euro: { + price: parsePrice(room.euroPrice.price), + currency: room.euroPrice.currency!, + }, }) } else { setTotalPrice({ - local: - parsePrice(room.localPrice.price) + - parsePrice(breakfast.localPrice.totalPrice), - euro: - parsePrice(room.euroPrice.price) + - parsePrice(breakfast.requestedPrice.totalPrice), + local: { + price: + parsePrice(room.localPrice.price) + + parsePrice(breakfast.localPrice.totalPrice), + currency: room.localPrice.currency!, + }, + euro: { + price: + parsePrice(room.euroPrice.price) + + parsePrice(breakfast.requestedPrice.totalPrice), + currency: room.euroPrice.currency!, + }, }) } } - }, [bedType, breakfast, room.localPrice, room.euroPrice]) + }, [bedType, breakfast, room.localPrice, room.euroPrice, setTotalPrice]) + + useEffect(() => { + setTotalPrice({ + local: { + price: parsePrice(room.localPrice.price), + currency: room.localPrice.currency!, + }, + euro: { + price: parsePrice(room.euroPrice.price), + currency: room.euroPrice.currency!, + }, + }) + }, [room.localPrice, room.euroPrice, setTotalPrice]) + + const showToggleButton = true return ( <section className={styles.summary}> @@ -95,6 +127,7 @@ export default function Summary({ <ArrowRightIcon color="peach80" height={15} width={15} /> {dt(toDate).locale(lang).format("ddd, D MMM")} ({nights}) </Body> + {showToggleButton ? <CloseIcon onClick={toggleSummaryOpen} /> : null} </header> <Divider color="primaryLightSubtle" /> <div className={styles.addOns}> @@ -203,8 +236,8 @@ export default function Summary({ {intl.formatMessage( { id: "{amount} {currency}" }, { - amount: intl.formatNumber(totalPrice.local), - currency: room.localPrice.currency, + amount: intl.formatNumber(totalPrice.local.price), + currency: totalPrice.local.currency, } )} </Body> @@ -213,14 +246,14 @@ export default function Summary({ {intl.formatMessage( { id: "{amount} {currency}" }, { - amount: intl.formatNumber(totalPrice.euro), - currency: room.euroPrice.currency, + amount: intl.formatNumber(totalPrice.euro.price), + currency: totalPrice.euro.currency, } )} </Caption> </div> </div> - <Divider color="primaryLightSubtle" /> + <Divider className={styles.bottomDivider} color="primaryLightSubtle" /> </div> </section> ) diff --git a/components/HotelReservation/EnterDetails/Summary/summary.module.css b/components/HotelReservation/EnterDetails/Summary/summary.module.css index b7a8d2db4..3b933f9b8 100644 --- a/components/HotelReservation/EnterDetails/Summary/summary.module.css +++ b/components/HotelReservation/EnterDetails/Summary/summary.module.css @@ -38,3 +38,13 @@ flex-direction: column; gap: var(--Spacing-x2); } + +.bottomDivider { + display: none; +} + +@media screen and (min-width: 1367px) { + .bottomDivider { + display: block; + } +} diff --git a/components/TempDesignSystem/Text/Caption/caption.module.css b/components/TempDesignSystem/Text/Caption/caption.module.css index bbd4a037d..3eb44199a 100644 --- a/components/TempDesignSystem/Text/Caption/caption.module.css +++ b/components/TempDesignSystem/Text/Caption/caption.module.css @@ -25,6 +25,15 @@ p.caption { text-decoration: var(--typography-Caption-Labels-textDecoration); } +.underline { + font-family: var(--typography-Caption-Underline-fontFamily); + font-size: var(--typography-Caption-Underline-fontSize); + font-weight: var(--typography-Caption-Underline-fontWeight); + letter-spacing: var(--typography-Caption-Underline-letterSpacing); + line-height: var(--typography-Caption-Underline-lineHeight); + text-decoration: underline; /* var(--typography-Caption-Underline-textDecoration) /* Commented till figma values are fixed to underline instead of "underline" */ +} + .regular { font-family: var(--typography-Caption-Regular-fontFamily); font-size: var(--typography-Caption-Regular-fontSize); @@ -58,6 +67,10 @@ p.caption { color: var(--Base-Text-Medium-contrast); } +.baseTextHighContrast { + color: var(--Base-Text-High-contrast); +} + .red { color: var(--Scandic-Brand-Scandic-Red); } diff --git a/components/TempDesignSystem/Text/Caption/variants.ts b/components/TempDesignSystem/Text/Caption/variants.ts index 6fe5b4dde..b0672430b 100644 --- a/components/TempDesignSystem/Text/Caption/variants.ts +++ b/components/TempDesignSystem/Text/Caption/variants.ts @@ -8,6 +8,7 @@ const config = { regular: styles.regular, bold: styles.bold, label: styles.labels, + underline: styles.underline, }, color: { baseTextAccent: styles.baseTextAccent, @@ -22,6 +23,7 @@ const config = { uiTextMediumContrast: styles.uiTextMediumContrast, uiTextPlaceholder: styles.uiTextPlaceholder, disabled: styles.disabled, + baseTextHighContrast: styles.baseTextHighContrast, }, textTransform: { uppercase: styles.uppercase, @@ -48,6 +50,7 @@ const fontOnlyConfig = { regular: styles.regular, bold: styles.bold, label: styles.labels, + underline: styles.underline, }, textTransform: { uppercase: styles.uppercase, diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index a40e050ed..1ebe5d3f9 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -53,6 +53,7 @@ "Bus terminal": "Busstation", "Business": "Forretning", "Cancel": "Afbestille", + "Change room": "Skift værelse", "Check in": "Check ind", "Check out": "Check ud", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Tjek de kreditkort, der er gemt på din profil. Betal med et gemt kort, når du er logget ind for en mere jævn weboplevelse.", @@ -71,6 +72,7 @@ "Code / Voucher": "Bookingkoder / voucher", "Coming up": "Er lige om hjørnet", "Compare all levels": "Sammenlign alle niveauer", + "Complete booking": "Fuldfør bookingen", "Complete booking & go to payment": "Udfyld booking & gå til betaling", "Complete the booking": "Fuldfør bookingen", "Contact information": "Kontaktoplysninger", @@ -285,6 +287,7 @@ "Search": "Søge", "See all FAQ": "Se alle FAQ", "See all photos": "Se alle billeder", + "See details": "Se detaljer", "See hotel details": "Se hoteloplysninger", "See less FAQ": "Se mindre FAQ", "See on map": "Se på kort", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index abe5d9431..71231512a 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -53,6 +53,7 @@ "Bus terminal": "Busbahnhof", "Business": "Geschäft", "Cancel": "Stornieren", + "Change room": "Zimmer ändern", "Check in": "Einchecken", "Check out": "Auschecken", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Sehen Sie sich die in Ihrem Profil gespeicherten Kreditkarten an. Bezahlen Sie mit einer gespeicherten Karte, wenn Sie angemeldet sind, für ein reibungsloseres Web-Erlebnis.", @@ -71,6 +72,7 @@ "Code / Voucher": "Buchungscodes / Gutscheine", "Coming up": "Demnächst", "Compare all levels": "Vergleichen Sie alle Levels", + "Complete booking": "Buchung abschließen", "Complete booking & go to payment": "Buchung abschließen & zur Bezahlung gehen", "Complete the booking": "Buchung abschließen", "Contact information": "Kontaktinformationen", @@ -284,6 +286,7 @@ "Search": "Suchen", "See all FAQ": "Siehe alle FAQ", "See all photos": "Alle Fotos ansehen", + "See details": "Siehe Einzelheiten", "See hotel details": "Hotelinformationen ansehen", "See less FAQ": "Weniger anzeigen FAQ", "See on map": "Karte ansehen", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 75abdc4d4..604b52c60 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -80,6 +80,7 @@ "Code / Voucher": "Code / Voucher", "Coming up": "Coming up", "Compare all levels": "Compare all levels", + "Complete booking": "Complete booking", "Complete booking & go to payment": "Complete booking & go to payment", "Complete the booking": "Complete the booking", "Contact information": "Contact information", @@ -314,6 +315,7 @@ "Search": "Search", "See all FAQ": "See all FAQ", "See all photos": "See all photos", + "See details": "See details", "See hotel details": "See hotel details", "See less FAQ": "See less FAQ", "See on map": "See on map", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index c55b5cd2c..665a6e374 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -53,6 +53,7 @@ "Bus terminal": "Bussiasema", "Business": "Business", "Cancel": "Peruuttaa", + "Change room": "Vaihda huonetta", "Check in": "Sisäänkirjautuminen", "Check out": "Uloskirjautuminen", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Tarkista profiiliisi tallennetut luottokortit. Maksa tallennetulla kortilla kirjautuneena, jotta verkkokokemus on sujuvampi.", @@ -71,6 +72,7 @@ "Code / Voucher": "Varauskoodit / kupongit", "Coming up": "Tulossa", "Compare all levels": "Vertaa kaikkia tasoja", + "Complete booking": "Täydennä varaus", "Complete booking & go to payment": "Täydennä varaus & siirry maksamaan", "Complete the booking": "Täydennä varaus", "Contact information": "Yhteystiedot", @@ -286,6 +288,7 @@ "Search": "Haku", "See all FAQ": "Katso kaikki UKK", "See all photos": "Katso kaikki kuvat", + "See details": "Katso tiedot", "See hotel details": "Katso hotellin tiedot", "See less FAQ": "Katso vähemmän UKK", "See on map": "Näytä kartalla", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 91d28c0b2..075c0b2d8 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -53,6 +53,7 @@ "Bus terminal": "Bussterminal", "Business": "Forretnings", "Cancel": "Avbryt", + "Change room": "Endre rom", "Check in": "Sjekk inn", "Check out": "Sjekk ut", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Sjekk ut kredittkortene som er lagret på profilen din. Betal med et lagret kort når du er pålogget for en jevnere nettopplevelse.", @@ -71,6 +72,7 @@ "Code / Voucher": "Bestillingskoder / kuponger", "Coming up": "Kommer opp", "Compare all levels": "Sammenlign alle nivåer", + "Complete booking": "Fullfør reservasjonen", "Complete booking & go to payment": "Fullfør bestilling & gå til betaling", "Complete the booking": "Fullfør reservasjonen", "Contact information": "Kontaktinformasjon", @@ -283,6 +285,7 @@ "Search": "Søk", "See all FAQ": "Se alle FAQ", "See all photos": "Se alle bilder", + "See details": "Se detaljer", "See hotel details": "Se hotellinformasjon", "See less FAQ": "Se mindre FAQ", "See on map": "Se på kart", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 9834bb6ac..9b6b51748 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -53,6 +53,7 @@ "Bus terminal": "Bussterminal", "Business": "Business", "Cancel": "Avbryt", + "Change room": "Ändra rum", "Check in": "Checka in", "Check out": "Checka ut", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Kolla in kreditkorten som sparats i din profil. Betala med ett sparat kort när du är inloggad för en smidigare webbupplevelse.", @@ -71,6 +72,7 @@ "Code / Voucher": "Bokningskoder / kuponger", "Coming up": "Kommer härnäst", "Compare all levels": "Jämför alla nivåer", + "Complete booking": "Slutför bokning", "Complete booking & go to payment": "Fullför bokning & gå till betalning", "Complete the booking": "Slutför bokningen", "Contact information": "Kontaktinformation", @@ -283,6 +285,7 @@ "Search": "Sök", "See all FAQ": "Se alla FAQ", "See all photos": "Se alla foton", + "See details": "Se detaljer", "See hotel details": "Se hotellinformation", "See less FAQ": "See färre FAQ", "See on map": "Se på karta", diff --git a/stores/enter-details.ts b/stores/enter-details.ts index 88290619e..18c0695fe 100644 --- a/stores/enter-details.ts +++ b/stores/enter-details.ts @@ -23,6 +23,11 @@ import { BreakfastPackageEnum } from "@/types/enums/breakfast" const SESSION_STORAGE_KEY = "enterDetails" +type TotalPrice = { + local: { price: number; currency: string } + euro: { price: number; currency: string } +} + interface EnterDetailsState { userData: { bedType: BedTypeSchema | undefined @@ -32,6 +37,8 @@ interface EnterDetailsState { steps: StepEnum[] selectRateUrl: string currentStep: StepEnum + totalPrice: TotalPrice + isSummaryOpen: boolean isValid: Record<StepEnum, boolean> completeStep: (updatedData: Partial<EnterDetailsState["userData"]>) => void navigate: ( @@ -42,6 +49,8 @@ interface EnterDetailsState { > ) => void setCurrentStep: (step: StepEnum) => void + toggleSummaryOpen: () => void + setTotalPrice: (totalPrice: TotalPrice) => void } export function initEditDetailsState( @@ -129,6 +138,11 @@ export function initEditDetailsState( roomData, selectRateUrl, steps: Object.values(StepEnum), + totalPrice: { + local: { price: 0, currency: "" }, + euro: { price: 0, currency: "" }, + }, + isSummaryOpen: false, setCurrentStep: (step) => set({ currentStep: step }), navigate: (step, updatedData) => set( @@ -166,6 +180,8 @@ export function initEditDetailsState( get().navigate(nextStep, updatedData) }) ), + toggleSummaryOpen: () => set({ isSummaryOpen: !get().isSummaryOpen }), + setTotalPrice: (totalPrice) => set({ totalPrice: totalPrice }), })) } From ee6aa8d1885cc5fad32d7299ce7f5e82ca117f83 Mon Sep 17 00:00:00 2001 From: Christel Westerberg <christel.westerberg@scandichotels.com> Date: Mon, 11 Nov 2024 10:15:47 +0100 Subject: [PATCH 04/98] feat: add mobile design to accordions --- .../EnterDetails/SectionAccordion/index.tsx | 27 +++----- .../sectionAccordion.module.css | 61 +++++++++++------ .../SelectedRoom/ToggleSidePeek.tsx | 2 + .../EnterDetails/SelectedRoom/index.tsx | 37 +++++------ .../SelectedRoom/selectedRoom.module.css | 65 ++++++++++++++----- .../BottomSheet/bottomSheet.module.css | 7 +- .../Summary/BottomSheet/index.tsx | 5 +- .../EnterDetails/Summary/index.tsx | 21 ++++-- .../EnterDetails/Summary/summary.module.css | 25 +++++++ 9 files changed, 164 insertions(+), 86 deletions(-) diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx index 9ece9b322..da8fbdcb5 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx +++ b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx @@ -5,7 +5,6 @@ import { useIntl } from "react-intl" import { useEnterDetailsStore } from "@/stores/enter-details" import { CheckIcon, ChevronDownIcon } from "@/components/Icons" -import Button from "@/components/TempDesignSystem/Button" import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" @@ -72,9 +71,10 @@ export default function SectionAccordion({ </div> </div> <div className={styles.main}> - <header className={styles.headerContainer}> - <div> + <header> + <button onClick={onModify} className={styles.modifyButton}> <Footnote + className={styles.title} asChild textTransform="uppercase" type="label" @@ -83,26 +83,17 @@ export default function SectionAccordion({ <h2>{header}</h2> </Footnote> <Subtitle - type="two" className={styles.selection} + type="two" color="uiTextHighContrast" > {title} </Subtitle> - </div> - {isComplete && !isOpen && ( - <Button - onClick={onModify} - theme="base" - size="small" - variant="icon" - intent="text" - wrapping - > - {intl.formatMessage({ id: "Modify" })}{" "} - <ChevronDownIcon color="burgundy" width={20} height={20} /> - </Button> - )} + + {isComplete && !isOpen && ( + <ChevronDownIcon className={styles.button} color="burgundy" /> + )} + </button> </header> <div className={styles.content}>{children}</div> </div> diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css index 7b9ea37dc..fc3de1764 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css +++ b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css @@ -2,25 +2,33 @@ position: relative; display: flex; flex-direction: row; - gap: var(--Spacing-x3); - + gap: var(--Spacing-x-one-and-half); padding-top: var(--Spacing-x3); } -.wrapper:not(:last-child)::after { - position: absolute; - left: 12px; - bottom: 0; - top: var(--Spacing-x5); - height: 100%; - content: ""; - border-left: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); -} - .wrapper:last-child .main { border-bottom: none; } +.modifyButton { + display: grid; + grid-template-areas: "title button" "selection button"; + cursor: pointer; + background-color: transparent; + border: none; + width: 100%; +} + +.title { + grid-area: title; + text-align: start; +} + +.button { + grid-area: button; + justify-self: flex-end; +} + .main { display: grid; gap: var(--Spacing-x3); @@ -31,21 +39,14 @@ grid-template-rows: 2em 0fr; } -.headerContainer { - display: flex; - justify-content: space-between; - align-items: center; -} - .selection { font-weight: 450; font-size: var(--typography-Title-4-fontSize); + grid-area: selection; } .iconWrapper { position: relative; - top: var(--Spacing-x1); - z-index: 1; } .circle { @@ -78,3 +79,23 @@ .content { overflow: hidden; } + +@media screen and (min-width: 1367px) { + .wrapper { + gap: var(--Spacing-x3); + } + + .iconWrapper { + top: var(--Spacing-x1); + } + + .wrapper:not(:last-child)::after { + position: absolute; + left: 12px; + bottom: 0; + top: var(--Spacing-x7); + height: 100%; + content: ""; + border-left: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); + } +} diff --git a/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek.tsx b/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek.tsx index cf14b995b..db0d5c097 100644 --- a/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek.tsx +++ b/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek.tsx @@ -4,6 +4,7 @@ import { useIntl } from "react-intl" import useSidePeekStore from "@/stores/sidepeek" +import ChevronRight from "@/components/Icons/ChevronRight" import Button from "@/components/TempDesignSystem/Button" import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek" @@ -28,6 +29,7 @@ export default function ToggleSidePeek({ wrapping > {intl.formatMessage({ id: "See room details" })}{" "} + <ChevronRight height="14" /> </Button> ) } diff --git a/components/HotelReservation/EnterDetails/SelectedRoom/index.tsx b/components/HotelReservation/EnterDetails/SelectedRoom/index.tsx index 4f673e923..64e9e0960 100644 --- a/components/HotelReservation/EnterDetails/SelectedRoom/index.tsx +++ b/components/HotelReservation/EnterDetails/SelectedRoom/index.tsx @@ -33,26 +33,25 @@ export default function SelectedRoom({ </div> <div className={styles.main}> <div className={styles.headerContainer}> - <div> - <Footnote - asChild - textTransform="uppercase" - type="label" - color="uiTextHighContrast" - > - <h2>{intl.formatMessage({ id: "Your room" })}</h2> - </Footnote> - <Subtitle - type="two" - className={styles.selection} - color="uiTextHighContrast" - > - {room.roomType}{" "} - <span className={styles.rate}>{`(${rateDescription})`}</span> - </Subtitle> - </div> - + <Footnote + className={styles.title} + asChild + textTransform="uppercase" + type="label" + color="uiTextHighContrast" + > + <h2>{intl.formatMessage({ id: "Your room" })}</h2> + </Footnote> + <Subtitle + type="two" + className={styles.description} + color="uiTextHighContrast" + > + {room.roomType}{" "} + <span className={styles.rate}>{rateDescription}</span> + </Subtitle> <Link + className={styles.button} color="burgundy" href={selectRateUrl} size="small" diff --git a/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css b/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css index f88a56606..03af9d904 100644 --- a/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css +++ b/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css @@ -2,43 +2,42 @@ position: relative; display: flex; flex-direction: row; - gap: var(--Spacing-x3); + gap: var(--Spacing-x-one-and-half); padding-top: var(--Spacing-x3); } -.wrapper::after { - position: absolute; - left: 12px; - bottom: 0; - top: var(--Spacing-x5); - height: 100%; - content: ""; - border-left: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); -} - .main { - display: grid; width: 100%; border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); padding-bottom: var(--Spacing-x3); - grid-template-rows: 2em 0fr; } .headerContainer { - display: flex; + display: grid; justify-content: space-between; align-items: center; + grid-template-areas: + "title button" + "description button"; } -.selection { +.title { + grid-area: title; +} + +.description { font-weight: 450; font-size: var(--typography-Title-4-fontSize); + grid-area: description; +} + +.button { + grid-area: button; + justify-self: flex-end; } .iconWrapper { position: relative; - top: var(--Spacing-x1); - z-index: 1; } .circle { @@ -57,9 +56,41 @@ .rate { color: var(--UI-Text-Placeholder); + display: block; } .details { display: flex; justify-content: flex-start; } + +@media screen and (min-width: 1367px) { + .wrapper { + gap: var(--Spacing-x3); + } + + .iconWrapper { + top: var(--Spacing-x1); + } + + .rate { + display: inline; + } + + .rate::before { + content: "("; + } + .rate::after { + content: ")"; + } + + .wrapper:not(:last-child)::after { + position: absolute; + left: 12px; + bottom: 0; + top: var(--Spacing-x7); + height: 100%; + content: ""; + border-left: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); + } +} diff --git a/components/HotelReservation/EnterDetails/Summary/BottomSheet/bottomSheet.module.css b/components/HotelReservation/EnterDetails/Summary/BottomSheet/bottomSheet.module.css index c7e82d8af..75f34791b 100644 --- a/components/HotelReservation/EnterDetails/Summary/BottomSheet/bottomSheet.module.css +++ b/components/HotelReservation/EnterDetails/Summary/BottomSheet/bottomSheet.module.css @@ -19,7 +19,7 @@ transition: 0.5s ease-in-out; } -.priceDetails { +.priceDetailsButton { display: block; border: none; background: none; @@ -28,6 +28,7 @@ transition: opacity 0.5s ease-in-out, padding 0.5s ease-in-out; + cursor: pointer; } .wrapper[data-open="true"] { @@ -38,12 +39,12 @@ grid-template-columns: 0fr 1fr; } -.wrapper[data-open="true"] .priceDetails { +.wrapper[data-open="true"] .priceDetailsButton { opacity: 0; padding: 0; } .content, -.priceDetails { +.priceDetailsButton { overflow: hidden; } diff --git a/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx b/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx index fa617b256..b01e928b8 100644 --- a/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx @@ -8,7 +8,6 @@ import { useEnterDetailsStore } from "@/stores/enter-details" import Button from "@/components/TempDesignSystem/Button" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import { formatNumber } from "@/utils/format" import styles from "./bottomSheet.module.css" @@ -30,14 +29,14 @@ export function SummaryBottomSheet({ children }: PropsWithChildren) { <button data-open={isSummaryOpen} onClick={toggleSummaryOpen} - className={styles.priceDetails} + className={styles.priceDetailsButton} > <Caption>{intl.formatMessage({ id: "Total price" })}:</Caption> <Subtitle> {intl.formatMessage( { id: "{amount} {currency}" }, { - amount: formatNumber(totalPrice.local.price), + amount: intl.formatNumber(totalPrice.local.price), currency: totalPrice.local.currency, } )} diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index 188ba94e3..738f26fd9 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -1,12 +1,14 @@ "use client" import { useEffect, useState } from "react" +import { ChevronDown } from "react-feather" import { useIntl } from "react-intl" import { dt } from "@/lib/dt" import { useEnterDetailsStore } from "@/stores/enter-details" -import { ArrowRightIcon, CloseIcon } from "@/components/Icons" +import { ArrowRightIcon } from "@/components/Icons" +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" @@ -116,18 +118,25 @@ export default function Summary({ }) }, [room.localPrice, room.euroPrice, setTotalPrice]) - const showToggleButton = true - return ( <section className={styles.summary}> - <header> - <Subtitle type="two">{intl.formatMessage({ id: "Summary" })}</Subtitle> + <header className={styles.header}> + <Subtitle className={styles.title} type="two"> + {intl.formatMessage({ id: "Summary" })} + </Subtitle> <Body className={styles.date} color="baseTextMediumContrast"> {dt(fromDate).locale(lang).format("ddd, D MMM")} <ArrowRightIcon color="peach80" height={15} width={15} /> {dt(toDate).locale(lang).format("ddd, D MMM")} ({nights}) </Body> - {showToggleButton ? <CloseIcon onClick={toggleSummaryOpen} /> : null} + <Button + intent="text" + size="small" + className={styles.chevronButton} + onClick={toggleSummaryOpen} + > + <ChevronDown height="20" width="20" /> + </Button> </header> <Divider color="primaryLightSubtle" /> <div className={styles.addOns}> diff --git a/components/HotelReservation/EnterDetails/Summary/summary.module.css b/components/HotelReservation/EnterDetails/Summary/summary.module.css index 3b933f9b8..426afbc7d 100644 --- a/components/HotelReservation/EnterDetails/Summary/summary.module.css +++ b/components/HotelReservation/EnterDetails/Summary/summary.module.css @@ -7,11 +7,28 @@ height: 100%; } +.header { + display: grid; + grid-template-areas: "title button" "date button"; +} + +.title { + grid-area: title; +} + +.chevronButton { + grid-area: button; + justify-self: end; + align-items: center; + margin-right: calc(0px - var(--Spacing-x2)); +} + .date { align-items: center; display: flex; gap: var(--Spacing-x1); justify-content: flex-start; + grid-area: date; } .link { @@ -47,4 +64,12 @@ .bottomDivider { display: block; } + + .header { + display: block; + } + + .chevronButton { + display: none; + } } From daed74481edbb67dafa9ee2161364996aa3ddc44 Mon Sep 17 00:00:00 2001 From: Christel Westerberg <christel.westerberg@scandichotels.com> Date: Mon, 11 Nov 2024 11:00:43 +0100 Subject: [PATCH 05/98] fix: handle submit from summary bottom sheet --- .../(standard)/[step]/enterDetailsLayout.css | 3 +- .../EnterDetails/Payment/index.tsx | 43 +++++++++++++------ .../EnterDetails/Payment/payment.module.css | 9 +++- .../SelectedRoom/selectedRoom.module.css | 2 +- .../BottomSheet/bottomSheet.module.css | 34 ++++++++++++--- .../Summary/BottomSheet/index.tsx | 20 ++++++--- stores/enter-details.ts | 5 +++ 7 files changed, 87 insertions(+), 29 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/enterDetailsLayout.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/enterDetailsLayout.css index 31054ccd4..74fa4b622 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/enterDetailsLayout.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/enterDetailsLayout.css @@ -11,7 +11,7 @@ .enter-details-layout__content { display: grid; gap: var(--Spacing-x3) var(--Spacing-x9); - margin: var(--Spacing-x5) auto 0; + margin: var(--Spacing-x3) var(--Spacing-x2) 0; /* simulates padding on viewport smaller than --max-width-navigation */ width: min( calc(100dvw - (var(--Spacing-x2) * 2)), @@ -31,6 +31,7 @@ .enter-details-layout__content { grid-template-columns: 1fr 340px; grid-template-rows: auto 1fr; + margin: var(--Spacing-x5) auto 0; } .enter-details-layout__summaryContainer { diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index 77821d8a2..d85fc3f77 100644 --- a/components/HotelReservation/EnterDetails/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -45,6 +45,8 @@ import { BreakfastPackageEnum } from "@/types/enums/breakfast" const maxRetries = 40 const retryInterval = 2000 +export const formId = "submit-booking" + function isPaymentMethodEnum(value: string): value is PaymentMethodEnum { return Object.values(PaymentMethodEnum).includes(value as PaymentMethodEnum) } @@ -59,10 +61,13 @@ export default function Payment({ const lang = useLang() const intl = useIntl() const queryParams = useSearchParams() - const { userData, roomData } = useEnterDetailsStore((state) => ({ - userData: state.userData, - roomData: state.roomData, - })) + const { userData, roomData, setIsSubmittingDisabled } = useEnterDetailsStore( + (state) => ({ + userData: state.userData, + roomData: state.roomData, + setIsSubmittingDisabled: state.setIsSubmittingDisabled, + }) + ) const { firstName, @@ -119,6 +124,16 @@ export default function Payment({ } }, [bookingStatus, router]) + useEffect(() => { + setIsSubmittingDisabled( + !methods.formState.isValid || methods.formState.isSubmitting + ) + }, [ + methods.formState.isValid, + methods.formState.isSubmitting, + setIsSubmittingDisabled, + ]) + function handleSubmit(data: PaymentFormData) { const allQueryParams = queryParams.size > 0 ? `?${queryParams.toString()}` : "" @@ -209,6 +224,7 @@ export default function Payment({ <form className={styles.paymentContainer} onSubmit={methods.handleSubmit(handleSubmit)} + id={formId} > {mustBeGuaranteed ? ( <section className={styles.section}> @@ -309,15 +325,16 @@ export default function Payment({ </Caption> </AriaLabel> </section> - <Button - type="submit" - className={styles.submitButton} - disabled={ - !methods.formState.isValid || methods.formState.isSubmitting - } - > - {intl.formatMessage({ id: "Complete booking & go to payment" })} - </Button> + <div className={styles.submitButton}> + <Button + type="submit" + disabled={ + !methods.formState.isValid || methods.formState.isSubmitting + } + > + {intl.formatMessage({ id: "Complete booking" })} + </Button> + </div> </form> </FormProvider> ) diff --git a/components/HotelReservation/EnterDetails/Payment/payment.module.css b/components/HotelReservation/EnterDetails/Payment/payment.module.css index 7f4f2899a..c8cc6184c 100644 --- a/components/HotelReservation/EnterDetails/Payment/payment.module.css +++ b/components/HotelReservation/EnterDetails/Payment/payment.module.css @@ -18,7 +18,7 @@ } .submitButton { - align-self: flex-start; + display: none; } .paymentContainer .link { @@ -31,3 +31,10 @@ flex-direction: row; gap: var(--Spacing-x-one-and-half); } + +@media screen and (min-width: 1367px) { + .submitButton { + display: flex; + align-self: flex-start; + } +} diff --git a/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css b/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css index 03af9d904..3149cf709 100644 --- a/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css +++ b/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css @@ -3,7 +3,6 @@ display: flex; flex-direction: row; gap: var(--Spacing-x-one-and-half); - padding-top: var(--Spacing-x3); } .main { @@ -67,6 +66,7 @@ @media screen and (min-width: 1367px) { .wrapper { gap: var(--Spacing-x3); + padding-top: var(--Spacing-x3); } .iconWrapper { diff --git a/components/HotelReservation/EnterDetails/Summary/BottomSheet/bottomSheet.module.css b/components/HotelReservation/EnterDetails/Summary/BottomSheet/bottomSheet.module.css index 75f34791b..789362b0c 100644 --- a/components/HotelReservation/EnterDetails/Summary/BottomSheet/bottomSheet.module.css +++ b/components/HotelReservation/EnterDetails/Summary/BottomSheet/bottomSheet.module.css @@ -13,8 +13,6 @@ grid-template-columns: 1fr auto; padding: var(--Spacing-x2) var(--Spacing-x3) var(--Spacing-x5) var(--Spacing-x3); - justify-content: space-between; - width: auto; align-items: flex-start; transition: 0.5s ease-in-out; } @@ -24,11 +22,9 @@ border: none; background: none; text-align: start; - opacity: 1; - transition: - opacity 0.5s ease-in-out, - padding 0.5s ease-in-out; + transition: padding 0.5s ease-in-out; cursor: pointer; + white-space: nowrap; } .wrapper[data-open="true"] { @@ -36,15 +32,39 @@ } .wrapper[data-open="true"] .bottomSheet { - grid-template-columns: 0fr 1fr; + grid-template-columns: 0fr auto; } .wrapper[data-open="true"] .priceDetailsButton { + animation: fadeOut 0.3s ease-out; opacity: 0; padding: 0; } +.wrapper[data-open="false"] .priceDetailsButton { + animation: fadeIn 0.8s ease-in; + opacity: 1; +} + .content, .priceDetailsButton { overflow: hidden; } + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} diff --git a/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx b/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx index b01e928b8..ac7921aec 100644 --- a/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx @@ -1,6 +1,6 @@ "use client" -import { PropsWithChildren, useState } from "react" +import { PropsWithChildren } from "react" import { useIntl } from "react-intl" import { useEnterDetailsStore } from "@/stores/enter-details" @@ -9,18 +9,20 @@ import Button from "@/components/TempDesignSystem/Button" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import { formId } from "../../Payment" + import styles from "./bottomSheet.module.css" export function SummaryBottomSheet({ children }: PropsWithChildren) { const intl = useIntl() - const { isSummaryOpen, toggleSummaryOpen, totalPrice } = useEnterDetailsStore( - (state) => ({ + const { isSummaryOpen, toggleSummaryOpen, totalPrice, isSubmittingDisabled } = + useEnterDetailsStore((state) => ({ isSummaryOpen: state.isSummaryOpen, toggleSummaryOpen: state.toggleSummaryOpen, totalPrice: state.totalPrice, - }) - ) + isSubmittingDisabled: state.isSubmittingDisabled, + })) return ( <div className={styles.wrapper} data-open={isSummaryOpen}> @@ -45,7 +47,13 @@ export function SummaryBottomSheet({ children }: PropsWithChildren) { {intl.formatMessage({ id: "See details" })} </Caption> </button> - <Button intent="primary" size="large" type="submit"> + <Button + intent="primary" + size="large" + type="submit" + disabled={isSubmittingDisabled} + form={formId} + > {intl.formatMessage({ id: "Complete booking" })} </Button> </div> diff --git a/stores/enter-details.ts b/stores/enter-details.ts index 18c0695fe..0649fd837 100644 --- a/stores/enter-details.ts +++ b/stores/enter-details.ts @@ -38,6 +38,7 @@ interface EnterDetailsState { selectRateUrl: string currentStep: StepEnum totalPrice: TotalPrice + isSubmittingDisabled: boolean isSummaryOpen: boolean isValid: Record<StepEnum, boolean> completeStep: (updatedData: Partial<EnterDetailsState["userData"]>) => void @@ -51,6 +52,7 @@ interface EnterDetailsState { setCurrentStep: (step: StepEnum) => void toggleSummaryOpen: () => void setTotalPrice: (totalPrice: TotalPrice) => void + setIsSubmittingDisabled: (isSubmittingDisabled: boolean) => void } export function initEditDetailsState( @@ -143,6 +145,7 @@ export function initEditDetailsState( euro: { price: 0, currency: "" }, }, isSummaryOpen: false, + isSubmittingDisabled: false, setCurrentStep: (step) => set({ currentStep: step }), navigate: (step, updatedData) => set( @@ -182,6 +185,8 @@ export function initEditDetailsState( ), toggleSummaryOpen: () => set({ isSummaryOpen: !get().isSummaryOpen }), setTotalPrice: (totalPrice) => set({ totalPrice: totalPrice }), + setIsSubmittingDisabled: (isSubmittingDisabled) => + set({ isSubmittingDisabled }), })) } From 5e2d2abd17b27a49234e7f57647d307130e19b4a Mon Sep 17 00:00:00 2001 From: Christel Westerberg <christel.westerberg@scandichotels.com> Date: Mon, 11 Nov 2024 15:27:40 +0100 Subject: [PATCH 06/98] fix: make incomplete steps in accordion look disabled --- .../(standard)/[step]/@summary/page.tsx | 12 +++++------ .../(standard)/[step]/enterDetailsLayout.css | 20 ++++++++++--------- .../(standard)/[step]/layout.tsx | 8 +++++--- .../EnterDetails/SectionAccordion/index.tsx | 11 +++++----- .../FlexibilityOption/PriceList/index.tsx | 2 +- .../Text/Footnote/footnote.module.css | 4 ++++ .../Text/Footnote/variants.ts | 1 + .../Text/Subtitle/subtitle.module.css | 2 +- .../Text/Subtitle/variants.ts | 2 +- server/routers/hotels/output.ts | 6 +++--- 10 files changed, 38 insertions(+), 30 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx index adfc99808..7002f07bb 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx @@ -45,12 +45,12 @@ export default async function SummaryPage({ user && availability.memberRate ? { local: { - price: availability.memberRate?.localPrice.pricePerStay, - currency: availability.memberRate?.localPrice.currency, + price: availability.memberRate.localPrice.pricePerStay, + currency: availability.memberRate.localPrice.currency, }, euro: { - price: availability.memberRate?.requestedPrice?.pricePerStay, - currency: availability.memberRate?.requestedPrice?.currency, + price: availability.memberRate.requestedPrice.pricePerStay, + currency: availability.memberRate.requestedPrice.currency, }, } : { @@ -59,8 +59,8 @@ export default async function SummaryPage({ currency: availability.publicRate?.localPrice.currency, }, euro: { - price: availability.publicRate?.requestedPrice?.pricePerStay, - currency: availability.publicRate?.requestedPrice?.currency, + price: availability.publicRate?.requestedPrice.pricePerStay, + currency: availability.publicRate?.requestedPrice.currency, }, } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/enterDetailsLayout.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/enterDetailsLayout.css index 74fa4b622..0322e44a7 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/enterDetailsLayout.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/enterDetailsLayout.css @@ -8,30 +8,32 @@ background-color: var(--Scandic-Brand-Warm-White); } -.enter-details-layout__content { +.enter-details-layout__container { display: grid; gap: var(--Spacing-x3) var(--Spacing-x9); - margin: var(--Spacing-x3) var(--Spacing-x2) 0; /* simulates padding on viewport smaller than --max-width-navigation */ - width: min( - calc(100dvw - (var(--Spacing-x2) * 2)), - var(--max-width-navigation) - ); +} + +.enter-details-layout__content { + margin: var(--Spacing-x3) var(--Spacing-x2) 0; } .enter-details-layout__summaryContainer { - position: fixed; - z-index: 1; + position: sticky; bottom: 0; left: 0; right: 0; } @media screen and (min-width: 1367px) { - .enter-details-layout__content { + .enter-details-layout__container { grid-template-columns: 1fr 340px; grid-template-rows: auto 1fr; margin: var(--Spacing-x5) auto 0; + width: min( + calc(100dvw - (var(--Spacing-x2) * 2)), + var(--max-width-navigation) + ); } .enter-details-layout__summaryContainer { diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx index 2b5a6241f..fbd462544 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx @@ -28,9 +28,11 @@ export default async function StepLayout({ <EnterDetailsProvider step={params.step} isMember={!!user}> <main className="enter-details-layout__layout"> {hotelHeader} - <div className={"enter-details-layout__content"}> - {children} - <aside className={"enter-details-layout__summaryContainer"}>{summary}</aside> + <div className={"enter-details-layout__container"}> + <div className={"enter-details-layout__content"}>{children}</div> + <aside className={"enter-details-layout__summaryContainer"}> + {summary} + </aside> </div> </main> </EnterDetailsProvider> diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx index da8fbdcb5..dee985295 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx +++ b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx @@ -61,6 +61,9 @@ export default function SectionAccordion({ function onModify() { navigate(step) } + + const textColor = + isComplete || isOpen ? "uiTextHighContrast" : "baseTextDisabled" return ( <section className={styles.wrapper} data-open={isOpen} data-step={step}> <div className={styles.iconWrapper}> @@ -78,15 +81,11 @@ export default function SectionAccordion({ asChild textTransform="uppercase" type="label" - color="uiTextHighContrast" + color={textColor} > <h2>{header}</h2> </Footnote> - <Subtitle - className={styles.selection} - type="two" - color="uiTextHighContrast" - > + <Subtitle className={styles.selection} type="two" color={textColor}> {title} </Subtitle> diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx index c3791d57f..96be9795b 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx @@ -81,7 +81,7 @@ export default function PriceList({ </Body> </div> ) : ( - <Subtitle type="two" color="disabled"> + <Subtitle type="two" color="baseTextDisabled"> {intl.formatMessage({ id: "n/a" })} </Subtitle> )} diff --git a/components/TempDesignSystem/Text/Footnote/footnote.module.css b/components/TempDesignSystem/Text/Footnote/footnote.module.css index 31fd9b16b..cddf0ef0d 100644 --- a/components/TempDesignSystem/Text/Footnote/footnote.module.css +++ b/components/TempDesignSystem/Text/Footnote/footnote.module.css @@ -76,3 +76,7 @@ .white { color: var(--Main-Grey-White); } + +.baseTextDisabled { + color: var(--Base-Text-Disabled); +} diff --git a/components/TempDesignSystem/Text/Footnote/variants.ts b/components/TempDesignSystem/Text/Footnote/variants.ts index 5b9c2d4b1..a7595df73 100644 --- a/components/TempDesignSystem/Text/Footnote/variants.ts +++ b/components/TempDesignSystem/Text/Footnote/variants.ts @@ -18,6 +18,7 @@ const config = { uiTextHighContrast: styles.uiTextHighContrast, uiTextPlaceholder: styles.uiTextPlaceholder, white: styles.white, + baseTextDisabled: styles.baseTextDisabled, }, textAlign: { center: styles.center, diff --git a/components/TempDesignSystem/Text/Subtitle/subtitle.module.css b/components/TempDesignSystem/Text/Subtitle/subtitle.module.css index 66eeec8c3..8207d7523 100644 --- a/components/TempDesignSystem/Text/Subtitle/subtitle.module.css +++ b/components/TempDesignSystem/Text/Subtitle/subtitle.module.css @@ -79,6 +79,6 @@ color: var(--Scandic-Brand-Scandic-Red); } -.disabled { +.baseTextDisabled { color: var(--Base-Text-Disabled); } diff --git a/components/TempDesignSystem/Text/Subtitle/variants.ts b/components/TempDesignSystem/Text/Subtitle/variants.ts index f0e88b3bc..18a0880a8 100644 --- a/components/TempDesignSystem/Text/Subtitle/variants.ts +++ b/components/TempDesignSystem/Text/Subtitle/variants.ts @@ -7,7 +7,7 @@ const config = { color: { black: styles.black, burgundy: styles.burgundy, - disabled: styles.disabled, + baseTextDisabled: styles.baseTextDisabled, pale: styles.pale, baseTextMediumContrast: styles.baseTextMediumContrast, uiTextHighContrast: styles.uiTextHighContrast, diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 1859284e4..23282b04a 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -549,7 +549,7 @@ export const productTypePriceSchema = z.object({ rateCode: z.string(), rateType: z.string().optional(), localPrice: priceSchema, - requestedPrice: priceSchema.optional(), + requestedPrice: priceSchema, }) const productSchema = z.object({ @@ -671,7 +671,7 @@ export const apiCitiesByCountrySchema = z.object({ }) export interface CitiesByCountry - extends z.output<typeof apiCitiesByCountrySchema> { } + extends z.output<typeof apiCitiesByCountrySchema> {} export type CitiesGroupedByCountry = Record<string, CitiesByCountry["data"]> export const apiCountriesSchema = z.object({ @@ -701,7 +701,7 @@ export const apiCountriesSchema = z.object({ }), }) -export interface Countries extends z.output<typeof apiCountriesSchema> { } +export interface Countries extends z.output<typeof apiCountriesSchema> {} export const apiLocationCitySchema = z.object({ attributes: z.object({ From 32d12bae58ddeaa1d38ba14d61ff09f1ff235c33 Mon Sep 17 00:00:00 2001 From: Christel Westerberg <christel.westerberg@scandichotels.com> Date: Tue, 12 Nov 2024 08:57:45 +0100 Subject: [PATCH 07/98] fix: refacotr away optional pricing --- .../(standard)/[step]/@summary/page.tsx | 8 +++---- .../EnterDetails/Summary/index.tsx | 4 ++-- server/routers/hotels/output.ts | 21 +++++++------------ server/routers/hotels/query.ts | 14 +++++++++---- .../enterDetails/bookingData.ts | 4 ++-- 5 files changed, 25 insertions(+), 26 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx index 7002f07bb..ccda720a2 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx @@ -55,12 +55,12 @@ export default async function SummaryPage({ } : { local: { - price: availability.publicRate?.localPrice.pricePerStay, - currency: availability.publicRate?.localPrice.currency, + price: availability.publicRate.localPrice.pricePerStay, + currency: availability.publicRate.localPrice.currency, }, euro: { - price: availability.publicRate?.requestedPrice.pricePerStay, - currency: availability.publicRate?.requestedPrice.currency, + price: availability.publicRate.requestedPrice.pricePerStay, + currency: availability.publicRate.requestedPrice.currency, }, } diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index 738f26fd9..72a4dded8 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -109,11 +109,11 @@ export default function Summary({ setTotalPrice({ local: { price: parsePrice(room.localPrice.price), - currency: room.localPrice.currency!, + currency: room.localPrice.currency, }, euro: { price: parsePrice(room.euroPrice.price), - currency: room.euroPrice.currency!, + currency: room.euroPrice.currency, }, }) }, [room.localPrice, room.euroPrice, setTotalPrice]) diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 23282b04a..08553d882 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -554,15 +554,14 @@ export const productTypePriceSchema = z.object({ const productSchema = z.object({ productType: z.object({ - public: productTypePriceSchema.optional(), + public: productTypePriceSchema, member: productTypePriceSchema.optional(), }), }) const roomConfigurationSchema = z.object({ status: z.string(), - // TODO: Remove the optional when the API change has been deployed - roomTypeCode: z.string().optional(), + roomTypeCode: z.string(), roomType: z.string(), roomsLeft: z.number(), features: z.array( @@ -825,17 +824,11 @@ export const apiLocationsSchema = z.object({ ), }) -const breakfastPackagePriceSchema = z - .object({ - currency: z.nativeEnum(CurrencyEnum), - price: z.string(), - totalPrice: z.string(), - }) - .default({ - currency: CurrencyEnum.SEK, - price: "0", - totalPrice: "0", - }) // TODO: Remove optional and default when the API change has been deployed +const breakfastPackagePriceSchema = z.object({ + currency: z.nativeEnum(CurrencyEnum), + price: z.string(), + totalPrice: z.string(), +}) export const breakfastPackageSchema = z.object({ code: z.string(), diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 882eefb6b..192d88e33 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -733,9 +733,15 @@ export const hotelQueryRouter = router({ const rateTypes = selectedRoom.products.find( (rate) => - rate.productType.public?.rateCode === rateCode || + rate.productType.public.rateCode === rateCode || rate.productType.member?.rateCode === rateCode - )?.productType + ) + + if (!rateTypes) { + console.error("No matching rate found") + return null + } + const rates = rateTypes.productType const mustBeGuaranteed = validateAvailabilityData.data.rateDefinitions.filter( @@ -785,8 +791,8 @@ export const hotelQueryRouter = router({ selectedRoom, mustBeGuaranteed, cancellationText, - memberRate: rateTypes?.member, - publicRate: rateTypes?.public, + memberRate: rates?.member, + publicRate: rates.public, bedTypes, } }), diff --git a/types/components/hotelReservation/enterDetails/bookingData.ts b/types/components/hotelReservation/enterDetails/bookingData.ts index 6aad97085..89ca94efb 100644 --- a/types/components/hotelReservation/enterDetails/bookingData.ts +++ b/types/components/hotelReservation/enterDetails/bookingData.ts @@ -16,8 +16,8 @@ export interface BookingData { } type Price = { - price?: string - currency?: string + price: string + currency: string } export type RoomsData = { From 060f6b6a82196b20baf8806ac8524536963f075a Mon Sep 17 00:00:00 2001 From: Christel Westerberg <christel.westerberg@scandichotels.com> Date: Tue, 12 Nov 2024 10:09:59 +0100 Subject: [PATCH 08/98] fix: breakout selector for enter details in summary --- .../EnterDetails/Summary/index.tsx | 32 +++++++++++-------- stores/enter-details.ts | 2 +- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index 72a4dded8..818c54e8c 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -5,7 +5,7 @@ import { ChevronDown } from "react-feather" import { useIntl } from "react-intl" import { dt } from "@/lib/dt" -import { useEnterDetailsStore } from "@/stores/enter-details" +import { EnterDetailsState, useEnterDetailsStore } from "@/stores/enter-details" import { ArrowRightIcon } from "@/components/Icons" import Button from "@/components/TempDesignSystem/Button" @@ -23,6 +23,18 @@ import { RoomsData } from "@/types/components/hotelReservation/enterDetails/book import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast" import { BreakfastPackageEnum } from "@/types/enums/breakfast" +function storeSelector(state: EnterDetailsState) { + return { + fromDate: state.roomData.fromDate, + toDate: state.roomData.toDate, + bedType: state.userData.bedType, + breakfast: state.userData.breakfast, + toggleSummaryOpen: state.toggleSummaryOpen, + setTotalPrice: state.setTotalPrice, + totalPrice: state.totalPrice, + } +} + function parsePrice(price: string | undefined) { return price ? parseInt(price) : 0 } @@ -48,15 +60,7 @@ export default function Summary({ setTotalPrice, totalPrice, toggleSummaryOpen, - } = useEnterDetailsStore((state) => ({ - fromDate: state.roomData.fromDate, - toDate: state.roomData.toDate, - bedType: state.userData.bedType, - breakfast: state.userData.breakfast, - toggleSummaryOpen: state.toggleSummaryOpen, - setTotalPrice: state.setTotalPrice, - totalPrice: state.totalPrice, - })) + } = useEnterDetailsStore(storeSelector) const diff = dt(toDate).diff(fromDate, "days") @@ -79,11 +83,11 @@ export default function Summary({ setTotalPrice({ local: { price: parsePrice(room.localPrice.price), - currency: room.localPrice.currency!, + currency: room.localPrice.currency, }, euro: { price: parsePrice(room.euroPrice.price), - currency: room.euroPrice.currency!, + currency: room.euroPrice.currency, }, }) } else { @@ -92,13 +96,13 @@ export default function Summary({ price: parsePrice(room.localPrice.price) + parsePrice(breakfast.localPrice.totalPrice), - currency: room.localPrice.currency!, + currency: room.localPrice.currency, }, euro: { price: parsePrice(room.euroPrice.price) + parsePrice(breakfast.requestedPrice.totalPrice), - currency: room.euroPrice.currency!, + currency: room.euroPrice.currency, }, }) } diff --git a/stores/enter-details.ts b/stores/enter-details.ts index 0649fd837..374f1dc89 100644 --- a/stores/enter-details.ts +++ b/stores/enter-details.ts @@ -28,7 +28,7 @@ type TotalPrice = { euro: { price: number; currency: string } } -interface EnterDetailsState { +export interface EnterDetailsState { userData: { bedType: BedTypeSchema | undefined breakfast: BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST | undefined From 169751c5a6881b737f9740c3c69264f2cc763247 Mon Sep 17 00:00:00 2001 From: Tobias Johansson <tobias.johansson@scandichotels.com> Date: Tue, 12 Nov 2024 09:37:17 +0000 Subject: [PATCH 09/98] Merged in fix/SW-878-alternate-payment-options (pull request #878) fix(SW-878): fix issue with alternate pay options showing when flex rate is selected * fix(SW-878): fix issue with alternate pay options showing when flex rate is selected Approved-by: Christel Westerberg Approved-by: Simon.Emanuelsson --- .../(public)/hotelreservation/(standard)/[step]/page.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 37f1412ff..982157ceb 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -62,10 +62,6 @@ export default async function StepPage({ roomTypeCode, }) - const hotelData = await getHotelData({ - hotelId, - language: lang, - }) const roomAvailability = await getSelectedRoomAvailability({ hotelId, adults, @@ -75,6 +71,11 @@ export default async function StepPage({ rateCode, roomTypeCode, }) + const hotelData = await getHotelData({ + hotelId, + language: lang, + isCardOnlyPayment: roomAvailability?.mustBeGuaranteed, + }) const breakfastPackages = await getBreakfastPackages(breakfastInput) const user = await getProfileSafely() const savedCreditCards = await getCreditCardsSafely() From b73421dbded21f02d1f28c934daeaf395d294c58 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Tue, 12 Nov 2024 09:19:11 +0100 Subject: [PATCH 10/98] feat(SW-344): Hotel list in mobile --- .../select-hotel/@modal/(.)map/page.tsx | 7 +-- .../(standard)/select-hotel/utils.ts | 19 -------- .../HotelCardDialog/index.tsx | 6 +-- .../HotelCardDialogListing/index.tsx | 47 +++++++++++++++++++ .../HotelCardDialogListing/utils.ts | 21 +++++++++ .../HotelListing/hotelListing.module.css | 36 ++++++++++++++ .../SelectHotelMap/HotelListing/index.tsx | 22 +++++---- .../hotelListingMapContent.module.css | 10 ++++ .../HotelListingMapContent/index.tsx | 23 ++++----- .../hotelReservation/selectHotel/map.ts | 2 +- 10 files changed, 146 insertions(+), 47 deletions(-) create mode 100644 components/HotelReservation/HotelCardDialogListing/index.tsx create mode 100644 components/HotelReservation/HotelCardDialogListing/utils.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx index 0f636136f..6d8e62ed3 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx @@ -3,6 +3,7 @@ import { notFound } from "next/navigation" import { env } from "@/env/server" import { getLocations } from "@/lib/trpc/memoizedRequests" +import { getHotelPins } from "@/components/HotelReservation/HotelCardDialogListing/utils" import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap" import { generateChildrenString, @@ -11,11 +12,7 @@ import { import { MapModal } from "@/components/MapModal" import { setLang } from "@/i18n/serverContext" -import { - fetchAvailableHotels, - getCentralCoordinates, - getHotelPins, -} from "../../utils" +import { fetchAvailableHotels, getCentralCoordinates } from "../../utils" import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" import type { LangParams, PageArgs } from "@/types/params" diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts index ee541bb7a..5b5fc958e 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts @@ -88,25 +88,6 @@ export function getFiltersFromHotels(hotels: HotelData[]): CategorizedFilters { ) } -export function getHotelPins(hotels: HotelData[]): HotelPin[] { - return hotels.map((hotel) => ({ - coordinates: { - lat: hotel.hotelData.location.latitude, - lng: hotel.hotelData.location.longitude, - }, - name: hotel.hotelData.name, - publicPrice: hotel.price?.regularAmount ?? null, - memberPrice: hotel.price?.memberAmount ?? null, - currency: hotel.price?.currency || null, - images: [ - hotel.hotelData.hotelContent.images, - ...(hotel.hotelData.gallery?.heroImages ?? []), - ], - amenities: hotel.hotelData.detailedFacilities.slice(0, 3), - ratings: hotel.hotelData.ratings?.tripAdvisor.rating ?? null, - })) -} - export function getCentralCoordinates(hotels: HotelPin[]) { const centralCoordinates = hotels.reduce( (acc, pin) => { diff --git a/components/HotelReservation/HotelCardDialog/index.tsx b/components/HotelReservation/HotelCardDialog/index.tsx index adeb0e176..91ce9e548 100644 --- a/components/HotelReservation/HotelCardDialog/index.tsx +++ b/components/HotelReservation/HotelCardDialog/index.tsx @@ -17,13 +17,13 @@ import styles from "./hotelCardDialog.module.css" import type { HotelCardDialogProps } from "@/types/components/hotelReservation/selectHotel/map" export default function HotelCardDialog({ - pin, + data, isOpen, handleClose, }: HotelCardDialogProps) { const intl = useIntl() - if (!pin) { + if (!data) { return null } @@ -35,7 +35,7 @@ export default function HotelCardDialog({ amenities, images, ratings, - } = pin + } = data const firstImage = images[0]?.imageSizes?.small const altText = images[0]?.metaData?.altText diff --git a/components/HotelReservation/HotelCardDialogListing/index.tsx b/components/HotelReservation/HotelCardDialogListing/index.tsx new file mode 100644 index 000000000..1a2097ada --- /dev/null +++ b/components/HotelReservation/HotelCardDialogListing/index.tsx @@ -0,0 +1,47 @@ +"use client" + +import { useEffect, useRef } from "react" + +import HotelCardDialog from "../HotelCardDialog" +import { getHotelPins } from "./utils" + +import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" + +export default function HotelCardDialogListing({ + hotels, + activeCard, +}: { + hotels: HotelData[] + activeCard: string | null | undefined +}) { + const hotelsPinData = getHotelPins(hotels) + const activeCardRef = useRef<HTMLDivElement | null>(null) + + useEffect(() => { + if (activeCardRef.current) { + activeCardRef.current.scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "center", + }) + } + }, [activeCard]) + + return ( + <> + {hotelsPinData?.length && + hotelsPinData.map((data) => { + const isActive = data.name === activeCard + return ( + <div key={data.name} ref={isActive ? activeCardRef : null}> + <HotelCardDialog + data={data} + isOpen={!!activeCard} + handleClose={() => {}} + /> + </div> + ) + })} + </> + ) +} diff --git a/components/HotelReservation/HotelCardDialogListing/utils.ts b/components/HotelReservation/HotelCardDialogListing/utils.ts new file mode 100644 index 000000000..b63b9b7ce --- /dev/null +++ b/components/HotelReservation/HotelCardDialogListing/utils.ts @@ -0,0 +1,21 @@ +import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" +import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map" + +export function getHotelPins(hotels: HotelData[]): HotelPin[] { + return hotels.map((hotel) => ({ + coordinates: { + lat: hotel.hotelData.location.latitude, + lng: hotel.hotelData.location.longitude, + }, + name: hotel.hotelData.name, + publicPrice: hotel.price?.regularAmount ?? null, + memberPrice: hotel.price?.memberAmount ?? null, + currency: hotel.price?.currency || null, + images: [ + hotel.hotelData.hotelContent.images, + ...(hotel.hotelData.gallery?.heroImages ?? []), + ], + amenities: hotel.hotelData.detailedFacilities.slice(0, 3), + ratings: hotel.hotelData.ratings?.tripAdvisor.rating ?? null, + })) +} diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/hotelListing.module.css b/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/hotelListing.module.css index 253df80d6..3ba5cf884 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/hotelListing.module.css +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/hotelListing.module.css @@ -2,6 +2,37 @@ display: none; } +.hotelListingMobile { + display: none; + align-items: flex-end; + overflow-x: auto; + position: absolute; + bottom: 0px; + left: 0; + right: 0; + z-index: 10; + height: 350px; + gap: var(--Spacing-x1); +} + +.hotelListingMobile[data-open="true"] { + display: flex; +} + +.hotelListingMobile dialog { + position: relative; + padding: 0; + margin: 0; +} + +.hotelListingMobile > div:first-child { + margin-left: 16px; +} + +.hotelListingMobile > div:last-child { + margin-right: 16px; +} + @media (min-width: 768px) { .hotelListing { display: block; @@ -9,4 +40,9 @@ overflow-y: auto; padding-top: var(--Spacing-x2); } + + .hotelListingMobile, + .hotelListingMobile[data-open="true"] { + display: none; + } } diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/index.tsx b/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/index.tsx index 65bd476e6..7f237d62e 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/index.tsx +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/index.tsx @@ -1,5 +1,6 @@ "use client" +import HotelCardDialogListing from "@/components/HotelReservation/HotelCardDialogListing" import HotelCardListing from "@/components/HotelReservation/HotelCardListing" import styles from "./hotelListing.module.css" @@ -13,13 +14,18 @@ export default function HotelListing({ onHotelCardHover, }: HotelListingProps) { return ( - <div className={styles.hotelListing}> - <HotelCardListing - hotelData={hotels} - type={HotelCardListingTypeEnum.MapListing} - activeCard={activeHotelPin} - onHotelCardHover={onHotelCardHover} - /> - </div> + <> + <div className={styles.hotelListing}> + <HotelCardListing + hotelData={hotels} + type={HotelCardListingTypeEnum.MapListing} + activeCard={activeHotelPin} + onHotelCardHover={onHotelCardHover} + /> + </div> + <div className={styles.hotelListingMobile} data-open={!!activeHotelPin}> + <HotelCardDialogListing hotels={hotels} activeCard={activeHotelPin} /> + </div> + </> ) } diff --git a/components/Maps/InteractiveMap/HotelListingMapContent/hotelListingMapContent.module.css b/components/Maps/InteractiveMap/HotelListingMapContent/hotelListingMapContent.module.css index 359edad50..dc35427cb 100644 --- a/components/Maps/InteractiveMap/HotelListingMapContent/hotelListingMapContent.module.css +++ b/components/Maps/InteractiveMap/HotelListingMapContent/hotelListingMapContent.module.css @@ -8,6 +8,10 @@ min-width: 109px !important; /* Overwriting the default width of the @vis.gl/react-google-maps AdvancedMarker */ } +.dialogContainer { + display: none; +} + .pin { position: absolute; top: 0; @@ -67,3 +71,9 @@ .card.active { display: block; } + +@media (min-width: 768px) { + .dialogContainer { + display: block; + } +} diff --git a/components/Maps/InteractiveMap/HotelListingMapContent/index.tsx b/components/Maps/InteractiveMap/HotelListingMapContent/index.tsx index f121687ef..c3b83edb3 100644 --- a/components/Maps/InteractiveMap/HotelListingMapContent/index.tsx +++ b/components/Maps/InteractiveMap/HotelListingMapContent/index.tsx @@ -50,17 +50,18 @@ export default function HotelListingMapContent({ ) } > - <HotelCardDialog - isOpen={isActiveOrHovered} - handleClose={(event: { stopPropagation: () => void }) => { - event.stopPropagation() - if (activeHotelPin === pin.name) { - toggleActiveHotelPin(null) - } - }} - pin={pin} - /> - + <div className={styles.dialogContainer}> + <HotelCardDialog + isOpen={isActiveOrHovered} + handleClose={(event: { stopPropagation: () => void }) => { + event.stopPropagation() + if (activeHotelPin === pin.name) { + toggleActiveHotelPin(null) + } + }} + data={pin} + /> + </div> <span className={`${styles.pin} ${isActiveOrHovered ? styles.active : ""}`} > diff --git a/types/components/hotelReservation/selectHotel/map.ts b/types/components/hotelReservation/selectHotel/map.ts index 28965807a..64e13dad9 100644 --- a/types/components/hotelReservation/selectHotel/map.ts +++ b/types/components/hotelReservation/selectHotel/map.ts @@ -50,6 +50,6 @@ export interface HotelListingMapContentProps { export interface HotelCardDialogProps { isOpen: boolean - pin: HotelPin + data: HotelPin handleClose: (event: { stopPropagation: () => void }) => void } From 96a52778810c102d2b37050c74a1a269c8da10a5 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Tue, 12 Nov 2024 09:31:20 +0100 Subject: [PATCH 11/98] feat(SW-344): set active pin on scroll --- .../HotelCardDialogListing/index.tsx | 48 ++++++++++++++++--- .../SelectHotelMap/HotelListing/index.tsx | 10 ++-- .../SelectHotel/SelectHotelMap/index.tsx | 2 +- .../hotelReservation/selectHotel/map.ts | 2 +- 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/components/HotelReservation/HotelCardDialogListing/index.tsx b/components/HotelReservation/HotelCardDialogListing/index.tsx index 1a2097ada..a91f68041 100644 --- a/components/HotelReservation/HotelCardDialogListing/index.tsx +++ b/components/HotelReservation/HotelCardDialogListing/index.tsx @@ -1,22 +1,54 @@ "use client" -import { useEffect, useRef } from "react" +import { useCallback, useEffect, useRef } from "react" import HotelCardDialog from "../HotelCardDialog" import { getHotelPins } from "./utils" import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" +interface HotelCardDialogListingProps { + hotels: HotelData[] + activeCard: string | null | undefined + onActiveCardChange: (hotelName: string | null) => void +} + export default function HotelCardDialogListing({ hotels, activeCard, -}: { - hotels: HotelData[] - activeCard: string | null | undefined -}) { + onActiveCardChange, +}: HotelCardDialogListingProps) { const hotelsPinData = getHotelPins(hotels) const activeCardRef = useRef<HTMLDivElement | null>(null) + const handleIntersection = useCallback( + (entries: IntersectionObserverEntry[]) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const cardName = entry.target.getAttribute("data-name") + if (cardName) { + onActiveCardChange(cardName) + } + } + }) + }, + [onActiveCardChange] + ) + + useEffect(() => { + const observer = new IntersectionObserver(handleIntersection, { + root: null, + threshold: 0.5, // Adjust threshold as needed + }) + + const elements = document.querySelectorAll("[data-name]") + elements.forEach((el) => observer.observe(el)) + + return () => { + elements.forEach((el) => observer.unobserve(el)) + } + }, [handleIntersection]) + useEffect(() => { if (activeCardRef.current) { activeCardRef.current.scrollIntoView({ @@ -33,7 +65,11 @@ export default function HotelCardDialogListing({ hotelsPinData.map((data) => { const isActive = data.name === activeCard return ( - <div key={data.name} ref={isActive ? activeCardRef : null}> + <div + key={data.name} + ref={isActive ? activeCardRef : null} + data-name={data.name} + > <HotelCardDialog data={data} isOpen={!!activeCard} diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/index.tsx b/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/index.tsx index 7f237d62e..71d324155 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/index.tsx +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/index.tsx @@ -11,7 +11,7 @@ import type { HotelListingProps } from "@/types/components/hotelReservation/sele export default function HotelListing({ hotels, activeHotelPin, - onHotelCardHover, + setActiveHotelPin, }: HotelListingProps) { return ( <> @@ -20,11 +20,15 @@ export default function HotelListing({ hotelData={hotels} type={HotelCardListingTypeEnum.MapListing} activeCard={activeHotelPin} - onHotelCardHover={onHotelCardHover} + onHotelCardHover={setActiveHotelPin} /> </div> <div className={styles.hotelListingMobile} data-open={!!activeHotelPin}> - <HotelCardDialogListing hotels={hotels} activeCard={activeHotelPin} /> + <HotelCardDialogListing + hotels={hotels} + activeCard={activeHotelPin} + onActiveCardChange={setActiveHotelPin} + /> </div> </> ) diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx b/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx index 3367d25b7..f92ec1895 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx @@ -95,7 +95,7 @@ export default function SelectHotelMap({ <HotelListing hotels={hotels} activeHotelPin={activeHotelPin} - onHotelCardHover={setActiveHotelPin} + setActiveHotelPin={setActiveHotelPin} /> {showBackToTop && ( <Button diff --git a/types/components/hotelReservation/selectHotel/map.ts b/types/components/hotelReservation/selectHotel/map.ts index 64e13dad9..b913be9b1 100644 --- a/types/components/hotelReservation/selectHotel/map.ts +++ b/types/components/hotelReservation/selectHotel/map.ts @@ -13,7 +13,7 @@ import type { Coordinates } from "@/types/components/maps/coordinates" export interface HotelListingProps { hotels: HotelData[] activeHotelPin?: string | null - onHotelCardHover?: (hotelName: string | null) => void + setActiveHotelPin: (hotelName: string | null) => void } export interface SelectHotelMapProps { From 87b999676b928e341abc51e43c83ae192a7cf6e0 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Tue, 12 Nov 2024 14:33:37 +0100 Subject: [PATCH 12/98] feat(SW-344) Correct position of pins in mobile --- .../select-hotel/@modal/(.)map/page.tsx | 6 +--- .../(standard)/select-hotel/utils.ts | 16 ---------- .../Header/MainMenu/MobileMenu/index.tsx | 2 +- .../MainMenu/MyPagesMobileMenu/index.tsx | 2 +- .../HotelCard/hotelCard.module.css | 12 ++++++++ .../HotelReservation/HotelCard/index.tsx | 16 +++++++++- .../HotelCardDialogListing/index.tsx | 29 +++++++++++-------- .../SelectHotel/SelectHotelMap/index.tsx | 28 +++++++++++++----- .../SelectHotel/SelectHotelMap/utils.ts | 17 +++++++++++ hooks/useMediaQuery.ts | 21 -------------- package-lock.json | 16 +++++++++- package.json | 1 + .../hotelReservation/selectHotel/map.ts | 8 +++-- 13 files changed, 106 insertions(+), 68 deletions(-) create mode 100644 components/HotelReservation/SelectHotel/SelectHotelMap/utils.ts delete mode 100644 hooks/useMediaQuery.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx index 6d8e62ed3..fd9cc33c5 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx @@ -12,7 +12,7 @@ import { import { MapModal } from "@/components/MapModal" import { setLang } from "@/i18n/serverContext" -import { fetchAvailableHotels, getCentralCoordinates } from "../../utils" +import { fetchAvailableHotels } from "../../utils" import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" import type { LangParams, PageArgs } from "@/types/params" @@ -58,16 +58,12 @@ export default async function SelectHotelMapPage({ const hotelPins = getHotelPins(hotels) - const centralCoordinates = getCentralCoordinates(hotelPins) - return ( <MapModal> <SelectHotelMap apiKey={googleMapsApiKey} - coordinates={centralCoordinates} hotelPins={hotelPins} mapId={googleMapId} - isModal={true} hotels={hotels} /> </MapModal> diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts index 5b5fc958e..a021d9355 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts @@ -87,19 +87,3 @@ export function getFiltersFromHotels(hotels: HotelData[]): CategorizedFilters { { facilityFilters: [], surroundingsFilters: [] } ) } - -export function getCentralCoordinates(hotels: HotelPin[]) { - const centralCoordinates = hotels.reduce( - (acc, pin) => { - acc.lat += pin.coordinates.lat - acc.lng += pin.coordinates.lng - return acc - }, - { lat: 0, lng: 0 } - ) - - centralCoordinates.lat /= hotels.length - centralCoordinates.lng /= hotels.length - - return centralCoordinates -} diff --git a/components/Header/MainMenu/MobileMenu/index.tsx b/components/Header/MainMenu/MobileMenu/index.tsx index ae79675dd..272c4b231 100644 --- a/components/Header/MainMenu/MobileMenu/index.tsx +++ b/components/Header/MainMenu/MobileMenu/index.tsx @@ -3,13 +3,13 @@ import { Suspense, useEffect } from "react" import { Dialog, Modal } from "react-aria-components" import { useIntl } from "react-intl" +import { useMediaQuery } from "usehooks-ts" import useDropdownStore from "@/stores/main-menu" import { GiftIcon, SearchIcon, ServiceIcon } from "@/components/Icons" import LanguageSwitcher from "@/components/LanguageSwitcher" import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" -import useMediaQuery from "@/hooks/useMediaQuery" import HeaderLink from "../../HeaderLink" diff --git a/components/Header/MainMenu/MyPagesMobileMenu/index.tsx b/components/Header/MainMenu/MyPagesMobileMenu/index.tsx index 466014bd2..e0711fe5c 100644 --- a/components/Header/MainMenu/MyPagesMobileMenu/index.tsx +++ b/components/Header/MainMenu/MyPagesMobileMenu/index.tsx @@ -3,11 +3,11 @@ import { useEffect } from "react" import { Dialog, Modal } from "react-aria-components" import { useIntl } from "react-intl" +import { useMediaQuery } from "usehooks-ts" import useDropdownStore from "@/stores/main-menu" import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" -import useMediaQuery from "@/hooks/useMediaQuery" import { getInitials } from "@/utils/user" import Avatar from "../Avatar" diff --git a/components/HotelReservation/HotelCard/hotelCard.module.css b/components/HotelReservation/HotelCard/hotelCard.module.css index 95ea52ad2..a5d90de17 100644 --- a/components/HotelReservation/HotelCard/hotelCard.module.css +++ b/components/HotelReservation/HotelCard/hotelCard.module.css @@ -70,6 +70,10 @@ justify-content: center; } +.address { + display: none; +} + @media screen and (min-width: 1367px) { .card.pageListing { grid-template-areas: @@ -118,4 +122,12 @@ .pageListing .button { width: 160px; } + + .address { + display: block; + } + + .addressMobile { + display: none; + } } diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index d36ce0205..f72945b6d 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -1,6 +1,9 @@ "use client" +import { useParams } from "next/dist/client/components/navigation" import { useIntl } from "react-intl" +import { selectHotelMap } from "@/constants/routes/hotelReservation" + import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import { PriceTagIcon, ScandicLogoIcon } from "@/components/Icons" import TripAdvisorIcon from "@/components/Icons/TripAdvisor" @@ -26,6 +29,7 @@ export default function HotelCard({ state = "default", onHotelCardHover, }: HotelCardProps) { + const params = useParams() const intl = useIntl() const { hotelData } = hotel @@ -77,9 +81,19 @@ export default function HotelCard({ <Title as="h4" textTransform="capitalize"> {hotelData.name} - + {`${hotelData.address.streetAddress}, ${hotelData.address.city}`} + + + {`${hotelData.address.streetAddress}, ${hotelData.address.city}`} + + {`${hotelData.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`} diff --git a/components/HotelReservation/HotelCardDialogListing/index.tsx b/components/HotelReservation/HotelCardDialogListing/index.tsx index a91f68041..fe75c9b33 100644 --- a/components/HotelReservation/HotelCardDialogListing/index.tsx +++ b/components/HotelReservation/HotelCardDialogListing/index.tsx @@ -5,13 +5,7 @@ import { useCallback, useEffect, useRef } from "react" import HotelCardDialog from "../HotelCardDialog" import { getHotelPins } from "./utils" -import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" - -interface HotelCardDialogListingProps { - hotels: HotelData[] - activeCard: string | null | undefined - onActiveCardChange: (hotelName: string | null) => void -} +import type { HotelCardDialogListingProps } from "@/types/components/hotelReservation/selectHotel/map" export default function HotelCardDialogListing({ hotels, @@ -20,6 +14,7 @@ export default function HotelCardDialogListing({ }: HotelCardDialogListingProps) { const hotelsPinData = getHotelPins(hotels) const activeCardRef = useRef(null) + const observerRef = useRef(null) const handleIntersection = useCallback( (entries: IntersectionObserverEntry[]) => { @@ -36,26 +31,36 @@ export default function HotelCardDialogListing({ ) useEffect(() => { - const observer = new IntersectionObserver(handleIntersection, { + observerRef.current = new IntersectionObserver(handleIntersection, { root: null, - threshold: 0.5, // Adjust threshold as needed + threshold: 0.5, }) const elements = document.querySelectorAll("[data-name]") - elements.forEach((el) => observer.observe(el)) + elements.forEach((el) => observerRef.current?.observe(el)) return () => { - elements.forEach((el) => observer.unobserve(el)) + elements.forEach((el) => observerRef.current?.unobserve(el)) + observerRef.current?.disconnect() } }, [handleIntersection]) useEffect(() => { if (activeCardRef.current) { + // Temporarily disconnect the observer + observerRef.current?.disconnect() + activeCardRef.current.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center", }) + + // Reconnect the observer after scrolling + const elements = document.querySelectorAll("[data-name]") + setTimeout(() => { + elements.forEach((el) => observerRef.current?.observe(el)) + }, 500) } }, [activeCard]) @@ -73,7 +78,7 @@ export default function HotelCardDialogListing({ {}} + handleClose={() => onActiveCardChange(null)} /> ) diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx b/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx index f92ec1895..21b804306 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx @@ -3,6 +3,7 @@ import { APIProvider } from "@vis.gl/react-google-maps" import { useRouter, useSearchParams } from "next/navigation" import { useEffect, useState } from "react" import { useIntl } from "react-intl" +import { useMediaQuery } from "usehooks-ts" import { selectHotel } from "@/constants/routes/hotelReservation" @@ -12,6 +13,7 @@ import Button from "@/components/TempDesignSystem/Button" import useLang from "@/hooks/useLang" import HotelListing from "./HotelListing" +import { getCentralCoordinates } from "./utils" import styles from "./selectHotelMap.module.css" @@ -19,19 +21,33 @@ import { SelectHotelMapProps } from "@/types/components/hotelReservation/selectH export default function SelectHotelMap({ apiKey, - coordinates, hotelPins, mapId, - isModal, hotels, }: SelectHotelMapProps) { const searchParams = useSearchParams() const router = useRouter() const lang = useLang() const intl = useIntl() + const isAboveMobile = useMediaQuery("(min-width: 768px)") const [activeHotelPin, setActiveHotelPin] = useState(null) const [showBackToTop, setShowBackToTop] = useState(false) + const centralCoordinates = getCentralCoordinates(hotelPins) + + const coordinates = isAboveMobile + ? centralCoordinates + : { ...centralCoordinates, lat: centralCoordinates.lat - 0.006 } + + const selectHotelParams = new URLSearchParams(searchParams.toString()) + const selectedHotel = selectHotelParams.get("selectedHotel") + + useEffect(() => { + if (selectedHotel) { + setActiveHotelPin(selectedHotel) + } + }, [selectedHotel]) + useEffect(() => { const hotelListingElement = document.querySelector( `.${styles.listingContainer}` @@ -54,10 +70,6 @@ export default function SelectHotelMap({ hotelListingElement?.scrollTo({ top: 0, behavior: "smooth" }) } - function handleModalDismiss() { - router.back() - } - function handlePageRedirect() { router.push(`${selectHotel[lang]}?${searchParams.toString()}`) } @@ -68,7 +80,7 @@ export default function SelectHotelMap({ size="small" theme="base" className={styles.closeButton} - onClick={isModal ? handleModalDismiss : handlePageRedirect} + onClick={handlePageRedirect} > {intl.formatMessage({ id: "Close the map" })} @@ -84,7 +96,7 @@ export default function SelectHotelMap({ size="small" variant="icon" wrapping - onClick={isModal ? handleModalDismiss : handlePageRedirect} + onClick={handlePageRedirect} className={styles.filterContainerCloseButton} > diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/utils.ts b/components/HotelReservation/SelectHotel/SelectHotelMap/utils.ts new file mode 100644 index 000000000..7a4c32ecc --- /dev/null +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/utils.ts @@ -0,0 +1,17 @@ +import { HotelPin } from "@/types/components/hotelReservation/selectHotel/map" + +export function getCentralCoordinates(hotels: HotelPin[]) { + const centralCoordinates = hotels.reduce( + (acc, pin) => { + acc.lat += pin.coordinates.lat + acc.lng += pin.coordinates.lng + return acc + }, + { lat: 0, lng: 0 } + ) + + centralCoordinates.lat /= hotels.length + centralCoordinates.lng /= hotels.length + + return centralCoordinates +} diff --git a/hooks/useMediaQuery.ts b/hooks/useMediaQuery.ts deleted file mode 100644 index bbb9eabac..000000000 --- a/hooks/useMediaQuery.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useEffect, useState } from "react" - -function useMediaQuery(query: string) { - const [isMatch, setIsMatch] = useState(false) - - useEffect(() => { - const media = window.matchMedia(query) - if (media.matches !== isMatch) { - setIsMatch(media.matches) - } - - const listener = () => setIsMatch(media.matches) - media.addEventListener("change", listener) - - return () => media.removeEventListener("change", listener) - }, [isMatch, query]) - - return isMatch -} - -export default useMediaQuery diff --git a/package-lock.json b/package-lock.json index 766d5cb95..a2d63c0f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "server-only": "^0.0.1", "sonner": "^1.5.0", "superjson": "^2.2.1", + "usehooks-ts": "3.1.0", "zod": "^3.22.4", "zustand": "^4.5.2" }, @@ -14703,7 +14704,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true, "license": "MIT" }, "node_modules/lodash.isempty": { @@ -19454,6 +19454,20 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/usehooks-ts": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.0.tgz", + "integrity": "sha512-bBIa7yUyPhE1BCc0GmR96VU/15l/9gP1Ch5mYdLcFBaFGQsdmXkvjV0TtOqW1yUd6VjIwDunm+flSciCQXujiw==", + "dependencies": { + "lodash.debounce": "^4.0.8" + }, + "engines": { + "node": ">=16.15.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index fa08f8495..4afcb9bf1 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "server-only": "^0.0.1", "sonner": "^1.5.0", "superjson": "^2.2.1", + "usehooks-ts": "3.1.0", "zod": "^3.22.4", "zustand": "^4.5.2" }, diff --git a/types/components/hotelReservation/selectHotel/map.ts b/types/components/hotelReservation/selectHotel/map.ts index b913be9b1..12d51e292 100644 --- a/types/components/hotelReservation/selectHotel/map.ts +++ b/types/components/hotelReservation/selectHotel/map.ts @@ -18,10 +18,8 @@ export interface HotelListingProps { export interface SelectHotelMapProps { apiKey: string - coordinates: Coordinates hotelPins: HotelPin[] mapId: string - isModal: boolean hotels: HotelData[] } @@ -53,3 +51,9 @@ export interface HotelCardDialogProps { data: HotelPin handleClose: (event: { stopPropagation: () => void }) => void } + +export interface HotelCardDialogListingProps { + hotels: HotelData[] + activeCard: string | null | undefined + onActiveCardChange: (hotelName: string | null) => void +} From 79e3aca089a817a99857871f9c4ecf351d6608dc Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Tue, 12 Nov 2024 15:11:31 +0100 Subject: [PATCH 13/98] feat(SW-344): UI-fixes on hotel card dialog --- .../HotelCardDialog/hotelCardDialog.module.css | 9 +++++++-- .../HotelReservation/HotelCardDialog/index.tsx | 12 ++++++++---- components/TempDesignSystem/Chip/chip.module.css | 5 +++++ components/TempDesignSystem/Chip/variants.ts | 1 + 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css b/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css index dce743fa1..7a55be6ca 100644 --- a/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css +++ b/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css @@ -18,6 +18,12 @@ position: relative; } +.name { + height: 48px; + display: flex; + align-items: center; +} + .closeIcon { position: absolute; top: 7px; @@ -52,7 +58,7 @@ .facilities { display: flex; flex-wrap: wrap; - gap: var(--Spacing-x1); + gap: 0 var(--Spacing-x1); } .facilitiesItem { @@ -67,7 +73,6 @@ background: var(--Base-Surface-Secondary-light-Normal); display: flex; flex-direction: column; - gap: var(--Spacing-x-half); } .perNight { diff --git a/components/HotelReservation/HotelCardDialog/index.tsx b/components/HotelReservation/HotelCardDialog/index.tsx index 91ce9e548..6fa212af2 100644 --- a/components/HotelReservation/HotelCardDialog/index.tsx +++ b/components/HotelReservation/HotelCardDialog/index.tsx @@ -52,20 +52,24 @@ export default function HotelCardDialog({
- - + + {ratings}
- {name} +
+ {name} +
{amenities.map((facility) => { const IconComponent = mapFacilityToIcon(facility.id) return (
- {IconComponent && } + {IconComponent && ( + + )}
diff --git a/components/TempDesignSystem/Chip/chip.module.css b/components/TempDesignSystem/Chip/chip.module.css index dbfb0e98b..efebaee95 100644 --- a/components/TempDesignSystem/Chip/chip.module.css +++ b/components/TempDesignSystem/Chip/chip.module.css @@ -12,3 +12,8 @@ div.chip { background-color: var(--Scandic-Red-90); color: var(--Primary-Dark-On-Surface-Accent); } + +.secondary { + background-color: var(--Base-Surface-Primary-light-Normal); + color: var(--Primary-Light-On-Surface-Text); +} diff --git a/components/TempDesignSystem/Chip/variants.ts b/components/TempDesignSystem/Chip/variants.ts index a17e6d98d..e9d30cd64 100644 --- a/components/TempDesignSystem/Chip/variants.ts +++ b/components/TempDesignSystem/Chip/variants.ts @@ -6,6 +6,7 @@ export const chipVariants = cva(styles.chip, { variants: { intent: { primary: styles.primary, + secondary: styles.secondary, }, variant: { default: styles.default, From 32429d0a57edac783177bef0c3d2bf9ca6d04bd8 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Tue, 12 Nov 2024 15:14:41 +0100 Subject: [PATCH 14/98] feat(SW-344): Updated height on card wrapper --- .../SelectHotelMap/HotelListing/hotelListing.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/hotelListing.module.css b/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/hotelListing.module.css index 3ba5cf884..962684640 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/hotelListing.module.css +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/hotelListing.module.css @@ -11,7 +11,7 @@ left: 0; right: 0; z-index: 10; - height: 350px; + height: 280px; gap: var(--Spacing-x1); } From 3d235b317694ad8002bb2b6df0daea2176053b05 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Tue, 12 Nov 2024 15:26:57 +0100 Subject: [PATCH 15/98] feat(SW-344): Fixed button in card --- .../HotelReservation/HotelCard/index.tsx | 4 +++- .../HotelReservation/HotelCardDialog/index.tsx | 18 ++++++++++++++++-- .../HotelCardDialogListing/utils.ts | 1 + .../hotelReservation/selectHotel/map.ts | 1 + 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index f72945b6d..6d462c993 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -2,6 +2,7 @@ import { useParams } from "next/dist/client/components/navigation" import { useIntl } from "react-intl" +import { Lang } from "@/constants/languages" import { selectHotelMap } from "@/constants/routes/hotelReservation" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" @@ -30,6 +31,7 @@ export default function HotelCard({ onHotelCardHover, }: HotelCardProps) { const params = useParams() + const lang = params.lang as Lang const intl = useIntl() const { hotelData } = hotel @@ -86,7 +88,7 @@ export default function HotelCard({ diff --git a/components/HotelReservation/HotelCardDialog/index.tsx b/components/HotelReservation/HotelCardDialog/index.tsx index 6fa212af2..16d1ce860 100644 --- a/components/HotelReservation/HotelCardDialog/index.tsx +++ b/components/HotelReservation/HotelCardDialog/index.tsx @@ -1,13 +1,18 @@ "use client" +import { useParams } from "next/navigation" import { useIntl } from "react-intl" +import { Lang } from "@/constants/languages" +import { selectRate } from "@/constants/routes/hotelReservation" + import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import { CloseLargeIcon } from "@/components/Icons" import TripAdvisorIcon from "@/components/Icons/TripAdvisor" import Image from "@/components/Image" import Button from "@/components/TempDesignSystem/Button" import Chip from "@/components/TempDesignSystem/Chip" +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" @@ -21,6 +26,8 @@ export default function HotelCardDialog({ isOpen, handleClose, }: HotelCardDialogProps) { + const params = useParams() + const lang = params.lang as Lang const intl = useIntl() if (!data) { @@ -94,8 +101,15 @@ export default function HotelCardDialog({ )} - diff --git a/components/HotelReservation/HotelCardDialogListing/utils.ts b/components/HotelReservation/HotelCardDialogListing/utils.ts index b63b9b7ce..6183c5da4 100644 --- a/components/HotelReservation/HotelCardDialogListing/utils.ts +++ b/components/HotelReservation/HotelCardDialogListing/utils.ts @@ -17,5 +17,6 @@ export function getHotelPins(hotels: HotelData[]): HotelPin[] { ], amenities: hotel.hotelData.detailedFacilities.slice(0, 3), ratings: hotel.hotelData.ratings?.tripAdvisor.rating ?? null, + operaId: hotel.hotelData.operaId, })) } diff --git a/types/components/hotelReservation/selectHotel/map.ts b/types/components/hotelReservation/selectHotel/map.ts index 12d51e292..233fc2105 100644 --- a/types/components/hotelReservation/selectHotel/map.ts +++ b/types/components/hotelReservation/selectHotel/map.ts @@ -38,6 +38,7 @@ export type HotelPin = { }[] amenities: Filter[] ratings: number | null + operaId: string } export interface HotelListingMapContentProps { From 0618b10870b30cb1ae0f16ff40ebb8ea8a4594c4 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Tue, 12 Nov 2024 15:52:24 +0100 Subject: [PATCH 16/98] feat(SW-874): Show correct rooms --- .../SelectRate/Rooms/index.tsx | 25 ++++--- .../SelectRate/Rooms/utils.ts | 71 +++++++++++++++++++ 2 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 components/HotelReservation/SelectRate/Rooms/utils.ts diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx index 8c122a9c0..a3454818a 100644 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -2,10 +2,9 @@ import { useCallback, useState } from "react" -import { RoomsAvailability } from "@/server/routers/hotels/output" - import RoomFilter from "../RoomFilter" import RoomSelection from "../RoomSelection" +import { getLowestPricedRooms } from "./utils" import styles from "./rooms.module.css" @@ -14,6 +13,10 @@ import { type RoomPackageCodes, } from "@/types/components/hotelReservation/selectRate/roomFilter" import type { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection" +import type { + RoomConfiguration, + RoomsAvailability, +} from "@/server/routers/hotels/output" export default function Rooms({ roomsAvailability, @@ -21,7 +24,12 @@ export default function Rooms({ user, packages, }: Omit) { - const defaultRooms = roomsAvailability.roomConfigurations + console.log("roomsAvailability", roomsAvailability) + const visibleRooms: RoomConfiguration[] = getLowestPricedRooms( + roomsAvailability.roomConfigurations + ) + + const defaultRooms = visibleRooms const [rooms, setRooms] = useState({ ...roomsAvailability, roomConfigurations: defaultRooms, @@ -46,15 +54,14 @@ export default function Rooms({ return } - const filteredRooms = roomsAvailability.roomConfigurations.filter( - (room) => - filteredPackages.every((filteredPackage) => - room.features.some((feature) => feature.code === filteredPackage) - ) + const filteredRooms = visibleRooms.filter((room) => + filteredPackages.every((filteredPackage) => + room.features.some((feature) => feature.code === filteredPackage) + ) ) setRooms({ ...roomsAvailability, roomConfigurations: filteredRooms }) }, - [roomsAvailability, defaultRooms] + [roomsAvailability, defaultRooms, visibleRooms] ) return ( diff --git a/components/HotelReservation/SelectRate/Rooms/utils.ts b/components/HotelReservation/SelectRate/Rooms/utils.ts new file mode 100644 index 000000000..bb1ff48c9 --- /dev/null +++ b/components/HotelReservation/SelectRate/Rooms/utils.ts @@ -0,0 +1,71 @@ +import { RoomConfiguration } from "@/server/routers/hotels/output" + +export function getLowestPricedRooms(roomConfigurations: RoomConfiguration[]) { + const roomTypeCount = roomConfigurations.reduce( + (acc, room) => { + acc[room.roomType] = (acc[room.roomType] || 0) + 1 + return acc + }, + {} as Record + ) + + const duplicateRoomTypes = new Set( + Object.keys(roomTypeCount).filter((roomType) => roomTypeCount[roomType] > 1) + ) + + const roomMap = new Map() + + roomConfigurations.forEach((room) => { + const { roomType, products } = room + + if (!duplicateRoomTypes.has(roomType)) { + roomMap.set(roomType, room) + return + } + + products.forEach((product) => { + const { productType } = product + const publicProduct = productType.public + const memberProduct = productType.member || { + requestedPrice: null, + localPrice: null, + } + + const { + requestedPrice: publicRequestedPrice, + localPrice: publicLocalPrice, + } = publicProduct + const { + requestedPrice: memberRequestedPrice, + localPrice: memberLocalPrice, + } = memberProduct + + const currentLowest = roomMap.get(roomType) + + const currentRequestedPrice = Math.min( + Number(publicRequestedPrice.pricePerNight) ?? Infinity, + Number(memberRequestedPrice?.pricePerNight) ?? Infinity + ) + const currentLocalPrice = Math.min( + Number(publicLocalPrice.pricePerNight) ?? Infinity, + Number(memberLocalPrice?.pricePerNight) ?? Infinity + ) + + if ( + !currentLowest || + currentRequestedPrice < currentLowest.requestedPrice || + (currentRequestedPrice === currentLowest.requestedPrice && + currentLocalPrice < currentLowest.localPrice) + ) { + roomMap.set(roomType, { + ...room, + product, + requestedPrice: currentRequestedPrice, + localPrice: currentLocalPrice, + }) + } + }) + }) + + return Array.from(roomMap.values()) +} From 70fbe44e51716dc8c639d8f07642475888f14a40 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 13 Nov 2024 08:17:53 +0100 Subject: [PATCH 17/98] feat(SW-874): Don't add petRoomPrice if filter is not selected --- .../SelectRate/RoomSelection/FlexibilityOption/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx index a0fd30b9e..9bc6ca1f3 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx @@ -56,7 +56,7 @@ export default function FlexibilityOption({ priceName: name, public: publicPrice, member: memberPrice, - features, + features: petRoomPackage ? features : [], } handleSelectRate(rate) } From 3869c41f58371e0706f81bcf5e59883d0d52c7a3 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 13 Nov 2024 08:21:47 +0100 Subject: [PATCH 18/98] feat(SW-874): Rename function --- components/HotelReservation/SelectRate/Rooms/index.tsx | 4 ++-- components/HotelReservation/SelectRate/Rooms/utils.ts | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx index a3454818a..d4c8ec50b 100644 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -4,7 +4,7 @@ import { useCallback, useState } from "react" import RoomFilter from "../RoomFilter" import RoomSelection from "../RoomSelection" -import { getLowestPricedRooms } from "./utils" +import { getLowestPricedDuplicateRooms } from "./utils" import styles from "./rooms.module.css" @@ -25,7 +25,7 @@ export default function Rooms({ packages, }: Omit) { console.log("roomsAvailability", roomsAvailability) - const visibleRooms: RoomConfiguration[] = getLowestPricedRooms( + const visibleRooms: RoomConfiguration[] = getLowestPricedDuplicateRooms( roomsAvailability.roomConfigurations ) diff --git a/components/HotelReservation/SelectRate/Rooms/utils.ts b/components/HotelReservation/SelectRate/Rooms/utils.ts index bb1ff48c9..3c0507371 100644 --- a/components/HotelReservation/SelectRate/Rooms/utils.ts +++ b/components/HotelReservation/SelectRate/Rooms/utils.ts @@ -1,6 +1,12 @@ import { RoomConfiguration } from "@/server/routers/hotels/output" -export function getLowestPricedRooms(roomConfigurations: RoomConfiguration[]) { +/** + * Get the lowest priced room for each room type that appears more than once. + */ + +export function getLowestPricedDuplicateRooms( + roomConfigurations: RoomConfiguration[] +) { const roomTypeCount = roomConfigurations.reduce( (acc, room) => { acc[room.roomType] = (acc[room.roomType] || 0) + 1 From a7ef5857eee9cc4c01bf530d5520ce9330f2b428 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 13 Nov 2024 08:23:08 +0100 Subject: [PATCH 19/98] feat(SW-874): Removed log --- components/HotelReservation/SelectRate/Rooms/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx index d4c8ec50b..5e982a10f 100644 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -24,7 +24,6 @@ export default function Rooms({ user, packages, }: Omit) { - console.log("roomsAvailability", roomsAvailability) const visibleRooms: RoomConfiguration[] = getLowestPricedDuplicateRooms( roomsAvailability.roomConfigurations ) From cf8585a3df83991f97fd782b915e407963038867 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 13 Nov 2024 08:30:43 +0100 Subject: [PATCH 20/98] fix(SW-449): Re-added YYYY-MM-DD --- .../hotelreservation/(standard)/select-rate/page.tsx | 4 ++-- components/DatePicker/index.tsx | 10 ++++++---- components/TempDesignSystem/Form/Date/index.tsx | 2 +- server/routers/hotels/query.ts | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index 99554d1c1..816c937ae 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -49,11 +49,11 @@ export default async function SelectRatePage({ searchParams.fromDate && dt(searchParams.fromDate).isAfter(dt().subtract(1, "day")) ? searchParams.fromDate - : dt().utc().format("YYYY-MM-D") + : dt().utc().format("YYYY-MM-DD") const validToDate = searchParams.toDate && dt(searchParams.toDate).isAfter(validFromDate) ? searchParams.toDate - : dt().utc().add(1, "day").format("YYYY-MM-D") + : dt().utc().add(1, "day").format("YYYY-MM-DD") const adults = selectRoomParamsObject.room[0].adults || 1 // TODO: Handle multiple rooms const childrenCount = selectRoomParamsObject.room[0].child?.length const children = selectRoomParamsObject.room[0].child diff --git a/components/DatePicker/index.tsx b/components/DatePicker/index.tsx index ca4fec3bd..962a8fc96 100644 --- a/components/DatePicker/index.tsx +++ b/components/DatePicker/index.tsx @@ -47,7 +47,7 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { if (!dt(selected).isBefore(dt(), "day")) { if (isSelectingFrom) { setValue(name, { - fromDate: dt(selected).format("YYYY-MM-D"), + fromDate: dt(selected).format("YYYY-MM-DD"), toDate: undefined, }) setIsSelectingFrom(false) @@ -57,11 +57,11 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { if (toDate.isAfter(fromDate)) { setValue(name, { fromDate: selectedDate.fromDate, - toDate: toDate.format("YYYY-MM-D"), + toDate: toDate.format("YYYY-MM-DD"), }) } else { setValue(name, { - fromDate: toDate.format("YYYY-MM-D"), + fromDate: toDate.format("YYYY-MM-DD"), toDate: selectedDate.fromDate, }) } @@ -75,7 +75,9 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { if (!selectedDate.toDate) { setValue(name, { fromDate: selectedDate.fromDate, - toDate: dt(selectedDate.fromDate).add(1, "day").format("YYYY-MM-D"), + toDate: dt(selectedDate.fromDate) + .add(1, "day") + .format("YYYY-MM-DD"), }) setIsSelectingFrom(true) } diff --git a/components/TempDesignSystem/Form/Date/index.tsx b/components/TempDesignSystem/Form/Date/index.tsx index 3712cb94b..4e8f4a7af 100644 --- a/components/TempDesignSystem/Form/Date/index.tsx +++ b/components/TempDesignSystem/Form/Date/index.tsx @@ -83,7 +83,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { .date(Number(day)) if (newDate.isValid()) { - setValue(name, newDate.format("YYYY-MM-D"), { + setValue(name, newDate.format("YYYY-MM-DD"), { shouldDirty: true, shouldTouch: true, shouldValidate: true, diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 192d88e33..6a96f39c3 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -982,8 +982,8 @@ export const hotelQueryRouter = router({ const apiLang = toApiLang(lang) const params = { Adults: input.adults, - EndDate: dt(input.toDate).format("YYYY-MM-D"), - StartDate: dt(input.fromDate).format("YYYY-MM-D"), + EndDate: dt(input.toDate).format("YYYY-MM-DD"), + StartDate: dt(input.fromDate).format("YYYY-MM-DD"), language: apiLang, } From a16062a705bd26df00fb3f2f1231634405eb6fe6 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 13 Nov 2024 08:57:14 +0100 Subject: [PATCH 21/98] fix(SW-449): dateFormat middleware --- middleware.ts | 2 ++ middlewares/dateFormat.ts | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 middlewares/dateFormat.ts diff --git a/middleware.ts b/middleware.ts index 1f1c36a0b..27b4eca97 100644 --- a/middleware.ts +++ b/middleware.ts @@ -8,6 +8,7 @@ import * as cmsContent from "@/middlewares/cmsContent" import * as currentWebLogin from "@/middlewares/currentWebLogin" import * as currentWebLoginEmail from "@/middlewares/currentWebLoginEmail" import * as currentWebLogout from "@/middlewares/currentWebLogout" +import * as dateFormat from "@/middlewares/dateFormat" import * as handleAuth from "@/middlewares/handleAuth" import * as myPages from "@/middlewares/myPages" import { getDefaultRequestHeaders } from "@/middlewares/utils" @@ -52,6 +53,7 @@ export const middleware: NextMiddleware = async (request, event) => { webView, bookingFlow, cmsContent, + dateFormat, ] try { diff --git a/middlewares/dateFormat.ts b/middlewares/dateFormat.ts new file mode 100644 index 000000000..e2e5c0a9e --- /dev/null +++ b/middlewares/dateFormat.ts @@ -0,0 +1,39 @@ +import { NextMiddleware, NextResponse } from "next/server" + +import { MiddlewareMatcher } from "@/types/middleware" + +/* +Middleware function to normalize date formats to support +YYYY-MM-D and YYYY-MM-DD since the current web uses YYYY-MM-D +in the URL as parameters (toDate and fromDate) +*/ +export const middleware: NextMiddleware = (request) => { + const url = request.nextUrl.clone() + const { searchParams } = url + + function normalizeDate(date: string): string { + const datePattern = /^\d{4}-\d{1,2}-\d{1,2}$/ + if (datePattern.test(date)) { + const [year, month, day] = date.split("-").map(Number) + return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}` + } + return date + } + + if (searchParams.has("fromDate")) { + const fromDate = searchParams.get("fromDate")! + searchParams.set("fromDate", normalizeDate(fromDate)) + } + + if (searchParams.has("toDate")) { + const toDate = searchParams.get("toDate")! + searchParams.set("toDate", normalizeDate(toDate)) + } + + return NextResponse.rewrite(url) +} + +export const matcher: MiddlewareMatcher = (request) => { + const { searchParams } = request.nextUrl + return searchParams.has("fromDate") || searchParams.has("toDate") +} From b6c390fdad337282c85045281759f374dc92aa95 Mon Sep 17 00:00:00 2001 From: Bianca Widstam Date: Wed, 13 Nov 2024 08:51:51 +0000 Subject: [PATCH 22/98] feat/SW-763-image-gallery-update (pull request #846) feat(SW-763): update to smallerImages for select-hotel/rate page and slice size depending on signature * feat(SW-763): update to smallerImages for select- hotel/rate page and slice size depending on signature * fix(SW-763): add hotelType enum * feat(SW-763): move hotel type check to the route for the hotelData * fix(SW-763): remove unused import * fix(SW-763): fix comment * fix(SW-763): add optional galleryImages check Approved-by: Christian Andolf Approved-by: Pontus Dreij Approved-by: Niclas Edenvin --- components/HotelReservation/HotelCard/index.tsx | 7 ++----- .../SelectRate/HotelInfoCard/index.tsx | 7 ++----- server/routers/hotels/output.ts | 1 + server/routers/hotels/query.ts | 14 ++++++++++++-- types/enums/hotelType.ts | 5 +++++ 5 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 types/enums/hotelType.ts diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index d36ce0205..623990987 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -56,13 +56,10 @@ export default function HotelCard({ onMouseLeave={handleMouseLeave} >
- {hotelData.gallery && ( + {hotelData?.galleryImages && ( )}
diff --git a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx index 8c4971f06..1d0fb8df7 100644 --- a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx +++ b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx @@ -29,13 +29,10 @@ export default function HotelInfoCard({ hotelData }: HotelInfoCardProps) { {hotelAttributes && (
- {hotelAttributes.gallery && ( + {hotelAttributes?.galleryImages && ( )} {hotelAttributes.ratings?.tripAdvisor && ( diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 08553d882..32640cf62 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -449,6 +449,7 @@ export const getHotelDataSchema = z.object({ facilities.sort((a, b) => b.sortOrder - a.sortOrder) ), gallery: gallerySchema.optional(), + galleryImages: z.array(imageSchema).optional(), healthAndWellness: facilitySchema.optional(), healthFacilities: z.array(healthFacilitySchema), hotelContent: hotelContentSchema, diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 192d88e33..4e195c856 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -60,6 +60,7 @@ import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities" import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType" import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" import { BreakfastPackageEnum } from "@/types/enums/breakfast" +import { HotelTypeEnum } from "@/types/enums/hotelType" import type { RequestOptionsWithOutBody } from "@/types/fetch" import type { Facility } from "@/types/hotel" import type { GetHotelPageData } from "@/types/trpc/routers/contentstack/hotelPage" @@ -257,13 +258,22 @@ export const getHotelData = cache( query: { hotelId, params: params }, }) ) + const hotelData = validateHotelData.data if (isCardOnlyPayment) { - validateHotelData.data.data.attributes.merchantInformationData.alternatePaymentOptions = + hotelData.data.attributes.merchantInformationData.alternatePaymentOptions = [] } + if (hotelData.data.attributes.gallery) { + const smallerImages = hotelData.data.attributes.gallery.smallerImages + const hotelGalleryImages = + hotelData.data.attributes.hotelType === HotelTypeEnum.Signature + ? smallerImages.slice(0, 10) + : smallerImages.slice(0, 6) + hotelData.data.attributes.galleryImages = hotelGalleryImages + } - return validateHotelData.data + return hotelData } ) diff --git a/types/enums/hotelType.ts b/types/enums/hotelType.ts new file mode 100644 index 000000000..00826f4a0 --- /dev/null +++ b/types/enums/hotelType.ts @@ -0,0 +1,5 @@ +export enum HotelTypeEnum { + Signature = "signature", + ScandicGo = "scandicgo", + Regular = "regular", +} From 35a527be05637e4de6152d2b60755942269b9e1f Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Fri, 8 Nov 2024 13:01:54 +0100 Subject: [PATCH 23/98] chore(SW-739): cleanup reward endpoint related code --- server/routers/contentstack/reward/query.ts | 403 ++++++-------------- server/routers/contentstack/reward/utils.ts | 168 ++++++++ 2 files changed, 292 insertions(+), 279 deletions(-) create mode 100644 server/routers/contentstack/reward/utils.ts diff --git a/server/routers/contentstack/reward/query.ts b/server/routers/contentstack/reward/query.ts index b4e0e54b2..0c1577182 100644 --- a/server/routers/contentstack/reward/query.ts +++ b/server/routers/contentstack/reward/query.ts @@ -1,10 +1,4 @@ -import { metrics } from "@opentelemetry/api" -import { unstable_cache } from "next/cache" - -import { Lang } from "@/constants/languages" import * as api from "@/lib/api" -import { GetRewards } from "@/lib/graphql/Query/Rewards.graphql" -import { request } from "@/lib/graphql/request" import { notFound } from "@/server/errors/trpc" import { contentStackBaseWithProtectedProcedure, @@ -12,8 +6,6 @@ import { router, } from "@/server/trpc" -import { generateLoyaltyConfigTag } from "@/utils/generateTag" - import { getAllLoyaltyLevels, getLoyaltyLevel } from "../loyaltyLevel/query" import { rewardsAllInput, @@ -21,172 +13,135 @@ import { rewardsCurrentInput, rewardsUpdateInput, } from "./input" +import { Reward, SurpriseReward, validateApiRewardSchema } from "./output" import { - CmsRewardsResponse, - Reward, - SurpriseReward, - validateApiRewardSchema, - validateApiTierRewardsSchema, - validateCmsRewardsSchema, -} from "./output" + getAllCachedApiRewards, + getAllRewardCounter, + getAllRewardFailCounter, + getAllRewardSuccessCounter, + getByLevelRewardCounter, + getByLevelRewardFailCounter, + getByLevelRewardSuccessCounter, + getCmsRewards, + getCurrentRewardCounter, + getCurrentRewardFailCounter, + getCurrentRewardSuccessCounter, + getUniqueRewardIds, +} from "./utils" import { Surprise } from "@/types/components/blocks/surprises" -const meter = metrics.getMeter("trpc.reward") -// OpenTelemetry metrics: Reward - -const getCurrentRewardCounter = meter.createCounter( - "trpc.contentstack.reward.current" -) -const getCurrentRewardSuccessCounter = meter.createCounter( - "trpc.contentstack.reward.current-success" -) - -const getCurrentRewardFailCounter = meter.createCounter( - "trpc.contentstack.reward.current-fail" -) - -const getByLevelRewardCounter = meter.createCounter( - "trpc.contentstack.reward.byLevel" -) -const getByLevelRewardSuccessCounter = meter.createCounter( - "trpc.contentstack.reward.byLevel-success" -) - -const getByLevelRewardFailCounter = meter.createCounter( - "trpc.contentstack.reward.byLevel-fail" -) - -const getAllRewardCounter = meter.createCounter("trpc.contentstack.reward.all") - -const getAllRewardSuccessCounter = meter.createCounter( - "trpc.contentstack.reward.all-success" -) -const getAllRewardFailCounter = meter.createCounter( - "trpc.contentstack.reward.all-fail" -) - -const ONE_HOUR = 60 * 60 - -function getUniqueRewardIds(rewardIds: string[]) { - const uniqueRewardIds = new Set(rewardIds) - return Array.from(uniqueRewardIds) -} - -const getAllCachedApiRewards = unstable_cache( - async function (token) { - const apiResponse = await api.get(api.endpoints.v1.Profile.tierRewards, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - if (!apiResponse.ok) { - const text = await apiResponse.text() - getCurrentRewardFailCounter.add(1, { - error_type: "http_error", - error: JSON.stringify({ - status: apiResponse.status, - statusText: apiResponse.statusText, - text, - }), - }) - console.error( - "api.rewards.tierRewards error ", - JSON.stringify({ - error: { - status: apiResponse.status, - statusText: apiResponse.statusText, - text, - }, - }) - ) - - throw apiResponse - } - - const data = await apiResponse.json() - const validatedApiTierRewards = validateApiTierRewardsSchema.safeParse(data) - - if (!validatedApiTierRewards.success) { - getAllRewardFailCounter.add(1, { - error_type: "validation_error", - error: JSON.stringify(validatedApiTierRewards.error), - }) - console.error(validatedApiTierRewards.error) - console.error( - "api.rewards validation error", - JSON.stringify({ - error: validatedApiTierRewards.error, - }) - ) - throw validatedApiTierRewards.error - } - - return validatedApiTierRewards.data - }, - ["getAllApiRewards"], - { revalidate: ONE_HOUR } -) - -async function getCmsRewards(locale: Lang, rewardIds: string[]) { - const tags = rewardIds.map((id) => - generateLoyaltyConfigTag(locale, "reward", id) - ) - const cmsRewardsResponse = await request( - GetRewards, - { - locale: locale, - rewardIds, - }, - { next: { tags }, cache: "force-cache" } - ) - - if (!cmsRewardsResponse.data) { - getAllRewardFailCounter.add(1, { - lang: locale, - error_type: "validation_error", - error: JSON.stringify(cmsRewardsResponse.data), - }) - const notFoundError = notFound(cmsRewardsResponse) - console.error( - "contentstack.rewards not found error", - JSON.stringify({ - query: { - locale, - rewardIds, - }, - error: { code: notFoundError.code }, - }) - ) - throw notFoundError - } - - const validatedCmsRewards = - validateCmsRewardsSchema.safeParse(cmsRewardsResponse) - - if (!validatedCmsRewards.success) { - getAllRewardFailCounter.add(1, { - locale, - rewardIds, - error_type: "validation_error", - error: JSON.stringify(validatedCmsRewards.error), - }) - console.error(validatedCmsRewards.error) - console.error( - "contentstack.rewards validation error", - JSON.stringify({ - query: { locale, rewardIds }, - error: validatedCmsRewards.error, - }) - ) - return null - } - - return validatedCmsRewards.data -} - export const rewardQueryRouter = router({ + all: contentStackBaseWithServiceProcedure + .input(rewardsAllInput) + .query(async function ({ input, ctx }) { + getAllRewardCounter.add(1) + const allApiRewards = await getAllCachedApiRewards(ctx.serviceToken) + + if (!allApiRewards) { + return [] + } + + const rewardIds = Object.values(allApiRewards) + .flatMap((level) => level.map((reward) => reward?.rewardId)) + .filter((id): id is string => Boolean(id)) + + const contentStackRewards = await getCmsRewards( + ctx.lang, + getUniqueRewardIds(rewardIds) + ) + + if (!contentStackRewards) { + return [] + } + + const loyaltyLevelsConfig = await getAllLoyaltyLevels(ctx) + const levelsWithRewards = Object.entries(allApiRewards).map( + ([level, rewards]) => { + const combinedRewards = rewards + .filter((r) => (input.unique ? r?.rewardTierLevel === level : true)) + .map((reward) => { + const contentStackReward = contentStackRewards.find((r) => { + return r.reward_id === reward?.rewardId + }) + + if (contentStackReward) { + return contentStackReward + } else { + console.error("No contentStackReward found", reward?.rewardId) + } + }) + .filter((reward): reward is Reward => Boolean(reward)) + + const levelConfig = loyaltyLevelsConfig.find( + (l) => l.level_id === level + ) + + if (!levelConfig) { + getAllRewardFailCounter.add(1) + + console.error("contentstack.loyaltyLevels level not found") + throw notFound() + } + return { ...levelConfig, rewards: combinedRewards } + } + ) + + getAllRewardSuccessCounter.add(1) + return levelsWithRewards + }), + byLevel: contentStackBaseWithServiceProcedure + .input(rewardsByLevelInput) + .query(async function ({ input, ctx }) { + getByLevelRewardCounter.add(1) + const { level_id } = input + + const allUpcomingApiRewards = await getAllCachedApiRewards( + ctx.serviceToken + ) + + if (!allUpcomingApiRewards || !allUpcomingApiRewards[level_id]) { + getByLevelRewardFailCounter.add(1) + + return null + } + + let apiRewards = allUpcomingApiRewards[level_id]! + + if (input.unique) { + apiRewards = allUpcomingApiRewards[level_id]!.filter( + (reward) => reward?.rewardTierLevel === level_id + ) + } + + const rewardIds = apiRewards + .map((reward) => reward?.rewardId) + .filter((id): id is string => Boolean(id)) + + const contentStackRewards = await getCmsRewards(ctx.lang, rewardIds) + if (!contentStackRewards) { + return null + } + + const loyaltyLevelsConfig = await getLoyaltyLevel(ctx, input.level_id) + + const levelsWithRewards = apiRewards + .map((reward) => { + const contentStackReward = contentStackRewards.find((r) => { + return r.reward_id === reward?.rewardId + }) + + if (contentStackReward) { + return contentStackReward + } else { + console.error("No contentStackReward found", reward?.rewardId) + } + }) + .filter((reward): reward is Reward => Boolean(reward)) + + getByLevelRewardSuccessCounter.add(1) + return { level: loyaltyLevelsConfig, rewards: levelsWithRewards } + }), current: contentStackBaseWithProtectedProcedure .input(rewardsCurrentInput) .query(async function ({ input, ctx }) { @@ -280,116 +235,6 @@ export const rewardQueryRouter = router({ nextCursor, } }), - byLevel: contentStackBaseWithServiceProcedure - .input(rewardsByLevelInput) - .query(async function ({ input, ctx }) { - getByLevelRewardCounter.add(1) - const { level_id } = input - - const allUpcomingApiRewards = await getAllCachedApiRewards( - ctx.serviceToken - ) - - if (!allUpcomingApiRewards || !allUpcomingApiRewards[level_id]) { - getByLevelRewardFailCounter.add(1) - - return null - } - - let apiRewards = allUpcomingApiRewards[level_id]! - - if (input.unique) { - apiRewards = allUpcomingApiRewards[level_id]!.filter( - (reward) => reward?.rewardTierLevel === level_id - ) - } - - const rewardIds = apiRewards - .map((reward) => reward?.rewardId) - .filter((id): id is string => Boolean(id)) - - const contentStackRewards = await getCmsRewards(ctx.lang, rewardIds) - if (!contentStackRewards) { - return null - } - - const loyaltyLevelsConfig = await getLoyaltyLevel(ctx, input.level_id) - - const levelsWithRewards = apiRewards - .map((reward) => { - const contentStackReward = contentStackRewards.find((r) => { - return r.reward_id === reward?.rewardId - }) - - if (contentStackReward) { - return contentStackReward - } else { - console.error("No contentStackReward found", reward?.rewardId) - } - }) - .filter((reward): reward is Reward => Boolean(reward)) - - getByLevelRewardSuccessCounter.add(1) - return { level: loyaltyLevelsConfig, rewards: levelsWithRewards } - }), - all: contentStackBaseWithServiceProcedure - .input(rewardsAllInput) - .query(async function ({ input, ctx }) { - getAllRewardCounter.add(1) - const allApiRewards = await getAllCachedApiRewards(ctx.serviceToken) - - if (!allApiRewards) { - return [] - } - - const rewardIds = Object.values(allApiRewards) - .flatMap((level) => level.map((reward) => reward?.rewardId)) - .filter((id): id is string => Boolean(id)) - - const contentStackRewards = await getCmsRewards( - ctx.lang, - getUniqueRewardIds(rewardIds) - ) - - if (!contentStackRewards) { - return [] - } - - const loyaltyLevelsConfig = await getAllLoyaltyLevels(ctx) - const levelsWithRewards = Object.entries(allApiRewards).map( - ([level, rewards]) => { - const combinedRewards = rewards - .filter((r) => (input.unique ? r?.rewardTierLevel === level : true)) - .map((reward) => { - const contentStackReward = contentStackRewards.find((r) => { - return r.reward_id === reward?.rewardId - }) - - if (contentStackReward) { - return contentStackReward - } else { - console.error("No contentStackReward found", reward?.rewardId) - } - }) - .filter((reward): reward is Reward => Boolean(reward)) - - const levelConfig = loyaltyLevelsConfig.find( - (l) => l.level_id === level - ) - - if (!levelConfig) { - getAllRewardFailCounter.add(1) - - console.error("contentstack.loyaltyLevels level not found") - throw notFound() - } - return { ...levelConfig, rewards: combinedRewards } - } - ) - - getAllRewardSuccessCounter.add(1) - return levelsWithRewards - }), surprises: contentStackBaseWithProtectedProcedure.query(async ({ ctx }) => { getCurrentRewardCounter.add(1) diff --git a/server/routers/contentstack/reward/utils.ts b/server/routers/contentstack/reward/utils.ts new file mode 100644 index 000000000..8aab49955 --- /dev/null +++ b/server/routers/contentstack/reward/utils.ts @@ -0,0 +1,168 @@ +import { metrics } from "@opentelemetry/api" +import { unstable_cache } from "next/cache" + +import { Lang } from "@/constants/languages" +import * as api from "@/lib/api" +import { GetRewards } from "@/lib/graphql/Query/Rewards.graphql" +import { request } from "@/lib/graphql/request" +import { notFound } from "@/server/errors/trpc" + +import { generateLoyaltyConfigTag } from "@/utils/generateTag" + +import { + CmsRewardsResponse, + validateApiTierRewardsSchema, + validateCmsRewardsSchema, +} from "./output" + +const meter = metrics.getMeter("trpc.reward") +export const getAllRewardCounter = meter.createCounter( + "trpc.contentstack.reward.all" +) +export const getAllRewardFailCounter = meter.createCounter( + "trpc.contentstack.reward.all-fail" +) +export const getAllRewardSuccessCounter = meter.createCounter( + "trpc.contentstack.reward.all-success" +) +export const getCurrentRewardCounter = meter.createCounter( + "trpc.contentstack.reward.current" +) +export const getCurrentRewardFailCounter = meter.createCounter( + "trpc.contentstack.reward.current-fail" +) +export const getCurrentRewardSuccessCounter = meter.createCounter( + "trpc.contentstack.reward.current-success" +) +export const getByLevelRewardCounter = meter.createCounter( + "trpc.contentstack.reward.byLevel" +) +export const getByLevelRewardFailCounter = meter.createCounter( + "trpc.contentstack.reward.byLevel-fail" +) +export const getByLevelRewardSuccessCounter = meter.createCounter( + "trpc.contentstack.reward.byLevel-success" +) + +const ONE_HOUR = 60 * 60 + +export function getUniqueRewardIds(rewardIds: string[]) { + const uniqueRewardIds = new Set(rewardIds) + return Array.from(uniqueRewardIds) +} + +/** + * Cached for 1 hour. + */ +export const getAllCachedApiRewards = unstable_cache( + async function (token) { + const apiResponse = await api.get(api.endpoints.v1.Profile.tierRewards, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + + if (!apiResponse.ok) { + const text = await apiResponse.text() + getAllRewardFailCounter.add(1, { + error_type: "http_error", + error: JSON.stringify({ + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }), + }) + console.error( + "api.rewards.tierRewards error ", + JSON.stringify({ + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }, + }) + ) + + throw apiResponse + } + + const data = await apiResponse.json() + const validatedApiTierRewards = validateApiTierRewardsSchema.safeParse(data) + + if (!validatedApiTierRewards.success) { + getAllRewardFailCounter.add(1, { + error_type: "validation_error", + error: JSON.stringify(validatedApiTierRewards.error), + }) + console.error(validatedApiTierRewards.error) + console.error( + "api.rewards validation error", + JSON.stringify({ + error: validatedApiTierRewards.error, + }) + ) + throw validatedApiTierRewards.error + } + + return validatedApiTierRewards.data + }, + ["getAllApiRewards"], + { revalidate: ONE_HOUR } +) + +export async function getCmsRewards(locale: Lang, rewardIds: string[]) { + const tags = rewardIds.map((id) => + generateLoyaltyConfigTag(locale, "reward", id) + ) + const cmsRewardsResponse = await request( + GetRewards, + { + locale: locale, + rewardIds, + }, + { next: { tags }, cache: "force-cache" } + ) + + if (!cmsRewardsResponse.data) { + getAllRewardFailCounter.add(1, { + lang: locale, + error_type: "validation_error", + error: JSON.stringify(cmsRewardsResponse.data), + }) + const notFoundError = notFound(cmsRewardsResponse) + console.error( + "contentstack.rewards not found error", + JSON.stringify({ + query: { + locale, + rewardIds, + }, + error: { code: notFoundError.code }, + }) + ) + throw notFoundError + } + + const validatedCmsRewards = + validateCmsRewardsSchema.safeParse(cmsRewardsResponse) + + if (!validatedCmsRewards.success) { + getAllRewardFailCounter.add(1, { + locale, + rewardIds, + error_type: "validation_error", + error: JSON.stringify(validatedCmsRewards.error), + }) + console.error(validatedCmsRewards.error) + console.error( + "contentstack.rewards validation error", + JSON.stringify({ + query: { locale, rewardIds }, + error: validatedCmsRewards.error, + }) + ) + return null + } + + return validatedCmsRewards.data +} From 48af26a772c7d78ac57db688190486bbb4fe6a76 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Fri, 8 Nov 2024 14:50:18 +0100 Subject: [PATCH 24/98] feat(SW-739): use new allTiers endpoint and add feature flag --- .env.local.example | 1 + .env.test | 1 + env/server.ts | 8 +++ lib/api/endpoints.ts | 10 ++- server/routers/contentstack/reward/output.ts | 16 +++++ server/routers/contentstack/reward/query.ts | 13 ++-- server/routers/contentstack/reward/utils.ts | 66 +++++++++++++++++++- 7 files changed, 107 insertions(+), 8 deletions(-) diff --git a/.env.local.example b/.env.local.example index ab0b7ee72..e1b8e182f 100644 --- a/.env.local.example +++ b/.env.local.example @@ -52,3 +52,4 @@ GOOGLE_STATIC_MAP_ID="" GOOGLE_DYNAMIC_MAP_ID="" HIDE_FOR_NEXT_RELEASE="true" +USE_NEW_REWARDS_ENDPOINT="true" diff --git a/.env.test b/.env.test index ce2b20278..f651cbe63 100644 --- a/.env.test +++ b/.env.test @@ -43,3 +43,4 @@ GOOGLE_STATIC_MAP_ID="test" GOOGLE_DYNAMIC_MAP_ID="test" HIDE_FOR_NEXT_RELEASE="true" SALESFORCE_PREFERENCE_BASE_URL="test" +USE_NEW_REWARDS_ENDPOINT="true" diff --git a/env/server.ts b/env/server.ts index c4b9355b7..1f81557ce 100644 --- a/env/server.ts +++ b/env/server.ts @@ -72,6 +72,13 @@ export const env = createEnv({ .refine((s) => s === "true" || s === "false") // transform to boolean .transform((s) => s === "true"), + USE_NEW_REWARDS_ENDPOINT: z + .string() + // only allow "true" or "false" + .refine((s) => s === "true" || s === "false") + // transform to boolean + .transform((s) => s === "true") + .default("false"), }, emptyStringAsUndefined: true, runtimeEnv: { @@ -126,5 +133,6 @@ export const env = createEnv({ GOOGLE_STATIC_MAP_ID: process.env.GOOGLE_STATIC_MAP_ID, GOOGLE_DYNAMIC_MAP_ID: process.env.GOOGLE_DYNAMIC_MAP_ID, HIDE_FOR_NEXT_RELEASE: process.env.HIDE_FOR_NEXT_RELEASE, + USE_NEW_REWARDS_ENDPOINT: process.env.USE_NEW_REWARDS_ENDPOINT, }, }) diff --git a/lib/api/endpoints.ts b/lib/api/endpoints.ts index 100fb7518..4e5258db0 100644 --- a/lib/api/endpoints.ts +++ b/lib/api/endpoints.ts @@ -151,8 +151,10 @@ export namespace endpoints { export const invalidateSessions = `${base.path.profile}/${version}/${base.enitity.Profile}/invalidateSessions` export const membership = `${base.path.profile}/${version}/${base.enitity.Profile}/membership` export const profile = `${base.path.profile}/${version}/${base.enitity.Profile}` - export const reward = `${base.path.profile}/${version}/${base.enitity.Profile}/reward` export const subscriberId = `${base.path.profile}/${version}/${base.enitity.Profile}/SubscriberId` + + // TODO: Remove once new endpoints are out in production. + export const reward = `${base.path.profile}/${version}/${base.enitity.Profile}/reward` export const tierRewards = `${base.path.profile}/${version}/${base.enitity.Profile}/tierRewards` export function deleteProfile(profileId: string) { @@ -172,9 +174,11 @@ export namespace endpoints { } export namespace Reward { - export const allTiers = `${base.path.profile}/${version}/${base.enitity.Reward}/AllTiers` + export const allTiers = `${base.path.profile}/${version}/${base.enitity.Reward}/allTiers` export const reward = `${base.path.profile}/${version}/${base.enitity.Reward}` - export const unwrap = `${base.path.profile}/${version}/${base.enitity.Reward}/Unwrap` + export const redeem = `${base.path.profile}/${version}/${base.enitity.Reward}/redeem` + export const unwrap = `${base.path.profile}/${version}/${base.enitity.Reward}/unwrap` + // TODO: add surprise endpoint once available. export function claim(rewardId: string) { return `${base.path.profile}/${version}/${base.enitity.Reward}/Claim/${rewardId}` diff --git a/server/routers/contentstack/reward/output.ts b/server/routers/contentstack/reward/output.ts index 5bd2c7d75..a6687b92a 100644 --- a/server/routers/contentstack/reward/output.ts +++ b/server/routers/contentstack/reward/output.ts @@ -91,6 +91,22 @@ export const validateApiTierRewardsSchema = z.record( ) ) +export const validateApiAllTiersSchema = z.record( + z.nativeEnum(TierKey).transform((data) => { + return TierKey[data as unknown as Key] + }), + z.array( + z.object({ + id: z.string().optional(), + status: z.string().optional(), + rewardId: z.string().optional(), + rewardTierLevel: z.string().optional(), + rewardType: z.string().optional(), + title: z.string().optional(), + }) + ) +) + export const validateCmsRewardsSchema = z .object({ data: z.object({ diff --git a/server/routers/contentstack/reward/query.ts b/server/routers/contentstack/reward/query.ts index 0c1577182..b09cb320e 100644 --- a/server/routers/contentstack/reward/query.ts +++ b/server/routers/contentstack/reward/query.ts @@ -1,3 +1,4 @@ +import { env } from "@/env/server" import * as api from "@/lib/api" import { notFound } from "@/server/errors/trpc" import { @@ -22,6 +23,7 @@ import { getByLevelRewardCounter, getByLevelRewardFailCounter, getByLevelRewardSuccessCounter, + getCachedAllTierRewards, getCmsRewards, getCurrentRewardCounter, getCurrentRewardFailCounter, @@ -36,7 +38,10 @@ export const rewardQueryRouter = router({ .input(rewardsAllInput) .query(async function ({ input, ctx }) { getAllRewardCounter.add(1) - const allApiRewards = await getAllCachedApiRewards(ctx.serviceToken) + + const allApiRewards = !!env.USE_NEW_REWARDS_ENDPOINT + ? await getCachedAllTierRewards(ctx.serviceToken) + : await getAllCachedApiRewards(ctx.serviceToken) if (!allApiRewards) { return [] @@ -96,9 +101,9 @@ export const rewardQueryRouter = router({ getByLevelRewardCounter.add(1) const { level_id } = input - const allUpcomingApiRewards = await getAllCachedApiRewards( - ctx.serviceToken - ) + const allUpcomingApiRewards = !!env.USE_NEW_REWARDS_ENDPOINT + ? await getCachedAllTierRewards(ctx.serviceToken) + : await getAllCachedApiRewards(ctx.serviceToken) if (!allUpcomingApiRewards || !allUpcomingApiRewards[level_id]) { getByLevelRewardFailCounter.add(1) diff --git a/server/routers/contentstack/reward/utils.ts b/server/routers/contentstack/reward/utils.ts index 8aab49955..8f1e50343 100644 --- a/server/routers/contentstack/reward/utils.ts +++ b/server/routers/contentstack/reward/utils.ts @@ -11,6 +11,7 @@ import { generateLoyaltyConfigTag } from "@/utils/generateTag" import { CmsRewardsResponse, + validateApiAllTiersSchema, validateApiTierRewardsSchema, validateCmsRewardsSchema, } from "./output" @@ -52,7 +53,8 @@ export function getUniqueRewardIds(rewardIds: string[]) { } /** - * Cached for 1 hour. + * Uses profile/v1/Profile/tierRewards. + * Will be removed when new endpoint is out in production. */ export const getAllCachedApiRewards = unstable_cache( async function (token) { @@ -110,6 +112,68 @@ export const getAllCachedApiRewards = unstable_cache( { revalidate: ONE_HOUR } ) +/** + * Cached for 1 hour. + */ +export const getCachedAllTierRewards = unstable_cache( + async function (token) { + const apiResponse = await api.get( + api.endpoints.v1.Profile.Reward.allTiers, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) + + if (!apiResponse.ok) { + const text = await apiResponse.text() + getAllRewardFailCounter.add(1, { + error_type: "http_error", + error: JSON.stringify({ + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }), + }) + console.error( + "api.rewards.allTiers error ", + JSON.stringify({ + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }, + }) + ) + + throw apiResponse + } + + const data = await apiResponse.json() + const validatedApiAllTierRewards = validateApiAllTiersSchema.safeParse(data) + + if (!validatedApiAllTierRewards.success) { + getAllRewardFailCounter.add(1, { + error_type: "validation_error", + error: JSON.stringify(validatedApiAllTierRewards.error), + }) + console.error(validatedApiAllTierRewards.error) + console.error( + "api.rewards validation error", + JSON.stringify({ + error: validatedApiAllTierRewards.error, + }) + ) + throw validatedApiAllTierRewards.error + } + + return validatedApiAllTierRewards.data + }, + ["getApiAllTierRewards"], + { revalidate: ONE_HOUR } +) + export async function getCmsRewards(locale: Lang, rewardIds: string[]) { const tags = rewardIds.map((id) => generateLoyaltyConfigTag(locale, "reward", id) From 31a94cfa6af0a4076aa98b8b6cf082b10326c1e1 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Tue, 12 Nov 2024 14:28:52 +0100 Subject: [PATCH 25/98] feat: update current & surprise queries to support new endpoint --- server/routers/contentstack/reward/output.ts | 74 +++++++++++++++----- server/routers/contentstack/reward/query.ts | 39 ++++++++--- server/routers/contentstack/reward/utils.ts | 4 +- 3 files changed, 89 insertions(+), 28 deletions(-) diff --git a/server/routers/contentstack/reward/output.ts b/server/routers/contentstack/reward/output.ts index a6687b92a..8e954e656 100644 --- a/server/routers/contentstack/reward/output.ts +++ b/server/routers/contentstack/reward/output.ts @@ -91,22 +91,6 @@ export const validateApiTierRewardsSchema = z.record( ) ) -export const validateApiAllTiersSchema = z.record( - z.nativeEnum(TierKey).transform((data) => { - return TierKey[data as unknown as Key] - }), - z.array( - z.object({ - id: z.string().optional(), - status: z.string().optional(), - rewardId: z.string().optional(), - rewardTierLevel: z.string().optional(), - rewardType: z.string().optional(), - title: z.string().optional(), - }) - ) -) - export const validateCmsRewardsSchema = z .object({ data: z.object({ @@ -138,3 +122,61 @@ export type SurpriseReward = z.output export type CmsRewardsResponse = z.input export type Reward = z.output[0] + +// New endpoint related types and schemas. + +const BenefitReward = z.object({ + title: z.string().optional(), + id: z.string().optional(), + status: z.string().optional(), + rewardId: z.string().optional(), + rewardType: z.string().optional(), + rewardTierLevel: z.string().optional(), +}) + +const CouponState = z.enum(["claimed", "redeemed", "viewed"]) +const CouponData = z.object({ + couponCode: z.string().optional(), + unwrapped: z.boolean().default(false), + state: CouponState, + expiresAt: z.string().datetime({ offset: true }).optional(), +}) + +const CouponReward = z.object({ + title: z.string().optional(), + id: z.string().optional(), + rewardId: z.string().optional(), + rewardType: z.string().optional(), + status: z.string().optional(), + coupon: z.array(CouponData).optional(), +}) + +/** + * Schema for the new /profile/v1/Reward endpoint. + * + * TODO: Once we fully migrate to the new endpoint: + * 1. Remove the data transform and use the categorized structure directly. + * 2. Simplify surprise filtering in the query. + */ +export const validateCategorizedRewardsSchema = z + .object({ + benefits: z.array(BenefitReward), + coupons: z.array(CouponReward), + }) + .transform((data) => [ + ...data.benefits.map((benefit) => ({ + ...benefit, + type: "custom" as const, // Added for legacy compatibility. + })), + ...data.coupons.map((coupon) => ({ + ...coupon, + type: "coupon" as const, // Added for legacy compatibility. + })), + ]) + +export const validateApiAllTiersSchema = z.record( + z.nativeEnum(TierKey).transform((data) => { + return TierKey[data as unknown as Key] + }), + z.array(BenefitReward) +) diff --git a/server/routers/contentstack/reward/query.ts b/server/routers/contentstack/reward/query.ts index b09cb320e..565ead0a5 100644 --- a/server/routers/contentstack/reward/query.ts +++ b/server/routers/contentstack/reward/query.ts @@ -14,7 +14,12 @@ import { rewardsCurrentInput, rewardsUpdateInput, } from "./input" -import { Reward, SurpriseReward, validateApiRewardSchema } from "./output" +import { + Reward, + SurpriseReward, + validateApiRewardSchema, + validateCategorizedRewardsSchema, +} from "./output" import { getAllCachedApiRewards, getAllRewardCounter, @@ -33,6 +38,8 @@ import { import { Surprise } from "@/types/components/blocks/surprises" +const ONE_HOUR = 60 * 60 + export const rewardQueryRouter = router({ all: contentStackBaseWithServiceProcedure .input(rewardsAllInput) @@ -154,12 +161,17 @@ export const rewardQueryRouter = router({ const { limit, cursor } = input - const apiResponse = await api.get(api.endpoints.v1.Profile.reward, { + const isNewEndpoint = !!env.USE_NEW_REWARDS_ENDPOINT + const endpoint = isNewEndpoint + ? api.endpoints.v1.Profile.Reward.reward + : api.endpoints.v1.Profile.reward + + const apiResponse = await api.get(endpoint, { cache: undefined, // override defaultOptions headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, }, - next: { revalidate: 60 * 60 }, + next: { revalidate: ONE_HOUR }, }) if (!apiResponse.ok) { @@ -186,8 +198,9 @@ export const rewardQueryRouter = router({ } const data = await apiResponse.json() - - const validatedApiRewards = validateApiRewardSchema.safeParse(data) + const validatedApiRewards = isNewEndpoint + ? validateCategorizedRewardsSchema.safeParse(data) + : validateApiRewardSchema.safeParse(data) if (!validatedApiRewards.success) { getCurrentRewardFailCounter.add(1, { @@ -243,12 +256,17 @@ export const rewardQueryRouter = router({ surprises: contentStackBaseWithProtectedProcedure.query(async ({ ctx }) => { getCurrentRewardCounter.add(1) - const apiResponse = await api.get(api.endpoints.v1.Profile.reward, { - cache: undefined, // override defaultOptions + const isNewEndpoint = !!env.USE_NEW_REWARDS_ENDPOINT + const endpoint = isNewEndpoint + ? api.endpoints.v1.Profile.Reward.reward + : api.endpoints.v1.Profile.reward + + const apiResponse = await api.get(endpoint, { + cache: undefined, headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, }, - next: { revalidate: 60 * 60 }, + next: { revalidate: ONE_HOUR }, }) if (!apiResponse.ok) { @@ -275,8 +293,9 @@ export const rewardQueryRouter = router({ } const data = await apiResponse.json() - - const validatedApiRewards = validateApiRewardSchema.safeParse(data) + const validatedApiRewards = isNewEndpoint + ? validateCategorizedRewardsSchema.safeParse(data) + : validateApiRewardSchema.safeParse(data) if (!validatedApiRewards.success) { getCurrentRewardFailCounter.add(1, { diff --git a/server/routers/contentstack/reward/utils.ts b/server/routers/contentstack/reward/utils.ts index 8f1e50343..d06f2666b 100644 --- a/server/routers/contentstack/reward/utils.ts +++ b/server/routers/contentstack/reward/utils.ts @@ -53,8 +53,8 @@ export function getUniqueRewardIds(rewardIds: string[]) { } /** - * Uses profile/v1/Profile/tierRewards. - * Will be removed when new endpoint is out in production. + * Uses the legacy profile/v1/Profile/tierRewards endpoint. + * TODO: Delete when the new endpoint is out in production. */ export const getAllCachedApiRewards = unstable_cache( async function (token) { From 876370e81738e109bf5bf1d437f0e653062e680f Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Tue, 12 Nov 2024 15:22:35 +0100 Subject: [PATCH 26/98] fix(SW-739): pr review fixes --- lib/api/endpoints.ts | 1 - server/routers/contentstack/reward/query.ts | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/api/endpoints.ts b/lib/api/endpoints.ts index 4e5258db0..781a19f38 100644 --- a/lib/api/endpoints.ts +++ b/lib/api/endpoints.ts @@ -178,7 +178,6 @@ export namespace endpoints { export const reward = `${base.path.profile}/${version}/${base.enitity.Reward}` export const redeem = `${base.path.profile}/${version}/${base.enitity.Reward}/redeem` export const unwrap = `${base.path.profile}/${version}/${base.enitity.Reward}/unwrap` - // TODO: add surprise endpoint once available. export function claim(rewardId: string) { return `${base.path.profile}/${version}/${base.enitity.Reward}/Claim/${rewardId}` diff --git a/server/routers/contentstack/reward/query.ts b/server/routers/contentstack/reward/query.ts index 565ead0a5..b1f5ccb8c 100644 --- a/server/routers/contentstack/reward/query.ts +++ b/server/routers/contentstack/reward/query.ts @@ -46,7 +46,7 @@ export const rewardQueryRouter = router({ .query(async function ({ input, ctx }) { getAllRewardCounter.add(1) - const allApiRewards = !!env.USE_NEW_REWARDS_ENDPOINT + const allApiRewards = env.USE_NEW_REWARDS_ENDPOINT ? await getCachedAllTierRewards(ctx.serviceToken) : await getAllCachedApiRewards(ctx.serviceToken) @@ -108,7 +108,7 @@ export const rewardQueryRouter = router({ getByLevelRewardCounter.add(1) const { level_id } = input - const allUpcomingApiRewards = !!env.USE_NEW_REWARDS_ENDPOINT + const allUpcomingApiRewards = env.USE_NEW_REWARDS_ENDPOINT ? await getCachedAllTierRewards(ctx.serviceToken) : await getAllCachedApiRewards(ctx.serviceToken) @@ -146,7 +146,7 @@ export const rewardQueryRouter = router({ if (contentStackReward) { return contentStackReward } else { - console.error("No contentStackReward found", reward?.rewardId) + console.info("No contentStackReward found", reward?.rewardId) } }) .filter((reward): reward is Reward => Boolean(reward)) @@ -161,7 +161,7 @@ export const rewardQueryRouter = router({ const { limit, cursor } = input - const isNewEndpoint = !!env.USE_NEW_REWARDS_ENDPOINT + const isNewEndpoint = env.USE_NEW_REWARDS_ENDPOINT const endpoint = isNewEndpoint ? api.endpoints.v1.Profile.Reward.reward : api.endpoints.v1.Profile.reward @@ -256,7 +256,7 @@ export const rewardQueryRouter = router({ surprises: contentStackBaseWithProtectedProcedure.query(async ({ ctx }) => { getCurrentRewardCounter.add(1) - const isNewEndpoint = !!env.USE_NEW_REWARDS_ENDPOINT + const isNewEndpoint = env.USE_NEW_REWARDS_ENDPOINT const endpoint = isNewEndpoint ? api.endpoints.v1.Profile.Reward.reward : api.endpoints.v1.Profile.reward From d5efda747a0c3915cabca81adbc64d6855046b4b Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 13 Nov 2024 11:59:55 +0100 Subject: [PATCH 27/98] feat(SW-828): Added placeholder image --- .../RoomSelection/RoomCard/index.tsx | 5 +++-- .../RoomCard/roomCard.module.css | 19 ++++++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index 93dc20ab4..83602b102 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -7,7 +7,6 @@ import { RateDefinition } from "@/server/routers/hotels/output" import ToggleSidePeek from "@/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek" import FlexibilityOption from "@/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption" -import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" @@ -165,8 +164,10 @@ export default function RoomCard({
{/*NOTE: images from the test API are hosted on test3.scandichotels.com, which can't be accessed unless on Scandic's Wifi or using Citrix. */} - {images && ( + {images ? ( + ) : ( +
)}
)} diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css index a2c5724cd..9567e3734 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css @@ -78,6 +78,23 @@ } .imageContainer { - min-height: 185px; + min-height: 190px; position: relative; } + +.imagePlaceholder { + height: 100%; + min-height: 190px; + width: 100%; + background-color: #fff; + background-image: linear-gradient(45deg, #000000 25%, transparent 25%), + linear-gradient(-45deg, #000000 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, #000000 75%), + linear-gradient(-45deg, transparent 75%, #000000 75%); + background-size: 160px 160px; + background-position: + 0 0, + 0 80px, + 80px -80px, + -80px 0; +} From 16d7f5bf8f6ffba2d14ad055845467ba4aa0d44f Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 13 Nov 2024 12:06:30 +0100 Subject: [PATCH 28/98] feat(SW-828): Small UI fixes --- .../FlexibilityOption/PriceList/priceList.module.css | 1 - 1 file changed, 1 deletion(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/priceList.module.css b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/priceList.module.css index c09ba7df0..3209c4584 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/priceList.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/priceList.module.css @@ -6,7 +6,6 @@ display: flex; justify-content: space-between; align-items: baseline; - padding: var(--Spacing-x-quarter) 0; } .priceTable { From 9a667257a4d80ca4915f891e5830a6953c72978f Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 13 Nov 2024 13:50:08 +0100 Subject: [PATCH 29/98] feat(SW-829): Added MemberDiscountBanner to Rate Summary --- .../RoomSelection/RateSummary/index.tsx | 146 +++++++++++------- .../RateSummary/rateSummary.module.css | 49 +++++- i18n/dictionaries/da.json | 2 + i18n/dictionaries/de.json | 2 + i18n/dictionaries/en.json | 2 + i18n/dictionaries/fi.json | 2 + i18n/dictionaries/no.json | 2 + i18n/dictionaries/sv.json | 2 + 8 files changed, 146 insertions(+), 61 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx index f3ead2161..daa4ceacd 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx @@ -52,69 +52,105 @@ export default function RateSummary({ const checkOutDate = new Date(roomsAvailability.checkOutDate) const nights = dt(checkOutDate).diff(dt(checkInDate), "days") + const showMemberDiscountBanner = member && !isUserLoggedIn + return (
+ {showMemberDiscountBanner && ( +
+ + {intl.formatMessage({ + id: "Join or log in while booking for member pricing.", + })} + +
+ )}
{roomType} {priceName}
-
-
- - {priceToShow?.localPrice.pricePerStay}{" "} - {priceToShow?.localPrice.currency} - - - {intl.formatMessage({ id: "Approx." })}{" "} - {priceToShow?.requestedPrice?.pricePerStay}{" "} - {priceToShow?.requestedPrice?.currency} - -
-
-
- - {priceToShow?.localPrice.pricePerStay}{" "} - {priceToShow?.localPrice.currency} - - - {intl.formatMessage( - { id: "booking.nights" }, - { totalNights: nights } - )} - ,{" "} - {intl.formatMessage( - { id: "booking.adults" }, - { totalAdults: roomsAvailability.occupancy?.adults } - )} - {roomsAvailability.occupancy?.children?.length && ( - <> - ,{" "} - {intl.formatMessage( - { id: "booking.children" }, - { totalChildren: roomsAvailability.occupancy.children.length } - )} - - )} - - - {isPetRoomSelected && ( -
- - + {petRoomPrice} {petRoomCurrency} - - - {intl.formatMessage({ id: "Pet charge" })} - +
+ {showMemberDiscountBanner && ( +
+ + {intl.formatMessage( + { + id: "To get the member price {amount} {currency}, log in or join when completing the booking.", + }, + { + span: (str) => ( +
+ ), + amount: member.localPrice.pricePerStay, + currency: member.localPrice.currency, + } + )} + )} - +
+
+ + {priceToShow?.localPrice.pricePerStay}{" "} + {priceToShow?.localPrice.currency} + + + {intl.formatMessage({ id: "Approx." })}{" "} + {priceToShow?.requestedPrice?.pricePerStay}{" "} + {priceToShow?.requestedPrice?.currency} + +
+
+
+ + {priceToShow?.localPrice.pricePerStay}{" "} + {priceToShow?.localPrice.currency} + + + {intl.formatMessage( + { id: "booking.nights" }, + { totalNights: nights } + )} + ,{" "} + {intl.formatMessage( + { id: "booking.adults" }, + { totalAdults: roomsAvailability.occupancy?.adults } + )} + {roomsAvailability.occupancy?.children?.length && ( + <> + ,{" "} + {intl.formatMessage( + { id: "booking.children" }, + { + totalChildren: + roomsAvailability.occupancy.children.length, + } + )} + + )} + + + {isPetRoomSelected && ( +
+ + + {petRoomPrice} {petRoomCurrency} + + + {intl.formatMessage({ id: "Pet charge" })} + +
+ )} + + ) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css index 3152b5cf6..4c49741c5 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css @@ -5,8 +5,9 @@ left: 0; right: 0; background-color: var(--Base-Surface-Primary-light-Normal); - padding: var(--Spacing-x2) var(--Spacing-x3) var(--Spacing-x5); + padding: 0 0 var(--Spacing-x5); display: flex; + flex-direction: column; justify-content: space-between; align-items: center; border-top: 1px solid var(--Base-Border-Subtle); @@ -17,6 +18,14 @@ bottom: 0; } +.summaryPriceContainer { + display: flex; + flex-direction: row; + gap: var(--Spacing-x4); + padding: var(--Spacing-x2) var(--Spacing-x3) 0; + width: 100%; +} + .summaryPrice { display: flex; width: 100%; @@ -48,22 +57,50 @@ white-space: nowrap; } +.memberDiscountBannerDesktop { + display: none; + background: var(--Primary-Light-Surface-Normal); + border-radius: var(--Corner-radius-xLarge) var(--Corner-radius-xLarge) 0px + var(--Corner-radius-xLarge); + flex-direction: row; + align-items: center; + padding: var(--Spacing-x1) var(--Spacing-x-one-and-half); + gap: var(--Spacing-x2); + max-width: 264px; +} + +.memberDiscountBannerMobile { + width: 100%; + background: var(--Primary-Light-Surface-Normal); + padding: var(--Spacing-x-one-and-half); + display: flex; + align-items: center; + justify-content: center; +} + @media (min-width: 768px) { .summary { - padding: var(--Spacing-x3) var(--Spacing-x7) var(--Spacing-x5); + padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x5); + flex-direction: row; } .petInfo, .summaryText, .summaryPriceTextDesktop { display: block; } - .summaryPriceTextMobile { + .memberDiscountBannerDesktop { + display: flex; + } + .summaryPriceTextMobile, + .memberDiscountBannerMobile { display: none; } - .summaryPrice { - width: auto; - } + .summaryPrice, .continueButton { width: auto; } + .summaryPriceContainer { + width: auto; + padding: 0; + } } diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 1ebe5d3f9..6ead0af95 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -159,6 +159,7 @@ "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke muligt at administrere dine kommunikationspræferencer lige nu, prøv venligst igen senere eller kontakt support, hvis problemet fortsætter.", "Join Scandic Friends": "Tilmeld dig Scandic Friends", "Join at no cost": "Tilmeld dig uden omkostninger", + "Join or log in while booking for member pricing.": "Tilmeld dig eller log ind under booking for medlemspris.", "King bed": "Kingsize-seng", "Language": "Sprog", "Last name": "Efternavn", @@ -330,6 +331,7 @@ "Theatre": "Teater", "There are no transactions to display": "Der er ingen transaktioner at vise", "Things nearby HOTEL_NAME": "Ting i nærheden af {hotelName}", + "To get the member price {amount} {currency}, log in or join when completing the booking.": "For at få medlemsprisen {amount} {currency}, log ind eller tilmeld dig, når du udfylder bookingen.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For at sikre din reservation, beder vi om at du giver os dine betalingsoplysninger. Du kan så være sikker på, at ingen gebyrer vil blive opkrævet på dette tidspunkt.", "Total Points": "Samlet antal point", "Total price": "Samlet pris", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 71231512a..8a1a463d1 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -159,6 +159,7 @@ "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Es ist derzeit nicht möglich, Ihre Kommunikationseinstellungen zu verwalten. Bitte versuchen Sie es später erneut oder wenden Sie sich an den Support, wenn das Problem weiterhin besteht.", "Join Scandic Friends": "Treten Sie Scandic Friends bei", "Join at no cost": "Kostenlos beitreten", + "Join or log in while booking for member pricing.": "Treten Sie Scandic Friends bei oder loggen Sie sich ein, um den Mitgliederpreis zu erhalten.", "King bed": "Kingsize-Bett", "Language": "Sprache", "Last name": "Nachname", @@ -329,6 +330,7 @@ "Theatre": "Theater", "There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden", "Things nearby HOTEL_NAME": "Dinge in der Nähe von {hotelName}", + "To get the member price {amount} {currency}, log in or join when completing the booking.": "Um den Mitgliederpreis von {amount} {currency} zu erhalten, loggen Sie sich ein oder treten Sie Scandic Friends bei, wenn Sie die Buchung abschließen.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Um Ihre Reservierung zu sichern, bitten wir Sie, Ihre Zahlungskarteninformationen zu geben. Sie können sicher sein, dass keine Gebühren zu diesem Zeitpunkt erhoben werden.", "Total Points": "Gesamtpunktzahl", "Total price": "Gesamtpreis", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 604b52c60..0f28e3231 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -171,6 +171,7 @@ "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.", "Join Scandic Friends": "Join Scandic Friends", "Join at no cost": "Join at no cost", + "Join or log in while booking for member pricing.": "Join or log in while booking for member pricing.", "King bed": "King bed", "Language": "Language", "Last name": "Last name", @@ -359,6 +360,7 @@ "Theatre": "Theatre", "There are no transactions to display": "There are no transactions to display", "Things nearby HOTEL_NAME": "Things nearby {hotelName}", + "To get the member price {amount} {currency}, log in or join when completing the booking.": "To get the member price {amount} {currency}, log in or join when completing the booking.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.", "Total Points": "Total Points", "Total cost": "Total cost", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 665a6e374..24325460c 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -159,6 +159,7 @@ "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Viestintäasetuksiasi ei voi hallita juuri nyt. Yritä myöhemmin uudelleen tai ota yhteyttä tukeen, jos ongelma jatkuu.", "Join Scandic Friends": "Liity jäseneksi", "Join at no cost": "Liity maksutta", + "Join or log in while booking for member pricing.": "Liity tai kirjaudu sisään, kun varaat jäsenhinnan.", "King bed": "King-vuode", "Language": "Kieli", "Last name": "Sukunimi", @@ -331,6 +332,7 @@ "Theatre": "Teatteri", "There are no transactions to display": "Näytettäviä tapahtumia ei ole", "Things nearby HOTEL_NAME": "Lähellä olevia asioita {hotelName}", + "To get the member price {amount} {currency}, log in or join when completing the booking.": "Jäsenhintaan saavat sisäänkirjautuneet tai liittyneet jäsenet.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Varmistaaksesi varauksen, pyydämme sinua antamaan meille maksukortin tiedot. Varmista, että ei veloiteta maksusi tällä hetkellä.", "Total Points": "Kokonaispisteet", "Total price": "Kokonaishinta", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 075c0b2d8..5a7dfe511 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -157,6 +157,7 @@ "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke mulig å administrere kommunikasjonspreferansene dine akkurat nå, prøv igjen senere eller kontakt support hvis problemet vedvarer.", "Join Scandic Friends": "Bli med i Scandic Friends", "Join at no cost": "Bli med uten kostnad", + "Join or log in while booking for member pricing.": "Bli med eller logg inn under bestilling for medlemspris.", "King bed": "King-size-seng", "Language": "Språk", "Last name": "Etternavn", @@ -328,6 +329,7 @@ "Theatre": "Teater", "There are no transactions to display": "Det er ingen transaksjoner å vise", "Things nearby HOTEL_NAME": "Ting i nærheten av {hotelName}", + "To get the member price {amount} {currency}, log in or join when completing the booking.": "For å få medlemsprisen {amount} {currency}, logg inn eller bli med når du fullfører bestillingen.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For å sikre din reservasjon, ber vi om at du gir oss dine betalingskortdetaljer. Vær sikker på at ingen gebyrer vil bli belastet på dette tidspunktet.", "Total Points": "Totale poeng", "Total incl VAT": "Sum inkl mva", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 9b6b51748..9aeced109 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -157,6 +157,7 @@ "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det gick inte att hantera dina kommunikationsinställningar just nu, försök igen senare eller kontakta supporten om problemet kvarstår.", "Join Scandic Friends": "Gå med i Scandic Friends", "Join at no cost": "Gå med utan kostnad", + "Join or log in while booking for member pricing.": "Bli medlem eller logga in när du bokar för medlemspriser.", "King bed": "King size-säng", "Language": "Språk", "Last name": "Efternamn", @@ -328,6 +329,7 @@ "Theatre": "Teater", "There are no transactions to display": "Det finns inga transaktioner att visa", "Things nearby HOTEL_NAME": "Saker i närheten av {hotelName}", + "To get the member price {amount} {currency}, log in or join when completing the booking.": "För att få medlemsprisen {amount} {currency}, logga in eller bli medlem när du slutför bokningen.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "För att säkra din bokning ber vi om att du ger oss dina betalkortdetaljer. Välj säker på att ingen avgifter kommer att debiteras just nu.", "Total Points": "Poäng totalt", "Total incl VAT": "Totalt inkl moms", From 80377f4e4e3ea4d8c33ad991d556212d107420fe Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 13 Nov 2024 14:05:01 +0100 Subject: [PATCH 30/98] feat(SW-829): Added Total price (VAT) --- .../RoomSelection/RateSummary/index.tsx | 59 +++++++++++-------- .../Text/Body/body.module.css | 4 ++ .../TempDesignSystem/Text/Body/variants.ts | 2 + .../Text/Subtitle/subtitle.module.css | 4 ++ .../Text/Subtitle/variants.ts | 1 + 5 files changed, 46 insertions(+), 24 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx index daa4ceacd..33400efaf 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx @@ -54,6 +54,21 @@ export default function RateSummary({ const showMemberDiscountBanner = member && !isUserLoggedIn + const summaryPriceTex = `${intl.formatMessage( + { id: "booking.nights" }, + { totalNights: nights } + )}, ${intl.formatMessage( + { id: "booking.adults" }, + { totalAdults: roomsAvailability.occupancy?.adults } + )}${ + roomsAvailability.occupancy?.children?.length + ? `, ${intl.formatMessage( + { id: "booking.children" }, + { totalChildren: roomsAvailability.occupancy.children.length } + )}` + : "" + }` + return (
{showMemberDiscountBanner && ( @@ -90,9 +105,21 @@ export default function RateSummary({
)} +
+ + {intl.formatMessage( + { id: "Total price (incl VAT)" }, + { b: (str) => {str} } + )} + +
+
- + {priceToShow?.localPrice.pricePerStay}{" "} {priceToShow?.localPrice.currency} @@ -114,35 +141,19 @@ export default function RateSummary({ color="uiTextMediumContrast" className={styles.summaryPriceTextMobile} > - {intl.formatMessage( - { id: "booking.nights" }, - { totalNights: nights } - )} - ,{" "} - {intl.formatMessage( - { id: "booking.adults" }, - { totalAdults: roomsAvailability.occupancy?.adults } - )} - {roomsAvailability.occupancy?.children?.length && ( - <> - ,{" "} - {intl.formatMessage( - { id: "booking.children" }, - { - totalChildren: - roomsAvailability.occupancy.children.length, - } - )} - - )} + {summaryPriceTex}
{isPetRoomSelected && (
- + + {petRoomPrice} {petRoomCurrency} - + {intl.formatMessage({ id: "Pet charge" })}
diff --git a/components/TempDesignSystem/Text/Body/body.module.css b/components/TempDesignSystem/Text/Body/body.module.css index dc2c8e067..a39439815 100644 --- a/components/TempDesignSystem/Text/Body/body.module.css +++ b/components/TempDesignSystem/Text/Body/body.module.css @@ -52,6 +52,10 @@ text-align: left; } +.textAlignRight { + text-align: right; +} + .black { color: var(--Main-Grey-100); } diff --git a/components/TempDesignSystem/Text/Body/variants.ts b/components/TempDesignSystem/Text/Body/variants.ts index 898adc3c4..aa614fe14 100644 --- a/components/TempDesignSystem/Text/Body/variants.ts +++ b/components/TempDesignSystem/Text/Body/variants.ts @@ -30,6 +30,7 @@ const config = { textAlign: { center: styles.textAlignCenter, left: styles.textAlignLeft, + right: styles.textAlignRight, }, textTransform: { bold: styles.bold, @@ -52,6 +53,7 @@ const fontOnlyconfig = { textAlign: { center: styles.textAlignCenter, left: styles.textAlignLeft, + right: styles.textAlignRight, }, textTransform: { bold: styles.bold, diff --git a/components/TempDesignSystem/Text/Subtitle/subtitle.module.css b/components/TempDesignSystem/Text/Subtitle/subtitle.module.css index 8207d7523..b15623491 100644 --- a/components/TempDesignSystem/Text/Subtitle/subtitle.module.css +++ b/components/TempDesignSystem/Text/Subtitle/subtitle.module.css @@ -47,6 +47,10 @@ text-align: left; } +.right { + text-align: right; +} + .black { color: var(--Main-Grey-100); } diff --git a/components/TempDesignSystem/Text/Subtitle/variants.ts b/components/TempDesignSystem/Text/Subtitle/variants.ts index 18a0880a8..f2edbb9f5 100644 --- a/components/TempDesignSystem/Text/Subtitle/variants.ts +++ b/components/TempDesignSystem/Text/Subtitle/variants.ts @@ -18,6 +18,7 @@ const config = { textAlign: { center: styles.center, left: styles.left, + right: styles.right, }, textTransform: { regular: styles.regular, From 60b6f0457bd58442f488724eb98791da3bceefc7 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 13 Nov 2024 14:18:33 +0100 Subject: [PATCH 31/98] feat(SW-828): Moved placeholder image to ImageGallery --- components/HotelReservation/HotelCard/index.tsx | 7 +------ .../SelectRate/HotelInfoCard/index.tsx | 10 ++++------ .../ImageGallery/imageGallery.module.css | 17 +++++++++++++++++ .../SelectRate/ImageGallery/index.tsx | 3 +++ .../SelectRate/RoomSelection/RoomCard/index.tsx | 6 +----- .../RoomSelection/RoomCard/roomCard.module.css | 17 ----------------- components/SidePeeks/RoomSidePeek/index.tsx | 8 +++----- .../hotelReservation/selectRate/imageGallery.ts | 2 +- 8 files changed, 30 insertions(+), 40 deletions(-) diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 623990987..87b191cf0 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -56,12 +56,7 @@ export default function HotelCard({ onMouseLeave={handleMouseLeave} >
- {hotelData?.galleryImages && ( - - )} +
diff --git a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx index 1d0fb8df7..9ec4cf8fd 100644 --- a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx +++ b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx @@ -29,12 +29,10 @@ export default function HotelInfoCard({ hotelData }: HotelInfoCardProps) { {hotelAttributes && (
- {hotelAttributes?.galleryImages && ( - - )} + {hotelAttributes.ratings?.tripAdvisor && (
diff --git a/components/HotelReservation/SelectRate/ImageGallery/imageGallery.module.css b/components/HotelReservation/SelectRate/ImageGallery/imageGallery.module.css index 5d156f77a..c346f4d04 100644 --- a/components/HotelReservation/SelectRate/ImageGallery/imageGallery.module.css +++ b/components/HotelReservation/SelectRate/ImageGallery/imageGallery.module.css @@ -15,3 +15,20 @@ .triggerArea { cursor: pointer; } + +.imagePlaceholder { + height: 100%; + min-height: 190px; + width: 100%; + background-color: #fff; + background-image: linear-gradient(45deg, #000000 25%, transparent 25%), + linear-gradient(-45deg, #000000 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, #000000 75%), + linear-gradient(-45deg, transparent 75%, #000000 75%); + background-size: 160px 160px; + background-position: + 0 0, + 0 80px, + 80px -80px, + -80px 0; +} diff --git a/components/HotelReservation/SelectRate/ImageGallery/index.tsx b/components/HotelReservation/SelectRate/ImageGallery/index.tsx index 4ff21af9f..83a88c3d0 100644 --- a/components/HotelReservation/SelectRate/ImageGallery/index.tsx +++ b/components/HotelReservation/SelectRate/ImageGallery/index.tsx @@ -8,6 +8,9 @@ import styles from "./imageGallery.module.css" import type { ImageGalleryProps } from "@/types/components/hotelReservation/selectRate/imageGallery" export default function ImageGallery({ images, title }: ImageGalleryProps) { + if (!images || images.length === 0) + return
+ return (
diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index 83602b102..fc4681412 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -164,11 +164,7 @@ export default function RoomCard({
{/*NOTE: images from the test API are hosted on test3.scandichotels.com, which can't be accessed unless on Scandic's Wifi or using Citrix. */} - {images ? ( - - ) : ( -
- )} +
)}
diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css index 9567e3734..799726d1b 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css @@ -81,20 +81,3 @@ min-height: 190px; position: relative; } - -.imagePlaceholder { - height: 100%; - min-height: 190px; - width: 100%; - background-color: #fff; - background-image: linear-gradient(45deg, #000000 25%, transparent 25%), - linear-gradient(-45deg, #000000 25%, transparent 25%), - linear-gradient(45deg, transparent 75%, #000000 75%), - linear-gradient(-45deg, transparent 75%, #000000 75%); - background-size: 160px 160px; - background-position: - 0 0, - 0 80px, - 80px -80px, - -80px 0; -} diff --git a/components/SidePeeks/RoomSidePeek/index.tsx b/components/SidePeeks/RoomSidePeek/index.tsx index 4a66117b8..2453968e8 100644 --- a/components/SidePeeks/RoomSidePeek/index.tsx +++ b/components/SidePeeks/RoomSidePeek/index.tsx @@ -43,11 +43,9 @@ export default function RoomSidePeek({ { nrOfGuests: occupancy } )} - {images && ( -
- -
- )} +
+ +
{roomDescription}
diff --git a/types/components/hotelReservation/selectRate/imageGallery.ts b/types/components/hotelReservation/selectRate/imageGallery.ts index 5d75189fa..0c16c82e0 100644 --- a/types/components/hotelReservation/selectRate/imageGallery.ts +++ b/types/components/hotelReservation/selectRate/imageGallery.ts @@ -1,3 +1,3 @@ import type { GalleryImage } from "@/types/hotel" -export type ImageGalleryProps = { images: GalleryImage[]; title: string } +export type ImageGalleryProps = { images?: GalleryImage[]; title: string } From 7ee7d60307894d64aa120a4d3eb328f5029145eb Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 13 Nov 2024 14:25:15 +0100 Subject: [PATCH 32/98] feat(SW-874): removed defaultRooms since we have visibleRooms --- components/HotelReservation/SelectRate/Rooms/index.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx index 5e982a10f..c6d271550 100644 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -28,10 +28,9 @@ export default function Rooms({ roomsAvailability.roomConfigurations ) - const defaultRooms = visibleRooms const [rooms, setRooms] = useState({ ...roomsAvailability, - roomConfigurations: defaultRooms, + roomConfigurations: visibleRooms, }) const [selectedPackages, setSelectedPackages] = useState( [] @@ -48,7 +47,7 @@ export default function Rooms({ if (filteredPackages.length === 0) { setRooms({ ...roomsAvailability, - roomConfigurations: defaultRooms, + roomConfigurations: visibleRooms, }) return } @@ -60,7 +59,7 @@ export default function Rooms({ ) setRooms({ ...roomsAvailability, roomConfigurations: filteredRooms }) }, - [roomsAvailability, defaultRooms, visibleRooms] + [roomsAvailability, visibleRooms] ) return ( From a66427fcccbeed5bb7b796765400b28888a64f75 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 13 Nov 2024 15:49:40 +0100 Subject: [PATCH 33/98] feat(SW-826): Hide Button if not isSelectHotelPage and removed accordion for Accessibility --- components/SidePeeks/HotelSidePeek/index.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/components/SidePeeks/HotelSidePeek/index.tsx b/components/SidePeeks/HotelSidePeek/index.tsx index 481db8564..8664ace88 100644 --- a/components/SidePeeks/HotelSidePeek/index.tsx +++ b/components/SidePeeks/HotelSidePeek/index.tsx @@ -31,6 +31,8 @@ export default function HotelSidePeek({ const intl = useIntl() const amenitiesList = getAmenitiesList(hotel) + const isSelectHotelPage = window.location.href.includes("select-hotel") + return ( ) : null} - - TODO: What content should be in the accessibility section? - +
+ {intl.formatMessage({ id: "Accessibility" })} +
{amenitiesList.map((amenity) => { return (
@@ -62,8 +64,10 @@ export default function HotelSidePeek({ ) })} - {/* TODO: handle linking to Hotel Page */} - + {isSelectHotelPage && ( + /* TODO: handle linking to Hotel Page */ + + )}
) From 1595b18644545d633b3ef5a4e051b460f0167c91 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 13 Nov 2024 15:57:49 +0100 Subject: [PATCH 34/98] feat(SW-826): Added Google Maps Link --- components/HotelReservation/Contact/contact.module.css | 6 ++++++ components/HotelReservation/Contact/index.tsx | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/components/HotelReservation/Contact/contact.module.css b/components/HotelReservation/Contact/contact.module.css index 768e01ca2..b9fba3d9b 100644 --- a/components/HotelReservation/Contact/contact.module.css +++ b/components/HotelReservation/Contact/contact.module.css @@ -46,3 +46,9 @@ flex-direction: column; justify-content: center; } + +.googleMaps { + text-decoration: none; + font-family: var(--typography-Body-Regular-fontFamily); + color: var(--Base-Text-Medium-contrast); +} diff --git a/components/HotelReservation/Contact/index.tsx b/components/HotelReservation/Contact/index.tsx index 2f4e8ccd5..b490b6e89 100644 --- a/components/HotelReservation/Contact/index.tsx +++ b/components/HotelReservation/Contact/index.tsx @@ -13,6 +13,7 @@ import styles from "./contact.module.css" import { ContactProps } from "@/types/components/hotelReservation/selectHotel/selectHotel" export default function Contact({ hotel }: ContactProps) { + console.log(hotel) const lang = useLang() const intl = useIntl() @@ -32,9 +33,12 @@ export default function Contact({ hotel }: ContactProps) { {intl.formatMessage({ id: "Driving directions" })} - + Google Maps - +
  • From 56139f4e3c1adb5a24cf2b2f8ebe4ddf47934018 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 13 Nov 2024 15:58:44 +0100 Subject: [PATCH 35/98] feat(SW-826) removed log --- components/HotelReservation/Contact/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/components/HotelReservation/Contact/index.tsx b/components/HotelReservation/Contact/index.tsx index b490b6e89..5aca6f1eb 100644 --- a/components/HotelReservation/Contact/index.tsx +++ b/components/HotelReservation/Contact/index.tsx @@ -13,7 +13,6 @@ import styles from "./contact.module.css" import { ContactProps } from "@/types/components/hotelReservation/selectHotel/selectHotel" export default function Contact({ hotel }: ContactProps) { - console.log(hotel) const lang = useLang() const intl = useIntl() From 69ca6211ec899ac9618c5af54814bb692805e6ac Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 13 Nov 2024 20:13:24 +0100 Subject: [PATCH 36/98] feat(SW-826): Added target="_blank" --- components/HotelReservation/Contact/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/components/HotelReservation/Contact/index.tsx b/components/HotelReservation/Contact/index.tsx index 5aca6f1eb..8eea2e95a 100644 --- a/components/HotelReservation/Contact/index.tsx +++ b/components/HotelReservation/Contact/index.tsx @@ -35,6 +35,7 @@ export default function Contact({ hotel }: ContactProps) { Google Maps From 2beba213231e2fd7f5995109af886ee3ee3355f1 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 13 Nov 2024 20:27:53 +0100 Subject: [PATCH 37/98] feat(SW-874): Updated logic for getLowestPricedDuplicateRooms --- .../SelectRate/Rooms/utils.ts | 47 ++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/components/HotelReservation/SelectRate/Rooms/utils.ts b/components/HotelReservation/SelectRate/Rooms/utils.ts index 3c0507371..d7f7d2d85 100644 --- a/components/HotelReservation/SelectRate/Rooms/utils.ts +++ b/components/HotelReservation/SelectRate/Rooms/utils.ts @@ -46,7 +46,7 @@ export function getLowestPricedDuplicateRooms( localPrice: memberLocalPrice, } = memberProduct - const currentLowest = roomMap.get(roomType) + const previousLowest = roomMap.get(roomType) const currentRequestedPrice = Math.min( Number(publicRequestedPrice.pricePerNight) ?? Infinity, @@ -58,17 +58,42 @@ export function getLowestPricedDuplicateRooms( ) if ( - !currentLowest || - currentRequestedPrice < currentLowest.requestedPrice || - (currentRequestedPrice === currentLowest.requestedPrice && - currentLocalPrice < currentLowest.localPrice) + !previousLowest || + currentRequestedPrice < + Math.min( + Number( + previousLowest.products[0].productType.public.requestedPrice + .pricePerNight + ) ?? Infinity, + Number( + previousLowest.products[0].productType.member?.requestedPrice + ?.pricePerNight + ) ?? Infinity + ) || + (currentRequestedPrice === + Math.min( + Number( + previousLowest.products[0].productType.public.requestedPrice + .pricePerNight + ) ?? Infinity, + Number( + previousLowest.products[0].productType.member?.requestedPrice + ?.pricePerNight + ) ?? Infinity + ) && + currentLocalPrice < + Math.min( + Number( + previousLowest.products[0].productType.public.localPrice + .pricePerNight + ) ?? Infinity, + Number( + previousLowest.products[0].productType.member?.localPrice + ?.pricePerNight + ) ?? Infinity + )) ) { - roomMap.set(roomType, { - ...room, - product, - requestedPrice: currentRequestedPrice, - localPrice: currentLocalPrice, - }) + roomMap.set(roomType, room) } }) }) From 5c85e59bba520c309323bea0e6986cb7845ba0f1 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 13 Nov 2024 20:39:13 +0100 Subject: [PATCH 38/98] feat(SW-826): Added showCTA prop --- components/HotelReservation/ReadMore/index.tsx | 4 ++-- .../HotelReservation/SelectRate/HotelInfoCard/index.tsx | 1 + components/HotelReservation/SidePeek/index.tsx | 2 ++ components/SidePeeks/HotelSidePeek/index.tsx | 5 ++--- stores/sidepeek.ts | 8 ++++++-- types/components/hotelReservation/hotelSidePeek.ts | 1 + .../hotelReservation/selectHotel/selectHotel.ts | 1 + 7 files changed, 15 insertions(+), 7 deletions(-) diff --git a/components/HotelReservation/ReadMore/index.tsx b/components/HotelReservation/ReadMore/index.tsx index fbd6be1ae..f7ec2af84 100644 --- a/components/HotelReservation/ReadMore/index.tsx +++ b/components/HotelReservation/ReadMore/index.tsx @@ -10,12 +10,12 @@ import styles from "./readMore.module.css" import { ReadMoreProps } from "@/types/components/hotelReservation/selectHotel/selectHotel" import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek" -export default function ReadMore({ label, hotelId }: ReadMoreProps) { +export default function ReadMore({ label, hotelId, showCTA }: ReadMoreProps) { const openSidePeek = useSidePeekStore((state) => state.openSidePeek) return (
  • diff --git a/components/HotelReservation/SidePeek/index.tsx b/components/HotelReservation/SidePeek/index.tsx index 505bbe4a4..8ea0ca977 100644 --- a/components/HotelReservation/SidePeek/index.tsx +++ b/components/HotelReservation/SidePeek/index.tsx @@ -17,6 +17,7 @@ export default function HotelReservationSidePeek({ const activeSidePeek = useSidePeekStore((state) => state.activeSidePeek) const hotelId = useSidePeekStore((state) => state.hotelId) const roomTypeCode = useSidePeekStore((state) => state.roomTypeCode) + const showCTA = useSidePeekStore((state) => state.showCTA) const close = useSidePeekStore((state) => state.closeSidePeek) const lang = useLang() @@ -43,6 +44,7 @@ export default function HotelReservationSidePeek({ hotel={hotelData.data?.attributes} activeSidePeek={activeSidePeek} close={close} + showCTA={showCTA} /> )} {selectedRoom && ( diff --git a/components/SidePeeks/HotelSidePeek/index.tsx b/components/SidePeeks/HotelSidePeek/index.tsx index 8664ace88..2cc0eb53a 100644 --- a/components/SidePeeks/HotelSidePeek/index.tsx +++ b/components/SidePeeks/HotelSidePeek/index.tsx @@ -27,12 +27,11 @@ export default function HotelSidePeek({ hotel, activeSidePeek, close, + showCTA = true, }: HotelSidePeekProps) { const intl = useIntl() const amenitiesList = getAmenitiesList(hotel) - const isSelectHotelPage = window.location.href.includes("select-hotel") - return ( - {isSelectHotelPage && ( + {showCTA && ( /* TODO: handle linking to Hotel Page */ )} diff --git a/stores/sidepeek.ts b/stores/sidepeek.ts index 30d6a00e2..9912617ac 100644 --- a/stores/sidepeek.ts +++ b/stores/sidepeek.ts @@ -6,14 +6,17 @@ interface SidePeekState { activeSidePeek: SidePeekEnum | null hotelId: string | null roomTypeCode: string | null + showCTA: boolean openSidePeek: ({ key, hotelId, roomTypeCode, + showCTA, }: { key: SidePeekEnum | null hotelId: string roomTypeCode?: string + showCTA?: boolean }) => void closeSidePeek: () => void } @@ -22,8 +25,9 @@ const useSidePeekStore = create((set) => ({ activeSidePeek: null, hotelId: null, roomTypeCode: null, - openSidePeek: ({ key, hotelId, roomTypeCode }) => - set({ activeSidePeek: key, hotelId, roomTypeCode }), + showCTA: true, + openSidePeek: ({ key, hotelId, roomTypeCode, showCTA }) => + set({ activeSidePeek: key, hotelId, roomTypeCode, showCTA }), closeSidePeek: () => set({ activeSidePeek: null, hotelId: null, roomTypeCode: null }), })) diff --git a/types/components/hotelReservation/hotelSidePeek.ts b/types/components/hotelReservation/hotelSidePeek.ts index 3f5b2ce7c..b531498ae 100644 --- a/types/components/hotelReservation/hotelSidePeek.ts +++ b/types/components/hotelReservation/hotelSidePeek.ts @@ -5,4 +5,5 @@ export type HotelSidePeekProps = { hotel: Hotel activeSidePeek: SidePeekEnum close: () => void + showCTA?: boolean } diff --git a/types/components/hotelReservation/selectHotel/selectHotel.ts b/types/components/hotelReservation/selectHotel/selectHotel.ts index b50521040..233cf0076 100644 --- a/types/components/hotelReservation/selectHotel/selectHotel.ts +++ b/types/components/hotelReservation/selectHotel/selectHotel.ts @@ -9,6 +9,7 @@ export interface ReadMoreProps { label: string hotelId: string hotel: Hotel + showCTA: boolean } export interface ContactProps { From 71ef99147890ce58d453ca87cd0c05375f5e6ce3 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 13 Nov 2024 20:40:22 +0100 Subject: [PATCH 39/98] feat(SW-826): added showCTA --- components/HotelReservation/HotelCard/index.tsx | 1 + components/SidePeeks/HotelSidePeek/index.tsx | 2 +- types/components/hotelReservation/hotelSidePeek.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 248d08779..86a483536 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -108,6 +108,7 @@ export default function HotelCard({ label={intl.formatMessage({ id: "See hotel details" })} hotelId={hotelData.operaId} hotel={hotelData} + showCTA={true} />
    diff --git a/components/SidePeeks/HotelSidePeek/index.tsx b/components/SidePeeks/HotelSidePeek/index.tsx index 2cc0eb53a..06ac3e5f2 100644 --- a/components/SidePeeks/HotelSidePeek/index.tsx +++ b/components/SidePeeks/HotelSidePeek/index.tsx @@ -27,7 +27,7 @@ export default function HotelSidePeek({ hotel, activeSidePeek, close, - showCTA = true, + showCTA, }: HotelSidePeekProps) { const intl = useIntl() const amenitiesList = getAmenitiesList(hotel) diff --git a/types/components/hotelReservation/hotelSidePeek.ts b/types/components/hotelReservation/hotelSidePeek.ts index b531498ae..d188215b5 100644 --- a/types/components/hotelReservation/hotelSidePeek.ts +++ b/types/components/hotelReservation/hotelSidePeek.ts @@ -5,5 +5,5 @@ export type HotelSidePeekProps = { hotel: Hotel activeSidePeek: SidePeekEnum close: () => void - showCTA?: boolean + showCTA: boolean } From 768bd40ad8052801ec8b3f6852b8975185d416b7 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 13 Nov 2024 20:41:48 +0100 Subject: [PATCH 40/98] feat(SW-874): Update function name --- components/HotelReservation/SelectRate/Rooms/index.tsx | 7 +++---- components/HotelReservation/SelectRate/Rooms/utils.ts | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx index c6d271550..8e56555a2 100644 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -4,7 +4,7 @@ import { useCallback, useState } from "react" import RoomFilter from "../RoomFilter" import RoomSelection from "../RoomSelection" -import { getLowestPricedDuplicateRooms } from "./utils" +import { filterDuplicateRoomTypesByLowestPrice } from "./utils" import styles from "./rooms.module.css" @@ -24,9 +24,8 @@ export default function Rooms({ user, packages, }: Omit) { - const visibleRooms: RoomConfiguration[] = getLowestPricedDuplicateRooms( - roomsAvailability.roomConfigurations - ) + const visibleRooms: RoomConfiguration[] = + filterDuplicateRoomTypesByLowestPrice(roomsAvailability.roomConfigurations) const [rooms, setRooms] = useState({ ...roomsAvailability, diff --git a/components/HotelReservation/SelectRate/Rooms/utils.ts b/components/HotelReservation/SelectRate/Rooms/utils.ts index d7f7d2d85..9d6f24e00 100644 --- a/components/HotelReservation/SelectRate/Rooms/utils.ts +++ b/components/HotelReservation/SelectRate/Rooms/utils.ts @@ -4,7 +4,7 @@ import { RoomConfiguration } from "@/server/routers/hotels/output" * Get the lowest priced room for each room type that appears more than once. */ -export function getLowestPricedDuplicateRooms( +export function filterDuplicateRoomTypesByLowestPrice( roomConfigurations: RoomConfiguration[] ) { const roomTypeCount = roomConfigurations.reduce( From d732138696074892a072c22253d6a434637e0997 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Mon, 11 Nov 2024 14:03:08 +0100 Subject: [PATCH 41/98] feat(SW-572): Added support for logged in and logged out variants of the top link inside the header --- app/globals.css | 2 +- .../Header/HeaderLink/headerLink.module.css | 13 +++- components/Header/HeaderLink/index.tsx | 26 ++++--- .../Header/MainMenu/MobileMenu/index.tsx | 17 ++--- .../MainMenu/MobileMenuWrapper/index.tsx | 13 +++- components/Header/TopLink/index.tsx | 26 +++++++ components/Header/TopMenu/index.tsx | 31 +++----- components/Header/index.tsx | 8 +- components/Icons/get-icon-by-icon-name.ts | 7 +- components/LanguageSwitcher/index.tsx | 3 +- lib/graphql/Query/Header.graphql | 73 +++++++++++++++---- server/routers/contentstack/base/output.ts | 36 ++++++++- server/routers/contentstack/base/utils.ts | 9 ++- types/components/header/headerLink.ts | 10 ++- types/components/header/mobileMenu.ts | 1 + types/components/header/topLink.ts | 7 ++ types/components/icon.ts | 1 + 17 files changed, 215 insertions(+), 68 deletions(-) create mode 100644 components/Header/TopLink/index.tsx create mode 100644 types/components/header/topLink.ts diff --git a/app/globals.css b/app/globals.css index 3a6b68d69..7dd0c44fb 100644 --- a/app/globals.css +++ b/app/globals.css @@ -106,7 +106,7 @@ --max-width-navigation: 89.5rem; --main-menu-mobile-height: 75px; - --main-menu-desktop-height: 129px; + --main-menu-desktop-height: 125px; --booking-widget-mobile-height: 75px; --booking-widget-desktop-height: 77px; --hotel-page-map-desktop-width: 23.75rem; diff --git a/components/Header/HeaderLink/headerLink.module.css b/components/Header/HeaderLink/headerLink.module.css index 198e0c61b..bf642527f 100644 --- a/components/Header/HeaderLink/headerLink.module.css +++ b/components/Header/HeaderLink/headerLink.module.css @@ -2,5 +2,16 @@ display: flex; align-items: center; gap: var(--Spacing-x1); - font-size: var(--typography-Caption-Regular-fontSize); +} + +.headerLink:hover { + color: var(--Base-Text-High-contrast); +} + +.headerLink .icon * { + fill: var(--Base-Text-Medium-contrast); +} + +.headerLink:hover .icon * { + fill: var(--Base-Text-High-contrast); } diff --git a/components/Header/HeaderLink/index.tsx b/components/Header/HeaderLink/index.tsx index 1bfa286ac..78c700733 100644 --- a/components/Header/HeaderLink/index.tsx +++ b/components/Header/HeaderLink/index.tsx @@ -1,4 +1,7 @@ -import Link from "@/components/TempDesignSystem/Link" +import Link from "next/link" + +import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name" +import Caption from "@/components/TempDesignSystem/Text/Caption" import styles from "./headerLink.module.css" @@ -6,16 +9,19 @@ import type { HeaderLinkProps } from "@/types/components/header/headerLink" export default function HeaderLink({ children, - className, - ...props + href, + iconName, + iconSize = 20, }: HeaderLinkProps) { + const Icon = getIconByIconName(iconName) return ( - - {children} - +
    ) } diff --git a/components/Header/MainMenu/MobileMenu/index.tsx b/components/Header/MainMenu/MobileMenu/index.tsx index 272c4b231..621529b0f 100644 --- a/components/Header/MainMenu/MobileMenu/index.tsx +++ b/components/Header/MainMenu/MobileMenu/index.tsx @@ -7,21 +7,23 @@ import { useMediaQuery } from "usehooks-ts" import useDropdownStore from "@/stores/main-menu" -import { GiftIcon, SearchIcon, ServiceIcon } from "@/components/Icons" import LanguageSwitcher from "@/components/LanguageSwitcher" import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" import HeaderLink from "../../HeaderLink" +import TopLink from "../../TopLink" import styles from "./mobileMenu.module.css" import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown" import type { MobileMenuProps } from "@/types/components/header/mobileMenu" +import { IconName } from "@/types/components/icon" export default function MobileMenu({ children, languageUrls, topLink, + isLoggedIn, }: React.PropsWithChildren) { const intl = useIntl() const { @@ -77,18 +79,11 @@ export default function MobileMenu({ > {children}
    - - + {intl.formatMessage({ id: "Find booking" })} - {topLink.link ? ( - - - {topLink.title} - - ) : null} - - + + {intl.formatMessage({ id: "Customer service" })} diff --git a/components/Header/MainMenu/MobileMenuWrapper/index.tsx b/components/Header/MainMenu/MobileMenuWrapper/index.tsx index edd1993ff..43a1c7c79 100644 --- a/components/Header/MainMenu/MobileMenuWrapper/index.tsx +++ b/components/Header/MainMenu/MobileMenuWrapper/index.tsx @@ -1,4 +1,8 @@ -import { getHeader, getLanguageSwitcher } from "@/lib/trpc/memoizedRequests" +import { + getHeader, + getLanguageSwitcher, + getName, +} from "@/lib/trpc/memoizedRequests" import MobileMenu from "../MobileMenu" @@ -8,13 +12,18 @@ export default async function MobileMenuWrapper({ // preloaded const languages = await getLanguageSwitcher() const header = await getHeader() + const user = await getName() if (!languages || !header) { return null } return ( - + {children} ) diff --git a/components/Header/TopLink/index.tsx b/components/Header/TopLink/index.tsx new file mode 100644 index 000000000..fe4594f5c --- /dev/null +++ b/components/Header/TopLink/index.tsx @@ -0,0 +1,26 @@ +import HeaderLink from "../HeaderLink" + +import type { TopLinkProps } from "@/types/components/header/topLink" +import { IconName } from "@/types/components/icon" + +export default function TopLink({ + isLoggedIn, + topLink, + iconSize = 16, +}: TopLinkProps) { + const linkData = isLoggedIn ? topLink.logged_in : topLink.logged_out + + if (!linkData?.link?.url || !linkData?.title) { + return null + } + + return ( + + {linkData.title} + + ) +} diff --git a/components/Header/TopMenu/index.tsx b/components/Header/TopMenu/index.tsx index 325744dca..1fed5161e 100644 --- a/components/Header/TopMenu/index.tsx +++ b/components/Header/TopMenu/index.tsx @@ -1,21 +1,27 @@ -import { getHeader, getLanguageSwitcher } from "@/lib/trpc/memoizedRequests" +import { + getHeader, + getLanguageSwitcher, + getName, +} from "@/lib/trpc/memoizedRequests" -import { GiftIcon, SearchIcon } from "@/components/Icons" import LanguageSwitcher from "@/components/LanguageSwitcher" -import Link from "@/components/TempDesignSystem/Link" import Caption from "@/components/TempDesignSystem/Text/Caption" import { getIntl } from "@/i18n" import HeaderLink from "../HeaderLink" +import TopLink from "../TopLink" import styles from "./topMenu.module.css" +import { IconName } from "@/types/components/icon" + export default async function TopMenu() { // cached const intl = await getIntl() // both preloaded const languages = await getLanguageSwitcher() const header = await getHeader() + const user = await getName() if (!languages || !header) { return null @@ -24,28 +30,15 @@ export default async function TopMenu() { return (
    - {header.data.topLink.link ? ( -
    - ) : null} +
    - diff --git a/components/Header/index.tsx b/components/Header/index.tsx index c04f35607..2d5fde2ad 100644 --- a/components/Header/index.tsx +++ b/components/Header/index.tsx @@ -1,6 +1,10 @@ import { Suspense } from "react" -import { getHeader, getLanguageSwitcher } from "@/lib/trpc/memoizedRequests" +import { + getHeader, + getLanguageSwitcher, + getName, +} from "@/lib/trpc/memoizedRequests" import MainMenu from "./MainMenu" import TopMenu from "./TopMenu" @@ -10,6 +14,8 @@ import styles from "./header.module.css" export default function Header() { void getHeader() void getLanguageSwitcher() + void getName() + return (
    diff --git a/components/Icons/get-icon-by-icon-name.ts b/components/Icons/get-icon-by-icon-name.ts index 08c305f1d..989941db7 100644 --- a/components/Icons/get-icon-by-icon-name.ts +++ b/components/Icons/get-icon-by-icon-name.ts @@ -77,6 +77,7 @@ import { PhoneIcon, PlusCircleIcon, PlusIcon, + PriceTagIcon, RestaurantIcon, RoomServiceIcon, SaunaIcon, @@ -101,7 +102,9 @@ import { import { IconName, IconProps } from "@/types/components/icon" -export function getIconByIconName(icon?: IconName): FC | null { +export function getIconByIconName( + icon: IconName | null = null +): FC | null { switch (icon) { case IconName.Accesories: return AccesoriesIcon @@ -253,6 +256,8 @@ export function getIconByIconName(icon?: IconName): FC | null { return PlusIcon case IconName.PlusCircle: return PlusCircleIcon + case IconName.PriceTag: + return PriceTagIcon case IconName.Restaurant: return RestaurantIcon case IconName.RoomService: diff --git a/components/LanguageSwitcher/index.tsx b/components/LanguageSwitcher/index.tsx index 1ed809af1..f087502c0 100644 --- a/components/LanguageSwitcher/index.tsx +++ b/components/LanguageSwitcher/index.tsx @@ -39,6 +39,7 @@ export default function LanguageSwitcher({ const languageSwitcherRef = useRef(null) const isFooter = type === LanguageSwitcherTypesEnum.Footer const isHeader = !isFooter + const globeIconSize = type === "desktopHeader" ? 16 : 20 const position = isFooter ? "footer" : "header" @@ -87,7 +88,7 @@ export default function LanguageSwitcher({ })} onClick={handleClick} > - +
    diff --git a/lib/graphql/Query/Header.graphql b/lib/graphql/Query/Header.graphql index cd893f135..892a36195 100644 --- a/lib/graphql/Query/Header.graphql +++ b/lib/graphql/Query/Header.graphql @@ -1,11 +1,15 @@ #import "../Fragments/System.graphql" +#import "../Fragments/PageLink/AccountPageLink.graphql" +#import "../Fragments/PageLink/CollectionPageLink.graphql" #import "../Fragments/PageLink/ContentPageLink.graphql" #import "../Fragments/PageLink/HotelPageLink.graphql" #import "../Fragments/PageLink/LoyaltyPageLink.graphql" #import "../Fragments/Blocks/Card.graphql" #import "../Fragments/Blocks/Refs/Card.graphql" +#import "../Fragments/AccountPage/Ref.graphql" +#import "../Fragments/CollectionPage/Ref.graphql" #import "../Fragments/ContentPage/Ref.graphql" #import "../Fragments/HotelPage/Ref.graphql" #import "../Fragments/LoyaltyPage/Ref.graphql" @@ -14,14 +18,35 @@ query GetHeader($locale: String!) { all_header(limit: 1, locale: $locale) { items { top_link { - title - linkConnection { - edges { - node { - __typename - ...ContentPageLink - ...HotelPageLink - ...LoyaltyPageLink + logged_in { + icon + title + linkConnection { + edges { + node { + __typename + ...AccountPageLink + ...CollectionPageLink + ...ContentPageLink + ...HotelPageLink + ...LoyaltyPageLink + } + } + } + } + logged_out { + icon + title + linkConnection { + edges { + node { + __typename + ...AccountPageLink + ...CollectionPageLink + ...ContentPageLink + ...HotelPageLink + ...LoyaltyPageLink + } } } } @@ -84,13 +109,31 @@ query GetHeaderRef($locale: String!) { all_header(limit: 1, locale: $locale) { items { top_link { - linkConnection { - edges { - node { - __typename - ...ContentPageRef - ...HotelPageRef - ...LoyaltyPageRef + logged_in { + linkConnection { + edges { + node { + __typename + ...AccountPageRef + ...CollectionPageRef + ...ContentPageRef + ...HotelPageRef + ...LoyaltyPageRef + } + } + } + } + logged_out { + linkConnection { + edges { + node { + __typename + ...AccountPageRef + ...CollectionPageRef + ...ContentPageRef + ...HotelPageRef + ...LoyaltyPageRef + } } } } diff --git a/server/routers/contentstack/base/output.ts b/server/routers/contentstack/base/output.ts index 6bc0bd9d4..d9f1f1790 100644 --- a/server/routers/contentstack/base/output.ts +++ b/server/routers/contentstack/base/output.ts @@ -14,6 +14,7 @@ import { removeMultipleSlashes } from "@/utils/url" import { systemSchema } from "../schemas/system" +import { IconName } from "@/types/components/icon" import { AlertTypeEnum } from "@/types/enums/alert" import type { Image } from "@/types/image" @@ -514,6 +515,11 @@ const menuItemsRefsSchema = z.intersection( }) ) +const topLinkRefsSchema = z.object({ + logged_in: linkRefsSchema.nullable(), + logged_out: linkRefsSchema.nullable(), +}) + export const headerRefsSchema = z .object({ all_header: z.object({ @@ -522,7 +528,7 @@ export const headerRefsSchema = z z.object({ menu_items: z.array(menuItemsRefsSchema), system: systemSchema, - top_link: linkRefsSchema, + top_link: topLinkRefsSchema, }) ) .max(1), @@ -636,6 +642,32 @@ export const menuItemSchema = z } }) +const topLinkItemSchema = z.intersection( + linkAndTitleSchema, + z.object({ + icon: z + .enum(["loyalty", "info", "offer"]) + .nullable() + .transform((icon) => { + switch (icon) { + case "loyalty": + return IconName.Gift + case "info": + return IconName.InfoCircle + case "offer": + return IconName.PriceTag + default: + return null + } + }), + }) +) + +export const topLinkSchema = z.object({ + logged_in: topLinkItemSchema.nullable(), + logged_out: topLinkItemSchema.nullable(), +}) + export const headerSchema = z .object({ all_header: z.object({ @@ -643,7 +675,7 @@ export const headerSchema = z .array( z.object({ menu_items: z.array(menuItemSchema), - top_link: linkAndTitleSchema, + top_link: topLinkSchema, }) ) .max(1), diff --git a/server/routers/contentstack/base/utils.ts b/server/routers/contentstack/base/utils.ts index b25fc945c..27ec2304f 100644 --- a/server/routers/contentstack/base/utils.ts +++ b/server/routers/contentstack/base/utils.ts @@ -14,8 +14,13 @@ import type { ContactConfig } from "./output" export function getConnections({ header }: HeaderRefs) { const connections: System["system"][] = [header.system] - if (header.top_link?.link) { - connections.push(header.top_link.link) + if (header.top_link) { + if (header.top_link.logged_in?.link) { + connections.push(header.top_link.logged_in.link) + } + if (header.top_link.logged_out?.link) { + connections.push(header.top_link.logged_out.link) + } } if (header.menu_items.length) { diff --git a/types/components/header/headerLink.ts b/types/components/header/headerLink.ts index deb1c71ab..3ee168dea 100644 --- a/types/components/header/headerLink.ts +++ b/types/components/header/headerLink.ts @@ -1,3 +1,9 @@ -import type { LinkProps } from "@/components/TempDesignSystem/Link/link" +import type { LinkProps } from "next/link" -export interface HeaderLinkProps extends React.PropsWithChildren {} +import type { IconName } from "../icon" + +export interface HeaderLinkProps extends React.PropsWithChildren { + href: LinkProps["href"] + iconName: IconName | null + iconSize?: number +} diff --git a/types/components/header/mobileMenu.ts b/types/components/header/mobileMenu.ts index d773b397b..1d0b73513 100644 --- a/types/components/header/mobileMenu.ts +++ b/types/components/header/mobileMenu.ts @@ -4,4 +4,5 @@ import type { Header } from "@/types/trpc/routers/contentstack/header" export interface MobileMenuProps { languageUrls: LanguageSwitcherData topLink: Header["header"]["topLink"] + isLoggedIn: boolean } diff --git a/types/components/header/topLink.ts b/types/components/header/topLink.ts new file mode 100644 index 000000000..5f9e855da --- /dev/null +++ b/types/components/header/topLink.ts @@ -0,0 +1,7 @@ +import type { Header } from "@/types/trpc/routers/contentstack/header" + +export interface TopLinkProps { + isLoggedIn: boolean + topLink: Header["header"]["topLink"] + iconSize?: number +} diff --git a/types/components/icon.ts b/types/components/icon.ts index 17156dee4..3d47bbe9a 100644 --- a/types/components/icon.ts +++ b/types/components/icon.ts @@ -82,6 +82,7 @@ export enum IconName { Phone = "Phone", Plus = "Plus", PlusCircle = "PlusCircle", + PriceTag = "PriceTag", Restaurant = "Restaurant", RoomService = "RoomService", Sauna = "Sauna", From 962760ae1bef5e131e0b9c3a2eca81c3ed26bc56 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Tue, 12 Nov 2024 10:39:42 +0100 Subject: [PATCH 42/98] feat(SW-842): Added lightbox to roomcard --- .../HotelPage/PreviewImages/index.tsx | 68 +++++---- .../HotelPage/Rooms/RoomCard/index.tsx | 38 +++-- .../SelectRate/ImageGallery/index.tsx | 25 +++- components/Lightbox/index.tsx | 133 +++++++----------- i18n/dictionaries/da.json | 4 +- i18n/dictionaries/de.json | 4 +- i18n/dictionaries/en.json | 4 +- i18n/dictionaries/fi.json | 4 +- i18n/dictionaries/no.json | 4 +- i18n/dictionaries/sv.json | 4 +- types/components/lightbox/lightbox.ts | 4 +- 11 files changed, 150 insertions(+), 142 deletions(-) diff --git a/components/ContentType/HotelPage/PreviewImages/index.tsx b/components/ContentType/HotelPage/PreviewImages/index.tsx index 2a0074c5c..307d77a65 100644 --- a/components/ContentType/HotelPage/PreviewImages/index.tsx +++ b/components/ContentType/HotelPage/PreviewImages/index.tsx @@ -1,44 +1,54 @@ +"use client" + +import { useState } from "react" +import { useIntl } from "react-intl" + import Image from "@/components/Image" import Lightbox from "@/components/Lightbox/" import Button from "@/components/TempDesignSystem/Button" -import { getIntl } from "@/i18n" import styles from "./previewImages.module.css" import type { PreviewImagesProps } from "@/types/components/hotelPage/previewImages" -export default async function PreviewImages({ +export default function PreviewImages({ images, hotelName, }: PreviewImagesProps) { - const intl = await getIntl() - const imageGalleryText = intl.formatMessage({ id: "Image gallery" }) - const dialogTitle = `${hotelName} - ${imageGalleryText}` + const intl = useIntl() + const [lightboxIsOpen, setLightboxIsOpen] = useState(false) return ( - -
    - {images.slice(0, 3).map((image, index) => ( - - ))} - -
    -
    +
    + {images.slice(0, 3).map((image, index) => ( + + ))} + + setLightboxIsOpen(false)} + /> +
    ) } diff --git a/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx b/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx index 2b0620885..2acb1c9a3 100644 --- a/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx +++ b/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx @@ -1,9 +1,11 @@ "use client" +import { useState } from "react" import { useIntl } from "react-intl" import { GalleryIcon } from "@/components/Icons" import Image from "@/components/Image" +import Lightbox from "@/components/Lightbox" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" @@ -16,6 +18,7 @@ import type { RoomCardProps } from "@/types/components/hotelPage/room" export function RoomCard({ hotelId, room }: RoomCardProps) { const { images, name, roomSize, occupancy, id } = room const intl = useIntl() + const [lightboxIsOpen, setLightboxIsOpen] = useState(false) const mainImage = images[0] const size = @@ -23,21 +26,12 @@ export function RoomCard({ hotelId, room }: RoomCardProps) { ? `${roomSize.min} m²` : `${roomSize.min} - ${roomSize.max} m²` - const personLabel = intl.formatMessage( - { id: "hotelPages.rooms.roomCard.persons" }, - { totalOccupancy: occupancy.total } - ) - - const subtitle = `${size} (${personLabel})` - - function handleImageClick() { - // TODO: Implement opening of a model with carousel - console.log("Image clicked: ", id) - } - return (
    - + setLightboxIsOpen(false)} + />
    {name} - {subtitle} + + {intl.formatMessage( + { id: "hotelPages.rooms.roomCard.persons" }, + { size, totalOccupancy: occupancy.total } + )} +
    + } return ( - -
    + <> +
    setLightboxIsOpen(true)} + >
    -
    + setLightboxIsOpen(false)} + /> + ) } diff --git a/components/Lightbox/index.tsx b/components/Lightbox/index.tsx index e9e5d8fa1..3b651c907 100644 --- a/components/Lightbox/index.tsx +++ b/components/Lightbox/index.tsx @@ -1,6 +1,6 @@ "use client" import { AnimatePresence, motion } from "framer-motion" -import React, { useState } from "react" +import { useEffect, useState } from "react" import { Dialog, Modal, ModalOverlay } from "react-aria-components" import FullView from "./FullView" @@ -12,24 +12,19 @@ import type { LightboxProps } from "@/types/components/lightbox/lightbox" export default function Lightbox({ images, - children, dialogTitle, + onClose, + isOpen, }: LightboxProps) { - const [isOpen, setIsOpen] = useState(false) const [selectedImageIndex, setSelectedImageIndex] = useState(0) const [isFullView, setIsFullView] = useState(false) - function handleOpenChange(open: boolean) { - if (!open) { - setTimeout(() => { - setIsOpen(false) - setSelectedImageIndex(0) - setIsFullView(false) - }, 300) // 300ms delay - } else { - setIsOpen(true) + useEffect(() => { + if (isOpen) { + setSelectedImageIndex(0) + setIsFullView(false) } - } + }, [isOpen]) function handleNext() { setSelectedImageIndex((prevIndex) => (prevIndex + 1) % images.length) @@ -41,75 +36,53 @@ export default function Lightbox({ ) } - const triggerElement = React.Children.map( - children, - function mapChild(child): React.ReactNode { - if (React.isValidElement(child)) { - if (child.props.id === "lightboxTrigger") { - return React.cloneElement(child, { - onClick: () => setIsOpen(true), - } as React.HTMLAttributes) - } else if (child.props.children) { - return React.cloneElement(child, { - children: React.Children.map(child.props.children, mapChild), - } as React.HTMLAttributes) - } - } - return child - } - ) - return ( - <> - {triggerElement} - - - + + + + {isOpen && ( - - - {isFullView ? ( - setIsFullView(false)} - onNext={handleNext} - onPrev={handlePrev} - currentIndex={selectedImageIndex} - totalImages={images.length} - /> - ) : ( - setIsOpen(false)} - onSelectImage={(image) => { - setSelectedImageIndex( - images.findIndex((img) => img === image) - ) - }} - onImageClick={() => setIsFullView(true)} - selectedImage={images[selectedImageIndex]} - /> - )} - - + + {isFullView ? ( + setIsFullView(false)} + onNext={handleNext} + onPrev={handlePrev} + currentIndex={selectedImageIndex} + totalImages={images.length} + /> + ) : ( + { + setSelectedImageIndex( + images.findIndex((img) => img === image) + ) + }} + onImageClick={() => setIsFullView(true)} + selectedImage={images[selectedImageIndex]} + /> + )} + )} - - - - + + + + ) } diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 6ead0af95..6610d66a5 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -144,6 +144,7 @@ "Highest level": "Højeste niveau", "Hospital": "Hospital", "Hotel": "Hotel", + "Hotel Image gallery": "{hotel} - Billedgalleri", "Hotel facilities": "Hotel faciliteter", "Hotel surroundings": "Hotel omgivelser", "Hotels": "Hoteller", @@ -151,7 +152,6 @@ "How it works": "Hvordan det virker", "Hurry up and use them before they expire!": "Skynd dig og brug dem, før de udløber!", "I would like to get my booking confirmation via sms": "Jeg vil gerne få min booking bekræftelse via SMS", - "Image gallery": "Billedgalleri", "In adults bed": "i de voksnes seng", "In crib": "i tremmeseng", "In extra bed": "i ekstra seng", @@ -409,7 +409,7 @@ "guaranteeing": "garanti", "guest": "gæst", "guests": "gæster", - "hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# person} other {# personer}}", + "hotelPages.rooms.roomCard.persons": "{size} ({totalOccupancy, plural, one {# person} other {# personer}})", "hotelPages.rooms.roomCard.seeRoomDetails": "Se værelsesdetaljer", "km to city center": "km til byens centrum", "lowercase letter": "lille bogstav", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 8a1a463d1..ae5a29025 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -144,6 +144,7 @@ "Highest level": "Höchstes Level", "Hospital": "Krankenhaus", "Hotel": "Hotel", + "Hotel Image gallery": "{hotel} - Bildergalerie", "Hotel facilities": "Hotel-Infos", "Hotel surroundings": "Umgebung des Hotels", "Hotels": "Hotels", @@ -151,7 +152,6 @@ "How it works": "Wie es funktioniert", "Hurry up and use them before they expire!": "Beeilen Sie sich und nutzen Sie sie, bevor sie ablaufen!", "I would like to get my booking confirmation via sms": "Ich möchte meine Buchungsbestätigung per SMS erhalten", - "Image gallery": "Bildergalerie", "In adults bed": "Im Bett der Eltern", "In crib": "im Kinderbett", "In extra bed": "im zusätzlichen Bett", @@ -408,7 +408,7 @@ "guaranteeing": "garantiert", "guest": "gast", "guests": "gäste", - "hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# person} other {# personen}}", + "hotelPages.rooms.roomCard.persons": "{size} ({totalOccupancy, plural, one {# person} other {# personen}})", "hotelPages.rooms.roomCard.seeRoomDetails": "Zimmerdetails ansehen", "km to city center": "km bis zum Stadtzentrum", "lowercase letter": "Kleinbuchstabe", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 0f28e3231..7ca27c217 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -156,6 +156,7 @@ "Highest level": "Highest level", "Hospital": "Hospital", "Hotel": "Hotel", + "Hotel Image gallery": "{hotel} - Image gallery", "Hotel facilities": "Hotel facilities", "Hotel surroundings": "Hotel surroundings", "Hotels": "Hotels", @@ -163,7 +164,6 @@ "How it works": "How it works", "Hurry up and use them before they expire!": "Hurry up and use them before they expire!", "I would like to get my booking confirmation via sms": "I would like to get my booking confirmation via sms", - "Image gallery": "Image gallery", "In adults bed": "In adults bed", "In crib": "In crib", "In extra bed": "In extra bed", @@ -446,7 +446,7 @@ "guest": "guest", "guest.paid": "{amount} {currency} has been paid", "guests": "guests", - "hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# person} other {# persons}}", + "hotelPages.rooms.roomCard.persons": "{size} ({totalOccupancy, plural, one {# person} other {# persons}})", "hotelPages.rooms.roomCard.seeRoomDetails": "See room details", "km to city center": "km to city center", "lowercase letter": "lowercase letter", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 24325460c..544f52e8d 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -144,6 +144,7 @@ "Highest level": "Korkein taso", "Hospital": "Sairaala", "Hotel": "Hotelli", + "Hotel Image gallery": "{hotel} - Kuvagalleria", "Hotel facilities": "Hotellin palvelut", "Hotel surroundings": "Hotellin ympäristö", "Hotels": "Hotellit", @@ -151,7 +152,6 @@ "How it works": "Kuinka se toimii", "Hurry up and use them before they expire!": "Ole nopea ja käytä ne ennen kuin ne vanhenevat!", "I would like to get my booking confirmation via sms": "Haluan saada varauksen vahvistuksen SMS-viestillä", - "Image gallery": "Kuvagalleria", "In adults bed": "Aikuisten vuoteessa", "In crib": "Pinnasängyssä", "In extra bed": "Oma vuodepaikka", @@ -408,7 +408,7 @@ "guaranteeing": "varmistetaan", "guest": "Vieras", "guests": "Vieraita", - "hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# henkilö} other {# Henkilöä}}", + "hotelPages.rooms.roomCard.persons": "{size} ({totalOccupancy, plural, one {# henkilö} other {# Henkilöä}})", "hotelPages.rooms.roomCard.seeRoomDetails": "Katso huoneen tiedot", "km to city center": "km keskustaan", "lowercase letter": "pien kirjain", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 5a7dfe511..810664462 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -143,13 +143,13 @@ "Highest level": "Høyeste nivå", "Hospital": "Sykehus", "Hotel": "Hotel", + "Hotel Image gallery": "{hotel} - Bildegalleri", "Hotel facilities": "Hotelfaciliteter", "Hotel surroundings": "Hotellomgivelser", "Hotels": "Hoteller", "How do you want to sleep?": "Hvordan vil du sove?", "How it works": "Hvordan det fungerer", "Hurry up and use them before they expire!": "Skynd deg og bruk dem før de utløper!", - "Image gallery": "Bildegalleri", "In adults bed": "i voksnes seng", "In crib": "i sprinkelseng", "In extra bed": "i ekstraseng", @@ -406,7 +406,7 @@ "guaranteeing": "garantiert", "guest": "gjest", "guests": "gjester", - "hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# person} other {# personer}}", + "hotelPages.rooms.roomCard.persons": "{size} ({totalOccupancy, plural, one {# person} other {# personer}})", "hotelPages.rooms.roomCard.seeRoomDetails": "Se detaljer om rommet", "km to city center": "km til sentrum", "lowercase letter": "liten bokstav", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 9aeced109..fc796030e 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -143,13 +143,13 @@ "Highest level": "Högsta nivå", "Hospital": "Sjukhus", "Hotel": "Hotell", + "Hotel Image gallery": "{hotel} - Bildgalleri", "Hotel facilities": "Hotellfaciliteter", "Hotel surroundings": "Hotellomgivning", "Hotels": "Hotell", "How do you want to sleep?": "Hur vill du sova?", "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!", - "Image gallery": "Bildgalleri", "In adults bed": "I vuxens säng", "In crib": "I spjälsäng", "In extra bed": "Egen sängplats", @@ -407,7 +407,7 @@ "guaranteeing": "garanterar", "guest": "gäst", "guests": "gäster", - "hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# person} other {# personer}}", + "hotelPages.rooms.roomCard.persons": "{size} ({totalOccupancy, plural, one {# person} other {# personer}})", "hotelPages.rooms.roomCard.seeRoomDetails": "Se information om rummet", "km to city center": "km till stadens centrum", "lowercase letter": "liten bokstav", diff --git a/types/components/lightbox/lightbox.ts b/types/components/lightbox/lightbox.ts index af592bca6..3c0b7db16 100644 --- a/types/components/lightbox/lightbox.ts +++ b/types/components/lightbox/lightbox.ts @@ -3,12 +3,12 @@ import type { GalleryImage } from "@/types/hotel" export interface LightboxProps { images: GalleryImage[] dialogTitle: string /* Accessible title for dialog screen readers */ - children: React.ReactNode + onClose: () => void + isOpen: boolean } export interface GalleryProps { images: GalleryImage[] - dialogTitle: string onClose: () => void onSelectImage: (image: GalleryImage) => void onImageClick: () => void From 692320bd6191dd2889309f36f6f4f680a62868cd Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Tue, 12 Nov 2024 11:29:51 +0100 Subject: [PATCH 43/98] feat(SW-842): Some refactoring regarding room card --- .../HotelPage/PreviewImages/index.tsx | 4 +- .../HotelPage/Rooms/RoomCard/index.tsx | 74 ++++++++----------- .../Rooms/RoomCard/roomCard.module.css | 58 +-------------- .../Rooms/RoomDetailsButton/index.tsx | 34 --------- .../HotelReservation/HotelCard/index.tsx | 10 ++- .../SelectRate/HotelInfoCard/index.tsx | 3 +- .../RoomSelection/RoomCard/index.tsx | 8 +- .../ImageGallery/imageGallery.module.css | 10 ++- .../SelectRate => }/ImageGallery/index.tsx | 15 +++- components/SidePeeks/RoomSidePeek/index.tsx | 4 +- .../RoomSidePeek/roomSidePeek.module.css | 1 - i18n/dictionaries/da.json | 2 +- i18n/dictionaries/de.json | 2 +- i18n/dictionaries/en.json | 2 +- i18n/dictionaries/fi.json | 2 +- i18n/dictionaries/no.json | 2 +- i18n/dictionaries/sv.json | 2 +- types/components/imageGallery.ts | 9 +++ 18 files changed, 87 insertions(+), 155 deletions(-) delete mode 100644 components/ContentType/HotelPage/Rooms/RoomDetailsButton/index.tsx rename components/{HotelReservation/SelectRate => }/ImageGallery/imageGallery.module.css (87%) rename components/{HotelReservation/SelectRate => }/ImageGallery/index.tsx (74%) create mode 100644 types/components/imageGallery.ts diff --git a/components/ContentType/HotelPage/PreviewImages/index.tsx b/components/ContentType/HotelPage/PreviewImages/index.tsx index 307d77a65..e3b4117de 100644 --- a/components/ContentType/HotelPage/PreviewImages/index.tsx +++ b/components/ContentType/HotelPage/PreviewImages/index.tsx @@ -43,8 +43,8 @@ export default function PreviewImages({ setLightboxIsOpen(false)} diff --git a/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx b/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx index 2acb1c9a3..b6784513a 100644 --- a/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx +++ b/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx @@ -1,25 +1,24 @@ "use client" -import { useState } from "react" import { useIntl } from "react-intl" -import { GalleryIcon } from "@/components/Icons" -import Image from "@/components/Image" -import Lightbox from "@/components/Lightbox" +import useSidePeekStore from "@/stores/sidepeek" + +import { ChevronRightSmallIcon } from "@/components/Icons" +import ImageGallery from "@/components/ImageGallery" +import Button from "@/components/TempDesignSystem/Button" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import RoomDetailsButton from "../RoomDetailsButton" - import styles from "./roomCard.module.css" import type { RoomCardProps } from "@/types/components/hotelPage/room" +import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek" export function RoomCard({ hotelId, room }: RoomCardProps) { - const { images, name, roomSize, occupancy, id } = room + const { images, name, roomSize, occupancy } = room const intl = useIntl() - const [lightboxIsOpen, setLightboxIsOpen] = useState(false) - const mainImage = images[0] + const openSidePeek = useSidePeekStore((state) => state.openSidePeek) const size = roomSize?.min === roomSize?.max @@ -28,39 +27,13 @@ export function RoomCard({ hotelId, room }: RoomCardProps) { return (
    - - setLightboxIsOpen(false)} - /> +
    {name} @@ -79,10 +51,22 @@ export function RoomCard({ hotelId, room }: RoomCardProps) { )}
    - +
    ) diff --git a/components/ContentType/HotelPage/Rooms/RoomCard/roomCard.module.css b/components/ContentType/HotelPage/Rooms/RoomCard/roomCard.module.css index 5270e9882..6e43c8936 100644 --- a/components/ContentType/HotelPage/Rooms/RoomCard/roomCard.module.css +++ b/components/ContentType/HotelPage/Rooms/RoomCard/roomCard.module.css @@ -3,34 +3,7 @@ background-color: var(--UI-Opacity-White-100); border: 1px solid var(--Base-Border-Subtle); display: grid; -} - -/*TODO: Build Chip/Badge component. */ -.badge { - position: absolute; - top: var(--Spacing-x1); - left: var(--Spacing-x1); - background-color: var(--Tertiary-Dark-Surface-Hover); - padding: var(--Spacing-x-half) var(--Spacing-x1); - border-radius: var(--Corner-radius-Medium); - color: var(--Tertiary-Dark-On-Surface-Text); - text-transform: uppercase; - font-size: var(--typography-Chip-fontSize-Placeholder); - font-weight: 400; -} - -.imageCount { - position: absolute; - right: var(--Spacing-x1); - bottom: var(--Spacing-x1); - display: flex; - gap: var(--Spacing-x-half); - align-items: center; - background-color: var(--UI-Grey-90); - opacity: 90%; - color: var(--UI-Input-Controls-Fill-Normal); - padding: var(--Spacing-x-half) var(--Spacing-x1); - border-radius: var(--Corner-radius-Small); + overflow: hidden; } .content { @@ -46,32 +19,7 @@ gap: var(--Spacing-x1); } -.title { - display: flex; - align-items: center; -} - -.title:first-child { - height: 2em; -} - -.imageWrapper { +.imageContainer { position: relative; - background-color: transparent; - border-width: 0; - cursor: pointer; - margin: 0; - padding: 0; - display: flex; -} - -.image { - width: 100%; - object-fit: cover; - border-top-left-radius: var(--Corner-radius-Medium); - border-top-right-radius: var(--Corner-radius-Medium); -} - -.subtitle { - color: var(--UI-Text-Placeholder); + height: 200px; } diff --git a/components/ContentType/HotelPage/Rooms/RoomDetailsButton/index.tsx b/components/ContentType/HotelPage/Rooms/RoomDetailsButton/index.tsx deleted file mode 100644 index d68ba8b4a..000000000 --- a/components/ContentType/HotelPage/Rooms/RoomDetailsButton/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -"use client" - -import { useIntl } from "react-intl" - -import useSidePeekStore from "@/stores/sidepeek" - -import { ChevronRightSmallIcon } from "@/components/Icons" -import Button from "@/components/TempDesignSystem/Button" - -import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek" -import { ToggleSidePeekProps } from "@/types/components/hotelReservation/toggleSidePeekProps" - -export default function RoomDetailsButton({ - hotelId, - roomTypeCode, -}: ToggleSidePeekProps) { - const intl = useIntl() - const openSidePeek = useSidePeekStore((state) => state.openSidePeek) - - return ( - - ) -} diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 86a483536..17be8a31a 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -8,6 +8,7 @@ import { selectHotelMap } from "@/constants/routes/hotelReservation" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import { PriceTagIcon, ScandicLogoIcon } from "@/components/Icons" import TripAdvisorIcon from "@/components/Icons/TripAdvisor" +import ImageGallery from "@/components/ImageGallery" import Button from "@/components/TempDesignSystem/Button" import Chip from "@/components/TempDesignSystem/Chip" import Link from "@/components/TempDesignSystem/Link" @@ -16,7 +17,6 @@ import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Title from "@/components/TempDesignSystem/Text/Title" import ReadMore from "../ReadMore" -import ImageGallery from "../SelectRate/ImageGallery" import { hotelCardVariants } from "./variants" import styles from "./hotelCard.module.css" @@ -62,7 +62,13 @@ export default function HotelCard({ onMouseLeave={handleMouseLeave} >
    - + {hotelData.gallery && ( + + )}
    diff --git a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx index 3811ba513..bef792555 100644 --- a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx +++ b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx @@ -3,6 +3,7 @@ import { useIntl } from "react-intl" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import TripAdvisorIcon from "@/components/Icons/TripAdvisor" +import ImageGallery from "@/components/ImageGallery" import Alert from "@/components/TempDesignSystem/Alert" import Divider from "@/components/TempDesignSystem/Divider" import Body from "@/components/TempDesignSystem/Text/Body" @@ -10,7 +11,6 @@ import Caption from "@/components/TempDesignSystem/Text/Caption" import Title from "@/components/TempDesignSystem/Text/Title" import ReadMore from "../../ReadMore" -import ImageGallery from "../ImageGallery" import styles from "./hotelInfoCard.module.css" @@ -32,6 +32,7 @@ export default function HotelInfoCard({ hotelData }: HotelInfoCardProps) { {hotelAttributes.ratings?.tripAdvisor && (
    diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index fc4681412..c9a5e2143 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -7,11 +7,11 @@ import { RateDefinition } from "@/server/routers/hotels/output" import ToggleSidePeek from "@/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek" import FlexibilityOption from "@/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption" +import ImageGallery from "@/components/ImageGallery" import Caption from "@/components/TempDesignSystem/Text/Caption" import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import ImageGallery from "../../ImageGallery" import { getIconForFeatureCode } from "../../utils" import styles from "./roomCard.module.css" @@ -164,7 +164,11 @@ export default function RoomCard({
    {/*NOTE: images from the test API are hosted on test3.scandichotels.com, which can't be accessed unless on Scandic's Wifi or using Citrix. */} - +
    )} diff --git a/components/HotelReservation/SelectRate/ImageGallery/imageGallery.module.css b/components/ImageGallery/imageGallery.module.css similarity index 87% rename from components/HotelReservation/SelectRate/ImageGallery/imageGallery.module.css rename to components/ImageGallery/imageGallery.module.css index c346f4d04..2ffdcf595 100644 --- a/components/HotelReservation/SelectRate/ImageGallery/imageGallery.module.css +++ b/components/ImageGallery/imageGallery.module.css @@ -1,4 +1,4 @@ -.galleryIcon { +.imageCount { position: absolute; bottom: 16px; right: 16px; @@ -13,7 +13,15 @@ } .triggerArea { + display: flex; cursor: pointer; + width: 100%; + height: 100%; +} + +.image { + width: 100%; + object-fit: cover; } .imagePlaceholder { diff --git a/components/HotelReservation/SelectRate/ImageGallery/index.tsx b/components/ImageGallery/index.tsx similarity index 74% rename from components/HotelReservation/SelectRate/ImageGallery/index.tsx rename to components/ImageGallery/index.tsx index 4f1be93cf..f5cfee34d 100644 --- a/components/HotelReservation/SelectRate/ImageGallery/index.tsx +++ b/components/ImageGallery/index.tsx @@ -9,10 +9,16 @@ import Footnote from "@/components/TempDesignSystem/Text/Footnote" import styles from "./imageGallery.module.css" -import type { ImageGalleryProps } from "@/types/components/hotelReservation/selectRate/imageGallery" +import type { ImageGalleryProps } from "@/types/components/imageGallery" -export default function ImageGallery({ images, title }: ImageGalleryProps) { +export default function ImageGallery({ + images, + title, + fill, + height = 280, +}: ImageGalleryProps) { const [lightboxIsOpen, setLightboxIsOpen] = useState(false) + const imageProps = fill ? { fill } : { height, width: height * 1.5 } if (!images || images.length === 0) { return
    @@ -26,11 +32,12 @@ export default function ImageGallery({ images, title }: ImageGalleryProps) { onClick={() => setLightboxIsOpen(true)} > -
    +
    {images.length} diff --git a/components/SidePeeks/RoomSidePeek/index.tsx b/components/SidePeeks/RoomSidePeek/index.tsx index 2453968e8..34fc3afca 100644 --- a/components/SidePeeks/RoomSidePeek/index.tsx +++ b/components/SidePeeks/RoomSidePeek/index.tsx @@ -1,11 +1,11 @@ import { useIntl } from "react-intl" +import ImageGallery from "@/components/ImageGallery" import Button from "@/components/TempDesignSystem/Button" import SidePeek from "@/components/TempDesignSystem/SidePeek" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import ImageGallery from "../../HotelReservation/SelectRate/ImageGallery" import { getFacilityIcon } from "./facilityIcon" import styles from "./roomSidePeek.module.css" @@ -44,7 +44,7 @@ export default function RoomSidePeek({ )}
    - +
    {roomDescription}
    diff --git a/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css b/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css index 8b96ef1b8..c76c57877 100644 --- a/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css +++ b/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css @@ -22,7 +22,6 @@ } .imageContainer { - min-height: 280px; position: relative; border-radius: var(--Corner-radius-Medium); overflow: hidden; diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 6610d66a5..a4455f55f 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -144,7 +144,6 @@ "Highest level": "Højeste niveau", "Hospital": "Hospital", "Hotel": "Hotel", - "Hotel Image gallery": "{hotel} - Billedgalleri", "Hotel facilities": "Hotel faciliteter", "Hotel surroundings": "Hotel omgivelser", "Hotels": "Hoteller", @@ -152,6 +151,7 @@ "How it works": "Hvordan det virker", "Hurry up and use them before they expire!": "Skynd dig og brug dem, før de udløber!", "I would like to get my booking confirmation via sms": "Jeg vil gerne få min booking bekræftelse via SMS", + "Image gallery": "{name} - Billedgalleri", "In adults bed": "i de voksnes seng", "In crib": "i tremmeseng", "In extra bed": "i ekstra seng", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index ae5a29025..92c8cf452 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -144,7 +144,6 @@ "Highest level": "Höchstes Level", "Hospital": "Krankenhaus", "Hotel": "Hotel", - "Hotel Image gallery": "{hotel} - Bildergalerie", "Hotel facilities": "Hotel-Infos", "Hotel surroundings": "Umgebung des Hotels", "Hotels": "Hotels", @@ -152,6 +151,7 @@ "How it works": "Wie es funktioniert", "Hurry up and use them before they expire!": "Beeilen Sie sich und nutzen Sie sie, bevor sie ablaufen!", "I would like to get my booking confirmation via sms": "Ich möchte meine Buchungsbestätigung per SMS erhalten", + "Image gallery": "{name} - Bildergalerie", "In adults bed": "Im Bett der Eltern", "In crib": "im Kinderbett", "In extra bed": "im zusätzlichen Bett", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 7ca27c217..dccd25229 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -156,7 +156,6 @@ "Highest level": "Highest level", "Hospital": "Hospital", "Hotel": "Hotel", - "Hotel Image gallery": "{hotel} - Image gallery", "Hotel facilities": "Hotel facilities", "Hotel surroundings": "Hotel surroundings", "Hotels": "Hotels", @@ -164,6 +163,7 @@ "How it works": "How it works", "Hurry up and use them before they expire!": "Hurry up and use them before they expire!", "I would like to get my booking confirmation via sms": "I would like to get my booking confirmation via sms", + "Image gallery": "{name} - Image gallery", "In adults bed": "In adults bed", "In crib": "In crib", "In extra bed": "In extra bed", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 544f52e8d..23a384600 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -144,7 +144,6 @@ "Highest level": "Korkein taso", "Hospital": "Sairaala", "Hotel": "Hotelli", - "Hotel Image gallery": "{hotel} - Kuvagalleria", "Hotel facilities": "Hotellin palvelut", "Hotel surroundings": "Hotellin ympäristö", "Hotels": "Hotellit", @@ -152,6 +151,7 @@ "How it works": "Kuinka se toimii", "Hurry up and use them before they expire!": "Ole nopea ja käytä ne ennen kuin ne vanhenevat!", "I would like to get my booking confirmation via sms": "Haluan saada varauksen vahvistuksen SMS-viestillä", + "Image gallery": "{name} - Kuvagalleria", "In adults bed": "Aikuisten vuoteessa", "In crib": "Pinnasängyssä", "In extra bed": "Oma vuodepaikka", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 810664462..7aea7d309 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -143,13 +143,13 @@ "Highest level": "Høyeste nivå", "Hospital": "Sykehus", "Hotel": "Hotel", - "Hotel Image gallery": "{hotel} - Bildegalleri", "Hotel facilities": "Hotelfaciliteter", "Hotel surroundings": "Hotellomgivelser", "Hotels": "Hoteller", "How do you want to sleep?": "Hvordan vil du sove?", "How it works": "Hvordan det fungerer", "Hurry up and use them before they expire!": "Skynd deg og bruk dem før de utløper!", + "Image gallery": "{name} - Bildegalleri", "In adults bed": "i voksnes seng", "In crib": "i sprinkelseng", "In extra bed": "i ekstraseng", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index fc796030e..4d763a690 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -143,13 +143,13 @@ "Highest level": "Högsta nivå", "Hospital": "Sjukhus", "Hotel": "Hotell", - "Hotel Image gallery": "{hotel} - Bildgalleri", "Hotel facilities": "Hotellfaciliteter", "Hotel surroundings": "Hotellomgivning", "Hotels": "Hotell", "How do you want to sleep?": "Hur vill du sova?", "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!", + "Image gallery": "{name} - Bildgalleri", "In adults bed": "I vuxens säng", "In crib": "I spjälsäng", "In extra bed": "Egen sängplats", diff --git a/types/components/imageGallery.ts b/types/components/imageGallery.ts new file mode 100644 index 000000000..019fd8032 --- /dev/null +++ b/types/components/imageGallery.ts @@ -0,0 +1,9 @@ +import type { GalleryImage } from "@/types/hotel" + +export type ImageGalleryProps = { + images?: GalleryImage[] + title: string + fill?: boolean + width?: number + height?: number +} From cc7f4e04789e4c5c2c77aa2f94f0495979c40c26 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Thu, 14 Nov 2024 07:51:06 +0100 Subject: [PATCH 44/98] fix(SW-842): fixed close button positioning --- .../HotelReservation/HotelCard/index.tsx | 12 ++++---- components/ImageGallery/index.tsx | 3 ++ components/Lightbox/Gallery.tsx | 28 ++++++------------- components/Lightbox/Lightbox.module.css | 19 +++++++++---- i18n/dictionaries/da.json | 1 + i18n/dictionaries/de.json | 1 + i18n/dictionaries/en.json | 1 + i18n/dictionaries/fi.json | 1 + i18n/dictionaries/no.json | 1 + i18n/dictionaries/sv.json | 1 + 10 files changed, 37 insertions(+), 31 deletions(-) diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 17be8a31a..289a93320 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -62,13 +62,11 @@ export default function HotelCard({ onMouseLeave={handleMouseLeave} >
    - {hotelData.gallery && ( - - )} +
    diff --git a/components/ImageGallery/index.tsx b/components/ImageGallery/index.tsx index f5cfee34d..eaeee684b 100644 --- a/components/ImageGallery/index.tsx +++ b/components/ImageGallery/index.tsx @@ -1,6 +1,7 @@ "use client" import { useState } from "react" +import { useIntl } from "react-intl" import { GalleryIcon } from "@/components/Icons" import Image from "@/components/Image" @@ -17,6 +18,7 @@ export default function ImageGallery({ fill, height = 280, }: ImageGalleryProps) { + const intl = useIntl() const [lightboxIsOpen, setLightboxIsOpen] = useState(false) const imageProps = fill ? { fill } : { height, width: height * 1.5 } @@ -30,6 +32,7 @@ export default function ImageGallery({ className={styles.triggerArea} role="button" onClick={() => setLightboxIsOpen(true)} + aria-label={intl.formatMessage({ id: "Open image gallery" })} > img === mainImage) @@ -46,16 +48,17 @@ export default function Gallery({ {/* Desktop Gallery */}
    @@ -129,19 +132,6 @@ export default function Gallery({ {/* Mobile Gallery */}
    -
    {images.map((image, index) => ( diff --git a/components/Lightbox/Lightbox.module.css b/components/Lightbox/Lightbox.module.css index 231e652b7..4c62a90dc 100644 --- a/components/Lightbox/Lightbox.module.css +++ b/components/Lightbox/Lightbox.module.css @@ -16,10 +16,13 @@ gap: var(--Spacing-x2); } -.mobileGalleryCloseButton { +.closeButton { justify-content: flex-start; width: fit-content; } +.closeButton .desktopCloseIcon { + display: none; +} .mobileGalleryContent { display: block; @@ -84,7 +87,6 @@ } .desktopGallery, -.desktopGalleryCloseButton, .desktopThumbnailGrid, .navigationButton { display: none; @@ -247,15 +249,22 @@ overflow: hidden; } - .desktopGalleryCloseButton { + .closeButton { display: block; position: absolute; top: var(--Spacing-x-one-and-half); - right: var(--Spacing-x-half); + right: var(--Spacing-x1); z-index: 1; } - .desktopGalleryCloseButton:hover .desktopGalleryCloseIcon { + .closeButton .mobileCloseIcon { + display: none; + } + .closeButton .desktopCloseIcon { + display: block; + } + + .closeButton:hover .desktopCloseIcon { background-color: var(--Base-Surface-Primary-light-Hover-alt); border-radius: 50%; } diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index a4455f55f..5d32423eb 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -227,6 +227,7 @@ "On your journey": "På din rejse", "Open": "Åben", "Open gift(s)": "Åbne {amount, plural, one {gave} other {gaver}}", + "Open image gallery": "Åbn billedgalleri", "Open language menu": "Åbn sprogmenuen", "Open menu": "Åbn menuen", "Open my pages menu": "Åbn mine sider menuen", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 92c8cf452..90c115061 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -225,6 +225,7 @@ "On your journey": "Auf deiner Reise", "Open": "Offen", "Open gift(s)": "{amount, plural, one {Geschenk} other {Geschenke}} öffnen", + "Open image gallery": "Bildergalerie öffnen", "Open language menu": "Sprachmenü öffnen", "Open menu": "Menü öffnen", "Open my pages menu": "Meine Seiten Menü öffnen", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index dccd25229..44f400079 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -244,6 +244,7 @@ "On your journey": "On your journey", "Open": "Open", "Open gift(s)": "Open {amount, plural, one {gift} other {gifts}}", + "Open image gallery": "Open image gallery", "Open language menu": "Open language menu", "Open menu": "Open menu", "Open my pages menu": "Open my pages menu", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 23a384600..fb538db4b 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -227,6 +227,7 @@ "On your journey": "Matkallasi", "Open": "Avata", "Open gift(s)": "{amount, plural, one {Avoin lahja} other {Avoimet lahjat}}", + "Open image gallery": "Avaa kuvagalleria", "Open language menu": "Avaa kielivalikko", "Open menu": "Avaa valikko", "Open my pages menu": "Avaa omat sivut -valikko", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 7aea7d309..98dadb098 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -225,6 +225,7 @@ "On your journey": "På reisen din", "Open": "Åpen", "Open gift(s)": "{amount, plural, one {Åpen gave} other {Åpnen gaver}}", + "Open image gallery": "Åpne bildegalleri", "Open language menu": "Åpne språkmenyen", "Open menu": "Åpne menyen", "Open my pages menu": "Åpne mine sider menyen", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 4d763a690..074c500e6 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -225,6 +225,7 @@ "On your journey": "På din resa", "Open": "Öppna", "Open gift(s)": "Öppna {amount, plural, one {gåva} other {gåvor}}", + "Open image gallery": "Öppna bildgalleri", "Open language menu": "Öppna språkmenyn", "Open menu": "Öppna menyn", "Open my pages menu": "Öppna mina sidor menyn", From 87a89c5d817acc65f3e97e3e1fc1764df771f7ed Mon Sep 17 00:00:00 2001 From: Bianca Widstam Date: Thu, 14 Nov 2024 09:14:26 +0000 Subject: [PATCH 45/98] feat/SW-843-UI-hotel-card-select-hotel (pull request #887) Approved-by: Pontus Dreij Approved-by: Niclas Edenvin --- .../HotelCard/HotelLogo/index.tsx | 34 ++++ .../HotelPriceList/HotelPriceCard/index.tsx | 71 +++++++ .../HotelPriceList/hotelPriceList.module.css | 29 +++ .../HotelCard/HotelPriceList/index.tsx | 42 ++++ .../HotelCard/hotelCard.module.css | 125 ++++++------ .../HotelReservation/HotelCard/index.tsx | 190 +++++++++--------- .../hotelCardListing.module.css | 3 +- .../HotelInfoCard/hotelInfoCard.module.css | 12 -- .../SelectRate/HotelInfoCard/index.tsx | 11 +- .../TripAdvisorChip/index.tsx | 15 ++ .../tripAdvisorChip.module.css | 11 + components/Icons/Logos/DowntownCamper.tsx | 113 +++++++++++ components/Icons/Logos/GrandHotelOslo.tsx | 27 +++ components/Icons/Logos/Haymarket.tsx | 63 ++++++ components/Icons/Logos/HotelNorge.tsx | 96 +++++++++ components/Icons/Logos/Marski.tsx | 59 ++++++ components/Icons/Logos/ScandicGoLogo.tsx | 69 +++++++ components/Icons/{ => Logos}/ScandicLogo.tsx | 2 +- components/Icons/index.tsx | 8 +- .../Text/Caption/caption.module.css | 4 + .../TempDesignSystem/Text/Caption/variants.ts | 1 + i18n/dictionaries/da.json | 5 +- i18n/dictionaries/de.json | 3 +- i18n/dictionaries/en.json | 3 +- i18n/dictionaries/fi.json | 3 +- i18n/dictionaries/no.json | 3 +- i18n/dictionaries/sv.json | 3 +- .../selectHotel/hotePriceListProps.ts | 5 + .../selectHotel/hotelLogoProps.ts | 6 + .../selectHotel/priceCardProps.ts | 5 + .../hotelReservation/tripAdvisorProps.ts | 3 + types/enums/signatureHotel.ts | 7 + 32 files changed, 848 insertions(+), 183 deletions(-) create mode 100644 components/HotelReservation/HotelCard/HotelLogo/index.tsx create mode 100644 components/HotelReservation/HotelCard/HotelPriceList/HotelPriceCard/index.tsx create mode 100644 components/HotelReservation/HotelCard/HotelPriceList/hotelPriceList.module.css create mode 100644 components/HotelReservation/HotelCard/HotelPriceList/index.tsx create mode 100644 components/HotelReservation/TripAdvisorChip/index.tsx create mode 100644 components/HotelReservation/TripAdvisorChip/tripAdvisorChip.module.css create mode 100644 components/Icons/Logos/DowntownCamper.tsx create mode 100644 components/Icons/Logos/GrandHotelOslo.tsx create mode 100644 components/Icons/Logos/Haymarket.tsx create mode 100644 components/Icons/Logos/HotelNorge.tsx create mode 100644 components/Icons/Logos/Marski.tsx create mode 100644 components/Icons/Logos/ScandicGoLogo.tsx rename components/Icons/{ => Logos}/ScandicLogo.tsx (98%) create mode 100644 types/components/hotelReservation/selectHotel/hotePriceListProps.ts create mode 100644 types/components/hotelReservation/selectHotel/hotelLogoProps.ts create mode 100644 types/components/hotelReservation/selectHotel/priceCardProps.ts create mode 100644 types/components/hotelReservation/tripAdvisorProps.ts create mode 100644 types/enums/signatureHotel.ts diff --git a/components/HotelReservation/HotelCard/HotelLogo/index.tsx b/components/HotelReservation/HotelCard/HotelLogo/index.tsx new file mode 100644 index 000000000..c305e1838 --- /dev/null +++ b/components/HotelReservation/HotelCard/HotelLogo/index.tsx @@ -0,0 +1,34 @@ +import { + DowntownCamperIcon, + GrandHotelOsloLogoIcon, + HaymarketIcon, + HotelNorgeIcon, + MarskiLogoIcon, + ScandicGoLogoIcon, + ScandicLogoIcon, +} from "@/components/Icons" + +import type { HotelLogoProps } from "@/types/components/hotelReservation/selectHotel/hotelLogoProps" +import { HotelTypeEnum } from "@/types/enums/hotelType" +import { SignatureHotelEnum } from "@/types/enums/signatureHotel" + +export default function HotelLogo({ hotelId, hotelType }: HotelLogoProps) { + if (hotelType === HotelTypeEnum.ScandicGo) { + return + } + + switch (hotelId) { + case SignatureHotelEnum.Haymarket: + return + case SignatureHotelEnum.HotelNorge: + return + case SignatureHotelEnum.DowntownCamper: + return + case SignatureHotelEnum.GrandHotelOslo: + return + case SignatureHotelEnum.Marski: + return + default: + return + } +} diff --git a/components/HotelReservation/HotelCard/HotelPriceList/HotelPriceCard/index.tsx b/components/HotelReservation/HotelCard/HotelPriceList/HotelPriceCard/index.tsx new file mode 100644 index 000000000..942f9fafe --- /dev/null +++ b/components/HotelReservation/HotelCard/HotelPriceList/HotelPriceCard/index.tsx @@ -0,0 +1,71 @@ +import { useIntl } from "react-intl" + +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" + +import styles from "../hotelPriceList.module.css" + +import type { PriceCardProps } from "@/types/components/hotelReservation/selectHotel/priceCardProps" + +export default function HotelPriceCard({ + currency, + memberAmount, + regularAmount, +}: PriceCardProps) { + const intl = useIntl() + + return ( +
    + {memberAmount && ( +
    +
    +
    + + + )} +
    +
    +
    + +
    +
    + + {memberAmount ? memberAmount : regularAmount} + + + {currency} + + /{intl.formatMessage({ id: "night" })} + + +
    +
    + + {/* TODO add correct local price when API change */} +
    +
    +
    + +
    +
    + + + + ) +} diff --git a/components/HotelReservation/HotelCard/HotelPriceList/hotelPriceList.module.css b/components/HotelReservation/HotelCard/HotelPriceList/hotelPriceList.module.css new file mode 100644 index 000000000..fe28eef93 --- /dev/null +++ b/components/HotelReservation/HotelCard/HotelPriceList/hotelPriceList.module.css @@ -0,0 +1,29 @@ +.priceCard { + padding: var(--Spacing-x-one-and-half); + background-color: var(--Base-Surface-Secondary-light-Normal); + border-radius: var(--Corner-radius-Medium); + margin: 0; + width: 100%; +} + +.noRooms { + display: flex; + gap: var(--Spacing-x1); +} + +.priceRow { + display: flex; + justify-content: space-between; + align-items: baseline; + padding: var(--Spacing-x-quarter) 0; +} + +.price { + display: flex; + gap: var(--Spacing-x-half); +} + +.perNight { + font-weight: 400; + font-size: var(--typography-Caption-Regular-fontSize); +} diff --git a/components/HotelReservation/HotelCard/HotelPriceList/index.tsx b/components/HotelReservation/HotelCard/HotelPriceList/index.tsx new file mode 100644 index 000000000..4167e044a --- /dev/null +++ b/components/HotelReservation/HotelCard/HotelPriceList/index.tsx @@ -0,0 +1,42 @@ +import { useIntl } from "react-intl" + +import { ErrorCircleIcon } from "@/components/Icons" +import Body from "@/components/TempDesignSystem/Text/Body" + +import HotelPriceCard from "./HotelPriceCard" + +import styles from "./hotelPriceList.module.css" + +import { HotelPriceListProps } from "@/types/components/hotelReservation/selectHotel/hotePriceListProps" + +export default function HotelPriceList({ price }: HotelPriceListProps) { + const intl = useIntl() + + return ( + <> + {price ? ( + <> + + + + ) : ( +
    +
    + + + {intl.formatMessage({ + id: "There are no rooms available that match your request", + })} + +
    +
    + )} + + ) +} diff --git a/components/HotelReservation/HotelCard/hotelCard.module.css b/components/HotelReservation/HotelCard/hotelCard.module.css index a5d90de17..a0c15eb5b 100644 --- a/components/HotelReservation/HotelCard/hotelCard.module.css +++ b/components/HotelReservation/HotelCard/hotelCard.module.css @@ -1,15 +1,15 @@ .card { - display: grid; - grid-template-areas: - "image header" - "hotel hotel" - "prices prices"; - gap: var(--Spacing-x2); - padding: var(--Spacing-x2); + display: flex; + flex-direction: column; background-color: var(--Base-Surface-Primary-light-Normal); border: 1px solid var(--Base-Border-Subtle); border-radius: var(--Corner-radius-Medium); width: 100%; + overflow: hidden; +} + +.card.active { + border: 1px solid var(--Base-Border-Hover); } .card.active { @@ -17,14 +17,9 @@ } .imageContainer { - grid-area: image; position: relative; - height: 100%; - width: 116px; -} - -.tripAdvisor { - display: none; + height: 200px; + width: 100%; } .imageContainer img { @@ -32,19 +27,41 @@ } .hotelInformation { - grid-area: header; + margin-bottom: var(--Spacing-x-half); } -.hotel { +.hotelContent { display: flex; flex-direction: column; - grid-area: hotel; + padding: var(--Spacing-x2); } +.hotelDescription { + display: none; +} + +.titleContainer { + display: flex; + flex-direction: column; + gap: var(--Spacing-x-half); + margin-top: var(--Spacing-x-half); +} + +.addressContainer { + display: flex; + flex-wrap: wrap; + gap: var(--Spacing-x1); +} + +.address { + display: none; + font-style: normal; +} .facilities { display: flex; flex-wrap: wrap; gap: var(--Spacing-x1); + margin-top: var(--Spacing-x-one-and-half); } .facilitiesItem { @@ -56,78 +73,70 @@ .prices { display: flex; flex-direction: column; - gap: var(--Spacing-x2); - grid-area: prices; + gap: var(--Spacing-x-one-and-half); + width: 100%; } -.public, -.member { - max-width: fit-content; - margin-bottom: var(--Spacing-x-half); +.detailsButton { + border-bottom: none; } .button { - justify-content: center; -} - -.address { - display: none; + min-width: 160px; } @media screen and (min-width: 1367px) { .card.pageListing { - grid-template-areas: - "image header" - "image hotel" - "image prices"; + flex-direction: row; overflow: hidden; padding: 0; } - .pageListing .imageContainer { - position: relative; - min-height: 200px; - width: 518px; + .pageListing .hotelDescription { + display: block; } - .pageListing .tripAdvisor { - position: absolute; - display: block; - left: 7px; - top: 7px; + .pageListing .imageContainer { + position: relative; + height: 100%; + width: 314px; } .pageListing .hotelInformation { - padding-top: var(--Spacing-x2); + width: min(422px, 100%); padding-right: var(--Spacing-x2); + margin: 0; } - .pageListing .hotel { + .pageListing .facilities { + margin: var(--Spacing-x1) 0; + } + + .pageListing .hotelContent { + flex-direction: row; + align-items: center; gap: var(--Spacing-x2); - padding-right: var(--Spacing-x2); + padding-left: var(--Spacing-x3); + } + + .pageListing .titleContainer { + margin-bottom: var(--Spacing-x-one-and-half); } .pageListing .prices { - flex-direction: row; align-items: center; - justify-content: space-between; - padding-right: var(--Spacing-x2); - padding-bottom: var(--Spacing-x2); - } - - .pageListing .detailsButton { - border-bottom: none; + width: 260px; } .pageListing .button { - width: 160px; + width: 100%; } - .address { - display: block; - } - - .addressMobile { + .pageListing .addressMobile { display: none; } + + .pageListing .address { + display: inline; + } } diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 289a93320..920dc3108 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -6,17 +6,18 @@ import { Lang } from "@/constants/languages" import { selectHotelMap } from "@/constants/routes/hotelReservation" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" -import { PriceTagIcon, ScandicLogoIcon } from "@/components/Icons" -import TripAdvisorIcon from "@/components/Icons/TripAdvisor" import ImageGallery from "@/components/ImageGallery" import Button from "@/components/TempDesignSystem/Button" -import Chip from "@/components/TempDesignSystem/Chip" +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 Footnote from "@/components/TempDesignSystem/Text/Footnote" -import Title from "@/components/TempDesignSystem/Text/Title" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import ReadMore from "../ReadMore" +import TripAdvisorChip from "../TripAdvisorChip" +import HotelLogo from "./HotelLogo" +import HotelPriceList from "./HotelPriceList" import { hotelCardVariants } from "./variants" import styles from "./hotelCard.module.css" @@ -61,100 +62,97 @@ export default function HotelCard({ onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} > -
    - -
    - - - {hotelData.ratings?.tripAdvisor.rating} - +
    +
    + + {hotelData.ratings?.tripAdvisor && ( + + )}
    -
    -
    - - - {hotelData.name} - - - {`${hotelData.address.streetAddress}, ${hotelData.address.city}`} - - - - {`${hotelData.address.streetAddress}, ${hotelData.address.city}`} - - - - {`${hotelData.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`} - -
    -
    -
    - {amenities.map((facility) => { - const IconComponent = mapFacilityToIcon(facility.id) - return ( -
    - {IconComponent && } -
    + +
    +
    +
    + + + {hotelData.name} + +
    +
    +
    + + + + +
    +
    - ) - })} - - - -
    -
    - - - {intl.formatMessage({ id: "Public price from" })} - -
    - approx 280 eur - -
    - - - {intl.formatMessage({ id: "Member price from" })} - -
    - approx 280 eur - - + + ) + })} + + + +
    + + - + {/* TODO: Localize link and also use correct search params */} + + {intl.formatMessage({ id: "See rooms" })} + + +
    + ) } diff --git a/components/HotelReservation/HotelCardListing/hotelCardListing.module.css b/components/HotelReservation/HotelCardListing/hotelCardListing.module.css index be62321df..00bd7ec84 100644 --- a/components/HotelReservation/HotelCardListing/hotelCardListing.module.css +++ b/components/HotelReservation/HotelCardListing/hotelCardListing.module.css @@ -1,5 +1,6 @@ .hotelCards { display: flex; flex-direction: column; - gap: var(--Spacing-x4); + gap: var(--Spacing-x2); + margin-bottom: var(--Spacing-x2); } diff --git a/components/HotelReservation/SelectRate/HotelInfoCard/hotelInfoCard.module.css b/components/HotelReservation/SelectRate/HotelInfoCard/hotelInfoCard.module.css index e3e5642db..a6ef23b5e 100644 --- a/components/HotelReservation/SelectRate/HotelInfoCard/hotelInfoCard.module.css +++ b/components/HotelReservation/SelectRate/HotelInfoCard/hotelInfoCard.module.css @@ -24,18 +24,6 @@ border-radius: var(--Corner-radius-Medium); } -.tripAdvisor { - display: flex; - align-items: center; - gap: var(--Spacing-x-half); - background-color: var(--Base-Surface-Primary-light-Normal); - position: absolute; - left: 8px; - top: 8px; - padding: var(--Spacing-x-quarter) var(--Spacing-x1); - border-radius: var(--Corner-radius-Small); -} - .hotelContent { display: flex; flex-direction: column; diff --git a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx index bef792555..28ce32377 100644 --- a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx +++ b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx @@ -2,7 +2,6 @@ import { useIntl } from "react-intl" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" -import TripAdvisorIcon from "@/components/Icons/TripAdvisor" import ImageGallery from "@/components/ImageGallery" import Alert from "@/components/TempDesignSystem/Alert" import Divider from "@/components/TempDesignSystem/Divider" @@ -11,6 +10,7 @@ import Caption from "@/components/TempDesignSystem/Text/Caption" import Title from "@/components/TempDesignSystem/Text/Title" import ReadMore from "../../ReadMore" +import TripAdvisorChip from "../../TripAdvisorChip" import styles from "./hotelInfoCard.module.css" @@ -35,12 +35,9 @@ export default function HotelInfoCard({ hotelData }: HotelInfoCardProps) { fill /> {hotelAttributes.ratings?.tripAdvisor && ( -
    - -
    - + )}
    diff --git a/components/HotelReservation/TripAdvisorChip/index.tsx b/components/HotelReservation/TripAdvisorChip/index.tsx new file mode 100644 index 000000000..96005bd60 --- /dev/null +++ b/components/HotelReservation/TripAdvisorChip/index.tsx @@ -0,0 +1,15 @@ +import TripAdvisorIcon from "@/components/Icons/TripAdvisor" +import Caption from "@/components/TempDesignSystem/Text/Caption" + +import styles from "./tripAdvisorChip.module.css" + +import type { TripAdvisorProps } from "@/types/components/hotelReservation/tripAdvisorProps" + +export default function TripAdvisorChip({ rating }: TripAdvisorProps) { + return ( +
    + +
    + + ) +} diff --git a/components/HotelReservation/TripAdvisorChip/tripAdvisorChip.module.css b/components/HotelReservation/TripAdvisorChip/tripAdvisorChip.module.css new file mode 100644 index 000000000..a5328e1a3 --- /dev/null +++ b/components/HotelReservation/TripAdvisorChip/tripAdvisorChip.module.css @@ -0,0 +1,11 @@ +.tripAdvisor { + display: flex; + align-items: center; + gap: var(--Spacing-x-half); + background-color: var(--Base-Surface-Primary-light-Normal); + position: absolute; + left: 16px; + top: 16px; + padding: var(--Spacing-x-quarter) var(--Spacing-x1); + border-radius: var(--Corner-radius-Small); +} diff --git a/components/Icons/Logos/DowntownCamper.tsx b/components/Icons/Logos/DowntownCamper.tsx new file mode 100644 index 000000000..97baf9820 --- /dev/null +++ b/components/Icons/Logos/DowntownCamper.tsx @@ -0,0 +1,113 @@ +import { iconVariants } from "../variants" + +import type { IconProps } from "@/types/components/icon" + +export default function DowntownCamperIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + + + + + + + + + + ) +} diff --git a/components/Icons/Logos/GrandHotelOslo.tsx b/components/Icons/Logos/GrandHotelOslo.tsx new file mode 100644 index 000000000..1cac092f7 --- /dev/null +++ b/components/Icons/Logos/GrandHotelOslo.tsx @@ -0,0 +1,27 @@ +import { iconVariants } from "../variants" + +import type { IconProps } from "@/types/components/icon" + +export default function GrandHotelOsloLogoIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + ) +} diff --git a/components/Icons/Logos/Haymarket.tsx b/components/Icons/Logos/Haymarket.tsx new file mode 100644 index 000000000..00d8519fb --- /dev/null +++ b/components/Icons/Logos/Haymarket.tsx @@ -0,0 +1,63 @@ +import { iconVariants } from "../variants" + +import type { IconProps } from "@/types/components/icon" + +export default function HaymarketIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + + + + + ) +} diff --git a/components/Icons/Logos/HotelNorge.tsx b/components/Icons/Logos/HotelNorge.tsx new file mode 100644 index 000000000..daa5ff940 --- /dev/null +++ b/components/Icons/Logos/HotelNorge.tsx @@ -0,0 +1,96 @@ +import { iconVariants } from "../variants" + +import type { IconProps } from "@/types/components/icon" + +export default function HotelNorgeIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + + + + + + + + ) +} diff --git a/components/Icons/Logos/Marski.tsx b/components/Icons/Logos/Marski.tsx new file mode 100644 index 000000000..57473a53a --- /dev/null +++ b/components/Icons/Logos/Marski.tsx @@ -0,0 +1,59 @@ +import { iconVariants } from "../variants" + +import type { IconProps } from "@/types/components/icon" + +export default function MarskiLogoIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Logos/ScandicGoLogo.tsx b/components/Icons/Logos/ScandicGoLogo.tsx new file mode 100644 index 000000000..8878cf16c --- /dev/null +++ b/components/Icons/Logos/ScandicGoLogo.tsx @@ -0,0 +1,69 @@ +import { iconVariants } from "../variants" + +import type { IconProps } from "@/types/components/icon" + +export default function ScandicGoLogoIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + + + ) +} diff --git a/components/Icons/ScandicLogo.tsx b/components/Icons/Logos/ScandicLogo.tsx similarity index 98% rename from components/Icons/ScandicLogo.tsx rename to components/Icons/Logos/ScandicLogo.tsx index d3710f666..cdb992753 100644 --- a/components/Icons/ScandicLogo.tsx +++ b/components/Icons/Logos/ScandicLogo.tsx @@ -1,4 +1,4 @@ -import { iconVariants } from "./variants" +import { iconVariants } from "../variants" import type { IconProps } from "@/types/components/icon" diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index 2a30ffe78..fc6e5c069 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -82,6 +82,13 @@ export { default as LaundryMachineIcon } from "./LaundryMachine" export { default as LocalBarIcon } from "./LocalBar" export { default as LocationIcon } from "./Location" export { default as LockIcon } from "./Lock" +export { default as DowntownCamperIcon } from "./Logos/DowntownCamper" +export { default as GrandHotelOsloLogoIcon } from "./Logos/GrandHotelOslo" +export { default as HaymarketIcon } from "./Logos/Haymarket" +export { default as HotelNorgeIcon } from "./Logos/HotelNorge" +export { default as MarskiLogoIcon } from "./Logos/Marski" +export { default as ScandicGoLogoIcon } from "./Logos/ScandicGoLogo" +export { default as ScandicLogoIcon } from "./Logos/ScandicLogo" export { default as MapIcon } from "./Map" export { default as MinusIcon } from "./Minus" export { default as MirrorIcon } from "./Mirror" @@ -104,7 +111,6 @@ export { default as RestaurantIcon } from "./Restaurant" export { default as RoomServiceIcon } from "./RoomService" export { default as SafetyBoxIcon } from "./SafetyBox" export { default as SaunaIcon } from "./Sauna" -export { default as ScandicLogoIcon } from "./ScandicLogo" export { default as SearchIcon } from "./Search" export { default as ServiceIcon } from "./Service" export { default as ShoppingIcon } from "./Shopping" diff --git a/components/TempDesignSystem/Text/Caption/caption.module.css b/components/TempDesignSystem/Text/Caption/caption.module.css index 3eb44199a..7d2cbe752 100644 --- a/components/TempDesignSystem/Text/Caption/caption.module.css +++ b/components/TempDesignSystem/Text/Caption/caption.module.css @@ -71,6 +71,10 @@ p.caption { color: var(--Base-Text-High-contrast); } +.baseTextMediumContrast { + color: var(--Base-Text-Medium-contrast); +} + .red { color: var(--Scandic-Brand-Scandic-Red); } diff --git a/components/TempDesignSystem/Text/Caption/variants.ts b/components/TempDesignSystem/Text/Caption/variants.ts index b0672430b..96cfab491 100644 --- a/components/TempDesignSystem/Text/Caption/variants.ts +++ b/components/TempDesignSystem/Text/Caption/variants.ts @@ -24,6 +24,7 @@ const config = { uiTextPlaceholder: styles.uiTextPlaceholder, disabled: styles.disabled, baseTextHighContrast: styles.baseTextHighContrast, + baseTextMediumContrast: styles.baseTextMediumContrast, }, textTransform: { uppercase: styles.uppercase, diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 5d32423eb..4486b9e51 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -7,7 +7,7 @@ "ACCE": "Tilgængelighed", "ALLG": "Allergi", "About meetings & conferences": "About meetings & conferences", - "About the hotel": "About the hotel", + "About the hotel": "Om hotellet", "Accessibility": "Tilgængelighed", "Accessible Room": "Tilgængelighedsrum", "Activities": "Aktiviteter", @@ -98,7 +98,7 @@ "Disabled booking options text": "Koder, checks og bonusnætter er endnu ikke tilgængelige på den nye hjemmeside.", "Discard changes": "Kassér ændringer", "Discard unsaved changes?": "Slette ændringer, der ikke er gemt?", - "Distance to city centre": "{number}km til centrum", + "Distance to city centre": "{number} km til centrum", "Distance to hotel": "Afstand til hotel", "Do you want to start the day with Scandics famous breakfast buffé?": "Vil du starte dagen med Scandics berømte morgenbuffet?", "Done": "Færdig", @@ -330,6 +330,7 @@ "Terms and conditions": "Vilkår og betingelser", "Thank you": "Tak", "Theatre": "Teater", + "There are no rooms available that match your request": "Der er ingen ledige værelser, der matcher din anmodning", "There are no transactions to display": "Der er ingen transaktioner at vise", "Things nearby HOTEL_NAME": "Ting i nærheden af {hotelName}", "To get the member price {amount} {currency}, log in or join when completing the booking.": "For at få medlemsprisen {amount} {currency}, log ind eller tilmeld dig, når du udfylder bookingen.", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 90c115061..ee34c0c1b 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -98,7 +98,7 @@ "Disabled booking options text": "Codes, Schecks und Bonusnächte sind auf der neuen Website noch nicht verfügbar.", "Discard changes": "Änderungen verwerfen", "Discard unsaved changes?": "Nicht gespeicherte Änderungen verwerfen?", - "Distance to city centre": "{number}km zum Stadtzentrum", + "Distance to city centre": "{number} km zum Stadtzentrum", "Distance to hotel": "Entfernung zum Hotel", "Do you want to start the day with Scandics famous breakfast buffé?": "Möchten Sie den Tag mit Scandics berühmtem Frühstücksbuffet beginnen?", "Done": "Fertig", @@ -329,6 +329,7 @@ "Terms and conditions": "Geschäftsbedingungen", "Thank you": "Danke", "Theatre": "Theater", + "There are no rooms available that match your request": "Es sind keine Zimmer verfügbar, die Ihrer Anfrage entsprechen", "There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden", "Things nearby HOTEL_NAME": "Dinge in der Nähe von {hotelName}", "To get the member price {amount} {currency}, log in or join when completing the booking.": "Um den Mitgliederpreis von {amount} {currency} zu erhalten, loggen Sie sich ein oder treten Sie Scandic Friends bei, wenn Sie die Buchung abschließen.", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 44f400079..6d3712067 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -106,7 +106,7 @@ "Disabled booking options text": "Codes, cheques and reward nights aren't available on the new website yet.", "Discard changes": "Discard changes", "Discard unsaved changes?": "Discard unsaved changes?", - "Distance to city centre": "{number}km to city centre", + "Distance to city centre": "{number} km to city centre", "Distance to hotel": "Distance to hotel", "Do you want to start the day with Scandics famous breakfast buffé?": "Do you want to start the day with Scandics famous breakfast buffé?", "Done": "Done", @@ -359,6 +359,7 @@ "Terms and conditions": "Terms and conditions", "Thank you": "Thank you", "Theatre": "Theatre", + "There are no rooms available that match your request": "There are no rooms available that match your request", "There are no transactions to display": "There are no transactions to display", "Things nearby HOTEL_NAME": "Things nearby {hotelName}", "To get the member price {amount} {currency}, log in or join when completing the booking.": "To get the member price {amount} {currency}, log in or join when completing the booking.", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index fb538db4b..a067b87f5 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -98,7 +98,7 @@ "Disabled booking options text": "Koodit, sekit ja palkintoillat eivät ole vielä saatavilla uudella verkkosivustolla.", "Discard changes": "Hylkää muutokset", "Discard unsaved changes?": "Hylkäätkö tallentamattomat muutokset?", - "Distance to city centre": "{number}km Etäisyys kaupunkiin", + "Distance to city centre": "{number} km Etäisyys kaupunkiin", "Distance to hotel": "Etäisyys hotelliin", "Do you want to start the day with Scandics famous breakfast buffé?": "Haluatko aloittaa päiväsi Scandicsin kuuluisalla aamiaisbuffella?", "Done": "Valmis", @@ -331,6 +331,7 @@ "Terms and conditions": "Käyttöehdot", "Thank you": "Kiitos", "Theatre": "Teatteri", + "There are no rooms available that match your request": "Pyyntöäsi vastaavia huoneita ei ole saatavilla", "There are no transactions to display": "Näytettäviä tapahtumia ei ole", "Things nearby HOTEL_NAME": "Lähellä olevia asioita {hotelName}", "To get the member price {amount} {currency}, log in or join when completing the booking.": "Jäsenhintaan saavat sisäänkirjautuneet tai liittyneet jäsenet.", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 98dadb098..98d778f4c 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -97,7 +97,7 @@ "Disabled booking options text": "Koder, checks og belønningsnætter er enda ikke tilgjengelige på den nye nettsiden.", "Discard changes": "Forkaste endringer", "Discard unsaved changes?": "Forkaste endringer som ikke er lagret?", - "Distance to city centre": "{number}km til sentrum", + "Distance to city centre": "{number} km til sentrum", "Distance to hotel": "Avstand til hotell", "Do you want to start the day with Scandics famous breakfast buffé?": "Vil du starte dagen med Scandics berømte frokostbuffé?", "Done": "Ferdig", @@ -328,6 +328,7 @@ "Terms and conditions": "Vilkår og betingelser", "Thank you": "Takk", "Theatre": "Teater", + "There are no rooms available that match your request": "Det er ingen tilgjengelige rom som samsvarer med forespørselen din", "There are no transactions to display": "Det er ingen transaksjoner å vise", "Things nearby HOTEL_NAME": "Ting i nærheten av {hotelName}", "To get the member price {amount} {currency}, log in or join when completing the booking.": "For å få medlemsprisen {amount} {currency}, logg inn eller bli med når du fullfører bestillingen.", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 074c500e6..01c293426 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -97,7 +97,7 @@ "Disabled booking options text": "Koder, bonuscheckar och belöningsnätter är inte tillgängliga på den nya webbplatsen än.", "Discard changes": "Ignorera ändringar", "Discard unsaved changes?": "Vill du ignorera ändringar som inte har sparats?", - "Distance to city centre": "{number}km till centrum", + "Distance to city centre": "{number} km till centrum", "Distance to hotel": "Avstånd till hotell", "Do you want to start the day with Scandics famous breakfast buffé?": "Vill du starta dagen med Scandics berömda frukostbuffé?", "Done": "Klar", @@ -328,6 +328,7 @@ "Terms and conditions": "Allmänna villkor", "Thank you": "Tack", "Theatre": "Teater", + "There are no rooms available that match your request": "Det finns inga tillgängliga rum som matchar din förfrågan", "There are no transactions to display": "Det finns inga transaktioner att visa", "Things nearby HOTEL_NAME": "Saker i närheten av {hotelName}", "To get the member price {amount} {currency}, log in or join when completing the booking.": "För att få medlemsprisen {amount} {currency}, logga in eller bli medlem när du slutför bokningen.", diff --git a/types/components/hotelReservation/selectHotel/hotePriceListProps.ts b/types/components/hotelReservation/selectHotel/hotePriceListProps.ts new file mode 100644 index 000000000..2464fad43 --- /dev/null +++ b/types/components/hotelReservation/selectHotel/hotePriceListProps.ts @@ -0,0 +1,5 @@ +import type { HotelsAvailabilityPrices } from "@/server/routers/hotels/output" + +export type HotelPriceListProps = { + price: HotelsAvailabilityPrices +} diff --git a/types/components/hotelReservation/selectHotel/hotelLogoProps.ts b/types/components/hotelReservation/selectHotel/hotelLogoProps.ts new file mode 100644 index 000000000..8f19490a6 --- /dev/null +++ b/types/components/hotelReservation/selectHotel/hotelLogoProps.ts @@ -0,0 +1,6 @@ +import { Hotel } from "@/types/hotel" + +export type HotelLogoProps = { + hotelId: Hotel["operaId"] + hotelType: Hotel["hotelType"] +} diff --git a/types/components/hotelReservation/selectHotel/priceCardProps.ts b/types/components/hotelReservation/selectHotel/priceCardProps.ts new file mode 100644 index 000000000..d339b4a06 --- /dev/null +++ b/types/components/hotelReservation/selectHotel/priceCardProps.ts @@ -0,0 +1,5 @@ +export type PriceCardProps = { + currency: string + memberAmount?: string | undefined + regularAmount?: string | undefined +} diff --git a/types/components/hotelReservation/tripAdvisorProps.ts b/types/components/hotelReservation/tripAdvisorProps.ts new file mode 100644 index 000000000..62636cdcc --- /dev/null +++ b/types/components/hotelReservation/tripAdvisorProps.ts @@ -0,0 +1,3 @@ +export type TripAdvisorProps = { + rating: number +} diff --git a/types/enums/signatureHotel.ts b/types/enums/signatureHotel.ts new file mode 100644 index 000000000..1f5b68d6a --- /dev/null +++ b/types/enums/signatureHotel.ts @@ -0,0 +1,7 @@ +export enum SignatureHotelEnum { + DowntownCamper = "879", + GrandHotelOslo = "340", + Haymarket = "890", + HotelNorge = "785", + Marski = "605", +} From 3698d6a5bb1b2871d3a634b4a1b87673680fe6b0 Mon Sep 17 00:00:00 2001 From: Bianca Widstam Date: Thu, 14 Nov 2024 09:26:54 +0000 Subject: [PATCH 46/98] fix/update-UI-hotel-card (pull request #894) fix: update new name for hotel sidepeek on hotel page Approved-by: Niclas Edenvin --- components/HotelReservation/HotelCard/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 920dc3108..e93a24ef3 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -127,7 +127,7 @@ export default function HotelCard({ })} Date: Thu, 14 Nov 2024 10:31:11 +0000 Subject: [PATCH 47/98] fix/SW-674-rate-cards-vertically-aligned (pull request #858) Fix/SW-674 rate cards vertically aligned * fix(SW-674): fix cards aligned * fix(SW-674): fix columns for smaller ipad/tablet * fix(SW-674): fix height on flexibility options * fix(SW-674): fix comments * fix(SW-674): remove margin * fix(SW-674): auto fill with columns Approved-by: Simon.Emanuelsson Approved-by: Pontus Dreij Approved-by: Niclas Edenvin --- .../RoomSelection/RoomCard/index.tsx | 154 ++++++++++-------- .../RoomCard/roomCard.module.css | 12 +- .../RoomSelection/roomSelection.module.css | 12 +- 3 files changed, 89 insertions(+), 89 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index c9a5e2143..70f1c5aa6 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -1,6 +1,6 @@ "use client" -import { createElement } from "react" +import { createElement, useCallback } from "react" import { useIntl } from "react-intl" import { RateDefinition } from "@/server/routers/hotels/output" @@ -69,9 +69,61 @@ export default function RoomCard({ const { roomSize, occupancy, descriptions, images } = selectedRoom || {} const mainImage = images?.[0] + const freeCancelation = intl.formatMessage({ id: "Free cancellation" }) + const nonRefundable = intl.formatMessage({ id: "Non-refundable" }) + const freeBooking = intl.formatMessage({ id: "Free rebooking" }) + const payLater = intl.formatMessage({ id: "Pay later" }) + const payNow = intl.formatMessage({ id: "Pay now" }) + + const rateKey = useCallback( + (key: string) => { + switch (key) { + case "flexRate": + return freeCancelation + case "saveRate": + return nonRefundable + default: + return freeBooking + } + }, + [freeCancelation, freeBooking, nonRefundable] + ) + return (
    -
    +
    + {mainImage && ( +
    +
    + {roomConfiguration.roomsLeft < 5 && ( + + {`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`} + + )} + {roomConfiguration.features + .filter((feature) => selectedPackages.includes(feature.code)) + .map((feature) => ( + + {createElement(getIconForFeatureCode(feature.code), { + width: 16, + height: 16, + color: "burgundy", + })} + + ))} +
    + {/*NOTE: images from the test API are hosted on test3.scandichotels.com, + which can't be accessed unless on Scandic's Wifi or using Citrix. */} + +
    + )}
    - - -
    - {Object.entries(rates).map(([key, rate]) => ( - - ))} -
    - {mainImage && ( -
    -
    - {roomConfiguration.roomsLeft < 5 && ( - - {`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`} - - )} - {roomConfiguration.features - .filter((feature) => selectedPackages.includes(feature.code)) - .map((feature) => ( - - {createElement(getIconForFeatureCode(feature.code), { - width: 16, - height: 16, - color: "burgundy", - })} - - ))} -
    - {/*NOTE: images from the test API are hosted on test3.scandichotels.com, - which can't be accessed unless on Scandic's Wifi or using Citrix. */} - +
    +
    +
    + {Object.entries(rates).map(([key, rate]) => ( + + ))}
    - )} + ) } diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css index 799726d1b..1d7ac0a0e 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css @@ -1,16 +1,13 @@ .card { font-size: 14px; display: flex; - flex-direction: column-reverse; + flex-direction: column; background-color: #fff; border-radius: var(--Corner-radius-Large); border: 1px solid var(--Base-Border-Subtle); position: relative; -} - -.cardBody { - display: flex; - flex-direction: column; + height: 100%; + justify-content: space-between; } .specification { @@ -41,7 +38,8 @@ .roomDetails { display: flex; flex-direction: column; - gap: var(--Spacing-x2); + gap: var(--Spacing-x1); + padding: var(--Spacing-x1) var(--Spacing-x2) var(--Spacing-x2); } .name { diff --git a/components/HotelReservation/SelectRate/RoomSelection/roomSelection.module.css b/components/HotelReservation/SelectRate/RoomSelection/roomSelection.module.css index 1dab63afb..3da3c9fb2 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/roomSelection.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/roomSelection.module.css @@ -19,14 +19,6 @@ width: 0; } -@media (min-width: 767px) { - .roomList { - grid-template-columns: repeat(3, minmax(240px, 1fr)); - } -} - -@media (min-width: 1367px) { - .roomList { - grid-template-columns: repeat(4, 1fr); - } +.roomList { + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); } From 58678244fc6e047df558140f2b14fe3d12471690 Mon Sep 17 00:00:00 2001 From: Arvid Norlin Date: Tue, 12 Nov 2024 13:34:42 +0100 Subject: [PATCH 48/98] fix: refactor GuestRoomsPicker to avoid performance bugs --- components/GuestsRoomsPicker/Dialog.tsx | 130 +++++++++++++++++ .../GuestsRoomsPicker/GuestsRoomsPicker.tsx | 136 ------------------ .../guests-rooms-picker.module.css | 55 ++----- components/GuestsRoomsPicker/index.tsx | 58 ++------ .../bookingWidget/guestsRoomsPicker.ts | 4 - 5 files changed, 158 insertions(+), 225 deletions(-) create mode 100644 components/GuestsRoomsPicker/Dialog.tsx delete mode 100644 components/GuestsRoomsPicker/GuestsRoomsPicker.tsx diff --git a/components/GuestsRoomsPicker/Dialog.tsx b/components/GuestsRoomsPicker/Dialog.tsx new file mode 100644 index 000000000..1c5d8e53d --- /dev/null +++ b/components/GuestsRoomsPicker/Dialog.tsx @@ -0,0 +1,130 @@ +"use client" +import { Dialog } from "react-aria-components" +import { useFormContext } from "react-hook-form" +import { useIntl } from "react-intl" + +import { useGuestsRoomsStore } from "@/stores/guests-rooms" + +import { CloseLargeIcon, PlusCircleIcon, PlusIcon } from "../Icons" +import Button from "../TempDesignSystem/Button" +import Divider from "../TempDesignSystem/Divider" +import Subtitle from "../TempDesignSystem/Text/Subtitle" +import { Tooltip } from "../TempDesignSystem/Tooltip" +import AdultSelector from "./AdultSelector" +import ChildSelector from "./ChildSelector" + +import styles from "./guests-rooms-picker.module.css" + +import { BookingWidgetSchema } from "@/types/components/bookingWidget" + +export default function GuestsRoomsPickerDialog() { + const intl = useIntl() + const doneLabel = intl.formatMessage({ id: "Done" }) + const roomLabel = intl.formatMessage({ id: "Room" }) + const disabledBookingOptionsHeader = intl.formatMessage({ + id: "Disabled booking options header", + }) + const disabledBookingOptionsText = intl.formatMessage({ + id: "Disabled adding room", + }) + const addRoomLabel = intl.formatMessage({ id: "Add Room" }) + + const { getFieldState } = useFormContext() + + const rooms = useGuestsRoomsStore((state) => state.rooms) + + return ( + + {({ close }) => { + return ( + <> +
    + +
    +
    + {rooms.map((room, index) => ( +
    +
    + + {roomLabel} {index + 1} + + + +
    + +
    + ))} +
    + + {rooms.length < 4 ? ( + + ) : null} + +
    +
    +
    +
    + + {rooms.length < 4 ? ( + + ) : null} + +
    + + +
    + + ) + }} +
    + ) +} diff --git a/components/GuestsRoomsPicker/GuestsRoomsPicker.tsx b/components/GuestsRoomsPicker/GuestsRoomsPicker.tsx deleted file mode 100644 index 8950a1585..000000000 --- a/components/GuestsRoomsPicker/GuestsRoomsPicker.tsx +++ /dev/null @@ -1,136 +0,0 @@ -"use client" -import { useFormContext } from "react-hook-form" -import { useIntl } from "react-intl" - -import { useGuestsRoomsStore } from "@/stores/guests-rooms" - -import { CloseLargeIcon, PlusCircleIcon, PlusIcon } from "../Icons" -import Button from "../TempDesignSystem/Button" -import Divider from "../TempDesignSystem/Divider" -import Subtitle from "../TempDesignSystem/Text/Subtitle" -import { Tooltip } from "../TempDesignSystem/Tooltip" -import AdultSelector from "./AdultSelector" -import ChildSelector from "./ChildSelector" - -import styles from "./guests-rooms-picker.module.css" - -import { BookingWidgetSchema } from "@/types/components/bookingWidget" -import { GuestsRoomsPickerProps } from "@/types/components/bookingWidget/guestsRoomsPicker" - -export default function GuestsRoomsPicker({ - closePicker, -}: GuestsRoomsPickerProps) { - const intl = useIntl() - const doneLabel = intl.formatMessage({ id: "Done" }) - const roomLabel = intl.formatMessage({ id: "Room" }) - const disabledBookingOptionsHeader = intl.formatMessage({ - id: "Disabled booking options header", - }) - const disabledBookingOptionsText = intl.formatMessage({ - id: "Disabled adding room", - }) - const addRoomLabel = intl.formatMessage({ id: "Add Room" }) - - const { getFieldState } = useFormContext() - - const rooms = useGuestsRoomsStore((state) => state.rooms) - - // Not in MVP - // const increaseRoom = useGuestsRoomsStore.use.increaseRoom() - // const decreaseRoom = useGuestsRoomsStore.use.decreaseRoom() - - return ( -
    -
    - -
    -
    - {rooms.map((room, index) => ( -
    -
    - - {roomLabel} {index + 1} - - - -
    - {/* Not in MVP - {index > 0 ? ( - - ) : null} */} - -
    - ))} -
    - - {rooms.length < 4 ? ( - - ) : null} - -
    -
    -
    -
    - - {rooms.length < 4 ? ( - - ) : null} - -
    - - -
    -
    - ) -} diff --git a/components/GuestsRoomsPicker/guests-rooms-picker.module.css b/components/GuestsRoomsPicker/guests-rooms-picker.module.css index 23a84fd62..01dea2e78 100644 --- a/components/GuestsRoomsPicker/guests-rooms-picker.module.css +++ b/components/GuestsRoomsPicker/guests-rooms-picker.module.css @@ -1,9 +1,6 @@ .container { overflow: hidden; position: relative; - &[data-isopen="true"] { - overflow: visible; - } } .roomContainer { display: grid; @@ -14,9 +11,6 @@ gap: var(--Spacing-x2); padding-bottom: var(--Spacing-x1); } -.hideWrapper { - background-color: var(--Main-Grey-White); -} .roomHeading { margin-bottom: var(--Spacing-x1); } @@ -39,33 +33,25 @@ margin-top: var(--Spacing-x2); } -@media screen and (max-width: 1366px) { - .hideWrapper { - border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; - bottom: 0; - left: 0; - position: fixed; - right: 0; - top: 100%; - transition: top 300ms ease; - z-index: 10002; - overflow: hidden; - } +.pickerContainer { + --header-height: 72px; + --sticky-button-height: 140px; + background-color: var(--Main-Grey-White); + display: grid; + border-radius: var(--Corner-radius-Large); + box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1); + + max-width: calc(100vw - 20px); + padding: var(--Spacing-x2) var(--Spacing-x3); + + width: 360px; +} +@media screen and (max-width: 1366px) { .container[data-isopen="true"] .hideWrapper { top: 20px; } - .pickerContainer { - --header-height: 72px; - --sticky-button-height: 140px; - display: grid; - grid-template-areas: - "header" - "content"; - grid-template-rows: var(--header-height) calc(100dvh - var(--header-height)); - position: relative; - } .contentContainer { grid-area: content; overflow-y: scroll; @@ -121,19 +107,6 @@ } @media screen and (min-width: 1367px) { - .hideWrapper { - border-radius: var(--Corner-radius-Large); - box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1); - left: calc((var(--Spacing-x1) + var(--Spacing-x2)) * -1); - max-width: calc(100vw - 20px); - padding: var(--Spacing-x2) var(--Spacing-x3); - position: absolute; - top: calc(100% + var(--Spacing-x2) + 1px + var(--Spacing-x4)); - width: 360px; - max-height: calc(100dvh - 77px - var(--Spacing-x6)); - overflow-y: auto; - } - .header { display: none; } diff --git a/components/GuestsRoomsPicker/index.tsx b/components/GuestsRoomsPicker/index.tsx index 090fc3803..84ef6f930 100644 --- a/components/GuestsRoomsPicker/index.tsx +++ b/components/GuestsRoomsPicker/index.tsx @@ -1,6 +1,7 @@ "use client" import { useCallback, useEffect, useRef, useState } from "react" +import { Button, DialogTrigger, Popover } from "react-aria-components" import { useFormContext } from "react-hook-form" import { useIntl } from "react-intl" @@ -9,7 +10,7 @@ import { useGuestsRoomsStore } from "@/stores/guests-rooms" import { guestRoomsSchema } from "@/components/Forms/BookingWidget/schema" import Body from "@/components/TempDesignSystem/Text/Body" -import GuestsRoomsPicker from "./GuestsRoomsPicker" +import Dialog from "./Dialog" import styles from "./guests-rooms-picker.module.css" @@ -19,47 +20,16 @@ export default function GuestsRoomsPickerForm({ name: string }) { const intl = useIntl() - const [isOpen, setIsOpen] = useState(false) - const { setValue } = useFormContext() - const { rooms, adultCount, childCount, setIsValidated } = useGuestsRoomsStore( - (state) => ({ - rooms: state.rooms, - adultCount: state.adultCount, - childCount: state.childCount, - setIsValidated: state.setIsValidated, - }) - ) - const ref = useRef(null) - function handleOnClick() { - setIsOpen((prevIsOpen) => !prevIsOpen) - } - const closePicker = useCallback(() => { - const guestRoomsValidData = guestRoomsSchema.safeParse(rooms) - if (guestRoomsValidData.success) { - setIsOpen(false) - setIsValidated(false) - setValue(name, guestRoomsValidData.data, { shouldValidate: true }) - } else { - setIsValidated(true) - } - }, [rooms, name, setValue, setIsValidated, setIsOpen]) - useEffect(() => { - function handleClickOutside(evt: Event) { - const target = evt.target as HTMLElement - if (ref.current && target && !ref.current.contains(target)) { - closePicker() - } - } - document.addEventListener("click", handleClickOutside) - return () => { - document.removeEventListener("click", handleClickOutside) - } - }, [closePicker]) + const { rooms, adultCount, childCount } = useGuestsRoomsStore((state) => ({ + rooms: state.rooms, + adultCount: state.adultCount, + childCount: state.childCount, + })) return ( -
    - -
    - -
    -
    + + + + + ) } diff --git a/types/components/bookingWidget/guestsRoomsPicker.ts b/types/components/bookingWidget/guestsRoomsPicker.ts index 61e8f7d7a..b59744afc 100644 --- a/types/components/bookingWidget/guestsRoomsPicker.ts +++ b/types/components/bookingWidget/guestsRoomsPicker.ts @@ -13,10 +13,6 @@ export type GuestsRoom = { child: Child[] } -export interface GuestsRoomsPickerProps { - closePicker: () => void -} - export type GuestsRoomPickerProps = { index: number } From ca3819f7cc52131a268b61e811a229bd2f779af8 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Tue, 12 Nov 2024 16:45:25 +0100 Subject: [PATCH 49/98] fix: remove guest picker store --- app/globals.css | 2 + components/BookingWidget/Client.tsx | 40 +-- .../MobileToggleButton/button.module.css | 2 - .../MobileToggleButton/index.tsx | 7 - .../BookingWidget/bookingWidget.module.css | 96 ++++---- .../Forms/BookingWidget/FormContent/index.tsx | 12 +- .../GuestsRoomsPicker/AdultSelector/index.tsx | 38 ++- .../ChildSelector/ChildInfoSelector.tsx | 35 +-- .../GuestsRoomsPicker/ChildSelector/index.tsx | 41 ++-- components/GuestsRoomsPicker/Dialog.tsx | 130 ---------- components/GuestsRoomsPicker/Form.tsx | 146 +++++++++++ .../Provider/GuestsRoomsProvider.tsx | 26 -- .../guests-rooms-picker.module.css | 70 ++++-- components/GuestsRoomsPicker/index.tsx | 80 +++--- stores/guests-rooms.ts | 227 ------------------ stores/sticky-position.ts | 2 - .../bookingWidget/guestsRoomsPicker.ts | 11 +- 17 files changed, 368 insertions(+), 597 deletions(-) delete mode 100644 components/GuestsRoomsPicker/Dialog.tsx create mode 100644 components/GuestsRoomsPicker/Form.tsx delete mode 100644 components/GuestsRoomsPicker/Provider/GuestsRoomsProvider.tsx delete mode 100644 stores/guests-rooms.ts diff --git a/app/globals.css b/app/globals.css index 7dd0c44fb..a266e9f31 100644 --- a/app/globals.css +++ b/app/globals.css @@ -114,6 +114,8 @@ /* Z-INDEX */ --header-z-index: 11; --menu-overlay-z-index: 11; + --booking-widget-z-index: 10; + --booking-widget-open-z-index: 100; --dialog-z-index: 9; --sidepeek-z-index: 100; --lightbox-z-index: 150; diff --git a/components/BookingWidget/Client.tsx b/components/BookingWidget/Client.tsx index 70daa6a20..3819eb912 100644 --- a/components/BookingWidget/Client.tsx +++ b/components/BookingWidget/Client.tsx @@ -36,7 +36,6 @@ export default function BookingWidgetClient({ name: StickyElementNameEnum.BOOKING_WIDGET, }) - const bookingWidgetSearchData: BookingWidgetSearchParams | undefined = searchParams ? (getFormattedUrlQueryParams(new URLSearchParams(searchParams), { @@ -79,9 +78,7 @@ export default function BookingWidgetClient({ const methods = useForm({ defaultValues: { search: selectedLocation?.name ?? "", - location: selectedLocation - ? JSON.stringify(selectedLocation) - : undefined, + location: selectedLocation ? JSON.stringify(selectedLocation) : undefined, date: { // 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. @@ -147,29 +144,34 @@ export default function BookingWidgetClient({ ? JSON.parse(sessionStorageSearchData) : undefined - !(selectedLocation?.name) && initialSelectedLocation?.name && + !selectedLocation?.name && + initialSelectedLocation?.name && methods.setValue("search", initialSelectedLocation.name) - !selectedLocation && sessionStorageSearchData && + !selectedLocation && + sessionStorageSearchData && methods.setValue("location", encodeURIComponent(sessionStorageSearchData)) }, [methods, selectedLocation]) return ( -
    -
    -
    -
    - - +
    + +
    + + +
    - ) } diff --git a/components/BookingWidget/MobileToggleButton/button.module.css b/components/BookingWidget/MobileToggleButton/button.module.css index 9a0912b07..1742ee38a 100644 --- a/components/BookingWidget/MobileToggleButton/button.module.css +++ b/components/BookingWidget/MobileToggleButton/button.module.css @@ -6,8 +6,6 @@ display: grid; gap: var(--Spacing-x-one-and-half); padding: var(--Spacing-x2); - position: sticky; - top: 0; z-index: 1; background-color: var(--Base-Surface-Primary-light-Normal); } diff --git a/components/BookingWidget/MobileToggleButton/index.tsx b/components/BookingWidget/MobileToggleButton/index.tsx index 80026ff93..8ecdfd606 100644 --- a/components/BookingWidget/MobileToggleButton/index.tsx +++ b/components/BookingWidget/MobileToggleButton/index.tsx @@ -31,12 +31,6 @@ export default function MobileToggleButton({ const location = useWatch({ name: "location" }) const rooms: BookingWidgetSchema["rooms"] = useWatch({ name: "rooms" }) - const bookingWidgetMobileRef = useRef(null) - useStickyPosition({ - ref: bookingWidgetMobileRef, - name: StickyElementNameEnum.BOOKING_WIDGET_MOBILE, - }) - const parsedLocation: Location | null = location ? JSON.parse(decodeURIComponent(location)) : null @@ -75,7 +69,6 @@ export default function MobileToggleButton({ className={locationAndDateIsSet ? styles.complete : styles.partial} onClick={openMobileSearch} role="button" - ref={bookingWidgetMobileRef} > {!locationAndDateIsSet && ( <> diff --git a/components/BookingWidget/bookingWidget.module.css b/components/BookingWidget/bookingWidget.module.css index f0dec979d..ccf77032c 100644 --- a/components/BookingWidget/bookingWidget.module.css +++ b/components/BookingWidget/bookingWidget.module.css @@ -1,60 +1,62 @@ -.containerDesktop, -.containerMobile, -.close { - display: none; +.wrapper { + position: sticky; + z-index: var(--booking-widget-z-index); } -@media screen and (max-width: 767px) { - .containerMobile { - background-color: var(--UI-Input-Controls-Surface-Normal); - bottom: -100%; - display: grid; - gap: var(--Spacing-x3); - grid-template-rows: 36px 1fr; - height: calc(100dvh - 20px); - padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x7); - position: fixed; - transition: bottom 300ms ease; - width: 100%; - z-index: 10000; - border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; - } +.formContainer { + display: grid; + grid-template-rows: 36px 1fr; + background-color: var(--UI-Input-Controls-Surface-Normal); + border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; + gap: var(--Spacing-x3); + height: calc(100dvh - 20px); + width: 100%; + padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x7); + position: fixed; + bottom: -100%; + transition: bottom 300ms ease; +} - .containerMobile[data-open="true"] { - bottom: 0; - } +.wrapper[data-open="true"] { + z-index: var(--booking-widget-open-z-index); +} - .close { - background: none; - border: none; - cursor: pointer; - justify-self: flex-end; - } +.wrapper[data-open="true"] .formContainer { + bottom: 0; +} - .containerMobile[data-open="true"] + .backdrop { - background-color: rgba(0, 0, 0, 0.4); - height: 100%; - left: 0; - position: absolute; - top: 0; - width: 100%; - z-index: 1000; - } +.close { + background: none; + border: none; + cursor: pointer; + justify-self: flex-end; +} + +.wrapper[data-open="true"] + .backdrop { + background-color: rgba(0, 0, 0, 0.4); + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; + z-index: calc(var(--booking-widget-open-z-index) - 1); } @media screen and (min-width: 768px) { - .containerDesktop { - display: block; - box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.05); - position: sticky; + .wrapper { top: 0; - z-index: 10; - background-color: var(--Base-Surface-Primary-light-Normal); } -} -@media screen and (min-width: 1367px) { - .container { - z-index: 9; + .formContainer { + display: block; + background-color: var(--Base-Surface-Primary-light-Normal); + box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.05); + height: auto; + position: static; + padding: 0; + } + + .close { + display: none; } } diff --git a/components/Forms/BookingWidget/FormContent/index.tsx b/components/Forms/BookingWidget/FormContent/index.tsx index d5dba4d47..8001747e6 100644 --- a/components/Forms/BookingWidget/FormContent/index.tsx +++ b/components/Forms/BookingWidget/FormContent/index.tsx @@ -1,5 +1,4 @@ "use client" -import { useState } from "react" import { useWatch } from "react-hook-form" import { useIntl } from "react-intl" @@ -7,7 +6,6 @@ import { dt } from "@/lib/dt" import DatePicker from "@/components/DatePicker" import GuestsRoomsPickerForm from "@/components/GuestsRoomsPicker" -import GuestsRoomsProvider from "@/components/GuestsRoomsPicker/Provider/GuestsRoomsProvider" import { SearchIcon } from "@/components/Icons" import Button from "@/components/TempDesignSystem/Button" import Caption from "@/components/TempDesignSystem/Text/Caption" @@ -26,12 +24,10 @@ export default function FormContent({ const intl = useIntl() const selectedDate = useWatch({ name: "date" }) - const rooms = intl.formatMessage({ id: "Guests & Rooms" }) + const roomsLabel = intl.formatMessage({ id: "Guests & Rooms" }) const nights = dt(selectedDate.toDate).diff(dt(selectedDate.fromDate), "days") - const selectedGuests = useWatch({ name: "rooms" }) - return ( <>
    @@ -51,12 +47,10 @@ export default function FormContent({
    - - - +
    diff --git a/components/GuestsRoomsPicker/AdultSelector/index.tsx b/components/GuestsRoomsPicker/AdultSelector/index.tsx index b24d5f681..d9069b6d0 100644 --- a/components/GuestsRoomsPicker/AdultSelector/index.tsx +++ b/components/GuestsRoomsPicker/AdultSelector/index.tsx @@ -3,8 +3,6 @@ import { useFormContext } from "react-hook-form" import { useIntl } from "react-intl" -import { useGuestsRoomsStore } from "@/stores/guests-rooms" - import Caption from "@/components/TempDesignSystem/Text/Caption" import Counter from "../Counter" @@ -13,39 +11,37 @@ import styles from "./adult-selector.module.css" import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" import { - AdultSelectorProps, Child, + SelectorProps, } from "@/types/components/bookingWidget/guestsRoomsPicker" -export default function AdultSelector({ roomIndex = 0 }: AdultSelectorProps) { +export default function AdultSelector({ + roomIndex = 0, + currentAdults, + currentChildren, + childrenInAdultsBed, +}: SelectorProps) { const intl = useIntl() const adultsLabel = intl.formatMessage({ id: "Adults" }) const { setValue } = useFormContext() - const { adults, child, childrenInAdultsBed } = useGuestsRoomsStore( - (state) => state.rooms[roomIndex] - ) - const increaseAdults = useGuestsRoomsStore((state) => state.increaseAdults) - const decreaseAdults = useGuestsRoomsStore((state) => state.decreaseAdults) function increaseAdultsCount(roomIndex: number) { - if (adults < 6) { - increaseAdults(roomIndex) - setValue(`rooms.${roomIndex}.adults`, adults + 1) + if (currentAdults < 6) { + setValue(`rooms.${roomIndex}.adults`, currentAdults + 1) } } function decreaseAdultsCount(roomIndex: number) { - if (adults > 1) { - decreaseAdults(roomIndex) - setValue(`rooms.${roomIndex}.adults`, adults - 1) - if (childrenInAdultsBed > adults) { - const toUpdateIndex = child.findIndex( + if (currentAdults > 1) { + setValue(`rooms.${roomIndex}.adults`, currentAdults - 1) + if (childrenInAdultsBed > currentAdults) { + const toUpdateIndex = currentChildren.findIndex( (child: Child) => child.bed == ChildBedMapEnum.IN_ADULTS_BED ) if (toUpdateIndex != -1) { setValue( `rooms.${roomIndex}.children.${toUpdateIndex}.bed`, - child[toUpdateIndex].age < 3 + currentChildren[toUpdateIndex].age < 3 ? ChildBedMapEnum.IN_CRIB : ChildBedMapEnum.IN_EXTRA_BED ) @@ -60,15 +56,15 @@ export default function AdultSelector({ roomIndex = 0 }: AdultSelectorProps) { {adultsLabel} { decreaseAdultsCount(roomIndex) }} handleOnIncrease={() => { increaseAdultsCount(roomIndex) }} - disableDecrease={adults == 1} - disableIncrease={adults == 6} + disableDecrease={currentAdults == 1} + disableIncrease={currentAdults == 6} /> ) diff --git a/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx b/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx index d8e541eb3..83e3893d1 100644 --- a/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx +++ b/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx @@ -3,8 +3,6 @@ import { useFormContext } from "react-hook-form" import { useIntl } from "react-intl" -import { useGuestsRoomsStore } from "@/stores/guests-rooms" - import { ErrorCircleIcon } from "@/components/Icons" import Select from "@/components/TempDesignSystem/Select" import Caption from "@/components/TempDesignSystem/Text/Caption" @@ -19,6 +17,8 @@ import { export default function ChildInfoSelector({ child = { age: -1, bed: -1 }, + childrenInAdultsBed, + adults, index = 0, roomIndex = 0, }: ChildInfoSelectorProps) { @@ -26,23 +26,7 @@ export default function ChildInfoSelector({ const ageLabel = intl.formatMessage({ id: "Age" }) const ageReqdErrMsg = intl.formatMessage({ id: "Child age is required" }) const bedLabel = intl.formatMessage({ id: "Bed" }) - const { setValue } = useFormContext() - const { adults, childrenInAdultsBed } = useGuestsRoomsStore( - (state) => state.rooms[roomIndex] - ) - const { - isValidated, - updateChildAge, - updateChildBed, - increaseChildInAdultsBed, - decreaseChildInAdultsBed, - } = useGuestsRoomsStore((state) => ({ - isValidated: state.isValidated, - updateChildAge: state.updateChildAge, - updateChildBed: state.updateChildBed, - increaseChildInAdultsBed: state.increaseChildInAdultsBed, - decreaseChildInAdultsBed: state.decreaseChildInAdultsBed, - })) + const { setValue, formState } = useFormContext() const ageList = Array.from(Array(13).keys()).map((age) => ({ label: `${age}`, @@ -50,7 +34,6 @@ export default function ChildInfoSelector({ })) function updateSelectedAge(age: number) { - updateChildAge(age, roomIndex, index) setValue(`rooms.${roomIndex}.child.${index}.age`, age, { shouldValidate: true, }) @@ -59,12 +42,6 @@ export default function ChildInfoSelector({ } function updateSelectedBed(bed: number) { - if (bed == ChildBedMapEnum.IN_ADULTS_BED) { - increaseChildInAdultsBed(roomIndex) - } else if (child.bed == ChildBedMapEnum.IN_ADULTS_BED) { - decreaseChildInAdultsBed(roomIndex) - } - updateChildBed(bed, roomIndex, index) setValue(`rooms.${roomIndex}.child.${index}.bed`, bed) } @@ -97,6 +74,7 @@ export default function ChildInfoSelector({ return availableBedTypes } + console.log("ALL TJHE ERORRORS", formState.errors) return ( <>
    @@ -131,12 +109,13 @@ export default function ChildInfoSelector({ ) : null}
    - {isValidated && child.age < 0 ? ( + + {/* {isValidated && child.age < 0 ? (
    - ) : null} + ) : null} */} ) } diff --git a/components/GuestsRoomsPicker/ChildSelector/index.tsx b/components/GuestsRoomsPicker/ChildSelector/index.tsx index 22d594397..2d8db11f0 100644 --- a/components/GuestsRoomsPicker/ChildSelector/index.tsx +++ b/components/GuestsRoomsPicker/ChildSelector/index.tsx @@ -3,8 +3,6 @@ import { useFormContext } from "react-hook-form" import { useIntl } from "react-intl" -import { useGuestsRoomsStore } from "@/stores/guests-rooms" - import Caption from "@/components/TempDesignSystem/Text/Caption" import Counter from "../Counter" @@ -13,25 +11,22 @@ import ChildInfoSelector from "./ChildInfoSelector" import styles from "./child-selector.module.css" import { BookingWidgetSchema } from "@/types/components/bookingWidget" -import { ChildSelectorProps } from "@/types/components/bookingWidget/guestsRoomsPicker" +import { SelectorProps } from "@/types/components/bookingWidget/guestsRoomsPicker" -export default function ChildSelector({ roomIndex = 0 }: ChildSelectorProps) { +export default function ChildSelector({ + roomIndex = 0, + currentAdults, + childrenInAdultsBed, + currentChildren, +}: SelectorProps) { const intl = useIntl() const childrenLabel = intl.formatMessage({ id: "Children" }) - const { setValue, trigger } = useFormContext() - const children = useGuestsRoomsStore((state) => state.rooms[roomIndex].child) - const increaseChildren = useGuestsRoomsStore( - (state) => state.increaseChildren - ) - const decreaseChildren = useGuestsRoomsStore( - (state) => state.decreaseChildren - ) + const { setValue } = useFormContext() function increaseChildrenCount(roomIndex: number) { - if (children.length < 5) { - increaseChildren(roomIndex) + if (currentChildren.length < 5) { setValue( - `rooms.${roomIndex}.child.${children.length}`, + `rooms.${roomIndex}.child.${currentChildren.length}`, { age: -1, bed: -1, @@ -41,9 +36,9 @@ export default function ChildSelector({ roomIndex = 0 }: ChildSelectorProps) { } } function decreaseChildrenCount(roomIndex: number) { - if (children.length > 0) { - const newChildrenList = decreaseChildren(roomIndex) - setValue(`rooms.${roomIndex}.child`, newChildrenList, { + if (currentChildren.length > 0) { + currentChildren.pop() + setValue(`rooms.${roomIndex}.child`, currentChildren, { shouldValidate: true, }) } @@ -56,23 +51,25 @@ export default function ChildSelector({ roomIndex = 0 }: ChildSelectorProps) { {childrenLabel} { decreaseChildrenCount(roomIndex) }} handleOnIncrease={() => { increaseChildrenCount(roomIndex) }} - disableDecrease={children.length == 0} - disableIncrease={children.length == 5} + disableDecrease={currentChildren.length == 0} + disableIncrease={currentChildren.length == 5} /> - {children.map((child, index) => ( + {currentChildren.map((child, index) => ( ))} diff --git a/components/GuestsRoomsPicker/Dialog.tsx b/components/GuestsRoomsPicker/Dialog.tsx deleted file mode 100644 index 1c5d8e53d..000000000 --- a/components/GuestsRoomsPicker/Dialog.tsx +++ /dev/null @@ -1,130 +0,0 @@ -"use client" -import { Dialog } from "react-aria-components" -import { useFormContext } from "react-hook-form" -import { useIntl } from "react-intl" - -import { useGuestsRoomsStore } from "@/stores/guests-rooms" - -import { CloseLargeIcon, PlusCircleIcon, PlusIcon } from "../Icons" -import Button from "../TempDesignSystem/Button" -import Divider from "../TempDesignSystem/Divider" -import Subtitle from "../TempDesignSystem/Text/Subtitle" -import { Tooltip } from "../TempDesignSystem/Tooltip" -import AdultSelector from "./AdultSelector" -import ChildSelector from "./ChildSelector" - -import styles from "./guests-rooms-picker.module.css" - -import { BookingWidgetSchema } from "@/types/components/bookingWidget" - -export default function GuestsRoomsPickerDialog() { - const intl = useIntl() - const doneLabel = intl.formatMessage({ id: "Done" }) - const roomLabel = intl.formatMessage({ id: "Room" }) - const disabledBookingOptionsHeader = intl.formatMessage({ - id: "Disabled booking options header", - }) - const disabledBookingOptionsText = intl.formatMessage({ - id: "Disabled adding room", - }) - const addRoomLabel = intl.formatMessage({ id: "Add Room" }) - - const { getFieldState } = useFormContext() - - const rooms = useGuestsRoomsStore((state) => state.rooms) - - return ( - - {({ close }) => { - return ( - <> -
    - -
    -
    - {rooms.map((room, index) => ( -
    -
    - - {roomLabel} {index + 1} - - - -
    - -
    - ))} -
    - - {rooms.length < 4 ? ( - - ) : null} - -
    -
    -
    -
    - - {rooms.length < 4 ? ( - - ) : null} - -
    - - -
    - - ) - }} -
    - ) -} diff --git a/components/GuestsRoomsPicker/Form.tsx b/components/GuestsRoomsPicker/Form.tsx new file mode 100644 index 000000000..74f1dab6b --- /dev/null +++ b/components/GuestsRoomsPicker/Form.tsx @@ -0,0 +1,146 @@ +"use client" + +import { useFormContext } from "react-hook-form" +import { useIntl } from "react-intl" + +import { CloseLargeIcon, PlusCircleIcon, PlusIcon } from "../Icons" +import Button from "../TempDesignSystem/Button" +import Divider from "../TempDesignSystem/Divider" +import Subtitle from "../TempDesignSystem/Text/Subtitle" +import { Tooltip } from "../TempDesignSystem/Tooltip" +import AdultSelector from "./AdultSelector" +import ChildSelector from "./ChildSelector" + +import styles from "./guests-rooms-picker.module.css" + +import { BookingWidgetSchema } from "@/types/components/bookingWidget" +import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" +import { GuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker" + +export default function GuestsRoomsPickerDialog({ + rooms, + onClose, +}: { + rooms: GuestsRoom[] + onClose: () => void +}) { + const intl = useIntl() + const doneLabel = intl.formatMessage({ id: "Done" }) + const roomLabel = intl.formatMessage({ id: "Room" }) + const disabledBookingOptionsHeader = intl.formatMessage({ + id: "Disabled booking options header", + }) + const disabledBookingOptionsText = intl.formatMessage({ + id: "Disabled adding room", + }) + const addRoomLabel = intl.formatMessage({ id: "Add Room" }) + + const { getFieldState } = useFormContext() + + return ( + <> +
    + +
    +
    + {rooms.map((room, index) => { + const currentAdults = room.adults + const currentChildren = room.child + const childrenInAdultsBed = currentChildren.filter( + (child) => child.bed === ChildBedMapEnum.IN_ADULTS_BED + ).length + + return ( +
    +
    + + {roomLabel} {index + 1} + + + +
    + +
    + ) + })} +
    + + {rooms.length < 4 ? ( + + ) : null} + +
    +
    +
    +
    + + {rooms.length < 4 ? ( + + ) : null} + +
    + + +
    + + ) +} diff --git a/components/GuestsRoomsPicker/Provider/GuestsRoomsProvider.tsx b/components/GuestsRoomsPicker/Provider/GuestsRoomsProvider.tsx deleted file mode 100644 index 5a85102e7..000000000 --- a/components/GuestsRoomsPicker/Provider/GuestsRoomsProvider.tsx +++ /dev/null @@ -1,26 +0,0 @@ -"use client" -import { PropsWithChildren, useRef } from "react" - -import { - GuestsRoomsContext, - type GuestsRoomsStore, - initGuestsRoomsState, -} from "@/stores/guests-rooms" - -import { GuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker" - -export default function GuestsRoomsProvider({ - selectedGuests, - children, -}: PropsWithChildren<{ selectedGuests?: GuestsRoom[] }>) { - const initialStore = useRef() - if (!initialStore.current) { - initialStore.current = initGuestsRoomsState(selectedGuests) - } - - return ( - - {children} - - ) -} diff --git a/components/GuestsRoomsPicker/guests-rooms-picker.module.css b/components/GuestsRoomsPicker/guests-rooms-picker.module.css index 01dea2e78..0a6c09a6f 100644 --- a/components/GuestsRoomsPicker/guests-rooms-picker.module.css +++ b/components/GuestsRoomsPicker/guests-rooms-picker.module.css @@ -1,7 +1,23 @@ -.container { - overflow: hidden; - position: relative; +.triggerDesktop { + display: none; } + +.pickerContainerMobile { + background-color: var(--Main-Grey-White); + border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; + bottom: 0; + left: 0; + position: fixed; + right: 0; + top: 0; + transition: top 300ms ease; + z-index: 1000; +} + +.pickerContainerDesktop { + display: none; +} + .roomContainer { display: grid; gap: var(--Spacing-x2); @@ -23,9 +39,7 @@ width: 100%; text-align: left; } -.body { - opacity: 0.8; -} + .footer { display: grid; gap: var(--Spacing-x1); @@ -33,25 +47,7 @@ margin-top: var(--Spacing-x2); } -.pickerContainer { - --header-height: 72px; - --sticky-button-height: 140px; - background-color: var(--Main-Grey-White); - display: grid; - - border-radius: var(--Corner-radius-Large); - box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1); - - max-width: calc(100vw - 20px); - padding: var(--Spacing-x2) var(--Spacing-x3); - - width: 360px; -} @media screen and (max-width: 1366px) { - .container[data-isopen="true"] .hideWrapper { - top: 20px; - } - .contentContainer { grid-area: content; overflow-y: scroll; @@ -107,6 +103,32 @@ } @media screen and (min-width: 1367px) { + .pickerContainerMobisse { + display: none; + } + .triggerMobile { + display: none; + } + + .triggerDesktop { + display: block; + } + + .pickerContainerDesktop { + --header-height: 72px; + --sticky-button-height: 140px; + background-color: var(--Main-Grey-White); + display: grid; + + border-radius: var(--Corner-radius-Large); + box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1); + + max-width: calc(100vw - 20px); + padding: var(--Spacing-x2) var(--Spacing-x3); + + width: 360px; + } + .header { display: none; } diff --git a/components/GuestsRoomsPicker/index.tsx b/components/GuestsRoomsPicker/index.tsx index 84ef6f930..84595815c 100644 --- a/components/GuestsRoomsPicker/index.tsx +++ b/components/GuestsRoomsPicker/index.tsx @@ -1,37 +1,64 @@ "use client" -import { useCallback, useEffect, useRef, useState } from "react" -import { Button, DialogTrigger, Popover } from "react-aria-components" +import { + Button, + Dialog, + DialogTrigger, + Modal, + Popover, +} from "react-aria-components" import { useFormContext } from "react-hook-form" import { useIntl } from "react-intl" -import { useGuestsRoomsStore } from "@/stores/guests-rooms" - -import { guestRoomsSchema } from "@/components/Forms/BookingWidget/schema" import Body from "@/components/TempDesignSystem/Text/Body" -import Dialog from "./Dialog" +import PickerForm from "./Form" import styles from "./guests-rooms-picker.module.css" -export default function GuestsRoomsPickerForm({ - name = "rooms", +import { GuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker" + +export default function GuestsRoomsPickerForm() { + const { watch } = useFormContext() + + const rooms = watch("rooms") as GuestsRoom[] + + return ( + <> + + + + + {({ close }) => } + + + + + + + + {({ close }) => } + + + + + ) +} + +function Trigger({ + rooms, + className, }: { - name: string + rooms: GuestsRoom[] + className: string }) { const intl = useIntl() - const { rooms, adultCount, childCount } = useGuestsRoomsStore((state) => ({ - rooms: state.rooms, - adultCount: state.adultCount, - childCount: state.childCount, - })) - return ( - - - - - - + ))} + + ) } diff --git a/stores/guests-rooms.ts b/stores/guests-rooms.ts deleted file mode 100644 index 04cfbc3ec..000000000 --- a/stores/guests-rooms.ts +++ /dev/null @@ -1,227 +0,0 @@ -"use client" - -import { produce } from "immer" -import { createContext, useContext } from "react" -import { create, useStore } from "zustand" - -import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" -import { - Child, - GuestsRoom, -} from "@/types/components/bookingWidget/guestsRoomsPicker" - -const SESSION_STORAGE_KEY = "guests_rooms" - -interface extendedGuestsRoom extends GuestsRoom { - childrenInAdultsBed: number -} -interface GuestsRoomsState { - rooms: extendedGuestsRoom[] - adultCount: number - childCount: number - isValidated: boolean -} - -interface GuestsRoomsStoreState extends GuestsRoomsState { - increaseAdults: (roomIndex: number) => void - decreaseAdults: (roomIndex: number) => void - increaseChildren: (roomIndex: number) => void - decreaseChildren: (roomIndex: number) => Child[] - updateChildAge: (age: number, roomIndex: number, childIndex: number) => void - updateChildBed: (bed: number, roomIndex: number, childIndex: number) => void - increaseChildInAdultsBed: (roomIndex: number) => void - decreaseChildInAdultsBed: (roomIndex: number) => void - increaseRoom: () => void - decreaseRoom: (roomIndex: number) => void - setIsValidated: (isValidated: boolean) => void -} - -export function validateBedTypes(data: extendedGuestsRoom[]) { - data.forEach((room) => { - room.child.forEach((child) => { - const allowedBedTypes: number[] = [] - if (child.age <= 5 && room.adults >= room.childrenInAdultsBed) { - allowedBedTypes.push(ChildBedMapEnum.IN_ADULTS_BED) - } else if (child.age <= 5) { - room.childrenInAdultsBed = room.childrenInAdultsBed - 1 - } - if (child.age < 3) { - allowedBedTypes.push(ChildBedMapEnum.IN_CRIB) - } - if (child.age > 2) { - allowedBedTypes.push(ChildBedMapEnum.IN_EXTRA_BED) - } - if (!allowedBedTypes.includes(child.bed)) { - child.bed = allowedBedTypes[0] - } - }) - }) -} - -export function initGuestsRoomsState(initData?: GuestsRoom[]) { - const isBrowser = typeof window !== "undefined" - const sessionData = isBrowser - ? sessionStorage.getItem(SESSION_STORAGE_KEY) - : null - - const defaultGuestsData: extendedGuestsRoom = { - adults: 1, - child: [], - childrenInAdultsBed: 0, - } - const defaultData: GuestsRoomsState = { - rooms: [defaultGuestsData], - adultCount: 1, - childCount: 0, - isValidated: false, - } - - let inputData: GuestsRoomsState = defaultData - if (sessionData) { - inputData = JSON.parse(sessionData) - } - if (initData) { - inputData.rooms = initData.map((room) => { - const childrenInAdultsBed = room.child - ? room.child.reduce((acc, child) => { - acc = acc + (child.bed == ChildBedMapEnum.IN_ADULTS_BED ? 1 : 0) - return acc - }, 0) - : 0 - return { ...defaultGuestsData, ...room, childrenInAdultsBed } - }) as extendedGuestsRoom[] - - inputData.adultCount = initData.reduce((acc, room) => { - acc = acc + room.adults - return acc - }, 0) - inputData.childCount = initData.reduce((acc, room) => { - acc = acc + room.child?.length - return acc - }, 0) - validateBedTypes(inputData.rooms) - } - - return create()((set, get) => ({ - ...inputData, - increaseAdults: (roomIndex) => - set( - produce((state: GuestsRoomsState) => { - state.rooms[roomIndex].adults = state.rooms[roomIndex].adults + 1 - state.adultCount = state.adultCount + 1 - }) - ), - decreaseAdults: (roomIndex) => - set( - produce((state: GuestsRoomsState) => { - state.rooms[roomIndex].adults = state.rooms[roomIndex].adults - 1 - state.adultCount = state.adultCount - 1 - if ( - state.rooms[roomIndex].childrenInAdultsBed > - state.rooms[roomIndex].adults - ) { - const toUpdateIndex = state.rooms[roomIndex].child.findIndex( - (child) => child.bed == ChildBedMapEnum.IN_ADULTS_BED - ) - if (toUpdateIndex != -1) { - state.rooms[roomIndex].child[toUpdateIndex].bed = - state.rooms[roomIndex].child[toUpdateIndex].age < 3 - ? ChildBedMapEnum.IN_CRIB - : ChildBedMapEnum.IN_EXTRA_BED - state.rooms[roomIndex].childrenInAdultsBed = - state.rooms[roomIndex].adults - } - } - }) - ), - increaseChildren: (roomIndex) => - set( - produce((state: GuestsRoomsState) => { - state.rooms[roomIndex].child.push({ - age: -1, - bed: -1, - }) - state.childCount = state.childCount + 1 - }) - ), - decreaseChildren: (roomIndex) => { - set( - produce((state: GuestsRoomsState) => { - const roomChildren = state.rooms[roomIndex].child - if ( - roomChildren.length && - roomChildren[roomChildren.length - 1].bed == - ChildBedMapEnum.IN_ADULTS_BED - ) { - state.rooms[roomIndex].childrenInAdultsBed = - state.rooms[roomIndex].childrenInAdultsBed - 1 - } - state.rooms[roomIndex].child.pop() - state.childCount = state.childCount - 1 - }) - ) - return get().rooms[roomIndex].child - }, - updateChildAge: (age, roomIndex, childIndex) => - set( - produce((state: GuestsRoomsState) => { - state.rooms[roomIndex].child[childIndex].age = age - }) - ), - updateChildBed: (bed, roomIndex, childIndex) => - set( - produce((state: GuestsRoomsState) => { - state.rooms[roomIndex].child[childIndex].bed = bed - }) - ), - increaseChildInAdultsBed: (roomIndex) => - set( - produce((state: GuestsRoomsState) => { - state.rooms[roomIndex].childrenInAdultsBed = - state.rooms[roomIndex].childrenInAdultsBed + 1 - }) - ), - decreaseChildInAdultsBed: (roomIndex) => - set( - produce((state: GuestsRoomsState) => { - state.rooms[roomIndex].childrenInAdultsBed = - state.rooms[roomIndex].childrenInAdultsBed - 1 - }) - ), - increaseRoom: () => - set( - produce((state: GuestsRoomsState) => { - state.rooms.push({ - adults: 1, - child: [], - childrenInAdultsBed: 0, - }) - }) - ), - decreaseRoom: (roomIndex) => - set( - produce((state: GuestsRoomsState) => { - state.rooms.splice(roomIndex, 1) - }) - ), - setIsValidated: (isValidated) => set(() => ({ isValidated })), - })) -} - -export type GuestsRoomsStore = ReturnType - -export const GuestsRoomsContext = createContext(null) - -export const useGuestsRoomsStore = ( - selector: (store: GuestsRoomsStoreState) => T -): T => { - const guestsRoomsContextStore = useContext(GuestsRoomsContext) - - if (!guestsRoomsContextStore) { - throw new Error( - `guestsRoomsContextStore must be used within GuestsRoomsContextProvider` - ) - } - - return useStore(guestsRoomsContextStore, selector) -} diff --git a/stores/sticky-position.ts b/stores/sticky-position.ts index 99272ab62..93328b9e9 100644 --- a/stores/sticky-position.ts +++ b/stores/sticky-position.ts @@ -3,7 +3,6 @@ import { create } from "zustand" export enum StickyElementNameEnum { SITEWIDE_ALERT = "SITEWIDE_ALERT", BOOKING_WIDGET = "BOOKING_WIDGET", - BOOKING_WIDGET_MOBILE = "BOOKING_WIDGET_MOBILE", HOTEL_TAB_NAVIGATION = "HOTEL_TAB_NAVIGATION", HOTEL_STATIC_MAP = "HOTEL_STATIC_MAP", } @@ -32,7 +31,6 @@ interface StickyStore { const priorityMap: Record = { [StickyElementNameEnum.SITEWIDE_ALERT]: 1, [StickyElementNameEnum.BOOKING_WIDGET]: 2, - [StickyElementNameEnum.BOOKING_WIDGET_MOBILE]: 2, [StickyElementNameEnum.HOTEL_TAB_NAVIGATION]: 3, [StickyElementNameEnum.HOTEL_STATIC_MAP]: 3, diff --git a/types/components/bookingWidget/guestsRoomsPicker.ts b/types/components/bookingWidget/guestsRoomsPicker.ts index b59744afc..a075c9f8d 100644 --- a/types/components/bookingWidget/guestsRoomsPicker.ts +++ b/types/components/bookingWidget/guestsRoomsPicker.ts @@ -17,18 +17,19 @@ export type GuestsRoomPickerProps = { index: number } -export type AdultSelectorProps = { - roomIndex: number -} - -export type ChildSelectorProps = { +export type SelectorProps = { roomIndex: number + currentAdults: number + currentChildren: Child[] + childrenInAdultsBed: number } export type ChildInfoSelectorProps = { child: Child + adults: number index: number roomIndex: number + childrenInAdultsBed: number } export interface CounterProps { From 7a3194f978e17e5ecf46c1cec4798bc0e0066826 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Wed, 13 Nov 2024 14:43:06 +0100 Subject: [PATCH 50/98] fix: handle stickyness when scrolling is locked --- components/BookingWidget/Client.tsx | 21 ++-- .../BookingWidget/bookingWidget.module.css | 3 +- .../DatePicker/Screen/mobile.module.css | 8 +- components/DatePicker/date-picker.module.css | 2 +- components/Forms/BookingWidget/schema.ts | 14 ++- .../ChildSelector/ChildInfoSelector.tsx | 17 +-- components/GuestsRoomsPicker/Form.tsx | 117 +++++++++--------- .../guests-rooms-picker.module.css | 37 ++++-- components/GuestsRoomsPicker/index.tsx | 22 +++- 9 files changed, 141 insertions(+), 100 deletions(-) diff --git a/components/BookingWidget/Client.tsx b/components/BookingWidget/Client.tsx index 3819eb912..930cad4c0 100644 --- a/components/BookingWidget/Client.tsx +++ b/components/BookingWidget/Client.tsx @@ -38,11 +38,11 @@ export default function BookingWidgetClient({ const bookingWidgetSearchData: BookingWidgetSearchParams | undefined = searchParams - ? (getFormattedUrlQueryParams(new URLSearchParams(searchParams), { + ? getFormattedUrlQueryParams(new URLSearchParams(searchParams), { adults: "number", age: "number", bed: "number", - }) as BookingWidgetSearchParams) + }) : undefined const getLocationObj = (destination: string): Location | undefined => { @@ -75,6 +75,16 @@ export default function BookingWidgetClient({ ) : undefined + const defaultRoomsData = bookingWidgetSearchData?.room?.map((room) => ({ + adults: room.adults, + child: room.child ?? [], + })) ?? [ + { + adults: 1, + child: [], + }, + ] + const methods = useForm({ defaultValues: { search: selectedLocation?.name ?? "", @@ -92,12 +102,7 @@ export default function BookingWidgetClient({ bookingCode: "", redemption: false, voucher: false, - rooms: bookingWidgetSearchData?.room ?? [ - { - adults: 1, - child: [], - }, - ], + rooms: defaultRoomsData, }, shouldFocusError: false, mode: "all", diff --git a/components/BookingWidget/bookingWidget.module.css b/components/BookingWidget/bookingWidget.module.css index ccf77032c..07f4880a4 100644 --- a/components/BookingWidget/bookingWidget.module.css +++ b/components/BookingWidget/bookingWidget.module.css @@ -5,7 +5,7 @@ .formContainer { display: grid; - grid-template-rows: 36px 1fr; + grid-template-rows: auto 1fr; background-color: var(--UI-Input-Controls-Surface-Normal); border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; gap: var(--Spacing-x3); @@ -30,6 +30,7 @@ border: none; cursor: pointer; justify-self: flex-end; + padding: 0; } .wrapper[data-open="true"] + .backdrop { diff --git a/components/DatePicker/Screen/mobile.module.css b/components/DatePicker/Screen/mobile.module.css index ef3f97ca0..5ee03bde5 100644 --- a/components/DatePicker/Screen/mobile.module.css +++ b/components/DatePicker/Screen/mobile.module.css @@ -1,5 +1,5 @@ .container { - --header-height: 68px; + --header-height: 72px; --sticky-button-height: 120px; display: grid; @@ -11,12 +11,10 @@ } .header { - align-self: flex-start; + align-self: flex-end; background-color: var(--Main-Grey-White); - display: grid; grid-area: header; - grid-template-columns: 1fr 24px; - padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x2); + padding: var(--Spacing-x3) var(--Spacing-x2); position: sticky; top: 0; z-index: 10; diff --git a/components/DatePicker/date-picker.module.css b/components/DatePicker/date-picker.module.css index ee9034648..985c9d63c 100644 --- a/components/DatePicker/date-picker.module.css +++ b/components/DatePicker/date-picker.module.css @@ -38,7 +38,7 @@ .hideWrapper { bottom: 0; left: 0; - overflow: auto; + overflow: hidden; position: fixed; right: 0; top: 100%; diff --git a/components/Forms/BookingWidget/schema.ts b/components/Forms/BookingWidget/schema.ts index 973ab6ad6..90f4f6712 100644 --- a/components/Forms/BookingWidget/schema.ts +++ b/components/Forms/BookingWidget/schema.ts @@ -4,12 +4,14 @@ import type { Location } from "@/types/trpc/routers/hotel/locations" export const guestRoomSchema = z.object({ adults: z.number().default(1), - child: z.array( - z.object({ - age: z.number().nonnegative(), - bed: z.number(), - }) - ), + child: z + .array( + z.object({ + age: z.number().nonnegative(), + bed: z.number(), + }) + ) + .default([]), }) export const guestRoomsSchema = z.array(guestRoomSchema) diff --git a/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx b/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx index 83e3893d1..08ff83423 100644 --- a/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx +++ b/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx @@ -15,6 +15,11 @@ import { ChildInfoSelectorProps, } from "@/types/components/bookingWidget/guestsRoomsPicker" +const ageList = Array.from(Array(13).keys()).map((age) => ({ + label: age.toString(), + value: age, +})) + export default function ChildInfoSelector({ child = { age: -1, bed: -1 }, childrenInAdultsBed, @@ -28,11 +33,6 @@ export default function ChildInfoSelector({ const bedLabel = intl.formatMessage({ id: "Bed" }) const { setValue, formState } = useFormContext() - const ageList = Array.from(Array(13).keys()).map((age) => ({ - label: `${age}`, - value: age, - })) - function updateSelectedAge(age: number) { setValue(`rooms.${roomIndex}.child.${index}.age`, age, { shouldValidate: true, @@ -74,7 +74,8 @@ export default function ChildInfoSelector({ return availableBedTypes } - console.log("ALL TJHE ERORRORS", formState.errors) + //@ts-expect-error: formState is typed with FormValues + const roomErrors = formState.errors.rooms?.[roomIndex]?.child?.[index] return ( <>
    @@ -110,12 +111,12 @@ export default function ChildInfoSelector({
    - {/* {isValidated && child.age < 0 ? ( + {roomErrors ? (
    - ) : null} */} + ) : null} ) } diff --git a/components/GuestsRoomsPicker/Form.tsx b/components/GuestsRoomsPicker/Form.tsx index 74f1dab6b..9288f80ec 100644 --- a/components/GuestsRoomsPicker/Form.tsx +++ b/components/GuestsRoomsPicker/Form.tsx @@ -39,65 +39,68 @@ export default function GuestsRoomsPickerDialog({ return ( <> -
    - -
    -
    - {rooms.map((room, index) => { - const currentAdults = room.adults - const currentChildren = room.child - const childrenInAdultsBed = currentChildren.filter( - (child) => child.bed === ChildBedMapEnum.IN_ADULTS_BED - ).length +
    +
    + +
    +
    + {rooms.map((room, index) => { + const currentAdults = room.adults + const currentChildren = room.child + const childrenInAdultsBed = + currentChildren.filter( + (child) => child.bed === ChildBedMapEnum.IN_ADULTS_BED + ).length ?? 0 - return ( -
    -
    - - {roomLabel} {index + 1} - - - -
    - -
    - ) - })} -
    - - {rooms.length < 4 ? ( - - ) : null} - + return ( +
    +
    + + {roomLabel} {index + 1} + + + +
    + +
    + ) + })} +
    + + {rooms.length < 4 ? ( + + ) : null} + +
    -
    +
    - + {({ close }) => } - + - + {({ close }) => } From 9189d588d1c256651954bca03ef30a5e79df0a2c Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Thu, 14 Nov 2024 11:41:56 +0100 Subject: [PATCH 51/98] fix: validation --- components/Forms/BookingWidget/schema.ts | 39 ++++++++++---- .../GuestsRoomsPicker/AdultSelector/index.tsx | 38 +++----------- .../ChildSelector/ChildInfoSelector.tsx | 52 ++++++++++++------- .../GuestsRoomsPicker/ChildSelector/index.tsx | 19 +++---- components/GuestsRoomsPicker/Form.tsx | 26 ++++++++-- components/GuestsRoomsPicker/index.tsx | 40 +++++++------- 6 files changed, 120 insertions(+), 94 deletions(-) diff --git a/components/Forms/BookingWidget/schema.ts b/components/Forms/BookingWidget/schema.ts index 90f4f6712..9d0c8f247 100644 --- a/components/Forms/BookingWidget/schema.ts +++ b/components/Forms/BookingWidget/schema.ts @@ -1,18 +1,37 @@ import { z } from "zod" +import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" import type { Location } from "@/types/trpc/routers/hotel/locations" -export const guestRoomSchema = z.object({ - adults: z.number().default(1), - child: z - .array( - z.object({ - age: z.number().nonnegative(), - bed: z.number(), - }) +export const guestRoomSchema = z + .object({ + adults: z.number().default(1), + child: z + .array( + z.object({ + age: z.number().min(0, "Age is required"), + bed: z.number().min(0, "Bed choice is required"), + }) + ) + .default([]), + }) + .superRefine((value, ctx) => { + const childrenInAdultsBed = value.child.filter( + (c) => c.bed === ChildBedMapEnum.IN_ADULTS_BED ) - .default([]), -}) + if (value.adults < childrenInAdultsBed.length) { + const lastAdultBedIndex = value.child + .map((c) => c.bed) + .lastIndexOf(ChildBedMapEnum.IN_ADULTS_BED) + + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "You cannot have more children in adults bed than adults in the room", + path: ["child", lastAdultBedIndex], + }) + } + }) export const guestRoomsSchema = z.array(guestRoomSchema) diff --git a/components/GuestsRoomsPicker/AdultSelector/index.tsx b/components/GuestsRoomsPicker/AdultSelector/index.tsx index d9069b6d0..008037300 100644 --- a/components/GuestsRoomsPicker/AdultSelector/index.tsx +++ b/components/GuestsRoomsPicker/AdultSelector/index.tsx @@ -9,44 +9,26 @@ import Counter from "../Counter" import styles from "./adult-selector.module.css" -import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" -import { - Child, - SelectorProps, -} from "@/types/components/bookingWidget/guestsRoomsPicker" +import { SelectorProps } from "@/types/components/bookingWidget/guestsRoomsPicker" export default function AdultSelector({ roomIndex = 0, currentAdults, - currentChildren, - childrenInAdultsBed, }: SelectorProps) { + const name = `rooms.${roomIndex}.adults` const intl = useIntl() const adultsLabel = intl.formatMessage({ id: "Adults" }) const { setValue } = useFormContext() - function increaseAdultsCount(roomIndex: number) { + function increaseAdultsCount() { if (currentAdults < 6) { - setValue(`rooms.${roomIndex}.adults`, currentAdults + 1) + setValue(name, currentAdults + 1) } } - function decreaseAdultsCount(roomIndex: number) { + function decreaseAdultsCount() { if (currentAdults > 1) { - setValue(`rooms.${roomIndex}.adults`, currentAdults - 1) - if (childrenInAdultsBed > currentAdults) { - const toUpdateIndex = currentChildren.findIndex( - (child: Child) => child.bed == ChildBedMapEnum.IN_ADULTS_BED - ) - if (toUpdateIndex != -1) { - setValue( - `rooms.${roomIndex}.children.${toUpdateIndex}.bed`, - currentChildren[toUpdateIndex].age < 3 - ? ChildBedMapEnum.IN_CRIB - : ChildBedMapEnum.IN_EXTRA_BED - ) - } - } + setValue(name, currentAdults - 1) } } @@ -57,12 +39,8 @@ export default function AdultSelector({ { - decreaseAdultsCount(roomIndex) - }} - handleOnIncrease={() => { - increaseAdultsCount(roomIndex) - }} + handleOnDecrease={decreaseAdultsCount} + handleOnIncrease={increaseAdultsCount} disableDecrease={currentAdults == 1} disableIncrease={currentAdults == 6} /> diff --git a/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx b/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx index 08ff83423..b5a34c168 100644 --- a/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx +++ b/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx @@ -15,9 +15,9 @@ import { ChildInfoSelectorProps, } from "@/types/components/bookingWidget/guestsRoomsPicker" -const ageList = Array.from(Array(13).keys()).map((age) => ({ - label: age.toString(), - value: age, +const ageList = [...Array(13)].map((_, i) => ({ + label: i.toString(), + value: i, })) export default function ChildInfoSelector({ @@ -27,22 +27,23 @@ export default function ChildInfoSelector({ index = 0, roomIndex = 0, }: ChildInfoSelectorProps) { + const ageFieldName = `rooms.${roomIndex}.child.${index}.age` + const bedFieldName = `rooms.${roomIndex}.child.${index}.bed` const intl = useIntl() const ageLabel = intl.formatMessage({ id: "Age" }) - const ageReqdErrMsg = intl.formatMessage({ id: "Child age is required" }) const bedLabel = intl.formatMessage({ id: "Bed" }) - const { setValue, formState } = useFormContext() - - function updateSelectedAge(age: number) { - setValue(`rooms.${roomIndex}.child.${index}.age`, age, { - shouldValidate: true, - }) - const availableBedTypes = getAvailableBeds(age) - updateSelectedBed(availableBedTypes[0].value) - } + const errorMessage = intl.formatMessage({ id: "Child age is required" }) + const { setValue, formState, register, trigger } = useFormContext() function updateSelectedBed(bed: number) { setValue(`rooms.${roomIndex}.child.${index}.bed`, bed) + trigger() + } + + function updateSelectedAge(age: number) { + setValue(`rooms.${roomIndex}.child.${index}.age`, age) + const availableBedTypes = getAvailableBeds(age) + updateSelectedBed(availableBedTypes[0].value) } const allBedTypes: ChildBed[] = [ @@ -76,6 +77,10 @@ export default function ChildInfoSelector({ //@ts-expect-error: formState is typed with FormValues const roomErrors = formState.errors.rooms?.[roomIndex]?.child?.[index] + + const ageError = roomErrors?.age + const bedError = roomErrors?.bed + return ( <>
    @@ -89,13 +94,15 @@ export default function ChildInfoSelector({ onSelect={(key) => { updateSelectedAge(key as number) }} - name={`rooms.${roomIndex}.child.${index}.age`} placeholder={ageLabel} maxHeight={150} + {...register(ageFieldName, { + required: true, + })} />
    - {child.age !== -1 ? ( + {child.age >= 0 ? (
    + ) : null} + + {ageError || bedError ? ( + ) : null} diff --git a/components/GuestsRoomsPicker/ChildSelector/index.tsx b/components/GuestsRoomsPicker/ChildSelector/index.tsx index 2d8db11f0..be0695ae4 100644 --- a/components/GuestsRoomsPicker/ChildSelector/index.tsx +++ b/components/GuestsRoomsPicker/ChildSelector/index.tsx @@ -10,7 +10,6 @@ import ChildInfoSelector from "./ChildInfoSelector" import styles from "./child-selector.module.css" -import { BookingWidgetSchema } from "@/types/components/bookingWidget" import { SelectorProps } from "@/types/components/bookingWidget/guestsRoomsPicker" export default function ChildSelector({ @@ -21,26 +20,20 @@ export default function ChildSelector({ }: SelectorProps) { const intl = useIntl() const childrenLabel = intl.formatMessage({ id: "Children" }) - const { setValue } = useFormContext() + const { setValue } = useFormContext() function increaseChildrenCount(roomIndex: number) { if (currentChildren.length < 5) { - setValue( - `rooms.${roomIndex}.child.${currentChildren.length}`, - { - age: -1, - bed: -1, - }, - { shouldValidate: true } - ) + setValue(`rooms.${roomIndex}.child.${currentChildren.length}`, { + age: undefined, + bed: undefined, + }) } } function decreaseChildrenCount(roomIndex: number) { if (currentChildren.length > 0) { currentChildren.pop() - setValue(`rooms.${roomIndex}.child`, currentChildren, { - shouldValidate: true, - }) + setValue(`rooms.${roomIndex}.child`, currentChildren) } } diff --git a/components/GuestsRoomsPicker/Form.tsx b/components/GuestsRoomsPicker/Form.tsx index 9288f80ec..01ded876a 100644 --- a/components/GuestsRoomsPicker/Form.tsx +++ b/components/GuestsRoomsPicker/Form.tsx @@ -1,6 +1,7 @@ "use client" -import { useFormContext } from "react-hook-form" +import { useEffect } from "react" +import { useFormContext, useWatch } from "react-hook-form" import { useIntl } from "react-intl" import { CloseLargeIcon, PlusCircleIcon, PlusIcon } from "../Icons" @@ -35,7 +36,24 @@ export default function GuestsRoomsPickerDialog({ }) const addRoomLabel = intl.formatMessage({ id: "Add Room" }) - const { getFieldState } = useFormContext() + const { getFieldState, trigger } = useFormContext() + + const roomsValue = useWatch({ name: "rooms" }) + + async function handleOnClose() { + const state = await trigger("rooms") + if (state) { + onClose() + } + } + + const fieldState = getFieldState("rooms") + + useEffect(() => { + if (fieldState.invalid) { + trigger("rooms") + } + }, [roomsValue, fieldState.invalid, trigger]) return ( <> @@ -124,7 +142,7 @@ export default function GuestsRoomsPickerDialog({ + + +
    +
    + + + +
    + +
    +
    +
    + + + + + + + + + {/*
    + +
    +
    +
    + */} + + ) +} diff --git a/components/Forms/BookingWidget/FormContent/Search/index.tsx b/components/Forms/BookingWidget/FormContent/Search/index.tsx index ac820da51..abbb28112 100644 --- a/components/Forms/BookingWidget/FormContent/Search/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Search/index.tsx @@ -11,6 +11,7 @@ import { import { useFormContext, useWatch } from "react-hook-form" import { useIntl } from "react-intl" +import SkeletonShimmer from "@/components/SkeletonShimmer" import Caption from "@/components/TempDesignSystem/Text/Caption" import Input from "../Input" @@ -203,3 +204,18 @@ export default function Search({ locations }: SearchProps) { ) } + +export function SearchSkeleton() { + return ( +
    +
    +
    + +
    + +
    + + ) +} diff --git a/components/Forms/BookingWidget/FormContent/Voucher/index.tsx b/components/Forms/BookingWidget/FormContent/Voucher/index.tsx index dce678dda..abb770413 100644 --- a/components/Forms/BookingWidget/FormContent/Voucher/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Voucher/index.tsx @@ -1,4 +1,5 @@ "use client" +import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" @@ -78,3 +79,54 @@ export default function Voucher() { ) } + +export function VoucherSkeleton() { + const intl = useIntl() + + const vouchers = intl.formatMessage({ id: "Code / Voucher" }) + const useVouchers = intl.formatMessage({ id: "Use code/voucher" }) + const addVouchers = intl.formatMessage({ id: "Add code" }) + const bonus = intl.formatMessage({ id: "Use bonus cheque" }) + const reward = intl.formatMessage({ id: "Book reward night" }) + + const form = useForm() + + return ( + +
    +
    +
    + {/* Out of scope for this release */} + + + +
    +
    + +
    + + +
    + +
    + + +
    + +
    + + + + + + ) +} diff --git a/components/Header/MainMenu/MyPagesMenu/index.tsx b/components/Header/MainMenu/MyPagesMenu/index.tsx index 86f03e0ff..ed2fcf949 100644 --- a/components/Header/MainMenu/MyPagesMenu/index.tsx +++ b/components/Header/MainMenu/MyPagesMenu/index.tsx @@ -6,6 +6,7 @@ import { useIntl } from "react-intl" import useDropdownStore from "@/stores/main-menu" import { ChevronDownSmallIcon } from "@/components/Icons" +import SkeletonShimmer from "@/components/SkeletonShimmer" import Body from "@/components/TempDesignSystem/Text/Body" import useClickOutside from "@/hooks/useClickOutside" import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" @@ -73,3 +74,13 @@ export default function MyPagesMenu({ ) } + +export function MyPagesMenuSkeleton() { + return ( + + + + + + ) +} diff --git a/components/Header/MainMenu/MyPagesMenuWrapper/index.tsx b/components/Header/MainMenu/MyPagesMenuWrapper/index.tsx index c4e7fc761..af4a24288 100644 --- a/components/Header/MainMenu/MyPagesMenuWrapper/index.tsx +++ b/components/Header/MainMenu/MyPagesMenuWrapper/index.tsx @@ -10,7 +10,7 @@ import LoginButton from "@/components/LoginButton" import { getIntl } from "@/i18n" import Avatar from "../Avatar" -import MyPagesMenu from "../MyPagesMenu" +import MyPagesMenu, { MyPagesMenuSkeleton } from "../MyPagesMenu" import MyPagesMobileMenu from "../MyPagesMobileMenu" import styles from "./myPagesMenuWrapper.module.css" @@ -62,3 +62,12 @@ export default async function MyPagesMenuWrapper() { ) } + +export function MyPagesMenuWrapperSkeleton() { + return ( +
    + + {/* */} +
    + ) +} diff --git a/components/Header/MainMenu/index.tsx b/components/Header/MainMenu/index.tsx index ff3c8c514..fd089df3e 100644 --- a/components/Header/MainMenu/index.tsx +++ b/components/Header/MainMenu/index.tsx @@ -6,7 +6,9 @@ import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" import MobileMenuWrapper from "./MobileMenuWrapper" -import MyPagesMenuWrapper from "./MyPagesMenuWrapper" +import MyPagesMenuWrapper, { + MyPagesMenuWrapperSkeleton, +} from "./MyPagesMenuWrapper" import NavigationMenu from "./NavigationMenu" import styles from "./mainMenu.module.css" @@ -32,7 +34,7 @@ export default async function MainMenu() { - + }> diff --git a/components/SkeletonShimmer/index.tsx b/components/SkeletonShimmer/index.tsx new file mode 100644 index 000000000..68586e3ca --- /dev/null +++ b/components/SkeletonShimmer/index.tsx @@ -0,0 +1,19 @@ +import styles from "./skeleton.module.css" + +export default function SkeletonShimmer({ + height, + width, +}: { + height?: string + width?: string +}) { + return ( +
    + ) +} diff --git a/components/SkeletonShimmer/skeleton.module.css b/components/SkeletonShimmer/skeleton.module.css new file mode 100644 index 000000000..36d399ef1 --- /dev/null +++ b/components/SkeletonShimmer/skeleton.module.css @@ -0,0 +1,29 @@ +.shimmer { + background-color: hsla(0, 0%, 85%, 0.5); + position: relative; + overflow: hidden; + border-radius: 4px; + min-height: 1em; +} +.shimmer::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + transform: translateX(-100%); + background-image: linear-gradient( + 90deg, + rgba(255, 255, 255, 0) 0, + rgba(255, 255, 255, 0.2) 20%, + rgba(255, 255, 255, 0.5) 60%, + rgba(255, 255, 255, 0) 100% + ); + animation: shimmer 3s infinite; + content: ""; +} +@keyframes shimmer { + 100% { + transform: translateX(100%); + } +} From 16882fc20a4905e9f3d69217924468beb6025798 Mon Sep 17 00:00:00 2001 From: Anton Gunnarsson Date: Thu, 14 Nov 2024 13:25:33 +0100 Subject: [PATCH 53/98] Skeleton loader for footer --- app/[lang]/(live)/@footer/loading.tsx | 10 ++++- components/Footer/Details/index.tsx | 38 ++++++++++++++++ .../Footer/Navigation/MainNav/index.tsx | 22 +++++++++ .../Footer/Navigation/SecondaryNav/index.tsx | 45 ++++++++++++++++++- components/Footer/Navigation/index.tsx | 16 ++++++- components/SkeletonShimmer/index.tsx | 4 +- .../SkeletonShimmer/skeleton.module.css | 30 +++++++++---- 7 files changed, 151 insertions(+), 14 deletions(-) diff --git a/app/[lang]/(live)/@footer/loading.tsx b/app/[lang]/(live)/@footer/loading.tsx index 029d8ce71..9078cc122 100644 --- a/app/[lang]/(live)/@footer/loading.tsx +++ b/app/[lang]/(live)/@footer/loading.tsx @@ -1,11 +1,17 @@ import { env } from "@/env/server" import CurrentLoadingSpinner from "@/components/Current/LoadingSpinner" -import LoadingSpinner from "@/components/LoadingSpinner" +import { FooterDetailsSkeleton } from "@/components/Footer/Details" +import { FooterNavigationSkeleton } from "@/components/Footer/Navigation" export default function LoadingFooter() { if (env.HIDE_FOR_NEXT_RELEASE) { return } - return + return ( +
    + + +
    + ) } diff --git a/components/Footer/Details/index.tsx b/components/Footer/Details/index.tsx index 5cb8bbf4b..b95c20cca 100644 --- a/components/Footer/Details/index.tsx +++ b/components/Footer/Details/index.tsx @@ -3,6 +3,7 @@ import { getFooter, getLanguageSwitcher } from "@/lib/trpc/memoizedRequests" import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name" import Image from "@/components/Image" import LanguageSwitcher from "@/components/LanguageSwitcher" +import SkeletonShimmer from "@/components/SkeletonShimmer" import Link from "@/components/TempDesignSystem/Link" import Footnote from "@/components/TempDesignSystem/Text/Footnote" import { getIntl } from "@/i18n" @@ -92,3 +93,40 @@ export default async function FooterDetails() { ) } + +export async function FooterDetailsSkeleton() { + const lang = getLang() + const intl = await getIntl() + const currentYear = new Date().getFullYear() + + return ( +
    +
    + + + + +
    +
    +
    + + © {currentYear}{" "} + {intl.formatMessage({ id: "Copyright all rights reserved" })} + +
    +
    + +
    +
    +
    + ) +} diff --git a/components/Footer/Navigation/MainNav/index.tsx b/components/Footer/Navigation/MainNav/index.tsx index 15b42dbc1..11057af43 100644 --- a/components/Footer/Navigation/MainNav/index.tsx +++ b/components/Footer/Navigation/MainNav/index.tsx @@ -1,4 +1,5 @@ import { ArrowRightIcon } from "@/components/Icons" +import SkeletonShimmer from "@/components/SkeletonShimmer" import Link from "@/components/TempDesignSystem/Link" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" @@ -30,3 +31,24 @@ export default function FooterMainNav({ mainLinks }: FooterMainNavProps) { ) } + +export function FooterMainNavSkeleton() { + const items = Array.from({ length: 4 }).map((_, i) => i) + + return ( + + ) +} diff --git a/components/Footer/Navigation/SecondaryNav/index.tsx b/components/Footer/Navigation/SecondaryNav/index.tsx index 6f9f9b0b3..1af85c16c 100644 --- a/components/Footer/Navigation/SecondaryNav/index.tsx +++ b/components/Footer/Navigation/SecondaryNav/index.tsx @@ -1,6 +1,6 @@ import Image from "@/components/Image" +import SkeletonShimmer from "@/components/SkeletonShimmer" import Link from "@/components/TempDesignSystem/Link" -import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import { getLang } from "@/i18n/serverContext" @@ -80,3 +80,46 @@ export default function FooterSecondaryNav({
    ) } + +export function FooterSecondaryNavSkeleton() { + return ( +
    + + + +
    + ) +} diff --git a/components/Footer/Navigation/index.tsx b/components/Footer/Navigation/index.tsx index 84e96faf4..53f4c1346 100644 --- a/components/Footer/Navigation/index.tsx +++ b/components/Footer/Navigation/index.tsx @@ -1,7 +1,7 @@ import { getFooter } from "@/lib/trpc/memoizedRequests" -import FooterMainNav from "./MainNav" -import FooterSecondaryNav from "./SecondaryNav" +import FooterMainNav, { FooterMainNavSkeleton } from "./MainNav" +import FooterSecondaryNav, { FooterSecondaryNavSkeleton } from "./SecondaryNav" import styles from "./navigation.module.css" @@ -10,6 +10,7 @@ export default async function FooterNavigation() { if (!footer) { return null } + return (
    @@ -22,3 +23,14 @@ export default async function FooterNavigation() {
    ) } + +export function FooterNavigationSkeleton() { + return ( +
    +
    + + +
    +
    + ) +} diff --git a/components/SkeletonShimmer/index.tsx b/components/SkeletonShimmer/index.tsx index 68586e3ca..e4bc5364b 100644 --- a/components/SkeletonShimmer/index.tsx +++ b/components/SkeletonShimmer/index.tsx @@ -3,13 +3,15 @@ import styles from "./skeleton.module.css" export default function SkeletonShimmer({ height, width, + contrast = "light", }: { height?: string width?: string + contrast?: "light" | "dark" }) { return (
    Date: Thu, 14 Nov 2024 13:25:44 +0100 Subject: [PATCH 54/98] Fix skeleton for MyPagesMobileMenu --- components/Header/MainMenu/MyPagesMenu/index.tsx | 12 +++++++----- .../Header/MainMenu/MyPagesMenuWrapper/index.tsx | 6 ++++-- .../Header/MainMenu/MyPagesMobileMenu/index.tsx | 10 ++++++++++ 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/components/Header/MainMenu/MyPagesMenu/index.tsx b/components/Header/MainMenu/MyPagesMenu/index.tsx index ed2fcf949..a75df882f 100644 --- a/components/Header/MainMenu/MyPagesMenu/index.tsx +++ b/components/Header/MainMenu/MyPagesMenu/index.tsx @@ -77,10 +77,12 @@ export default function MyPagesMenu({ export function MyPagesMenuSkeleton() { return ( - - - - - +
    + + + + + +
    ) } diff --git a/components/Header/MainMenu/MyPagesMenuWrapper/index.tsx b/components/Header/MainMenu/MyPagesMenuWrapper/index.tsx index af4a24288..de5a5ab53 100644 --- a/components/Header/MainMenu/MyPagesMenuWrapper/index.tsx +++ b/components/Header/MainMenu/MyPagesMenuWrapper/index.tsx @@ -11,7 +11,9 @@ import { getIntl } from "@/i18n" import Avatar from "../Avatar" import MyPagesMenu, { MyPagesMenuSkeleton } from "../MyPagesMenu" -import MyPagesMobileMenu from "../MyPagesMobileMenu" +import MyPagesMobileMenu, { + MyPagesMobileMenuSkeleton, +} from "../MyPagesMobileMenu" import styles from "./myPagesMenuWrapper.module.css" @@ -67,7 +69,7 @@ export function MyPagesMenuWrapperSkeleton() { return (
    - {/* */} +
    ) } diff --git a/components/Header/MainMenu/MyPagesMobileMenu/index.tsx b/components/Header/MainMenu/MyPagesMobileMenu/index.tsx index e0711fe5c..50afd17df 100644 --- a/components/Header/MainMenu/MyPagesMobileMenu/index.tsx +++ b/components/Header/MainMenu/MyPagesMobileMenu/index.tsx @@ -76,3 +76,13 @@ export default function MyPagesMobileMenu({
    ) } + +export function MyPagesMobileMenuSkeleton() { + return ( +
    + + + +
    + ) +} From 2bfe8df741b864fbf221c0e7588e030df1598226 Mon Sep 17 00:00:00 2001 From: Anton Gunnarsson Date: Thu, 14 Nov 2024 13:25:49 +0100 Subject: [PATCH 55/98] Refactor booking widget skeleton --- app/[lang]/(live)/@bookingwidget/loading.tsx | 2 +- .../BookingWidget/BookingWidgetSkeleton.tsx | 90 ------------------- components/BookingWidget/Client.tsx | 30 +++++-- .../MobileToggleButton/index.tsx | 40 +++++++-- .../Forms/BookingWidget/FormContent/index.tsx | 56 +++++++++++- components/Forms/BookingWidget/index.tsx | 16 +++- 6 files changed, 122 insertions(+), 112 deletions(-) delete mode 100644 components/BookingWidget/BookingWidgetSkeleton.tsx diff --git a/app/[lang]/(live)/@bookingwidget/loading.tsx b/app/[lang]/(live)/@bookingwidget/loading.tsx index f62e3acb5..c45dee2ad 100644 --- a/app/[lang]/(live)/@bookingwidget/loading.tsx +++ b/app/[lang]/(live)/@bookingwidget/loading.tsx @@ -1,6 +1,6 @@ import { env } from "@/env/server" -import BookingWidgetSkeleton from "@/components/BookingWidget/BookingWidgetSkeleton" +import { BookingWidgetSkeleton } from "@/components/BookingWidget/Client" export default function LoadingBookingWidget() { if (env.HIDE_FOR_NEXT_RELEASE) { diff --git a/components/BookingWidget/BookingWidgetSkeleton.tsx b/components/BookingWidget/BookingWidgetSkeleton.tsx deleted file mode 100644 index a784f0636..000000000 --- a/components/BookingWidget/BookingWidgetSkeleton.tsx +++ /dev/null @@ -1,90 +0,0 @@ -"use client" -import { useIntl } from "react-intl" - -import { SearchIcon } from "@/components/Icons" - -import { SearchSkeleton } from "../Forms/BookingWidget/FormContent/Search" -import { VoucherSkeleton } from "../Forms/BookingWidget/FormContent/Voucher" -import { bookingWidgetVariants } from "../Forms/BookingWidget/variants" -import SkeletonShimmer from "../SkeletonShimmer" -import Button from "../TempDesignSystem/Button" -import Caption from "../TempDesignSystem/Text/Caption" - -import formStyles from "../Forms/BookingWidget/form.module.css" -import formContentStyles from "../Forms/BookingWidget/FormContent/formContent.module.css" -import widgetStyles from "./bookingWidget.module.css" - -export default function BookingWidgetSkeleton() { - const intl = useIntl() - - const classNames = bookingWidgetVariants({ - type: "full", - }) - - return ( -
    -
    - -
    -
    -
    - -
    -
    -
    - - -
    -
    - - - -
    - -
    -
    -
    - - - - - - - - - {/*
    - -
    -
    -
    - */} - - ) -} diff --git a/components/BookingWidget/Client.tsx b/components/BookingWidget/Client.tsx index 70daa6a20..f3944e7ab 100644 --- a/components/BookingWidget/Client.tsx +++ b/components/BookingWidget/Client.tsx @@ -6,14 +6,18 @@ import { FormProvider, useForm } from "react-hook-form" import { dt } from "@/lib/dt" import { StickyElementNameEnum } from "@/stores/sticky-position" -import Form from "@/components/Forms/BookingWidget" +import Form, { + BookingWidgetFormSkeleton, +} from "@/components/Forms/BookingWidget" import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema" import { CloseLargeIcon } from "@/components/Icons" import useStickyPosition from "@/hooks/useStickyPosition" import { debounce } from "@/utils/debounce" import { getFormattedUrlQueryParams } from "@/utils/url" -import MobileToggleButton from "./MobileToggleButton" +import MobileToggleButton, { + MobileToggleButtonSkeleton, +} from "./MobileToggleButton" import styles from "./bookingWidget.module.css" @@ -36,7 +40,6 @@ export default function BookingWidgetClient({ name: StickyElementNameEnum.BOOKING_WIDGET, }) - const bookingWidgetSearchData: BookingWidgetSearchParams | undefined = searchParams ? (getFormattedUrlQueryParams(new URLSearchParams(searchParams), { @@ -79,9 +82,7 @@ export default function BookingWidgetClient({ const methods = useForm({ defaultValues: { search: selectedLocation?.name ?? "", - location: selectedLocation - ? JSON.stringify(selectedLocation) - : undefined, + location: selectedLocation ? JSON.stringify(selectedLocation) : undefined, date: { // 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. @@ -147,9 +148,11 @@ export default function BookingWidgetClient({ ? JSON.parse(sessionStorageSearchData) : undefined - !(selectedLocation?.name) && initialSelectedLocation?.name && + !selectedLocation?.name && + initialSelectedLocation?.name && methods.setValue("search", initialSelectedLocation.name) - !selectedLocation && sessionStorageSearchData && + !selectedLocation && + sessionStorageSearchData && methods.setValue("location", encodeURIComponent(sessionStorageSearchData)) }, [methods, selectedLocation]) @@ -173,3 +176,14 @@ export default function BookingWidgetClient({ ) } + +export function BookingWidgetSkeleton() { + return ( + <> +
    + +
    + + + ) +} diff --git a/components/BookingWidget/MobileToggleButton/index.tsx b/components/BookingWidget/MobileToggleButton/index.tsx index 80026ff93..c8ef26189 100644 --- a/components/BookingWidget/MobileToggleButton/index.tsx +++ b/components/BookingWidget/MobileToggleButton/index.tsx @@ -7,6 +7,7 @@ import { dt } from "@/lib/dt" import { StickyElementNameEnum } from "@/stores/sticky-position" import { EditIcon, SearchIcon } from "@/components/Icons" +import SkeletonShimmer from "@/components/SkeletonShimmer" import Divider from "@/components/TempDesignSystem/Divider" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" @@ -24,7 +25,6 @@ import type { Location } from "@/types/trpc/routers/hotel/locations" export default function MobileToggleButton({ openMobileSearch, }: BookingWidgetToggleButtonProps) { - const [hasMounted, setHasMounted] = useState(false) const intl = useIntl() const lang = useLang() const d = useWatch({ name: "date" }) @@ -46,14 +46,6 @@ export default function MobileToggleButton({ const selectedFromDate = dt(d.fromDate).locale(lang).format("D MMM") const selectedToDate = dt(d.toDate).locale(lang).format("D MMM") - useEffect(() => { - setHasMounted(true) - }, []) - - if (!hasMounted) { - return null - } - const locationAndDateIsSet = parsedLocation && d const totalRooms = rooms.length @@ -133,3 +125,33 @@ export default function MobileToggleButton({
    ) } + +export function MobileToggleButtonSkeleton() { + const intl = useIntl() + const bookingWidgetMobileRef = useRef(null) + useStickyPosition({ + ref: bookingWidgetMobileRef, + name: StickyElementNameEnum.BOOKING_WIDGET_MOBILE, + }) + + return ( +
    +
    +
    + + + +
    +
    + + +
    + +
    + + ) +} diff --git a/components/Forms/BookingWidget/FormContent/index.tsx b/components/Forms/BookingWidget/FormContent/index.tsx index d5dba4d47..dc7306fca 100644 --- a/components/Forms/BookingWidget/FormContent/index.tsx +++ b/components/Forms/BookingWidget/FormContent/index.tsx @@ -1,5 +1,4 @@ "use client" -import { useState } from "react" import { useWatch } from "react-hook-form" import { useIntl } from "react-intl" @@ -9,11 +8,12 @@ import DatePicker from "@/components/DatePicker" import GuestsRoomsPickerForm from "@/components/GuestsRoomsPicker" import GuestsRoomsProvider from "@/components/GuestsRoomsPicker/Provider/GuestsRoomsProvider" import { SearchIcon } from "@/components/Icons" +import SkeletonShimmer from "@/components/SkeletonShimmer" import Button from "@/components/TempDesignSystem/Button" import Caption from "@/components/TempDesignSystem/Text/Caption" -import Search from "./Search" -import Voucher from "./Voucher" +import Search, { SearchSkeleton } from "./Search" +import Voucher, { VoucherSkeleton } from "./Voucher" import styles from "./formContent.module.css" @@ -90,3 +90,53 @@ export default function FormContent({ ) } + +export function BookingWidgetFormContentSkeleton() { + const intl = useIntl() + + return ( +
    +
    +
    + +
    +
    +
    + + +
    +
    + + + +
    + +
    +
    +
    + + + + + + + ) +} diff --git a/components/Forms/BookingWidget/index.tsx b/components/Forms/BookingWidget/index.tsx index 6c78ea341..b1018368b 100644 --- a/components/Forms/BookingWidget/index.tsx +++ b/components/Forms/BookingWidget/index.tsx @@ -6,7 +6,7 @@ import { selectHotel, selectRate } from "@/constants/routes/hotelReservation" import useLang from "@/hooks/useLang" -import FormContent from "./FormContent" +import FormContent, { BookingWidgetFormContentSkeleton } from "./FormContent" import { bookingWidgetVariants } from "./variants" import styles from "./form.module.css" @@ -69,3 +69,17 @@ export default function Form({ locations, type }: BookingWidgetFormProps) { ) } + +export function BookingWidgetFormSkeleton() { + const classNames = bookingWidgetVariants({ + type: "full", + }) + + return ( +
    + + + +
    + ) +} From 4c6d72a4023b8134773b04fb2005bc0d6e660e50 Mon Sep 17 00:00:00 2001 From: Anton Gunnarsson Date: Thu, 14 Nov 2024 13:25:56 +0100 Subject: [PATCH 56/98] Set min-height on layout loaders --- app/[lang]/(live)/(protected)/my-pages/loading.tsx | 2 +- app/[lang]/(live)/(public)/loading.tsx | 2 +- app/[lang]/(live)/loading.tsx | 2 +- components/LoadingSpinner/index.tsx | 4 ++-- components/LoadingSpinner/loading.module.css | 4 ++++ 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/[lang]/(live)/(protected)/my-pages/loading.tsx b/app/[lang]/(live)/(protected)/my-pages/loading.tsx index c739b6635..92ff5739e 100644 --- a/app/[lang]/(live)/(protected)/my-pages/loading.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/loading.tsx @@ -1,5 +1,5 @@ import LoadingSpinner from "@/components/LoadingSpinner" export default function Loading() { - return + return } diff --git a/app/[lang]/(live)/(public)/loading.tsx b/app/[lang]/(live)/(public)/loading.tsx index c739b6635..92ff5739e 100644 --- a/app/[lang]/(live)/(public)/loading.tsx +++ b/app/[lang]/(live)/(public)/loading.tsx @@ -1,5 +1,5 @@ import LoadingSpinner from "@/components/LoadingSpinner" export default function Loading() { - return + return } diff --git a/app/[lang]/(live)/loading.tsx b/app/[lang]/(live)/loading.tsx index c739b6635..92ff5739e 100644 --- a/app/[lang]/(live)/loading.tsx +++ b/app/[lang]/(live)/loading.tsx @@ -1,5 +1,5 @@ import LoadingSpinner from "@/components/LoadingSpinner" export default function Loading() { - return + return } diff --git a/components/LoadingSpinner/index.tsx b/components/LoadingSpinner/index.tsx index efcd4d5a9..93c87d3c1 100644 --- a/components/LoadingSpinner/index.tsx +++ b/components/LoadingSpinner/index.tsx @@ -1,8 +1,8 @@ import styles from "./loading.module.css" -export default function LoadingSpinner() { +export default function LoadingSpinner({ fullPage }: { fullPage?: boolean }) { return ( -
    +
    diff --git a/components/LoadingSpinner/loading.module.css b/components/LoadingSpinner/loading.module.css index ae97df077..5c9caa634 100644 --- a/components/LoadingSpinner/loading.module.css +++ b/components/LoadingSpinner/loading.module.css @@ -5,6 +5,10 @@ height: 100px; } +.fullPage { + min-height: 100dvh; +} + .spinner { display: inline-block; position: relative; From ef825d85aadabe6fcdb9a48607478578239b46fb Mon Sep 17 00:00:00 2001 From: Anton Gunnarsson Date: Thu, 14 Nov 2024 13:44:58 +0100 Subject: [PATCH 57/98] Add skeleton loader for MobileMenu --- components/Header/MainMenu/MobileMenu/index.tsx | 17 +++++++++++++++++ components/Header/MainMenu/index.tsx | 3 ++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/components/Header/MainMenu/MobileMenu/index.tsx b/components/Header/MainMenu/MobileMenu/index.tsx index 621529b0f..d5c2063a0 100644 --- a/components/Header/MainMenu/MobileMenu/index.tsx +++ b/components/Header/MainMenu/MobileMenu/index.tsx @@ -93,3 +93,20 @@ export default function MobileMenu({ ) } + +export function MobileMenuSkeleton() { + const intl = useIntl() + + return ( + + ) +} diff --git a/components/Header/MainMenu/index.tsx b/components/Header/MainMenu/index.tsx index fd089df3e..9f49a09d9 100644 --- a/components/Header/MainMenu/index.tsx +++ b/components/Header/MainMenu/index.tsx @@ -5,6 +5,7 @@ import Image from "@/components/Image" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" +import { MobileMenuSkeleton } from "./MobileMenu" import MobileMenuWrapper from "./MobileMenuWrapper" import MyPagesMenuWrapper, { MyPagesMenuWrapperSkeleton, @@ -37,7 +38,7 @@ export default async function MainMenu() { }> - + }> From d4afa54934eec0ca450a0df60ed7fb4104e10eb4 Mon Sep 17 00:00:00 2001 From: Anton Gunnarsson Date: Thu, 14 Nov 2024 13:48:48 +0100 Subject: [PATCH 58/98] Add skeleton for TopMenu --- components/Header/TopMenu/index.tsx | 13 +++++++++++++ components/Header/index.tsx | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/components/Header/TopMenu/index.tsx b/components/Header/TopMenu/index.tsx index 1fed5161e..097430b32 100644 --- a/components/Header/TopMenu/index.tsx +++ b/components/Header/TopMenu/index.tsx @@ -5,6 +5,7 @@ import { } from "@/lib/trpc/memoizedRequests" import LanguageSwitcher from "@/components/LanguageSwitcher" +import SkeletonShimmer from "@/components/SkeletonShimmer" import Caption from "@/components/TempDesignSystem/Text/Caption" import { getIntl } from "@/i18n" @@ -44,3 +45,15 @@ export default async function TopMenu() {
    ) } + +export function TopMenuSkeleton() { + return ( +
    +
    +
    + +
    +
    +
    + ) +} diff --git a/components/Header/index.tsx b/components/Header/index.tsx index 2d5fde2ad..9a71d45cf 100644 --- a/components/Header/index.tsx +++ b/components/Header/index.tsx @@ -7,7 +7,7 @@ import { } from "@/lib/trpc/memoizedRequests" import MainMenu from "./MainMenu" -import TopMenu from "./TopMenu" +import TopMenu, { TopMenuSkeleton } from "./TopMenu" import styles from "./header.module.css" @@ -18,7 +18,7 @@ export default function Header() { return (
    - + }> From 446e1c7e51d9c2a88486e4aa96ad476dbd0d6632 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Thu, 14 Nov 2024 14:10:56 +0100 Subject: [PATCH 59/98] fix: update state based on breakpoint --- components/GuestsRoomsPicker/index.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/components/GuestsRoomsPicker/index.tsx b/components/GuestsRoomsPicker/index.tsx index 810394195..2ce978b39 100644 --- a/components/GuestsRoomsPicker/index.tsx +++ b/components/GuestsRoomsPicker/index.tsx @@ -1,5 +1,6 @@ "use client" +import { useEffect, useState } from "react" import { Button, Dialog, @@ -23,7 +24,8 @@ export default function GuestsRoomsPickerForm() { const { watch } = useFormContext() const rooms = watch("rooms") as GuestsRoom[] - const isDesktop = useMediaQuery("(min-width: 1367px)") + const checkIsDesktop = useMediaQuery("(min-width: 1367px)") + const [isDesktop, setIsDesktop] = useState(true) const htmlElement = typeof window !== "undefined" ? document.querySelector("body") : null @@ -40,6 +42,10 @@ export default function GuestsRoomsPickerForm() { } } + useEffect(() => { + setIsDesktop(checkIsDesktop) + }, [checkIsDesktop]) + return isDesktop ? ( From 354e4556847bb01ead4bc6b61df5654223603661 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 14 Nov 2024 10:23:06 +0100 Subject: [PATCH 60/98] feat(SW-892): Moved Pet Room Info Tooltip inside filter button --- .../HotelReservation/SelectRate/RoomFilter/index.tsx | 8 +++++--- .../TempDesignSystem/Form/FilterChip/_Chip/index.tsx | 9 ++++++++- types/components/form/filterChip.ts | 1 + 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomFilter/index.tsx b/components/HotelReservation/SelectRate/RoomFilter/index.tsx index 9869d355b..fb41f3cc7 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/index.tsx +++ b/components/HotelReservation/SelectRate/RoomFilter/index.tsx @@ -111,11 +111,13 @@ export default function RoomFilter({ } selected={getValues(option.code)} Icon={getIconForFeatureCode(option.code)} + tooltipText={ + option.code === RoomPackageCodeEnum.PET_ROOM + ? tooltipText + : undefined + } /> ))} - - -
    diff --git a/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx b/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx index 528469df1..d1f935fa2 100644 --- a/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx +++ b/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx @@ -1,8 +1,9 @@ import { useMemo } from "react" import { useFormContext } from "react-hook-form" -import { HeartIcon } from "@/components/Icons" +import { HeartIcon, InfoCircleIcon } from "@/components/Icons" import Caption from "@/components/TempDesignSystem/Text/Caption" +import { Tooltip } from "@/components/TempDesignSystem/Tooltip" import styles from "./chip.module.css" @@ -19,6 +20,7 @@ export default function FilterChip({ value, selected, disabled, + tooltipText, }: FilterChipProps) { const { register } = useFormContext() @@ -43,6 +45,11 @@ export default function FilterChip({
    + {tooltipText && ( + + + + )} From 8aa615dfc78fb3baabab12e613e86571c0f53ed1 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 14 Nov 2024 10:53:16 +0100 Subject: [PATCH 61/98] feat(SW-892) Fixed bug with summary not adding pet charge on filter --- .../(standard)/select-rate/page.tsx | 2 +- .../RoomSelection/RoomCard/index.tsx | 2 +- .../SelectRate/RoomSelection/index.tsx | 4 +- .../SelectRate/Rooms/index.tsx | 37 +++++++++++++++++-- .../selectRate/roomSelection.ts | 10 +++++ 5 files changed, 47 insertions(+), 8 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index 816c937ae..dc5c7a6b5 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -18,7 +18,7 @@ import { setLang } from "@/i18n/serverContext" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" -import { LangParams, PageArgs } from "@/types/params" +import type { LangParams, PageArgs } from "@/types/params" export default async function SelectRatePage({ params, diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index 70f1c5aa6..4f3b398db 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -66,7 +66,7 @@ export default function RoomCard({ (room) => room.name === roomConfiguration.roomType ) - const { roomSize, occupancy, descriptions, images } = selectedRoom || {} + const { roomSize, occupancy, images } = selectedRoom || {} const mainImage = images?.[0] const freeCancelation = intl.formatMessage({ id: "Free cancellation" }) diff --git a/components/HotelReservation/SelectRate/RoomSelection/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/index.tsx index 89c4dc9a2..ceb86c5f3 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/index.tsx @@ -17,9 +17,9 @@ export default function RoomSelection({ user, packages, selectedPackages, + setRateSummary, + rateSummary, }: RoomSelectionProps) { - const [rateSummary, setRateSummary] = useState(null) - const router = useRouter() const searchParams = useSearchParams() const isUserLoggedIn = !!user diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx index 8e56555a2..abd5b2f86 100644 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -12,7 +12,8 @@ import { RoomPackageCodeEnum, type RoomPackageCodes, } from "@/types/components/hotelReservation/selectRate/roomFilter" -import type { RoomSelectionProps } 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 { RoomConfiguration, RoomsAvailability, @@ -23,10 +24,10 @@ export default function Rooms({ roomCategories = [], user, packages, -}: Omit) { +}: SelectRateProps) { const visibleRooms: RoomConfiguration[] = filterDuplicateRoomTypesByLowestPrice(roomsAvailability.roomConfigurations) - + const [rateSummary, setRateSummary] = useState(null) const [rooms, setRooms] = useState({ ...roomsAvailability, roomConfigurations: visibleRooms, @@ -48,6 +49,14 @@ export default function Rooms({ ...roomsAvailability, roomConfigurations: visibleRooms, }) + + if (!!rateSummary) { + setRateSummary({ + ...rateSummary, + features: [], + }) + } + return } @@ -57,8 +66,26 @@ export default function Rooms({ ) ) setRooms({ ...roomsAvailability, roomConfigurations: filteredRooms }) + + const petRoomPackage = + (filteredPackages.includes(RoomPackageCodeEnum.PET_ROOM) && + packages.find((pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM)) || + undefined + + const features = filteredRooms.find((room) => + room.features.some( + (feature) => feature.code === RoomPackageCodeEnum.PET_ROOM + ) + )?.features + + if (!!rateSummary) { + setRateSummary({ + ...rateSummary, + features: petRoomPackage && features ? features : [], + }) + } }, - [roomsAvailability, visibleRooms] + [roomsAvailability, visibleRooms, rateSummary, packages] ) return ( @@ -74,6 +101,8 @@ export default function Rooms({ user={user} packages={packages} selectedPackages={selectedPackages} + setRateSummary={setRateSummary} + rateSummary={rateSummary} /> ) diff --git a/types/components/hotelReservation/selectRate/roomSelection.ts b/types/components/hotelReservation/selectRate/roomSelection.ts index 3e3a6117e..63b17ed6f 100644 --- a/types/components/hotelReservation/selectRate/roomSelection.ts +++ b/types/components/hotelReservation/selectRate/roomSelection.ts @@ -2,6 +2,7 @@ import type { RoomData } from "@/types/hotel" import type { SafeUser } from "@/types/user" import type { RoomsAvailability } from "@/server/routers/hotels/output" import type { RoomPackageCodes, RoomPackageData } from "./roomFilter" +import type { Rate } from "./selectRate" export interface RoomSelectionProps { roomsAvailability: RoomsAvailability @@ -9,4 +10,13 @@ export interface RoomSelectionProps { user: SafeUser packages: RoomPackageData selectedPackages: RoomPackageCodes[] + setRateSummary: (rateSummary: Rate) => void + rateSummary: Rate | null +} + +export interface SelectRateProps { + roomsAvailability: RoomsAvailability + roomCategories: RoomData[] + user: SafeUser + packages: RoomPackageData } From e12185b2e8737b5dc77d0997c18de03130e5a63a Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 14 Nov 2024 10:53:42 +0100 Subject: [PATCH 62/98] feat(SW-892) Fixed issue with decimals on price --- .../FlexibilityOption/PriceList/utils.ts | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/utils.ts b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/utils.ts index 4043aa652..95825f585 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/utils.ts +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/utils.ts @@ -11,30 +11,38 @@ export function calculatePricesPerNight({ }: CalculatePricesPerNightProps) { const totalPublicLocalPricePerNight = publicLocalPrice ? petRoomLocalPrice - ? Number(publicLocalPrice.pricePerNight) + - Number(petRoomLocalPrice.price) / nights - : Number(publicLocalPrice.pricePerNight) + ? Math.floor( + Number(publicLocalPrice.pricePerNight) + + Number(petRoomLocalPrice.price) / nights + ) + : Math.floor(Number(publicLocalPrice.pricePerNight)) : undefined const totalMemberLocalPricePerNight = memberLocalPrice ? petRoomLocalPrice - ? Number(memberLocalPrice.pricePerNight) + - Number(petRoomLocalPrice.price) / nights - : Number(memberLocalPrice.pricePerNight) + ? Math.floor( + Number(memberLocalPrice.pricePerNight) + + Number(petRoomLocalPrice.price) / nights + ) + : Math.floor(Number(memberLocalPrice.pricePerNight)) : undefined const totalPublicRequestedPricePerNight = publicRequestedPrice ? petRoomRequestedPrice - ? Number(publicRequestedPrice.pricePerNight) + - Number(petRoomRequestedPrice.price) / nights - : Number(publicRequestedPrice.pricePerNight) + ? Math.floor( + Number(publicRequestedPrice.pricePerNight) + + Number(petRoomRequestedPrice.price) / nights + ) + : Math.floor(Number(publicRequestedPrice.pricePerNight)) : undefined const totalMemberRequestedPricePerNight = memberRequestedPrice ? petRoomRequestedPrice - ? Number(memberRequestedPrice.pricePerNight) + - Number(petRoomRequestedPrice.price) / nights - : Number(memberRequestedPrice.pricePerNight) + ? Math.floor( + Number(memberRequestedPrice.pricePerNight) + + Number(petRoomRequestedPrice.price) / nights + ) + : Math.floor(Number(memberRequestedPrice.pricePerNight)) : undefined return { From 87713d133222b3df2fc4ba8f80ab86a6a3bfc48f Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 14 Nov 2024 11:00:50 +0100 Subject: [PATCH 63/98] feat(SW-892) Fixed active state on info icon --- components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx | 2 +- components/TempDesignSystem/Tooltip/tooltip.module.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx b/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx index d1f935fa2..2d11e1173 100644 --- a/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx +++ b/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx @@ -47,7 +47,7 @@ export default function FilterChip({ {tooltipText && ( - + )} Date: Thu, 14 Nov 2024 14:52:30 +0100 Subject: [PATCH 64/98] feat(SW-892) Show toolTip on button hover --- .../SelectRate/RoomFilter/index.tsx | 52 ++++++++++++------- .../Form/FilterChip/_Chip/index.tsx | 11 ++-- types/components/form/filterChip.ts | 2 +- 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomFilter/index.tsx b/components/HotelReservation/SelectRate/RoomFilter/index.tsx index fb41f3cc7..a07e8353f 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/index.tsx +++ b/components/HotelReservation/SelectRate/RoomFilter/index.tsx @@ -98,26 +98,38 @@ export default function RoomFilter({
    - {filterOptions.map((option) => ( - - ))} + {filterOptions.map((option) => { + const { code, description } = option + const isPetRoom = code === RoomPackageCodeEnum.PET_ROOM + const isAllergyRoom = code === RoomPackageCodeEnum.ALLERGY_ROOM + const isDisabled = + (isAllergyRoom && petFriendly) || (isPetRoom && allergyFriendly) + + const checkboxChip = ( + + ) + + return isPetRoom ? ( + + {checkboxChip} + + ) : ( + checkboxChip + ) + })}
    diff --git a/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx b/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx index 2d11e1173..12992bbc4 100644 --- a/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx +++ b/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx @@ -3,7 +3,6 @@ import { useFormContext } from "react-hook-form" import { HeartIcon, InfoCircleIcon } from "@/components/Icons" import Caption from "@/components/TempDesignSystem/Text/Caption" -import { Tooltip } from "@/components/TempDesignSystem/Tooltip" import styles from "./chip.module.css" @@ -20,7 +19,7 @@ export default function FilterChip({ value, selected, disabled, - tooltipText, + hasTooltip, }: FilterChipProps) { const { register } = useFormContext() @@ -45,11 +44,11 @@ export default function FilterChip({
    - {tooltipText && ( - - - + + {hasTooltip && ( + )} + From f3037a1b3debcd63084bf8794ec1d777bb3014e1 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 14 Nov 2024 10:01:59 +0100 Subject: [PATCH 65/98] feat(SW-827): Added bed types and icons --- components/Icons/BedSingle.tsx | 29 +++++++++++++++ components/Icons/KingBedSmall.tsx | 29 +++++++++++++++ components/Icons/index.tsx | 2 + components/SidePeeks/RoomSidePeek/bedIcon.ts | 37 +++++++++++++++++++ components/SidePeeks/RoomSidePeek/index.tsx | 28 ++++++++++---- .../RoomSidePeek/roomSidePeek.module.css | 6 +++ 6 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 components/Icons/BedSingle.tsx create mode 100644 components/Icons/KingBedSmall.tsx create mode 100644 components/SidePeeks/RoomSidePeek/bedIcon.ts diff --git a/components/Icons/BedSingle.tsx b/components/Icons/BedSingle.tsx new file mode 100644 index 000000000..0e9aaa68f --- /dev/null +++ b/components/Icons/BedSingle.tsx @@ -0,0 +1,29 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function BedSingleIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + ) +} diff --git a/components/Icons/KingBedSmall.tsx b/components/Icons/KingBedSmall.tsx new file mode 100644 index 000000000..711ce0f12 --- /dev/null +++ b/components/Icons/KingBedSmall.tsx @@ -0,0 +1,29 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function KingBedSmallIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + ) +} diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index fc6e5c069..1bc195717 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -9,6 +9,7 @@ export { default as ArrowRightIcon } from "./ArrowRight" export { default as BarIcon } from "./Bar" export { default as BathtubIcon } from "./Bathtub" export { default as BedDoubleIcon } from "./BedDouble" +export { default as BedSingleIcon } from "./BedSingle" export { default as BikingIcon } from "./Biking" export { default as BreakfastIcon } from "./Breakfast" export { default as BusinessIcon } from "./Business" @@ -77,6 +78,7 @@ export { default as IronIcon } from "./Iron" export { default as KayakingIcon } from "./Kayaking" export { default as KettleIcon } from "./Kettle" export { default as KingBedIcon } from "./KingBed" +export { default as KingBedSmallIcon } from "./KingBedSmall" export { default as LampIcon } from "./Lamp" export { default as LaundryMachineIcon } from "./LaundryMachine" export { default as LocalBarIcon } from "./LocalBar" diff --git a/components/SidePeeks/RoomSidePeek/bedIcon.ts b/components/SidePeeks/RoomSidePeek/bedIcon.ts new file mode 100644 index 000000000..41947bd77 --- /dev/null +++ b/components/SidePeeks/RoomSidePeek/bedIcon.ts @@ -0,0 +1,37 @@ +import { FC } from "react" + +import { + BedDoubleIcon, + BedSingleIcon, + KingBedSmallIcon, +} from "@/components/Icons" + +import { IconProps } from "@/types/components/icon" + +export function getBedIcon(name: string): FC | null { + const iconMappings = [ + { + icon: BedDoubleIcon, + texts: ["Queen"], + }, + { + icon: KingBedSmallIcon, + texts: ["King"], + }, + { + icon: KingBedSmallIcon, + texts: ["CustomOccupancy"], + }, + { + icon: BedSingleIcon, + texts: ["Twin"], + }, + { + icon: BedSingleIcon, + texts: ["Single"], + }, + ] + + const icon = iconMappings.find((icon) => icon.texts.includes(name)) + return icon ? icon.icon : BedSingleIcon +} diff --git a/components/SidePeeks/RoomSidePeek/index.tsx b/components/SidePeeks/RoomSidePeek/index.tsx index 34fc3afca..da46a6f42 100644 --- a/components/SidePeeks/RoomSidePeek/index.tsx +++ b/components/SidePeeks/RoomSidePeek/index.tsx @@ -1,11 +1,11 @@ import { useIntl } from "react-intl" import ImageGallery from "@/components/ImageGallery" -import Button from "@/components/TempDesignSystem/Button" import SidePeek from "@/components/TempDesignSystem/SidePeek" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import { getBedIcon } from "./bedIcon" import { getFacilityIcon } from "./facilityIcon" import styles from "./roomSidePeek.module.css" @@ -79,15 +79,27 @@ export default function RoomSidePeek({ {intl.formatMessage({ id: "booking.basedOnAvailability" })} - {/* TODO: Get data for bed options */} +
      + {room.roomTypes.map((roomType) => { + const BedIcon = getBedIcon(roomType.mainBed.type) + return ( +
    • + {BedIcon && ( + + )} + + {roomType.mainBed.description} + +
    • + ) + })} +
    -
    - -
    ) } diff --git a/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css b/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css index c76c57877..2e1fb5e6f 100644 --- a/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css +++ b/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css @@ -44,6 +44,12 @@ margin-bottom: var(--Spacing-x-half); } +.bedOptions li { + display: flex; + gap: var(--Spacing-x1); + margin-bottom: var(--Spacing-x-half); +} + .noIcon { margin-left: var(--Spacing-x4); } From 38c48006bec2028f0685852e2e63d3823a1f5132 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 14 Nov 2024 10:02:36 +0100 Subject: [PATCH 66/98] feat(827): Added fallback on image if is error --- components/ImageGallery/imageGallery.module.css | 9 +++++---- components/ImageGallery/index.tsx | 4 +++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/components/ImageGallery/imageGallery.module.css b/components/ImageGallery/imageGallery.module.css index 2ffdcf595..050f9be3b 100644 --- a/components/ImageGallery/imageGallery.module.css +++ b/components/ImageGallery/imageGallery.module.css @@ -27,16 +27,17 @@ .imagePlaceholder { height: 100%; min-height: 190px; + aspect-ratio: 16/9; width: 100%; background-color: #fff; background-image: linear-gradient(45deg, #000000 25%, transparent 25%), linear-gradient(-45deg, #000000 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #000000 75%), linear-gradient(-45deg, transparent 75%, #000000 75%); - background-size: 160px 160px; + background-size: 180px 180px; background-position: 0 0, - 0 80px, - 80px -80px, - -80px 0; + 0 90px, + 90px -90px, + -90px 0; } diff --git a/components/ImageGallery/index.tsx b/components/ImageGallery/index.tsx index eaeee684b..3fc10448b 100644 --- a/components/ImageGallery/index.tsx +++ b/components/ImageGallery/index.tsx @@ -20,9 +20,10 @@ export default function ImageGallery({ }: ImageGalleryProps) { const intl = useIntl() const [lightboxIsOpen, setLightboxIsOpen] = useState(false) + const [imageError, setImageError] = useState(false) const imageProps = fill ? { fill } : { height, width: height * 1.5 } - if (!images || images.length === 0) { + if (!images || images.length === 0 || imageError) { return
    } @@ -38,6 +39,7 @@ export default function ImageGallery({ className={styles.image} src={images[0].imageSizes.medium} alt={images[0].metaData.altText} + onError={() => setImageError(true)} {...imageProps} />
    From b9ff5dc920990257f77103e05e87984d4009b219 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 14 Nov 2024 10:02:54 +0100 Subject: [PATCH 67/98] feat(SW-827): updated sidepeek width after design --- components/TempDesignSystem/SidePeek/sidePeek.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/TempDesignSystem/SidePeek/sidePeek.module.css b/components/TempDesignSystem/SidePeek/sidePeek.module.css index 68f588ab3..5fb93dc80 100644 --- a/components/TempDesignSystem/SidePeek/sidePeek.module.css +++ b/components/TempDesignSystem/SidePeek/sidePeek.module.css @@ -1,5 +1,5 @@ .modal { - --sidepeek-desktop-width: 600px; + --sidepeek-desktop-width: 560px; } @keyframes slide-in { from { From dc988e32fa9acb69c1ea3a7271fd5a825935cdfb Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 14 Nov 2024 10:10:42 +0100 Subject: [PATCH 68/98] feat(SW-827): Added type --- components/SidePeeks/RoomSidePeek/bedIcon.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/SidePeeks/RoomSidePeek/bedIcon.ts b/components/SidePeeks/RoomSidePeek/bedIcon.ts index 41947bd77..389045159 100644 --- a/components/SidePeeks/RoomSidePeek/bedIcon.ts +++ b/components/SidePeeks/RoomSidePeek/bedIcon.ts @@ -6,7 +6,7 @@ import { KingBedSmallIcon, } from "@/components/Icons" -import { IconProps } from "@/types/components/icon" +import type { IconProps } from "@/types/components/icon" export function getBedIcon(name: string): FC | null { const iconMappings = [ From b005af5bf1ffbc5e0d83e45210cee0bd3af5855a Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 14 Nov 2024 11:05:36 +0100 Subject: [PATCH 69/98] feat(SW-827): Fixed fillRule and clipRule --- components/Icons/KingBedSmall.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/Icons/KingBedSmall.tsx b/components/Icons/KingBedSmall.tsx index 711ce0f12..6c3bc9be6 100644 --- a/components/Icons/KingBedSmall.tsx +++ b/components/Icons/KingBedSmall.tsx @@ -19,8 +19,8 @@ export default function KingBedSmallIcon({ {...props} > From ff1434cd45dfa39e079a54e734c61d7349efa865 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 14 Nov 2024 14:54:27 +0100 Subject: [PATCH 70/98] feat(SW-827) fixed fillRule and clipRule --- components/Icons/BedSingle.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/Icons/BedSingle.tsx b/components/Icons/BedSingle.tsx index 0e9aaa68f..9ff42333a 100644 --- a/components/Icons/BedSingle.tsx +++ b/components/Icons/BedSingle.tsx @@ -19,8 +19,8 @@ export default function BedSingleIcon({ {...props} > From d158aca6fddf4ce2ea54c0c7f824dde1940773d1 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 14 Nov 2024 16:23:44 +0100 Subject: [PATCH 71/98] feat(SW-629): Added no availabilty if no hotels are listed --- .../(standard)/select-hotel/page.module.css | 7 +++ .../(standard)/select-hotel/page.tsx | 50 ++++++++++++++----- .../HotelCardListing/index.tsx | 24 ++++----- .../SelectHotel/HotelFilter/index.tsx | 4 ++ i18n/dictionaries/da.json | 2 + i18n/dictionaries/de.json | 2 + i18n/dictionaries/en.json | 2 + i18n/dictionaries/fi.json | 2 + i18n/dictionaries/no.json | 2 + i18n/dictionaries/sv.json | 2 + 10 files changed, 72 insertions(+), 25 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css index d8e3db57e..89de83d4d 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css @@ -38,6 +38,13 @@ flex: 1; } +.hotelList { + flex: 1; + display: flex; + flex-direction: column; + gap: var(--Spacing-x3); +} + @media (min-width: 768px) { .link { display: flex; diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx index 7ddffacae..4dfefd7ac 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -17,6 +17,7 @@ import { } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { ChevronRightIcon } from "@/components/Icons" import StaticMap from "@/components/Maps/StaticMap" +import Alert from "@/components/TempDesignSystem/Alert" import Link from "@/components/TempDesignSystem/Link" import { getIntl } from "@/i18n" import { setLang } from "@/i18n/serverContext" @@ -24,6 +25,7 @@ import { setLang } from "@/i18n/serverContext" import styles from "./page.module.css" import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" +import { AlertTypeEnum } from "@/types/enums/alert" import { LangParams, PageArgs } from "@/types/params" export default async function SelectHotelPage({ @@ -69,12 +71,29 @@ export default async function SelectHotelPage({
    - + {hotels.length > 0 ? ( // TODO: Temp fix until API returns hotels that are not available + +
    + +
    + {intl.formatMessage({ id: "Show map" })} + +
    +
    + + ) : (
    -
    - {intl.formatMessage({ id: "Show map" })} - -
    - + )}
    - +
    + {!hotels.length && ( + + )} + +
    ) diff --git a/components/HotelReservation/HotelCardListing/index.tsx b/components/HotelReservation/HotelCardListing/index.tsx index 18b491a45..4ba65ed9c 100644 --- a/components/HotelReservation/HotelCardListing/index.tsx +++ b/components/HotelReservation/HotelCardListing/index.tsx @@ -71,19 +71,17 @@ export default function HotelCardListing({ return (
    - {hotels?.length ? ( - hotels.map((hotel) => ( - - )) - ) : ( - No hotels found - )} + {hotels?.length + ? hotels.map((hotel) => ( + + )) + : null}
    ) } diff --git a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx index 97096f909..df6689f2d 100644 --- a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx +++ b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx @@ -50,6 +50,10 @@ export default function HotelFilter({ filters }: HotelFiltersProps) { return () => subscription.unsubscribe() }, [handleSubmit, watch, submitFilter]) + if (!filters.facilityFilters.length && !filters.surroundingsFilters.length) { + return null + } + return (
    diff --git a/components/ContentType/HotelPage/IntroSection/index.tsx b/components/ContentType/HotelPage/IntroSection/index.tsx index 010530f74..fe2db96c0 100644 --- a/components/ContentType/HotelPage/IntroSection/index.tsx +++ b/components/ContentType/HotelPage/IntroSection/index.tsx @@ -25,7 +25,7 @@ export default async function IntroSection({ const { streetAddress, city } = address const { distanceToCentre } = location const formattedDistanceText = intl.formatMessage( - { id: "Distance to city centre" }, + { id: "Distance in km to city centre" }, { number: distanceToCentre } ) const lang = getLang() diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index e93a24ef3..451de4630 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -104,7 +104,7 @@ export default function HotelCard({ diff --git a/components/HotelReservation/HotelSelectionHeader/index.tsx b/components/HotelReservation/HotelSelectionHeader/index.tsx index 69b4134eb..4cb88f67f 100644 --- a/components/HotelReservation/HotelSelectionHeader/index.tsx +++ b/components/HotelReservation/HotelSelectionHeader/index.tsx @@ -31,7 +31,7 @@ export default function HotelSelectionHeader({ diff --git a/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css b/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css index 156874b54..c81b31cbd 100644 --- a/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css +++ b/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css @@ -3,16 +3,30 @@ display: none; } +.container form { + display: flex; + flex-direction: column; + gap: var(--Spacing-x3); +} + .facilities { font-family: var(--typography-Body-Bold-fontFamily); - margin-bottom: var(--Spacing-x3); + padding-bottom: var(--Spacing-x3); +} + +.facilities:first-of-type { + border-bottom: 1px solid var(--Base-Border-Subtle); +} + +.facilities ul { + margin-top: var(--Spacing-x2); } .filter { display: grid; grid-template-columns: repeat(2, minmax(min-content, max-content)); gap: var(--Spacing-x-one-and-half); - margin-bottom: var(--Spacing-x-one-and-half); + margin-bottom: var(--Spacing-x1); align-items: center; } diff --git a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx index 97096f909..05f3f829a 100644 --- a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx +++ b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx @@ -2,26 +2,29 @@ import { usePathname, useSearchParams } from "next/navigation" import { useCallback, useEffect } from "react" -import { useForm } from "react-hook-form" +import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" +import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import Title from "@/components/TempDesignSystem/Text/Title" + import styles from "./hotelFilter.module.css" -import { HotelFiltersProps } from "@/types/components/hotelReservation/selectHotel/hotelFilters" +import type { HotelFiltersProps } from "@/types/components/hotelReservation/selectHotel/hotelFilters" export default function HotelFilter({ filters }: HotelFiltersProps) { const intl = useIntl() const searchParams = useSearchParams() const pathname = usePathname() - const { watch, handleSubmit, getValues, register } = useForm< - Record - >({ + const methods = useForm>({ defaultValues: searchParams ?.get("filters") ?.split(",") .reduce((acc, curr) => ({ ...acc, [curr]: true }), {}), }) + const { watch, handleSubmit, getValues, register } = methods const submitFilter = useCallback(() => { const newSearchParams = new URLSearchParams(searchParams) @@ -52,41 +55,36 @@ export default function HotelFilter({ filters }: HotelFiltersProps) { return ( ) } diff --git a/components/HotelReservation/SelectHotel/HotelSorter/hotelSorter.module.css b/components/HotelReservation/SelectHotel/HotelSorter/hotelSorter.module.css new file mode 100644 index 000000000..7c991c602 --- /dev/null +++ b/components/HotelReservation/SelectHotel/HotelSorter/hotelSorter.module.css @@ -0,0 +1,3 @@ +.container { + width: 339px; +} diff --git a/components/HotelReservation/SelectHotel/HotelSorter/index.tsx b/components/HotelReservation/SelectHotel/HotelSorter/index.tsx index d18934c76..d9a364fcd 100644 --- a/components/HotelReservation/SelectHotel/HotelSorter/index.tsx +++ b/components/HotelReservation/SelectHotel/HotelSorter/index.tsx @@ -6,24 +6,19 @@ import { useIntl } from "react-intl" import Select from "@/components/TempDesignSystem/Select" +import styles from "./hotelSorter.module.css" + import { type SortItem, SortOrder, } from "@/types/components/hotelReservation/selectHotel/hotelSorter" -const sortItems: SortItem[] = [ - { label: "Distance", value: SortOrder.Distance }, - { label: "Name", value: SortOrder.Name }, - { label: "Price", value: SortOrder.Price }, - { label: "TripAdvisor rating", value: SortOrder.TripAdvisorRating }, -] - export const DEFAULT_SORT = SortOrder.Distance export default function HotelSorter() { const searchParams = useSearchParams() const pathname = usePathname() - const i18n = useIntl() + const intl = useIntl() const onSelect = useCallback( (value: string | number) => { @@ -43,14 +38,30 @@ export default function HotelSorter() { }, [pathname, searchParams] ) + const sortItems: SortItem[] = [ + { + label: intl.formatMessage({ id: "Distance to city center" }), + value: SortOrder.Distance, + }, + { label: intl.formatMessage({ id: "Name" }), value: SortOrder.Name }, + { label: intl.formatMessage({ id: "Price" }), value: SortOrder.Price }, + { + label: intl.formatMessage({ id: "TripAdvisor rating" }), + value: SortOrder.TripAdvisorRating, + }, + ] return ( - + ) } diff --git a/components/TempDesignSystem/Form/Label/label.module.css b/components/TempDesignSystem/Form/Label/label.module.css index 9d92486c3..2359414f7 100644 --- a/components/TempDesignSystem/Form/Label/label.module.css +++ b/components/TempDesignSystem/Form/Label/label.module.css @@ -18,6 +18,12 @@ span.regular { order: 1; } +span.discreet { + color: var(--Base-Text-High-contrast); + font-weight: 500; + order: unset; +} + input:active ~ .label, input:not(:placeholder-shown) ~ .label { display: block; diff --git a/components/TempDesignSystem/Form/Label/variants.ts b/components/TempDesignSystem/Form/Label/variants.ts index 3a6c3417c..0e9e79f83 100644 --- a/components/TempDesignSystem/Form/Label/variants.ts +++ b/components/TempDesignSystem/Form/Label/variants.ts @@ -7,6 +7,7 @@ export const labelVariants = cva(styles.label, { size: { small: styles.small, regular: styles.regular, + discreet: styles.discreet, }, }, defaultVariants: { diff --git a/components/TempDesignSystem/Form/SelectChevron/index.tsx b/components/TempDesignSystem/Form/SelectChevron/index.tsx index 2033283d4..d7c53c97f 100644 --- a/components/TempDesignSystem/Form/SelectChevron/index.tsx +++ b/components/TempDesignSystem/Form/SelectChevron/index.tsx @@ -2,10 +2,12 @@ import { ChevronDownIcon } from "@/components/Icons" import styles from "./chevron.module.css" -export default function SelectChevron() { +import type { IconProps } from "@/types/components/icon" + +export default function SelectChevron(props: IconProps) { return ( ) } diff --git a/components/TempDesignSystem/Select/index.tsx b/components/TempDesignSystem/Select/index.tsx index bb0f9a126..bce5132ab 100644 --- a/components/TempDesignSystem/Select/index.tsx +++ b/components/TempDesignSystem/Select/index.tsx @@ -36,6 +36,7 @@ export default function Select({ value, maxHeight, showRadioButton = false, + discreet = false, }: SelectProps) { const [rootDiv, setRootDiv] = useState(undefined) @@ -53,7 +54,7 @@ export default function Select({
    diff --git a/components/TempDesignSystem/Select/select.module.css b/components/TempDesignSystem/Select/select.module.css index 8c551b13a..f6d2dfade 100644 --- a/components/TempDesignSystem/Select/select.module.css +++ b/components/TempDesignSystem/Select/select.module.css @@ -10,11 +10,29 @@ gap: var(--Spacing-x-half); } -.select[data-focused="true"] { +.select[data-focused="true"], +.select[data-focused="true"].discreet { border: 1px solid var(--Scandic-Blue-90); outline: none; } +.select.discreet { + border: 1px solid transparent; +} + +.select.discreet .input { + background-color: unset; + color: var(--Base-Text-High-contrast); + gap: var(--Spacing-x1); +} + +.select.discreet .inputContentWrapper { + align-items: center; + justify-content: flex-end; + flex-direction: row; + font-weight: 500; +} + .input { align-items: center; background-color: var(--UI-Opacity-White-100); @@ -72,7 +90,7 @@ } .listBoxItem.showRadioButton:before { - display: flex; + flex-shrink: 0; content: ""; margin-right: var(--Spacing-x-one-and-half); background-color: white; diff --git a/components/TempDesignSystem/Select/select.ts b/components/TempDesignSystem/Select/select.ts index 0b7b7fc06..942223277 100644 --- a/components/TempDesignSystem/Select/select.ts +++ b/components/TempDesignSystem/Select/select.ts @@ -11,6 +11,7 @@ export interface SelectProps value?: string | number maxHeight?: number showRadioButton?: boolean + discreet?: boolean } export type SelectPortalContainer = HTMLDivElement | undefined diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 4486b9e51..3c8db2a65 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -98,7 +98,8 @@ "Disabled booking options text": "Koder, checks og bonusnætter er endnu ikke tilgængelige på den nye hjemmeside.", "Discard changes": "Kassér ændringer", "Discard unsaved changes?": "Slette ændringer, der ikke er gemt?", - "Distance to city centre": "{number} km til centrum", + "Distance in km to city centre": "{number} km til centrum", + "Distance to city centre": "Afstand til centrum", "Distance to hotel": "Afstand til hotel", "Do you want to start the day with Scandics famous breakfast buffé?": "Vil du starte dagen med Scandics berømte morgenbuffet?", "Done": "Færdig", @@ -120,6 +121,7 @@ "Failed to delete credit card, please try again later.": "Kunne ikke slette kreditkort. Prøv venligst igen senere.", "Fair": "Messe", "Filter": "Filter", + "Filter by": "Filtrer efter", "Find booking": "Find booking", "Find hotels": "Find hotel", "First name": "Fornavn", @@ -204,6 +206,7 @@ "My pages menu": "Mine sider menu", "My payment cards": "Mine betalingskort", "My wishes": "Mine ønsker", + "Name": "Navn", "Nearby": "I nærheden", "Nearby companies": "Nærliggende virksomheder", "New password": "Nyt kodeord", @@ -258,6 +261,7 @@ "Practical information": "Praktisk information", "Previous": "Forudgående", "Previous victories": "Tidligere sejre", + "Price": "Pris", "Price details": "Prisoplysninger", "Proceed to login": "Fortsæt til login", "Proceed to payment method": "Fortsæt til betalingsmetode", @@ -342,6 +346,7 @@ "Transaction date": "Overførselsdato", "Transactions": "Transaktioner", "Transportations": "Transport", + "TripAdvisor rating": "TripAdvisor vurdering", "Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)", "Type of bed": "Sengtype", "Type of room": "Værelsestype", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index ee34c0c1b..5a950a322 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -98,7 +98,8 @@ "Disabled booking options text": "Codes, Schecks und Bonusnächte sind auf der neuen Website noch nicht verfügbar.", "Discard changes": "Änderungen verwerfen", "Discard unsaved changes?": "Nicht gespeicherte Änderungen verwerfen?", - "Distance to city centre": "{number} km zum Stadtzentrum", + "Distance in km to city centre": "{number} km zum Stadtzentrum", + "Distance to city centre": "Entfernung zum Stadtzentrum", "Distance to hotel": "Entfernung zum Hotel", "Do you want to start the day with Scandics famous breakfast buffé?": "Möchten Sie den Tag mit Scandics berühmtem Frühstücksbuffet beginnen?", "Done": "Fertig", @@ -120,6 +121,7 @@ "Failed to delete credit card, please try again later.": "Kreditkarte konnte nicht gelöscht werden. Bitte versuchen Sie es später noch einmal.", "Fair": "Messe", "Filter": "Filter", + "Filter by": "Filtern nach", "Find booking": "Buchung finden", "Find hotels": "Hotels finden", "First name": "Vorname", @@ -202,6 +204,7 @@ "My pages menu": "Meine Seite Menü", "My payment cards": "Meine Zahlungskarten", "My wishes": "Meine Wünsche", + "Name": "Name", "Nearby": "In der Nähe", "Nearby companies": "Nahe gelegene Unternehmen", "New password": "Neues Kennwort", @@ -256,6 +259,7 @@ "Practical information": "Praktische Informationen", "Previous": "Früher", "Previous victories": "Bisherige Siege", + "Price": "Preis", "Price details": "Preisdetails", "Proceed to login": "Weiter zum Login", "Proceed to payment method": "Weiter zur Zahlungsmethode", @@ -341,6 +345,7 @@ "Transaction date": "Transaktionsdatum", "Transactions": "Transaktionen", "Transportations": "Transportmittel", + "TripAdvisor rating": "TripAdvisor-Bewertung", "Tripadvisor reviews": "{rating} ({count} Bewertungen auf Tripadvisor)", "Type of bed": "Bettentyp", "Type of room": "Zimmerart", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 6d3712067..f804092c8 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -106,7 +106,8 @@ "Disabled booking options text": "Codes, cheques and reward nights aren't available on the new website yet.", "Discard changes": "Discard changes", "Discard unsaved changes?": "Discard unsaved changes?", - "Distance to city centre": "{number} km to city centre", + "Distance in km to city centre": "{number} km to city centre", + "Distance to city centre": "Distance to city centre", "Distance to hotel": "Distance to hotel", "Do you want to start the day with Scandics famous breakfast buffé?": "Do you want to start the day with Scandics famous breakfast buffé?", "Done": "Done", @@ -129,6 +130,7 @@ "Failed to delete credit card, please try again later.": "Failed to delete credit card, please try again later.", "Fair": "Fair", "Filter": "Filter", + "Filter by": "Filter by", "Find booking": "Find booking", "Find hotels": "Find hotels", "First name": "First name", @@ -221,6 +223,7 @@ "My pages menu": "My pages menu", "My payment cards": "My payment cards", "My wishes": "My wishes", + "Name": "Name", "Nearby": "Nearby", "Nearby companies": "Nearby companies", "New password": "New password", @@ -279,6 +282,7 @@ "Practical information": "Practial information", "Previous": "Previous", "Previous victories": "Previous victories", + "Price": "Price", "Price details": "Price details", "Price excl VAT": "Price excl VAT", "Price incl VAT": "Price incl VAT", @@ -371,6 +375,7 @@ "Transaction date": "Transaction date", "Transactions": "Transactions", "Transportations": "Transportations", + "TripAdvisor rating": "TripAdvisor rating", "Tripadvisor reviews": "{rating} ({count} reviews on Tripadvisor)", "Type of bed": "Type of bed", "Type of room": "Type of room", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index a067b87f5..120689943 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -98,7 +98,8 @@ "Disabled booking options text": "Koodit, sekit ja palkintoillat eivät ole vielä saatavilla uudella verkkosivustolla.", "Discard changes": "Hylkää muutokset", "Discard unsaved changes?": "Hylkäätkö tallentamattomat muutokset?", - "Distance to city centre": "{number} km Etäisyys kaupunkiin", + "Distance in km to city centre": "{number} km Etäisyys kaupunkiin", + "Distance to city centre": "Etäisyys kaupungin keskustaan", "Distance to hotel": "Etäisyys hotelliin", "Do you want to start the day with Scandics famous breakfast buffé?": "Haluatko aloittaa päiväsi Scandicsin kuuluisalla aamiaisbuffella?", "Done": "Valmis", @@ -120,6 +121,7 @@ "Failed to delete credit card, please try again later.": "Luottokortin poistaminen epäonnistui, yritä myöhemmin uudelleen.", "Fair": "Messukeskus", "Filter": "Suodatin", + "Filter by": "Suodatusperuste", "Find booking": "Etsi varaus", "Find hotels": "Etsi hotelleja", "First name": "Etunimi", @@ -204,6 +206,7 @@ "My pages menu": "Omat sivut -valikko", "My payment cards": "Minun maksukortit", "My wishes": "Toiveeni", + "Name": "Nimi", "Nearby": "Lähistöllä", "Nearby companies": "Läheiset yritykset", "New password": "Uusi salasana", @@ -258,6 +261,7 @@ "Practical information": "Käytännön tietoa", "Previous": "Aikaisempi", "Previous victories": "Edelliset voitot", + "Price": "Hinta", "Price details": "Hintatiedot", "Proceed to login": "Jatka kirjautumiseen", "Proceed to payment method": "Siirry maksutavalle", @@ -343,6 +347,7 @@ "Transaction date": "Tapahtuman päivämäärä", "Transactions": "Tapahtumat", "Transportations": "Kuljetukset", + "TripAdvisor rating": "TripAdvisor-luokitus", "Tripadvisor reviews": "{rating} ({count} arvostelua TripAdvisorissa)", "Type of bed": "Vuodetyyppi", "Type of room": "Huonetyyppi", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 98d778f4c..45defba7a 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -97,7 +97,8 @@ "Disabled booking options text": "Koder, checks og belønningsnætter er enda ikke tilgjengelige på den nye nettsiden.", "Discard changes": "Forkaste endringer", "Discard unsaved changes?": "Forkaste endringer som ikke er lagret?", - "Distance to city centre": "{number} km til sentrum", + "Distance in km to city centre": "{number} km til sentrum", + "Distance to city centre": "Avstand til sentrum", "Distance to hotel": "Avstand til hotell", "Do you want to start the day with Scandics famous breakfast buffé?": "Vil du starte dagen med Scandics berømte frokostbuffé?", "Done": "Ferdig", @@ -119,6 +120,7 @@ "Failed to delete credit card, please try again later.": "Kunne ikke slette kredittkortet, prøv igjen senere.", "Fair": "Messe", "Filter": "Filter", + "Filter by": "Filtrer etter", "Find booking": "Finn booking", "Find hotels": "Finn hotell", "First name": "Fornavn", @@ -202,6 +204,7 @@ "My pages menu": "Mine sider-menyen", "My payment cards": "Mine betalingskort", "My wishes": "Mine ønsker", + "Name": "Navn", "Nearby": "I nærheten", "Nearby companies": "Nærliggende selskaper", "New password": "Nytt passord", @@ -256,6 +259,7 @@ "Practical information": "Praktisk informasjon", "Previous": "Tidligere", "Previous victories": "Tidligere seire", + "Price": "Pris", "Price details": "Prisdetaljer", "Proceed to login": "Fortsett til innlogging", "Proceed to payment method": "Fortsett til betalingsmetode", @@ -340,6 +344,7 @@ "Transaction date": "Transaksjonsdato", "Transactions": "Transaksjoner", "Transportations": "Transport", + "TripAdvisor rating": "TripAdvisor vurdering", "Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)", "Type of bed": "Sengtype", "Type of room": "Romtype", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 01c293426..29e6702b9 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -97,7 +97,8 @@ "Disabled booking options text": "Koder, bonuscheckar och belöningsnätter är inte tillgängliga på den nya webbplatsen än.", "Discard changes": "Ignorera ändringar", "Discard unsaved changes?": "Vill du ignorera ändringar som inte har sparats?", - "Distance to city centre": "{number} km till centrum", + "Distance in km to city centre": "{number} km till centrum", + "Distance to city centre": "Avstånd till centrum", "Distance to hotel": "Avstånd till hotell", "Do you want to start the day with Scandics famous breakfast buffé?": "Vill du starta dagen med Scandics berömda frukostbuffé?", "Done": "Klar", @@ -119,6 +120,7 @@ "Failed to delete credit card, please try again later.": "Det gick inte att ta bort kreditkortet, försök igen senare.", "Fair": "Mässa", "Filter": "Filter", + "Filter by": "Filtrera på", "Find booking": "Hitta bokning", "Find hotels": "Hitta hotell", "First name": "Förnamn", @@ -202,6 +204,7 @@ "My pages menu": "Mina sidor meny", "My payment cards": "Mina betalningskort", "My wishes": "Mina önskningar", + "Name": "Namn", "Nearby": "I närheten", "Nearby companies": "Närliggande företag", "New password": "Nytt lösenord", @@ -256,6 +259,7 @@ "Practical information": "Praktisk information", "Previous": "Föregående", "Previous victories": "Tidigare segrar", + "Price": "Pris", "Price details": "Prisdetaljer", "Proceed to login": "Fortsätt till inloggning", "Proceed to payment method": "Gå vidare till betalningsmetod", @@ -340,6 +344,7 @@ "Transaction date": "Transaktionsdatum", "Transactions": "Transaktioner", "Transportations": "Transport", + "TripAdvisor rating": "TripAdvisor-betyg", "Tripadvisor reviews": "{rating} ({count} recensioner på Tripadvisor)", "Type of bed": "Sängtyp", "Type of room": "Rumstyp", From aec6361cd77e9c94e7ec5457ce3b84c3440e5cbe Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Fri, 15 Nov 2024 09:09:38 +0100 Subject: [PATCH 74/98] fix: header in current content pages --- app/[lang]/(live-current)/layout.tsx | 1 - components/Current/Header/header.module.css | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/[lang]/(live-current)/layout.tsx b/app/[lang]/(live-current)/layout.tsx index ec3be8b3c..7bdf6d300 100644 --- a/app/[lang]/(live-current)/layout.tsx +++ b/app/[lang]/(live-current)/layout.tsx @@ -65,7 +65,6 @@ export default async function RootLayout({ {header} - {children}
    diff --git a/components/Current/Header/header.module.css b/components/Current/Header/header.module.css index 91b5581a4..90e35642b 100644 --- a/components/Current/Header/header.module.css +++ b/components/Current/Header/header.module.css @@ -1,5 +1,6 @@ .header { display: grid; + background-color: var(--Main-Grey-White); } @media screen and (max-width: 1366px) { From 8007fea61e3e0ae7b7dfbaa63a903f948cb8a2da Mon Sep 17 00:00:00 2001 From: Anton Gunnarsson Date: Fri, 15 Nov 2024 09:24:15 +0100 Subject: [PATCH 75/98] Refactor MainMenu suspense boundary --- components/Header/MainMenu/index.tsx | 40 ++++++++++++++++++---------- components/Header/index.tsx | 6 ++--- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/components/Header/MainMenu/index.tsx b/components/Header/MainMenu/index.tsx index 9f49a09d9..9fb174df2 100644 --- a/components/Header/MainMenu/index.tsx +++ b/components/Header/MainMenu/index.tsx @@ -14,23 +14,13 @@ import NavigationMenu from "./NavigationMenu" import styles from "./mainMenu.module.css" -export default async function MainMenu() { - const lang = getLang() - - const intl = await getIntl() - +export default function MainMenu() { return (
    + + )) + : null} {chosenBed ? (
    - {chosenBed.description} + {chosenBed.description}
    - - + + - + + ) } diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/cardVariants.ts b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/cardVariants.ts new file mode 100644 index 000000000..b34d00e23 --- /dev/null +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/cardVariants.ts @@ -0,0 +1,12 @@ +import { cva } from "class-variance-authority" + +import styles from "./roomCard.module.css" + +export const cardVariants = cva(styles.card, { + variants: { + availability: { + noAvailability: styles.noAvailability, + default: "", + }, + }, +}) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index 4f3b398db..47533dcdc 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -7,12 +7,14 @@ import { RateDefinition } from "@/server/routers/hotels/output" import ToggleSidePeek from "@/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek" import FlexibilityOption from "@/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption" +import { ErrorCircleIcon } from "@/components/Icons" import ImageGallery from "@/components/ImageGallery" import Caption from "@/components/TempDesignSystem/Text/Caption" import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import { getIconForFeatureCode } from "../../utils" +import { cardVariants } from "./cardVariants" import styles from "./roomCard.module.css" @@ -89,8 +91,15 @@ export default function RoomCard({ [freeCancelation, freeBooking, nonRefundable] ) + const classNames = cardVariants({ + availability: + roomConfiguration.status === "NotAvailable" + ? "noAvailability" + : "default", + }) + return ( -
    +
    {mainImage && (
    @@ -163,23 +172,36 @@ export default function RoomCard({ id: "Breakfast selection in next step.", })} -
    - {Object.entries(rates).map(([key, rate]) => ( - - ))} -
    + {roomConfiguration.status === "NotAvailable" ? ( +
    +
    + +
    + + + ) : ( +
    + {Object.entries(rates).map(([key, rate]) => ( + + ))} +
    + )} ) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css index 1d7ac0a0e..8ec1978e6 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css @@ -10,6 +10,22 @@ justify-content: space-between; } +.card.noAvailability { + justify-content: flex-start; +} + +.card.noAvailability:before { + background-color: rgba(0, 0, 0, 40%); + content: ""; + display: block; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 2; +} + .specification { display: flex; flex-direction: row; @@ -79,3 +95,16 @@ min-height: 190px; position: relative; } + +.noRoomsContainer { + padding: var(--Spacing-x2); + background-color: var(--Base-Surface-Secondary-light-Normal); + border-radius: var(--Corner-radius-Medium); + margin: 0; + width: 100%; +} + +.noRooms { + display: flex; + gap: var(--Spacing-x1); +} diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 47feb8b4e..7cd40f4c5 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -339,6 +339,7 @@ "There are no rooms available that match your request.": "Der er ingen værelser tilgængelige, der matcher din forespørgsel.", "There are no transactions to display": "Der er ingen transaktioner at vise", "Things nearby HOTEL_NAME": "Ting i nærheden af {hotelName}", + "This room is not available": "Dette værelse er ikke tilgængeligt", "To get the member price {amount} {currency}, log in or join when completing the booking.": "For at få medlemsprisen {amount} {currency}, log ind eller tilmeld dig, når du udfylder bookingen.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For at sikre din reservation, beder vi om at du giver os dine betalingsoplysninger. Du kan så være sikker på, at ingen gebyrer vil blive opkrævet på dette tidspunkt.", "Total Points": "Samlet antal point", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 91256509d..2d28e2626 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -338,6 +338,7 @@ "There are no rooms available that match your request.": "Es sind keine Zimmer verfügbar, die Ihrer Anfrage entsprechen.", "There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden", "Things nearby HOTEL_NAME": "Dinge in der Nähe von {hotelName}", + "This room is not available": "Dieses Zimmer ist nicht verfügbar", "To get the member price {amount} {currency}, log in or join when completing the booking.": "Um den Mitgliederpreis von {amount} {currency} zu erhalten, loggen Sie sich ein oder treten Sie Scandic Friends bei, wenn Sie die Buchung abschließen.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Um Ihre Reservierung zu sichern, bitten wir Sie, Ihre Zahlungskarteninformationen zu geben. Sie können sicher sein, dass keine Gebühren zu diesem Zeitpunkt erhoben werden.", "Total Points": "Gesamtpunktzahl", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 664069250..d5ab06e41 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -368,6 +368,7 @@ "There are no rooms available that match your request.": "There are no rooms available that match your request.", "There are no transactions to display": "There are no transactions to display", "Things nearby HOTEL_NAME": "Things nearby {hotelName}", + "This room is not available": "This room is not available", "To get the member price {amount} {currency}, log in or join when completing the booking.": "To get the member price {amount} {currency}, log in or join when completing the booking.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.", "Total Points": "Total Points", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index ca9e330ea..114531609 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -340,6 +340,7 @@ "There are no rooms available that match your request.": "Ei huoneita saatavilla, jotka vastaavat pyyntöäsi.", "There are no transactions to display": "Näytettäviä tapahtumia ei ole", "Things nearby HOTEL_NAME": "Lähellä olevia asioita {hotelName}", + "This room is not available": "Tämä huone ei ole käytettävissä", "To get the member price {amount} {currency}, log in or join when completing the booking.": "Jäsenhintaan saavat sisäänkirjautuneet tai liittyneet jäsenet.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Varmistaaksesi varauksen, pyydämme sinua antamaan meille maksukortin tiedot. Varmista, että ei veloiteta maksusi tällä hetkellä.", "Total Points": "Kokonaispisteet", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 2c61f1596..a2d8f8bee 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -337,6 +337,7 @@ "There are no rooms available that match your request.": "Det er ingen rom tilgjengelige som matcher din forespørsel.", "There are no transactions to display": "Det er ingen transaksjoner å vise", "Things nearby HOTEL_NAME": "Ting i nærheten av {hotelName}", + "This room is not available": "Dette rommet er ikke tilgjengelig", "To get the member price {amount} {currency}, log in or join when completing the booking.": "For å få medlemsprisen {amount} {currency}, logg inn eller bli med når du fullfører bestillingen.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For å sikre din reservasjon, ber vi om at du gir oss dine betalingskortdetaljer. Vær sikker på at ingen gebyrer vil bli belastet på dette tidspunktet.", "Total Points": "Totale poeng", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 911553c7b..117af5485 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -337,6 +337,7 @@ "There are no rooms available that match your request.": "Det finns inga rum tillgängliga som matchar din begäran.", "There are no transactions to display": "Det finns inga transaktioner att visa", "Things nearby HOTEL_NAME": "Saker i närheten av {hotelName}", + "This room is not available": "Detta rum är inte tillgängligt", "To get the member price {amount} {currency}, log in or join when completing the booking.": "För att få medlemsprisen {amount} {currency}, logga in eller bli medlem när du slutför bokningen.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "För att säkra din bokning ber vi om att du ger oss dina betalkortdetaljer. Välj säker på att ingen avgifter kommer att debiteras just nu.", "Total Points": "Poäng totalt", diff --git a/types/components/hotelReservation/selectRate/hotelInfoCardProps.ts b/types/components/hotelReservation/selectRate/hotelInfoCardProps.ts index 0a3526167..088c54e51 100644 --- a/types/components/hotelReservation/selectRate/hotelInfoCardProps.ts +++ b/types/components/hotelReservation/selectRate/hotelInfoCardProps.ts @@ -2,4 +2,5 @@ import type { HotelData } from "@/types/hotel" export type HotelInfoCardProps = { hotelData: HotelData | null + noAvailability: boolean } From f43250811210095bbf23a71f103e8cbff8057d1d Mon Sep 17 00:00:00 2001 From: Hrishikesh Vaipurkar Date: Fri, 15 Nov 2024 12:18:16 +0100 Subject: [PATCH 96/98] feat: SW-602 Update states on filter changes --- .../SelectRate/HotelInfoCard/index.tsx | 18 ++++++- .../SelectRate/Rooms/index.tsx | 47 ++++++++++++++++++- stores/roomAvailability.ts | 17 +++++++ 3 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 stores/roomAvailability.ts diff --git a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx index 8eabe85ba..946b82ad2 100644 --- a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx +++ b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx @@ -1,6 +1,9 @@ "use client" +import { useEffect } from "react" import { useIntl } from "react-intl" +import useRoomAvailableStore from "@/stores/roomAvailability" + import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import ImageGallery from "@/components/ImageGallery" import Alert from "@/components/TempDesignSystem/Alert" @@ -24,10 +27,23 @@ export default function HotelInfoCard({ const hotelAttributes = hotelData?.data.attributes const intl = useIntl() + const noRoomsAvailable = useRoomAvailableStore( + (state) => state.noRoomsAvailable + ) + const setNoRoomsAvailable = useRoomAvailableStore( + (state) => state.setNoRoomsAvailable + ) + const sortedFacilities = hotelAttributes?.detailedFacilities .sort((a, b) => b.sortOrder - a.sortOrder) .slice(0, 5) + useEffect(() => { + if (noAvailability) { + setNoRoomsAvailable() + } + }, [noAvailability, setNoRoomsAvailable]) + return (
    {hotelAttributes && ( @@ -101,7 +117,7 @@ export default function HotelInfoCard({ ) })} - {noAvailability ? ( + {noRoomsAvailable ? (
    ( [] ) + const noRoomsAvailable = useRoomAvailableStore( + (state) => state.noRoomsAvailable + ) + const setNoRoomsAvailable = useRoomAvailableStore( + (state) => state.setNoRoomsAvailable + ) + const setRoomsAvailable = useRoomAvailableStore( + (state) => state.setRoomsAvailable + ) const handleFilter = useCallback( (filter: Record) => { @@ -57,6 +68,10 @@ export default function Rooms({ }) } + if (noRoomsAvailable) { + setRoomsAvailable() + } + return } @@ -65,7 +80,27 @@ export default function Rooms({ room.features.some((feature) => feature.code === filteredPackage) ) ) - setRooms({ ...roomsAvailability, roomConfigurations: filteredRooms }) + let notAvailableRooms = visibleRooms.filter((room) => + filteredPackages.every( + (filteredPackage) => + !room.features.some((feature) => feature.code === filteredPackage) + ) + ) + // Clone nested object to keep original object intact and not messup the room data + notAvailableRooms = JSON.parse(JSON.stringify(notAvailableRooms)) + notAvailableRooms.forEach((room) => { + room.status = "NotAvailable" + }) + setRooms({ + ...roomsAvailability, + roomConfigurations: [...filteredRooms, ...notAvailableRooms], + }) + + if (filteredRooms.length == 0) { + setNoRoomsAvailable() + } else if (noRoomsAvailable) { + setRoomsAvailable() + } const petRoomPackage = (filteredPackages.includes(RoomPackageCodeEnum.PET_ROOM) && @@ -85,7 +120,15 @@ export default function Rooms({ }) } }, - [roomsAvailability, visibleRooms, rateSummary, packages] + [ + roomsAvailability, + visibleRooms, + rateSummary, + packages, + noRoomsAvailable, + setNoRoomsAvailable, + setRoomsAvailable, + ] ) return ( diff --git a/stores/roomAvailability.ts b/stores/roomAvailability.ts new file mode 100644 index 000000000..ad01453e4 --- /dev/null +++ b/stores/roomAvailability.ts @@ -0,0 +1,17 @@ +"use client" + +import { create } from "zustand" + +interface RoomAvailabilityState { + noRoomsAvailable: boolean + setNoRoomsAvailable: () => void + setRoomsAvailable: () => void +} + +const useRoomAvailableStore = create((set) => ({ + noRoomsAvailable: false, + setNoRoomsAvailable: () => set(() => ({ noRoomsAvailable: true })), + setRoomsAvailable: () => set(() => ({ noRoomsAvailable: false })), +})) + +export default useRoomAvailableStore From 0a3b9450b6addd5399c3f860585a7803867df2fe Mon Sep 17 00:00:00 2001 From: Hrishikesh Vaipurkar Date: Fri, 15 Nov 2024 12:45:43 +0100 Subject: [PATCH 97/98] feat: SW-602 Optimized code --- .../(standard)/select-rate/page.tsx | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index 29c602779..fd9db4d6c 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -84,13 +84,17 @@ export default async function SelectRatePage({ getProfileSafely(), ]) + if (!roomsAvailability) { + return "No rooms found" // TODO: Add a proper error message + } + if (!hotelData) { return "No hotel data found" // TODO: Add a proper error message } const roomCategories = hotelData?.included - const noRoomsAvailable = roomsAvailability?.roomConfigurations.reduce( + const noRoomsAvailable = roomsAvailability.roomConfigurations.reduce( (acc, room) => { return acc && room.status === "NotAvailable" }, @@ -99,18 +103,13 @@ export default async function SelectRatePage({ return ( <> - + - {roomsAvailability ? ( - - ) : null} ) } From 2750a4f4038ba096bb98cfdf7182d538e91b27a5 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Fri, 15 Nov 2024 20:18:57 +0100 Subject: [PATCH 98/98] fix(SW-881) fixing data issues --- .../SelectRate/RoomSelection/RateSummary/index.tsx | 2 +- .../SelectRate/RoomSelection/RoomCard/index.tsx | 2 +- .../HotelReservation/SelectRate/RoomSelection/index.tsx | 3 +-- server/routers/hotels/output.ts | 4 ++-- .../components/hotelReservation/selectRate/rateSummary.ts | 2 +- types/components/hotelReservation/selectRate/roomCard.ts | 2 +- .../components/hotelReservation/selectRate/roomFilter.ts | 8 +++----- .../hotelReservation/selectRate/roomSelection.ts | 2 +- 8 files changed, 11 insertions(+), 14 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx index 33400efaf..7700b1d29 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx @@ -41,7 +41,7 @@ export default function RateSummary({ (feature) => feature.code === RoomPackageCodeEnum.PET_ROOM ) - const petRoomPackage = packages.find( + const petRoomPackage = packages?.find( (pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM ) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index 47533dcdc..fb713fddb 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -61,7 +61,7 @@ export default function RoomCard({ const petRoomPackage = (selectedPackages.includes(RoomPackageCodeEnum.PET_ROOM) && - packages.find((pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM)) || + packages?.find((pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM)) || undefined const selectedRoom = roomCategories.find( diff --git a/components/HotelReservation/SelectRate/RoomSelection/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/index.tsx index ceb86c5f3..b3624552f 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/index.tsx @@ -1,6 +1,6 @@ "use client" import { useRouter, useSearchParams } from "next/navigation" -import { useMemo, useState } from "react" +import { useMemo } from "react" import RateSummary from "./RateSummary" import RoomCard from "./RoomCard" @@ -9,7 +9,6 @@ import { getHotelReservationQueryParams } from "./utils" import styles from "./roomSelection.module.css" import type { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection" -import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate" export default function RoomSelection({ roomsAvailability, diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index da202c7d3..9bc81225e 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -855,7 +855,7 @@ export const breakfastPackagesSchema = z export const packagesSchema = z.object({ code: z.nativeEnum(RoomPackageCodeEnum), - itemCode: z.string(), + itemCode: z.string().optional(), description: z.string(), localPrice: packagePriceSchema, requestedPrice: packagePriceSchema, @@ -873,7 +873,7 @@ export const getRoomPackagesSchema = z data: z.object({ attributes: z.object({ hotelId: z.number(), - packages: z.array(packagesSchema), + packages: z.array(packagesSchema).optional().default([]), }), relationships: z .object({ diff --git a/types/components/hotelReservation/selectRate/rateSummary.ts b/types/components/hotelReservation/selectRate/rateSummary.ts index f6c0f03b6..40c595508 100644 --- a/types/components/hotelReservation/selectRate/rateSummary.ts +++ b/types/components/hotelReservation/selectRate/rateSummary.ts @@ -5,6 +5,6 @@ import type { Rate } from "./selectRate" export interface RateSummaryProps { rateSummary: Rate isUserLoggedIn: boolean - packages: RoomPackageData + packages: RoomPackageData | undefined roomsAvailability: RoomsAvailability } diff --git a/types/components/hotelReservation/selectRate/roomCard.ts b/types/components/hotelReservation/selectRate/roomCard.ts index 0f76afb56..aa0d647be 100644 --- a/types/components/hotelReservation/selectRate/roomCard.ts +++ b/types/components/hotelReservation/selectRate/roomCard.ts @@ -18,7 +18,7 @@ export type RoomCardProps = { rateDefinitions: RateDefinition[] roomCategories: RoomData[] selectedPackages: RoomPackageCodes[] - packages: RoomPackageData + packages: RoomPackageData | undefined handleSelectRate: (rate: Rate) => void } diff --git a/types/components/hotelReservation/selectRate/roomFilter.ts b/types/components/hotelReservation/selectRate/roomFilter.ts index 78980be83..f895ed73a 100644 --- a/types/components/hotelReservation/selectRate/roomFilter.ts +++ b/types/components/hotelReservation/selectRate/roomFilter.ts @@ -16,9 +16,7 @@ export interface RoomFilterProps { filterOptions: RoomPackageData } -export interface RoomPackageData - extends z.output {} - -export type RoomPackageCodes = RoomPackageData[number]["code"] - export type RoomPackage = z.output +export interface RoomPackageData extends Array {} + +export type RoomPackageCodes = RoomPackage["code"] diff --git a/types/components/hotelReservation/selectRate/roomSelection.ts b/types/components/hotelReservation/selectRate/roomSelection.ts index 63b17ed6f..163bfd6fa 100644 --- a/types/components/hotelReservation/selectRate/roomSelection.ts +++ b/types/components/hotelReservation/selectRate/roomSelection.ts @@ -8,7 +8,7 @@ export interface RoomSelectionProps { roomsAvailability: RoomsAvailability roomCategories: RoomData[] user: SafeUser - packages: RoomPackageData + packages: RoomPackageData | undefined selectedPackages: RoomPackageCodes[] setRateSummary: (rateSummary: Rate) => void rateSummary: Rate | null
    {facility.name} - {intl.formatMessage({ id: "Total price" })} - + {str} + + {intl.formatMessage({ id: "Total price" })} + {summaryPriceTex} + + {Icon ? ( + + ) : null} + {children} + + - - - {header.data.topLink.title} - - - - + {intl.formatMessage({ id: "Find booking" })} - + {languages[currentLanguage]} + {intl.formatMessage({ id: "Member price" })} + + {intl.formatMessage({ id: "From" })} + + {intl.formatMessage({ id: "Approx." })} + - EUR{facility.name} + {hotelData.address.streetAddress}, {hotelData.address.city} + + {hotelData.address.streetAddress}, {hotelData.address.city} + - {price?.regularAmount} {price?.currency} / - {intl.formatMessage({ id: "night" })} - - {price?.memberAmount} {price?.currency} / - {intl.formatMessage({ id: "night" })} - + {facility.name} + - {hotelAttributes.ratings.tripAdvisor.rating} - {rating} {intl.formatMessage( @@ -96,81 +148,39 @@ export default function RoomCard({ )} -
    -
    - - {roomConfiguration.roomType} - - {/* Out of scope for now +
    + + {roomConfiguration.roomType} + + {/* Out of scope for now {descriptions?.short} */} -
    - {intl.formatMessage({ - id: "Breakfast selection in next step.", - })} - + {intl.formatMessage({ + id: "Breakfast selection in next step.", + })} + - {rooms} + {roomsLabel} {ageReqdErrMsg} {ageReqdErrMsg} - {ageReqdErrMsg} + {roomErrors.message} + + + {errorMessage} + {intl.formatMessage( + { id: "booking.nights" }, + { totalNights: 0 } + )} + + {intl.formatMessage({ id: "Guests & Rooms" })} + + {intl.formatMessage({ id: "Search" })} + + Where to + + {vouchers} + + {useVouchers} + + {bonus} + + {reward} + - {intl.formatMessage( - { id: "booking.nights" }, - { totalNights: 0 } - )} - - {intl.formatMessage({ id: "Guests & Rooms" })} - - {intl.formatMessage({ id: "Search" })} - + {intl.formatMessage({ id: "Where to" })} + + {intl.formatMessage({ id: "booking.nights" }, { totalNights: 0 })} + + {intl.formatMessage({ id: "booking.nights" }, { totalNights: 0 })} + + {intl.formatMessage({ id: "Guests & Rooms" })} + + {intl.formatMessage({ id: "Search" })} + {label} {label} {intl.formatMessage( - { id: "Distance to city centre" }, + { id: "Distance in km to city centre" }, { number: distanceToCentre } )} {intl.formatMessage( - { id: "Distance to city centre" }, + { id: "Distance in km to city centre" }, { number: hotelData.location.distanceToCentre } )} {intl.formatMessage( - { id: "Distance to city centre" }, + { id: "Distance in km to city centre" }, { number: hotel.location.distanceToCentre } )} diff --git a/components/TempDesignSystem/Tooltip/tooltip.module.css b/components/TempDesignSystem/Tooltip/tooltip.module.css index 58676e967..e25433f7c 100644 --- a/components/TempDesignSystem/Tooltip/tooltip.module.css +++ b/components/TempDesignSystem/Tooltip/tooltip.module.css @@ -140,3 +140,15 @@ border-width: 7px 8px 7px 0; border-color: transparent var(--UI-Text-Active) transparent transparent; } + +@media screen and (max-width: 768px) { + .tooltipContainer[data-active="true"] .tooltip { + visibility: visible; + opacity: 1; + } + + .tooltipContainer[data-active="false"] .tooltip { + visibility: hidden; + opacity: 0; + } +} From 06da80c9f0ed12c27b7fa5652c5b959d46561bdd Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Thu, 14 Nov 2024 16:28:41 +0100 Subject: [PATCH 78/98] fix: conditionally use user token or service token for booking --- server/routers/booking/mutation.ts | 148 ++++++++++++++--------------- 1 file changed, 73 insertions(+), 75 deletions(-) diff --git a/server/routers/booking/mutation.ts b/server/routers/booking/mutation.ts index 2edbd5bdd..dc3bad0fe 100644 --- a/server/routers/booking/mutation.ts +++ b/server/routers/booking/mutation.ts @@ -2,7 +2,7 @@ import { metrics } from "@opentelemetry/api" import * as api from "@/lib/api" import { getVerifiedUser } from "@/server/routers/user/query" -import { router, serviceProcedure } from "@/server/trpc" +import { router, safeProtectedServiceProcedure } from "@/server/trpc" import { getMembership } from "@/utils/user" @@ -35,95 +35,93 @@ async function getMembershipNumber( } export const bookingMutationRouter = router({ - create: serviceProcedure.input(createBookingInput).mutation(async function ({ - ctx, - input, - }) { - const { checkInDate, checkOutDate, hotelId } = input + create: safeProtectedServiceProcedure + .input(createBookingInput) + .mutation(async function ({ ctx, input }) { + const accessToken = ctx.session?.token.access_token ?? ctx.serviceToken + const { checkInDate, checkOutDate, hotelId } = input - // TODO: add support for user token OR service token in procedure - // then we can fetch membership number if user token exists - const loggingAttributes = { - // membershipNumber: await getMembershipNumber(ctx.session), - checkInDate, - checkOutDate, - hotelId, - } - - createBookingCounter.add(1, { hotelId, checkInDate, checkOutDate }) - - console.info( - "api.booking.create start", - JSON.stringify({ - query: loggingAttributes, - }) - ) - const headers = { - Authorization: `Bearer ${ctx.serviceToken}`, - } - - const apiResponse = await api.post(api.endpoints.v1.Booking.bookings, { - headers, - body: input, - }) - - if (!apiResponse.ok) { - const text = await apiResponse.text() - createBookingFailCounter.add(1, { - hotelId, + const loggingAttributes = { + membershipNumber: await getMembershipNumber(ctx.session), checkInDate, checkOutDate, - error_type: "http_error", - error: JSON.stringify({ - status: apiResponse.status, - }), - }) - console.error( - "api.booking.create error", + hotelId, + } + + createBookingCounter.add(1, { hotelId, checkInDate, checkOutDate }) + + console.info( + "api.booking.create start", JSON.stringify({ query: loggingAttributes, - error: { + }) + ) + const headers = { + Authorization: `Bearer ${accessToken}`, + } + + const apiResponse = await api.post(api.endpoints.v1.Booking.bookings, { + headers, + body: input, + }) + + if (!apiResponse.ok) { + const text = await apiResponse.text() + createBookingFailCounter.add(1, { + hotelId, + checkInDate, + checkOutDate, + error_type: "http_error", + error: JSON.stringify({ status: apiResponse.status, - statusText: apiResponse.statusText, - error: text, - }, + }), }) - ) - return null - } + console.error( + "api.booking.create error", + JSON.stringify({ + query: loggingAttributes, + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + error: text, + }, + }) + ) + return null + } - const apiJson = await apiResponse.json() - const verifiedData = createBookingSchema.safeParse(apiJson) - if (!verifiedData.success) { - createBookingFailCounter.add(1, { + const apiJson = await apiResponse.json() + const verifiedData = createBookingSchema.safeParse(apiJson) + if (!verifiedData.success) { + createBookingFailCounter.add(1, { + hotelId, + checkInDate, + checkOutDate, + error_type: "validation_error", + }) + + console.error( + "api.booking.create validation error", + JSON.stringify({ + query: loggingAttributes, + error: verifiedData.error, + }) + ) + return null + } + + createBookingSuccessCounter.add(1, { hotelId, checkInDate, checkOutDate, - error_type: "validation_error", }) - console.error( - "api.booking.create validation error", + console.info( + "api.booking.create success", JSON.stringify({ query: loggingAttributes, - error: verifiedData.error, }) ) - return null - } - - createBookingSuccessCounter.add(1, { - hotelId, - checkInDate, - checkOutDate, - }) - - console.info( - "api.booking.create success", - JSON.stringify({ - query: loggingAttributes, - }) - ) - return verifiedData.data - }), + return verifiedData.data + }), }) From 6496ec233eb762b697585fb452bca1cdd8a46798 Mon Sep 17 00:00:00 2001 From: Anton Gunnarsson Date: Fri, 15 Nov 2024 10:39:27 +0100 Subject: [PATCH 79/98] Add NavigationMenuListSkeleton --- .../NavigationMenu/NavigationMenuList/index.tsx | 12 ++++++++++++ components/Header/MainMenu/index.tsx | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/components/Header/MainMenu/NavigationMenu/NavigationMenuList/index.tsx b/components/Header/MainMenu/NavigationMenu/NavigationMenuList/index.tsx index e56e494a1..5d1b10185 100644 --- a/components/Header/MainMenu/NavigationMenu/NavigationMenuList/index.tsx +++ b/components/Header/MainMenu/NavigationMenu/NavigationMenuList/index.tsx @@ -1,8 +1,10 @@ +import SkeletonShimmer from "@/components/SkeletonShimmer" import NavigationMenuItem from "../NavigationMenuItem" import styles from "./navigationMenuList.module.css" import type { NavigationMenuListProps } from "@/types/components/header/navigationMenuList" +import { cx } from "class-variance-authority" export default function NavigationMenuList({ isMobile, @@ -20,3 +22,13 @@ export default function NavigationMenuList({ ) } + +export function NavigationMenuListSkeleton() { + return ( +
      +
    • + +
    • +
    + ) +} diff --git a/components/Header/MainMenu/index.tsx b/components/Header/MainMenu/index.tsx index 9fb174df2..208a64b89 100644 --- a/components/Header/MainMenu/index.tsx +++ b/components/Header/MainMenu/index.tsx @@ -13,6 +13,7 @@ import MyPagesMenuWrapper, { import NavigationMenu from "./NavigationMenu" import styles from "./mainMenu.module.css" +import { NavigationMenuListSkeleton } from "./NavigationMenu/NavigationMenuList" export default function MainMenu() { return ( @@ -22,7 +23,7 @@ export default function MainMenu() {
    - + }> }> From fd141e6d2e42ff33b32ad6085cd91e7baa768741 Mon Sep 17 00:00:00 2001 From: Anton Gunnarsson Date: Fri, 15 Nov 2024 10:39:50 +0100 Subject: [PATCH 80/98] Remove unneccesary suspense --- components/Header/MainMenu/MobileMenu/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Header/MainMenu/MobileMenu/index.tsx b/components/Header/MainMenu/MobileMenu/index.tsx index d5c2063a0..ca81791cd 100644 --- a/components/Header/MainMenu/MobileMenu/index.tsx +++ b/components/Header/MainMenu/MobileMenu/index.tsx @@ -77,7 +77,7 @@ export default function MobileMenu({ className={styles.dialog} aria-label={intl.formatMessage({ id: "Menu" })} > - {children} + {children}
    {intl.formatMessage({ id: "Find booking" })} From f786f6fa07268c33b0a4538eb93a118c648182dc Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Fri, 15 Nov 2024 11:02:17 +0100 Subject: [PATCH 81/98] fix(SW-350): Close booking widget on search (mobile) --- components/BookingWidget/Client.tsx | 2 +- components/Forms/BookingWidget/index.tsx | 8 ++++++-- types/components/form/bookingwidget.ts | 8 ++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/components/BookingWidget/Client.tsx b/components/BookingWidget/Client.tsx index 930cad4c0..0c7ef1b88 100644 --- a/components/BookingWidget/Client.tsx +++ b/components/BookingWidget/Client.tsx @@ -173,7 +173,7 @@ export default function BookingWidgetClient({ > -
    +
    diff --git a/components/Forms/BookingWidget/index.tsx b/components/Forms/BookingWidget/index.tsx index 6c78ea341..243fceaa5 100644 --- a/components/Forms/BookingWidget/index.tsx +++ b/components/Forms/BookingWidget/index.tsx @@ -17,7 +17,11 @@ import { Location } from "@/types/trpc/routers/hotel/locations" const formId = "booking-widget" -export default function Form({ locations, type }: BookingWidgetFormProps) { +export default function Form({ + locations, + type, + setIsOpen, +}: BookingWidgetFormProps) { const router = useRouter() const lang = useLang() @@ -52,7 +56,7 @@ export default function Form({ locations, type }: BookingWidgetFormProps) { ) }) }) - + setIsOpen(false) router.push(`${bookingFlowPage}?${bookingWidgetParams.toString()}`) } diff --git a/types/components/form/bookingwidget.ts b/types/components/form/bookingwidget.ts index 14a548952..887c4eae2 100644 --- a/types/components/form/bookingwidget.ts +++ b/types/components/form/bookingwidget.ts @@ -1,14 +1,10 @@ -import { FormState, UseFormReturn } from "react-hook-form" - -import type { - BookingWidgetSchema, - BookingWidgetType, -} from "@/types/components/bookingWidget" +import type { BookingWidgetType } from "@/types/components/bookingWidget" import type { Location, Locations } from "@/types/trpc/routers/hotel/locations" export interface BookingWidgetFormProps { locations: Locations type?: BookingWidgetType + setIsOpen: (isOpen: boolean) => void } export interface BookingWidgetFormContentProps { From ef2ef3c9a8b24a91901488de6313f8c5577f771e Mon Sep 17 00:00:00 2001 From: Bianca Widstam Date: Fri, 15 Nov 2024 10:15:26 +0000 Subject: [PATCH 82/98] feat/SW-604-select-hotel-page-UI (pull request #905) Feat/SW-604 select hotel page UI * feat(SW-604): update header, scandic logo, and map text * feat(SW-604): fix map button * feat(SW-604): fix align header * feat(SW-604): hide filter for mobile * feat(SW-604): fix padding * feat(SW-604): fix padding again * feat(SW-604): fix mobile design * feat(SW-604): fix padding Approved-by: Pontus Dreij --- .../(standard)/select-hotel/page.module.css | 42 ++++++++++++------- .../(standard)/select-hotel/page.tsx | 26 ++++++++---- .../HotelReservation/ReadMore/index.tsx | 6 ++- .../HotelSorter/hotelSorter.module.css | 6 +++ .../SelectHotel/HotelSorter/index.tsx | 2 +- components/Icons/Logos/ScandicLogo.tsx | 36 ++++++++-------- i18n/dictionaries/da.json | 2 +- i18n/dictionaries/de.json | 2 +- i18n/dictionaries/en.json | 2 +- i18n/dictionaries/fi.json | 2 +- i18n/dictionaries/no.json | 2 +- i18n/dictionaries/sv.json | 2 +- 12 files changed, 82 insertions(+), 48 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css index 89de83d4d..8bf36ee38 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css @@ -1,21 +1,23 @@ .main { display: flex; - gap: var(--Spacing-x3); - padding: var(--Spacing-x4) var(--Spacing-x4) 0 var(--Spacing-x4); + padding: 0 var(--Spacing-x2) var(--Spacing-x3) var(--Spacing-x2); background-color: var(--Scandic-Brand-Warm-White); min-height: 100dvh; flex-direction: column; max-width: var(--max-width); margin: 0 auto; } - .header { display: flex; - margin: 0 auto; - padding: var(--Spacing-x4) var(--Spacing-x5) var(--Spacing-x3) - var(--Spacing-x5); - justify-content: space-between; - max-width: var(--max-width); + flex-direction: column; + gap: var(--Spacing-x2); + padding: var(--Spacing-x3) var(--Spacing-x2) 0 var(--Spacing-x2); +} + +.cityInformation { + display: flex; + flex-wrap: wrap; + gap: var(--Spacing-x1); } .sideBar { @@ -46,6 +48,23 @@ } @media (min-width: 768px) { + .main { + padding: var(--Spacing-x5); + } + .header { + display: block; + background-color: var(--Base-Surface-Subtle-Normal); + padding: var(--Spacing-x4) var(--Spacing-x5) var(--Spacing-x3) + var(--Spacing-x5); + } + + .title { + margin: 0 auto; + display: flex; + max-width: var(--max-width-navigation); + align-items: center; + justify-content: space-between; + } .link { display: flex; padding-bottom: var(--Spacing-x6); @@ -57,13 +76,6 @@ border-radius: var(--Corner-radius-Medium); border: 1px solid var(--Base-Border-Subtle); } - .mapLinkText { - display: flex; - align-items: center; - justify-content: center; - gap: var(--Spacing-x-half); - padding: var(--Spacing-x-one-and-half) var(--Spacing-x0); - } .main { flex-direction: row; gap: var(--Spacing-x5); diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx index 4dfefd7ac..0493c9d70 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -18,7 +18,10 @@ import { import { ChevronRightIcon } from "@/components/Icons" import StaticMap from "@/components/Maps/StaticMap" import Alert from "@/components/TempDesignSystem/Alert" +import Button from "@/components/TempDesignSystem/Button" import Link from "@/components/TempDesignSystem/Link" +import Preamble from "@/components/TempDesignSystem/Text/Preamble" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import { getIntl } from "@/i18n" import { setLang } from "@/i18n/serverContext" @@ -66,8 +69,14 @@ export default async function SelectHotelPage({ return ( <>
    -
    {city.name}
    - +
    +
    + {city.name} + {hotels.length} hotels +
    + +
    +
    @@ -87,10 +96,14 @@ export default async function SelectHotelPage({ mapType="roadmap" altText={`Map of ${searchParams.city} city center`} /> -
    - {intl.formatMessage({ id: "Show map" })} - -
    +
    ) : ( @@ -105,7 +118,6 @@ export default async function SelectHotelPage({ />
    )} -
    diff --git a/components/HotelReservation/ReadMore/index.tsx b/components/HotelReservation/ReadMore/index.tsx index f7ec2af84..5b55f59ca 100644 --- a/components/HotelReservation/ReadMore/index.tsx +++ b/components/HotelReservation/ReadMore/index.tsx @@ -23,7 +23,11 @@ export default function ReadMore({ label, hotelId, showCTA }: ReadMoreProps) { className={styles.detailsButton} > {label} - + ) } diff --git a/components/HotelReservation/SelectHotel/HotelSorter/hotelSorter.module.css b/components/HotelReservation/SelectHotel/HotelSorter/hotelSorter.module.css index 7c991c602..535808a35 100644 --- a/components/HotelReservation/SelectHotel/HotelSorter/hotelSorter.module.css +++ b/components/HotelReservation/SelectHotel/HotelSorter/hotelSorter.module.css @@ -1,3 +1,9 @@ .container { width: 339px; } + +@media (max-width: 768px) { + .container { + display: none; + } +} diff --git a/components/HotelReservation/SelectHotel/HotelSorter/index.tsx b/components/HotelReservation/SelectHotel/HotelSorter/index.tsx index d9a364fcd..2e48364a1 100644 --- a/components/HotelReservation/SelectHotel/HotelSorter/index.tsx +++ b/components/HotelReservation/SelectHotel/HotelSorter/index.tsx @@ -40,7 +40,7 @@ export default function HotelSorter() { ) const sortItems: SortItem[] = [ { - label: intl.formatMessage({ id: "Distance to city center" }), + label: intl.formatMessage({ id: "Distance to city centre" }), value: SortOrder.Distance, }, { label: intl.formatMessage({ id: "Name" }), value: SortOrder.Name }, diff --git a/components/Icons/Logos/ScandicLogo.tsx b/components/Icons/Logos/ScandicLogo.tsx index cdb992753..f96025768 100644 --- a/components/Icons/Logos/ScandicLogo.tsx +++ b/components/Icons/Logos/ScandicLogo.tsx @@ -11,52 +11,52 @@ export default function ScandicLogoIcon({ return ( ) diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 6529382a8..47feb8b4e 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -297,6 +297,7 @@ "See details": "Se detaljer", "See hotel details": "Se hoteloplysninger", "See less FAQ": "Se mindre FAQ", + "See map": "Vis kort", "See on map": "Se på kort", "See room details": "Se værelsesdetaljer", "See rooms": "Se værelser", @@ -314,7 +315,6 @@ "Show all amenities": "Vis alle faciliteter", "Show less": "Vis mindre", "Show less rooms": "Vise færre rum", - "Show map": "Vis kort", "Show more": "Vis mere", "Show more rooms": "Vise flere rum", "Sign up bonus": "Velkomstbonus", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index f69e9bb0e..91256509d 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -296,6 +296,7 @@ "See details": "Siehe Einzelheiten", "See hotel details": "Hotelinformationen ansehen", "See less FAQ": "Weniger anzeigen FAQ", + "See map": "Karte anzeigen", "See on map": "Karte ansehen", "See room details": "Zimmerdetails ansehen", "See rooms": "Zimmer ansehen", @@ -313,7 +314,6 @@ "Show all amenities": "Alle Annehmlichkeiten anzeigen", "Show less": "Weniger anzeigen", "Show less rooms": "Weniger Zimmer anzeigen", - "Show map": "Karte anzeigen", "Show more": "Mehr anzeigen", "Show more rooms": "Weitere Räume anzeigen", "Sign up bonus": "Anmelde-Bonus", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 049adce52..664069250 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -325,6 +325,7 @@ "See details": "See details", "See hotel details": "See hotel details", "See less FAQ": "See less FAQ", + "See map": "See map", "See on map": "See on map", "See room details": "See room details", "See rooms": "See rooms", @@ -343,7 +344,6 @@ "Show all amenities": "Show all amenities", "Show less": "Show less", "Show less rooms": "Show less rooms", - "Show map": "Show map", "Show more": "Show more", "Show more rooms": "Show more rooms", "Sign up bonus": "Sign up bonus", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 30b5afff0..ca9e330ea 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -298,6 +298,7 @@ "See details": "Katso tiedot", "See hotel details": "Katso hotellin tiedot", "See less FAQ": "Katso vähemmän UKK", + "See map": "Näytä kartta", "See on map": "Näytä kartalla", "See room details": "Katso huoneen tiedot", "See rooms": "Katso huoneet", @@ -315,7 +316,6 @@ "Show all amenities": "Näytä kaikki mukavuudet", "Show less": "Näytä vähemmän", "Show less rooms": "Näytä vähemmän huoneita", - "Show map": "Näytä kartta", "Show more": "Näytä lisää", "Show more rooms": "Näytä lisää huoneita", "Sign up bonus": "Liittymisbonus", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 5db8c271f..2c61f1596 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -295,6 +295,7 @@ "See details": "Se detaljer", "See hotel details": "Se hotellinformasjon", "See less FAQ": "Se mindre FAQ", + "See map": "Vis kart", "See on map": "Se på kart", "See room details": "Se detaljer om rommet", "See rooms": "Se rom", @@ -312,7 +313,6 @@ "Show all amenities": "Vis alle fasiliteter", "Show less": "Vis mindre", "Show less rooms": "Vise færre rom", - "Show map": "Vis kart", "Show more": "Vis mer", "Show more rooms": "Vise flere rom", "Sign up bonus": "Velkomstbonus", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index e466c1524..911553c7b 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -295,6 +295,7 @@ "See details": "Se detaljer", "See hotel details": "Se hotellinformation", "See less FAQ": "See färre FAQ", + "See map": "Visa karta", "See on map": "Se på karta", "See room details": "Se rumsdetaljer", "See rooms": "Se rum", @@ -312,7 +313,6 @@ "Show all amenities": "Visa alla bekvämligheter", "Show less": "Visa mindre", "Show less rooms": "Visa färre rum", - "Show map": "Visa karta", "Show more": "Visa mer", "Show more rooms": "Visa fler rum", "Sign up bonus": "Välkomstbonus", From eaf9aa7277140bae238468e7d12abf2e25aa367d Mon Sep 17 00:00:00 2001 From: Anton Gunnarsson Date: Fri, 15 Nov 2024 11:48:52 +0100 Subject: [PATCH 83/98] Fix BookingWidgetSkeleton after merge --- components/BookingWidget/Client.tsx | 8 +++++--- components/BookingWidget/MobileToggleButton/index.tsx | 7 +------ .../MainMenu/NavigationMenu/NavigationMenuList/index.tsx | 4 +++- components/Header/MainMenu/index.tsx | 2 +- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/components/BookingWidget/Client.tsx b/components/BookingWidget/Client.tsx index 39f186814..3f7bc5ca3 100644 --- a/components/BookingWidget/Client.tsx +++ b/components/BookingWidget/Client.tsx @@ -188,10 +188,12 @@ export default function BookingWidgetClient({ export function BookingWidgetSkeleton() { return ( <> -
    - +
    + +
    + +
    - ) } diff --git a/components/BookingWidget/MobileToggleButton/index.tsx b/components/BookingWidget/MobileToggleButton/index.tsx index 03d8d8f93..99e691359 100644 --- a/components/BookingWidget/MobileToggleButton/index.tsx +++ b/components/BookingWidget/MobileToggleButton/index.tsx @@ -121,14 +121,9 @@ export default function MobileToggleButton({ export function MobileToggleButtonSkeleton() { const intl = useIntl() - const bookingWidgetMobileRef = useRef(null) - useStickyPosition({ - ref: bookingWidgetMobileRef, - name: StickyElementNameEnum.BOOKING_WIDGET_MOBILE, - }) return ( -
    +
    {intl.formatMessage({ id: "Where to" })} diff --git a/components/Header/MainMenu/NavigationMenu/NavigationMenuList/index.tsx b/components/Header/MainMenu/NavigationMenu/NavigationMenuList/index.tsx index 5d1b10185..2aa95a0cb 100644 --- a/components/Header/MainMenu/NavigationMenu/NavigationMenuList/index.tsx +++ b/components/Header/MainMenu/NavigationMenu/NavigationMenuList/index.tsx @@ -1,10 +1,12 @@ +import { cx } from "class-variance-authority" + import SkeletonShimmer from "@/components/SkeletonShimmer" + import NavigationMenuItem from "../NavigationMenuItem" import styles from "./navigationMenuList.module.css" import type { NavigationMenuListProps } from "@/types/components/header/navigationMenuList" -import { cx } from "class-variance-authority" export default function NavigationMenuList({ isMobile, diff --git a/components/Header/MainMenu/index.tsx b/components/Header/MainMenu/index.tsx index 208a64b89..e1be0051f 100644 --- a/components/Header/MainMenu/index.tsx +++ b/components/Header/MainMenu/index.tsx @@ -5,6 +5,7 @@ import Image from "@/components/Image" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" +import { NavigationMenuListSkeleton } from "./NavigationMenu/NavigationMenuList" import { MobileMenuSkeleton } from "./MobileMenu" import MobileMenuWrapper from "./MobileMenuWrapper" import MyPagesMenuWrapper, { @@ -13,7 +14,6 @@ import MyPagesMenuWrapper, { import NavigationMenu from "./NavigationMenu" import styles from "./mainMenu.module.css" -import { NavigationMenuListSkeleton } from "./NavigationMenu/NavigationMenuList" export default function MainMenu() { return ( From 51f8412e2b70f88c70e13dabcbca8e8ba84d4fb3 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Fri, 15 Nov 2024 12:04:07 +0100 Subject: [PATCH 84/98] feat(SW-630) Added alerts --- components/HotelReservation/HotelCard/index.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 451de4630..e9204c709 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -7,6 +7,7 @@ import { selectHotelMap } from "@/constants/routes/hotelReservation" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import ImageGallery from "@/components/ImageGallery" +import Alert from "@/components/TempDesignSystem/Alert" import Button from "@/components/TempDesignSystem/Button" import Divider from "@/components/TempDesignSystem/Divider" import Link from "@/components/TempDesignSystem/Link" @@ -38,6 +39,8 @@ export default function HotelCard({ const { hotelData } = hotel const { price } = hotel + console.log(hotelData) + const amenities = hotelData.detailedFacilities.slice(0, 5) const classNames = hotelCardVariants({ @@ -132,6 +135,17 @@ export default function HotelCard({ hotel={hotelData} showCTA={true} /> + {hotelData.specialAlerts.length > 0 && ( +
    + {hotelData.specialAlerts.map((alert) => ( + + ))} +
    + )}
    From fd6c15ed78c7a80072544a01c047a9ca6216858f Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Mon, 4 Nov 2024 15:55:49 +0100 Subject: [PATCH 85/98] feat: add env var for signup flow --- .env.local.example | 3 ++- .../(live)/(public)/[contentType]/[uid]/page.tsx | 13 +++++++++++-- constants/routes/signup.ts | 10 ++++++++++ env/server.ts | 8 ++++++++ 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/.env.local.example b/.env.local.example index e1b8e182f..c6f85bec7 100644 --- a/.env.local.example +++ b/.env.local.example @@ -51,5 +51,6 @@ GOOGLE_STATIC_MAP_SIGNATURE_SECRET="" GOOGLE_STATIC_MAP_ID="" GOOGLE_DYNAMIC_MAP_ID="" -HIDE_FOR_NEXT_RELEASE="true" +HIDE_FOR_NEXT_RELEASE="false" +SHOW_SIGNUP_FLOW="true" USE_NEW_REWARDS_ENDPOINT="true" diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx index d96f10f38..5283bdeaf 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx @@ -1,5 +1,7 @@ +import { headers } from "next/headers" import { notFound } from "next/navigation" +import { isSignupPage } from "@/constants/routes/signup" import { env } from "@/env/server" import HotelPage from "@/components/ContentType/HotelPage" @@ -22,17 +24,24 @@ export default function ContentTypePage({ }: PageArgs) { setLang(params.lang) + const pathname = headers().get("x-pathname") || "" + const isSignupRoute = isSignupPage(pathname) + switch (params.contentType) { case "collection-page": if (env.HIDE_FOR_NEXT_RELEASE) { return notFound() } return - case "content-page": - if (env.HIDE_FOR_NEXT_RELEASE) { + case "content-page": { + if (!isSignupRoute && env.HIDE_FOR_NEXT_RELEASE) { + return notFound() + } + if (isSignupRoute && !env.SHOW_SIGNUP_FLOW) { return notFound() } return + } case "loyalty-page": return case "hotel-page": diff --git a/constants/routes/signup.ts b/constants/routes/signup.ts index 82a39ee41..44e368024 100644 --- a/constants/routes/signup.ts +++ b/constants/routes/signup.ts @@ -17,3 +17,13 @@ export const signupVerify: LangRoute = { da: `${signup.da}/bekraeft`, de: `${signup.de}/verifizieren`, } + +export function isSignupPage(path: string): boolean { + const signupPaths = [...Object.values(signup), ...Object.values(signupVerify)] + const result = signupPaths.some((signupPath) => { + const includes = signupPath.includes(path) + return includes + }) + + return result +} diff --git a/env/server.ts b/env/server.ts index 1f81557ce..57fc05acf 100644 --- a/env/server.ts +++ b/env/server.ts @@ -60,6 +60,13 @@ export const env = createEnv({ SEAMLESS_LOGOUT_FI: z.string(), SEAMLESS_LOGOUT_NO: z.string(), SEAMLESS_LOGOUT_SV: z.string(), + SHOW_SIGNUP_FLOW: z + .string() + // only allow "true" or "false" + .refine((s) => s === "true" || s === "false") + // transform to boolean + .transform((s) => s === "true") + .default("false"), WEBVIEW_ENCRYPTION_KEY: z.string(), BOOKING_ENCRYPTION_KEY: z.string(), GOOGLE_STATIC_MAP_KEY: z.string(), @@ -125,6 +132,7 @@ export const env = createEnv({ SEAMLESS_LOGOUT_FI: process.env.SEAMLESS_LOGOUT_FI, SEAMLESS_LOGOUT_NO: process.env.SEAMLESS_LOGOUT_NO, SEAMLESS_LOGOUT_SV: process.env.SEAMLESS_LOGOUT_SV, + SHOW_SIGNUP_FLOW: process.env.SHOW_SIGNUP_FLOW, WEBVIEW_ENCRYPTION_KEY: process.env.WEBVIEW_ENCRYPTION_KEY, BOOKING_ENCRYPTION_KEY: process.env.BOOKING_ENCRYPTION_KEY, GOOGLE_STATIC_MAP_KEY: process.env.GOOGLE_STATIC_MAP_KEY, From 5fef56cc971337110e65e468c526a6f57ead7df1 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Wed, 13 Nov 2024 14:10:15 +0100 Subject: [PATCH 86/98] fix(SW-890): better feature flag handling --- .../(public)/[contentType]/[uid]/page.tsx | 19 ++++++++++++++----- constants/routes/signup.ts | 7 +------ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx index 5283bdeaf..215e3aec8 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx @@ -25,7 +25,6 @@ export default function ContentTypePage({ setLang(params.lang) const pathname = headers().get("x-pathname") || "" - const isSignupRoute = isSignupPage(pathname) switch (params.contentType) { case "collection-page": @@ -34,12 +33,22 @@ export default function ContentTypePage({ } return case "content-page": { - if (!isSignupRoute && env.HIDE_FOR_NEXT_RELEASE) { - return notFound() + const isSignupRoute = isSignupPage(pathname) + + if (env.HIDE_FOR_NEXT_RELEASE) { + // Hide content pages for next release for non-signup routes. + if (!isSignupRoute) { + return notFound() + } } - if (isSignupRoute && !env.SHOW_SIGNUP_FLOW) { - return notFound() + + if (!env.SHOW_SIGNUP_FLOW) { + // Hide content pages for signup routes when signup flow is disabled. + if (isSignupRoute) { + return notFound() + } } + return } case "loyalty-page": diff --git a/constants/routes/signup.ts b/constants/routes/signup.ts index 44e368024..4c63cc47d 100644 --- a/constants/routes/signup.ts +++ b/constants/routes/signup.ts @@ -20,10 +20,5 @@ export const signupVerify: LangRoute = { export function isSignupPage(path: string): boolean { const signupPaths = [...Object.values(signup), ...Object.values(signupVerify)] - const result = signupPaths.some((signupPath) => { - const includes = signupPath.includes(path) - return includes - }) - - return result + return signupPaths.some((signupPath) => signupPath.includes(path)) } From d65be1845ac3237d15424a5883604365e9be29ee Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Fri, 15 Nov 2024 12:57:32 +0100 Subject: [PATCH 87/98] fix(SW-890): more reliable checking of user --- .../Blocks/DynamicContent/SignUpVerification/index.tsx | 6 +++--- .../Blocks/DynamicContent/SignupFormWrapper/index.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/components/Blocks/DynamicContent/SignUpVerification/index.tsx b/components/Blocks/DynamicContent/SignUpVerification/index.tsx index 2b182d4bf..4ae9fd899 100644 --- a/components/Blocks/DynamicContent/SignUpVerification/index.tsx +++ b/components/Blocks/DynamicContent/SignUpVerification/index.tsx @@ -1,8 +1,8 @@ import { redirect } from "next/navigation" import { overview } from "@/constants/routes/myPages" +import { getProfileSafely } from "@/lib/trpc/memoizedRequests" -import { auth } from "@/auth" import LoginButton from "@/components/LoginButton" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" @@ -14,8 +14,8 @@ import type { SignUpVerificationProps } from "@/types/components/blocks/dynamicC export default async function SignUpVerification({ dynamic_content, }: SignUpVerificationProps) { - const session = await auth() - if (session) { + const user = await getProfileSafely() + if (user) { redirect(overview[getLang()]) } const intl = await getIntl() diff --git a/components/Blocks/DynamicContent/SignupFormWrapper/index.tsx b/components/Blocks/DynamicContent/SignupFormWrapper/index.tsx index bf6af6294..7daf85588 100644 --- a/components/Blocks/DynamicContent/SignupFormWrapper/index.tsx +++ b/components/Blocks/DynamicContent/SignupFormWrapper/index.tsx @@ -1,8 +1,8 @@ import { redirect } from "next/navigation" import { overview } from "@/constants/routes/myPages" +import { getProfileSafely } from "@/lib/trpc/memoizedRequests" -import { auth } from "@/auth" import SignupForm from "@/components/Forms/Signup" import { getLang } from "@/i18n/serverContext" @@ -11,8 +11,8 @@ import { SignupFormWrapperProps } from "@/types/components/blocks/dynamicContent export default async function SignupFormWrapper({ dynamic_content, }: SignupFormWrapperProps) { - const session = await auth() - if (session) { + const user = await getProfileSafely() + if (user) { // We don't want to allow users to access signup if they are already authenticated. redirect(overview[getLang()]) } From 821f667d064463e3e502d904ef7d5f569f9a21a6 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Fri, 15 Nov 2024 14:06:33 +0100 Subject: [PATCH 88/98] feat(SW-630) Added alerts to hotel cards --- .../HotelCard/hotelCard.module.css | 6 ++++ .../HotelReservation/HotelCard/index.tsx | 8 +---- components/TempDesignSystem/Alert/index.tsx | 36 ++++++++++--------- 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/components/HotelReservation/HotelCard/hotelCard.module.css b/components/HotelReservation/HotelCard/hotelCard.module.css index a0c15eb5b..b20a7e30d 100644 --- a/components/HotelReservation/HotelCard/hotelCard.module.css +++ b/components/HotelReservation/HotelCard/hotelCard.module.css @@ -85,6 +85,12 @@ min-width: 160px; } +.specialAlerts { + display: flex; + flex-direction: column; + gap: var(--Spacing-x1); +} + @media screen and (min-width: 1367px) { .card.pageListing { flex-direction: row; diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index e9204c709..4c478d275 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -39,8 +39,6 @@ export default function HotelCard({ const { hotelData } = hotel const { price } = hotel - console.log(hotelData) - const amenities = hotelData.detailedFacilities.slice(0, 5) const classNames = hotelCardVariants({ @@ -138,11 +136,7 @@ export default function HotelCard({ {hotelData.specialAlerts.length > 0 && (
    {hotelData.specialAlerts.map((alert) => ( - + ))}
    )} diff --git a/components/TempDesignSystem/Alert/index.tsx b/components/TempDesignSystem/Alert/index.tsx index 2c511b725..8f063d39d 100644 --- a/components/TempDesignSystem/Alert/index.tsx +++ b/components/TempDesignSystem/Alert/index.tsx @@ -44,23 +44,25 @@ export default function Alert({

    {heading}

    ) : null} - - {text} - {phoneContact?.phoneNumber ? ( - <> - {phoneContact.displayText} - - {phoneContact.phoneNumber} - - {phoneContact.footnote ? ( - . ({phoneContact.footnote}) - ) : null} - - ) : null} - + {text ? ( + + {text} + {phoneContact?.phoneNumber ? ( + <> + {phoneContact.displayText} + + {phoneContact.phoneNumber} + + {phoneContact.footnote ? ( + . ({phoneContact.footnote}) + ) : null} + + ) : null} + + ) : null}
    {link ? ( From c8937d121f88c0244730a2a0e83736ae5c2d901d Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Fri, 15 Nov 2024 14:37:08 +0100 Subject: [PATCH 89/98] fix(SW-839): Fallback if member is not available --- .../SelectRate/RoomSelection/RateSummary/index.tsx | 2 +- types/components/hotelReservation/selectRate/selectRate.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx index 33400efaf..e23299e0d 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx @@ -35,7 +35,7 @@ export default function RateSummary({ roomType, priceName, } = rateSummary - const priceToShow = isUserLoggedIn ? member : publicRate + const priceToShow = isUserLoggedIn && member ? member : publicRate const isPetRoomSelected = features.some( (feature) => feature.code === RoomPackageCodeEnum.PET_ROOM diff --git a/types/components/hotelReservation/selectRate/selectRate.ts b/types/components/hotelReservation/selectRate/selectRate.ts index ba8f3f45c..a1da31b84 100644 --- a/types/components/hotelReservation/selectRate/selectRate.ts +++ b/types/components/hotelReservation/selectRate/selectRate.ts @@ -29,6 +29,6 @@ export interface Rate { roomTypeCode: RoomConfiguration["roomTypeCode"] priceName: string public: Product["productType"]["public"] - member: Product["productType"]["member"] + member?: Product["productType"]["member"] features: RoomConfiguration["features"] } From 43ef48e2c7fa5d3879e53a847d7b19510166625c Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Tue, 12 Nov 2024 08:43:29 +0100 Subject: [PATCH 90/98] fix: move packages schemas --- .../(standard)/[step]/@summary/page.tsx | 18 +++++- lib/trpc/memoizedRequests/index.ts | 11 +++- server/routers/hotels/input.ts | 9 +++ server/routers/hotels/output.ts | 43 ++++++++++++- server/routers/hotels/query.ts | 6 +- server/routers/hotels/schemas/packages.ts | 63 ------------------- .../hotelReservation/selectRate/roomCard.ts | 2 +- .../hotelReservation/selectRate/roomFilter.ts | 2 +- types/requests/packages.ts | 8 ++- 9 files changed, 87 insertions(+), 75 deletions(-) delete mode 100644 server/routers/hotels/schemas/packages.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx index ccda720a2..a91aa9cb0 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx @@ -1,4 +1,5 @@ import { + getPackages, getProfileSafely, getSelectedRoomAvailability, } from "@/lib/trpc/memoizedRequests" @@ -22,7 +23,13 @@ export default async function SummaryPage({ const { hotel, rooms, fromDate, toDate } = getQueryParamsForEnterDetails(selectRoomParams) - const { adults, children, roomTypeCode, rateCode } = rooms[0] // TODO: Handle multiple rooms + const { + adults, + children, + roomTypeCode, + rateCode, + packages: packageCodes, + } = rooms[0] // TODO: Handle multiple rooms const availability = await getSelectedRoomAvailability({ hotelId: hotel, @@ -34,6 +41,14 @@ export default async function SummaryPage({ roomTypeCode, }) const user = await getProfileSafely() + const packages = await getPackages({ + hotelId: hotel, + startDate: fromDate, + endDate: toDate, + adults, + children: children?.length, + packageCodes, + }) if (!availability) { console.error("No hotel or availability data", availability) @@ -64,6 +79,7 @@ export default async function SummaryPage({ }, } + console.log({ packages }) return ( <>
    diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index a90dc6907..407175703 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -8,7 +8,10 @@ import { import { serverClient } from "../server" -import type { BreackfastPackagesInput } from "@/types/requests/packages" +import type { + BreackfastPackagesInput, + PackagesInput, +} from "@/types/requests/packages" export const getLocations = cache(async function getMemoizedLocations() { return serverClient().hotel.locations.get() @@ -144,6 +147,12 @@ export const getBreakfastPackages = cache(async function getMemoizedPackages( return serverClient().hotel.packages.breakfast(input) }) +export const getPackages = cache(async function getMemoizedPackages( + input: PackagesInput +) { + return serverClient().hotel.packages.get(input) +}) + export const getBookingConfirmation = cache( function getMemoizedBookingConfirmation(confirmationNumber: string) { return serverClient().booking.confirmation({ confirmationNumber }) diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index 6b52918f7..f6b78f1ac 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -68,3 +68,12 @@ export const getBreakfastPackageInputSchema = z.object({ .min(1, { message: "toDate is required" }) .pipe(z.coerce.date()), }) + +export const getRoomPackagesInputSchema = z.object({ + hotelId: z.string(), + startDate: z.string(), + endDate: z.string(), + adults: z.number(), + children: z.number().optional().default(0), + packageCodes: z.array(z.string()).optional().default([]), +}) diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 32640cf62..aaacd60ae 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -825,7 +825,7 @@ export const apiLocationsSchema = z.object({ ), }) -const breakfastPackagePriceSchema = z.object({ +export const packagePriceSchema = z.object({ currency: z.nativeEnum(CurrencyEnum), price: z.string(), totalPrice: z.string(), @@ -834,8 +834,8 @@ const breakfastPackagePriceSchema = z.object({ export const breakfastPackageSchema = z.object({ code: z.string(), description: z.string(), - localPrice: breakfastPackagePriceSchema, - requestedPrice: breakfastPackagePriceSchema, + localPrice: packagePriceSchema, + requestedPrice: packagePriceSchema, packageType: z.literal(PackageTypeEnum.BreakfastAdult), }) @@ -852,3 +852,40 @@ export const breakfastPackagesSchema = z .transform(({ data }) => data.attributes.packages.filter((pkg) => pkg.code.match(/^(BRF\d+)$/gm)) ) + +export const packagesSchema = z.object({ + code: z.nativeEnum(RoomPackageCodeEnum), + itemCode: z.string(), + description: z.string(), + localPrice: packagePriceSchema, + requestedPrice: packagePriceSchema, + inventories: z.array( + z.object({ + date: z.string(), + total: z.number(), + available: z.number(), + }) + ), +}) + +export const getRoomPackagesSchema = z + .object({ + data: z.object({ + attributes: z.object({ + hotelId: z.number(), + packages: z.array(packagesSchema), + }), + relationships: z + .object({ + links: z.array( + z.object({ + url: z.string(), + type: z.string(), + }) + ), + }) + .optional(), + type: z.string(), + }), + }) + .transform((data) => data.data.attributes.packages) diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index d8a6ec2bc..9b0b97d59 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -28,15 +28,12 @@ import { validateHotelPageRefs, } from "../contentstack/hotelPage/utils" import { getVerifiedUser, parsedUser } from "../user/query" -import { - getRoomPackagesInputSchema, - getRoomPackagesSchema, -} from "./schemas/packages" import { getBreakfastPackageInputSchema, getHotelDataInputSchema, getHotelsAvailabilityInputSchema, getRatesInputSchema, + getRoomPackagesInputSchema, getRoomsAvailabilityInputSchema, getSelectedRoomAvailabilityInputSchema, type HotelDataInput, @@ -46,6 +43,7 @@ import { getHotelDataSchema, getHotelsAvailabilitySchema, getRatesSchema, + getRoomPackagesSchema, getRoomsAvailabilitySchema, } from "./output" import tempRatesData from "./tempRatesData.json" diff --git a/server/routers/hotels/schemas/packages.ts b/server/routers/hotels/schemas/packages.ts deleted file mode 100644 index 738da80ce..000000000 --- a/server/routers/hotels/schemas/packages.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { z } from "zod" - -import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" -import { CurrencyEnum } from "@/types/enums/currency" - -export const getRoomPackagesInputSchema = z.object({ - hotelId: z.string(), - startDate: z.string(), - endDate: z.string(), - adults: z.number(), - children: z.number().optional().default(0), - packageCodes: z.array(z.string()).optional().default([]), -}) - -export const packagePriceSchema = z - .object({ - currency: z.nativeEnum(CurrencyEnum), - price: z.string(), - totalPrice: z.string(), - }) - .optional() - .default({ - currency: CurrencyEnum.SEK, - price: "0", - totalPrice: "0", - }) // TODO: Remove optional and default when the API change has been deployed - -export const packagesSchema = z.object({ - code: z.nativeEnum(RoomPackageCodeEnum), - itemCode: z.string(), - description: z.string(), - localPrice: packagePriceSchema, - requestedPrice: packagePriceSchema, - inventories: z.array( - z.object({ - date: z.string(), - total: z.number(), - available: z.number(), - }) - ), -}) - -export const getRoomPackagesSchema = z - .object({ - data: z.object({ - attributes: z.object({ - hotelId: z.number(), - packages: z.array(packagesSchema), - }), - relationships: z - .object({ - links: z.array( - z.object({ - url: z.string(), - type: z.string(), - }) - ), - }) - .optional(), - type: z.string(), - }), - }) - .transform((data) => data.data.attributes.packages) diff --git a/types/components/hotelReservation/selectRate/roomCard.ts b/types/components/hotelReservation/selectRate/roomCard.ts index caf025524..0f76afb56 100644 --- a/types/components/hotelReservation/selectRate/roomCard.ts +++ b/types/components/hotelReservation/selectRate/roomCard.ts @@ -1,10 +1,10 @@ import { z } from "zod" import { + packagePriceSchema, RateDefinition, RoomConfiguration, } from "@/server/routers/hotels/output" -import { packagePriceSchema } from "@/server/routers/hotels/schemas/packages" import { RoomPriceSchema } from "./flexibilityOption" import { Rate } from "./selectRate" diff --git a/types/components/hotelReservation/selectRate/roomFilter.ts b/types/components/hotelReservation/selectRate/roomFilter.ts index 8250e7f32..78980be83 100644 --- a/types/components/hotelReservation/selectRate/roomFilter.ts +++ b/types/components/hotelReservation/selectRate/roomFilter.ts @@ -3,7 +3,7 @@ import { z } from "zod" import { getRoomPackagesSchema, packagesSchema, -} from "@/server/routers/hotels/schemas/packages" +} from "@/server/routers/hotels/output" export enum RoomPackageCodeEnum { PET_ROOM = "PETR", diff --git a/types/requests/packages.ts b/types/requests/packages.ts index 3d794e0f3..222a4970f 100644 --- a/types/requests/packages.ts +++ b/types/requests/packages.ts @@ -1,6 +1,12 @@ import { z } from "zod" -import { getBreakfastPackageInputSchema } from "@/server/routers/hotels/input" +import { + getBreakfastPackageInputSchema, + getRoomPackagesInputSchema, +} from "@/server/routers/hotels/input" export interface BreackfastPackagesInput extends z.input {} + +export interface PackagesInput + extends z.input {} From dbb5367df22d12c3a3715644f5fc70514ed6091c Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Tue, 12 Nov 2024 08:45:14 +0100 Subject: [PATCH 91/98] fix: filter out room availability based on packages --- .../(standard)/[step]/@summary/page.tsx | 9 ++++- .../(standard)/[step]/page.tsx | 4 +- server/routers/hotels/input.ts | 3 ++ server/routers/hotels/query.ts | 37 ++++++++++++++----- 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx index a91aa9cb0..d97115df5 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx @@ -1,3 +1,6 @@ +import { redirect } from "next/navigation" + +import { selectRate } from "@/constants/routes/hotelReservation" import { getPackages, getProfileSafely, @@ -17,6 +20,7 @@ import { SelectRateSearchParams } from "@/types/components/hotelReservation/sele import { LangParams, PageArgs, SearchParams } from "@/types/params" export default async function SummaryPage({ + params, searchParams, }: PageArgs>) { const selectRoomParams = new URLSearchParams(searchParams) @@ -39,6 +43,7 @@ export default async function SummaryPage({ roomStayEndDate: toDate, rateCode, roomTypeCode, + packageCodes, }) const user = await getProfileSafely() const packages = await getPackages({ @@ -50,10 +55,10 @@ export default async function SummaryPage({ packageCodes, }) - if (!availability) { + if (!availability || !availability.selectedRoom) { console.error("No hotel or availability data", availability) // TODO: handle this case - return null + redirect(selectRate[params.lang]) } const prices = diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 982157ceb..f7c155d6c 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -46,7 +46,7 @@ export default async function StepPage({ toDate, } = getQueryParamsForEnterDetails(selectRoomParams) - const { adults, children, roomTypeCode, rateCode } = rooms[0] // TODO: Handle multiple rooms + const { adults, children, roomTypeCode, rateCode, packages } = rooms[0] // TODO: Handle multiple rooms const childrenAsString = children && generateChildrenString(children) @@ -60,6 +60,7 @@ export default async function StepPage({ roomStayEndDate: toDate, rateCode, roomTypeCode, + packageCodes: packages, }) const roomAvailability = await getSelectedRoomAvailability({ @@ -70,6 +71,7 @@ export default async function StepPage({ roomStayEndDate: toDate, rateCode, roomTypeCode, + packageCodes: packages, }) const hotelData = await getHotelData({ hotelId, diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index f6b78f1ac..04bb16b17 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -1,5 +1,7 @@ import { z } from "zod" +import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" + export const getHotelsAvailabilityInputSchema = z.object({ cityId: z.string(), roomStayStartDate: z.string(), @@ -34,6 +36,7 @@ export const getSelectedRoomAvailabilityInputSchema = z.object({ attachedProfileId: z.string().optional().default(""), rateCode: z.string(), roomTypeCode: z.string(), + packageCodes: z.array(z.nativeEnum(RoomPackageCodeEnum)).optional(), }) export type GetSelectedRoomAvailabilityInput = z.input< diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 9b0b97d59..20db2922e 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -624,6 +624,7 @@ export const hotelQueryRouter = router({ attachedProfileId, rateCode, roomTypeCode, + packageCodes, } = input const params: Record = { @@ -723,17 +724,35 @@ export const hotelQueryRouter = router({ ctx.serviceToken ) - const selectedRoom = validateAvailabilityData.data.roomConfigurations - .filter((room) => room.status === "Available") - .find((room) => room.roomTypeCode === roomTypeCode) + console.log({ packageCodes }) - const availableRoomsInCategory = - validateAvailabilityData.data.roomConfigurations.filter( - (room) => - room.status === "Available" && - room.roomType === selectedRoom?.roomType - ) + const availableRooms = + validateAvailabilityData.data.roomConfigurations.filter((room) => { + if (packageCodes) { + return ( + room.status === "Available" && + room.features.some( + (feature) => + packageCodes.includes(feature.code) && feature.inventory > 0 + ) + ) + } + return room.status === "Available" + }) + console.log("hrteij", JSON.stringify(availableRooms, null, 4)) + const selectedRoom = availableRooms.find( + (room) => room.roomTypeCode === roomTypeCode + ) + + const availableRoomsInCategory = availableRooms.filter( + (room) => room.roomType === selectedRoom?.roomType + ) + + console.log( + "availableRoomsInCategory", + JSON.stringify(availableRoomsInCategory, null, 4) + ) if (!selectedRoom) { console.error("No matching room found") return null From dc3516f4e1dff57eff06482e08a0c88b7613ba21 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Tue, 12 Nov 2024 10:06:13 +0100 Subject: [PATCH 92/98] feat: add packages info to summary --- .../(standard)/[step]/@summary/page.tsx | 22 +-- .../(standard)/[step]/page.tsx | 12 +- .../GuestsRoomsPicker/Counter/index.tsx | 2 +- .../Header/MainMenu/MyPagesMenu/index.tsx | 2 +- .../EnterDetails/Payment/index.tsx | 20 +-- .../EnterDetails/Summary/index.tsx | 129 ++++++++++-------- .../HotelSelectionHeader/index.tsx | 2 +- .../HotelListingMapContent/index.tsx | 2 +- .../Text/Body/body.module.css | 2 +- .../TempDesignSystem/Text/Body/variants.ts | 2 +- server/routers/booking/input.ts | 4 +- server/routers/hotels/output.ts | 4 +- server/routers/hotels/query.ts | 8 -- .../enterDetails/bookingData.ts | 5 +- .../hotelReservation/selectRate/section.ts | 2 +- types/requests/packages.ts | 3 + 16 files changed, 116 insertions(+), 105 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx index d97115df5..f8c5f20ac 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx @@ -46,14 +46,17 @@ export default async function SummaryPage({ packageCodes, }) const user = await getProfileSafely() - const packages = await getPackages({ - hotelId: hotel, - startDate: fromDate, - endDate: toDate, - adults, - children: children?.length, - packageCodes, - }) + + const packages = packageCodes + ? await getPackages({ + hotelId: hotel, + startDate: fromDate, + endDate: toDate, + adults, + children: children?.length, + packageCodes, + }) + : null if (!availability || !availability.selectedRoom) { console.error("No hotel or availability data", availability) @@ -84,7 +87,6 @@ export default async function SummaryPage({ }, } - console.log({ packages }) return ( <>
    @@ -99,6 +101,7 @@ export default async function SummaryPage({ adults, children, cancellationText: availability.cancellationText, + packages, }} />
    @@ -116,6 +119,7 @@ export default async function SummaryPage({ adults, children, cancellationText: availability.cancellationText, + packages, }} />
    diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index f7c155d6c..70aef0ada 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -46,7 +46,13 @@ export default async function StepPage({ toDate, } = getQueryParamsForEnterDetails(selectRoomParams) - const { adults, children, roomTypeCode, rateCode, packages } = rooms[0] // TODO: Handle multiple rooms + const { + adults, + children, + roomTypeCode, + rateCode, + packages: packageCodes, + } = rooms[0] // TODO: Handle multiple rooms const childrenAsString = children && generateChildrenString(children) @@ -60,7 +66,7 @@ export default async function StepPage({ roomStayEndDate: toDate, rateCode, roomTypeCode, - packageCodes: packages, + packageCodes, }) const roomAvailability = await getSelectedRoomAvailability({ @@ -71,7 +77,7 @@ export default async function StepPage({ roomStayEndDate: toDate, rateCode, roomTypeCode, - packageCodes: packages, + packageCodes, }) const hotelData = await getHotelData({ hotelId, diff --git a/components/GuestsRoomsPicker/Counter/index.tsx b/components/GuestsRoomsPicker/Counter/index.tsx index 099bf9632..30c8e35fe 100644 --- a/components/GuestsRoomsPicker/Counter/index.tsx +++ b/components/GuestsRoomsPicker/Counter/index.tsx @@ -29,7 +29,7 @@ export default function Counter({ > - + {count}
    {intl.formatMessage( { id: "{amount} {currency}" }, { - amount: intl.formatNumber( - parseInt(room.localPrice.price ?? "0") - ), + amount: intl.formatNumber(room.localPrice.price), currency: room.localPrice.currency, } )} @@ -180,17 +175,37 @@ export default function Summary({ {intl.formatMessage({ id: "Rate details" })} + {room.packages + ? room.packages.map((roomPackage) => ( +
    +
    + + {roomPackage.description} + +
    +
    + {intl.formatMessage( + { id: "{amount} {currency}" }, + { + amount: roomPackage.localPrice.price, + currency: roomPackage.localPrice.currency, + } + )} + {intl.formatMessage({ id: "Based on availability" })} + {intl.formatMessage( { id: "{amount} {currency}" }, { amount: "0", currency: room.localPrice.currency } @@ -202,10 +217,10 @@ export default function Summary({ {chosenBreakfast ? ( chosenBreakfast === BreakfastPackageEnum.NO_BREAKFAST ? (
    - + {intl.formatMessage({ id: "No breakfast" })} -
    + {intl.formatMessage( { id: "{amount} {currency}" }, { amount: "0", currency: room.localPrice.currency } @@ -214,10 +229,10 @@ export default function Summary({ ) : (
    - + {intl.formatMessage({ id: "Breakfast buffet" })} -
    + {intl.formatMessage( { id: "{amount} {currency}" }, { diff --git a/components/HotelReservation/HotelSelectionHeader/index.tsx b/components/HotelReservation/HotelSelectionHeader/index.tsx index 4cb88f67f..c0045dff5 100644 --- a/components/HotelReservation/HotelSelectionHeader/index.tsx +++ b/components/HotelReservation/HotelSelectionHeader/index.tsx @@ -41,7 +41,7 @@ export default function HotelSelectionHeader({
    - + {hotel.hotelContent.texts.descriptions.short}
    diff --git a/components/Maps/InteractiveMap/HotelListingMapContent/index.tsx b/components/Maps/InteractiveMap/HotelListingMapContent/index.tsx index c3b83edb3..ce9e95020 100644 --- a/components/Maps/InteractiveMap/HotelListingMapContent/index.tsx +++ b/components/Maps/InteractiveMap/HotelListingMapContent/index.tsx @@ -73,7 +73,7 @@ export default function HotelListingMapContent({ {pin.memberPrice} {pin.currency} diff --git a/components/TempDesignSystem/Text/Body/body.module.css b/components/TempDesignSystem/Text/Body/body.module.css index a39439815..5cd96c0ae 100644 --- a/components/TempDesignSystem/Text/Body/body.module.css +++ b/components/TempDesignSystem/Text/Body/body.module.css @@ -80,7 +80,7 @@ color: var(--Base-Text-Medium-contrast); } -.textHighContrast { +.baseTextHighContrast { color: var(--Base-Text-High-contrast); } diff --git a/components/TempDesignSystem/Text/Body/variants.ts b/components/TempDesignSystem/Text/Body/variants.ts index aa614fe14..00ee46a3d 100644 --- a/components/TempDesignSystem/Text/Body/variants.ts +++ b/components/TempDesignSystem/Text/Body/variants.ts @@ -13,7 +13,7 @@ const config = { red: styles.red, textMediumContrast: styles.textMediumContrast, baseTextMediumContrast: styles.baseTextMediumContrast, - textHighContrast: styles.textHighContrast, + baseTextHighContrast: styles.baseTextHighContrast, white: styles.white, peach50: styles.peach50, uiTextHighContrast: styles.uiTextHighContrast, diff --git a/server/routers/booking/input.ts b/server/routers/booking/input.ts index b5bc65a30..4c7d802ef 100644 --- a/server/routers/booking/input.ts +++ b/server/routers/booking/input.ts @@ -14,13 +14,11 @@ const roomsSchema = z.array( ) .default([]), rateCode: z.string(), - roomTypeCode: z.string(), + roomTypeCode: z.coerce.string(), guest: z.object({ - title: z.string(), firstName: z.string(), lastName: z.string(), email: z.string().email(), - phoneCountryCodePrefix: z.string().nullable(), phoneNumber: z.string(), countryCode: z.string(), membershipNumber: z.string().optional(), diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index aaacd60ae..da202c7d3 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -541,8 +541,8 @@ export type HotelsAvailabilityPrices = HotelsAvailability["data"][number]["attributes"]["bestPricePerNight"] export const priceSchema = z.object({ - pricePerNight: z.string(), - pricePerStay: z.string(), + pricePerNight: z.coerce.number(), + pricePerStay: z.coerce.number(), currency: z.string(), }) diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 20db2922e..11f235ab6 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -724,8 +724,6 @@ export const hotelQueryRouter = router({ ctx.serviceToken ) - console.log({ packageCodes }) - const availableRooms = validateAvailabilityData.data.roomConfigurations.filter((room) => { if (packageCodes) { @@ -740,7 +738,6 @@ export const hotelQueryRouter = router({ return room.status === "Available" }) - console.log("hrteij", JSON.stringify(availableRooms, null, 4)) const selectedRoom = availableRooms.find( (room) => room.roomTypeCode === roomTypeCode ) @@ -748,11 +745,6 @@ export const hotelQueryRouter = router({ const availableRoomsInCategory = availableRooms.filter( (room) => room.roomType === selectedRoom?.roomType ) - - console.log( - "availableRoomsInCategory", - JSON.stringify(availableRoomsInCategory, null, 4) - ) if (!selectedRoom) { console.error("No matching room found") return null diff --git a/types/components/hotelReservation/enterDetails/bookingData.ts b/types/components/hotelReservation/enterDetails/bookingData.ts index 89ca94efb..74ed4bda4 100644 --- a/types/components/hotelReservation/enterDetails/bookingData.ts +++ b/types/components/hotelReservation/enterDetails/bookingData.ts @@ -1,6 +1,8 @@ import { RoomPackageCodeEnum } from "../selectRate/roomFilter" import { Child } from "../selectRate/selectRate" +import { Packages } from "@/types/requests/packages" + interface Room { adults: number roomTypeCode: string @@ -16,7 +18,7 @@ export interface BookingData { } type Price = { - price: string + price: number currency: string } @@ -27,4 +29,5 @@ export type RoomsData = { adults: number children?: Child[] cancellationText: string + packages: Packages | null } diff --git a/types/components/hotelReservation/selectRate/section.ts b/types/components/hotelReservation/selectRate/section.ts index df9ec7a71..578819fb1 100644 --- a/types/components/hotelReservation/selectRate/section.ts +++ b/types/components/hotelReservation/selectRate/section.ts @@ -28,7 +28,7 @@ export interface BreakfastSelectionProps extends SectionProps { export interface DetailsProps extends SectionProps {} export interface PaymentProps { - roomPrice: string + roomPrice: number otherPaymentOptions: string[] savedCreditCards: CreditCard[] | null mustBeGuaranteed: boolean diff --git a/types/requests/packages.ts b/types/requests/packages.ts index 222a4970f..fb242917f 100644 --- a/types/requests/packages.ts +++ b/types/requests/packages.ts @@ -4,9 +4,12 @@ import { getBreakfastPackageInputSchema, getRoomPackagesInputSchema, } from "@/server/routers/hotels/input" +import { getRoomPackagesSchema } from "@/server/routers/hotels/output" export interface BreackfastPackagesInput extends z.input {} export interface PackagesInput extends z.input {} + +export interface Packages extends z.output {} From 8b89801a6d8a5a86ba18de2809d6d8dcafb31ad8 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Fri, 15 Nov 2024 16:45:25 +0100 Subject: [PATCH 93/98] Fix(SW-343): Updated search and booking widget comments --- .../Forms/BookingWidget/FormContent/Search/search.module.css | 3 ++- .../Forms/BookingWidget/FormContent/formContent.module.css | 2 +- components/Header/MainMenu/mainMenu.module.css | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/components/Forms/BookingWidget/FormContent/Search/search.module.css b/components/Forms/BookingWidget/FormContent/Search/search.module.css index 0b9ecfa8c..902179af9 100644 --- a/components/Forms/BookingWidget/FormContent/Search/search.module.css +++ b/components/Forms/BookingWidget/FormContent/Search/search.module.css @@ -5,11 +5,12 @@ border-radius: var(--Corner-radius-Small); padding: var(--Spacing-x1) var(--Spacing-x-one-and-half); position: relative; + height: 60px; } .container:hover, .container:has(input:active, input:focus, input:focus-within) { - background-color: var(--Base-Surface-Primary-light-Hover-alt); + background-color: var(--Base-Background-Primary-Normal); } .container:has(input:active, input:focus, input:focus-within) { diff --git a/components/Forms/BookingWidget/FormContent/formContent.module.css b/components/Forms/BookingWidget/FormContent/formContent.module.css index 2012bfa7a..a6d7b50ea 100644 --- a/components/Forms/BookingWidget/FormContent/formContent.module.css +++ b/components/Forms/BookingWidget/FormContent/formContent.module.css @@ -80,7 +80,7 @@ .rooms:hover, .when:has([data-isopen="true"]), .rooms:has(.input:active, .input:focus, .input:focus-within) { - background-color: var(--Base-Surface-Primary-light-Hover-alt); + background-color: var(--Base-Background-Primary-Normal); } .where { diff --git a/components/Header/MainMenu/mainMenu.module.css b/components/Header/MainMenu/mainMenu.module.css index 67deaffe4..a4ea15478 100644 --- a/components/Header/MainMenu/mainMenu.module.css +++ b/components/Header/MainMenu/mainMenu.module.css @@ -1,7 +1,7 @@ .mainMenu { background-color: var(--Base-Surface-Primary-light-Normal); padding: var(--Spacing-x2); - border-bottom: 1px solid var(--Base-Border-Subtle); + border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); } .nav { From 0b36e661da395b7a02020a9bf379ccd7dbebbc03 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Fri, 15 Nov 2024 16:46:07 +0100 Subject: [PATCH 94/98] Fix(SW-343): reverted color --- .../Forms/BookingWidget/FormContent/formContent.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Forms/BookingWidget/FormContent/formContent.module.css b/components/Forms/BookingWidget/FormContent/formContent.module.css index a6d7b50ea..2012bfa7a 100644 --- a/components/Forms/BookingWidget/FormContent/formContent.module.css +++ b/components/Forms/BookingWidget/FormContent/formContent.module.css @@ -80,7 +80,7 @@ .rooms:hover, .when:has([data-isopen="true"]), .rooms:has(.input:active, .input:focus, .input:focus-within) { - background-color: var(--Base-Background-Primary-Normal); + background-color: var(--Base-Surface-Primary-light-Hover-alt); } .where { From a17010a431a71b84c8a9324dafbdcadaba98892f Mon Sep 17 00:00:00 2001 From: Hrishikesh Vaipurkar Date: Thu, 14 Nov 2024 23:20:04 +0100 Subject: [PATCH 95/98] feat: SW-602 Implemented no availability states --- .../(standard)/select-rate/page.tsx | 28 +++++---- .../SelectRate/HotelInfoCard/index.tsx | 16 ++++- .../flexibilityOption.module.css | 17 ++++-- .../RoomSelection/FlexibilityOption/index.tsx | 15 +++-- .../RoomSelection/RoomCard/cardVariants.ts | 12 ++++ .../RoomSelection/RoomCard/index.tsx | 58 +++++++++++++------ .../RoomCard/roomCard.module.css | 29 ++++++++++ i18n/dictionaries/da.json | 1 + i18n/dictionaries/de.json | 1 + i18n/dictionaries/en.json | 1 + i18n/dictionaries/fi.json | 1 + i18n/dictionaries/no.json | 1 + i18n/dictionaries/sv.json | 1 + .../selectRate/hotelInfoCardProps.ts | 1 + 14 files changed, 144 insertions(+), 38 deletions(-) create mode 100644 components/HotelReservation/SelectRate/RoomSelection/RoomCard/cardVariants.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index dc5c7a6b5..29c602779 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -84,25 +84,33 @@ export default async function SelectRatePage({ getProfileSafely(), ]) - if (!roomsAvailability) { - return "No rooms found" // TODO: Add a proper error message - } - if (!hotelData) { return "No hotel data found" // TODO: Add a proper error message } const roomCategories = hotelData?.included + const noRoomsAvailable = roomsAvailability?.roomConfigurations.reduce( + (acc, room) => { + return acc && room.status === "NotAvailable" + }, + true + ) + return ( <> - - + {roomsAvailability ? ( + + ) : null} ) } diff --git a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx index 28ce32377..8eabe85ba 100644 --- a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx +++ b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx @@ -15,8 +15,12 @@ import TripAdvisorChip from "../../TripAdvisorChip" import styles from "./hotelInfoCard.module.css" import type { HotelInfoCardProps } from "@/types/components/hotelReservation/selectRate/hotelInfoCardProps" +import { AlertTypeEnum } from "@/types/enums/alert" -export default function HotelInfoCard({ hotelData }: HotelInfoCardProps) { +export default function HotelInfoCard({ + hotelData, + noAvailability = false, +}: HotelInfoCardProps) { const hotelAttributes = hotelData?.data.attributes const intl = useIntl() @@ -97,6 +101,16 @@ export default function HotelInfoCard({ hotelData }: HotelInfoCardProps) { ) })} + {noAvailability ? ( +
    + +
    + ) : null} ) } diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/flexibilityOption.module.css b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/flexibilityOption.module.css index 1f1debe12..80da80184 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/flexibilityOption.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/flexibilityOption.module.css @@ -1,5 +1,5 @@ .card, -.disabledCard { +.noPricesCard { border-radius: var(--Corner-radius-Large); padding: var(--Spacing-x-one-and-half) var(--Spacing-x2); background-color: var(--Base-Surface-Secondary-light-Normal); @@ -10,11 +10,11 @@ gap: var(--Spacing-x-half); } -.disabledCard { - opacity: 0.6; +.noPricesCard { + gap: var(--Spacing-x2); } -.disabledCard:hover { +.noPricesCard:hover { cursor: not-allowed; } @@ -62,9 +62,16 @@ input[type="radio"]:checked + .card .checkIcon { outline: none; } +.noPricesLabel { + padding: var(--Spacing-x-one-and-half) var(--Spacing-x2); + text-align: center; + background-color: var(--Base-Surface-Subtle-Normal); + border-radius: var(--Corner-radius-Rounded); + margin: 0 auto var(--Spacing-x2); +} + .popover { background-color: var(--Main-Grey-White); - border-radius: var(--Corner-radius-Medium); left: 0px; max-height: 400px; diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx index 9bc6ca1f3..5a046978c 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx @@ -1,8 +1,10 @@ "use client" import { useRef, useState } from "react" import { Button } from "react-aria-components" +import { useIntl } from "react-intl" import { CheckIcon, InfoCircleIcon } from "@/components/Icons" +import Label from "@/components/TempDesignSystem/Form/Label" import Caption from "@/components/TempDesignSystem/Text/Caption" import PricePopover from "./Popover" @@ -27,6 +29,7 @@ export default function FlexibilityOption({ const [isPopoverOpen, setIsPopoverOpen] = useState(false) let triggerRef = useRef(null) const buttonClickedRef = useRef(false) + const intl = useIntl() function setRef(node: Element | null) { if (node) { @@ -36,13 +39,17 @@ export default function FlexibilityOption({ if (!product) { return ( -
    +
    -
    {name}({paymentTerm}){name}({paymentTerm}) + {intl.formatMessage({ id: "No Prices available" })} + + {intl.formatMessage({ + id: "This room is not available", + })} +