From 33de623f41a0eb90df94ffba56f1b24a46c16268 Mon Sep 17 00:00:00 2001 From: Simon Emanuelsson Date: Thu, 28 Nov 2024 14:22:31 +0100 Subject: [PATCH 01/37] feat(SW-864): add to calendar functionality --- .../booking-confirmation/page.module.css | 2 +- .../booking-confirmation/page.tsx | 2 - .../payment-callback/page.tsx | 1 - .../Header/Actions/AddToCalendar.tsx | 61 +++++++++++++++++++ .../Header/Actions/DownloadInvoice.tsx | 27 ++++++++ .../Header/Actions/ManageBooking.tsx | 15 +++++ .../Header/Actions/actions.module.css | 15 ----- .../Header/Actions/helpers.ts | 15 +++++ .../Header/Actions/index.tsx | 25 -------- .../Header/header.module.css | 16 +++++ .../BookingConfirmation/Header/index.tsx | 38 +++++++++++- package-lock.json | 53 ++++++++++++++++ package.json | 1 + .../actions/addToCalendar.ts | 9 +++ 14 files changed, 233 insertions(+), 47 deletions(-) create mode 100644 components/HotelReservation/BookingConfirmation/Header/Actions/AddToCalendar.tsx create mode 100644 components/HotelReservation/BookingConfirmation/Header/Actions/DownloadInvoice.tsx create mode 100644 components/HotelReservation/BookingConfirmation/Header/Actions/ManageBooking.tsx delete mode 100644 components/HotelReservation/BookingConfirmation/Header/Actions/actions.module.css create mode 100644 components/HotelReservation/BookingConfirmation/Header/Actions/helpers.ts delete mode 100644 components/HotelReservation/BookingConfirmation/Header/Actions/index.tsx create mode 100644 types/components/hotelReservation/bookingConfirmation/actions/addToCalendar.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.module.css index bba79b2d0..341c3b979 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.module.css @@ -39,4 +39,4 @@ display: grid; grid-area: receipt; } -} +} \ No newline at end of file diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx index c8096f3e6..ea32a277c 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx @@ -23,8 +23,6 @@ export default async function BookingConfirmationPage({ }: PageArgs) { setLang(params.lang) void getBookingConfirmation(searchParams.confirmationNumber) - const { confirmationNumber } = searchParams - return (
}> diff --git a/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx index d934e1155..364f50349 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx @@ -4,7 +4,6 @@ import { BOOKING_CONFIRMATION_NUMBER, PaymentErrorCodeEnum, } from "@/constants/booking" -import { Lang } from "@/constants/languages" import { bookingConfirmation, payment, diff --git a/components/HotelReservation/BookingConfirmation/Header/Actions/AddToCalendar.tsx b/components/HotelReservation/BookingConfirmation/Header/Actions/AddToCalendar.tsx new file mode 100644 index 000000000..a21055bee --- /dev/null +++ b/components/HotelReservation/BookingConfirmation/Header/Actions/AddToCalendar.tsx @@ -0,0 +1,61 @@ +"use client" +import { createEvent } from "ics" +import { useIntl } from "react-intl" + +import { dt } from "@/lib/dt" + +import { CalendarAddIcon } from "@/components/Icons" +import Button from "@/components/TempDesignSystem/Button" +import useLang from "@/hooks/useLang" + +import type { AddToCalendarProps } from "@/types/components/hotelReservation/bookingConfirmation/actions/addToCalendar" + +export default function AddToCalendar({ + checkInDate, + event, + hotelName, +}: AddToCalendarProps) { + const intl = useIntl() + const lang = useLang() + + async function downloadBooking() { + const d = dt(checkInDate).locale(lang).format("YYYY-MM-DD") + const filename = `${hotelName.toLowerCase().split(" ").join("_")}-${d}.ics` + + const file: Blob = await new Promise((resolve, reject) => { + createEvent(event, (error, value) => { + if (error) { + reject(error) + } + + resolve(new File([value], filename, { type: "text/calendar" })) + }) + }) + + const url = URL.createObjectURL(file) + + const anchor = document.createElement("a") + anchor.href = url + anchor.download = filename + + document.body.appendChild(anchor) + anchor.click() + document.body.removeChild(anchor) + + URL.revokeObjectURL(url) + } + + return ( + + ) +} diff --git a/components/HotelReservation/BookingConfirmation/Header/Actions/DownloadInvoice.tsx b/components/HotelReservation/BookingConfirmation/Header/Actions/DownloadInvoice.tsx new file mode 100644 index 000000000..3dea1f011 --- /dev/null +++ b/components/HotelReservation/BookingConfirmation/Header/Actions/DownloadInvoice.tsx @@ -0,0 +1,27 @@ +"use client" +import { useIntl } from "react-intl" + +import { DownloadIcon } from "@/components/Icons" +import Button from "@/components/TempDesignSystem/Button" + +export default function DownloadInvoice() { + const intl = useIntl() + + function downloadBooking() { + window.print() + } + + return ( + + ) +} diff --git a/components/HotelReservation/BookingConfirmation/Header/Actions/ManageBooking.tsx b/components/HotelReservation/BookingConfirmation/Header/Actions/ManageBooking.tsx new file mode 100644 index 000000000..d0c8c2a82 --- /dev/null +++ b/components/HotelReservation/BookingConfirmation/Header/Actions/ManageBooking.tsx @@ -0,0 +1,15 @@ +"use client" +import { useIntl } from "react-intl" + +import { EditIcon } from "@/components/Icons" +import Button from "@/components/TempDesignSystem/Button" + +export default function ManageBooking() { + const intl = useIntl() + return ( + + ) +} diff --git a/components/HotelReservation/BookingConfirmation/Header/Actions/actions.module.css b/components/HotelReservation/BookingConfirmation/Header/Actions/actions.module.css deleted file mode 100644 index c54e54d13..000000000 --- a/components/HotelReservation/BookingConfirmation/Header/Actions/actions.module.css +++ /dev/null @@ -1,15 +0,0 @@ -.actions { - border-radius: var(--Corner-radius-Medium); - display: grid; - grid-area: actions; - justify-content: flex-start; -} - -@media screen and (min-width: 768px) { - .actions { - gap: var(--Spacing-x3); - grid-auto-columns: auto; - grid-auto-flow: column; - grid-template-columns: auto; - } -} diff --git a/components/HotelReservation/BookingConfirmation/Header/Actions/helpers.ts b/components/HotelReservation/BookingConfirmation/Header/Actions/helpers.ts new file mode 100644 index 000000000..7df5288bb --- /dev/null +++ b/components/HotelReservation/BookingConfirmation/Header/Actions/helpers.ts @@ -0,0 +1,15 @@ +import { dt } from "@/lib/dt" + +import type { DateTime } from "ics" + +export function generateDateTime(d: Date): DateTime { + const _d = dt(d).utc() + return [ + _d.year(), + // Need to add +1 since month is 0 based + _d.month() + 1, + _d.date(), + _d.hour(), + _d.minute(), + ] +} diff --git a/components/HotelReservation/BookingConfirmation/Header/Actions/index.tsx b/components/HotelReservation/BookingConfirmation/Header/Actions/index.tsx deleted file mode 100644 index e37296182..000000000 --- a/components/HotelReservation/BookingConfirmation/Header/Actions/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { CalendarAddIcon, DownloadIcon, EditIcon } from "@/components/Icons" -import Button from "@/components/TempDesignSystem/Button" -import { getIntl } from "@/i18n" - -import styles from "./actions.module.css" - -export default async function Actions() { - const intl = await getIntl() - return ( -
- - - -
- ) -} diff --git a/components/HotelReservation/BookingConfirmation/Header/header.module.css b/components/HotelReservation/BookingConfirmation/Header/header.module.css index 5468c7dbf..ebf136448 100644 --- a/components/HotelReservation/BookingConfirmation/Header/header.module.css +++ b/components/HotelReservation/BookingConfirmation/Header/header.module.css @@ -17,6 +17,22 @@ max-width: 720px; } +.actions { + border-radius: var(--Corner-radius-Medium); + display: grid; + grid-area: actions; + justify-content: flex-start; +} + +@media screen and (min-width: 768px) { + .actions { + gap: var(--Spacing-x3); + grid-auto-columns: auto; + grid-auto-flow: column; + grid-template-columns: auto; + } +} + @media screen and (min-width: 1367px) { .header { padding-bottom: var(--Spacing-x4); diff --git a/components/HotelReservation/BookingConfirmation/Header/index.tsx b/components/HotelReservation/BookingConfirmation/Header/index.tsx index bc2660c90..6cb144202 100644 --- a/components/HotelReservation/BookingConfirmation/Header/index.tsx +++ b/components/HotelReservation/BookingConfirmation/Header/index.tsx @@ -5,17 +5,22 @@ import Body from "@/components/TempDesignSystem/Text/Body" import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" -import Actions from "./Actions" +import AddToCalendar from "./Actions/AddToCalendar" +import DownloadInvoice from "./Actions/DownloadInvoice" +import { generateDateTime } from "./Actions/helpers" +import ManageBooking from "./Actions/ManageBooking" import styles from "./header.module.css" +import type { EventAttributes } from "ics" + import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" export default async function Header({ confirmationNumber, }: BookingConfirmationProps) { const intl = await getIntl() - const { hotel } = await getBookingConfirmation(confirmationNumber) + const { booking, hotel } = await getBookingConfirmation(confirmationNumber) const text = intl.formatMessage( { id: "booking.confirmation.text" }, @@ -28,6 +33,25 @@ export default async function Header({ } ) + const event: EventAttributes = { + busyStatus: "FREE", + categories: ["booking", "hotel", "stay"], + created: generateDateTime(booking.createDateTime), + description: hotel.hotelContent.texts.descriptions.medium, + end: generateDateTime(booking.checkOutDate), + endInputType: "utc", + geo: { + lat: hotel.location.latitude, + lon: hotel.location.longitude, + }, + location: `${hotel.address.streetAddress}, ${hotel.address.zipCode} ${hotel.address.city} ${hotel.address.country}`, + start: generateDateTime(booking.checkInDate), + startInputType: "utc", + status: "CONFIRMED", + title: hotel.name, + url: hotel.contactInformation.websiteUrl, + } + return (
@@ -39,7 +63,15 @@ export default async function Header({
{text} - +
+ + + +
) } diff --git a/package-lock.json b/package-lock.json index b4509615e..d42a1d4e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "graphql": "^16.8.1", "graphql-request": "^6.1.0", "graphql-tag": "^2.12.6", + "ics": "^3.8.1", "immer": "10.1.1", "json-stable-stringify-without-jsonify": "^1.0.1", "libphonenumber-js": "^1.10.60", @@ -11794,6 +11795,16 @@ "node": ">=0.10.0" } }, + "node_modules/ics": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/ics/-/ics-3.8.1.tgz", + "integrity": "sha512-UqQlfkajfhrS4pUGQfGIJMYz/Jsl/ob3LqcfEhUmLbwumg+ZNkU0/6S734Vsjq3/FYNpEcZVKodLBoe+zBM69g==", + "dependencies": { + "nanoid": "^3.1.23", + "runes2": "^1.1.2", + "yup": "^1.2.0" + } + }, "node_modules/icss-utils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", @@ -16840,6 +16851,11 @@ "react-is": "^16.13.1" } }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==" + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -17859,6 +17875,11 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/runes2": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/runes2/-/runes2-1.1.4.tgz", + "integrity": "sha512-LNPnEDPOOU4ehF71m5JoQyzT2yxwD6ZreFJ7MxZUAoMKNMY1XrAo60H1CUoX5ncSm0rIuKlqn9JZNRrRkNou2g==" + }, "node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -19048,6 +19069,11 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -19103,6 +19129,11 @@ "node": ">=0.6" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "node_modules/tough-cookie": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", @@ -20348,6 +20379,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yup": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz", + "integrity": "sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/yup/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "3.22.4", "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", diff --git a/package.json b/package.json index efb824197..6f6f1f210 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "graphql": "^16.8.1", "graphql-request": "^6.1.0", "graphql-tag": "^2.12.6", + "ics": "^3.8.1", "immer": "10.1.1", "json-stable-stringify-without-jsonify": "^1.0.1", "libphonenumber-js": "^1.10.60", diff --git a/types/components/hotelReservation/bookingConfirmation/actions/addToCalendar.ts b/types/components/hotelReservation/bookingConfirmation/actions/addToCalendar.ts new file mode 100644 index 000000000..e24c1ffc7 --- /dev/null +++ b/types/components/hotelReservation/bookingConfirmation/actions/addToCalendar.ts @@ -0,0 +1,9 @@ +import type { EventAttributes } from "ics" + +import type { RouterOutput } from "@/lib/trpc/client" + +export interface AddToCalendarProps { + checkInDate: RouterOutput["booking"]["confirmation"]["booking"]["checkInDate"] + event: EventAttributes + hotelName: RouterOutput["booking"]["confirmation"]["hotel"]["name"] +} From aa9124b7d4ccfec3ee17676dcd529994cb64bdf8 Mon Sep 17 00:00:00 2001 From: Hrishikesh Vaipurkar Date: Mon, 2 Dec 2024 10:57:08 +0100 Subject: [PATCH 02/37] fix: SW-691 Fixed z-index for current header --- components/Current/Header/header.module.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/Current/Header/header.module.css b/components/Current/Header/header.module.css index 90e35642b..8c4962efd 100644 --- a/components/Current/Header/header.module.css +++ b/components/Current/Header/header.module.css @@ -1,6 +1,8 @@ .header { display: grid; background-color: var(--Main-Grey-White); + position: relative; + z-index: var(--header-z-index); } @media screen and (max-width: 1366px) { From 830f8bf2610aeb621b8f4f5e09b87c0d5e4239e7 Mon Sep 17 00:00:00 2001 From: Hrishikesh Vaipurkar Date: Mon, 2 Dec 2024 12:30:21 +0100 Subject: [PATCH 03/37] fix: SW-691 SW-1006 Removed rooms left label when no availability --- .../SelectRate/RoomSelection/RoomCard/index.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index 0499bd861..a5b21e10d 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -117,14 +117,15 @@ export default function RoomCard({
- {roomConfiguration.roomsLeft < 5 && ( - - {`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`} - - )} + {roomConfiguration.roomsLeft > 0 && + roomConfiguration.roomsLeft < 5 && ( + + {`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`} + + )} {roomConfiguration.features .filter((feature) => selectedPackages.includes(feature.code)) .map((feature) => ( From 4288e643c6d21cc48696fa27b64fdf301c783a0a Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Tue, 3 Dec 2024 09:11:32 +0100 Subject: [PATCH 04/37] fix: pass lang to create booking --- .../EnterDetails/Payment/PaymentClient.tsx | 1 + i18n/dictionaries/sv.json | 1 + server/routers/booking/input.ts | 2 ++ server/routers/booking/mutation.ts | 18 ++++++++++++------ 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx b/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx index ae1e523d8..827f34636 100644 --- a/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx +++ b/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx @@ -206,6 +206,7 @@ export default function PaymentClient({ const paymentRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}/${lang}/hotelreservation/payment-callback` initiateBooking.mutate({ + language: lang, hotelId: hotel, checkInDate: fromDate, checkOutDate: toDate, diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index a67214de5..3ed973d0a 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -171,6 +171,7 @@ "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!", "I accept the terms and conditions": "Jag accepterar villkoren", + "I would like to get my booking confirmation via sms": "Jag vill få min bokningsbekräftelse via sms", "Image gallery": "{name} - Bildgalleri", "In adults bed": "I vuxens säng", "In crib": "I spjälsäng", diff --git a/server/routers/booking/input.ts b/server/routers/booking/input.ts index b6c82f906..43c0e670d 100644 --- a/server/routers/booking/input.ts +++ b/server/routers/booking/input.ts @@ -1,6 +1,7 @@ import { z } from "zod" import { ChildBedTypeEnum } from "@/constants/booking" +import { Lang, langToApiLang } from "@/constants/languages" const signupSchema = z.discriminatedUnion("becomeMember", [ z.object({ @@ -81,6 +82,7 @@ export const createBookingInput = z.object({ checkOutDate: z.string(), rooms: roomsSchema, payment: paymentSchema, + language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]), }) export const priceChangeInput = z.object({ diff --git a/server/routers/booking/mutation.ts b/server/routers/booking/mutation.ts index 06ae675a7..e8028bf79 100644 --- a/server/routers/booking/mutation.ts +++ b/server/routers/booking/mutation.ts @@ -47,16 +47,18 @@ export const bookingMutationRouter = router({ .input(createBookingInput) .mutation(async function ({ ctx, input }) { const accessToken = ctx.session?.token.access_token ?? ctx.serviceToken - const { checkInDate, checkOutDate, hotelId } = input + const { language, ...inputWithoutLang } = input + const { hotelId, checkInDate, checkOutDate } = inputWithoutLang const loggingAttributes = { membershipNumber: await getMembershipNumber(ctx.session), checkInDate, checkOutDate, hotelId, + language, } - createBookingCounter.add(1, { hotelId, checkInDate, checkOutDate }) + createBookingCounter.add(1, loggingAttributes) console.info( "api.booking.create start", @@ -68,10 +70,14 @@ export const bookingMutationRouter = router({ Authorization: `Bearer ${accessToken}`, } - const apiResponse = await api.post(api.endpoints.v1.Booking.bookings, { - headers, - body: input, - }) + const apiResponse = await api.post( + api.endpoints.v1.Booking.bookings, + { + headers, + body: inputWithoutLang, + }, + { language } + ) if (!apiResponse.ok) { const text = await apiResponse.text() From 528107e0ef83b590a6b98f483c535a7e9f1598e9 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Tue, 3 Dec 2024 13:59:35 +0100 Subject: [PATCH 05/37] fix: change euro price to user's currency + merge state correctly --- .../EnterDetails/Summary/UI/index.tsx | 6 +-- stores/enter-details/helpers.ts | 44 ++++++++-------- stores/enter-details/index.ts | 52 ++++++++++++------- types/stores/enter-details.ts | 2 +- 4 files changed, 58 insertions(+), 46 deletions(-) diff --git a/components/HotelReservation/EnterDetails/Summary/UI/index.tsx b/components/HotelReservation/EnterDetails/Summary/UI/index.tsx index 5e61ef155..2a33812f0 100644 --- a/components/HotelReservation/EnterDetails/Summary/UI/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/UI/index.tsx @@ -227,11 +227,11 @@ export default function SummaryUI({ style: "currency", })} - {totalPrice.euro && ( + {totalPrice.requested && ( {intl.formatMessage({ id: "Approx." })}{" "} - {intl.formatNumber(totalPrice.euro.price, { - currency: CurrencyEnum.EUR, + {intl.formatNumber(totalPrice.requested.price, { + currency: totalPrice.requested.currency, style: "currency", })} diff --git a/stores/enter-details/helpers.ts b/stores/enter-details/helpers.ts index 496b75ba6..f76caeb19 100644 --- a/stores/enter-details/helpers.ts +++ b/stores/enter-details/helpers.ts @@ -77,9 +77,9 @@ export function subtract(...nums: (number | string | undefined)[]) { export function getInitialRoomPrice(roomRate: RoomRate, isMember: boolean) { if (isMember && roomRate.memberRate) { return { - euro: { - currency: CurrencyEnum.EUR, - price: roomRate.memberRate.requestedPrice?.pricePerStay ?? 0, + requested: roomRate.memberRate.requestedPrice && { + currency: roomRate.memberRate.requestedPrice.currency, + price: roomRate.memberRate.requestedPrice.pricePerStay, }, local: { currency: roomRate.memberRate.localPrice.currency, @@ -89,9 +89,9 @@ export function getInitialRoomPrice(roomRate: RoomRate, isMember: boolean) { } return { - euro: { - currency: CurrencyEnum.EUR, - price: roomRate.publicRate.requestedPrice?.pricePerStay ?? 0, + requested: roomRate.publicRate.requestedPrice && { + currency: roomRate.publicRate.requestedPrice.currency, + price: roomRate.publicRate.requestedPrice.pricePerStay, }, local: { currency: roomRate.publicRate.localPrice.currency, @@ -103,9 +103,9 @@ export function getInitialRoomPrice(roomRate: RoomRate, isMember: boolean) { export function getInitialTotalPrice(roomRate: RoomRate, isMember: boolean) { if (isMember && roomRate.memberRate) { return { - euro: { - currency: CurrencyEnum.EUR, - price: roomRate.memberRate.requestedPrice?.pricePerStay ?? 0, + requested: roomRate.memberRate.requestedPrice && { + currency: roomRate.memberRate.requestedPrice.currency, + price: roomRate.memberRate.requestedPrice.pricePerStay, }, local: { currency: roomRate.memberRate.localPrice.currency, @@ -115,9 +115,9 @@ export function getInitialTotalPrice(roomRate: RoomRate, isMember: boolean) { } return { - euro: { - currency: CurrencyEnum.EUR, - price: roomRate.publicRate.requestedPrice?.pricePerStay ?? 0, + requested: roomRate.publicRate.requestedPrice && { + currency: roomRate.publicRate.requestedPrice.currency, + price: roomRate.publicRate.requestedPrice.pricePerStay, }, local: { currency: roomRate.publicRate.localPrice.currency, @@ -165,31 +165,31 @@ export function calcTotalPrice( totalPrice: state.totalPrice, } if (state.requestedPrice?.pricePerStay) { - roomAndTotalPrice.roomPrice.euro = { - currency: CurrencyEnum.EUR, + roomAndTotalPrice.roomPrice.requested = { + currency: state.requestedPrice.currency, price: state.requestedPrice.pricePerStay, } - let totalPriceEuro = state.requestedPrice.pricePerStay + let totalPriceRequested = state.requestedPrice.pricePerStay if (state.breakfast) { - totalPriceEuro = add( - totalPriceEuro, + totalPriceRequested = add( + totalPriceRequested, state.breakfast.requestedPrice.totalPrice ) } if (state.packages) { - totalPriceEuro = state.packages.reduce((total, pkg) => { + totalPriceRequested = state.packages.reduce((total, pkg) => { if (pkg.requestedPrice.totalPrice) { total = add(total, pkg.requestedPrice.totalPrice) } return total - }, totalPriceEuro) + }, totalPriceRequested) } - roomAndTotalPrice.totalPrice.euro = { - currency: CurrencyEnum.EUR, - price: totalPriceEuro, + roomAndTotalPrice.totalPrice.requested = { + currency: state.requestedPrice.currency, + price: totalPriceRequested, } } diff --git a/stores/enter-details/index.ts b/stores/enter-details/index.ts index a78116374..4b70fb254 100644 --- a/stores/enter-details/index.ts +++ b/stores/enter-details/index.ts @@ -115,10 +115,12 @@ export function createDetailsStore( if (initialState.packages) { initialState.packages.forEach((pkg) => { - initialTotalPrice.euro.price = add( - initialTotalPrice.euro.price, - pkg.requestedPrice.totalPrice - ) + if (initialTotalPrice.requested) { + initialTotalPrice.requested.price = add( + initialTotalPrice.requested.price, + pkg.requestedPrice.totalPrice + ) + } initialTotalPrice.local.price = add( initialTotalPrice.local.price, pkg.localPrice.totalPrice @@ -165,7 +167,7 @@ export function createDetailsStore( setTotalPrice(totalPrice) { return set( produce((state: DetailsState) => { - state.totalPrice.euro = totalPrice.euro + state.totalPrice.requested = totalPrice.requested state.totalPrice.local = totalPrice.local }) ) @@ -194,7 +196,8 @@ export function createDetailsStore( return set( produce((state: DetailsState) => { state.isValid.breakfast = true - const stateTotalEuroPrice = state.totalPrice.euro?.price || 0 + const stateTotalRequestedPrice = + state.totalPrice.requested?.price || 0 const stateTotalLocalPrice = state.totalPrice.local.price const addToTotalPrice = @@ -206,7 +209,7 @@ export function createDetailsStore( breakfast === false if (addToTotalPrice) { - const breakfastTotalEuroPrice = parseInt( + const breakfastTotalRequestedPrice = parseInt( breakfast.requestedPrice.totalPrice ) const breakfastTotalPrice = parseInt( @@ -214,9 +217,10 @@ export function createDetailsStore( ) state.totalPrice = { - euro: { - currency: CurrencyEnum.EUR, - price: stateTotalEuroPrice + breakfastTotalEuroPrice, + requested: state.totalPrice.requested && { + currency: state.totalPrice.requested.currency, + price: + stateTotalRequestedPrice + breakfastTotalRequestedPrice, }, local: { currency: breakfast.localPrice.currency, @@ -229,21 +233,22 @@ export function createDetailsStore( let currency = state.totalPrice.local.currency ?? langToCurrency() let currentBreakfastTotalPrice = 0 - let currentBreakfastTotalEuroPrice = 0 + let currentBreakfastTotalRequestedPrice = 0 if (state.breakfast) { currentBreakfastTotalPrice = parseInt( state.breakfast.localPrice.totalPrice ) - currentBreakfastTotalEuroPrice = parseInt( + currentBreakfastTotalRequestedPrice = parseInt( state.breakfast.requestedPrice.totalPrice ) currency = state.breakfast.localPrice.currency } - let euroPrice = - stateTotalEuroPrice - currentBreakfastTotalEuroPrice - if (euroPrice < 0) { - euroPrice = 0 + let requestedPrice = + stateTotalRequestedPrice - + currentBreakfastTotalRequestedPrice + if (requestedPrice < 0) { + requestedPrice = 0 } let localPrice = stateTotalLocalPrice - currentBreakfastTotalPrice @@ -252,9 +257,9 @@ export function createDetailsStore( } state.totalPrice = { - euro: { - currency: CurrencyEnum.EUR, - price: euroPrice, + requested: state.totalPrice.requested && { + currency: state.totalPrice.requested.currency, + price: requestedPrice, }, local: { currency, @@ -349,8 +354,15 @@ export function createDetailsStore( persistedState.booking, currentState.booking ) + if (!isSameBooking) { - return deepmerge(persistedState, currentState, { arrayMerge }) + // We get the booking data from query params, and the "newest" booking data should always be used. + // Merging the two states can lead to issues since some params or values in arrays might be removed. + // @ts-expect-error - persistedState cannot be typed + delete persistedState.booking + return deepmerge(persistedState, currentState, { + arrayMerge, + }) } } return deepmerge(currentState, persistedState ?? {}, { arrayMerge }) diff --git a/types/stores/enter-details.ts b/types/stores/enter-details.ts index e44fd8ce1..85270217f 100644 --- a/types/stores/enter-details.ts +++ b/types/stores/enter-details.ts @@ -15,7 +15,7 @@ interface TPrice { } export interface Price { - euro: TPrice | undefined + requested: TPrice | undefined local: TPrice } From 6bc7e27908a8c8575b7b26e7d7b603a348233bd6 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Thu, 28 Nov 2024 15:44:17 +0100 Subject: [PATCH 06/37] fix: dateFormat normalization middleware --- middleware.ts | 3 ++- middlewares/dateFormat.ts | 29 +++++++++++++++++++++-------- next.config.js | 4 ++-- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/middleware.ts b/middleware.ts index 27b4eca97..58f151dab 100644 --- a/middleware.ts +++ b/middleware.ts @@ -43,6 +43,7 @@ export const middleware: NextMiddleware = async (request, event) => { ) } + // Note that the order of middlewares is important since that is the order they are matched by. const middlewares = [ currentWebLogin, currentWebLoginEmail, @@ -51,9 +52,9 @@ export const middleware: NextMiddleware = async (request, event) => { handleAuth, myPages, webView, + dateFormat, bookingFlow, cmsContent, - dateFormat, ] try { diff --git a/middlewares/dateFormat.ts b/middlewares/dateFormat.ts index b1ee329f1..d2f3c584f 100644 --- a/middlewares/dateFormat.ts +++ b/middlewares/dateFormat.ts @@ -7,6 +7,7 @@ import { MiddlewareMatcher } from "@/types/middleware" YYYY-MM-D and YYYY-MM-DD since the current web uses YYYY-MM-D in the URL as parameters (toDate and fromDate) */ +const legacyDatePattern = /^([12]\d{3}-(0[1-9]|1[0-2])-([1-9]))$/ function normalizeDate(date: string): string { const datePattern = /^\d{4}-\d{1,2}-\d{1,2}$/ @@ -18,20 +19,32 @@ function normalizeDate(date: string): string { } export const middleware: NextMiddleware = (request) => { - const url = request.nextUrl.clone() + const url = request.nextUrl const { searchParams } = url - if (searchParams.has("fromDate")) { + if ( + !!( + searchParams.has("fromDate") && + searchParams.get("fromDate")!.match(legacyDatePattern) + ) || + !!( + searchParams.has("toDate") && + searchParams.get("toDate")!.match(legacyDatePattern) + ) + ) { const fromDate = searchParams.get("fromDate")! - searchParams.set("fromDate", normalizeDate(fromDate)) - } + url.searchParams.set("fromDate", normalizeDate(fromDate)) - if (searchParams.has("toDate")) { const toDate = searchParams.get("toDate")! - searchParams.set("toDate", normalizeDate(toDate)) + url.searchParams.set("toDate", normalizeDate(toDate)) + return NextResponse.redirect(url) + } else { + const headers = new Headers(request.headers) + headers.set("x-continue", "1") + return NextResponse.next({ + headers, + }) } - - return NextResponse.rewrite(url) } export const matcher: MiddlewareMatcher = (request) => { diff --git a/next.config.js b/next.config.js index 72121b713..c73b08464 100644 --- a/next.config.js +++ b/next.config.js @@ -136,7 +136,7 @@ const nextConfig = { { key: "fromDate", type: "query", - value: "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$", + value: "^([12]\\d{3}-(0[1-9]|1[0-2])-(0?[1-9]|[12]\\d|3[01]))$", }, ], permanent: false, @@ -168,7 +168,7 @@ const nextConfig = { { key: "toDate", type: "query", - value: "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$", + value: "^([12]\\d{3}-(0[1-9]|1[0-2])-(0?[1-9]|[12]\\d|3[01]))$", }, ], permanent: false, From 4545de877fa70b4e896d2488f2f79724cc8a6380 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Thu, 28 Nov 2024 15:53:07 +0100 Subject: [PATCH 07/37] fix: normalize format of dates when setting them in middleware --- components/BookingWidget/Client.tsx | 21 +++++++++++++-------- lib/dt.ts | 2 ++ middlewares/dateFormat.ts | 12 +++--------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/components/BookingWidget/Client.tsx b/components/BookingWidget/Client.tsx index 922522742..84ac08cba 100644 --- a/components/BookingWidget/Client.tsx +++ b/components/BookingWidget/Client.tsx @@ -66,11 +66,16 @@ export default function BookingWidgetClient({ const reqFromDate = bookingWidgetSearchData?.fromDate?.toString() const reqToDate = bookingWidgetSearchData?.toDate?.toString() + const parsedFromDate = reqFromDate ? dt(reqFromDate) : undefined + const parsedToDate = reqToDate ? dt(reqToDate) : undefined + + const now = dt() + const isDateParamValid = - reqFromDate && - reqToDate && - dt(reqFromDate).isAfter(dt().subtract(1, "day")) && - dt(reqToDate).isAfter(dt(reqFromDate)) + parsedFromDate && + parsedToDate && + parsedFromDate.isSameOrAfter(now, "day") && + parsedToDate.isAfter(parsedFromDate) const selectedLocation = bookingWidgetSearchData ? getLocationObj( @@ -97,11 +102,11 @@ export default function BookingWidgetClient({ // UTC is required to handle requests from far away timezones https://scandichotels.atlassian.net/browse/SWAP-6375 & PET-507 // This is specifically to handle timezones falling in different dates. fromDate: isDateParamValid - ? dt(bookingWidgetSearchData?.fromDate).format("YYYY-M-D") - : dt().utc().format("YYYY-M-D"), + ? parsedFromDate.format("YYYY-MM-DD") + : now.utc().format("YYYY-MM-DD"), toDate: isDateParamValid - ? dt(bookingWidgetSearchData?.toDate).format("YYYY-M-D") - : dt().utc().add(1, "day").format("YYYY-M-D"), + ? parsedToDate.format("YYYY-MM-DD") + : now.utc().add(1, "day").format("YYYY-MM-DD"), }, bookingCode: "", redemption: false, diff --git a/lib/dt.ts b/lib/dt.ts index 5cbe77692..ca41dfbf2 100644 --- a/lib/dt.ts +++ b/lib/dt.ts @@ -5,6 +5,7 @@ import "dayjs/locale/sv" import d from "dayjs" import advancedFormat from "dayjs/plugin/advancedFormat" +import isSameOrAfter from "dayjs/plugin/isSameOrAfter" import isToday from "dayjs/plugin/isToday" import relativeTime from "dayjs/plugin/relativeTime" import timezone from "dayjs/plugin/timezone" @@ -62,5 +63,6 @@ d.extend(isToday) d.extend(relativeTime) d.extend(timezone) d.extend(utc) +d.extend(isSameOrAfter) export const dt = d diff --git a/middlewares/dateFormat.ts b/middlewares/dateFormat.ts index d2f3c584f..7906e13e5 100644 --- a/middlewares/dateFormat.ts +++ b/middlewares/dateFormat.ts @@ -19,18 +19,12 @@ function normalizeDate(date: string): string { } export const middleware: NextMiddleware = (request) => { - const url = request.nextUrl + const url = request.nextUrl.clone() const { searchParams } = url if ( - !!( - searchParams.has("fromDate") && - searchParams.get("fromDate")!.match(legacyDatePattern) - ) || - !!( - searchParams.has("toDate") && - searchParams.get("toDate")!.match(legacyDatePattern) - ) + legacyDatePattern.test(searchParams.get("fromDate")!) || + legacyDatePattern.test(searchParams.get("toDate")!) ) { const fromDate = searchParams.get("fromDate")! url.searchParams.set("fromDate", normalizeDate(fromDate)) From 2660faf4ac6efe60353144b39038187f70a9a5ec Mon Sep 17 00:00:00 2001 From: Niclas Edenvin Date: Tue, 3 Dec 2024 16:01:19 +0000 Subject: [PATCH 08/37] Merged in fix/sw-1013-calendar-no-scroll (pull request #1026) fix(SW-1013): extract custom components from day picker * fix(SW-1013): extract custom components from day picker The custom components in the mobile day picker made a lot of components in the day picker to unmount and mount again, which had weird scrolling behaviour. They don't need to be baked in to the day picker so they are now extracted to regular sibling elements. Approved-by: Pontus Dreij --- components/DatePicker/Screen/Mobile.tsx | 152 ++++++++---------- .../DatePicker/Screen/mobile.module.css | 6 +- 2 files changed, 75 insertions(+), 83 deletions(-) diff --git a/components/DatePicker/Screen/Mobile.tsx b/components/DatePicker/Screen/Mobile.tsx index f6cfcae15..8494f5c0f 100644 --- a/components/DatePicker/Screen/Mobile.tsx +++ b/components/DatePicker/Screen/Mobile.tsx @@ -35,87 +35,75 @@ export default function DatePickerMobile({ const endDate = dt().add(395, "day").toDate() const endOfLastMonth = dt(endDate).endOf("month").toDate() return ( - - -
- - ) - }, - MonthCaption(props) { - return ( -
- - {props.children} - -
- ) - }, - Root({ children, ...props }) { - return ( -
-
- -
- {children} -
- ) - }, - }} - /> +
+
+ +
+ + + {props.children} + +
+ ) + }, + }} + /> +
+ +
+
) } diff --git a/components/DatePicker/Screen/mobile.module.css b/components/DatePicker/Screen/mobile.module.css index 5ee03bde5..4fba31592 100644 --- a/components/DatePicker/Screen/mobile.module.css +++ b/components/DatePicker/Screen/mobile.module.css @@ -10,6 +10,11 @@ position: relative; } +.root { + display: grid; + grid-area: content; +} + .header { align-self: flex-end; background-color: var(--Main-Grey-White); @@ -37,7 +42,6 @@ div.months { display: grid; - grid-area: content; overflow-y: scroll; scroll-snap-type: y mandatory; } From 91fe2d25eb5cd13b0f8a7407058bd2d029cc4d54 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Fri, 22 Nov 2024 10:01:58 +0100 Subject: [PATCH 09/37] feat(SW-962): add activites sidepeek --- .../CardGrid/ActivitiesCardGrid.tsx | 4 ++-- .../Activities/activities.module.css | 2 ++ .../HotelPage/SidePeeks/Activities/index.tsx | 20 +++++++++++++++++++ .../ContentType/HotelPage/SidePeeks/index.ts | 1 + components/ContentType/HotelPage/index.tsx | 1 + 5 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 components/ContentType/HotelPage/SidePeeks/Activities/activities.module.css create mode 100644 components/ContentType/HotelPage/SidePeeks/Activities/index.tsx diff --git a/components/ContentType/HotelPage/Facilities/CardGrid/ActivitiesCardGrid.tsx b/components/ContentType/HotelPage/Facilities/CardGrid/ActivitiesCardGrid.tsx index c1bf12c31..dd865227e 100644 --- a/components/ContentType/HotelPage/Facilities/CardGrid/ActivitiesCardGrid.tsx +++ b/components/ContentType/HotelPage/Facilities/CardGrid/ActivitiesCardGrid.tsx @@ -20,7 +20,7 @@ export default function ActivitiesCardGrid(activitiesCard: ActivityCard) { theme: hasImage ? "image" : "primaryDark", primaryButton: hasImage ? { - href: activitiesCard.contentPage.href, + href: `?s=${activities[lang]}`, title: activitiesCard.ctaText, isExternal: false, } @@ -28,7 +28,7 @@ export default function ActivitiesCardGrid(activitiesCard: ActivityCard) { secondaryButton: hasImage ? undefined : { - href: activitiesCard.contentPage.href, + href: `?s=${activities[lang]}`, title: activitiesCard.ctaText, isExternal: false, }, diff --git a/components/ContentType/HotelPage/SidePeeks/Activities/activities.module.css b/components/ContentType/HotelPage/SidePeeks/Activities/activities.module.css new file mode 100644 index 000000000..ec81ef8e9 --- /dev/null +++ b/components/ContentType/HotelPage/SidePeeks/Activities/activities.module.css @@ -0,0 +1,2 @@ +.wrapper { +} diff --git a/components/ContentType/HotelPage/SidePeeks/Activities/index.tsx b/components/ContentType/HotelPage/SidePeeks/Activities/index.tsx new file mode 100644 index 000000000..19b17bc8d --- /dev/null +++ b/components/ContentType/HotelPage/SidePeeks/Activities/index.tsx @@ -0,0 +1,20 @@ +import { activities } from "@/constants/routes/hotelPageParams" + +import SidePeek from "@/components/TempDesignSystem/SidePeek" +import { getIntl } from "@/i18n" +import { getLang } from "@/i18n/serverContext" + +import styles from "./activities.module.css" + +export default async function ActivitiesSidePeek() { + const lang = getLang() + const intl = await getIntl() + return ( + +
This is some content for Activities
+
+ ) +} diff --git a/components/ContentType/HotelPage/SidePeeks/index.ts b/components/ContentType/HotelPage/SidePeeks/index.ts index 7eff75db2..e7233e010 100644 --- a/components/ContentType/HotelPage/SidePeeks/index.ts +++ b/components/ContentType/HotelPage/SidePeeks/index.ts @@ -1,4 +1,5 @@ export { default as AboutTheHotelSidePeek } from "./AboutTheHotel" +export { default as ActivitiesSidePeek } from "./Activities" export { default as AmenitiesSidePeek } from "./Amenities" export { default as RoomSidePeek } from "./Room" export { default as WellnessAndExerciseSidePeek } from "./WellnessAndExercise" diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index 8910a7cd8..f7c14c664 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -29,6 +29,7 @@ import PreviewImages from "./PreviewImages" import { Rooms } from "./Rooms" import { AboutTheHotelSidePeek, + ActivitiesSidePeek, AmenitiesSidePeek, RoomSidePeek, WellnessAndExerciseSidePeek, From e491d2cf4b9c5f51ff144ac7d4477daa90c2ea78 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Fri, 29 Nov 2024 15:06:32 +0100 Subject: [PATCH 10/37] feat(SW-962): add link --- .../Activities/activities.module.css | 15 ++++++++++++- .../HotelPage/SidePeeks/Activities/index.tsx | 21 +++++++++++++++++-- components/ContentType/HotelPage/index.tsx | 12 ++++------- 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 + .../hotelPage/sidepeek/activities.ts | 6 ++++++ 10 files changed, 49 insertions(+), 11 deletions(-) create mode 100644 types/components/hotelPage/sidepeek/activities.ts diff --git a/components/ContentType/HotelPage/SidePeeks/Activities/activities.module.css b/components/ContentType/HotelPage/SidePeeks/Activities/activities.module.css index ec81ef8e9..7c661d1a6 100644 --- a/components/ContentType/HotelPage/SidePeeks/Activities/activities.module.css +++ b/components/ContentType/HotelPage/SidePeeks/Activities/activities.module.css @@ -1,2 +1,15 @@ -.wrapper { +.preamble { + margin-bottom: calc( + var(--Spacing-x4) * 2 + 80px + ); /* Creates space between the wrapper and buttonContainer */ +} + +.buttonContainer { + background-color: var(--Base-Background-Primary-Normal); + border-top: 1px solid var(--Base-Border-Subtle); + padding: var(--Spacing-x4) var(--Spacing-x2); + width: 100%; + position: absolute; + left: 0; + bottom: 0; } diff --git a/components/ContentType/HotelPage/SidePeeks/Activities/index.tsx b/components/ContentType/HotelPage/SidePeeks/Activities/index.tsx index 19b17bc8d..16a7ae0cb 100644 --- a/components/ContentType/HotelPage/SidePeeks/Activities/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/Activities/index.tsx @@ -1,12 +1,20 @@ import { activities } from "@/constants/routes/hotelPageParams" +import Button from "@/components/TempDesignSystem/Button" +import Link from "@/components/TempDesignSystem/Link" import SidePeek from "@/components/TempDesignSystem/SidePeek" +import Preamble from "@/components/TempDesignSystem/Text/Preamble" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" import styles from "./activities.module.css" -export default async function ActivitiesSidePeek() { +import { ActivitiesSidePeekProps } from "@/types/components/hotelPage/sidepeek/activities" + +export default async function ActivitiesSidePeek({ + preamble, + contentPage, +}: ActivitiesSidePeekProps) { const lang = getLang() const intl = await getIntl() return ( @@ -14,7 +22,16 @@ export default async function ActivitiesSidePeek() { contentKey={activities[lang]} title={intl.formatMessage({ id: "Activities" })} > -
This is some content for Activities
+ {preamble} + {contentPage?.href && ( +
+ +
+ )} ) } diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index f7c14c664..6e278f3b6 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -1,7 +1,6 @@ import { notFound } from "next/navigation" import { - activities, meetingsAndConferences, restaurantAndBar, } from "@/constants/routes/hotelPageParams" @@ -201,13 +200,10 @@ export default async function HotelPage({ hotelId }: HotelPageProps) { Restaurant & Bar - - {/* TODO */} - Activities - + Date: Mon, 2 Dec 2024 17:14:38 +0100 Subject: [PATCH 11/37] feat(SW-962): add preamble --- .../HotelPage/SidePeeks/Activities/index.tsx | 18 ++++++++---------- components/ContentType/HotelPage/index.tsx | 10 ++++++---- lib/graphql/Query/HotelPage/HotelPage.graphql | 7 ++++++- .../schemas/blocks/activitiesCard.ts | 9 ++++++++- .../hotelPage/sidepeek/activities.ts | 4 +--- 5 files changed, 29 insertions(+), 19 deletions(-) diff --git a/components/ContentType/HotelPage/SidePeeks/Activities/index.tsx b/components/ContentType/HotelPage/SidePeeks/Activities/index.tsx index 16a7ae0cb..2495cd0ad 100644 --- a/components/ContentType/HotelPage/SidePeeks/Activities/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/Activities/index.tsx @@ -13,7 +13,7 @@ import { ActivitiesSidePeekProps } from "@/types/components/hotelPage/sidepeek/a export default async function ActivitiesSidePeek({ preamble, - contentPage, + contentPageLink, }: ActivitiesSidePeekProps) { const lang = getLang() const intl = await getIntl() @@ -23,15 +23,13 @@ export default async function ActivitiesSidePeek({ title={intl.formatMessage({ id: "Activities" })} > {preamble} - {contentPage?.href && ( -
- -
- )} +
+ +
) } diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index 6e278f3b6..b6fc20e26 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -200,10 +200,12 @@ export default async function HotelPage({ hotelId }: HotelPageProps) { Restaurant & Bar - + {activitiesCard && ( + + )} { let contentPage = { href: "" } + let preamble = "" if (data.hotel_page_activities_content_pageConnection.edges.length) { const page = data.hotel_page_activities_content_pageConnection.edges[0].node + preamble = page.header.preamble if (page.web.original_url) { contentPage = { href: page.web.original_url, @@ -54,6 +60,7 @@ export const activitiesCardSchema = z.object({ heading: data.heading, openInNewTab: !!data.open_in_new_tab, scriptedTopTitle: data.scripted_title, + preamble, } }), }) diff --git a/types/components/hotelPage/sidepeek/activities.ts b/types/components/hotelPage/sidepeek/activities.ts index 8b6d9581b..e61fac277 100644 --- a/types/components/hotelPage/sidepeek/activities.ts +++ b/types/components/hotelPage/sidepeek/activities.ts @@ -1,6 +1,4 @@ export type ActivitiesSidePeekProps = { preamble: string - contentPage?: { - href: string - } + contentPageLink: string } From f171bf9af79af2941117da328c09cee2bc538f00 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Mon, 2 Dec 2024 17:54:09 +0100 Subject: [PATCH 12/37] feat(SW-962): css change --- .../HotelPage/SidePeeks/Activities/activities.module.css | 6 ------ .../ContentType/HotelPage/SidePeeks/Activities/index.tsx | 4 ++-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/components/ContentType/HotelPage/SidePeeks/Activities/activities.module.css b/components/ContentType/HotelPage/SidePeeks/Activities/activities.module.css index 7c661d1a6..87cb2cdc0 100644 --- a/components/ContentType/HotelPage/SidePeeks/Activities/activities.module.css +++ b/components/ContentType/HotelPage/SidePeeks/Activities/activities.module.css @@ -1,9 +1,3 @@ -.preamble { - margin-bottom: calc( - var(--Spacing-x4) * 2 + 80px - ); /* Creates space between the wrapper and buttonContainer */ -} - .buttonContainer { background-color: var(--Base-Background-Primary-Normal); border-top: 1px solid var(--Base-Border-Subtle); diff --git a/components/ContentType/HotelPage/SidePeeks/Activities/index.tsx b/components/ContentType/HotelPage/SidePeeks/Activities/index.tsx index 2495cd0ad..a63337c85 100644 --- a/components/ContentType/HotelPage/SidePeeks/Activities/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/Activities/index.tsx @@ -9,7 +9,7 @@ import { getLang } from "@/i18n/serverContext" import styles from "./activities.module.css" -import { ActivitiesSidePeekProps } from "@/types/components/hotelPage/sidepeek/activities" +import type { ActivitiesSidePeekProps } from "@/types/components/hotelPage/sidepeek/activities" export default async function ActivitiesSidePeek({ preamble, @@ -22,7 +22,7 @@ export default async function ActivitiesSidePeek({ contentKey={activities[lang]} title={intl.formatMessage({ id: "Activities" })} > - {preamble} + {preamble}
diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index b6fc20e26..a9125edf6 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -201,10 +201,7 @@ export default async function HotelPage({ hotelId }: HotelPageProps) { {activitiesCard && ( - + )} { - let contentPage = { href: "" } - let preamble = "" + let contentPage = { href: "", preamble: "" } if (data.hotel_page_activities_content_pageConnection.edges.length) { const page = data.hotel_page_activities_content_pageConnection.edges[0].node - preamble = page.header.preamble + if (page.web.original_url) { contentPage = { href: page.web.original_url, + preamble: page.header.preamble, } } else { contentPage = { href: removeMultipleSlashes(`/${page.system.locale}/${page.url}`), + preamble: page.header.preamble, } } } @@ -58,9 +58,7 @@ export const activitiesCardSchema = z.object({ contentPage, ctaText: data.cta_text, heading: data.heading, - openInNewTab: !!data.open_in_new_tab, scriptedTopTitle: data.scripted_title, - preamble, } }), }) diff --git a/types/components/hotelPage/sidepeek/activities.ts b/types/components/hotelPage/sidepeek/activities.ts index e61fac277..60e2b3357 100644 --- a/types/components/hotelPage/sidepeek/activities.ts +++ b/types/components/hotelPage/sidepeek/activities.ts @@ -1,4 +1,6 @@ export type ActivitiesSidePeekProps = { - preamble: string - contentPageLink: string + contentPage: { + href: string + preamble: string + } } From ee7fc2f4970cf2ca9503783859f3f21d4f2a41e1 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Tue, 3 Dec 2024 14:06:23 +0100 Subject: [PATCH 14/37] feat(SW-962): set preamble value --- .../contentstack/schemas/blocks/activitiesCard.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/server/routers/contentstack/schemas/blocks/activitiesCard.ts b/server/routers/contentstack/schemas/blocks/activitiesCard.ts index cf5eeb16e..22be43b46 100644 --- a/server/routers/contentstack/schemas/blocks/activitiesCard.ts +++ b/server/routers/contentstack/schemas/blocks/activitiesCard.ts @@ -39,17 +39,13 @@ export const activitiesCardSchema = z.object({ if (data.hotel_page_activities_content_pageConnection.edges.length) { const page = data.hotel_page_activities_content_pageConnection.edges[0].node - + contentPage.preamble = page.header.preamble if (page.web.original_url) { - contentPage = { - href: page.web.original_url, - preamble: page.header.preamble, - } + contentPage.href = page.web.original_url } else { - contentPage = { - href: removeMultipleSlashes(`/${page.system.locale}/${page.url}`), - preamble: page.header.preamble, - } + contentPage.href = removeMultipleSlashes( + `/${page.system.locale}/${page.url}` + ) } } return { From e894192f96417529f487ee047515060db7243b1f Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Tue, 3 Dec 2024 09:31:57 +0100 Subject: [PATCH 15/37] fix(SW-1107): Fixed issue where current header was positioned on top of the alert --- components/Current/Header/MainMenu/mainMenu.module.css | 3 --- components/Current/Header/header.module.css | 6 ++++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/components/Current/Header/MainMenu/mainMenu.module.css b/components/Current/Header/MainMenu/mainMenu.module.css index 67131826d..0e19f88cf 100644 --- a/components/Current/Header/MainMenu/mainMenu.module.css +++ b/components/Current/Header/MainMenu/mainMenu.module.css @@ -4,10 +4,7 @@ box-shadow: 0px 1.001px 1.001px 0px rgba(0, 0, 0, 0.05); max-height: 100%; overflow: visible; - position: fixed; - top: 0; width: 100%; - z-index: var(--header-z-index); height: var(--current-mobile-site-header-height); max-width: var(--max-width-navigation); margin: 0 auto; diff --git a/components/Current/Header/header.module.css b/components/Current/Header/header.module.css index 8c4962efd..5430937c6 100644 --- a/components/Current/Header/header.module.css +++ b/components/Current/Header/header.module.css @@ -5,8 +5,10 @@ z-index: var(--header-z-index); } -@media screen and (max-width: 1366px) { +@media screen and (max-width: 950px) { .header { - height: var(--current-mobile-site-header-height); + position: sticky; + top: 0; + z-index: var(--header-z-index); } } From 562b186d40bf39ebeb95a6fedb7fa4c1bf5c0238 Mon Sep 17 00:00:00 2001 From: Hrishikesh Vaipurkar Date: Tue, 3 Dec 2024 20:31:14 +0100 Subject: [PATCH 16/37] fix: SW-1139 Fixed hydration error --- .../HotelReservation/SelectRate/RoomFilter/index.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomFilter/index.tsx b/components/HotelReservation/SelectRate/RoomFilter/index.tsx index c800c3cc9..1d874ec6e 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/index.tsx +++ b/components/HotelReservation/SelectRate/RoomFilter/index.tsx @@ -1,7 +1,7 @@ "use client" import { zodResolver } from "@hookform/resolvers/zod" -import { useCallback, useEffect, useMemo } from "react" +import { useCallback, useEffect, useMemo, useState } from "react" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" import { useMediaQuery } from "usehooks-ts" @@ -27,7 +27,8 @@ export default function RoomFilter({ onFilter, filterOptions, }: RoomFilterProps) { - const isAboveMobile = useMediaQuery("(min-width: 768px)") + const isTabletAndUp = useMediaQuery("(min-width: 768px)") + const [isAboveMobile, setIsAboveMobile] = useState(false) const initialFilterValues = useMemo( () => @@ -71,6 +72,10 @@ export default function RoomFilter({ return () => subscription.unsubscribe() }, [handleSubmit, watch, submitFilter]) + useEffect(() => { + setIsAboveMobile(isTabletAndUp) + }, [isTabletAndUp]) + return (
From 90337fc84bff534329ebfe19071ff1427e1b845b Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Mon, 2 Dec 2024 10:25:15 +0100 Subject: [PATCH 17/37] fix: set priority on logo above the fold for preloading --- components/Header/MainMenu/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/components/Header/MainMenu/index.tsx b/components/Header/MainMenu/index.tsx index e1be0051f..9119ecb5e 100644 --- a/components/Header/MainMenu/index.tsx +++ b/components/Header/MainMenu/index.tsx @@ -56,6 +56,7 @@ function Logo({ alt }: { alt: string }) { className={styles.logo} height={22} src="/_static/img/scandic-logotype.svg" + priority width={103} /> From 94d53e1da32c79ae7d91dea9478141c80e10cdd7 Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Wed, 4 Dec 2024 11:27:30 +0100 Subject: [PATCH 18/37] fix(LOY-39): add unwrapped surprises to benefits --- server/routers/contentstack/reward/query.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/server/routers/contentstack/reward/query.ts b/server/routers/contentstack/reward/query.ts index 10ddba478..f09dac201 100644 --- a/server/routers/contentstack/reward/query.ts +++ b/server/routers/contentstack/reward/query.ts @@ -238,15 +238,18 @@ export const rewardQueryRouter = router({ const nextCursor = limit + cursor < rewardIds.length ? limit + cursor : undefined - const surprisesIds = validatedApiRewards.data + const wrappedSurprisesIds = validatedApiRewards.data .filter( - ({ type, rewardType }) => - type === "coupon" && rewardType === "Surprise" + (reward) => + reward.type === "coupon" && + reward.rewardType === "Surprise" && + "coupon" in reward && + reward.coupon?.some(({ unwrapped }) => !unwrapped) ) .map(({ rewardId }) => rewardId) const rewards = cmsRewards.filter( - (reward) => !surprisesIds.includes(reward.reward_id) + (reward) => !wrappedSurprisesIds.includes(reward.reward_id) ) getCurrentRewardSuccessCounter.add(1) From 61f9290487aec2666d3689bc1a3c27931ad5d61c Mon Sep 17 00:00:00 2001 From: Bianca Widstam Date: Tue, 3 Dec 2024 11:16:28 +0100 Subject: [PATCH 19/37] fix(SW-1042)(SW-1114)(SW-1123): fix select room bugs --- .../RoomSelection/FlexibilityOption/index.tsx | 24 ++- .../RoomSelection/RateSummary/index.tsx | 163 +++++++++--------- .../RateSummary/rateSummary.module.css | 16 +- .../SelectRate/Rooms/index.tsx | 27 ++- .../selectRate/flexibilityOption.ts | 16 +- .../hotelReservation/selectRate/roomCard.ts | 15 +- .../selectRate/roomSelection.ts | 15 +- .../hotelReservation/selectRate/selectRate.ts | 3 +- 8 files changed, 173 insertions(+), 106 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx index e44c3312e..01e4890a5 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx @@ -18,9 +18,7 @@ export default function FlexibilityOption({ name, paymentTerm, priceInformation, - roomType, roomTypeCode, - features, petRoomPackage, handleSelectRate, }: FlexibilityOptionProps) { @@ -45,10 +43,22 @@ export default function FlexibilityOption({ const { public: publicPrice, member: memberPrice } = product.productType - function onChange() { - handleSelectRate({ - publicRateCode: publicPrice.rateCode, - roomTypeCode: roomTypeCode, + const onClick: React.MouseEventHandler = (e) => { + handleSelectRate((prev) => { + if ( + prev && + prev.publicRateCode === publicPrice.rateCode && + prev.roomTypeCode === roomTypeCode + ) { + if (e.currentTarget?.checked) e.currentTarget.checked = false + return undefined + } else + return { + publicRateCode: publicPrice.rateCode, + roomTypeCode: roomTypeCode, + name: name, + paymentTerm: paymentTerm, + } }) } @@ -58,7 +68,7 @@ export default function FlexibilityOption({ type="radio" name="rateCode" value={publicPrice?.rateCode} - onChange={onChange} + onClick={onClick} />
diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx index 222acef51..4322872dc 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx @@ -34,6 +34,7 @@ export default function RateSummary({ features, roomType, priceName, + priceTerm, } = rateSummary const priceToShow = isUserLoggedIn && member ? member : publicRate @@ -80,87 +81,93 @@ export default function RateSummary({
)} -
- {roomType} - {priceName} -
-
- {showMemberDiscountBanner && ( -
- - {intl.formatMessage( - { - id: "To get the member price {amount} {currency}, log in or join when completing the booking.", - }, - { - span: (str) => ( - - {str} - - ), - amount: member.localPrice.pricePerStay, - currency: member.localPrice.currency, - } - )} - -
- )} -
- - {intl.formatMessage( - { id: "Total price (incl VAT)" }, - { b: (str) => {str} } - )} - - {summaryPriceTex} +
+
+ {roomType} + {`${priceName}, ${priceTerm}`}
-
-
- - {priceToShow?.localPrice.pricePerStay}{" "} - {priceToShow?.localPrice.currency} - - - {intl.formatMessage({ id: "Approx." })}{" "} - {priceToShow?.requestedPrice?.pricePerStay}{" "} - {priceToShow?.requestedPrice?.currency} - -
-
- - {intl.formatMessage({ id: "Total price" })} - - - {priceToShow?.localPrice.pricePerStay}{" "} - {priceToShow?.localPrice.currency} - - - {summaryPriceTex} - -
- {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) => ( + + {str} + + ), + amount: member.localPrice.pricePerStay, + currency: member.localPrice.currency, + } + )} +
)} - +
+ + {intl.formatMessage( + { id: "Total price (incl VAT)" }, + { b: (str) => {str} } + )} + + {summaryPriceTex} +
+
+
+ + {priceToShow?.localPrice.pricePerStay}{" "} + {priceToShow?.localPrice.currency} + + + {intl.formatMessage({ id: "Approx." })}{" "} + {priceToShow?.requestedPrice?.pricePerStay}{" "} + {priceToShow?.requestedPrice?.currency} + +
+
+ + {intl.formatMessage({ id: "Total price" })} + + + {priceToShow?.localPrice.pricePerStay}{" "} + {priceToShow?.localPrice.currency} + + + {summaryPriceTex} + +
+ {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 4c49741c5..3271969ae 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css @@ -6,12 +6,19 @@ right: 0; background-color: var(--Base-Surface-Primary-light-Normal); padding: 0 0 var(--Spacing-x5); + align-items: center; + border-top: 1px solid var(--Base-Border-Subtle); + transition: bottom 300ms ease-in-out; +} + +.content { + width: 100%; + max-width: var(--max-width-navigation); + margin: 0 auto; display: flex; flex-direction: column; justify-content: space-between; align-items: center; - border-top: 1px solid var(--Base-Border-Subtle); - transition: bottom 300ms ease-in-out; } .summary[data-visible="true"] { @@ -80,7 +87,9 @@ @media (min-width: 768px) { .summary { - padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x5); + padding: var(--Spacing-x3) var(--Spacing-x7) var(--Spacing-x5); + } + .content { flex-direction: row; } .petInfo, @@ -102,5 +111,6 @@ .summaryPriceContainer { width: auto; padding: 0; + align-items: center; } } diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx index 857256e28..ee1758b47 100644 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -26,7 +26,13 @@ export default function Rooms({ const visibleRooms: RoomConfiguration[] = filterDuplicateRoomTypesByLowestPrice(roomsAvailability.roomConfigurations) const [selectedRate, setSelectedRate] = useState< - { publicRateCode: string; roomTypeCode: string } | undefined + | { + publicRateCode: string + roomTypeCode: string + name: string + paymentTerm: string + } + | undefined >(undefined) const [selectedPackages, setSelectedPackages] = useState( [] @@ -115,17 +121,30 @@ export default function Rooms({ ) )?.features + const roomType = roomCategories.find((roomCategory) => + roomCategory.roomTypes.some( + (roomType) => roomType.code === room.roomTypeCode + ) + ) + const rateSummary: Rate = { features: petRoomPackage && features ? features : [], - priceName: room.roomType, + priceName: selectedRate?.name, + priceTerm: selectedRate?.paymentTerm, public: product.productType.public, member: product.productType.member, - roomType: room.roomType, + roomType: roomType?.name || room.roomType, roomTypeCode: room.roomTypeCode, } return rateSummary - }, [filteredRooms, availablePackages, selectedPackages, selectedRate]) + }, [ + filteredRooms, + availablePackages, + selectedPackages, + selectedRate, + roomCategories, + ]) useEffect(() => { if (rateSummary) return diff --git a/types/components/hotelReservation/selectRate/flexibilityOption.ts b/types/components/hotelReservation/selectRate/flexibilityOption.ts index 5baef06a4..b7e5a0232 100644 --- a/types/components/hotelReservation/selectRate/flexibilityOption.ts +++ b/types/components/hotelReservation/selectRate/flexibilityOption.ts @@ -8,7 +8,6 @@ import { } from "@/server/routers/hotels/output" import { RoomPackage } from "./roomFilter" -import { Rate } from "./selectRate" type ProductPrice = z.output export type RoomPriceSchema = z.output @@ -23,10 +22,17 @@ export type FlexibilityOptionProps = { roomTypeCode: RoomConfiguration["roomTypeCode"] features: RoomConfiguration["features"] petRoomPackage: RoomPackage | undefined - handleSelectRate: (rateCode: { - publicRateCode: string - roomTypeCode: string - }) => void + handleSelectRate: React.Dispatch< + React.SetStateAction< + | { + publicRateCode: string + roomTypeCode: string + name: string + paymentTerm: string + } + | undefined + > + > } export interface PriceListProps { diff --git a/types/components/hotelReservation/selectRate/roomCard.ts b/types/components/hotelReservation/selectRate/roomCard.ts index cc7836a62..27cdf67ec 100644 --- a/types/components/hotelReservation/selectRate/roomCard.ts +++ b/types/components/hotelReservation/selectRate/roomCard.ts @@ -19,10 +19,17 @@ export type RoomCardProps = { roomCategories: RoomData[] selectedPackages: RoomPackageCodes[] packages: RoomPackageData | undefined - handleSelectRate: (rateCode: { - publicRateCode: string - roomTypeCode: string - }) => void + handleSelectRate: React.Dispatch< + React.SetStateAction< + | { + publicRateCode: string + roomTypeCode: string + name: string + paymentTerm: string + } + | undefined + > + > } type RoomPackagePriceSchema = z.output diff --git a/types/components/hotelReservation/selectRate/roomSelection.ts b/types/components/hotelReservation/selectRate/roomSelection.ts index abc07cf6a..c91fe6725 100644 --- a/types/components/hotelReservation/selectRate/roomSelection.ts +++ b/types/components/hotelReservation/selectRate/roomSelection.ts @@ -10,10 +10,17 @@ export interface RoomSelectionProps { user: SafeUser availablePackages: RoomPackageData | undefined selectedPackages: RoomPackageCodes[] - setRateCode: (rateCode: { - publicRateCode: string - roomTypeCode: string - }) => void + setRateCode: React.Dispatch< + React.SetStateAction< + | { + publicRateCode: string + roomTypeCode: string + name: string + paymentTerm: string + } + | undefined + > + > rateSummary: Rate | null } diff --git a/types/components/hotelReservation/selectRate/selectRate.ts b/types/components/hotelReservation/selectRate/selectRate.ts index 12eb83eb1..7a1fb89bb 100644 --- a/types/components/hotelReservation/selectRate/selectRate.ts +++ b/types/components/hotelReservation/selectRate/selectRate.ts @@ -27,7 +27,8 @@ export interface SelectRateSearchParams { export interface Rate { roomType: RoomConfiguration["roomType"] roomTypeCode: RoomConfiguration["roomTypeCode"] - priceName: string + priceName?: string + priceTerm?: string public: Product["productType"]["public"] member?: Product["productType"]["member"] features: RoomConfiguration["features"] From 5dec45474db6a6b625ccc5cabf7524c81bb1cae9 Mon Sep 17 00:00:00 2001 From: Bianca Widstam Date: Tue, 3 Dec 2024 11:22:48 +0100 Subject: [PATCH 20/37] fix(SW-1041): remove unused props --- .../SelectRate/RoomSelection/RoomCard/index.tsx | 2 -- .../components/hotelReservation/selectRate/flexibilityOption.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index a5b21e10d..4b8c47928 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -210,9 +210,7 @@ export default function RoomCard({ product={findProductForRate(rate)} priceInformation={getRateDefinitionForRate(rate)?.generalTerms} handleSelectRate={handleSelectRate} - roomType={roomConfiguration.roomType} roomTypeCode={roomConfiguration.roomTypeCode} - features={roomConfiguration.features} petRoomPackage={petRoomPackage} /> ))} diff --git a/types/components/hotelReservation/selectRate/flexibilityOption.ts b/types/components/hotelReservation/selectRate/flexibilityOption.ts index b7e5a0232..a4f185654 100644 --- a/types/components/hotelReservation/selectRate/flexibilityOption.ts +++ b/types/components/hotelReservation/selectRate/flexibilityOption.ts @@ -18,9 +18,7 @@ export type FlexibilityOptionProps = { value: string paymentTerm: string priceInformation?: Array - roomType: RoomConfiguration["roomType"] roomTypeCode: RoomConfiguration["roomTypeCode"] - features: RoomConfiguration["features"] petRoomPackage: RoomPackage | undefined handleSelectRate: React.Dispatch< React.SetStateAction< From a0c2d0bef797d53ae11f49e9cfa37836a24ca969 Mon Sep 17 00:00:00 2001 From: Bianca Widstam Date: Tue, 3 Dec 2024 15:16:33 +0100 Subject: [PATCH 21/37] fix(SW-1042): fix comment --- .../HotelReservation/SelectRate/Rooms/index.tsx | 17 +++++++---------- .../selectRate/flexibilityOption.ts | 14 +++----------- .../hotelReservation/selectRate/roomCard.ts | 14 ++------------ .../selectRate/roomSelection.ts | 14 ++------------ .../hotelReservation/selectRate/selectRate.ts | 7 +++++++ 5 files changed, 21 insertions(+), 45 deletions(-) diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx index ee1758b47..584e0195a 100644 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -14,7 +14,10 @@ import { type RoomPackageCodes, } from "@/types/components/hotelReservation/selectRate/roomFilter" import type { SelectRateProps } from "@/types/components/hotelReservation/selectRate/roomSelection" -import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate" +import type { + Rate, + RateCode, +} from "@/types/components/hotelReservation/selectRate/selectRate" import type { RoomConfiguration } from "@/server/routers/hotels/output" export default function Rooms({ @@ -25,15 +28,9 @@ export default function Rooms({ }: SelectRateProps) { const visibleRooms: RoomConfiguration[] = filterDuplicateRoomTypesByLowestPrice(roomsAvailability.roomConfigurations) - const [selectedRate, setSelectedRate] = useState< - | { - publicRateCode: string - roomTypeCode: string - name: string - paymentTerm: string - } - | undefined - >(undefined) + const [selectedRate, setSelectedRate] = useState( + undefined + ) const [selectedPackages, setSelectedPackages] = useState( [] ) diff --git a/types/components/hotelReservation/selectRate/flexibilityOption.ts b/types/components/hotelReservation/selectRate/flexibilityOption.ts index a4f185654..eb1b2383d 100644 --- a/types/components/hotelReservation/selectRate/flexibilityOption.ts +++ b/types/components/hotelReservation/selectRate/flexibilityOption.ts @@ -9,6 +9,8 @@ import { import { RoomPackage } from "./roomFilter" +import type { RateCode } from "./selectRate" + type ProductPrice = z.output export type RoomPriceSchema = z.output @@ -20,17 +22,7 @@ export type FlexibilityOptionProps = { priceInformation?: Array roomTypeCode: RoomConfiguration["roomTypeCode"] petRoomPackage: RoomPackage | undefined - handleSelectRate: React.Dispatch< - React.SetStateAction< - | { - publicRateCode: string - roomTypeCode: string - name: string - paymentTerm: string - } - | undefined - > - > + handleSelectRate: React.Dispatch> } export interface PriceListProps { diff --git a/types/components/hotelReservation/selectRate/roomCard.ts b/types/components/hotelReservation/selectRate/roomCard.ts index 27cdf67ec..41f0391e6 100644 --- a/types/components/hotelReservation/selectRate/roomCard.ts +++ b/types/components/hotelReservation/selectRate/roomCard.ts @@ -7,10 +7,10 @@ import { } from "@/server/routers/hotels/output" import { RoomPriceSchema } from "./flexibilityOption" -import { Rate } from "./selectRate" import type { RoomData } from "@/types/hotel" import type { RoomPackageCodes, RoomPackageData } from "./roomFilter" +import type { RateCode } from "./selectRate" export type RoomCardProps = { hotelId: string @@ -19,17 +19,7 @@ export type RoomCardProps = { roomCategories: RoomData[] selectedPackages: RoomPackageCodes[] packages: RoomPackageData | undefined - handleSelectRate: React.Dispatch< - React.SetStateAction< - | { - publicRateCode: string - roomTypeCode: string - name: string - paymentTerm: string - } - | undefined - > - > + handleSelectRate: React.Dispatch> } type RoomPackagePriceSchema = z.output diff --git a/types/components/hotelReservation/selectRate/roomSelection.ts b/types/components/hotelReservation/selectRate/roomSelection.ts index c91fe6725..31781dfc2 100644 --- a/types/components/hotelReservation/selectRate/roomSelection.ts +++ b/types/components/hotelReservation/selectRate/roomSelection.ts @@ -2,7 +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" +import type { Rate, RateCode } from "./selectRate" export interface RoomSelectionProps { roomsAvailability: RoomsAvailability @@ -10,17 +10,7 @@ export interface RoomSelectionProps { user: SafeUser availablePackages: RoomPackageData | undefined selectedPackages: RoomPackageCodes[] - setRateCode: React.Dispatch< - React.SetStateAction< - | { - publicRateCode: string - roomTypeCode: string - name: string - paymentTerm: string - } - | undefined - > - > + setRateCode: React.Dispatch> rateSummary: Rate | null } diff --git a/types/components/hotelReservation/selectRate/selectRate.ts b/types/components/hotelReservation/selectRate/selectRate.ts index 7a1fb89bb..5be51cf20 100644 --- a/types/components/hotelReservation/selectRate/selectRate.ts +++ b/types/components/hotelReservation/selectRate/selectRate.ts @@ -33,3 +33,10 @@ export interface Rate { member?: Product["productType"]["member"] features: RoomConfiguration["features"] } + +export type RateCode = { + publicRateCode: string + roomTypeCode: string + name: string + paymentTerm: string +} From 015d251bb67f1570bc1897c314b170e050eab4d8 Mon Sep 17 00:00:00 2001 From: Bianca Widstam Date: Wed, 4 Dec 2024 14:21:10 +0000 Subject: [PATCH 22/37] Merged in fix/SW-1148-font-date-picker (pull request #1032) fix(SW-1148): fix font date picker * fix(SW-1148): fix font date picker Approved-by: Pontus Dreij --- components/DatePicker/Screen/mobile.module.css | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/components/DatePicker/Screen/mobile.module.css b/components/DatePicker/Screen/mobile.module.css index 4fba31592..d26ed3423 100644 --- a/components/DatePicker/Screen/mobile.module.css +++ b/components/DatePicker/Screen/mobile.module.css @@ -159,13 +159,13 @@ td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton, } .weekDay { - color: var(--Base-Text-Medium-contrast); - font-family: var(--typography-Footnote-Labels-fontFamily); - font-size: var(--typography-Footnote-Labels-fontSize); - font-weight: var(--typography-Footnote-Labels-fontWeight); - letter-spacing: var(--typography-Footnote-Labels-letterSpacing); - line-height: var(--typography-Footnote-Labels-lineHeight); - text-decoration: var(--typography-Footnote-Labels-textDecoration); + color: var(--UI-Text-Placeholder); + font-family: var(--typography-Caption-Labels-fontFamily); + font-size: var(--typography-Caption-Labels-fontSize); + font-weight: var(--typography-Caption-Labels-fontWeight); + letter-spacing: var(--typography-Caption-Labels-letterSpacing); + line-height: var(--typography-Caption-Labels-lineHeight); + text-decoration: var(--typography-Caption-Labels-textDecoration); text-transform: uppercase; } From 50611d1be277e8504534101bb2247c3506ad325c Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Tue, 3 Dec 2024 15:33:37 +0100 Subject: [PATCH 23/37] feat(SW-644): add book parking button --- .../Parking/ParkingPrices/index.tsx | 4 +- .../AccordionAmenities/Parking/index.tsx | 37 +++++++++++++++++-- .../Parking/parkingAmenity.module.css | 8 +++- components/Icons/ExternalLink.tsx | 27 ++++++++++++++ components/Icons/get-icon-by-icon-name.ts | 3 ++ components/Icons/index.tsx | 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 + server/routers/hotels/output.ts | 1 + .../components/hotelPage/sidepeek/parking.ts | 3 +- types/components/icon.ts | 1 + 15 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 components/Icons/ExternalLink.tsx diff --git a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/ParkingPrices/index.tsx b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/ParkingPrices/index.tsx index b524c9896..3ca2bb527 100644 --- a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/ParkingPrices/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/ParkingPrices/index.tsx @@ -9,7 +9,7 @@ import { } from "@/types/components/hotelPage/sidepeek/parking" export default async function ParkingPrices({ - data, + pricing, currency, freeParking, }: ParkingPricesProps) { @@ -31,7 +31,7 @@ export default async function ParkingPrices({ } } - const filteredPeriods = data?.filter((filter) => filter.period !== "Hour") + const filteredPeriods = pricing?.filter((filter) => filter.period !== "Hour") return (
diff --git a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx index d706f03a8..47cb6afda 100644 --- a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx @@ -1,5 +1,8 @@ +import { ExternalLinkIcon } from "@/components/Icons" import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem" +import Button from "@/components/TempDesignSystem/Button" import Divider from "@/components/TempDesignSystem/Divider" +import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import { getIntl } from "@/i18n" @@ -12,7 +15,10 @@ import styles from "./parkingAmenity.module.css" import type { ParkingAmenityProps } from "@/types/components/hotelPage/sidepeek/parking" import { IconName } from "@/types/components/icon" -export default async function ParkingAmenity({ parking }: ParkingAmenityProps) { +export default async function ParkingAmenity({ + parking, + contentPageLink, +}: ParkingAmenityProps) { const intl = await getIntl() return ( @@ -43,7 +49,7 @@ export default async function ParkingAmenity({ parking }: ParkingAmenityProps) { @@ -54,15 +60,40 @@ export default async function ParkingAmenity({ parking }: ParkingAmenityProps) {
+ {data.externalParkingUrl && ( + + )}
))}
+ {contentPageLink && ( + + )} ) } diff --git a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/parkingAmenity.module.css b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/parkingAmenity.module.css index 7535e3543..6bec9b687 100644 --- a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/parkingAmenity.module.css +++ b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/parkingAmenity.module.css @@ -1,9 +1,9 @@ -.wrapper { +.wrapper, +.information { display: grid; gap: var(--Spacing-x3); } -.information, .list, .prices { display: grid; @@ -18,3 +18,7 @@ display: grid; gap: var(--Spacing-x1); } + +.contentPageLink { + margin-top: var(--Spacing-x2); +} diff --git a/components/Icons/ExternalLink.tsx b/components/Icons/ExternalLink.tsx new file mode 100644 index 000000000..b58260209 --- /dev/null +++ b/components/Icons/ExternalLink.tsx @@ -0,0 +1,27 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function ExternalLinkIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + ) +} diff --git a/components/Icons/get-icon-by-icon-name.ts b/components/Icons/get-icon-by-icon-name.ts index 7ba60f321..ed24e0377 100644 --- a/components/Icons/get-icon-by-icon-name.ts +++ b/components/Icons/get-icon-by-icon-name.ts @@ -36,6 +36,7 @@ import { ElectricBikeIcon, ElectricCarIcon, EmailIcon, + ExternalLinkIcon, EyeHideIcon, EyeShowIcon, FacebookIcon, @@ -176,6 +177,8 @@ export function getIconByIconName( return ElectricCarIcon case IconName.Email: return EmailIcon + case IconName.ExternalLink: + return ExternalLinkIcon case IconName.EyeHide: return EyeHideIcon case IconName.EyeShow: diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index f56e5128f..580e941cd 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -61,6 +61,7 @@ export { default as ElectricBikeIcon } from "./ElectricBike" export { default as ElectricCarIcon } from "./ElectricCar" export { default as EmailIcon } from "./Email" export { default as ErrorCircleIcon } from "./ErrorCircle" +export { default as ExternalLinkIcon } from "./ExternalLink" export { default as EyeHideIcon } from "./EyeHide" export { default as EyeShowIcon } from "./EyeShow" export { default as FacebookIcon } from "./Facebook" diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 9b25a0cf0..eefc1cd1d 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -8,6 +8,7 @@ "ALLG": "Allergi", "About accessibility": "Om tilgængelighed", "About meetings & conferences": "About meetings & conferences", + "About parking": "Om parkering", "About the hotel": "Om hotellet", "Accept new price": "Accepter ny pris", "Accessibility": "Tilgængelighed", @@ -47,6 +48,7 @@ "Bed type": "Seng type", "Birth date": "Fødselsdato", "Book": "Book", + "Book parking": "Book parkering", "Book reward night": "Book bonusnat", "Booking number": "Bookingnummer", "Breakfast": "Morgenmad", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 260a74ec3..8c5352c9b 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -8,6 +8,7 @@ "ALLG": "Allergie", "About accessibility": "Über Barrierefreiheit", "About meetings & conferences": "About meetings & conferences", + "About parking": "Über das Parken", "About the hotel": "Über das Hotel", "Accept new price": "Neuen Preis akzeptieren", "Accessibility": "Zugänglichkeit", @@ -47,6 +48,7 @@ "Bed type": "Bettentyp", "Birth date": "Geburtsdatum", "Book": "Buchen", + "Book parking": "Parkplatz buchen", "Book reward night": "Bonusnacht buchen", "Booking number": "Buchungsnummer", "Breakfast": "Frühstück", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index e1a898dc5..91c371258 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -8,6 +8,7 @@ "ALLG": "Allergy", "About accessibility": "About accessibility", "About meetings & conferences": "About meetings & conferences", + "About parking": "About parking", "About the hotel": "About the hotel", "Accept new price": "Accept new price", "Accessibility": "Accessibility", @@ -49,6 +50,7 @@ "Birth date": "Birth date", "Book": "Book", "Book another stay": "Book another stay", + "Book parking": "Book parking", "Book reward night": "Book reward night", "Book your next stay": "Book your next stay", "Booking": "Booking", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 35e197fbc..086e635de 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -8,6 +8,7 @@ "ALLG": "Allergia", "About accessibility": "Tietoja saavutettavuudesta", "About meetings & conferences": "About meetings & conferences", + "About parking": "Tietoja pysäköinnistä", "About the hotel": "Tietoja hotellista", "Accept new price": "Hyväksy uusi hinta", "Accessibility": "Saavutettavuus", @@ -47,6 +48,7 @@ "Bed type": "Vuodetyyppi", "Birth date": "Syntymäaika", "Book": "Varaa", + "Book parking": "Varaa pysäköinti", "Book reward night": "Kirjapalkinto-ilta", "Booking number": "Varausnumero", "Breakfast": "Aamiainen", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 64c3299cb..6de6aa1e4 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -8,6 +8,7 @@ "ALLG": "Allergi", "About accessibility": "Om tilgjengelighet", "About meetings & conferences": "About meetings & conferences", + "About parking": "Om parkering", "About the hotel": "Om hotellet", "Accept new price": "Aksepterer ny pris", "Accessibility": "Tilgjengelighet", @@ -47,6 +48,7 @@ "Bed type": "Seng type", "Birth date": "Fødselsdato", "Book": "Bestill", + "Book parking": "Bestill parkering", "Book reward night": "Bestill belønningskveld", "Booking number": "Bestillingsnummer", "Breakfast": "Frokost", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index ee471eee7..31ad5c056 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -8,6 +8,7 @@ "ALLG": "Allergi", "About accessibility": "Om tillgänglighet", "About meetings & conferences": "About meetings & conferences", + "About parking": "Om parkering", "About the hotel": "Om hotellet", "Accept new price": "Accepter ny pris", "Accessibility": "Tillgänglighet", @@ -47,6 +48,7 @@ "Bed type": "Sängtyp", "Birth date": "Födelsedatum", "Book": "Boka", + "Book parking": "Boka parkering", "Book reward night": "Boka frinatt", "Booking number": "Bokningsnummer", "Breakfast": "Frukost", diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 76f29349a..0b5cfb726 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -298,6 +298,7 @@ export const parkingSchema = z.object({ numberOfChargingSpaces: z.number().optional(), distanceToHotel: z.number().optional(), canMakeReservation: z.boolean(), + externalParkingUrl: z.string().optional(), pricing: parkingPricingSchema, }) diff --git a/types/components/hotelPage/sidepeek/parking.ts b/types/components/hotelPage/sidepeek/parking.ts index 67c1653e8..6a424246f 100644 --- a/types/components/hotelPage/sidepeek/parking.ts +++ b/types/components/hotelPage/sidepeek/parking.ts @@ -8,6 +8,7 @@ export enum Periods { export type ParkingAmenityProps = { parking: Hotel["parking"] + contentPageLink?: string } export type ParkingListProps = { @@ -19,7 +20,7 @@ export type ParkingListProps = { } export type ParkingPricesProps = { - data: Hotel["parking"][number]["pricing"]["localCurrency"]["ordinary"] + pricing: Hotel["parking"][number]["pricing"]["localCurrency"]["ordinary"] currency: Hotel["parking"][number]["pricing"]["localCurrency"]["currency"] freeParking: Hotel["parking"][number]["pricing"]["freeParking"] } diff --git a/types/components/icon.ts b/types/components/icon.ts index 3d47bbe9a..b21d12cdd 100644 --- a/types/components/icon.ts +++ b/types/components/icon.ts @@ -42,6 +42,7 @@ export enum IconName { ElectricBike = "ElectricBike", ElectricCar = "ElectricCar", Email = "Email", + ExternalLink = "ExternalLink", EyeHide = "EyeHide", EyeShow = "EyeShow", Facebook = "Facebook", From 5453cddd3a45152a35db4fe71c5bb49717c404d9 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Wed, 4 Dec 2024 15:54:46 +0100 Subject: [PATCH 24/37] feat(SW-644): rename prop name --- .../Amenities/AccordionAmenities/Parking/index.tsx | 8 ++++---- .../AccordionAmenities/Parking/parkingAmenity.module.css | 2 +- types/components/hotelPage/sidepeek/parking.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx index 47cb6afda..8a134dd74 100644 --- a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx @@ -17,7 +17,7 @@ import { IconName } from "@/types/components/icon" export default async function ParkingAmenity({ parking, - contentPageLink, + parkingPageLink, }: ParkingAmenityProps) { const intl = await getIntl() @@ -82,14 +82,14 @@ export default async function ParkingAmenity({
))}
- {contentPageLink && ( + {parkingPageLink && ( diff --git a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/parkingAmenity.module.css b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/parkingAmenity.module.css index 6bec9b687..b5fccc6cf 100644 --- a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/parkingAmenity.module.css +++ b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/parkingAmenity.module.css @@ -19,6 +19,6 @@ gap: var(--Spacing-x1); } -.contentPageLink { +.parkingPageLink { margin-top: var(--Spacing-x2); } diff --git a/types/components/hotelPage/sidepeek/parking.ts b/types/components/hotelPage/sidepeek/parking.ts index 6a424246f..cb8306385 100644 --- a/types/components/hotelPage/sidepeek/parking.ts +++ b/types/components/hotelPage/sidepeek/parking.ts @@ -8,7 +8,7 @@ export enum Periods { export type ParkingAmenityProps = { parking: Hotel["parking"] - contentPageLink?: string + parkingPageLink?: string } export type ParkingListProps = { From f40089daea50b371d0e3cb93930d15bf76220c87 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Wed, 4 Dec 2024 16:11:24 +0100 Subject: [PATCH 25/37] feat(SW-644): add boelan --- .../Amenities/AccordionAmenities/Parking/index.tsx | 7 ++++--- types/components/hotelPage/sidepeek/parking.ts | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx index 8a134dd74..697527048 100644 --- a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx @@ -17,7 +17,7 @@ import { IconName } from "@/types/components/icon" export default async function ParkingAmenity({ parking, - parkingPageLink, + hasParkingPage, }: ParkingAmenityProps) { const intl = await getIntl() @@ -82,14 +82,15 @@ export default async function ParkingAmenity({
))}
- {parkingPageLink && ( + {hasParkingPage && ( diff --git a/types/components/hotelPage/sidepeek/parking.ts b/types/components/hotelPage/sidepeek/parking.ts index cb8306385..6bda92d1c 100644 --- a/types/components/hotelPage/sidepeek/parking.ts +++ b/types/components/hotelPage/sidepeek/parking.ts @@ -8,7 +8,7 @@ export enum Periods { export type ParkingAmenityProps = { parking: Hotel["parking"] - parkingPageLink?: string + hasParkingPage?: boolean } export type ParkingListProps = { From a4db92a093bd6e450131295e4f487c3ce0176d50 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Wed, 4 Dec 2024 13:35:57 +0100 Subject: [PATCH 26/37] feat(SW-889): design changes --- components/Sidebar/index.tsx | 5 +---- .../TeaserCard/Sidepeek/index.tsx | 19 ++++--------------- .../TempDesignSystem/TeaserCard/index.tsx | 6 +++--- 3 files changed, 8 insertions(+), 22 deletions(-) diff --git a/components/Sidebar/index.tsx b/components/Sidebar/index.tsx index 53e88126e..5ceaadbe4 100644 --- a/components/Sidebar/index.tsx +++ b/components/Sidebar/index.tsx @@ -19,10 +19,7 @@ export default function Sidebar({ blocks }: SidebarProps) { switch (block.typename) { case SidebarEnums.blocks.Content: return ( -
+
{button.call_to_action_text} - +
{primary_button && ( -
) } + +export function MainMenuSkeleton() { + const intl = useIntl() + + const links = new Array(5).fill("") + return ( +
+
+ + +
+
+ ) +} diff --git a/components/Current/Header/MainMenu/mainMenu.module.css b/components/Current/Header/MainMenu/mainMenu.module.css index 67131826d..f2855d8ca 100644 --- a/components/Current/Header/MainMenu/mainMenu.module.css +++ b/components/Current/Header/MainMenu/mainMenu.module.css @@ -27,11 +27,9 @@ .navBar { display: grid; grid-template-columns: 1fr 80px 1fr; - grid-template-columns: auto auto 1fr auto; grid-template-areas: "expanderBtn logoLink . buttonContainer"; grid-template-rows: 100%; height: 100%; - padding: 0 var(--Spacing-x2); } .expanderBtn { @@ -50,7 +48,7 @@ background: #757575; border-radius: 2.3px; display: inline-block; - height: 3px; + height: 5px; position: relative; transition: 0.3s; width: 32px; @@ -107,7 +105,6 @@ align-items: center; height: 100%; width: 80px; - padding-left: var(--Spacing-x1); } .logo { @@ -241,6 +238,12 @@ display: none; } +.skeletonWrapper { + padding: 4px 10px; + height: 100%; + align-content: center; +} + @media (min-width: 1367px) { .navBar { grid-template-columns: 140px auto 1fr; diff --git a/components/Current/Header/TopMenu/index.tsx b/components/Current/Header/TopMenu/index.tsx index b33c44424..8f340e56b 100644 --- a/components/Current/Header/TopMenu/index.tsx +++ b/components/Current/Header/TopMenu/index.tsx @@ -3,6 +3,7 @@ import { overview } from "@/constants/routes/myPages" import { getName } from "@/lib/trpc/memoizedRequests" import LoginButton from "@/components/LoginButton" +import SkeletonShimmer from "@/components/SkeletonShimmer" import Link from "@/components/TempDesignSystem/Link" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" @@ -68,6 +69,8 @@ export default async function TopMenu({ position="hamburger menu" trackingId="loginStartTopMenu" className={`${styles.sessionLink} ${styles.loginLink}`} + variant="default" + size="small" > {intl.formatMessage({ id: "Log in" })} @@ -78,3 +81,32 @@ export default async function TopMenu({
) } + +export async function TopMenuSkeleton() { + const intl = await getIntl() + const links = new Array(5).fill("") + return ( +
+
+
    + {links.map((link, i) => ( +
  • + +
  • + ))} +
  • + + {intl.formatMessage({ id: "Log in" })} + +
  • +
+
+
+ ) +} diff --git a/components/Current/Header/TopMenu/topMenu.module.css b/components/Current/Header/TopMenu/topMenu.module.css index 67be14a58..ab8c0733c 100644 --- a/components/Current/Header/TopMenu/topMenu.module.css +++ b/components/Current/Header/TopMenu/topMenu.module.css @@ -49,6 +49,12 @@ display: block; } +.skeletonWrapper { + padding: 4px 10px; + height: 30px; + align-content: center; +} + @media screen and (min-width: 768px) { .container { padding: 0 30px; From bed674df87cecc5674cc75e6fff9e933414db878 Mon Sep 17 00:00:00 2001 From: Bianca Widstam Date: Thu, 5 Dec 2024 07:39:06 +0000 Subject: [PATCH 29/37] Merged in fix/SW-1128-side-peek (pull request #1031) Fix/SW-1128/SW-1124 side peek and gallery * fix(SW-1128): updated style and mobile design for sidepeek select hotel * fix(SW-1128): update link sidepeek * fix(SW-1124): fix padding gallery * fix(SW-1128): fix sidepeek mobile design * fix(SW-1128): fix mobile design * fix(SW-1128): fix gallery icon caption Approved-by: Niclas Edenvin --- .../Contact/contact.module.css | 25 +++++++--- components/HotelReservation/Contact/index.tsx | 47 +++++++++---------- components/ImageGallery/index.tsx | 7 +-- components/Lightbox/Lightbox.module.css | 1 + .../Accordions/Accessibility.tsx | 1 + .../Accordions/CheckInCheckOut.tsx | 1 + .../Accordions/MeetingsAndConferences.tsx | 1 + .../HotelSidePeek/Accordions/Parking.tsx | 1 + .../HotelSidePeek/Accordions/Restaurant.tsx | 1 + .../Accordions/sidePeekAccordion.module.css | 1 + .../HotelSidePeek/hotelSidePeek.module.css | 23 ++++++--- components/SidePeeks/HotelSidePeek/index.tsx | 19 ++++---- .../AccordionItem/accordionItem.module.css | 11 ++++- .../Accordion/AccordionItem/index.tsx | 22 +++++---- .../Accordion/AccordionItem/variants.ts | 1 + .../TempDesignSystem/Accordion/variants.ts | 1 + 16 files changed, 100 insertions(+), 63 deletions(-) diff --git a/components/HotelReservation/Contact/contact.module.css b/components/HotelReservation/Contact/contact.module.css index e35862fed..01451a32f 100644 --- a/components/HotelReservation/Contact/contact.module.css +++ b/components/HotelReservation/Contact/contact.module.css @@ -4,6 +4,7 @@ grid-template-rows: auto; gap: var(--Spacing-x2); font-family: var(--typography-Body-Regular-fontFamily); + margin-bottom: var(--Spacing-x3); } .address, @@ -20,6 +21,7 @@ list-style-type: none; display: flex; flex-direction: column; + min-width: 0; } .soMeIcons { @@ -28,6 +30,19 @@ } .ecoLabel { + width: 38px; + height: auto; +} + +.ecoLabel img { + width: 100%; + height: auto; + flex-shrink: 0; + grid-column: 1 / 3; + grid-row: 4 / 4; +} + +.ecoContainer { display: flex; align-items: center; column-gap: var(--Spacing-x-one-and-half); @@ -38,10 +53,6 @@ margin-bottom: var(--Spacing-x1); } -.ecoLabel img { - flex-shrink: 0; -} - .ecoLabelText { display: flex; color: var(--UI-Text-Medium-contrast); @@ -49,8 +60,8 @@ justify-content: center; } -.googleMaps { - text-decoration: none; +.link { + text-decoration: underline; font-family: var(--typography-Body-Regular-fontFamily); - color: var(--Base-Text-Medium-contrast); + color: var(--Base-Text-High-contrast); } diff --git a/components/HotelReservation/Contact/index.tsx b/components/HotelReservation/Contact/index.tsx index 360831c94..d62305355 100644 --- a/components/HotelReservation/Contact/index.tsx +++ b/components/HotelReservation/Contact/index.tsx @@ -24,31 +24,27 @@ export default function Contact({ hotel }: ContactProps) { {intl.formatMessage({ id: "Address" })} - - {`${hotel.address.streetAddress}, ${hotel.address.city}`} - + {`${hotel.address.streetAddress}, `} + {hotel.address.city}
  • {intl.formatMessage({ id: "Driving directions" })} - - Google Maps - + Google Maps +
  • {intl.formatMessage({ id: "Contact us" })} - - {hotel.contactInformation.phoneNumber} + + + {hotel.contactInformation.phoneNumber} +
  • @@ -76,23 +72,24 @@ export default function Contact({ hotel }: ContactProps) { {intl.formatMessage({ id: "Email" })} - - {hotel.contactInformation.email} + + + {hotel.contactInformation.email} +
  • {hotel.hotelFacts.ecoLabels?.nordicEcoLabel ? ( -
    - {intl.formatMessage({ +
    +
    + {intl.formatMessage({ +
    {intl.formatMessage({ id: "Nordic Swan Ecolabel" })} diff --git a/components/ImageGallery/index.tsx b/components/ImageGallery/index.tsx index d26ed0007..007785ad8 100644 --- a/components/ImageGallery/index.tsx +++ b/components/ImageGallery/index.tsx @@ -6,7 +6,8 @@ import { useIntl } from "react-intl" import { GalleryIcon } from "@/components/Icons" import Image from "@/components/Image" import Lightbox from "@/components/Lightbox" -import Footnote from "@/components/TempDesignSystem/Text/Footnote" + +import Caption from "../TempDesignSystem/Text/Caption" import styles from "./imageGallery.module.css" @@ -44,9 +45,9 @@ function ImageGallery({ />
    - + {images.length} - +
    {accessibilityElevatorPitchText} diff --git a/components/SidePeeks/HotelSidePeek/Accordions/CheckInCheckOut.tsx b/components/SidePeeks/HotelSidePeek/Accordions/CheckInCheckOut.tsx index d005e6688..6848bc801 100644 --- a/components/SidePeeks/HotelSidePeek/Accordions/CheckInCheckOut.tsx +++ b/components/SidePeeks/HotelSidePeek/Accordions/CheckInCheckOut.tsx @@ -13,6 +13,7 @@ export default function CheckinCheckOut({ checkin }: CheckInCheckOutProps) { {intl.formatMessage({ id: "Hours" })} {`${intl.formatMessage({ id: "Check in from" })}: ${checkin.checkInTime}`} diff --git a/components/SidePeeks/HotelSidePeek/Accordions/MeetingsAndConferences.tsx b/components/SidePeeks/HotelSidePeek/Accordions/MeetingsAndConferences.tsx index 558046d4b..8168a764f 100644 --- a/components/SidePeeks/HotelSidePeek/Accordions/MeetingsAndConferences.tsx +++ b/components/SidePeeks/HotelSidePeek/Accordions/MeetingsAndConferences.tsx @@ -14,6 +14,7 @@ export default function MeetingsAndConferences({ {meetingDescription} diff --git a/components/SidePeeks/HotelSidePeek/Accordions/Parking.tsx b/components/SidePeeks/HotelSidePeek/Accordions/Parking.tsx index 0e5ffcec0..ba4ee33f7 100644 --- a/components/SidePeeks/HotelSidePeek/Accordions/Parking.tsx +++ b/components/SidePeeks/HotelSidePeek/Accordions/Parking.tsx @@ -16,6 +16,7 @@ export default function Parking({ parking }: ParkingProps) { title={intl.formatMessage({ id: "Parking" })} icon={IconName.Parking} className={styles.parking} + variant="sidepeek" > {parking.map((p) => (
    diff --git a/components/SidePeeks/HotelSidePeek/Accordions/Restaurant.tsx b/components/SidePeeks/HotelSidePeek/Accordions/Restaurant.tsx index a8d1cd047..d35a30304 100644 --- a/components/SidePeeks/HotelSidePeek/Accordions/Restaurant.tsx +++ b/components/SidePeeks/HotelSidePeek/Accordions/Restaurant.tsx @@ -15,6 +15,7 @@ export default function Restaurant({ {restaurantsContentDescriptionMedium} diff --git a/components/SidePeeks/HotelSidePeek/Accordions/sidePeekAccordion.module.css b/components/SidePeeks/HotelSidePeek/Accordions/sidePeekAccordion.module.css index bd2ae8933..0134e2165 100644 --- a/components/SidePeeks/HotelSidePeek/Accordions/sidePeekAccordion.module.css +++ b/components/SidePeeks/HotelSidePeek/Accordions/sidePeekAccordion.module.css @@ -10,6 +10,7 @@ align-items: center; gap: var(--Spacing-x1); padding-left: var(--Spacing-x1); + justify-items: flex-start; } .list li svg { diff --git a/components/SidePeeks/HotelSidePeek/hotelSidePeek.module.css b/components/SidePeeks/HotelSidePeek/hotelSidePeek.module.css index 23b92b792..481e6b47d 100644 --- a/components/SidePeeks/HotelSidePeek/hotelSidePeek.module.css +++ b/components/SidePeeks/HotelSidePeek/hotelSidePeek.module.css @@ -9,13 +9,24 @@ gap: var(--Spacing-x2); } -.amenity { - font-family: var(--typography-Body-Regular-fontFamily); - border-bottom: 1px solid var(--Base-Border-Subtle); - /* padding set to align with AccordionItem which has a different composition */ - padding: calc(var(--Spacing-x1) + var(--Spacing-x-one-and-half)) - var(--Spacing-x3); +.content:last-child { + gap: 0; +} + +.content > p { + margin-bottom: var(--Spacing-x-one-and-half); +} + +.content > ul > li:first-child { + border-top: 1px solid var(--Base-Border-Subtle); +} + +.amenity > p { + border-top: 1px solid var(--Base-Border-Subtle); + padding: calc(var(--Spacing-x-one-and-half) + var(--Spacing-x1)) + var(--Spacing-x1); display: flex; + align-items: center; gap: var(--Spacing-x1); } diff --git a/components/SidePeeks/HotelSidePeek/index.tsx b/components/SidePeeks/HotelSidePeek/index.tsx index 4fc5bab8c..dad75e7b3 100644 --- a/components/SidePeeks/HotelSidePeek/index.tsx +++ b/components/SidePeeks/HotelSidePeek/index.tsx @@ -71,24 +71,21 @@ export default function HotelSidePeek({ } /> )} + +
    {amenitiesList.map((amenity) => { const Icon = mapFacilityToIcon(amenity.id) return ( -
    + {Icon && ( - + )} - - {amenity.name} - -
    + {amenity.name} + ) })} - +
    + {/* TODO: handle linking to Hotel Page */} {/* {showCTA && ( +
    +
    + + ) +} diff --git a/components/ContentType/HotelPage/SidePeeks/MeetingsAndConferences/meetingsAndConferences.module.css b/components/ContentType/HotelPage/SidePeeks/MeetingsAndConferences/meetingsAndConferences.module.css new file mode 100644 index 000000000..b150919a4 --- /dev/null +++ b/components/ContentType/HotelPage/SidePeeks/MeetingsAndConferences/meetingsAndConferences.module.css @@ -0,0 +1,35 @@ +.wrapper { + display: flex; + flex-direction: column; + gap: var(--Spacing-x4); + margin-bottom: calc( + var(--Spacing-x4) * 2 + 80px + ); /* Creates space between the wrapper and buttonContainer */ +} + +.information { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; + gap: var(--Spacing-x2); +} + +.image { + width: 100%; + height: 175px; + object-fit: cover; +} + +.text { + grid-column: 1 / 3; +} + +.buttonContainer { + background-color: var(--Base-Background-Primary-Normal); + border-top: 1px solid var(--Base-Border-Subtle); + padding: var(--Spacing-x4) var(--Spacing-x2); + width: 100%; + position: absolute; + left: 0; + bottom: 0; +} diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index eefc1cd1d..3a66fc700 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -95,6 +95,7 @@ "Could not find requested resource": "Kunne ikke finde den anmodede ressource", "Country": "Land", "Country code": "Landekode", + "Creative spaces for meetings": "Kreative rum til møder", "Credit card": "Kreditkort", "Credit card deleted successfully": "Kreditkort blev slettet", "Currency Code": "DKK", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 8c5352c9b..8768310d2 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -95,6 +95,7 @@ "Could not find requested resource": "Die angeforderte Ressource konnte nicht gefunden werden.", "Country": "Land", "Country code": "Landesvorwahl", + "Creative spaces for meetings": "Kreative Räume für Meetings", "Credit card": "Kreditkarte", "Credit card deleted successfully": "Kreditkarte erfolgreich gelöscht", "Currency Code": "EUR", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 91c371258..4193038a4 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -103,6 +103,7 @@ "Could not find requested resource": "Could not find requested resource", "Country": "Country", "Country code": "Country code", + "Creative spaces for meetings": "Creative spaces for meetings", "Credit card": "Credit card", "Credit card deleted successfully": "Credit card deleted successfully", "Currency Code": "EUR", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 086e635de..4a0ca7811 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -95,6 +95,7 @@ "Could not find requested resource": "Pyydettyä resurssia ei löytynyt", "Country": "Maa", "Country code": "Maatunnus", + "Creative spaces for meetings": "Luovia tiloja kokouksille", "Credit card": "Luottokortti", "Credit card deleted successfully": "Luottokortti poistettu onnistuneesti", "Currency Code": "EUR", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 6de6aa1e4..8f849219f 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -95,6 +95,7 @@ "Could not find requested resource": "Kunne ikke finne den forespurte ressursen", "Country": "Land", "Country code": "Landskode", + "Creative spaces for meetings": "Kreative rom for møter", "Credit card deleted successfully": "Kredittkort slettet", "Currency Code": "NOK", "Current password": "Nåværende passord", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 31ad5c056..620c4c747 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -95,6 +95,7 @@ "Could not find requested resource": "Det gick inte att hitta den begärda resursen", "Country": "Land", "Country code": "Landskod", + "Creative spaces for meetings": "Kreativa utrymmen för möten", "Credit card deleted successfully": "Kreditkort har tagits bort", "Currency Code": "SEK", "Current password": "Nuvarande lösenord", diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index e91905c6b..f91b4a762 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -210,6 +210,87 @@ export const getHotelData = cache( ) export const hotelQueryRouter = router({ + get: contentStackUidWithServiceProcedure.query(async ({ ctx }) => { + const { lang, uid } = ctx + + const contentstackData = await getContentstackData(lang, uid) + const hotelId = contentstackData?.hotel_page_id + + if (!hotelId) { + throw notFound(`Hotel not found for uid: ${uid}`) + } + + const hotelData = await getHotelData( + { + hotelId, + language: ctx.lang, + }, + ctx.serviceToken + ) + + if (!hotelData) { + throw notFound() + } + + const included = hotelData.included || [] + + const hotelAttributes = hotelData.data.attributes + const images = hotelAttributes.gallery?.smallerImages + const hotelAlerts = hotelAttributes.specialAlerts + + const roomCategories = included + ? included.filter((item) => item.type === "roomcategories") + : [] + + const activities = contentstackData?.content + ? contentstackData?.content[0] + : null + + const facilities: Facility[] = [ + { + ...hotelData.data.attributes.restaurantImages, + id: FacilityCardTypeEnum.restaurant, + headingText: + hotelData?.data.attributes.restaurantImages?.headingText ?? "", + heroImages: + hotelData?.data.attributes.restaurantImages?.heroImages ?? [], + }, + { + ...hotelData.data.attributes.conferencesAndMeetings, + id: FacilityCardTypeEnum.conference, + headingText: + hotelData?.data.attributes.conferencesAndMeetings?.headingText ?? "", + heroImages: + hotelData?.data.attributes.conferencesAndMeetings?.heroImages ?? [], + }, + { + ...hotelData.data.attributes.healthAndWellness, + id: FacilityCardTypeEnum.wellness, + headingText: + hotelData?.data.attributes.healthAndWellness?.headingText ?? "", + heroImages: + hotelData?.data.attributes.healthAndWellness?.heroImages ?? [], + }, + ] + + return { + hotelId, + hotelName: hotelAttributes.name, + hotelDescription: hotelAttributes.hotelContent.texts.descriptions.short, + hotelLocation: hotelAttributes.location, + hotelAddress: hotelAttributes.address, + hotelRatings: hotelAttributes.ratings, + hotelDetailedFacilities: hotelAttributes.detailedFacilities, + hotelImages: images, + pointsOfInterest: hotelAttributes.pointsOfInterest, + roomCategories, + activitiesCard: activities?.upcoming_activities_card, + facilities, + alerts: hotelAlerts, + faq: contentstackData?.faq, + healthFacilities: hotelAttributes.healthFacilities, + } + }), availability: router({ hotels: serviceProcedure .input(getHotelsAvailabilityInputSchema) diff --git a/types/components/hotelPage/sidepeek/meetingsAndConferences.ts b/types/components/hotelPage/sidepeek/meetingsAndConferences.ts new file mode 100644 index 000000000..abbfc8214 --- /dev/null +++ b/types/components/hotelPage/sidepeek/meetingsAndConferences.ts @@ -0,0 +1,5 @@ +import type { Hotel } from "@/types/hotel" + +export type MeetingsAndConferencesSidePeekProps = { + meetingFacilities: Hotel["conferencesAndMeetings"] +} From 159464d6722fa5ebf3f2194b53e4e799535014b4 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Fri, 29 Nov 2024 15:50:39 +0100 Subject: [PATCH 31/37] feat(SW-936): fix merge conflict --- server/routers/hotels/query.ts | 81 ---------------------------------- 1 file changed, 81 deletions(-) diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index f91b4a762..e91905c6b 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -210,87 +210,6 @@ export const getHotelData = cache( ) export const hotelQueryRouter = router({ - get: contentStackUidWithServiceProcedure.query(async ({ ctx }) => { - const { lang, uid } = ctx - - const contentstackData = await getContentstackData(lang, uid) - const hotelId = contentstackData?.hotel_page_id - - if (!hotelId) { - throw notFound(`Hotel not found for uid: ${uid}`) - } - - const hotelData = await getHotelData( - { - hotelId, - language: ctx.lang, - }, - ctx.serviceToken - ) - - if (!hotelData) { - throw notFound() - } - - const included = hotelData.included || [] - - const hotelAttributes = hotelData.data.attributes - const images = hotelAttributes.gallery?.smallerImages - const hotelAlerts = hotelAttributes.specialAlerts - - const roomCategories = included - ? included.filter((item) => item.type === "roomcategories") - : [] - - const activities = contentstackData?.content - ? contentstackData?.content[0] - : null - - const facilities: Facility[] = [ - { - ...hotelData.data.attributes.restaurantImages, - id: FacilityCardTypeEnum.restaurant, - headingText: - hotelData?.data.attributes.restaurantImages?.headingText ?? "", - heroImages: - hotelData?.data.attributes.restaurantImages?.heroImages ?? [], - }, - { - ...hotelData.data.attributes.conferencesAndMeetings, - id: FacilityCardTypeEnum.conference, - headingText: - hotelData?.data.attributes.conferencesAndMeetings?.headingText ?? "", - heroImages: - hotelData?.data.attributes.conferencesAndMeetings?.heroImages ?? [], - }, - { - ...hotelData.data.attributes.healthAndWellness, - id: FacilityCardTypeEnum.wellness, - headingText: - hotelData?.data.attributes.healthAndWellness?.headingText ?? "", - heroImages: - hotelData?.data.attributes.healthAndWellness?.heroImages ?? [], - }, - ] - - return { - hotelId, - hotelName: hotelAttributes.name, - hotelDescription: hotelAttributes.hotelContent.texts.descriptions.short, - hotelLocation: hotelAttributes.location, - hotelAddress: hotelAttributes.address, - hotelRatings: hotelAttributes.ratings, - hotelDetailedFacilities: hotelAttributes.detailedFacilities, - hotelImages: images, - pointsOfInterest: hotelAttributes.pointsOfInterest, - roomCategories, - activitiesCard: activities?.upcoming_activities_card, - facilities, - alerts: hotelAlerts, - faq: contentstackData?.faq, - healthFacilities: hotelAttributes.healthFacilities, - } - }), availability: router({ hotels: serviceProcedure .input(getHotelsAvailabilityInputSchema) From 2fb4f6a702fedb10f8483186c21fcfd750e31368 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Fri, 29 Nov 2024 16:41:57 +0100 Subject: [PATCH 32/37] feat(SW-936): add descriptions --- .../MeetingsAndConferences/index.tsx | 49 ++++++++++++------- .../meetingsAndConferences.module.css | 19 ++----- .../ContentType/HotelPage/SidePeeks/index.ts | 1 + components/ContentType/HotelPage/index.tsx | 17 +++---- .../sidepeek/meetingsAndConferences.ts | 2 + 5 files changed, 43 insertions(+), 45 deletions(-) diff --git a/components/ContentType/HotelPage/SidePeeks/MeetingsAndConferences/index.tsx b/components/ContentType/HotelPage/SidePeeks/MeetingsAndConferences/index.tsx index 637c4533c..21ca2b336 100644 --- a/components/ContentType/HotelPage/SidePeeks/MeetingsAndConferences/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/MeetingsAndConferences/index.tsx @@ -4,6 +4,7 @@ import Image from "@/components/Image" import Button from "@/components/TempDesignSystem/Button" import Link from "@/components/TempDesignSystem/Link" import SidePeek from "@/components/TempDesignSystem/SidePeek" +import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" @@ -11,13 +12,20 @@ import { getLang } from "@/i18n/serverContext" import styles from "./meetingsAndConferences.module.css" -import { MeetingsAndConferencesSidePeekProps } from "@/types/components/hotelPage/sidepeek/meetingsAndConferences" +import type { MeetingsAndConferencesSidePeekProps } from "@/types/components/hotelPage/sidepeek/meetingsAndConferences" export default async function MeetingsAndConferencesSidePeek({ meetingFacilities, + descriptions, + link, }: MeetingsAndConferencesSidePeekProps) { const lang = getLang() const intl = await getIntl() + const mainImage = meetingFacilities?.heroImages[0].imageSizes.medium + const altText = + meetingFacilities?.heroImages[0].metaData.altText || + intl.formatMessage({ id: "Creative spaces for meetings" }) + return ( -
    + {mainImage && ( - -
    {meetingFacilities?.headingText}
    -
    -
    - + )} + {descriptions.medium} +
    + + [Min to Max capacity square meter info] + + + [Min to Max capacity persons info] +
    + {link && ( +
    + +
    + )}
    ) diff --git a/components/ContentType/HotelPage/SidePeeks/MeetingsAndConferences/meetingsAndConferences.module.css b/components/ContentType/HotelPage/SidePeeks/MeetingsAndConferences/meetingsAndConferences.module.css index b150919a4..058c7338b 100644 --- a/components/ContentType/HotelPage/SidePeeks/MeetingsAndConferences/meetingsAndConferences.module.css +++ b/components/ContentType/HotelPage/SidePeeks/MeetingsAndConferences/meetingsAndConferences.module.css @@ -1,27 +1,16 @@ .wrapper { - display: flex; - flex-direction: column; - gap: var(--Spacing-x4); + display: grid; + gap: var(--Spacing-x3); margin-bottom: calc( var(--Spacing-x4) * 2 + 80px ); /* Creates space between the wrapper and buttonContainer */ } -.information { - display: grid; - grid-template-columns: 1fr 1fr; - grid-template-rows: 1fr 1fr; - gap: var(--Spacing-x2); -} - .image { width: 100%; - height: 175px; + height: 240px; object-fit: cover; -} - -.text { - grid-column: 1 / 3; + border-radius: var(--Corner-radius-Medium); } .buttonContainer { diff --git a/components/ContentType/HotelPage/SidePeeks/index.ts b/components/ContentType/HotelPage/SidePeeks/index.ts index e7233e010..f8de0f861 100644 --- a/components/ContentType/HotelPage/SidePeeks/index.ts +++ b/components/ContentType/HotelPage/SidePeeks/index.ts @@ -1,5 +1,6 @@ export { default as AboutTheHotelSidePeek } from "./AboutTheHotel" export { default as ActivitiesSidePeek } from "./Activities" export { default as AmenitiesSidePeek } from "./Amenities" +export { default as MeetingsAndConferencesSidePeek } from "./MeetingsAndConferences" export { default as RoomSidePeek } from "./Room" export { default as WellnessAndExerciseSidePeek } from "./WellnessAndExercise" diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index a9125edf6..38002af81 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -1,9 +1,6 @@ import { notFound } from "next/navigation" -import { - meetingsAndConferences, - restaurantAndBar, -} from "@/constants/routes/hotelPageParams" +import { restaurantAndBar } from "@/constants/routes/hotelPageParams" import { env } from "@/env/server" import { getHotelData, getHotelPage } from "@/lib/trpc/memoizedRequests" @@ -30,6 +27,7 @@ import { AboutTheHotelSidePeek, ActivitiesSidePeek, AmenitiesSidePeek, + MeetingsAndConferencesSidePeek, RoomSidePeek, WellnessAndExerciseSidePeek, } from "./SidePeeks" @@ -203,13 +201,10 @@ export default async function HotelPage({ hotelId }: HotelPageProps) { {activitiesCard && ( )} - - {/* TODO */} - Meetings & Conferences - + {roomCategories.map((room) => ( ))} diff --git a/types/components/hotelPage/sidepeek/meetingsAndConferences.ts b/types/components/hotelPage/sidepeek/meetingsAndConferences.ts index abbfc8214..50dc76da0 100644 --- a/types/components/hotelPage/sidepeek/meetingsAndConferences.ts +++ b/types/components/hotelPage/sidepeek/meetingsAndConferences.ts @@ -2,4 +2,6 @@ import type { Hotel } from "@/types/hotel" export type MeetingsAndConferencesSidePeekProps = { meetingFacilities: Hotel["conferencesAndMeetings"] + descriptions: Hotel["hotelContent"]["texts"]["meetingDescription"] + link?: string } From 7e93d59842c2ceb8fda2355bfae605f9361e6742 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Mon, 2 Dec 2024 20:38:49 +0100 Subject: [PATCH 33/37] feat(SW-936): add secondary image --- .../MeetingsAndConferences/index.tsx | 51 +++++++++++-------- .../meetingsAndConferences.module.css | 6 +++ 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/components/ContentType/HotelPage/SidePeeks/MeetingsAndConferences/index.tsx b/components/ContentType/HotelPage/SidePeeks/MeetingsAndConferences/index.tsx index 21ca2b336..ed9a96f18 100644 --- a/components/ContentType/HotelPage/SidePeeks/MeetingsAndConferences/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/MeetingsAndConferences/index.tsx @@ -21,9 +21,15 @@ export default async function MeetingsAndConferencesSidePeek({ }: MeetingsAndConferencesSidePeekProps) { const lang = getLang() const intl = await getIntl() - const mainImage = meetingFacilities?.heroImages[0].imageSizes.medium - const altText = - meetingFacilities?.heroImages[0].metaData.altText || + + const primaryImage = meetingFacilities?.heroImages[0]?.imageSizes.medium + const primaryAltText = + meetingFacilities?.heroImages[0]?.metaData.altText || + intl.formatMessage({ id: "Creative spaces for meetings" }) + + const secondaryImage = meetingFacilities?.heroImages[1]?.imageSizes.medium + const secondaryAltText = + meetingFacilities?.heroImages[1]?.metaData.altText || intl.formatMessage({ id: "Creative spaces for meetings" }) return ( @@ -37,24 +43,29 @@ export default async function MeetingsAndConferencesSidePeek({ {intl.formatMessage({ id: "Creative spaces for meetings" })} - {mainImage && ( - {altText} + {primaryImage && ( +
    + {primaryAltText} + {secondaryImage && ( + {secondaryAltText} + )} +
    + )} + {descriptions?.medium && ( + {descriptions.medium} )} - {descriptions.medium} -
    - - [Min to Max capacity square meter info] - - - [Min to Max capacity persons info] - -
    {link && (
    diff --git a/components/ContentType/HotelPage/SidePeeks/MeetingsAndConferences/meetingsAndConferences.module.css b/components/ContentType/HotelPage/SidePeeks/MeetingsAndConferences/meetingsAndConferences.module.css index a8cf8b7aa..ab19b9748 100644 --- a/components/ContentType/HotelPage/SidePeeks/MeetingsAndConferences/meetingsAndConferences.module.css +++ b/components/ContentType/HotelPage/SidePeeks/MeetingsAndConferences/meetingsAndConferences.module.css @@ -6,19 +6,17 @@ ); /* Creates space between the wrapper and buttonContainer */ } -.imageContainer { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--Spacing-x2); -} - .image { width: 100%; - height: 240px; + height: 300px; object-fit: cover; border-radius: var(--Corner-radius-Medium); } +.secondaryImage { + display: none; +} + .buttonContainer { background-color: var(--Base-Background-Primary-Normal); border-top: 1px solid var(--Base-Border-Subtle); @@ -28,3 +26,19 @@ left: 0; bottom: 0; } + +@media screen and (min-width: 768px) { + .imageContainer { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--Spacing-x2); + } + + .image { + height: 240px; + } + + .secondaryImage { + display: block; + } +} From 248dc4df19dae61de6eeaa401c09c0cc2dd366be Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Tue, 3 Dec 2024 23:03:23 +0100 Subject: [PATCH 36/37] feat(SW-936): remove function --- .../SidePeeks/MeetingsAndConferences/index.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/components/ContentType/HotelPage/SidePeeks/MeetingsAndConferences/index.tsx b/components/ContentType/HotelPage/SidePeeks/MeetingsAndConferences/index.tsx index 82fa7a023..f1a1c5101 100644 --- a/components/ContentType/HotelPage/SidePeeks/MeetingsAndConferences/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/MeetingsAndConferences/index.tsx @@ -23,16 +23,13 @@ export default async function MeetingsAndConferencesSidePeek({ const intl = await getIntl() const fallbackAlt = intl.formatMessage({ id: "Creative spaces for meetings" }) - function getImage(index: number) { - const list = meetingFacilities?.heroImages[index] - return { - image: list?.imageSizes.medium, - altText: list?.metaData.altText || fallbackAlt, - } - } + const primaryImage = meetingFacilities?.heroImages[0]?.imageSizes.medium + const primaryAltText = + meetingFacilities?.heroImages[0]?.metaData.altText || fallbackAlt - const { image: primaryImage, altText: primaryAltText } = getImage(0) - const { image: secondaryImage, altText: secondaryAltText } = getImage(1) + const secondaryImage = meetingFacilities?.heroImages[1]?.imageSizes.medium + const secondaryAltText = + meetingFacilities?.heroImages[1]?.metaData.altText || fallbackAlt return ( Date: Tue, 3 Dec 2024 13:25:38 +0100 Subject: [PATCH 37/37] feat(SW-866): download invoice --- .../booking-confirmation/page.tsx | 43 +++----------- .../Header/Actions/DownloadInvoice.tsx | 8 ++- .../BookingConfirmation/Header/index.tsx | 19 ++++--- .../HotelDetails/index.tsx | 15 +++-- .../PaymentDetails/index.tsx | 20 +++---- .../BookingConfirmation/Promos/index.tsx | 7 ++- .../BookingConfirmation/Receipt/index.tsx | 29 +++++----- .../BookingConfirmation/Rooms/Room/index.tsx | 14 +++-- .../BookingConfirmation/Rooms/index.tsx | 25 +++----- .../confirmation.module.css | 2 +- .../BookingConfirmation/index.tsx | 57 +++++++++++++++++++ package-lock.json | 9 +++ package.json | 1 + server/routers/booking/query.ts | 2 + server/routers/booking/utils.ts | 27 +++++++++ .../actions/downloadInvoice.ts | 5 ++ .../bookingConfirmation.ts | 4 +- .../bookingConfirmation/header.ts | 8 +++ .../bookingConfirmation/hotelDetails.ts | 5 ++ .../bookingConfirmation/paymentDetails.ts | 4 ++ .../bookingConfirmation/receipt.ts | 3 + .../bookingConfirmation/room.ts | 11 ---- .../bookingConfirmation/rooms.ts | 4 ++ .../bookingConfirmation/rooms/room.ts | 7 +++ types/trpc/routers/booking/confirmation.ts | 19 +++++++ utils/getBookedHotelRoom.ts | 23 -------- 26 files changed, 229 insertions(+), 142 deletions(-) rename app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.module.css => components/HotelReservation/BookingConfirmation/confirmation.module.css (99%) create mode 100644 components/HotelReservation/BookingConfirmation/index.tsx create mode 100644 server/routers/booking/utils.ts create mode 100644 types/components/hotelReservation/bookingConfirmation/actions/downloadInvoice.ts create mode 100644 types/components/hotelReservation/bookingConfirmation/header.ts create mode 100644 types/components/hotelReservation/bookingConfirmation/hotelDetails.ts create mode 100644 types/components/hotelReservation/bookingConfirmation/paymentDetails.ts create mode 100644 types/components/hotelReservation/bookingConfirmation/receipt.ts delete mode 100644 types/components/hotelReservation/bookingConfirmation/room.ts create mode 100644 types/components/hotelReservation/bookingConfirmation/rooms.ts create mode 100644 types/components/hotelReservation/bookingConfirmation/rooms/room.ts create mode 100644 types/trpc/routers/booking/confirmation.ts delete mode 100644 utils/getBookedHotelRoom.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx index ea32a277c..f7cc836ea 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx @@ -1,20 +1,8 @@ -import { Suspense } from "react" - import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests" -import Header from "@/components/HotelReservation/BookingConfirmation/Header" -import HotelDetails from "@/components/HotelReservation/BookingConfirmation/HotelDetails" -import PaymentDetails from "@/components/HotelReservation/BookingConfirmation/PaymentDetails" -import Promos from "@/components/HotelReservation/BookingConfirmation/Promos" -import Receipt from "@/components/HotelReservation/BookingConfirmation/Receipt" -import Rooms from "@/components/HotelReservation/BookingConfirmation/Rooms" -import SidePanel from "@/components/HotelReservation/SidePanel" -import LoadingSpinner from "@/components/LoadingSpinner" -import Divider from "@/components/TempDesignSystem/Divider" +import BookingConfirmation from "@/components/HotelReservation/BookingConfirmation" import { setLang } from "@/i18n/serverContext" -import styles from "./page.module.css" - import type { LangParams, PageArgs } from "@/types/params" export default async function BookingConfirmationPage({ @@ -22,29 +10,12 @@ export default async function BookingConfirmationPage({ searchParams, }: PageArgs) { setLang(params.lang) - void getBookingConfirmation(searchParams.confirmationNumber) + const bookingConfirmationPromise = getBookingConfirmation( + searchParams.confirmationNumber + ) return ( -
    - }> -
    -
    - - - - - -
    - -
    -
    - - -
    + ) } diff --git a/components/HotelReservation/BookingConfirmation/Header/Actions/DownloadInvoice.tsx b/components/HotelReservation/BookingConfirmation/Header/Actions/DownloadInvoice.tsx index 3dea1f011..9fe883683 100644 --- a/components/HotelReservation/BookingConfirmation/Header/Actions/DownloadInvoice.tsx +++ b/components/HotelReservation/BookingConfirmation/Header/Actions/DownloadInvoice.tsx @@ -1,14 +1,18 @@ "use client" import { useIntl } from "react-intl" +import { useReactToPrint } from "react-to-print" import { DownloadIcon } from "@/components/Icons" import Button from "@/components/TempDesignSystem/Button" -export default function DownloadInvoice() { +import type { DownloadInvoiceProps } from "@/types/components/hotelReservation/bookingConfirmation/actions/downloadInvoice" + +export default function DownloadInvoice({ mainRef }: DownloadInvoiceProps) { const intl = useIntl() + const reactToPrintFn = useReactToPrint({ contentRef: mainRef }) function downloadBooking() { - window.print() + reactToPrintFn() } return ( diff --git a/components/HotelReservation/BookingConfirmation/Header/index.tsx b/components/HotelReservation/BookingConfirmation/Header/index.tsx index 6cb144202..0d2d08f8b 100644 --- a/components/HotelReservation/BookingConfirmation/Header/index.tsx +++ b/components/HotelReservation/BookingConfirmation/Header/index.tsx @@ -1,9 +1,9 @@ -import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests" +"use client" +import { useIntl } from "react-intl" import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Title from "@/components/TempDesignSystem/Text/Title" -import { getIntl } from "@/i18n" import AddToCalendar from "./Actions/AddToCalendar" import DownloadInvoice from "./Actions/DownloadInvoice" @@ -14,13 +14,14 @@ import styles from "./header.module.css" import type { EventAttributes } from "ics" -import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" +import type { BookingConfirmationHeaderProps } from "@/types/components/hotelReservation/bookingConfirmation/header" -export default async function Header({ - confirmationNumber, -}: BookingConfirmationProps) { - const intl = await getIntl() - const { booking, hotel } = await getBookingConfirmation(confirmationNumber) +export default function Header({ + booking, + hotel, + mainRef, +}: BookingConfirmationHeaderProps) { + const intl = useIntl() const text = intl.formatMessage( { id: "booking.confirmation.text" }, @@ -70,7 +71,7 @@ export default async function Header({ hotelName={hotel.name} /> - +
    ) diff --git a/components/HotelReservation/BookingConfirmation/HotelDetails/index.tsx b/components/HotelReservation/BookingConfirmation/HotelDetails/index.tsx index dcef584a5..0c1b1dd06 100644 --- a/components/HotelReservation/BookingConfirmation/HotelDetails/index.tsx +++ b/components/HotelReservation/BookingConfirmation/HotelDetails/index.tsx @@ -1,20 +1,19 @@ -import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests" +"use client" +import { useIntl } from "react-intl" import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import { Toast } from "@/components/TempDesignSystem/Toasts" -import { getIntl } from "@/i18n" import styles from "./hotelDetails.module.css" -import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" +import type { BookingConfirmationHotelDetailsProps } from "@/types/components/hotelReservation/bookingConfirmation/hotelDetails" -export default async function HotelDetails({ - confirmationNumber, -}: BookingConfirmationProps) { - const intl = await getIntl() - const { hotel } = await getBookingConfirmation(confirmationNumber) +export default function HotelDetails({ + hotel, +}: BookingConfirmationHotelDetailsProps) { + const intl = useIntl() return (
    diff --git a/components/HotelReservation/BookingConfirmation/PaymentDetails/index.tsx b/components/HotelReservation/BookingConfirmation/PaymentDetails/index.tsx index 67f327455..500c7ed74 100644 --- a/components/HotelReservation/BookingConfirmation/PaymentDetails/index.tsx +++ b/components/HotelReservation/BookingConfirmation/PaymentDetails/index.tsx @@ -1,23 +1,23 @@ +"use client" +import { useIntl } from "react-intl" + import { dt } from "@/lib/dt" -import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests" import { CreditCardAddIcon } from "@/components/Icons" import Button from "@/components/TempDesignSystem/Button" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import { getIntl } from "@/i18n" -import { getLang } from "@/i18n/serverContext" +import useLang from "@/hooks/useLang" import styles from "./paymentDetails.module.css" -import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" +import type { BookingConfirmationPaymentDetailsProps } from "@/types/components/hotelReservation/bookingConfirmation/paymentDetails" -export default async function PaymentDetails({ - confirmationNumber, -}: BookingConfirmationProps) { - const intl = await getIntl() - const lang = getLang() - const { booking } = await getBookingConfirmation(confirmationNumber) +export default function PaymentDetails({ + booking, +}: BookingConfirmationPaymentDetailsProps) { + const intl = useIntl() + const lang = useLang() return (
    diff --git a/components/HotelReservation/BookingConfirmation/Promos/index.tsx b/components/HotelReservation/BookingConfirmation/Promos/index.tsx index d0692f917..4cf6d9076 100644 --- a/components/HotelReservation/BookingConfirmation/Promos/index.tsx +++ b/components/HotelReservation/BookingConfirmation/Promos/index.tsx @@ -1,11 +1,12 @@ -import { getIntl } from "@/i18n" +"use client" +import { useIntl } from "react-intl" import Promo from "./Promo" import styles from "./promos.module.css" -export default async function Promos() { - const intl = await getIntl() +export default function Promos() { + const intl = useIntl() return (
    {intl.formatMessage({ id: "Summary" })}
    - {roomAndBed.name} + {room.name} {booking.rateDefinition.isMemberRate ? (
    @@ -82,9 +81,7 @@ export default async function Receipt({
    - - {roomAndBed.bedType.description} - + {room.bedType.description} {intl.formatNumber(0, { currency: booking.currencyCode, diff --git a/components/HotelReservation/BookingConfirmation/Rooms/Room/index.tsx b/components/HotelReservation/BookingConfirmation/Rooms/Room/index.tsx index e721c3d8c..3d81e9511 100644 --- a/components/HotelReservation/BookingConfirmation/Rooms/Room/index.tsx +++ b/components/HotelReservation/BookingConfirmation/Rooms/Room/index.tsx @@ -1,3 +1,6 @@ +"use client" +import { useIntl } from "react-intl" + import { dt } from "@/lib/dt" import { @@ -10,16 +13,15 @@ import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import { getIntl } from "@/i18n" -import { getLang } from "@/i18n/serverContext" +import useLang from "@/hooks/useLang" import styles from "./room.module.css" -import type { RoomProps } from "@/types/components/hotelReservation/bookingConfirmation/room" +import type { RoomProps } from "@/types/components/hotelReservation/bookingConfirmation/rooms/room" -export default async function Room({ booking, img, roomName }: RoomProps) { - const intl = await getIntl() - const lang = getLang() +export default function Room({ booking, img, roomName }: RoomProps) { + const intl = useIntl() + const lang = useLang() const fromDate = dt(booking.checkInDate).locale(lang) const toDate = dt(booking.checkOutDate).locale(lang) diff --git a/components/HotelReservation/BookingConfirmation/Rooms/index.tsx b/components/HotelReservation/BookingConfirmation/Rooms/index.tsx index fdc973971..19954aec7 100644 --- a/components/HotelReservation/BookingConfirmation/Rooms/index.tsx +++ b/components/HotelReservation/BookingConfirmation/Rooms/index.tsx @@ -1,30 +1,23 @@ +"use client" + import { notFound } from "next/navigation" -import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests" - -import { getBookedHotelRoom } from "@/utils/getBookedHotelRoom" - import Room from "./Room" import styles from "./rooms.module.css" -import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" +import type { BookingConfirmationRoomsProps } from "@/types/components/hotelReservation/bookingConfirmation/rooms" -export default async function Rooms({ - confirmationNumber, -}: BookingConfirmationProps) { - const { booking, hotel } = await getBookingConfirmation(confirmationNumber) - const roomAndBed = getBookedHotelRoom(hotel, booking.roomTypeCode ?? "") - if (!roomAndBed) { +export default function Rooms({ + booking, + room, +}: BookingConfirmationRoomsProps) { + if (!room) { return notFound() } return (
    - +
    ) } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.module.css b/components/HotelReservation/BookingConfirmation/confirmation.module.css similarity index 99% rename from app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.module.css rename to components/HotelReservation/BookingConfirmation/confirmation.module.css index 341c3b979..bba79b2d0 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.module.css +++ b/components/HotelReservation/BookingConfirmation/confirmation.module.css @@ -39,4 +39,4 @@ display: grid; grid-area: receipt; } -} \ No newline at end of file +} diff --git a/components/HotelReservation/BookingConfirmation/index.tsx b/components/HotelReservation/BookingConfirmation/index.tsx new file mode 100644 index 000000000..c97da8908 --- /dev/null +++ b/components/HotelReservation/BookingConfirmation/index.tsx @@ -0,0 +1,57 @@ +"use client" +import { use, useRef } from "react" + +import Header from "@/components/HotelReservation/BookingConfirmation/Header" +import HotelDetails from "@/components/HotelReservation/BookingConfirmation/HotelDetails" +import PaymentDetails from "@/components/HotelReservation/BookingConfirmation/PaymentDetails" +import Promos from "@/components/HotelReservation/BookingConfirmation/Promos" +import Receipt from "@/components/HotelReservation/BookingConfirmation/Receipt" +import Rooms from "@/components/HotelReservation/BookingConfirmation/Rooms" +import SidePanel from "@/components/HotelReservation/SidePanel" +import Divider from "@/components/TempDesignSystem/Divider" + +import styles from "./confirmation.module.css" + +import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" + +export default function BookingConfirmation({ + bookingConfirmationPromise, +}: BookingConfirmationProps) { + const bookingConfirmation = use(bookingConfirmationPromise) + const mainRef = useRef(null) + return ( +
    +
    +
    + + + + + +
    + +
    +
    + +
    + ) +} diff --git a/package-lock.json b/package-lock.json index d42a1d4e8..8fd09c24b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,7 @@ "react-hook-form": "^7.51.2", "react-international-phone": "^4.2.6", "react-intl": "^6.6.8", + "react-to-print": "^3.0.2", "server-only": "^0.0.1", "sonner": "^1.7.0", "superjson": "^2.2.1", @@ -17417,6 +17418,14 @@ } } }, + "node_modules/react-to-print": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/react-to-print/-/react-to-print-3.0.2.tgz", + "integrity": "sha512-FS/Z4LLq0bgWaxd7obygFQ8yRFdKW74iE8fIVjFFsPJWIXmuL8CIO+4me1Hj44lrlxQ00gscSNb3BRM8olbwXg==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ~19" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", diff --git a/package.json b/package.json index 6f6f1f210..7e8af1b1e 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "react-hook-form": "^7.51.2", "react-international-phone": "^4.2.6", "react-intl": "^6.6.8", + "react-to-print": "^3.0.2", "server-only": "^0.0.1", "sonner": "^1.7.0", "superjson": "^2.2.1", diff --git a/server/routers/booking/query.ts b/server/routers/booking/query.ts index 0780549bb..3e7d1f216 100644 --- a/server/routers/booking/query.ts +++ b/server/routers/booking/query.ts @@ -8,6 +8,7 @@ import { router, serviceProcedure } from "@/server/trpc" import { getHotelData } from "../hotels/query" import { bookingConfirmationInput, getBookingStatusInput } from "./input" import { bookingConfirmationSchema, createBookingSchema } from "./output" +import { getBookedHotelRoom } from "./utils" const meter = metrics.getMeter("trpc.booking") const getBookingConfirmationCounter = meter.createCounter( @@ -144,6 +145,7 @@ export const bookingQueryRouter = router({ ...hotelData.data.attributes, included: hotelData.included, }, + room: getBookedHotelRoom(hotelData.included, booking.data.roomTypeCode), } }), status: serviceProcedure.input(getBookingStatusInput).query(async function ({ diff --git a/server/routers/booking/utils.ts b/server/routers/booking/utils.ts new file mode 100644 index 000000000..f4e5ad037 --- /dev/null +++ b/server/routers/booking/utils.ts @@ -0,0 +1,27 @@ +import { RoomData } from "@/types/hotel" +import { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" + +export function getBookedHotelRoom( + rooms: RoomData[] | undefined, + roomTypeCode: BookingConfirmation["booking"]["roomTypeCode"] +) { + if (!rooms?.length || !roomTypeCode) { + return null + } + const room = rooms?.find((r) => { + return r.roomTypes.find((roomType) => roomType.code === roomTypeCode) + }) + if (!room) { + return null + } + const bedType = room.roomTypes.find( + (roomType) => roomType.code === roomTypeCode + ) + if (!bedType) { + return null + } + return { + ...room, + bedType, + } +} diff --git a/types/components/hotelReservation/bookingConfirmation/actions/downloadInvoice.ts b/types/components/hotelReservation/bookingConfirmation/actions/downloadInvoice.ts new file mode 100644 index 000000000..6d4570893 --- /dev/null +++ b/types/components/hotelReservation/bookingConfirmation/actions/downloadInvoice.ts @@ -0,0 +1,5 @@ +import type { MutableRefObject } from "react" + +export interface DownloadInvoiceProps { + mainRef: MutableRefObject +} diff --git a/types/components/hotelReservation/bookingConfirmation/bookingConfirmation.ts b/types/components/hotelReservation/bookingConfirmation/bookingConfirmation.ts index aa178efe5..32eaf9965 100644 --- a/types/components/hotelReservation/bookingConfirmation/bookingConfirmation.ts +++ b/types/components/hotelReservation/bookingConfirmation/bookingConfirmation.ts @@ -1,3 +1,5 @@ +import type { RouterOutput } from "@/lib/trpc/client" + export interface BookingConfirmationProps { - confirmationNumber: string + bookingConfirmationPromise: Promise } diff --git a/types/components/hotelReservation/bookingConfirmation/header.ts b/types/components/hotelReservation/bookingConfirmation/header.ts new file mode 100644 index 000000000..629bcf4f1 --- /dev/null +++ b/types/components/hotelReservation/bookingConfirmation/header.ts @@ -0,0 +1,8 @@ +import type { MutableRefObject } from "react" + +import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" + +export interface BookingConfirmationHeaderProps + extends Pick { + mainRef: MutableRefObject +} diff --git a/types/components/hotelReservation/bookingConfirmation/hotelDetails.ts b/types/components/hotelReservation/bookingConfirmation/hotelDetails.ts new file mode 100644 index 000000000..f121e0aba --- /dev/null +++ b/types/components/hotelReservation/bookingConfirmation/hotelDetails.ts @@ -0,0 +1,5 @@ +import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" + +export interface BookingConfirmationHotelDetailsProps { + hotel: BookingConfirmation["hotel"] +} diff --git a/types/components/hotelReservation/bookingConfirmation/paymentDetails.ts b/types/components/hotelReservation/bookingConfirmation/paymentDetails.ts new file mode 100644 index 000000000..c85b10608 --- /dev/null +++ b/types/components/hotelReservation/bookingConfirmation/paymentDetails.ts @@ -0,0 +1,4 @@ +import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" + +export interface BookingConfirmationPaymentDetailsProps + extends Pick {} diff --git a/types/components/hotelReservation/bookingConfirmation/receipt.ts b/types/components/hotelReservation/bookingConfirmation/receipt.ts new file mode 100644 index 000000000..04824d8df --- /dev/null +++ b/types/components/hotelReservation/bookingConfirmation/receipt.ts @@ -0,0 +1,3 @@ +import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" + +export interface BookingConfirmationReceiptProps extends BookingConfirmation {} diff --git a/types/components/hotelReservation/bookingConfirmation/room.ts b/types/components/hotelReservation/bookingConfirmation/room.ts deleted file mode 100644 index 5f379c4ec..000000000 --- a/types/components/hotelReservation/bookingConfirmation/room.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { RouterOutput } from "@/lib/trpc/client" - -export interface RoomProps { - booking: RouterOutput["booking"]["confirmation"]["booking"] - img: NonNullable< - RouterOutput["booking"]["confirmation"]["hotel"]["included"] - >[number]["images"][number] - roomName: NonNullable< - RouterOutput["booking"]["confirmation"]["hotel"]["included"] - >[number]["name"] -} diff --git a/types/components/hotelReservation/bookingConfirmation/rooms.ts b/types/components/hotelReservation/bookingConfirmation/rooms.ts new file mode 100644 index 000000000..473bd18be --- /dev/null +++ b/types/components/hotelReservation/bookingConfirmation/rooms.ts @@ -0,0 +1,4 @@ +import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" + +export interface BookingConfirmationRoomsProps + extends Pick {} diff --git a/types/components/hotelReservation/bookingConfirmation/rooms/room.ts b/types/components/hotelReservation/bookingConfirmation/rooms/room.ts new file mode 100644 index 000000000..f6ccb96d6 --- /dev/null +++ b/types/components/hotelReservation/bookingConfirmation/rooms/room.ts @@ -0,0 +1,7 @@ +import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" + +export interface RoomProps { + booking: BookingConfirmation["booking"] + img: NonNullable["images"][number] + roomName: NonNullable["name"] +} diff --git a/types/trpc/routers/booking/confirmation.ts b/types/trpc/routers/booking/confirmation.ts new file mode 100644 index 000000000..7e0b580ce --- /dev/null +++ b/types/trpc/routers/booking/confirmation.ts @@ -0,0 +1,19 @@ +import { z } from "zod" + +import { bookingConfirmationSchema } from "@/server/routers/booking/output" + +import { Hotel, RoomData } from "@/types/hotel" + +export interface BookingConfirmationSchema + extends z.output {} +export interface BookingConfirmation { + booking: BookingConfirmationSchema + hotel: Hotel & { + included?: RoomData[] + } + room: + | (RoomData & { + bedType: RoomData["roomTypes"][number] + }) + | null +} diff --git a/utils/getBookedHotelRoom.ts b/utils/getBookedHotelRoom.ts deleted file mode 100644 index 45b5d65b8..000000000 --- a/utils/getBookedHotelRoom.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { RouterOutput } from "@/lib/trpc/client" - -export function getBookedHotelRoom( - hotel: RouterOutput["booking"]["confirmation"]["hotel"], - roomTypeCode: string -) { - const room = hotel.included?.find((include) => { - return include.roomTypes.find((roomType) => roomType.code === roomTypeCode) - }) - if (!room) { - return null - } - const bedType = room.roomTypes.find( - (roomType) => roomType.code === roomTypeCode - ) - if (!bedType) { - return null - } - return { - ...room, - bedType, - } -}