From a8ad0a13a283b8dfc5646b7de60796f14cb2799d Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Wed, 30 Oct 2024 13:25:39 +0100 Subject: [PATCH 001/120] WIP --- components/Image.tsx | 6 ++++++ next.config.js | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/components/Image.tsx b/components/Image.tsx index 419259fc3..a5ea6e16a 100644 --- a/components/Image.tsx +++ b/components/Image.tsx @@ -8,7 +8,13 @@ import type { CSSProperties } from "react" import type { ImageProps } from "@/types/components/image" function imageLoader({ quality, src, width }: ImageLoaderProps) { + const isAbsoluteUrl = src.startsWith("https://") || src.startsWith("http://") const hasQS = src.indexOf("?") !== -1 + + if (isAbsoluteUrl) { + return `https://image-scandic-hotels.netlify.app/.netlify/images?url=${src}&w=${width}${quality ? "&q=" + quality : ""}` + } + return `${src}${hasQS ? "&" : "?"}w=${width}${quality ? "&q=" + quality : ""}` } diff --git a/next.config.js b/next.config.js index f91af6594..ba16f5da6 100644 --- a/next.config.js +++ b/next.config.js @@ -43,6 +43,10 @@ const nextConfig = { protocol: "https", hostname: "*.scandichotels.com", }, + { + protocol: "https", + hostname: "image-scandic-hotels.netlify.app", + }, ], }, From b5dce01fd3deb7f8336edeface595eebb128bd7b Mon Sep 17 00:00:00 2001 From: Bianca Widstam Date: Wed, 30 Oct 2024 14:31:18 +0000 Subject: [PATCH 002/120] fix/SW-729-enter-details-page-error (pull request #796) feat(SW-729): fix availability params * feat(SW-729): fix availability params * feat(SW-729): use paramsObject Approved-by: Tobias Johansson --- .../(standard)/[step]/page.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 264f5b04d..9a673c26c 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -14,6 +14,7 @@ import Details from "@/components/HotelReservation/EnterDetails/Details" import HistoryStateManager from "@/components/HotelReservation/EnterDetails/HistoryStateManager" import Payment from "@/components/HotelReservation/EnterDetails/Payment" import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion" +import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { getIntl } from "@/i18n" import { StepEnum } from "@/types/components/enterDetails/step" @@ -36,13 +37,14 @@ export default async function StepPage({ redirect(`/${params.lang}`) } void getBreakfastPackages(searchParams.hotel) + const stepParams = new URLSearchParams(searchParams) + const paramsObject = getHotelReservationQueryParams(stepParams) void getRoomAvailability({ - hotelId: searchParams.hotel, - adults: Number(searchParams.adults), - roomStayStartDate: searchParams.checkIn, - roomStayEndDate: searchParams.checkOut, + hotelId: paramsObject.hotel, + adults: paramsObject.room[0].adults, + roomStayStartDate: paramsObject.fromDate, + roomStayEndDate: paramsObject.toDate, }) - const intl = await getIntl() const hotel = await getHotelData(searchParams.hotel, params.lang) @@ -51,11 +53,11 @@ export default async function StepPage({ const breakfastPackages = await getBreakfastPackages(searchParams.hotel) const roomAvailability = await getRoomAvailability({ - hotelId: searchParams.hotel, - adults: Number(searchParams.adults), - roomStayStartDate: searchParams.checkIn, - roomStayEndDate: searchParams.checkOut, - rateCode: searchParams.rateCode, + hotelId: paramsObject.hotel, + adults: paramsObject.room[0].adults, + roomStayStartDate: paramsObject.fromDate, + roomStayEndDate: paramsObject.toDate, + rateCode: paramsObject.room[0].ratecode, }) if (!isValidStep(params.step) || !hotel || !roomAvailability) { From 85fdefb5ace6262c10cdad6d51cd21af13fbcb72 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Tue, 22 Oct 2024 16:19:15 +0200 Subject: [PATCH 003/120] feat: save search params from select-rate to store --- .../(standard)/[step]/layout.tsx | 9 +- .../(standard)/[step]/page.tsx | 2 +- .../Overview/Stats/ExpiringPoints/index.tsx | 6 +- .../Points/EarnAndBurn/AwardPoints/index.tsx | 7 +- .../Footer/Navigation/SecondaryNav/index.tsx | 4 +- .../EnterDetails/BedType/index.tsx | 9 +- .../EnterDetails/Breakfast/index.tsx | 32 +- .../EnterDetails/Details/index.tsx | 20 +- .../EnterDetails/Provider/index.tsx | 6 +- .../EnterDetails/SidePeek/index.tsx | 2 +- .../EnterDetails/Summary/ToggleSidePeek.tsx | 2 +- .../EnterDetails/Summary/index.tsx | 284 +++++++++++------- .../EnterDetails/Summary/summary.module.css | 2 +- .../Text/Body/body.module.css | 2 +- .../TempDesignSystem/Text/Body/variants.ts | 2 +- i18n/dictionaries/da.json | 2 +- i18n/dictionaries/de.json | 2 +- i18n/dictionaries/en.json | 4 +- i18n/dictionaries/fi.json | 2 +- stores/enter-details.ts | 91 ++++-- .../enterDetails/bedType.ts | 0 .../enterDetails/bookingData.ts | 27 ++ .../enterDetails/breakfast.ts | 0 .../enterDetails/details.ts | 0 .../enterDetails/sidePeek.ts | 0 .../enterDetails/step.ts | 0 .../selectRate/sectionAccordion.ts | 2 +- utils/format.ts | 8 + 28 files changed, 332 insertions(+), 195 deletions(-) rename types/components/{ => hotelReservation}/enterDetails/bedType.ts (100%) create mode 100644 types/components/hotelReservation/enterDetails/bookingData.ts rename types/components/{ => hotelReservation}/enterDetails/breakfast.ts (100%) rename types/components/{ => hotelReservation}/enterDetails/details.ts (100%) rename types/components/{ => hotelReservation}/enterDetails/sidePeek.ts (100%) rename types/components/{ => hotelReservation}/enterDetails/step.ts (100%) create mode 100644 utils/format.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx index 271d19e6d..28a2bc570 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx @@ -7,7 +7,7 @@ import { preload } from "./page" import styles from "./layout.module.css" -import { StepEnum } from "@/types/components/enterDetails/step" +import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step" import type { LangParams, LayoutArgs } from "@/types/params" export default async function StepLayout({ @@ -19,19 +19,18 @@ export default async function StepLayout({ LayoutArgs & { hotelHeader: React.ReactNode sidePeek: React.ReactNode - } ->) { + }>) { setLang(params.lang) preload() return ( - +
{hotelHeader}
{children}
{sidePeek} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 9a673c26c..a8bb4b4e9 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -17,7 +17,7 @@ import SectionAccordion from "@/components/HotelReservation/EnterDetails/Section import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { getIntl } from "@/i18n" -import { StepEnum } from "@/types/components/enterDetails/step" +import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step" import type { LangParams, PageArgs } from "@/types/params" export function preload() { diff --git a/components/Blocks/DynamicContent/Overview/Stats/ExpiringPoints/index.tsx b/components/Blocks/DynamicContent/Overview/Stats/ExpiringPoints/index.tsx index e64ddb4ef..fa21a8c83 100644 --- a/components/Blocks/DynamicContent/Overview/Stats/ExpiringPoints/index.tsx +++ b/components/Blocks/DynamicContent/Overview/Stats/ExpiringPoints/index.tsx @@ -4,6 +4,7 @@ import { dt } from "@/lib/dt" import Body from "@/components/TempDesignSystem/Text/Body" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" +import { formatNumber } from "@/utils/format" import { getMembership } from "@/utils/user" import type { UserProps } from "@/types/components/myPages/user" @@ -16,9 +17,6 @@ export default async function ExpiringPoints({ user }: UserProps) { // TODO: handle this case? return null } - - // sv hardcoded to force space on thousands - const formatter = new Intl.NumberFormat(Lang.sv) const d = dt(membership.pointsExpiryDate) const dateFormat = getLang() == Lang.fi ? "DD.MM.YYYY" : "YYYY-MM-DD" @@ -29,7 +27,7 @@ export default async function ExpiringPoints({ user }: UserProps) { {intl.formatMessage( { id: "spendable points expiring by" }, { - points: formatter.format(membership.pointsToExpire), + points: formatNumber(membership.pointsToExpire), date: d.format(dateFormat), } )} diff --git a/components/Blocks/DynamicContent/Points/EarnAndBurn/AwardPoints/index.tsx b/components/Blocks/DynamicContent/Points/EarnAndBurn/AwardPoints/index.tsx index 119324806..e32c3c5ca 100644 --- a/components/Blocks/DynamicContent/Points/EarnAndBurn/AwardPoints/index.tsx +++ b/components/Blocks/DynamicContent/Points/EarnAndBurn/AwardPoints/index.tsx @@ -1,8 +1,7 @@ import { useIntl } from "react-intl" -import { Lang } from "@/constants/languages" - import Body from "@/components/TempDesignSystem/Text/Body" +import { formatNumber } from "@/utils/format" import { awardPointsVariants } from "./awardPointsVariants" @@ -32,12 +31,10 @@ export default function AwardPoints({ variant, }) - // sv hardcoded to force space on thousands - const formatter = new Intl.NumberFormat(Lang.sv) return ( {isCalculated - ? formatter.format(awardPoints) + ? formatNumber(awardPoints) : intl.formatMessage({ id: "Points being calculated" })} ) diff --git a/components/Footer/Navigation/SecondaryNav/index.tsx b/components/Footer/Navigation/SecondaryNav/index.tsx index 42266f915..78d4f544c 100644 --- a/components/Footer/Navigation/SecondaryNav/index.tsx +++ b/components/Footer/Navigation/SecondaryNav/index.tsx @@ -18,7 +18,7 @@ export default function FooterSecondaryNav({
{appDownloads && (
diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index a8bb4b4e9..44de099bf 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -7,6 +7,7 @@ import { getProfileSafely, getRoomAvailability, } from "@/lib/trpc/memoizedRequests" +import { HotelIncludeEnum } from "@/server/routers/hotels/input" import BedType from "@/components/HotelReservation/EnterDetails/BedType" import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast" @@ -14,11 +15,12 @@ import Details from "@/components/HotelReservation/EnterDetails/Details" import HistoryStateManager from "@/components/HotelReservation/EnterDetails/HistoryStateManager" import Payment from "@/components/HotelReservation/EnterDetails/Payment" import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion" -import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" +import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { getIntl } from "@/i18n" import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step" -import type { LangParams, PageArgs } from "@/types/params" +import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" +import type { LangParams, PageArgs, } from "@/types/params" export function preload() { void getProfileSafely() @@ -32,35 +34,52 @@ function isValidStep(step: string): step is StepEnum { export default async function StepPage({ params, searchParams, -}: PageArgs) { +}: PageArgs< + LangParams & { step: StepEnum }, + SelectRateSearchParams +>) { if (!searchParams.hotel) { redirect(`/${params.lang}`) } void getBreakfastPackages(searchParams.hotel) - const stepParams = new URLSearchParams(searchParams) - const paramsObject = getHotelReservationQueryParams(stepParams) - void getRoomAvailability({ - hotelId: paramsObject.hotel, - adults: paramsObject.room[0].adults, - roomStayStartDate: paramsObject.fromDate, - roomStayEndDate: paramsObject.toDate, - }) - const intl = await getIntl() - const hotel = await getHotelData(searchParams.hotel, params.lang) + const intl = await getIntl() + const selectRoomParams = new URLSearchParams(searchParams) + const { + hotel: hotelId, + adults, + children, + roomTypeCode, + rateCode, + fromDate, + toDate, + } = getQueryParamsForEnterDetails(selectRoomParams) + + void getRoomAvailability({ + hotelId: parseInt(hotelId), + adults, + children, + roomStayStartDate: fromDate, + roomStayEndDate: toDate, + rateCode + }) + + const hotelData = await getHotelData(hotelId, params.lang, undefined, [HotelIncludeEnum.RoomCategories]) + const user = await getProfileSafely() const savedCreditCards = await getCreditCardsSafely() const breakfastPackages = await getBreakfastPackages(searchParams.hotel) const roomAvailability = await getRoomAvailability({ - hotelId: paramsObject.hotel, - adults: paramsObject.room[0].adults, - roomStayStartDate: paramsObject.fromDate, - roomStayEndDate: paramsObject.toDate, - rateCode: paramsObject.room[0].ratecode, + hotelId: parseInt(hotelId), + adults, + children, + roomStayStartDate: fromDate, + roomStayEndDate: toDate, + rateCode }) - if (!isValidStep(params.step) || !hotel || !roomAvailability) { + if (!isValidStep(params.step) || !hotelData || !roomAvailability) { return notFound() } @@ -79,16 +98,32 @@ export default async function StepPage({ id: "Select payment method", }) + const availableRoom = roomAvailability?.roomConfigurations + .filter((room) => room.status === "Available") + .find((room) => room.roomTypeCode === roomTypeCode)?.roomType + const roomTypes = hotelData.included + ?.find((room) => room.name === availableRoom) + ?.roomTypes.map((room) => ({ + description: room.mainBed.description, + size: room.mainBed.widthRange, + value: room.code, + })) + return (
- - - + + {/* TODO: How to handle no beds found? */} + {roomTypes ? ( + + + + ) : null} + -
+ ) } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx index 36fbd59c7..5fe20e0d9 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -10,7 +10,7 @@ import { } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils" import HotelCardListing from "@/components/HotelReservation/HotelCardListing" import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter" -import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" +import { getHotelReservationQueryParams } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { ChevronRightIcon } from "@/components/Icons" import StaticMap from "@/components/Maps/StaticMap" import Link from "@/components/TempDesignSystem/Link" diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index 35a940f0b..502a5833c 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -2,10 +2,11 @@ import { notFound } from "next/navigation" import { getProfileSafely } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" +import { HotelIncludeEnum } from "@/server/routers/hotels/input" import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard" import Rooms from "@/components/HotelReservation/SelectRate/Rooms" -import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" +import { getHotelReservationQueryParams } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { setLang } from "@/i18n/serverContext" import { generateChildrenString } from "../select-hotel/utils" @@ -38,7 +39,7 @@ export default async function SelectRatePage({ serverClient().hotel.hotelData.get({ hotelId: searchParams.hotel, language: params.lang, - include: ["RoomCategories"], + include: [HotelIncludeEnum.RoomCategories], }), serverClient().hotel.availability.rooms({ hotelId: parseInt(searchParams.hotel, 10), diff --git a/components/BookingWidget/Client.tsx b/components/BookingWidget/Client.tsx index 6a594199e..2acb8a397 100644 --- a/components/BookingWidget/Client.tsx +++ b/components/BookingWidget/Client.tsx @@ -11,7 +11,6 @@ import { CloseLargeIcon } from "@/components/Icons" import { debounce } from "@/utils/debounce" import { getFormattedUrlQueryParams } from "@/utils/url" -import getHotelReservationQueryParams from "../HotelReservation/SelectRate/RoomSelection/utils" import MobileToggleButton from "./MobileToggleButton" import styles from "./bookingWidget.module.css" @@ -41,10 +40,10 @@ export default function BookingWidgetClient({ const bookingWidgetSearchData: BookingWidgetSearchParams | undefined = searchParams ? (getFormattedUrlQueryParams(new URLSearchParams(searchParams), { - adults: "number", - age: "number", - bed: "number", - }) as BookingWidgetSearchParams) + adults: "number", + age: "number", + bed: "number", + }) as BookingWidgetSearchParams) : undefined const getLocationObj = (destination: string): Location | undefined => { @@ -70,9 +69,9 @@ export default function BookingWidgetClient({ const selectedLocation = bookingWidgetSearchData ? getLocationObj( - (bookingWidgetSearchData.city ?? - bookingWidgetSearchData.hotel) as string - ) + (bookingWidgetSearchData.city ?? + bookingWidgetSearchData.hotel) as string + ) : undefined const methods = useForm({ diff --git a/components/HotelReservation/EnterDetails/BedType/index.tsx b/components/HotelReservation/EnterDetails/BedType/index.tsx index 4811a17d5..27c95d963 100644 --- a/components/HotelReservation/EnterDetails/BedType/index.tsx +++ b/components/HotelReservation/EnterDetails/BedType/index.tsx @@ -16,7 +16,18 @@ import styles from "./bedOptions.module.css" import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" -export default function BedType() { +export default function BedType({ + roomTypes, +}: { + roomTypes: { + description: string + size: { + min: number + max: number + } + value: string + }[] +}) { const intl = useIntl() const bedType = useEnterDetailsStore((state) => state.userData.bedType) @@ -57,38 +68,25 @@ export default function BedType() { return (
- - + {roomTypes.map((roomType) => { + const width = + roomType.size.max === roomType.size.min + ? roomType.size.max + : `${roomType.size.min} cm - ${roomType.size.max} cm` + return ( + + ) + })}
) diff --git a/components/HotelReservation/EnterDetails/BedType/schema.ts b/components/HotelReservation/EnterDetails/BedType/schema.ts index 8f77ba768..5323f4e02 100644 --- a/components/HotelReservation/EnterDetails/BedType/schema.ts +++ b/components/HotelReservation/EnterDetails/BedType/schema.ts @@ -3,5 +3,5 @@ import { z } from "zod" import { BedTypeEnum } from "@/types/enums/bedType" export const bedTypeSchema = z.object({ - bedType: z.nativeEnum(BedTypeEnum), + bedType: z.string(), }) diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index e29692993..f4ea2b9f9 100644 --- a/components/HotelReservation/EnterDetails/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -56,7 +56,7 @@ export default function Payment({ const intl = useIntl() const queryParams = useSearchParams() const { firstName, lastName, email, phoneNumber, countryCode } = - useEnterDetailsStore((state) => state.data) + useEnterDetailsStore((state) => state.userData) const [confirmationNumber, setConfirmationNumber] = useState("") const methods = useForm({ diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index 363585848..c1bad51f4 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -1,52 +1,43 @@ "use client" +import { useEffect, useState } from "react" import { useIntl } from "react-intl" import { dt } from "@/lib/dt" -import { trpc } from "@/lib/trpc/client" import { useEnterDetailsStore } from "@/stores/enter-details" import { ArrowRightIcon } from "@/components/Icons" -import LoadingSpinner from "@/components/LoadingSpinner" 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 Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useLang from "@/hooks/useLang" -import { formatNumber } from "@/utils/format" import styles from "./summary.module.css" import { RoomsData } from "@/types/components/hotelReservation/enterDetails/bookingData" -export default function Summary({ isMember }: { isMember: boolean }) { +export default function Summary({ + isMember, + room, +}: { + isMember: boolean + room: RoomsData +}) { + const [chosenBed, setChosenBed] = useState() + const [chosenBreakfast, setCosenBreakfast] = useState() const intl = useIntl() const lang = useLang() - const { fromDate, toDate, rooms, hotel, bedType, breakfast } = - useEnterDetailsStore((state) => ({ - fromDate: state.roomData.fromdate, - toDate: state.roomData.todate, + const { fromDate, toDate, bedType, breakfast } = useEnterDetailsStore( + (state) => ({ + fromDate: state.roomData.fromDate, + toDate: state.roomData.toDate, rooms: state.roomData.room, hotel: state.roomData.hotel, bedType: state.userData.bedType, breakfast: state.userData.breakfast, - })) - - const totalAdults = rooms.reduce((total, room) => total + room.adults, 0) - - const { - data: availabilityData, - isLoading, - error, - } = trpc.hotel.availability.rooms.useQuery( - { - hotelId: parseInt(hotel), - adults: totalAdults, - roomStayStartDate: dt(fromDate).format("YYYY-MM-DD"), - roomStayEndDate: dt(toDate).format("YYYY-MM-DD"), - }, - { enabled: !!hotel && !!fromDate && !!toDate } + }) ) const diff = dt(toDate).diff(fromDate, "days") @@ -56,37 +47,15 @@ export default function Summary({ isMember }: { isMember: boolean }) { { totalNights: diff } ) - if (isLoading) { - return + let color: "uiTextHighContrast" | "red" = "uiTextHighContrast" + if (isMember) { + color = "red" } - const populatedRooms = rooms - .map((room) => { - const chosenRoom = availabilityData?.roomConfigurations.find( - (availRoom) => room.roomtypecode === availRoom.roomTypeCode - ) - const cancellationText = availabilityData?.rateDefinitions.find( - (rate) => rate.rateCode === room.ratecode - )?.cancellationText - if (chosenRoom) { - const memberPrice = chosenRoom.products.find( - (rate) => rate.productType.member?.rateCode === room.ratecode - )?.productType.member?.localPrice.pricePerStay - const publicPrice = chosenRoom.products.find( - (rate) => rate.productType.public?.rateCode === room.ratecode - )?.productType.public?.localPrice.pricePerStay - - return { - roomType: chosenRoom.roomType, - memberPrice: memberPrice && formatNumber(parseInt(memberPrice)), - publicPrice: publicPrice && formatNumber(parseInt(publicPrice)), - adults: room.adults, - children: room.child, - cancellationText, - } - } - }) - .filter((room): room is RoomsData => room !== undefined) + useEffect(() => { + setChosenBed(bedType) + setCosenBreakfast(breakfast) + }, [bedType, breakfast]) return (
@@ -100,14 +69,48 @@ export default function Summary({ isMember }: { isMember: boolean }) {
- {populatedRooms.map((room, idx) => ( - - ))} - - {bedType ? ( +
- {bedType} - + {room.roomType} + + {intl.formatMessage( + { id: "{amount} {currency}" }, + { amount: room.price, currency: "SEK" } + )} + +
+ + {intl.formatMessage( + { id: "booking.adults" }, + { totalAdults: room.adults } + )} + + {room.children?.length ? ( + + {intl.formatMessage( + { id: "booking.children" }, + { totalChildren: room.children.length } + )} + + ) : null} + + {room.cancellationText} + + + {intl.formatMessage({ id: "Rate details" })} + +
+ + {chosenBed ? ( +
+
+ {chosenBed} + + {intl.formatMessage({ id: "Based on availability" })} + +
+ + {intl.formatMessage( { id: "{amount} {currency}" }, { amount: "0", currency: "SEK" } @@ -115,10 +118,11 @@ export default function Summary({ isMember }: { isMember: boolean }) {
) : null} - {breakfast ? ( + + {chosenBreakfast ? (
- {breakfast} - + {chosenBreakfast} + {intl.formatMessage( { id: "{amount} {currency}" }, { amount: "0", currency: "SEK" } @@ -130,77 +134,35 @@ export default function Summary({ isMember }: { isMember: boolean }) {
- - {intl.formatMessage({ id: "Total price (incl VAT)" })} - - - {intl.formatMessage( - { id: "{amount} {currency}" }, - { amount: "4686", currency: "SEK" } - )} - -
-
- - {intl.formatMessage({ id: "Approx." })} - - - {intl.formatMessage( - { id: "{amount} {currency}" }, - { amount: "455", currency: "EUR" } - )} - +
+ + {intl.formatMessage( + { id: "Total price (incl VAT)" }, + { b: (str) => {str} } + )} + + + {intl.formatMessage({ id: "Price details" })} + +
+
+ + {intl.formatMessage( + { id: "{amount} {currency}" }, + { amount: room.price, currency: "SEK" } // TODO: calculate total price + )} + + + {intl.formatMessage({ id: "Approx." })}{" "} + {intl.formatMessage( + { id: "{amount} {currency}" }, + { amount: "455", currency: "EUR" } + )} + +
+
) } - -function RoomBreakdown({ - room, - isMember, -}: { - room: RoomsData - isMember: boolean -}) { - const intl = useIntl() - - let color: "uiTextHighContrast" | "red" = "uiTextHighContrast" - let price = room.publicPrice - if (isMember) { - color = "red" - price = room.memberPrice - } - - return ( -
-
- {room.roomType} - - {intl.formatMessage( - { id: "{amount} {currency}" }, - { amount: price, currency: "SEK" } - )} - -
- - {intl.formatMessage( - { id: "booking.adults" }, - { totalAdults: room.adults } - )} - - {room.children?.length ? ( - - {intl.formatMessage( - { id: "booking.children" }, - { totalChildren: room.children.length } - )} - - ) : null} - {room.cancellationText} - - {intl.formatMessage({ id: "Rate details" })} - -
- ) -} diff --git a/components/HotelReservation/EnterDetails/Summary/summary.module.css b/components/HotelReservation/EnterDetails/Summary/summary.module.css index 472508e79..b7a8d2db4 100644 --- a/components/HotelReservation/EnterDetails/Summary/summary.module.css +++ b/components/HotelReservation/EnterDetails/Summary/summary.module.css @@ -1,11 +1,10 @@ .summary { - background-color: var(--Main-Grey-White); - border: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); border-radius: var(--Corner-radius-Large); display: flex; flex-direction: column; gap: var(--Spacing-x2); - padding: var(--Spacing-x2); + padding: var(--Spacing-x3); + height: 100%; } .date { @@ -31,6 +30,9 @@ justify-content: space-between; } +.entry > :last-child { + justify-items: flex-end; +} .total { display: flex; flex-direction: column; diff --git a/components/HotelReservation/SelectRate/RoomSelection/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/index.tsx index 9929a7451..4b6613e7e 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/index.tsx @@ -4,7 +4,7 @@ import { useMemo, useState } from "react" import RateSummary from "./RateSummary" import RoomCard from "./RoomCard" -import getHotelReservationQueryParams from "./utils" +import { getHotelReservationQueryParams } from "./utils" import styles from "./roomSelection.module.css" diff --git a/components/HotelReservation/SelectRate/RoomSelection/utils.ts b/components/HotelReservation/SelectRate/RoomSelection/utils.ts index 1ae94cc9c..24309d355 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/utils.ts +++ b/components/HotelReservation/SelectRate/RoomSelection/utils.ts @@ -2,11 +2,22 @@ import { getFormattedUrlQueryParams } from "@/utils/url" import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" -function getHotelReservationQueryParams(searchParams: URLSearchParams) { +export function getHotelReservationQueryParams(searchParams: URLSearchParams) { return getFormattedUrlQueryParams(searchParams, { adults: "number", age: "number", }) as SelectRateSearchParams } -export default getHotelReservationQueryParams +export function getQueryParamsForEnterDetails(searchParams: URLSearchParams) { + const selectRoomParamsObject = getHotelReservationQueryParams(searchParams) + + const { room } = selectRoomParamsObject + return { + ...selectRoomParamsObject, + adults: room[0].adults, // TODO: Handle multiple rooms + children: room[0].child?.length.toString(), // TODO: Handle multiple rooms + roomTypeCode: room[0].roomtypecode, + rateCode: room[0].ratecode, + } +} diff --git a/components/TempDesignSystem/Text/Body/variants.ts b/components/TempDesignSystem/Text/Body/variants.ts index 3cf9a1376..5acc36958 100644 --- a/components/TempDesignSystem/Text/Body/variants.ts +++ b/components/TempDesignSystem/Text/Body/variants.ts @@ -16,7 +16,6 @@ const config = { textHighContrast: styles.textHighContrast, white: styles.white, peach50: styles.peach50, - baseTextMediumContrast: styles.baseTextMediumContrast, uiTextHighContrast: styles.uiTextHighContrast, uiTextMediumContrast: styles.uiTextMediumContrast, uiTextPlaceholder: styles.uiTextPlaceholder, diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index fadfdedea..2cdcfa050 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -1,5 +1,7 @@ { "Included (based on availability)": "Inkluderet (baseret på tilgængelighed)", + "Total price (incl VAT)": "Samlet pris (inkl. moms)", + "{amount} {currency}/night per adult": "{amount} {currency}/nat pr. voksen", "A destination or hotel name is needed to be able to search for a hotel room.": "Et destinations- eller hotelnavn er nødvendigt for at kunne søge efter et hotelværelse.", "A photo of the room": "Et foto af værelset", "ACCE": "Tilgængelighed", @@ -35,6 +37,7 @@ "Attractions": "Attraktioner", "Back to scandichotels.com": "Tilbage til scandichotels.com", "Bar": "Bar", + "Based on availability": "Baseret på tilgængelighed", "Bed type": "Seng type", "Birth date": "Fødselsdato", "Book": "Book", @@ -242,12 +245,14 @@ "Points needed to stay on level": "Point nødvendige for at holde sig på niveau", "Previous": "Forudgående", "Previous victories": "Tidligere sejre", + "Price details": "Prisoplysninger", "Proceed to login": "Fortsæt til login", "Proceed to payment method": "Fortsæt til betalingsmetode", "Provide a payment card in the next step": "Giv os dine betalingsoplysninger i næste skridt", "Public price from": "Offentlig pris fra", "Public transport": "Offentlig transport", "Queen bed": "Queensize-seng", + "Rate details": "Oplysninger om værelsespris", "Read more": "Læs mere", "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Læs mere om hotellet", @@ -405,6 +410,5 @@ "uppercase letter": "stort bogstav", "{amount} out of {total}": "{amount} ud af {total}", "{amount} {currency}": "{amount} {currency}", - "{difference}{amount} {currency}": "{difference}{amount} {currency}", - "{width} cm × {length} cm": "{width} cm × {length} cm" + "{difference}{amount} {currency}": "{difference}{amount} {currency}" } diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index f7223efae..df92ae71e 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -1,5 +1,7 @@ { "Included (based on availability)": "Inbegriffen (je nach Verfügbarkeit)", + "Total price (incl VAT)": "Gesamtpreis (inkl. MwSt.)", + "{amount} {currency}/night per adult": "{amount} {currency}/Nacht pro Erwachsener", "A destination or hotel name is needed to be able to search for a hotel room.": "Ein Reiseziel oder Hotelname wird benötigt, um nach einem Hotelzimmer suchen zu können.", "A photo of the room": "Ein Foto des Zimmers", "ACCE": "Zugänglichkeit", @@ -35,6 +37,7 @@ "Attraction": "Attraktion", "Back to scandichotels.com": "Zurück zu scandichotels.com", "Bar": "Bar", + "Based on availability": "Je nach Verfügbarkeit", "Bed type": "Bettentyp", "Birth date": "Geburtsdatum", "Book": "Buchen", @@ -240,12 +243,14 @@ "Points needed to stay on level": "Erforderliche Punkte, um auf diesem Level zu bleiben", "Previous": "Früher", "Previous victories": "Bisherige Siege", + "Price details": "Preisdetails", "Proceed to login": "Weiter zum Login", "Proceed to payment method": "Weiter zur Zahlungsmethode", "Provide a payment card in the next step": "Geben Sie Ihre Zahlungskarteninformationen im nächsten Schritt an", "Public price from": "Öffentlicher Preis ab", "Public transport": "Öffentliche Verkehrsmittel", "Queen bed": "Queensize-Bett", + "Rate details": "Preisdetails", "Read more": "Mehr lesen", "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Lesen Sie mehr über das Hotel", @@ -404,6 +409,5 @@ "uppercase letter": "großbuchstabe", "{amount} out of {total}": "{amount} von {total}", "{amount} {currency}": "{amount} {currency}", - "{difference}{amount} {currency}": "{difference}{amount} {currency}", - "{width} cm × {length} cm": "{width} cm × {length} cm" + "{difference}{amount} {currency}": "{difference}{amount} {currency}" } diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 6cd2128c0..eec758e7a 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -1,5 +1,7 @@ { "Included (based on availability)": "Included (based on availability)", + "Total price (incl VAT)": "Total price (incl VAT)", + "{amount} {currency}/night per adult": "{amount} {currency}/night per adult", "A destination or hotel name is needed to be able to search for a hotel room.": "A destination or hotel name is needed to be able to search for a hotel room.", "A photo of the room": "A photo of the room", "ACCE": "Accessibility", @@ -38,6 +40,7 @@ "Attractions": "Attractions", "Back to scandichotels.com": "Back to scandichotels.com", "Bar": "Bar", + "Based on availability": "Based on availability", "Bed": "Bed", "Bed type": "Bed type", "Birth date": "Birth date", @@ -252,6 +255,7 @@ "Points needed to stay on level": "Points needed to stay on level", "Previous": "Previous", "Previous victories": "Previous victories", + "Price details": "Price details", "Print confirmation": "Print confirmation", "Proceed to login": "Proceed to login", "Proceed to payment method": "Proceed to payment method", @@ -259,6 +263,7 @@ "Public price from": "Public price from", "Public transport": "Public transport", "Queen bed": "Queen bed", + "Rate details": "Rate details", "Read more": "Read more", "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Read more about the hotel", @@ -329,7 +334,6 @@ "Total cost": "Total cost", "Total price": "Total price", "Total Points": "Total Points", - "Total price (incl VAT)": "Total price (incl VAT)", "Tourist": "Tourist", "Transaction date": "Transaction date", "Transactions": "Transactions", @@ -427,6 +431,5 @@ "{amount} out of {total}": "{amount} out of {total}", "{amount} {currency}": "{amount} {currency}", "{card} ending with {cardno}": "{card} ending with {cardno}", - "{difference}{amount} {currency}": "{difference}{amount} {currency}", - "{width} cm × {length} cm": "{width} cm × {length} cm" + "{difference}{amount} {currency}": "{difference}{amount} {currency}" } diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index f46700b16..25acf8a9a 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -1,5 +1,7 @@ { "Included (based on availability)": "Sisältyy (saatavuuden mukaan)", + "Total price (incl VAT)": "Kokonaishinta (sis. ALV)", + "{amount} {currency}/night per adult": "{amount} {currency}/yö per aikuinen", "A destination or hotel name is needed to be able to search for a hotel room.": "Kohteen tai hotellin nimi tarvitaan, jotta hotellihuonetta voidaan hakea.", "A photo of the room": "Kuva huoneesta", "ACCE": "Saavutettavuus", @@ -35,6 +37,7 @@ "Attractions": "Nähtävyydet", "Back to scandichotels.com": "Takaisin scandichotels.com", "Bar": "Bar", + "Based on availability": "Saatavuuden mukaan", "Bed type": "Vuodetyyppi", "Birth date": "Syntymäaika", "Book": "Varaa", @@ -242,12 +245,14 @@ "Points needed to stay on level": "Tällä tasolla pysymiseen tarvittavat pisteet", "Previous": "Aikaisempi", "Previous victories": "Edelliset voitot", + "Price details": "Hintatiedot", "Proceed to login": "Jatka kirjautumiseen", "Proceed to payment method": "Siirry maksutavalle", "Provide a payment card in the next step": "Anna maksukortin tiedot seuraavassa vaiheessa", "Public price from": "Julkinen hinta alkaen", "Public transport": "Julkinen liikenne", "Queen bed": "Queen-vuode", + "Rate details": "Hintatiedot", "Read more": "Lue lisää", "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Lue lisää hotellista", @@ -404,6 +409,5 @@ "uppercase letter": "iso kirjain", "{amount} out of {total}": "{amount}/{total}", "{amount} {currency}": "{amount} {currency}", - "{difference}{amount} {currency}": "{difference}{amount} {currency}", - "{width} cm × {length} cm": "{width} cm × {length} cm" + "{difference}{amount} {currency}": "{difference}{amount} {currency}" } diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index f13a25268..41d740068 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -1,5 +1,7 @@ { "Included (based on availability)": "Inkludert (basert på tilgjengelighet)", + "Total price (incl VAT)": "Totalpris (inkl. mva)", + "{amount} {currency}/night per adult": "{amount} {currency}/natt per voksen", "A destination or hotel name is needed to be able to search for a hotel room.": "Et reisemål eller hotellnavn er nødvendig for å kunne søke etter et hotellrom.", "A photo of the room": "Et bilde av rommet", "ACCE": "Tilgjengelighet", @@ -35,6 +37,7 @@ "Attractions": "Attraksjoner", "Back to scandichotels.com": "Tilbake til scandichotels.com", "Bar": "Bar", + "Based on availability": "Basert på tilgjengelighet", "Bed type": "Seng type", "Birth date": "Fødselsdato", "Book": "Bestill", @@ -240,12 +243,14 @@ "Points needed to stay on level": "Poeng som trengs for å holde seg på nivå", "Previous": "Tidligere", "Previous victories": "Tidligere seire", + "Price details": "Prisdetaljer", "Proceed to login": "Fortsett til innlogging", "Proceed to payment method": "Fortsett til betalingsmetode", "Provide a payment card in the next step": "Gi oss dine betalingskortdetaljer i neste steg", "Public price from": "Offentlig pris fra", "Public transport": "Offentlig transport", "Queen bed": "Queen-size-seng", + "Rate details": "Prisdetaljer", "Read more": "Les mer", "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Les mer om hotellet", @@ -402,6 +407,5 @@ "uppercase letter": "stor bokstav", "{amount} out of {total}": "{amount} av {total}", "{amount} {currency}": "{amount} {currency}", - "{difference}{amount} {currency}": "{difference}{amount} {currency}", - "{width} cm × {length} cm": "{width} cm × {length} cm" + "{difference}{amount} {currency}": "{difference}{amount} {currency}" } diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index b74e19c0b..01de7beaf 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -1,5 +1,7 @@ { "Included (based on availability)": "Ingår (baserat på tillgänglighet)", + "Total price (incl VAT)": "Totalpris (inkl moms)", + "{amount} {currency}/night per adult": "{amount} {currency}/natt per vuxen", "A destination or hotel name is needed to be able to search for a hotel room.": "Ett destinations- eller hotellnamn behövs för att kunna söka efter ett hotellrum.", "A photo of the room": "Ett foto av rummet", "ACCE": "Tillgänglighet", @@ -35,6 +37,7 @@ "Attractions": "Sevärdheter", "Back to scandichotels.com": "Tillbaka till scandichotels.com", "Bar": "Bar", + "Based on availability": "Baserat på tillgänglighet", "Bed type": "Sängtyp", "Birth date": "Födelsedatum", "Book": "Boka", @@ -240,12 +243,14 @@ "Points needed to stay on level": "Poäng som behövs för att hålla sig på nivå", "Previous": "Föregående", "Previous victories": "Tidigare segrar", + "Price details": "Prisdetaljer", "Proceed to login": "Fortsätt till inloggning", "Proceed to payment method": "Gå vidare till betalningsmetod", "Provide a payment card in the next step": "Ge oss dina betalkortdetaljer i nästa steg", "Public price from": "Offentligt pris från", "Public transport": "Kollektivtrafik", "Queen bed": "Queen size-säng", + "Rate details": "Detaljer om rumspriset", "Read more": "Läs mer", "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Läs mer om hotellet", @@ -405,6 +410,5 @@ "paying": "betalar", "uppercase letter": "stor bokstav", "{amount} {currency}": "{amount} {currency}", - "{difference}{amount} {currency}": "{difference}{amount} {currency}", - "{width} cm × {length} cm": "{width} cm × {length} cm" + "{difference}{amount} {currency}": "{difference}{amount} {currency}" } diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index ec361b96c..00c404bfb 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -1,6 +1,10 @@ import { cache } from "react" import { Lang } from "@/constants/languages" +import { + GetRoomsAvailabilityInput, + HotelIncludeEnum, +} from "@/server/routers/hotels/input" import { serverClient } from "../server" @@ -53,12 +57,14 @@ export const getUserTracking = cache(async function getMemoizedUserTracking() { export const getHotelData = cache(async function getMemoizedHotelData( hotelId: string, language: string, - isCardOnlyPayment?: boolean + isCardOnlyPayment?: boolean, + include?: HotelIncludeEnum[] ) { return serverClient().hotel.hotelData.get({ hotelId, language, isCardOnlyPayment, + include, }) }) @@ -71,17 +77,9 @@ export const getRoomAvailability = cache( children, promotionCode, rateCode, - }: { - hotelId: string - adults: number - roomStayStartDate: string - roomStayEndDate: string - children?: string - promotionCode?: string - rateCode?: string - }) { + }: GetRoomsAvailabilityInput) { return serverClient().hotel.availability.rooms({ - hotelId: parseInt(hotelId), + hotelId, adults, roomStayStartDate, roomStayEndDate, diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index 2d69ab642..ff8368b2a 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -29,17 +29,26 @@ export const getRoomsAvailabilityInputSchema = z.object({ rateCode: z.string().optional(), }) +export type GetRoomsAvailabilityInput = z.input< + typeof getRoomsAvailabilityInputSchema +> + export const getRatesInputSchema = z.object({ hotelId: z.string(), }) -export const getlHotelDataInputSchema = z.object({ +export enum HotelIncludeEnum { + "RoomCategories", + "NearbyHotels", + "Restaurants", + "City", +} + +export const getHotelDataInputSchema = z.object({ hotelId: z.string(), language: z.string(), isCardOnlyPayment: z.boolean().optional(), - include: z - .array(z.enum(["RoomCategories", "NearbyHotels", "Restaurants", "City"])) - .optional(), + include: z.array(z.nativeEnum(HotelIncludeEnum)).optional(), }) export const getBreakfastPackageInput = z.object({ diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 6ad27655a..4aacd6ff2 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -32,9 +32,9 @@ import { } from "./schemas/packages" import { getBreakfastPackageInput, + getHotelDataInputSchema, getHotelInputSchema, getHotelsAvailabilityInputSchema, - getlHotelDataInputSchema, getRatesInputSchema, getRoomsAvailabilityInputSchema, } from "./input" @@ -584,7 +584,7 @@ export const hotelQueryRouter = router({ }), hotelData: router({ get: serviceProcedure - .input(getlHotelDataInputSchema) + .input(getHotelDataInputSchema) .query(async ({ ctx, input }) => { const { hotelId, language, include, isCardOnlyPayment } = input diff --git a/server/routers/hotels/schemas/room.ts b/server/routers/hotels/schemas/room.ts index 19f922db0..5a1480097 100644 --- a/server/routers/hotels/schemas/room.ts +++ b/server/routers/hotels/schemas/room.ts @@ -87,8 +87,11 @@ export const roomSchema = z name: data.attributes.name, occupancy: data.attributes.occupancy, roomSize: data.attributes.roomSize, + roomTypes: data.attributes.roomTypes, sortOrder: data.attributes.sortOrder, type: data.type, roomFacilities: data.attributes.roomFacilities, } }) + +export type RoomType = Pick, "roomTypes" | "name"> diff --git a/stores/enter-details.ts b/stores/enter-details.ts index 44289cc21..ccea01655 100644 --- a/stores/enter-details.ts +++ b/stores/enter-details.ts @@ -6,21 +6,20 @@ import { create, useStore } from "zustand" import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema" import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" import { detailsSchema } from "@/components/HotelReservation/EnterDetails/Details/schema" -import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" +import { getHotelReservationQueryParams } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" +import type { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData" import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast" +import type { DetailsSchema } from "@/types/components/hotelReservation/enterDetails/details" import { SidePeekEnum } from "@/types/components/hotelReservation/enterDetails/sidePeek" import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step" -import { BedTypeEnum } from "@/types/enums/bedType" import { BreakfastPackageEnum } from "@/types/enums/breakfast" -import type { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData" -import type { DetailsSchema } from "@/types/components/hotelReservation/enterDetails/details" const SESSION_STORAGE_KEY = "enterDetails" interface EnterDetailsState { userData: { - bedType: BedTypeEnum | undefined + bedType: string | undefined breakfast: BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST | undefined } & DetailsSchema roomData: BookingData diff --git a/types/components/hotelReservation/enterDetails/bookingData.ts b/types/components/hotelReservation/enterDetails/bookingData.ts index 4b4a72898..9a780fa66 100644 --- a/types/components/hotelReservation/enterDetails/bookingData.ts +++ b/types/components/hotelReservation/enterDetails/bookingData.ts @@ -5,23 +5,21 @@ interface Child { interface Room { adults: number - roomtypecode: string - ratecode: string - child: Child[] + roomtypecode?: string + ratecode?: string + child?: Child[] } - export interface BookingData { hotel: string - fromdate: string - todate: string + fromDate: string + toDate: string room: Room[] } export type RoomsData = { roomType: string - memberPrice: string | undefined - publicPrice: string | undefined + price: string adults: number - children: Child[] - cancellationText: string | undefined + children?: Child[] + cancellationText: string } diff --git a/types/components/hotelReservation/selectRate/selectRate.ts b/types/components/hotelReservation/selectRate/selectRate.ts index 553d09827..42ba91bde 100644 --- a/types/components/hotelReservation/selectRate/selectRate.ts +++ b/types/components/hotelReservation/selectRate/selectRate.ts @@ -7,7 +7,7 @@ export interface Child { interface Room { adults: number - roomcode?: string + roomtypecode?: string ratecode?: string child?: Child[] } From 7710d3f8f90c11bccd7d58e4cac0e8ef35ccad1a Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Fri, 25 Oct 2024 11:09:03 +0200 Subject: [PATCH 005/120] fix: make summary sticky --- .../(standard)/[step]/@hotelHeader/page.tsx | 5 +- .../(standard)/[step]/@sidePeek/page.tsx | 5 +- .../(standard)/[step]/@summary/page.tsx | 59 ++++++++------ .../(standard)/[step]/layout.module.css | 79 ++++++++++++++++++- .../(standard)/[step]/layout.tsx | 6 +- .../(standard)/[step]/page.tsx | 18 +++-- .../(standard)/layout.module.css | 1 - .../EnterDetails/BedType/index.tsx | 7 +- .../EnterDetails/Summary/index.tsx | 21 +++-- .../SelectRate/RoomSelection/utils.ts | 4 +- .../Form/ChoiceCard/_Card/card.ts | 2 +- lib/trpc/memoizedRequests/index.ts | 15 ++-- next.config.js | 4 +- stores/enter-details.ts | 43 +--------- .../enterDetails/bookingData.ts | 5 +- .../hotelReservation/selectRate/selectRate.ts | 3 +- 16 files changed, 170 insertions(+), 107 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx index 58a216006..75101475a 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx @@ -14,7 +14,10 @@ export default async function HotelHeader({ if (!searchParams.hotel) { redirect(home) } - const hotel = await getHotelData(searchParams.hotel, params.lang) + const hotel = await getHotelData({ + hotelId: searchParams.hotel, + language: params.lang, + }) if (!hotel?.data) { redirect(home) } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/page.tsx index 13b770699..deca843c3 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/page.tsx @@ -13,7 +13,10 @@ export default async function HotelSidePeek({ if (!searchParams.hotel) { redirect(`/${params.lang}`) } - const hotel = await getHotelData(searchParams.hotel, params.lang) + const hotel = await getHotelData({ + hotelId: searchParams.hotel, + language: params.lang, + }) if (!hotel?.data) { redirect(`/${params.lang}`) } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx index 4a15e3994..4258d94d8 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx @@ -20,23 +20,31 @@ export default async function SummaryPage({ const { hotel, adults, children, roomTypeCode, rateCode, fromDate, toDate } = getQueryParamsForEnterDetails(selectRoomParams) - const user = await getProfileSafely() - const hotelData = await getHotelData(hotel, params.lang, undefined, [HotelIncludeEnum.RoomCategories]) - const availability = await getRoomAvailability({ - hotelId: parseInt(hotel), - adults, - children, - roomStayStartDate: fromDate, - roomStayEndDate: toDate, - }) + const [user, hotelData, availability] = await Promise.all([ + getProfileSafely(), + getHotelData({ + hotelId: hotel, + language: params.lang, + include: [HotelIncludeEnum.RoomCategories], + }), + getRoomAvailability({ + hotelId: parseInt(hotel), + adults, + children, + roomStayStartDate: fromDate, + roomStayEndDate: toDate, + }), + ]) if (!hotelData?.data || !hotelData?.included || !availability) { console.error("No hotel or availability data", hotelData, availability) - // TODO: handle this case return null } + const cancellationText = + availability?.rateDefinitions.find((rate) => rate.rateCode === rateCode) + ?.cancellationText ?? "" const chosenRoom = availability.roomConfigurations.find( (availRoom) => availRoom.roomTypeCode === roomTypeCode ) @@ -47,28 +55,31 @@ export default async function SummaryPage({ return null } - const cancellationText = - availability?.rateDefinitions.find((rate) => rate.rateCode === rateCode) - ?.cancellationText ?? "" + const memberRate = chosenRoom.products.find( + (rate) => rate.productType.member?.rateCode === rateCode + )?.productType.member - const memberPrice = - chosenRoom.products.find( - (rate) => rate.productType.member?.rateCode === rateCode - )?.productType.member?.localPrice.pricePerStay ?? "0" + const publicRate = chosenRoom.products.find( + (rate) => rate.productType.public?.rateCode === rateCode + )?.productType.public - const publicPrice = - chosenRoom.products.find( - (rate) => rate.productType.public?.rateCode === rateCode - )?.productType.public?.localPrice.pricePerStay ?? "0" - - const price = user ? memberPrice : publicPrice + const prices = user + ? { + local: memberRate?.localPrice.pricePerStay, + euro: memberRate?.requestedPrice?.pricePerStay, + } + : { + local: publicRate?.localPrice.pricePerStay, + euro: publicRate?.requestedPrice?.pricePerStay, + } return ( {children} - + {sidePeek} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 44de099bf..adfcbc34a 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -20,7 +20,7 @@ import { getIntl } from "@/i18n" import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step" import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" -import type { LangParams, PageArgs, } from "@/types/params" +import type { LangParams, PageArgs } from "@/types/params" export function preload() { void getProfileSafely() @@ -34,12 +34,10 @@ function isValidStep(step: string): step is StepEnum { export default async function StepPage({ params, searchParams, -}: PageArgs< - LangParams & { step: StepEnum }, - SelectRateSearchParams ->) { +}: PageArgs) { + const { lang } = params if (!searchParams.hotel) { - redirect(`/${params.lang}`) + redirect(`/${lang}`) } void getBreakfastPackages(searchParams.hotel) @@ -64,7 +62,11 @@ export default async function StepPage({ rateCode }) - const hotelData = await getHotelData(hotelId, params.lang, undefined, [HotelIncludeEnum.RoomCategories]) + const hotelData = await getHotelData({ + hotelId, + language: lang, + include: [HotelIncludeEnum.RoomCategories], + }) const user = await getProfileSafely() const savedCreditCards = await getCreditCardsSafely() @@ -153,6 +155,6 @@ export default async function StepPage({ mustBeGuaranteed={mustBeGuaranteed} /> - + ) } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.module.css index 0969a7151..1730ffa68 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.module.css @@ -1,4 +1,3 @@ .layout { - min-height: 100dvh; background-color: var(--Base-Background-Primary-Normal); } diff --git a/components/HotelReservation/EnterDetails/BedType/index.tsx b/components/HotelReservation/EnterDetails/BedType/index.tsx index 27c95d963..646ec1311 100644 --- a/components/HotelReservation/EnterDetails/BedType/index.tsx +++ b/components/HotelReservation/EnterDetails/BedType/index.tsx @@ -43,10 +43,6 @@ export default function BedType({ reValidateMode: "onChange", }) - const text = intl.formatMessage( - { id: "Included (based on availability)" }, - { b: (str) => {str} } - ) const completeStep = useEnterDetailsStore((state) => state.completeStep) const onSubmit = useCallback( @@ -71,7 +67,7 @@ export default function BedType({ {roomTypes.map((roomType) => { const width = roomType.size.max === roomType.size.min - ? roomType.size.max + ? `${roomType.size.min} cm` : `${roomType.size.min} cm - ${roomType.size.max} cm` return ( diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index c1bad51f4..094c9e860 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -17,6 +17,7 @@ import useLang from "@/hooks/useLang" import styles from "./summary.module.css" import { RoomsData } from "@/types/components/hotelReservation/enterDetails/bookingData" +import { BreakfastPackageEnum } from "@/types/enums/breakfast" export default function Summary({ isMember, @@ -25,8 +26,8 @@ export default function Summary({ isMember: boolean room: RoomsData }) { - const [chosenBed, setChosenBed] = useState() - const [chosenBreakfast, setCosenBreakfast] = useState() + const [chosenBed, setChosenBed] = useState() + const [chosenBreakfast, setCosenBreakfast] = useState() const intl = useIntl() const lang = useLang() const { fromDate, toDate, bedType, breakfast } = useEnterDetailsStore( @@ -54,7 +55,11 @@ export default function Summary({ useEffect(() => { setChosenBed(bedType) - setCosenBreakfast(breakfast) + if (breakfast === BreakfastPackageEnum.NO_BREAKFAST) { + setCosenBreakfast("No breakfast") + } else if (breakfast) { + setCosenBreakfast("Breakfast buffet") + } }, [bedType, breakfast]) return ( @@ -75,7 +80,7 @@ export default function Summary({ {intl.formatMessage( { id: "{amount} {currency}" }, - { amount: room.price, currency: "SEK" } + { amount: room.localPrice, currency: "SEK" } )} @@ -121,7 +126,9 @@ export default function Summary({ {chosenBreakfast ? (
- {chosenBreakfast} + + {intl.formatMessage({ id: chosenBreakfast })} + {intl.formatMessage( { id: "{amount} {currency}" }, @@ -149,14 +156,14 @@ export default function Summary({ {intl.formatMessage( { id: "{amount} {currency}" }, - { amount: room.price, currency: "SEK" } // TODO: calculate total price + { amount: room.localPrice, currency: "SEK" } // TODO: calculate total price )} {intl.formatMessage({ id: "Approx." })}{" "} {intl.formatMessage( { id: "{amount} {currency}" }, - { amount: "455", currency: "EUR" } + { amount: room.euroPrice, currency: "EUR" } )}
diff --git a/components/HotelReservation/SelectRate/RoomSelection/utils.ts b/components/HotelReservation/SelectRate/RoomSelection/utils.ts index 24309d355..1fe62f39c 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/utils.ts +++ b/components/HotelReservation/SelectRate/RoomSelection/utils.ts @@ -16,8 +16,8 @@ export function getQueryParamsForEnterDetails(searchParams: URLSearchParams) { return { ...selectRoomParamsObject, adults: room[0].adults, // TODO: Handle multiple rooms - children: room[0].child?.length.toString(), // TODO: Handle multiple rooms - roomTypeCode: room[0].roomtypecode, + children: room[0].child?.length.toString(), // TODO: Handle multiple rooms and children + roomTypeCode: room[0].roomtype, rateCode: room[0].ratecode, } } diff --git a/components/TempDesignSystem/Form/ChoiceCard/_Card/card.ts b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.ts index 145116409..8fe8476d3 100644 --- a/components/TempDesignSystem/Form/ChoiceCard/_Card/card.ts +++ b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.ts @@ -23,7 +23,7 @@ interface ListCardProps extends BaseCardProps { interface TextCardProps extends BaseCardProps { list?: never - text: React.ReactNode + text?: React.ReactNode } export type CardProps = ListCardProps | TextCardProps diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index 00c404bfb..547ff7874 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -54,12 +54,17 @@ export const getUserTracking = cache(async function getMemoizedUserTracking() { return serverClient().user.tracking() }) -export const getHotelData = cache(async function getMemoizedHotelData( - hotelId: string, - language: string, - isCardOnlyPayment?: boolean, +export const getHotelData = cache(async function getMemoizedHotelData({ + hotelId, + language, + isCardOnlyPayment, + include, +}: { + hotelId: string + language: string + isCardOnlyPayment?: boolean include?: HotelIncludeEnum[] -) { +}) { return serverClient().hotel.hotelData.get({ hotelId, language, diff --git a/next.config.js b/next.config.js index f91af6594..fae418d6c 100644 --- a/next.config.js +++ b/next.config.js @@ -87,12 +87,12 @@ const nextConfig = { // value: undefined, // }, // { - // key: "fromdate", + // key: "fromDate", // type: "query", // value: undefined, // }, // { - // key: "todate", + // key: "toDate", // type: "query", // value: undefined, // }, diff --git a/stores/enter-details.ts b/stores/enter-details.ts index ccea01655..1c2c6b8d8 100644 --- a/stores/enter-details.ts +++ b/stores/enter-details.ts @@ -6,7 +6,7 @@ import { create, useStore } from "zustand" import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema" import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" import { detailsSchema } from "@/components/HotelReservation/EnterDetails/Details/schema" -import { getHotelReservationQueryParams } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" +import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import type { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData" import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast" @@ -37,22 +37,6 @@ interface EnterDetailsState { closeSidePeek: () => void } -function getUpdatedValue( - searchParams: URLSearchParams, - key: string, - defaultValue: T -): T { - const value = searchParams.get(key) - if (value === null) return defaultValue - if (typeof defaultValue === "number") - return parseInt(value, 10) as unknown as T - if (typeof defaultValue === "boolean") - return (value === "true") as unknown as T - if (defaultValue instanceof Date) return new Date(value) as unknown as T - - return value as unknown as T -} - export function initEditDetailsState( currentStep: StepEnum, searchParams: ReadonlyURLSearchParams @@ -62,32 +46,9 @@ export function initEditDetailsState( ? sessionStorage.getItem(SESSION_STORAGE_KEY) : null - const today = new Date() - const tomorrow = new Date() - tomorrow.setDate(today.getDate() + 1) - let roomData: BookingData if (searchParams?.size) { - roomData = getHotelReservationQueryParams(searchParams) - - roomData.room = roomData.room.map((room, index) => ({ - ...room, - adults: getUpdatedValue( - searchParams, - `room[${index}].adults`, - room.adults - ), - roomtypecode: getUpdatedValue( - searchParams, - `room[${index}].roomtypecode`, - room.roomtypecode - ), - ratecode: getUpdatedValue( - searchParams, - `room[${index}].ratecode`, - room.ratecode - ), - })) + roomData = getQueryParamsForEnterDetails(searchParams) } const defaultUserData: EnterDetailsState["userData"] = { diff --git a/types/components/hotelReservation/enterDetails/bookingData.ts b/types/components/hotelReservation/enterDetails/bookingData.ts index 9a780fa66..058d67990 100644 --- a/types/components/hotelReservation/enterDetails/bookingData.ts +++ b/types/components/hotelReservation/enterDetails/bookingData.ts @@ -5,7 +5,7 @@ interface Child { interface Room { adults: number - roomtypecode?: string + roomtype?: string ratecode?: string child?: Child[] } @@ -18,7 +18,8 @@ export interface BookingData { export type RoomsData = { roomType: string - price: string + localPrice: string + euroPrice: string adults: number children?: Child[] cancellationText: string diff --git a/types/components/hotelReservation/selectRate/selectRate.ts b/types/components/hotelReservation/selectRate/selectRate.ts index 42ba91bde..fd1771955 100644 --- a/types/components/hotelReservation/selectRate/selectRate.ts +++ b/types/components/hotelReservation/selectRate/selectRate.ts @@ -7,8 +7,9 @@ export interface Child { interface Room { adults: number - roomtypecode?: string + roomtype?: string ratecode?: string + counterratecode?: string child?: Child[] } From 46622d0515c96a982699596fdb5ed4f271a79c23 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Wed, 30 Oct 2024 15:21:51 +0100 Subject: [PATCH 006/120] fix: move crunching of data to trpc layer --- .../(standard)/[step]/@summary/page.tsx | 76 ++++---- .../(standard)/[step]/page.tsx | 60 ++++--- .../EnterDetails/BedType/index.tsx | 24 +-- .../EnterDetails/Summary/index.tsx | 71 +++++--- .../Form/ChoiceCard/_Card/card.ts | 10 +- lib/trpc/memoizedRequests/index.ts | 9 + server/routers/hotels/input.ts | 17 ++ server/routers/hotels/query.ts | 166 ++++++++++++++++++ .../hotelReservation/enterDetails/bedType.ts | 12 ++ .../enterDetails/bookingData.ts | 9 +- 10 files changed, 345 insertions(+), 109 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx index 4258d94d8..f1e1897e1 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx @@ -1,13 +1,12 @@ +import { notFound } from "next/navigation" + import { - getHotelData, getProfileSafely, - getRoomAvailability, + getSelectedRoomAvailability, } from "@/lib/trpc/memoizedRequests" -import { HotelIncludeEnum } from "@/server/routers/hotels/input" import Summary from "@/components/HotelReservation/EnterDetails/Summary" import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" -import { formatNumber } from "@/utils/format" import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import { LangParams, PageArgs, SearchParams } from "@/types/params" @@ -20,68 +19,61 @@ export default async function SummaryPage({ const { hotel, adults, children, roomTypeCode, rateCode, fromDate, toDate } = getQueryParamsForEnterDetails(selectRoomParams) - const [user, hotelData, availability] = await Promise.all([ + if (!roomTypeCode || !rateCode) { + console.log("No roomTypeCode or rateCode") + return notFound() + } + + const [user, availability] = await Promise.all([ getProfileSafely(), - getHotelData({ - hotelId: hotel, - language: params.lang, - include: [HotelIncludeEnum.RoomCategories], - }), - getRoomAvailability({ + getSelectedRoomAvailability({ hotelId: parseInt(hotel), adults, children, roomStayStartDate: fromDate, roomStayEndDate: toDate, + rateCode, + roomTypeCode, }), ]) - if (!hotelData?.data || !hotelData?.included || !availability) { - console.error("No hotel or availability data", hotelData, availability) + if (!availability) { + console.error("No hotel or availability data", availability) // TODO: handle this case return null } - const cancellationText = - availability?.rateDefinitions.find((rate) => rate.rateCode === rateCode) - ?.cancellationText ?? "" - const chosenRoom = availability.roomConfigurations.find( - (availRoom) => availRoom.roomTypeCode === roomTypeCode - ) - - if (!chosenRoom) { - // TODO: handle this case - console.error("No chosen room", chosenRoom) - return null - } - - const memberRate = chosenRoom.products.find( - (rate) => rate.productType.member?.rateCode === rateCode - )?.productType.member - - const publicRate = chosenRoom.products.find( - (rate) => rate.productType.public?.rateCode === rateCode - )?.productType.public - const prices = user ? { - local: memberRate?.localPrice.pricePerStay, - euro: memberRate?.requestedPrice?.pricePerStay, + local: { + price: availability.memberRate?.localPrice.pricePerStay, + currency: availability.memberRate?.localPrice.currency, + }, + euro: { + price: availability.memberRate?.requestedPrice?.pricePerStay, + currency: availability.memberRate?.requestedPrice?.currency, + }, } : { - local: publicRate?.localPrice.pricePerStay, - euro: publicRate?.requestedPrice?.pricePerStay, + local: { + price: availability.publicRate?.localPrice.pricePerStay, + currency: availability.publicRate?.localPrice.currency, + }, + euro: { + price: availability.publicRate?.requestedPrice?.pricePerStay, + currency: availability.publicRate?.requestedPrice?.currency, + }, } return ( ) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index adfcbc34a..dc07ed3f0 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -6,6 +6,7 @@ import { getHotelData, getProfileSafely, getRoomAvailability, + getSelectedRoomAvailability, } from "@/lib/trpc/memoizedRequests" import { HotelIncludeEnum } from "@/server/routers/hotels/input" @@ -36,9 +37,7 @@ export default async function StepPage({ searchParams, }: PageArgs) { const { lang } = params - if (!searchParams.hotel) { - redirect(`/${lang}`) - } + void getBreakfastPackages(searchParams.hotel) const intl = await getIntl() @@ -62,24 +61,37 @@ export default async function StepPage({ rateCode }) - const hotelData = await getHotelData({ - hotelId, - language: lang, - include: [HotelIncludeEnum.RoomCategories], - }) - const user = await getProfileSafely() - const savedCreditCards = await getCreditCardsSafely() - const breakfastPackages = await getBreakfastPackages(searchParams.hotel) + if (!rateCode || !roomTypeCode) { + return notFound() + } - const roomAvailability = await getRoomAvailability({ - hotelId: parseInt(hotelId), - adults, - children, - roomStayStartDate: fromDate, - roomStayEndDate: toDate, - rateCode - }) + const [ + hotelData, + user, + savedCreditCards, + breakfastPackages, + roomAvailability, + ] = await Promise.all([ + getHotelData({ + hotelId, + language: lang, + include: [HotelIncludeEnum.RoomCategories], + }), + + getProfileSafely(), + getCreditCardsSafely(), + getBreakfastPackages(searchParams.hotel), + getSelectedRoomAvailability({ + hotelId: parseInt(searchParams.hotel), + adults, + children, + roomStayStartDate: fromDate, + roomStayEndDate: toDate, + rateCode, + roomTypeCode, + }), + ]) if (!isValidStep(params.step) || !hotelData || !roomAvailability) { return notFound() @@ -100,10 +112,8 @@ export default async function StepPage({ id: "Select payment method", }) - const availableRoom = roomAvailability?.roomConfigurations - .filter((room) => room.status === "Available") - .find((room) => room.roomTypeCode === roomTypeCode)?.roomType - const roomTypes = hotelData.included + const availableRoom = roomAvailability.selectedRoom?.roomType + const bedTypes = hotelData.included ?.find((room) => room.name === availableRoom) ?.roomTypes.map((room) => ({ description: room.mainBed.description, @@ -116,13 +126,13 @@ export default async function StepPage({ {/* TODO: How to handle no beds found? */} - {roomTypes ? ( + {bedTypes ? ( - + ) : null} diff --git a/components/HotelReservation/EnterDetails/BedType/index.tsx b/components/HotelReservation/EnterDetails/BedType/index.tsx index 646ec1311..f822024c1 100644 --- a/components/HotelReservation/EnterDetails/BedType/index.tsx +++ b/components/HotelReservation/EnterDetails/BedType/index.tsx @@ -14,28 +14,20 @@ import { bedTypeSchema } from "./schema" import styles from "./bedOptions.module.css" -import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" +import type { + BedTypeProps, + BedTypeSchema, +} from "@/types/components/hotelReservation/enterDetails/bedType" -export default function BedType({ - roomTypes, -}: { - roomTypes: { - description: string - size: { - min: number - max: number - } - value: string - }[] -}) { +export default function BedType({ bedTypes }: BedTypeProps) { const intl = useIntl() const bedType = useEnterDetailsStore((state) => state.userData.bedType) const methods = useForm({ defaultValues: bedType ? { - bedType, - } + bedType, + } : undefined, criteriaMode: "all", mode: "all", @@ -64,7 +56,7 @@ export default function BedType({ return (
- {roomTypes.map((roomType) => { + {bedTypes.map((roomType) => { const width = roomType.size.max === roomType.size.min ? `${roomType.size.min} cm` diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index 094c9e860..84c6280af 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -13,10 +13,12 @@ import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useLang from "@/hooks/useLang" +import { formatNumber } from "@/utils/format" import styles from "./summary.module.css" import { RoomsData } from "@/types/components/hotelReservation/enterDetails/bookingData" +import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast" import { BreakfastPackageEnum } from "@/types/enums/breakfast" export default function Summary({ @@ -27,15 +29,15 @@ export default function Summary({ room: RoomsData }) { const [chosenBed, setChosenBed] = useState() - const [chosenBreakfast, setCosenBreakfast] = useState() + const [chosenBreakfast, setChosenBreakfast] = useState< + BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST + >() const intl = useIntl() const lang = useLang() const { fromDate, toDate, bedType, breakfast } = useEnterDetailsStore( (state) => ({ fromDate: state.roomData.fromDate, toDate: state.roomData.toDate, - rooms: state.roomData.room, - hotel: state.roomData.hotel, bedType: state.userData.bedType, breakfast: state.userData.breakfast, }) @@ -55,10 +57,9 @@ export default function Summary({ useEffect(() => { setChosenBed(bedType) - if (breakfast === BreakfastPackageEnum.NO_BREAKFAST) { - setCosenBreakfast("No breakfast") - } else if (breakfast) { - setCosenBreakfast("Breakfast buffet") + + if (breakfast) { + setChosenBreakfast(breakfast) } }, [bedType, breakfast]) @@ -80,7 +81,10 @@ export default function Summary({ {intl.formatMessage( { id: "{amount} {currency}" }, - { amount: room.localPrice, currency: "SEK" } + { + amount: formatNumber(parseInt(room.localPrice.price ?? "0")), + currency: room.localPrice.currency, + } )} @@ -118,24 +122,41 @@ export default function Summary({ {intl.formatMessage( { id: "{amount} {currency}" }, - { amount: "0", currency: "SEK" } + { amount: "0", currency: room.localPrice.currency } )} ) : null} {chosenBreakfast ? ( -
- - {intl.formatMessage({ id: chosenBreakfast })} - - - {intl.formatMessage( - { id: "{amount} {currency}" }, - { amount: "0", currency: "SEK" } - )} - -
+ chosenBreakfast === BreakfastPackageEnum.NO_BREAKFAST ? ( +
+ + {intl.formatMessage({ id: "No breakfast" })} + + + {intl.formatMessage( + { id: "{amount} {currency}" }, + { amount: "0", currency: room.localPrice.currency } + )} + +
+ ) : ( +
+ + {intl.formatMessage({ id: "Breakfast buffet" })} + + + {intl.formatMessage( + { id: "{amount} {currency}" }, + { + amount: chosenBreakfast.totalPrice, + currency: chosenBreakfast.currency, + } + )} + +
+ ) ) : null} @@ -156,14 +177,20 @@ export default function Summary({ {intl.formatMessage( { id: "{amount} {currency}" }, - { amount: room.localPrice, currency: "SEK" } // TODO: calculate total price + { + amount: formatNumber(parseInt(room.localPrice.price ?? "0")), + currency: room.localPrice.currency, + } )} {intl.formatMessage({ id: "Approx." })}{" "} {intl.formatMessage( { id: "{amount} {currency}" }, - { amount: room.euroPrice, currency: "EUR" } + { + amount: formatNumber(parseInt(room.euroPrice.price ?? "0")), + currency: room.euroPrice.currency, + } )} diff --git a/components/TempDesignSystem/Form/ChoiceCard/_Card/card.ts b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.ts index 8fe8476d3..7d24e46d7 100644 --- a/components/TempDesignSystem/Form/ChoiceCard/_Card/card.ts +++ b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.ts @@ -23,10 +23,15 @@ interface ListCardProps extends BaseCardProps { interface TextCardProps extends BaseCardProps { list?: never - text?: React.ReactNode + text: React.ReactNode } -export type CardProps = ListCardProps | TextCardProps +interface CleanCardProps extends BaseCardProps { + list?: never + text?: never +} + +export type CardProps = ListCardProps | TextCardProps | CleanCardProps export type CheckboxProps = | Omit @@ -34,6 +39,7 @@ export type CheckboxProps = export type RadioProps = | Omit | Omit + | Omit export interface ListProps extends Pick { list?: ListCardProps["list"] diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index 547ff7874..63cde2d84 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -3,6 +3,7 @@ import { cache } from "react" import { Lang } from "@/constants/languages" import { GetRoomsAvailabilityInput, + GetSelectedRoomAvailabilityInput, HotelIncludeEnum, } from "@/server/routers/hotels/input" @@ -95,6 +96,14 @@ export const getRoomAvailability = cache( } ) +export const getSelectedRoomAvailability = cache( + async function getMemoizedRoomAvailability( + args: GetSelectedRoomAvailabilityInput + ) { + return serverClient().hotel.availability.room(args) + } +) + export const getFooter = cache(async function getMemoizedFooter() { return serverClient().contentstack.base.footer() }) diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index ff8368b2a..bfff4cd97 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -29,6 +29,23 @@ export const getRoomsAvailabilityInputSchema = z.object({ rateCode: z.string().optional(), }) +export const getSelectedRoomAvailabilityInputSchema = z.object({ + hotelId: z.number(), + roomStayStartDate: z.string(), + roomStayEndDate: z.string(), + adults: z.number(), + children: z.string().optional(), + promotionCode: z.string().optional(), + reservationProfileType: z.string().optional().default(""), + attachedProfileId: z.string().optional().default(""), + rateCode: z.string(), + roomTypeCode: z.string(), +}) + +export type GetSelectedRoomAvailabilityInput = z.input< + typeof getSelectedRoomAvailabilityInputSchema +> + export type GetRoomsAvailabilityInput = z.input< typeof getRoomsAvailabilityInputSchema > diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 4aacd6ff2..b4dc240a5 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -37,6 +37,7 @@ import { getHotelsAvailabilityInputSchema, getRatesInputSchema, getRoomsAvailabilityInputSchema, + getSelectedRoomAvailabilityInputSchema, } from "./input" import { breakfastPackagesSchema, @@ -93,6 +94,16 @@ const roomsAvailabilityFailCounter = meter.createCounter( "trpc.hotel.availability.rooms-fail" ) +const selectedRoomAvailabilityCounter = meter.createCounter( + "trpc.hotel.availability.room" +) +const selectedRoomAvailabilitySuccessCounter = meter.createCounter( + "trpc.hotel.availability.room-success" +) +const selectedRoomAvailabilityFailCounter = meter.createCounter( + "trpc.hotel.availability.room-fail" +) + const breakfastPackagesCounter = meter.createCounter("trpc.package.breakfast") const breakfastPackagesSuccessCounter = meter.createCounter( "trpc.package.breakfast-success" @@ -545,6 +556,161 @@ export const hotelQueryRouter = router({ return validateAvailabilityData.data }), + room: serviceProcedure + .input(getSelectedRoomAvailabilityInputSchema) + .query(async ({ input, ctx }) => { + const { + hotelId, + roomStayStartDate, + roomStayEndDate, + adults, + children, + promotionCode, + reservationProfileType, + attachedProfileId, + rateCode, + roomTypeCode, + } = input + + const params: Record = { + roomStayStartDate, + roomStayEndDate, + adults, + ...(children && { children }), + promotionCode, + reservationProfileType, + attachedProfileId, + } + + selectedRoomAvailabilityCounter.add(1, { + hotelId, + roomStayStartDate, + roomStayEndDate, + adults, + children, + promotionCode, + reservationProfileType, + }) + console.info( + "api.hotels.selectedRoomAvailability start", + JSON.stringify({ query: { hotelId, params } }) + ) + const apiResponseAvailability = await api.get( + api.endpoints.v1.Availability.hotel(hotelId.toString()), + { + headers: { + Authorization: `Bearer ${ctx.serviceToken}`, + }, + }, + params + ) + + if (!apiResponseAvailability.ok) { + const text = await apiResponseAvailability.text() + selectedRoomAvailabilityFailCounter.add(1, { + hotelId, + roomStayStartDate, + roomStayEndDate, + adults, + children, + promotionCode, + reservationProfileType, + error_type: "http_error", + error: JSON.stringify({ + status: apiResponseAvailability.status, + statusText: apiResponseAvailability.statusText, + text, + }), + }) + console.error( + "api.hotels.selectedRoomAvailability error", + JSON.stringify({ + query: { hotelId, params }, + error: { + status: apiResponseAvailability.status, + statusText: apiResponseAvailability.statusText, + text, + }, + }) + ) + return null + } + const apiJsonAvailability = await apiResponseAvailability.json() + const validateAvailabilityData = + getRoomsAvailabilitySchema.safeParse(apiJsonAvailability) + if (!validateAvailabilityData.success) { + selectedRoomAvailabilityFailCounter.add(1, { + hotelId, + roomStayStartDate, + roomStayEndDate, + adults, + children, + promotionCode, + reservationProfileType, + error_type: "validation_error", + error: JSON.stringify(validateAvailabilityData.error), + }) + console.error( + "api.hotels.selectedRoomAvailability validation error", + JSON.stringify({ + query: { hotelId, params }, + error: validateAvailabilityData.error, + }) + ) + throw badRequestError() + } + + const selectedRoom = validateAvailabilityData.data.roomConfigurations + .filter((room) => room.status === "Available") + .find((room) => room.roomTypeCode === roomTypeCode) + + if (!selectedRoom) { + console.error("No matching room found") + return null + } + + const memberRate = selectedRoom.products.find( + (rate) => rate.productType.member?.rateCode === rateCode + )?.productType.member + + const publicRate = selectedRoom.products.find( + (rate) => rate.productType.public?.rateCode === rateCode + )?.productType.public + + const mustBeGuaranteed = + validateAvailabilityData.data.rateDefinitions.filter( + (rate) => rate.rateCode === rateCode + )[0].mustBeGuaranteed + + const cancellationText = + validateAvailabilityData.data.rateDefinitions.find( + (rate) => rate.rateCode === rateCode + )?.cancellationText ?? "" + + selectedRoomAvailabilitySuccessCounter.add(1, { + hotelId, + roomStayStartDate, + roomStayEndDate, + adults, + children, + promotionCode, + reservationProfileType, + }) + console.info( + "api.hotels.selectedRoomAvailability success", + JSON.stringify({ + query: { hotelId, params: params }, + }) + ) + + return { + selectedRoom, + mustBeGuaranteed, + cancellationText, + memberRate, + publicRate, + } + }), }), rates: router({ get: publicProcedure diff --git a/types/components/hotelReservation/enterDetails/bedType.ts b/types/components/hotelReservation/enterDetails/bedType.ts index c4e6e4ff0..35f41ee27 100644 --- a/types/components/hotelReservation/enterDetails/bedType.ts +++ b/types/components/hotelReservation/enterDetails/bedType.ts @@ -2,4 +2,16 @@ import { z } from "zod" import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema" +type BedType = { + description: string + size: { + min: number + max: number + } + value: string +} +export type BedTypeProps = { + bedTypes: BedType[] +} + export interface BedTypeSchema extends z.output {} diff --git a/types/components/hotelReservation/enterDetails/bookingData.ts b/types/components/hotelReservation/enterDetails/bookingData.ts index 058d67990..249bb466d 100644 --- a/types/components/hotelReservation/enterDetails/bookingData.ts +++ b/types/components/hotelReservation/enterDetails/bookingData.ts @@ -16,10 +16,15 @@ export interface BookingData { room: Room[] } +type Price = { + price?: string + currency?: string +} + export type RoomsData = { roomType: string - localPrice: string - euroPrice: string + localPrice: Price + euroPrice: Price adults: number children?: Child[] cancellationText: string From 317619ea78b09964099a2d23aedb49d8f888ee0e Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Wed, 30 Oct 2024 16:39:04 +0100 Subject: [PATCH 007/120] fix: preload requests to avoid needing to promise.all --- .../(standard)/[step]/@summary/page.tsx | 22 +++---- .../(standard)/[step]/page.tsx | 61 ++++++++----------- 2 files changed, 36 insertions(+), 47 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx index f1e1897e1..38444447f 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx @@ -24,18 +24,16 @@ export default async function SummaryPage({ return notFound() } - const [user, availability] = await Promise.all([ - getProfileSafely(), - getSelectedRoomAvailability({ - hotelId: parseInt(hotel), - adults, - children, - roomStayStartDate: fromDate, - roomStayEndDate: toDate, - rateCode, - roomTypeCode, - }), - ]) + const availability = await getSelectedRoomAvailability({ + hotelId: parseInt(hotel), + adults, + children, + roomStayStartDate: fromDate, + roomStayEndDate: toDate, + rateCode, + roomTypeCode, + }) + const user = await getProfileSafely() if (!availability) { console.error("No hotel or availability data", availability) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index dc07ed3f0..e5bf9f73b 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -52,46 +52,37 @@ export default async function StepPage({ toDate, } = getQueryParamsForEnterDetails(selectRoomParams) - void getRoomAvailability({ - hotelId: parseInt(hotelId), - adults, - children, - roomStayStartDate: fromDate, - roomStayEndDate: toDate, - rateCode - }) - - if (!rateCode || !roomTypeCode) { return notFound() } - const [ - hotelData, - user, - savedCreditCards, - breakfastPackages, - roomAvailability, - ] = await Promise.all([ - getHotelData({ - hotelId, - language: lang, - include: [HotelIncludeEnum.RoomCategories], - }), + void getSelectedRoomAvailability({ + hotelId: parseInt(searchParams.hotel), + adults, + children, + roomStayStartDate: fromDate, + roomStayEndDate: toDate, + rateCode, + roomTypeCode, + }) - getProfileSafely(), - getCreditCardsSafely(), - getBreakfastPackages(searchParams.hotel), - getSelectedRoomAvailability({ - hotelId: parseInt(searchParams.hotel), - adults, - children, - roomStayStartDate: fromDate, - roomStayEndDate: toDate, - rateCode, - roomTypeCode, - }), - ]) + const hotelData = await getHotelData({ + hotelId, + language: lang, + include: [HotelIncludeEnum.RoomCategories], + }) + const roomAvailability = await getSelectedRoomAvailability({ + hotelId: parseInt(searchParams.hotel), + adults, + children, + roomStayStartDate: fromDate, + roomStayEndDate: toDate, + rateCode, + roomTypeCode, + }) + const breakfastPackages = await getBreakfastPackages(searchParams.hotel) + const user = await getProfileSafely() + const savedCreditCards = await getCreditCardsSafely() if (!isValidStep(params.step) || !hotelData || !roomAvailability) { return notFound() From 1d244b285a78d8a8b1aed92805c0561119e904aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matilda=20Landstr=C3=B6m?= Date: Wed, 30 Oct 2024 15:08:47 +0100 Subject: [PATCH 008/120] fix(SW-727): remove 'test' from matcher --- middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middleware.ts b/middleware.ts index 489d30503..1f1c36a0b 100644 --- a/middleware.ts +++ b/middleware.ts @@ -116,5 +116,5 @@ export const config = { * public routes inside middleware. * (https://clerk.com/docs/quickstarts/nextjs?utm_source=sponsorship&utm_medium=youtube&utm_campaign=code-with-antonio&utm_content=12-31-2023#add-authentication-to-your-app) */ - matcher: ["/((?!.+\\.[\\w]+$|_next|_static|.netlify|en/test|api|trpc).*)"], + matcher: ["/((?!.+\\.[\\w]+$|_next|_static|.netlify|api|trpc).*)"], } From b20731e0965bb695948dc1b4f2bb5c37b01ae9d7 Mon Sep 17 00:00:00 2001 From: Michael Zetterberg Date: Wed, 30 Oct 2024 16:58:08 +0100 Subject: [PATCH 009/120] fix: remove loading.tsx from webviews because token refresh strategy does not work with it --- app/[lang]/webview/loading.tsx | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 app/[lang]/webview/loading.tsx diff --git a/app/[lang]/webview/loading.tsx b/app/[lang]/webview/loading.tsx deleted file mode 100644 index c739b6635..000000000 --- a/app/[lang]/webview/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import LoadingSpinner from "@/components/LoadingSpinner" - -export default function Loading() { - return -} From 4529fc7f51d8a4a3e4ee02156b61f28c7ed605fd Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Thu, 31 Oct 2024 08:23:20 +0100 Subject: [PATCH 010/120] Image loader exclude blocked domains --- components/Image.tsx | 19 ++++++++++++++++++- next.config.js | 4 ---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/components/Image.tsx b/components/Image.tsx index a5ea6e16a..44c2ca662 100644 --- a/components/Image.tsx +++ b/components/Image.tsx @@ -7,12 +7,29 @@ import type { CSSProperties } from "react" import type { ImageProps } from "@/types/components/image" +const isBlockedByFirewall = (src: string) => { + if ( + src.includes("test.scandichotels.com") || + src.includes("test2.scandichotels.com") || + src.includes("test3.scandichotels.com") || + src.includes("stage.scandichotels.com") || + src.includes("prod.scandichotels.com") + ) { + return true + } + return false +} + function imageLoader({ quality, src, width }: ImageLoaderProps) { const isAbsoluteUrl = src.startsWith("https://") || src.startsWith("http://") const hasQS = src.indexOf("?") !== -1 if (isAbsoluteUrl) { - return `https://image-scandic-hotels.netlify.app/.netlify/images?url=${src}&w=${width}${quality ? "&q=" + quality : ""}` + if (isBlockedByFirewall(src)) { + return `${src}${hasQS ? "&" : "?"}w=${width}${quality ? "&q=" + quality : ""}` + } else { + return `https://img.scandichotels.com/.netlify/images?url=${src}&w=${width}${quality ? "&q=" + quality : ""}` + } } return `${src}${hasQS ? "&" : "?"}w=${width}${quality ? "&q=" + quality : ""}` diff --git a/next.config.js b/next.config.js index ba16f5da6..f91af6594 100644 --- a/next.config.js +++ b/next.config.js @@ -43,10 +43,6 @@ const nextConfig = { protocol: "https", hostname: "*.scandichotels.com", }, - { - protocol: "https", - hostname: "image-scandic-hotels.netlify.app", - }, ], }, From 3b75231eeefc78fe4f4ed5d91e24f3ee704d82af Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Thu, 24 Oct 2024 09:32:47 +0200 Subject: [PATCH 011/120] fix(SW-608): fixed positioning of dynamic map --- .../Map/DynamicMap/Sidebar/index.tsx | 124 +++++++++--------- .../Map/DynamicMap/Sidebar/sidebar.module.css | 95 ++++++++------ .../Map/DynamicMap/dynamicMap.module.css | 21 +-- .../HotelPage/Map/DynamicMap/index.tsx | 94 ++++++++----- .../MobileMapToggle/mobileToggle.module.css | 2 +- .../PreviewImages/previewImages.module.css | 1 + .../TabNavigation/tabNavigation.module.css | 2 +- .../HotelPage/hotelPage.module.css | 4 + 8 files changed, 201 insertions(+), 142 deletions(-) diff --git a/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx index 5ed761081..e73c2c6b4 100644 --- a/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx +++ b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx @@ -78,66 +78,72 @@ export default function Sidebar({ } return ( - + {poisInGroups.map(({ group, pois }) => + pois.length ? ( +
+ +

+ + {intl.formatMessage({ id: group })} +

+ +
    + {pois.map((poi) => ( +
  • + +
  • + ))} +
+
+ ) : null + )} + + +
+ ) } diff --git a/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/sidebar.module.css b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/sidebar.module.css index 11f26b822..4bab124e7 100644 --- a/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/sidebar.module.css +++ b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/sidebar.module.css @@ -1,50 +1,12 @@ .sidebar { - --sidebar-max-width: 26.25rem; - --sidebar-mobile-toggle-height: 91px; - --sidebar-mobile-fullscreen-height: calc( - 100vh - var(--main-menu-mobile-height) - var(--sidebar-mobile-toggle-height) - ); - - position: absolute; - top: var(--sidebar-mobile-fullscreen-height); - height: 100%; - right: 0; - left: 0; background-color: var(--Base-Surface-Primary-light-Normal); - z-index: 1; - transition: top 0.3s; -} - -.sidebar:not(.fullscreen) { - border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; -} - -.sidebar.fullscreen { - top: 0; -} - -.sidebarToggle { - position: relative; - margin: var(--Spacing-x4) 0 var(--Spacing-x2); - width: 100%; -} - -.sidebarToggle::before { - content: ""; - position: absolute; - display: block; - top: -0.5rem; - width: 100px; - height: 3px; - background-color: var(--UI-Text-High-contrast); + z-index: 2; } .sidebarContent { display: grid; gap: var(--Spacing-x5); align-content: start; - padding: var(--Spacing-x3) var(--Spacing-x2); - height: var(--sidebar-mobile-fullscreen-height); overflow-y: auto; } @@ -90,12 +52,65 @@ background-color: var(--Base-Surface-Primary-light-Hover); } +@media screen and (max-width: 767px) { + .sidebar { + --sidebar-mobile-toggle-height: 84px; + --sidebar-mobile-top-space: 40px; + --sidebar-mobile-content-height: calc( + var(--hotel-map-height) - var(--sidebar-mobile-toggle-height) - + var(--sidebar-mobile-top-space) + ); + + position: absolute; + bottom: calc(-1 * var(--sidebar-mobile-content-height)); + width: 100%; + transition: + bottom 0.3s, + top 0.3s; + border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; + } + + .sidebar.fullscreen + .backdrop { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 100%; + background-color: rgba(0, 0, 0, 0.4); + z-index: 1; + } + + .sidebar.fullscreen { + bottom: 0; + } + + .sidebarToggle { + position: relative; + margin-top: var(--Spacing-x4); + } + + .sidebarToggle::before { + content: ""; + position: absolute; + display: block; + top: -0.5rem; + width: 100px; + height: 3px; + background-color: var(--UI-Text-High-contrast); + } + + .sidebarContent { + padding: var(--Spacing-x3) var(--Spacing-x2); + height: var(--sidebar-mobile-content-height); + } +} + @media screen and (min-width: 768px) { .sidebar { position: static; width: 40vw; min-width: 10rem; - max-width: var(--sidebar-max-width); + max-width: 26.25rem; background-color: var(--Base-Surface-Primary-light-Normal); } diff --git a/components/ContentType/HotelPage/Map/DynamicMap/dynamicMap.module.css b/components/ContentType/HotelPage/Map/DynamicMap/dynamicMap.module.css index 32df5b502..7cdd02371 100644 --- a/components/ContentType/HotelPage/Map/DynamicMap/dynamicMap.module.css +++ b/components/ContentType/HotelPage/Map/DynamicMap/dynamicMap.module.css @@ -1,18 +1,19 @@ .dynamicMap { - position: fixed; - top: var(--main-menu-mobile-height); - right: 0; - bottom: 0; + --hotel-map-height: 100dvh; + + position: absolute; + top: 0; left: 0; - z-index: var(--dialog-z-index); + height: var(--hotel-map-height); + width: 100dvw; + z-index: var(--hotel-dynamic-map-z-index); display: flex; background-color: var(--Base-Surface-Primary-light-Normal); } - -@media screen and (min-width: 768px) { - .dynamicMap { - top: var(--main-menu-desktop-height); - } +.wrapper { + position: absolute; + top: 0; + left: 0; } .closeButton { diff --git a/components/ContentType/HotelPage/Map/DynamicMap/index.tsx b/components/ContentType/HotelPage/Map/DynamicMap/index.tsx index f539d7614..969b24588 100644 --- a/components/ContentType/HotelPage/Map/DynamicMap/index.tsx +++ b/components/ContentType/HotelPage/Map/DynamicMap/index.tsx @@ -1,6 +1,6 @@ "use client" import { APIProvider } from "@vis.gl/react-google-maps" -import { useEffect, useRef, useState } from "react" +import { useCallback, useEffect, useRef, useState } from "react" import { Dialog, Modal } from "react-aria-components" import { useIntl } from "react-intl" @@ -10,6 +10,7 @@ import CloseLargeIcon from "@/components/Icons/CloseLarge" import InteractiveMap from "@/components/Maps/InteractiveMap" import Button from "@/components/TempDesignSystem/Button" import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" +import { debounce } from "@/utils/debounce" import Sidebar from "./Sidebar" @@ -25,9 +26,10 @@ export default function DynamicMap({ mapId, }: DynamicMapProps) { const intl = useIntl() + const rootDiv = useRef(null) + const [mapHeight, setMapHeight] = useState("0px") const { isDynamicMapOpen, closeDynamicMap } = useHotelPageStore() const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0) - const hasMounted = useRef(false) const [activePoi, setActivePoi] = useState(null) useHandleKeyUp((event: KeyboardEvent) => { @@ -36,23 +38,47 @@ export default function DynamicMap({ } }) - // Making sure the map is always opened at the top of the page, just below the header. + // Calculate the height of the map based on the viewport height from the start-point (below the header and booking widget) + const handleMapHeight = useCallback(() => { + const topPosition = rootDiv.current?.getBoundingClientRect().top ?? 0 + const scrollY = window.scrollY + setMapHeight(`calc(100dvh - ${topPosition + scrollY}px)`) + }, []) + + // Making sure the map is always opened at the top of the page, + // just below the header and booking widget as these should stay visible. // When closing, the page should scroll back to the position it was before opening the map. useEffect(() => { // Skip the first render - if (!hasMounted.current) { - hasMounted.current = true + if (!rootDiv.current) { return } if (isDynamicMapOpen && scrollHeightWhenOpened === 0) { - setScrollHeightWhenOpened(window.scrollY) + const scrollY = window.scrollY + setScrollHeightWhenOpened(scrollY) window.scrollTo({ top: 0, behavior: "instant" }) } else if (!isDynamicMapOpen && scrollHeightWhenOpened !== 0) { window.scrollTo({ top: scrollHeightWhenOpened, behavior: "instant" }) setScrollHeightWhenOpened(0) } - }, [isDynamicMapOpen, scrollHeightWhenOpened]) + }, [isDynamicMapOpen, scrollHeightWhenOpened, rootDiv]) + + useEffect(() => { + const debouncedResizeHandler = debounce(function () { + handleMapHeight() + }) + + const observer = new ResizeObserver(debouncedResizeHandler) + + observer.observe(document.documentElement) + + return () => { + if (observer) { + observer.unobserve(document.documentElement) + } + } + }, [rootDiv, isDynamicMapOpen, handleMapHeight]) const closeButton = (
) } diff --git a/components/ContentType/HotelPage/Map/MobileMapToggle/mobileToggle.module.css b/components/ContentType/HotelPage/Map/MobileMapToggle/mobileToggle.module.css index e5bb3b75b..b2edd97a0 100644 --- a/components/ContentType/HotelPage/Map/MobileMapToggle/mobileToggle.module.css +++ b/components/ContentType/HotelPage/Map/MobileMapToggle/mobileToggle.module.css @@ -1,7 +1,7 @@ .mobileToggle { position: sticky; bottom: var(--Spacing-x5); - z-index: 1; + z-index: var(--hotel-mobile-map-toggle-button-z-index); margin: 0 auto; display: grid; grid-template-columns: repeat(2, 1fr); diff --git a/components/ContentType/HotelPage/PreviewImages/previewImages.module.css b/components/ContentType/HotelPage/PreviewImages/previewImages.module.css index 7a1818557..064d99dce 100644 --- a/components/ContentType/HotelPage/PreviewImages/previewImages.module.css +++ b/components/ContentType/HotelPage/PreviewImages/previewImages.module.css @@ -4,6 +4,7 @@ position: relative; width: 100%; padding: var(--Spacing-x2) var(--Spacing-x2) 0; + z-index: 0; } .image { diff --git a/components/ContentType/HotelPage/TabNavigation/tabNavigation.module.css b/components/ContentType/HotelPage/TabNavigation/tabNavigation.module.css index 702f7d1fc..4c8cbaca1 100644 --- a/components/ContentType/HotelPage/TabNavigation/tabNavigation.module.css +++ b/components/ContentType/HotelPage/TabNavigation/tabNavigation.module.css @@ -1,7 +1,7 @@ .stickyWrapper { position: sticky; top: var(--booking-widget-mobile-height); - z-index: 2; + z-index: var(--hotel-tab-navigation-z-index); background-color: var(--Base-Surface-Subtle-Normal); border-bottom: 1px solid var(--Base-Border-Subtle); overflow-x: auto; diff --git a/components/ContentType/HotelPage/hotelPage.module.css b/components/ContentType/HotelPage/hotelPage.module.css index 091444fe2..87d0e8a3f 100644 --- a/components/ContentType/HotelPage/hotelPage.module.css +++ b/components/ContentType/HotelPage/hotelPage.module.css @@ -1,4 +1,8 @@ .pageContainer { + --hotel-tab-navigation-z-index: 2; + --hotel-mobile-map-toggle-button-z-index: 1; + --hotel-dynamic-map-z-index: 2; + --hotel-page-navigation-height: 59px; --hotel-page-scroll-margin-top: calc( var(--hotel-page-navigation-height) + var(--Spacing-x2) From 105e730e893f32afa86a9f16ce779ee298ef2a7f Mon Sep 17 00:00:00 2001 From: Bianca Widstam Date: Thu, 31 Oct 2024 13:38:43 +0100 Subject: [PATCH 012/120] fix(SW-712): remove hotelFacts.hotelFacilityDetail, hotelFacts.hotelInformation and use detailedFacilities --- .../HotelReservation/ReadMore/index.tsx | 21 ++++--------- server/routers/hotels/output.ts | 30 ------------------- .../selectHotel/selectHotel.ts | 6 ---- 3 files changed, 6 insertions(+), 51 deletions(-) diff --git a/components/HotelReservation/ReadMore/index.tsx b/components/HotelReservation/ReadMore/index.tsx index c8d39fcbe..7efa4f4e8 100644 --- a/components/HotelReservation/ReadMore/index.tsx +++ b/components/HotelReservation/ReadMore/index.tsx @@ -16,22 +16,17 @@ import Contact from "../Contact" import styles from "./readMore.module.css" import { - DetailedAmenity, ParkingProps, ReadMoreProps, } from "@/types/components/hotelReservation/selectHotel/selectHotel" -import { Hotel } from "@/types/hotel" +import type { Amenities,Hotel } from "@/types/hotel" function getAmenitiesList(hotel: Hotel) { - const detailedAmenities: DetailedAmenity[] = Object.entries( - hotel.hotelFacts.hotelFacilityDetail - ).map(([key, value]) => ({ name: key, ...value })) - - // Remove Parking facilities since parking accordion is based on hotel.parking - const simpleAmenities = hotel.detailedFacilities.filter( - (facility) => !facility.name.startsWith("Parking") + const detailedAmenities: Amenities = hotel.detailedFacilities.filter( + // Remove Parking facilities since parking accordion is based on hotel.parking + (facility) => !facility.name.startsWith("Parking") && facility.public ) - return [...detailedAmenities, ...simpleAmenities] + return detailedAmenities } export default function ReadMore({ label, hotel, hotelId }: ReadMoreProps) { @@ -80,11 +75,7 @@ export default function ReadMore({ label, hotel, hotelId }: ReadMoreProps) { TODO: What content should be in the accessibility section? {amenitiesList.map((amenity) => { - return "description" in amenity ? ( - - {amenity.description} - - ) : ( + return (
{amenity.name}
diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index b69207a7d..1b49b5fbf 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -71,34 +71,6 @@ const ecoLabelsSchema = z.object({ svanenEcoLabelCertificateNumber: z.string().optional(), }) -const hotelFacilityDetailSchema = z.object({ - heading: z.string(), - description: z.string(), -}) - -const hotelFacilitySchema = z.object({ - breakfast: hotelFacilityDetailSchema, - checkout: hotelFacilityDetailSchema, - gym: hotelFacilityDetailSchema, - internet: hotelFacilityDetailSchema, - laundry: hotelFacilityDetailSchema, - luggage: hotelFacilityDetailSchema, - shop: hotelFacilityDetailSchema, - telephone: hotelFacilityDetailSchema, -}) - -const hotelInformationDetailSchema = z.object({ - heading: z.string(), - description: z.string(), - link: z.string().optional(), -}) - -const hotelInformationSchema = z.object({ - accessibility: hotelInformationDetailSchema, - safety: hotelInformationDetailSchema, - sustainability: hotelInformationDetailSchema, -}) - const interiorSchema = z.object({ numberOfBeds: z.number(), numberOfCribs: z.number(), @@ -423,8 +395,6 @@ export const getHotelDataSchema = z.object({ hotelFacts: z.object({ checkin: checkinSchema, ecoLabels: ecoLabelsSchema, - hotelFacilityDetail: hotelFacilitySchema, - hotelInformation: hotelInformationSchema, interior: interiorSchema, receptionHours: receptionHoursSchema, yearBuilt: z.string(), diff --git a/types/components/hotelReservation/selectHotel/selectHotel.ts b/types/components/hotelReservation/selectHotel/selectHotel.ts index b43779714..b50521040 100644 --- a/types/components/hotelReservation/selectHotel/selectHotel.ts +++ b/types/components/hotelReservation/selectHotel/selectHotel.ts @@ -5,12 +5,6 @@ export enum AvailabilityEnum { NotAvailable = "NotAvailable", } -export interface DetailedAmenity { - name: string - heading: string - description: string -} - export interface ReadMoreProps { label: string hotelId: string From 4513828ae7eb115212003917cf06b8547b28c60b Mon Sep 17 00:00:00 2001 From: Simon Emanuelsson Date: Thu, 31 Oct 2024 14:07:59 +0100 Subject: [PATCH 013/120] feat: block all entries to enter details that miss the required params --- .../(standard)/[step]/@summary/page.tsx | 5 - .../(standard)/[step]/page.tsx | 10 +- next.config.js | 196 +++++++++++++++--- .../hotelReservation/selectRate/selectRate.ts | 4 +- 4 files changed, 169 insertions(+), 46 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx index 38444447f..946aea241 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx @@ -19,11 +19,6 @@ export default async function SummaryPage({ const { hotel, adults, children, roomTypeCode, rateCode, fromDate, toDate } = getQueryParamsForEnterDetails(selectRoomParams) - if (!roomTypeCode || !rateCode) { - console.log("No roomTypeCode or rateCode") - return notFound() - } - const availability = await getSelectedRoomAvailability({ hotelId: parseInt(hotel), adults, diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index e5bf9f73b..ba4e44e34 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -1,11 +1,10 @@ -import { notFound, redirect } from "next/navigation" +import { notFound } from "next/navigation" import { getBreakfastPackages, getCreditCardsSafely, getHotelData, getProfileSafely, - getRoomAvailability, getSelectedRoomAvailability, } from "@/lib/trpc/memoizedRequests" import { HotelIncludeEnum } from "@/server/routers/hotels/input" @@ -38,8 +37,6 @@ export default async function StepPage({ }: PageArgs) { const { lang } = params - void getBreakfastPackages(searchParams.hotel) - const intl = await getIntl() const selectRoomParams = new URLSearchParams(searchParams) const { @@ -52,10 +49,7 @@ export default async function StepPage({ toDate, } = getQueryParamsForEnterDetails(selectRoomParams) - if (!rateCode || !roomTypeCode) { - return notFound() - } - + void getBreakfastPackages(searchParams.hotel) void getSelectedRoomAvailability({ hotelId: parseInt(searchParams.hotel), adults, diff --git a/next.config.js b/next.config.js index fae418d6c..2e8553f1c 100644 --- a/next.config.js +++ b/next.config.js @@ -72,38 +72,172 @@ const nextConfig = { // https://nextjs.org/docs/app/api-reference/next-config-js/redirects#header-cookie-and-query-matching redirects() { + // Param checks needs to be split as Next.js handles everything + // in the missing array as AND, therefore they need to be separate + // to handle when one or more are missing. + // + // Docs: all missing items must not match for the redirect to be applied. return [ - // { - // ---------------------------------------- - // Uncomment when Team Explorer has deployed - // the select room submission - // ---------------------------------------- - // source: "/:lang/hotelreservation/(select-bed|breakfast|details|payment)", - // destination: "/:lang/hotelreservation/select-rate", - // missing: [ - // { - // key: "hotel", - // type: "query", - // value: undefined, - // }, - // { - // key: "fromDate", - // type: "query", - // value: undefined, - // }, - // { - // key: "toDate", - // type: "query", - // value: undefined, - // }, - // { - // key: "room", - // type: "query", - // value: undefined, - // }, - // ], - // permanent: false, - // }, + { + // ---------------------------------------- + // hotel (hotelId) param missing + // ---------------------------------------- + source: + "/:lang/hotelreservation/(select-bed|breakfast|details|payment)", + destination: "/:lang/hotelreservation/select-rate", + missing: [ + { + key: "hotel", + type: "query", + value: undefined, + }, + ], + permanent: false, + }, + { + // ---------------------------------------- + // hotel (hotelId) param has to be an integer + // ---------------------------------------- + source: + "/:lang/hotelreservation/(select-bed|breakfast|details|payment)", + destination: "/:lang/hotelreservation/select-rate", + missing: [ + { + key: "hotel", + type: "query", + value: "^[0-9]+$", + }, + ], + permanent: false, + }, + { + // ---------------------------------------- + // fromDate param missing + // ---------------------------------------- + source: + "/:lang/hotelreservation/(select-bed|breakfast|details|payment)", + destination: "/:lang/hotelreservation/select-rate", + missing: [ + { + key: "fromDate", + type: "query", + value: undefined, + }, + ], + permanent: false, + }, + { + // ---------------------------------------- + // fromDate param has to be a date + // ---------------------------------------- + source: + "/:lang/hotelreservation/(select-bed|breakfast|details|payment)", + destination: "/:lang/hotelreservation/select-rate", + missing: [ + { + key: "fromDate", + type: "query", + value: "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$", + }, + ], + permanent: false, + }, + { + // ---------------------------------------- + // toDate param missing + // ---------------------------------------- + source: + "/:lang/hotelreservation/(select-bed|breakfast|details|payment)", + destination: "/:lang/hotelreservation/select-rate", + missing: [ + { + key: "toDate", + type: "query", + value: undefined, + }, + ], + permanent: false, + }, + { + // ---------------------------------------- + // toDate param has to be a date + // ---------------------------------------- + source: + "/:lang/hotelreservation/(select-bed|breakfast|details|payment)", + destination: "/:lang/hotelreservation/select-rate", + missing: [ + { + key: "toDate", + type: "query", + value: "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$", + }, + ], + permanent: false, + }, + { + // ---------------------------------------- + // room[0].adults param missing + // ---------------------------------------- + source: + "/:lang/hotelreservation/(select-bed|breakfast|details|payment)", + destination: "/:lang/hotelreservation/select-rate", + missing: [ + { + key: "room[0].adults", + type: "query", + value: undefined, + }, + ], + permanent: false, + }, + { + // ---------------------------------------- + // room[0].adults param has to be an integer + // ---------------------------------------- + source: + "/:lang/hotelreservation/(select-bed|breakfast|details|payment)", + destination: "/:lang/hotelreservation/select-rate", + missing: [ + { + key: "room[0].adults", + type: "query", + value: "^[0-9]+$", + }, + ], + permanent: false, + }, + { + // ---------------------------------------- + // room[0].ratecode param missing + // ---------------------------------------- + source: + "/:lang/hotelreservation/(select-bed|breakfast|details|payment)", + destination: "/:lang/hotelreservation/select-rate", + missing: [ + { + key: "room[0].ratecode", + type: "query", + value: undefined, + }, + ], + permanent: false, + }, + { + // ---------------------------------------- + // room[0].roomtype param missing + // ---------------------------------------- + source: + "/:lang/hotelreservation/(select-bed|breakfast|details|payment)", + destination: "/:lang/hotelreservation/select-rate", + missing: [ + { + key: "room[0].roomtype", + type: "query", + value: undefined, + }, + ], + permanent: false, + }, ] }, diff --git a/types/components/hotelReservation/selectRate/selectRate.ts b/types/components/hotelReservation/selectRate/selectRate.ts index fd1771955..7083afa6d 100644 --- a/types/components/hotelReservation/selectRate/selectRate.ts +++ b/types/components/hotelReservation/selectRate/selectRate.ts @@ -7,8 +7,8 @@ export interface Child { interface Room { adults: number - roomtype?: string - ratecode?: string + roomtype: string + ratecode: string counterratecode?: string child?: Child[] } From 48be8097296539e91671239487d18f458abae5bc Mon Sep 17 00:00:00 2001 From: Bianca Widstam Date: Fri, 1 Nov 2024 08:11:39 +0100 Subject: [PATCH 014/120] fix(SW-712): add missing translations --- components/HotelReservation/ReadMore/index.tsx | 4 ++-- i18n/dictionaries/da.json | 8 ++++++++ i18n/dictionaries/de.json | 8 ++++++++ i18n/dictionaries/en.json | 12 ++++++++++-- i18n/dictionaries/fi.json | 8 ++++++++ i18n/dictionaries/no.json | 14 +++++++++++--- i18n/dictionaries/sv.json | 14 +++++++++++--- 7 files changed, 58 insertions(+), 10 deletions(-) diff --git a/components/HotelReservation/ReadMore/index.tsx b/components/HotelReservation/ReadMore/index.tsx index 7efa4f4e8..9425b7c9f 100644 --- a/components/HotelReservation/ReadMore/index.tsx +++ b/components/HotelReservation/ReadMore/index.tsx @@ -19,7 +19,7 @@ import { ParkingProps, ReadMoreProps, } from "@/types/components/hotelReservation/selectHotel/selectHotel" -import type { Amenities,Hotel } from "@/types/hotel" +import type { Amenities, Hotel } from "@/types/hotel" function getAmenitiesList(hotel: Hotel) { const detailedAmenities: Amenities = hotel.detailedFacilities.filter( @@ -103,7 +103,7 @@ function Parking({ parking }: ParkingProps) {
  • {`${intl.formatMessage({ id: "Parking can be reserved in advance" })}: ${parking.canMakeReservation ? intl.formatMessage({ id: "Yes" }) : intl.formatMessage({ id: "No" })}`}
  • {`${intl.formatMessage({ id: "Number of parking spots" })}: ${parking.numberOfParkingSpots}`}
  • -
  • {`${intl.formatMessage({ id: "Distance to hotel" })}: ${parking.distanceToHotel}`}
  • +
  • {`${intl.formatMessage({ id: "Distance to hotel" })}: ${parking.distanceToHotel} m`}
  • {`${intl.formatMessage({ id: "Address" })}: ${parking.address}`}
  • diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 2cdcfa050..8a3bcc2d1 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -95,6 +95,7 @@ "Discard changes": "Kassér ændringer", "Discard unsaved changes?": "Slette ændringer, der ikke er gemt?", "Distance to city centre": "{number}km til centrum", + "Distance to hotel": "Afstand til hotel", "Do you want to start the day with Scandics famous breakfast buffé?": "Vil du starte dagen med Scandics berømte morgenbuffet?", "Done": "Færdig", "Download the Scandic app": "Download Scandic-appen", @@ -125,6 +126,7 @@ "Free cancellation": "Gratis afbestilling", "Free rebooking": "Gratis ombooking", "From": "Fra", + "Garage": "Garage", "Get inspired": "Bliv inspireret", "Get member benefits & offers": "Få medlemsfordele og tilbud", "Gift(s) added to your benefits": "{amount, plural, one {Gave} other {Gaver}} tilføjet til dine fordele", @@ -202,6 +204,7 @@ "New password": "Nyt kodeord", "Next": "Næste", "Nights needed to level up": "Nætter nødvendige for at komme i niveau", + "No": "Nej", "No breakfast": "Ingen morgenmad", "No content published": "Intet indhold offentliggjort", "No matching location found": "Der blev ikke fundet nogen matchende placering", @@ -213,6 +216,8 @@ "Nordic Swan Ecolabel": "Svanemærket", "Not found": "Ikke fundet", "Nr night, nr adult": "{nights, number} nat, {adults, number} voksen", + "Number of charging points for electric cars": "Antal ladepunkter til elbiler", + "Number of parking spots": "Antal parkeringspladser", "OTHER PAYMENT METHODS": "ANDRE BETALINGSMETODER", "On your journey": "På din rejse", "Open": "Åben", @@ -224,6 +229,7 @@ "PETR": "Kæledyr", "Parking": "Parkering", "Parking / Garage": "Parkering / Garage", + "Parking can be reserved in advance": "Parkering kan reserveres på forhånd", "Password": "Adgangskode", "Pay later": "Betal senere", "Pay now": "Betal nu", @@ -243,6 +249,7 @@ "Points may take up to 10 days to be displayed.": "Det kan tage op til 10 dage at få vist point.", "Points needed to level up": "Point nødvendige for at stige i niveau", "Points needed to stay on level": "Point nødvendige for at holde sig på niveau", + "Practical information": "Praktisk information", "Previous": "Forudgående", "Previous victories": "Tidligere sejre", "Price details": "Prisoplysninger", @@ -352,6 +359,7 @@ "Where to": "Hvor", "Which room class suits you the best?": "Hvilken rumklasse passer bedst til dig", "Year": "År", + "Yes": "Ja", "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Ja, jeg accepterer vilkårene for Scandic Friends og forstår, at Scandic vil behandle mine personlige oplysninger i henhold til", "Yes, discard changes": "Ja, kasser ændringer", "Yes, remove my card": "Ja, fjern mit kort", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index df92ae71e..1d389176e 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -95,6 +95,7 @@ "Discard changes": "Änderungen verwerfen", "Discard unsaved changes?": "Nicht gespeicherte Änderungen verwerfen?", "Distance to city centre": "{number}km zum Stadtzentrum", + "Distance to hotel": "Entfernung zum Hotel", "Do you want to start the day with Scandics famous breakfast buffé?": "Möchten Sie den Tag mit Scandics berühmtem Frühstücksbuffet beginnen?", "Done": "Fertig", "Download the Scandic app": "Laden Sie die Scandic-App herunter", @@ -125,6 +126,7 @@ "Free cancellation": "Kostenlose Stornierung", "Free rebooking": "Kostenlose Umbuchung", "From": "Fromm", + "Garage": "Garage", "Get inspired": "Lassen Sie sich inspieren", "Get member benefits & offers": "Holen Sie sich Vorteile und Angebote für Mitglieder", "Gift(s) added to your benefits": "{amount, plural, one {Geschenk zu Ihren Vorteilen hinzugefügt} other {Geschenke, die zu Ihren Vorteilen hinzugefügt werden}}", @@ -200,6 +202,7 @@ "New password": "Neues Kennwort", "Next": "Nächste", "Nights needed to level up": "Nächte, die zum Levelaufstieg benötigt werden", + "No": "Nein", "No breakfast": "Kein Frühstück", "No content published": "Kein Inhalt veröffentlicht", "No matching location found": "Kein passender Standort gefunden", @@ -211,6 +214,8 @@ "Nordic Swan Ecolabel": "Nordic Swan Ecolabel", "Not found": "Nicht gefunden", "Nr night, nr adult": "{nights, number} Nacht, {adults, number} Erwachsener", + "Number of charging points for electric cars": "Anzahl der Ladestationen für Elektroautos", + "Number of parking spots": "Anzahl der Parkplätze", "OTHER PAYMENT METHODS": "ANDERE BEZAHLMETHODE", "On your journey": "Auf deiner Reise", "Open": "Offen", @@ -222,6 +227,7 @@ "PETR": "Haustier", "Parking": "Parken", "Parking / Garage": "Parken / Garage", + "Parking can be reserved in advance": "Parkplätze können im Voraus reserviert werden", "Password": "Passwort", "Pay later": "Später bezahlen", "Pay now": "Jetzt bezahlen", @@ -241,6 +247,7 @@ "Points may take up to 10 days to be displayed.": "Es kann bis zu 10 Tage dauern, bis Punkte angezeigt werden.", "Points needed to level up": "Punkte, die zum Levelaufstieg benötigt werden", "Points needed to stay on level": "Erforderliche Punkte, um auf diesem Level zu bleiben", + "Practical information": "Praktische Informationen", "Previous": "Früher", "Previous victories": "Bisherige Siege", "Price details": "Preisdetails", @@ -351,6 +358,7 @@ "Where to": "Wohin", "Which room class suits you the best?": "Welche Zimmerklasse passt am besten zu Ihnen?", "Year": "Jahr", + "Yes": "Ja", "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Ja, ich akzeptiere die Geschäftsbedingungen für Scandic Friends und erkenne an, dass Scandic meine persönlichen Daten in Übereinstimmung mit", "Yes, discard changes": "Ja, Änderungen verwerfen", "Yes, remove my card": "Ja, meine Karte entfernen", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index eec758e7a..bc93f51ac 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -101,6 +101,7 @@ "Discard changes": "Discard changes", "Discard unsaved changes?": "Discard unsaved changes?", "Distance to city centre": "{number}km to city centre", + "Distance to hotel": "Distance to hotel", "Do you want to start the day with Scandics famous breakfast buffé?": "Do you want to start the day with Scandics famous breakfast buffé?", "Done": "Done", "Download invoice": "Download invoice", @@ -133,14 +134,15 @@ "Free rebooking": "Free rebooking", "Free until": "Free until", "From": "From", + "Garage": "Garage", "Get inspired": "Get inspired", "Get member benefits & offers": "Get member benefits & offers", "Gift(s) added to your benefits": "{amount, plural, one {Gift} other {Gifts}} added to your benefits", "Go back to edit": "Go back to edit", "Go back to overview": "Go back to overview", "Go to My Benefits": "Go to My Benefits", - "Guest": "Guest", "Guarantee booking with credit card": "Guarantee booking with credit card", + "Guest": "Guest", "Guest information": "Guest information", "Guests & Rooms": "Guests & Rooms", "Hi": "Hi", @@ -211,6 +213,7 @@ "New password": "New password", "Next": "Next", "Nights needed to level up": "Nights needed to level up", + "No": "No", "No breakfast": "No breakfast", "No content published": "No content published", "No matching location found": "No matching location found", @@ -222,6 +225,8 @@ "Nordic Swan Ecolabel": "Nordic Swan Ecolabel", "Not found": "Not found", "Nr night, nr adult": "{nights, number} night, {adults, number} adult", + "Number of charging points for electric cars": "Number of charging points for electric cars", + "Number of parking spots": "Number of parking spots", "OTHER PAYMENT METHODS": "OTHER PAYMENT METHODS", "On your journey": "On your journey", "Open": "Open", @@ -233,6 +238,7 @@ "PETR": "Pet", "Parking": "Parking", "Parking / Garage": "Parking / Garage", + "Parking can be reserved in advance": "Parking can be reserved in advance", "Password": "Password", "Pay later": "Pay later", "Pay now": "Pay now", @@ -253,6 +259,7 @@ "Points may take up to 10 days to be displayed.": "Points may take up to 10 days to be displayed.", "Points needed to level up": "Points needed to level up", "Points needed to stay on level": "Points needed to stay on level", + "Practical information": "Practial information", "Previous": "Previous", "Previous victories": "Previous victories", "Price details": "Price details", @@ -331,9 +338,9 @@ "There are no transactions to display": "There are no transactions to display", "Things nearby HOTEL_NAME": "Things nearby {hotelName}", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.", + "Total Points": "Total Points", "Total cost": "Total cost", "Total price": "Total price", - "Total Points": "Total Points", "Tourist": "Tourist", "Transaction date": "Transaction date", "Transactions": "Transactions", @@ -367,6 +374,7 @@ "Where to": "Where to", "Which room class suits you the best?": "Which room class suits you the best?", "Year": "Year", + "Yes": "Yes", "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with", "Yes, discard changes": "Yes, discard changes", "Yes, remove my card": "Yes, remove my card", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 25acf8a9a..33f7678c6 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -95,6 +95,7 @@ "Discard changes": "Hylkää muutokset", "Discard unsaved changes?": "Hylkäätkö tallentamattomat muutokset?", "Distance to city centre": "{number}km Etäisyys kaupunkiin", + "Distance to hotel": "Etäisyys hotelliin", "Do you want to start the day with Scandics famous breakfast buffé?": "Haluatko aloittaa päiväsi Scandicsin kuuluisalla aamiaisbuffella?", "Done": "Valmis", "Download the Scandic app": "Lataa Scandic-sovellus", @@ -125,6 +126,7 @@ "Free cancellation": "Ilmainen peruutus", "Free rebooking": "Ilmainen uudelleenvaraus", "From": "From", + "Garage": "Autotalli", "Get inspired": "Inspiroidu", "Get member benefits & offers": "Hanki jäsenetuja ja -tarjouksia", "Gift(s) added to your benefits": "{amount, plural, one {Lahja} other {Lahjat}} lisätty etuusi", @@ -202,6 +204,7 @@ "New password": "Uusi salasana", "Next": "Seuraava", "Nights needed to level up": "Yöt, joita tarvitaan tasolle", + "No": "Ei", "No breakfast": "Ei aamiaista", "No content published": "Ei julkaistua sisältöä", "No matching location found": "Vastaavaa sijaintia ei löytynyt", @@ -213,6 +216,8 @@ "Nordic Swan Ecolabel": "Ympäristömerkki Miljömärkt", "Not found": "Ei löydetty", "Nr night, nr adult": "{nights, number} yö, {adults, number} aikuinen", + "Number of charging points for electric cars": "Sähköautojen latauspisteiden määrä", + "Number of parking spots": "Pysäköintipaikkojen määrä", "OTHER PAYMENT METHODS": "MUISE KORT", "On your journey": "Matkallasi", "Open": "Avata", @@ -224,6 +229,7 @@ "PETR": "Lemmikki", "Parking": "Pysäköinti", "Parking / Garage": "Pysäköinti / Autotalli", + "Parking can be reserved in advance": "Pysäköintipaikan voi varata etukäteen", "Password": "Salasana", "Pay later": "Maksa myöhemmin", "Pay now": "Maksa nyt", @@ -243,6 +249,7 @@ "Points may take up to 10 days to be displayed.": "Pisteiden näyttäminen voi kestää jopa 10 päivää.", "Points needed to level up": "Tarvitset vielä", "Points needed to stay on level": "Tällä tasolla pysymiseen tarvittavat pisteet", + "Practical information": "Käytännön tietoa", "Previous": "Aikaisempi", "Previous victories": "Edelliset voitot", "Price details": "Hintatiedot", @@ -353,6 +360,7 @@ "Where to": "Minne", "Which room class suits you the best?": "Mikä huoneluokka sopii sinulle parhaiten?", "Year": "Vuosi", + "Yes": "Kyllä", "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Kyllä, hyväksyn Scandic Friends -käyttöehdot ja ymmärrän, että Scandic käsittelee minun henkilötietoni asianmukaisesti", "Yes, discard changes": "Kyllä, hylkää muutokset", "Yes, remove my card": "Kyllä, poista korttini", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 41d740068..6dc1c8cfa 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -94,6 +94,7 @@ "Discard changes": "Forkaste endringer", "Discard unsaved changes?": "Forkaste endringer som ikke er lagret?", "Distance to city centre": "{number}km til sentrum", + "Distance to hotel": "Avstand til hotell", "Do you want to start the day with Scandics famous breakfast buffé?": "Vil du starte dagen med Scandics berømte frokostbuffé?", "Done": "Ferdig", "Download the Scandic app": "Last ned Scandic-appen", @@ -124,6 +125,7 @@ "Free cancellation": "Gratis avbestilling", "Free rebooking": "Gratis ombooking", "From": "Fra", + "Garage": "Garasje", "Get inspired": "Bli inspirert", "Get member benefits & offers": "Få medlemsfordeler og tilbud", "Gift(s) added to your benefits": "{amount, plural, one {Gave} other {Gaver}} lagt til fordelene dine", @@ -200,6 +202,7 @@ "New password": "Nytt passord", "Next": "Neste", "Nights needed to level up": "Netter som trengs for å komme opp i nivå", + "No": "Nei", "No breakfast": "Ingen frokost", "No content published": "Ingen innhold publisert", "No matching location found": "Fant ingen samsvarende plassering", @@ -211,6 +214,8 @@ "Nordic Swan Ecolabel": "Svanemerket", "Not found": "Ikke funnet", "Nr night, nr adult": "{nights, number} natt, {adults, number} voksen", + "Number of charging points for electric cars": "Antall ladepunkter for elbiler", + "Number of parking spots": "Antall parkeringsplasser", "OTHER PAYMENT METHODS": "ANDRE BETALINGSMETODER", "On your journey": "På reisen din", "Open": "Åpen", @@ -222,6 +227,7 @@ "PETR": "Kjæledyr", "Parking": "Parkering", "Parking / Garage": "Parkering / Garasje", + "Parking can be reserved in advance": "Parkering kan reserveres på forhånd", "Password": "Passord", "Pay later": "Betal senere", "Pay now": "Betal nå", @@ -241,6 +247,7 @@ "Points may take up to 10 days to be displayed.": "Det kan ta opptil 10 dager før poeng vises.", "Points needed to level up": "Poeng som trengs for å komme opp i nivå", "Points needed to stay on level": "Poeng som trengs for å holde seg på nivå", + "Practical information": "Praktisk informasjon", "Previous": "Tidligere", "Previous victories": "Tidligere seire", "Price details": "Prisdetaljer", @@ -314,10 +321,10 @@ "Theatre": "Teater", "There are no transactions to display": "Det er ingen transaksjoner å vise", "Things nearby HOTEL_NAME": "Ting i nærheten av {hotelName}", - "Total price": "Totalpris", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For å sikre din reservasjon, ber vi om at du gir oss dine betalingskortdetaljer. Vær sikker på at ingen gebyrer vil bli belastet på dette tidspunktet.", "Total Points": "Totale poeng", "Total incl VAT": "Sum inkl mva", + "Total price": "Totalpris", "Tourist": "Turist", "Transaction date": "Transaksjonsdato", "Transactions": "Transaksjoner", @@ -350,6 +357,7 @@ "Where to": "Hvor skal du", "Which room class suits you the best?": "Hvilken romklasse passer deg best?", "Year": "År", + "Yes": "Ja", "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Ja, jeg aksepterer vilkårene for Scandic Friends og forstår at Scandic vil behandle mine personlige opplysninger i henhold til", "Yes, discard changes": "Ja, forkast endringer", "Yes, remove my card": "Ja, fjern kortet mitt", @@ -397,10 +405,10 @@ "nights": "netter", "number": "antall", "or": "eller", - "room type": "romtype", - "room types": "romtyper", "paying": "betaler", "points": "poeng", + "room type": "romtype", + "room types": "romtyper", "special character": "spesiell karakter", "spendable points expiring by": "{points} Brukbare poeng utløper innen {date}", "to": "til", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 01de7beaf..aa77a18e6 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -94,6 +94,7 @@ "Discard changes": "Ignorera ändringar", "Discard unsaved changes?": "Vill du ignorera ändringar som inte har sparats?", "Distance to city centre": "{number}km till centrum", + "Distance to hotel": "Avstånd till hotell", "Do you want to start the day with Scandics famous breakfast buffé?": "Vill du starta dagen med Scandics berömda frukostbuffé?", "Done": "Klar", "Download the Scandic app": "Ladda ner Scandic-appen", @@ -124,6 +125,7 @@ "Free cancellation": "Fri avbokning", "Free rebooking": "Fri ombokning", "From": "Från", + "Garage": "Garage", "Get inspired": "Bli inspirerad", "Get member benefits & offers": "Ta del av medlemsförmåner och erbjudanden", "Gift(s) added to your benefits": "{amount, plural, one {Gåva} other {Gåvor}} läggs till dina förmåner", @@ -200,6 +202,7 @@ "New password": "Nytt lösenord", "Next": "Nästa", "Nights needed to level up": "Nätter som behövs för att gå upp i nivå", + "No": "Nej", "No breakfast": "Ingen frukost", "No content published": "Inget innehåll publicerat", "No matching location found": "Ingen matchande plats hittades", @@ -211,6 +214,8 @@ "Nordic Swan Ecolabel": "Svanenmärkt", "Not found": "Hittades inte", "Nr night, nr adult": "{nights, number} natt, {adults, number} vuxen", + "Number of charging points for electric cars": "Antal laddplatser för elbilar", + "Number of parking spots": "Antal parkeringsplatser", "OTHER PAYMENT METHODS": "ANDRE BETALINGSMETODER", "On your journey": "På din resa", "Open": "Öppna", @@ -222,6 +227,7 @@ "PETR": "Husdjur", "Parking": "Parkering", "Parking / Garage": "Parkering / Garage", + "Parking can be reserved in advance": "Parkering kan reserveras i förväg", "Password": "Lösenord", "Pay later": "Betala senare", "Pay now": "Betala nu", @@ -241,6 +247,7 @@ "Points may take up to 10 days to be displayed.": "Det kan ta upp till 10 dagar innan poäng visas.", "Points needed to level up": "Poäng som behövs för att gå upp i nivå", "Points needed to stay on level": "Poäng som behövs för att hålla sig på nivå", + "Practical information": "Praktisk information", "Previous": "Föregående", "Previous victories": "Tidigare segrar", "Price details": "Prisdetaljer", @@ -314,10 +321,10 @@ "Theatre": "Teater", "There are no transactions to display": "Det finns inga transaktioner att visa", "Things nearby HOTEL_NAME": "Saker i närheten av {hotelName}", - "Total price": "Totalpris", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "För att säkra din bokning ber vi om att du ger oss dina betalkortdetaljer. Välj säker på att ingen avgifter kommer att debiteras just nu.", "Total Points": "Poäng totalt", "Total incl VAT": "Totalt inkl moms", + "Total price": "Totalpris", "Tourist": "Turist", "Transaction date": "Transaktionsdatum", "Transactions": "Transaktioner", @@ -350,6 +357,7 @@ "Where to": "Vart", "Which room class suits you the best?": "Vilken rumsklass passar dig bäst?", "Year": "År", + "Yes": "Ja", "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Ja, jag accepterar villkoren för Scandic Friends och förstår att Scandic kommer att bearbeta mina personliga uppgifter i enlighet med", "Yes, discard changes": "Ja, ignorera ändringar", "Yes, remove my card": "Ja, ta bort mitt kort", @@ -398,6 +406,7 @@ "nights": "nätter", "number": "nummer", "or": "eller", + "paying": "betalar", "points": "poäng", "room type": "rumtyp", "room types": "rumstyper", @@ -406,9 +415,8 @@ "to": "till", "type": "typ", "types": "typer", - "{amount} out of {total}": "{amount} av {total}", - "paying": "betalar", "uppercase letter": "stor bokstav", + "{amount} out of {total}": "{amount} av {total}", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}" } From 20e3c9a35f633c76b641a3eaf59cef0ee1bc6dc4 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Thu, 17 Oct 2024 15:28:24 +0200 Subject: [PATCH 015/120] feat(SW-650): added sticky position hook and store --- components/BookingWidget/Client.tsx | 15 +++- .../HotelPage/Map/MapWithCard/index.tsx | 24 +++++ .../Map/MapWithCard/mapWithCard.module.css | 12 +++ .../HotelPage/TabNavigation/index.tsx | 13 ++- .../HotelPage/hotelPage.module.css | 13 --- components/ContentType/HotelPage/index.tsx | 5 +- hooks/useStickyPosition.ts | 90 +++++++++++++++++++ stores/sticky-position.ts | 79 ++++++++++++++++ .../hotelPage/map/mapWithCardWrapper.ts | 8 ++ 9 files changed, 240 insertions(+), 19 deletions(-) create mode 100644 components/ContentType/HotelPage/Map/MapWithCard/index.tsx create mode 100644 components/ContentType/HotelPage/Map/MapWithCard/mapWithCard.module.css create mode 100644 hooks/useStickyPosition.ts create mode 100644 stores/sticky-position.ts create mode 100644 types/components/hotelPage/map/mapWithCardWrapper.ts diff --git a/components/BookingWidget/Client.tsx b/components/BookingWidget/Client.tsx index 2acb8a397..0b30cf844 100644 --- a/components/BookingWidget/Client.tsx +++ b/components/BookingWidget/Client.tsx @@ -1,13 +1,15 @@ "use client" import { zodResolver } from "@hookform/resolvers/zod" -import { useEffect, useState } from "react" +import { useEffect, useRef, useState } from "react" import { FormProvider, useForm } from "react-hook-form" import { dt } from "@/lib/dt" +import { StickyElementNameEnum } from "@/stores/sticky-position" import Form from "@/components/Forms/BookingWidget" import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema" import { CloseLargeIcon } from "@/components/Icons" +import useStickyPosition from "@/hooks/useStickyPosition" import { debounce } from "@/utils/debounce" import { getFormattedUrlQueryParams } from "@/utils/url" @@ -28,6 +30,11 @@ export default function BookingWidgetClient({ searchParams, }: BookingWidgetClientProps) { const [isOpen, setIsOpen] = useState(false) + const bookingWidgetRef = useRef(null) + useStickyPosition({ + ref: bookingWidgetRef, + name: StickyElementNameEnum.BOOKING_WIDGET, + }) const sessionStorageSearchData = typeof window !== "undefined" @@ -139,7 +146,11 @@ export default function BookingWidgetClient({ return ( -
    +
    + ) diff --git a/components/ContentType/HotelPage/Rooms/index.tsx b/components/ContentType/HotelPage/Rooms/index.tsx index b7ac43a90..e976ee8af 100644 --- a/components/ContentType/HotelPage/Rooms/index.tsx +++ b/components/ContentType/HotelPage/Rooms/index.tsx @@ -22,27 +22,6 @@ export function Rooms({ rooms }: RoomsProps) { const scrollRef = useRef(null) - const mappedRooms = rooms - .map((room) => { - const size = `${room.roomSize.min} - ${room.roomSize.max} m²` - const personLabel = - room.occupancy.total === 1 - ? intl.formatMessage({ id: "hotelPages.rooms.roomCard.person" }) - : intl.formatMessage({ id: "hotelPages.rooms.roomCard.persons" }) - - const subtitle = `${size} (${room.occupancy.total} ${personLabel})` - - return { - id: room.id, - images: room.images, - title: room.name, - subtitle: subtitle, - sortOrder: room.sortOrder, - popularChoice: null, - } - }) - .sort((a, b) => a.sortOrder - b.sortOrder) - function handleShowMore() { if (scrollRef.current && allRoomsVisible) { scrollRef.current.scrollIntoView({ behavior: "smooth" }) @@ -64,15 +43,9 @@ export function Rooms({ rooms }: RoomsProps) { - {mappedRooms.map(({ id, images, title, subtitle, popularChoice }) => ( -
    - + {rooms.map((room) => ( +
    +
    ))} diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index fd41bbced..7b0709ca9 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -78,10 +78,7 @@ export default function RoomCard({ : `${roomSize?.min}-${roomSize?.max}`} m² - +
    diff --git a/components/SidePeeks/RoomSidePeek/index.tsx b/components/SidePeeks/RoomSidePeek/index.tsx index f63b57d08..c1198f680 100644 --- a/components/SidePeeks/RoomSidePeek/index.tsx +++ b/components/SidePeeks/RoomSidePeek/index.tsx @@ -16,7 +16,7 @@ import type { RoomSidePeekProps } from "@/types/components/hotelReservation/sele export default function RoomSidePeek({ selectedRoom, - roomConfiguration, + buttonSize, }: RoomSidePeekProps) { const [isSidePeekOpen, setIsSidePeekOpen] = useState(false) const intl = useIntl() @@ -31,7 +31,7 @@ export default function RoomSidePeek({ setIsSidePeekOpen(false)} > @@ -51,16 +51,14 @@ export default function RoomSidePeek({ : `${roomSize?.min} - ${roomSize?.max}`} m².{" "} {intl.formatMessage( - { - id: "booking.accommodatesUpTo", - }, + { id: "booking.accommodatesUpTo" }, { nrOfGuests: occupancy } )} {images && (
    - +
    )} diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 8a3bcc2d1..6b48b1f31 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -397,8 +397,7 @@ "guaranteeing": "garanti", "guest": "gæst", "guests": "gæster", - "hotelPages.rooms.roomCard.person": "person", - "hotelPages.rooms.roomCard.persons": "personer", + "hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# person} other {# personer}}", "hotelPages.rooms.roomCard.seeRoomDetails": "Se værelsesdetaljer", "km to city center": "km til byens centrum", "lowercase letter": "lille bogstav", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 1d389176e..41d056496 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -396,8 +396,7 @@ "guaranteeing": "garantiert", "guest": "gast", "guests": "gäste", - "hotelPages.rooms.roomCard.person": "person", - "hotelPages.rooms.roomCard.persons": "personen", + "hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# person} other {# personen}}", "hotelPages.rooms.roomCard.seeRoomDetails": "Zimmerdetails ansehen", "km to city center": "km bis zum Stadtzentrum", "lowercase letter": "Kleinbuchstabe", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index bc93f51ac..a3e5dde34 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -416,8 +416,7 @@ "guaranteeing": "guaranteeing", "guest": "guest", "guests": "guests", - "hotelPages.rooms.roomCard.person": "person", - "hotelPages.rooms.roomCard.persons": "persons", + "hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# person} other {# persons}}", "hotelPages.rooms.roomCard.seeRoomDetails": "See room details", "km to city center": "km to city center", "lowercase letter": "lowercase letter", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 33f7678c6..5a169d558 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -396,8 +396,7 @@ "guaranteeing": "varmistetaan", "guest": "Vieras", "guests": "Vieraita", - "hotelPages.rooms.roomCard.person": "henkilö", - "hotelPages.rooms.roomCard.persons": "Henkilöä", + "hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# henkilö} other {# Henkilöä}}", "hotelPages.rooms.roomCard.seeRoomDetails": "Katso huoneen tiedot", "km to city center": "km keskustaan", "lowercase letter": "pien kirjain", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 6dc1c8cfa..97aa25fc5 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -394,8 +394,7 @@ "guaranteeing": "garantiert", "guest": "gjest", "guests": "gjester", - "hotelPages.rooms.roomCard.person": "person", - "hotelPages.rooms.roomCard.persons": "personer", + "hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# person} other {# personer}}", "hotelPages.rooms.roomCard.seeRoomDetails": "Se detaljer om rommet", "km to city center": "km til sentrum", "lowercase letter": "liten bokstav", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index aa77a18e6..2b3a16db3 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -395,8 +395,7 @@ "guaranteeing": "garanterar", "guest": "gäst", "guests": "gäster", - "hotelPages.rooms.roomCard.person": "person", - "hotelPages.rooms.roomCard.persons": "personer", + "hotelPages.rooms.roomCard.persons": "{totalOccupancy, plural, one {# person} other {# personer}}", "hotelPages.rooms.roomCard.seeRoomDetails": "Se information om rummet", "km to city center": "km till stadens centrum", "lowercase letter": "liten bokstav", diff --git a/types/components/hotelPage/room.ts b/types/components/hotelPage/room.ts index cfa3593c9..7be59609d 100644 --- a/types/components/hotelPage/room.ts +++ b/types/components/hotelPage/room.ts @@ -1,11 +1,7 @@ import type { RoomData } from "@/types/hotel" export interface RoomCardProps { - id: string - images: RoomData["images"] - title: string - subtitle: string - badgeTextTransKey: string | null + room: RoomData } export type RoomsProps = { diff --git a/types/components/hotelReservation/selectRate/roomSidePeek.ts b/types/components/hotelReservation/selectRate/roomSidePeek.ts index c526d04b4..1ed03c522 100644 --- a/types/components/hotelReservation/selectRate/roomSidePeek.ts +++ b/types/components/hotelReservation/selectRate/roomSidePeek.ts @@ -1,8 +1,6 @@ -import { RoomConfiguration } from "@/server/routers/hotels/output" - import { RoomData } from "@/types/hotel" export type RoomSidePeekProps = { - roomConfiguration: RoomConfiguration selectedRoom?: RoomData + buttonSize: "small" | "medium" } From e3a96a2c14c3542d61a2cf79c5530bfa35c5c862 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Wed, 30 Oct 2024 10:37:28 +0100 Subject: [PATCH 022/120] feat(SW-713): move roomsidepeek types --- components/SidePeeks/RoomSidePeek/index.tsx | 2 +- .../{hotelReservation/selectRate => sidePeeks}/roomSidePeek.ts | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename types/components/{hotelReservation/selectRate => sidePeeks}/roomSidePeek.ts (100%) diff --git a/components/SidePeeks/RoomSidePeek/index.tsx b/components/SidePeeks/RoomSidePeek/index.tsx index c1198f680..8e35a2a86 100644 --- a/components/SidePeeks/RoomSidePeek/index.tsx +++ b/components/SidePeeks/RoomSidePeek/index.tsx @@ -12,7 +12,7 @@ import { getFacilityIcon } from "./facilityIcon" import styles from "./roomSidePeek.module.css" -import type { RoomSidePeekProps } from "@/types/components/hotelReservation/selectRate/roomSidePeek" +import type { RoomSidePeekProps } from "@/types/components/sidePeeks/roomSidePeek" export default function RoomSidePeek({ selectedRoom, diff --git a/types/components/hotelReservation/selectRate/roomSidePeek.ts b/types/components/sidePeeks/roomSidePeek.ts similarity index 100% rename from types/components/hotelReservation/selectRate/roomSidePeek.ts rename to types/components/sidePeeks/roomSidePeek.ts From 4106f9a91d216b73f47598b13a2a87cfc4ae0e0c Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Wed, 30 Oct 2024 12:56:53 +0100 Subject: [PATCH 023/120] feat(Sw-713): conditional rendering --- .../SelectRate/RoomSelection/RoomCard/index.tsx | 5 ++++- types/components/sidePeeks/roomSidePeek.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index 7b0709ca9..07311f7fd 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -57,6 +57,7 @@ export default function RoomCard({ const selectedRoom = roomCategories.find( (room) => room.name === roomConfiguration.roomType ) + const { roomSize, occupancy, descriptions, images } = selectedRoom || {} const mainImage = images?.[0] @@ -78,7 +79,9 @@ export default function RoomCard({ : `${roomSize?.min}-${roomSize?.max}`} m² - + {selectedRoom && ( + + )}
    diff --git a/types/components/sidePeeks/roomSidePeek.ts b/types/components/sidePeeks/roomSidePeek.ts index 1ed03c522..c868d7ab3 100644 --- a/types/components/sidePeeks/roomSidePeek.ts +++ b/types/components/sidePeeks/roomSidePeek.ts @@ -1,6 +1,6 @@ import { RoomData } from "@/types/hotel" export type RoomSidePeekProps = { - selectedRoom?: RoomData + selectedRoom: RoomData buttonSize: "small" | "medium" } From 5b3958e01771bb9e06c3472153b43c01cba205fe Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Wed, 30 Oct 2024 16:44:08 +0100 Subject: [PATCH 024/120] feat(SW-713): update styling for sidepeek --- components/SidePeeks/RoomSidePeek/index.tsx | 100 ++++++++++-------- .../RoomSidePeek/roomSidePeek.module.css | 43 +++++++- i18n/dictionaries/da.json | 3 + i18n/dictionaries/de.json | 3 + i18n/dictionaries/en.json | 3 + i18n/dictionaries/fi.json | 3 + i18n/dictionaries/no.json | 3 + i18n/dictionaries/sv.json | 3 + 8 files changed, 114 insertions(+), 47 deletions(-) diff --git a/components/SidePeeks/RoomSidePeek/index.tsx b/components/SidePeeks/RoomSidePeek/index.tsx index 8e35a2a86..bbb13d9f7 100644 --- a/components/SidePeeks/RoomSidePeek/index.tsx +++ b/components/SidePeeks/RoomSidePeek/index.tsx @@ -45,49 +45,65 @@ export default function RoomSidePeek({ isOpen={isSidePeekOpen} handleClose={() => setIsSidePeekOpen(false)} > - - {roomSize?.min === roomSize?.max - ? roomSize?.min - : `${roomSize?.min} - ${roomSize?.max}`} - m².{" "} - {intl.formatMessage( - { id: "booking.accommodatesUpTo" }, - { nrOfGuests: occupancy } - )} - - - {images && ( -
    - +
    +
    + + {roomSize?.min === roomSize?.max + ? roomSize?.min + : `${roomSize?.min} - ${roomSize?.max}`} + m².{" "} + {intl.formatMessage( + { id: "booking.accommodatesUpTo" }, + { nrOfGuests: occupancy } + )} + + {images && ( +
    + +
    + )} + {roomDescription}
    - )} - - - {roomDescription} - - - {intl.formatMessage({ id: "booking.thisRoomIsEquippedWith" })} - -
      - {selectedRoom?.roomFacilities - .sort((a, b) => a.sortOrder - b.sortOrder) - .map((facility) => { - const Icon = getFacilityIcon(facility.name) - - return ( -
    • - {Icon && } - - {facility.name} - -
    • - ) - })} -
    +
    + + {intl.formatMessage({ id: "booking.thisRoomIsEquippedWith" })} + +
      + {selectedRoom?.roomFacilities + .sort((a, b) => a.sortOrder - b.sortOrder) + .map((facility) => { + const Icon = getFacilityIcon(facility.name) + return ( +
    • + {Icon && } + + {facility.name} + +
    • + ) + })} +
    +
    +
    + + {intl.formatMessage({ id: "booking.bedOptions" })} + + + {intl.formatMessage({ id: "booking.basedOnAvailability" })} + + {/* TODO: Get data for bed options */} +
    +
    +
    + +
    ) diff --git a/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css b/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css index 9197cb152..1b5403274 100644 --- a/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css +++ b/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css @@ -4,20 +4,40 @@ text-decoration: none; } -.imageContainer { - min-height: 185px; +.wrapper { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); position: relative; + margin-bottom: calc(var(--Spacing-x4) * 2 + 20px); } -.description { - margin-top: var(--Spacing-x-one-and-half); - margin-bottom: var(--Spacing-x2); +.mainContent, +.listContainer { + display: flex; + flex-direction: column; + gap: var(--Spacing-x-one-and-half); } + +.imageContainer { + min-height: 280px; + position: relative; + border-radius: var(--Corner-radius-Medium); + overflow: hidden; +} + +.imageContainer img { + width: 100%; + aspect-ratio: 16/9; + object-fit: cover; +} + .facilityList { margin-top: var(--Spacing-x-one-and-half); column-count: 2; column-gap: var(--Spacing-x2); } + .facilityList li { display: flex; gap: var(--Spacing-x1); @@ -27,3 +47,16 @@ .noIcon { margin-left: var(--Spacing-x4); } + +.buttonContainer { + background-color: var(--Base-Background-Primary-Normal); + border-top: 1px solid var(--Base-Border-Subtle); + padding: var(--Spacing-x4) var(--Spacing-x2); + overflow: hidden; + width: 100%; + position: absolute; + left: 0; + bottom: 0; + display: flex; + justify-content: center; +} diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 6b48b1f31..865dc6853 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -384,10 +384,13 @@ "as of today": "pr. dags dato", "booking.accommodatesUpTo": "Plads til {nrOfGuests, plural, one {# person} other {op til # personer}}", "booking.adults": "{totalAdults, plural, one {# voksen} other {# voksne}}", + "booking.basedOnAvailability": "Baseret på tilgængelighed", + "booking.bedOptions": "Sengemuligheder", "booking.children": "{totalChildren, plural, one {# barn} other {# børn}}", "booking.guests": "Maks {nrOfGuests, plural, one {# gæst} other {# gæster}}", "booking.nights": "{totalNights, plural, one {# nat} other {# nætter}}", "booking.rooms": "{totalRooms, plural, one {# værelse} other {# værelser}}", + "booking.selectRoom": "Zimmer auswählen", "booking.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle Vilkår og betingelser, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til Scandics Privatlivspolitik. Jeg accepterer, at Scandic kræver et gyldigt kreditkort under min besøg i tilfælde af, at noget er tilbagebetalt.", "booking.thisRoomIsEquippedWith": "Dette værelse er udstyret med", "breakfast.price": "{amount} {currency}/nat", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 41d056496..3187d3855 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -383,10 +383,13 @@ "as of today": "Stand heute", "booking.accommodatesUpTo": "Bietet Platz für {nrOfGuests, plural, one {# Person } other {bis zu # Personen}}", "booking.adults": "{totalAdults, plural, one {# erwachsene} other {# erwachsene}}", + "booking.basedOnAvailability": "Abhängig von der Verfügbarkeit", + "booking.bedOptions": "Bettoptionen", "booking.children": "{totalChildren, plural, one {# kind} other {# kinder}}", "booking.guests": "Max {nrOfGuests, plural, one {# gast} other {# gäste}}", "booking.nights": "{totalNights, plural, one {# nacht} other {# Nächte}}", "booking.rooms": "{totalRooms, plural, one {# zimmer} other {# räume}}", + "booking.selectRoom": "Vælg værelse", "booking.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle Vilkår og betingelser, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til Scandics Privatlivspolitik. Jeg accepterer, at Scandic kræver et gyldigt kreditkort under min besøg i tilfælde af, at noget er tilbagebetalt.", "booking.thisRoomIsEquippedWith": "Dieses Zimmer ist ausgestattet mit", "breakfast.price": "{amount} {currency}/Nacht", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index a3e5dde34..0ae7ba348 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -400,12 +400,15 @@ "as of today": "as of today", "booking.accommodatesUpTo": "Accommodates up to {nrOfGuests, plural, one {# person} other {# people}}", "booking.adults": "{totalAdults, plural, one {# adult} other {# adults}}", + "booking.basedOnAvailability": "Based on availability", + "booking.bedOptions": "Bed options", "booking.children": "{totalChildren, plural, one {# child} other {# children}}", "booking.confirmation.text": "Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please email us.", "booking.confirmation.title": "Your booking is confirmed", "booking.guests": "Max {nrOfGuests, plural, one {# guest} other {# guests}}", "booking.nights": "{totalNights, plural, one {# night} other {# nights}}", "booking.rooms": "{totalRooms, plural, one {# room} other {# rooms}}", + "booking.selectRoom": "Select room", "booking.terms": "By paying with any of the payment methods available, I accept the terms for this booking and the general Terms & Conditions, and understand that Scandic will process my personal data for this booking in accordance with Scandic's Privacy policy. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.", "booking.thisRoomIsEquippedWith": "This room is equipped with", "breakfast.price": "{amount} {currency}/night", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 5a169d558..36b424139 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -383,10 +383,13 @@ "as of today": "tänään", "booking.accommodatesUpTo": "Huoneeseen {nrOfGuests, plural, one {# person} other {mahtuu 2 henkilöä}}", "booking.adults": "{totalAdults, plural, one {# aikuinen} other {# aikuiset}}", + "booking.basedOnAvailability": "Saatavuuden mukaan", + "booking.bedOptions": "Vuodevaihtoehdot", "booking.children": "{totalChildren, plural, one {# lapsi} other {# lasta}}", "booking.guests": "Max {nrOfGuests, plural, one {# vieras} other {# vieraita}}", "booking.nights": "{totalNights, plural, one {# yö} other {# yötä}}", "booking.rooms": "{totalRooms, plural, one {# huone} other {# sviitti}}", + "booking.selectRoom": "Valitse huone", "booking.terms": "Maksamalla minkä tahansa saatavilla olevan maksutavan avulla hyväksyn tämän varauksen ehdot ja yleiset ehdot ja ehtoja, ja ymmärrän, että Scandic käsittelee minun henkilötietoni tässä varauksessa mukaisesti Scandicin tietosuojavaltuuden mukaisesti. Hyväksyn myös, että Scandic vaatii validin luottokortin majoituksen ajan, jos jokin jää maksamatta.", "booking.thisRoomIsEquippedWith": "Tämä huone on varustettu", "breakfast.price": "{amount} {currency}/yö", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 97aa25fc5..6aa6def5c 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -382,10 +382,13 @@ "as of today": "per i dag", "booking.accommodatesUpTo": "Plass til {nrOfGuests, plural, one {# person} other {opptil # personer}}", "booking.adults": "{totalAdults, plural, one {# voksen} other {# voksne}}", + "booking.basedOnAvailability": "Basert på tilgjengelighet", + "booking.bedOptions": "Sengemuligheter", "booking.children": "{totalChildren, plural, one {# barn} other {# barn}}", "booking.guests": "Maks {nrOfGuests, plural, one {# gjest} other {# gjester}}", "booking.nights": "{totalNights, plural, one {# natt} other {# netter}}", "booking.rooms": "{totalRooms, plural, one {# rom} other {# rom}}", + "booking.selectRoom": "Velg rom", "booking.thisRoomIsEquippedWith": "Dette rommet er utstyrt med", "breakfast.price": "{amount} {currency}/natt", "breakfast.price.free": "{amount} {currency} 0 {currency}/natt", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 2b3a16db3..bff5462fd 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -382,10 +382,13 @@ "as of today": "från och med idag", "booking.accommodatesUpTo": "Rymmer {nrOfGuests, plural, one {# person} other {upp till # personer}}", "booking.adults": "{totalAdults, plural, one {# vuxen} other {# vuxna}}", + "booking.basedOnAvailability": "Baserat på tillgänglighet", + "booking.bedOptions": "Sängalternativ", "booking.children": "{totalChildren, plural, one {# barn} other {# barn}}", "booking.guests": "Max {nrOfGuests, plural, one {# gäst} other {# gäster}}", "booking.nights": "{totalNights, plural, one {# natt} other {# nätter}}", "booking.rooms": "{totalRooms, plural, one {# rum} other {# rum}}", + "booking.selectRoom": "Välj rum", "booking.terms": "Genom att betala med någon av de tillgängliga betalningsmetoderna accepterar jag villkoren för denna bokning och de generella Villkoren och villkoren, och förstår att Scandic kommer att behandla min personliga data i samband med denna bokning i enlighet med Scandics integritetspolicy. Jag accepterar att Scandic kräver ett giltigt kreditkort under min besök i fall att något är tillbaka betalt.", "booking.thisRoomIsEquippedWith": "Detta rum är utrustat med", "breakfast.price": "{amount} {currency}/natt", From b794448fa27b014520be9ae824228a9327bd6432 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Thu, 31 Oct 2024 10:13:26 +0100 Subject: [PATCH 025/120] feat(SW-713): design change --- components/SidePeeks/RoomSidePeek/roomSidePeek.module.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css b/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css index 1b5403274..d8ac3e9f4 100644 --- a/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css +++ b/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css @@ -9,7 +9,7 @@ flex-direction: column; gap: var(--Spacing-x2); position: relative; - margin-bottom: calc(var(--Spacing-x4) * 2 + 20px); + margin-bottom: calc(var(--Spacing-x4) * 2 + 80px); } .mainContent, @@ -33,7 +33,6 @@ } .facilityList { - margin-top: var(--Spacing-x-one-and-half); column-count: 2; column-gap: var(--Spacing-x2); } From 13dfe9ea15fd7cafacf84b6f86481ff6646ba1a4 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Thu, 31 Oct 2024 11:39:47 +0100 Subject: [PATCH 026/120] feat(SW-713): overrider display none with important --- components/SidePeeks/RoomSidePeek/roomSidePeek.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css b/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css index d8ac3e9f4..3d7f8a3e6 100644 --- a/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css +++ b/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css @@ -38,7 +38,7 @@ } .facilityList li { - display: flex; + display: flex !important; /* Overrides the display none from grids.stackable */ gap: var(--Spacing-x1); margin-bottom: var(--Spacing-x-half); } From 0e33cfa0760a5a3e1c412642c1648c82f436ff9c Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Thu, 31 Oct 2024 16:03:13 +0100 Subject: [PATCH 027/120] feat(SW-713): add important --- components/SidePeeks/RoomSidePeek/index.tsx | 2 +- components/SidePeeks/RoomSidePeek/roomSidePeek.module.css | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/components/SidePeeks/RoomSidePeek/index.tsx b/components/SidePeeks/RoomSidePeek/index.tsx index bbb13d9f7..a1724fa86 100644 --- a/components/SidePeeks/RoomSidePeek/index.tsx +++ b/components/SidePeeks/RoomSidePeek/index.tsx @@ -41,7 +41,7 @@ export default function RoomSidePeek({ setIsSidePeekOpen(false)} > diff --git a/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css b/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css index 3d7f8a3e6..acf48ee0a 100644 --- a/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css +++ b/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css @@ -38,7 +38,7 @@ } .facilityList li { - display: flex !important; /* Overrides the display none from grids.stackable */ + display: flex !important; /* Overrides the display none from grids.stackable on Hotel Page */ gap: var(--Spacing-x1); margin-bottom: var(--Spacing-x-half); } @@ -51,11 +51,8 @@ background-color: var(--Base-Background-Primary-Normal); border-top: 1px solid var(--Base-Border-Subtle); padding: var(--Spacing-x4) var(--Spacing-x2); - overflow: hidden; width: 100%; position: absolute; left: 0; bottom: 0; - display: flex; - justify-content: center; } From 5f210caefc4db84731b67f34f39a3b7b610e4611 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Thu, 31 Oct 2024 16:31:50 +0100 Subject: [PATCH 028/120] feat(SW-713): add comment --- components/SidePeeks/RoomSidePeek/roomSidePeek.module.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css b/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css index acf48ee0a..8b96ef1b8 100644 --- a/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css +++ b/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css @@ -9,7 +9,9 @@ flex-direction: column; gap: var(--Spacing-x2); position: relative; - margin-bottom: calc(var(--Spacing-x4) * 2 + 80px); + margin-bottom: calc( + var(--Spacing-x4) * 2 + 80px + ); /* Creates space between the wrapper and buttonContainer */ } .mainContent, From a61460f6ea7333a9ead21fc8aaf8e57c7a91ff70 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Thu, 31 Oct 2024 22:09:53 +0100 Subject: [PATCH 029/120] feat(SW-713): remove undefined check --- .../HotelPage/Rooms/RoomCard/index.tsx | 2 +- .../RoomSelection/RoomCard/index.tsx | 2 +- components/SidePeeks/RoomSidePeek/index.tsx | 25 ++++++++----------- types/components/sidePeeks/roomSidePeek.ts | 2 +- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx b/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx index 931b1880c..2f7d33c25 100644 --- a/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx +++ b/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx @@ -70,7 +70,7 @@ export function RoomCard({ room }: RoomCardProps) { {subtitle}
    - +
    ) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index 07311f7fd..a045d639a 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -80,7 +80,7 @@ export default function RoomCard({ m² {selectedRoom && ( - + )}
    diff --git a/components/SidePeeks/RoomSidePeek/index.tsx b/components/SidePeeks/RoomSidePeek/index.tsx index a1724fa86..1e27577c8 100644 --- a/components/SidePeeks/RoomSidePeek/index.tsx +++ b/components/SidePeeks/RoomSidePeek/index.tsx @@ -14,17 +14,14 @@ import styles from "./roomSidePeek.module.css" import type { RoomSidePeekProps } from "@/types/components/sidePeeks/roomSidePeek" -export default function RoomSidePeek({ - selectedRoom, - buttonSize, -}: RoomSidePeekProps) { +export default function RoomSidePeek({ room, buttonSize }: RoomSidePeekProps) { const [isSidePeekOpen, setIsSidePeekOpen] = useState(false) const intl = useIntl() - const roomSize = selectedRoom?.roomSize - const occupancy = selectedRoom?.occupancy.total - const roomDescription = selectedRoom?.descriptions.medium - const images = selectedRoom?.images + const roomSize = room.roomSize + const occupancy = room.occupancy.total + const roomDescription = room.descriptions.medium + const images = room.images return (
    @@ -41,16 +38,16 @@ export default function RoomSidePeek({ setIsSidePeekOpen(false)} >
    - {roomSize?.min === roomSize?.max - ? roomSize?.min - : `${roomSize?.min} - ${roomSize?.max}`} + {roomSize.min === roomSize.max + ? roomSize.min + : `${roomSize.min} - ${roomSize.max}`} m².{" "} {intl.formatMessage( { id: "booking.accommodatesUpTo" }, @@ -59,7 +56,7 @@ export default function RoomSidePeek({ {images && (
    - +
    )} {roomDescription} @@ -69,7 +66,7 @@ export default function RoomSidePeek({ {intl.formatMessage({ id: "booking.thisRoomIsEquippedWith" })}
      - {selectedRoom?.roomFacilities + {room.roomFacilities .sort((a, b) => a.sortOrder - b.sortOrder) .map((facility) => { const Icon = getFacilityIcon(facility.name) diff --git a/types/components/sidePeeks/roomSidePeek.ts b/types/components/sidePeeks/roomSidePeek.ts index c868d7ab3..88028ec02 100644 --- a/types/components/sidePeeks/roomSidePeek.ts +++ b/types/components/sidePeeks/roomSidePeek.ts @@ -1,6 +1,6 @@ import { RoomData } from "@/types/hotel" export type RoomSidePeekProps = { - selectedRoom: RoomData + room: RoomData buttonSize: "small" | "medium" } From cdc5652347ff2dbcb498471fa51e2541b901f333 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Fri, 1 Nov 2024 09:46:55 +0100 Subject: [PATCH 030/120] feat(SW-713): import type --- types/components/sidePeeks/roomSidePeek.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/components/sidePeeks/roomSidePeek.ts b/types/components/sidePeeks/roomSidePeek.ts index 88028ec02..1aab7739a 100644 --- a/types/components/sidePeeks/roomSidePeek.ts +++ b/types/components/sidePeeks/roomSidePeek.ts @@ -1,4 +1,4 @@ -import { RoomData } from "@/types/hotel" +import type { RoomData } from "@/types/hotel" export type RoomSidePeekProps = { room: RoomData From 644ce369aab5e6a535430031a5d4efe618a1b93a Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Fri, 1 Nov 2024 10:24:56 +0100 Subject: [PATCH 031/120] fix: booking confirmation validation --- .../booking-confirmation/page.tsx | 27 ++- server/routers/booking/output.ts | 18 +- server/routers/booking/query.ts | 12 +- server/routers/hotels/input.ts | 2 + server/routers/hotels/query.ts | 204 +++++++++--------- 5 files changed, 145 insertions(+), 118 deletions(-) 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 ef7e818c4..c4a682da9 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx @@ -45,8 +45,8 @@ export default async function BookingConfirmationPage({ } ) - const fromDate = dt(booking.temp.fromDate).locale(params.lang) - const toDate = dt(booking.temp.toDate).locale(params.lang) + const fromDate = dt(booking.checkInDate).locale(params.lang) + const toDate = dt(booking.checkOutDate).locale(params.lang) const nights = intl.formatMessage( { id: "booking.nights" }, { @@ -77,7 +77,7 @@ export default async function BookingConfirmationPage({ textTransform="regular" type="h1" > - {booking.hotel.name} + {booking.hotel?.data.attributes.name} @@ -91,7 +91,7 @@ export default async function BookingConfirmationPage({ {intl.formatMessage( { id: "Reference #{bookingNr}" }, - { bookingNr: "A92320VV" } + { bookingNr: booking.confirmationNumber } )} @@ -183,11 +183,13 @@ export default async function BookingConfirmationPage({
      - {booking.hotel.name} + {booking.hotel?.data.attributes.name} - {booking.hotel.email} - {booking.hotel.phoneNumber} + {booking.hotel?.data.attributes.contactInformation.email} + + + {booking.hotel?.data.attributes.contactInformation.phoneNumber}
    @@ -219,7 +221,16 @@ export default async function BookingConfirmationPage({ {intl.formatMessage({ id: "Total cost" })} - {booking.temp.total} + + {" "} + {intl.formatMessage( + { id: "{amount} {currency}" }, + { + amount: intl.formatNumber(booking.totalPrice), + currency: booking.currencyCode, + } + )} + {`${intl.formatMessage({ id: "Approx." })} ${booking.temp.totalInEuro}`} diff --git a/server/routers/booking/output.ts b/server/routers/booking/output.ts index aacf1ca6b..cf4536a0d 100644 --- a/server/routers/booking/output.ts +++ b/server/routers/booking/output.ts @@ -44,14 +44,18 @@ const childrenAgesSchema = z.object({ const guestSchema = z.object({ firstName: z.string(), lastName: z.string(), + email: z.string().nullable(), + phoneNumber: z.string().nullable(), }) -const packagesSchema = z.object({ - accessibility: z.boolean(), - allergyFriendly: z.boolean(), - breakfast: z.boolean(), - petFriendly: z.boolean(), -}) +const packagesSchema = z.array( + z.object({ + accessibility: z.boolean().optional(), + allergyFriendly: z.boolean().optional(), + breakfast: z.boolean().optional(), + petFriendly: z.boolean().optional(), + }) +) export const bookingConfirmationSchema = z .object({ @@ -66,7 +70,7 @@ export const bookingConfirmationSchema = z confirmationNumber: z.string(), currencyCode: z.string(), guest: guestSchema, - hasPayRouting: z.boolean(), + hasPayRouting: z.boolean().optional(), hotelId: z.string(), packages: packagesSchema, rateCode: z.string(), diff --git a/server/routers/booking/query.ts b/server/routers/booking/query.ts index 5c72bb284..17555f8c4 100644 --- a/server/routers/booking/query.ts +++ b/server/routers/booking/query.ts @@ -4,6 +4,7 @@ import * as api from "@/lib/api" import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc" import { router, serviceProcedure } from "@/server/trpc" +import { getHotelData } from "../hotels/query" import { bookingConfirmationInput, getBookingStatusInput } from "./input" import { bookingConfirmationSchema, createBookingSchema } from "./output" @@ -81,6 +82,11 @@ export const bookingQueryRouter = router({ throw badRequestError() } + const hotelData = await getHotelData( + { hotelId: booking.data.hotelId, language: ctx.lang }, + ctx.serviceToken + ) + getBookingConfirmationSuccessCounter.add(1, { confirmationNumber }) console.info( "api.booking.confirmation success", @@ -91,6 +97,7 @@ export const bookingQueryRouter = router({ return { ...booking.data, + hotel: hotelData, temp: { breakfastFrom: "06:30", breakfastTo: "11:00", @@ -127,11 +134,6 @@ export const bookingQueryRouter = router({ memberbershipNumber: "19822", phoneNumber: "+46702446688", }, - hotel: { - email: "bookings@scandichotels.com", - name: "Downtown Camper by Scandic", - phoneNumber: "+4689001350", - }, } }), status: serviceProcedure.input(getBookingStatusInput).query(async function ({ diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index d4fa1a109..ecf4d5cad 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -68,6 +68,8 @@ export const getHotelDataInputSchema = z.object({ include: z.array(z.nativeEnum(HotelIncludeEnum)).optional(), }) +export type HotelDataInput = z.input + export const getBreakfastPackageInputSchema = z.object({ adults: z.number().min(1, { message: "at least one adult is required" }), fromDate: z diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index a8d21ca22..627ad1f8d 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -39,6 +39,7 @@ import { getRatesInputSchema, getRoomsAvailabilityInputSchema, getSelectedRoomAvailabilityInputSchema, + type HotelDataInput, } from "./input" import { breakfastPackagesSchema, @@ -162,6 +163,110 @@ async function getContentstackData(lang: Lang, uid?: string | null) { return hotelPageData.data.hotel_page } +export async function getHotelData( + input: HotelDataInput, + serviceToken: string +) { + const { hotelId, language, include, isCardOnlyPayment } = input + + const params: Record = { + hotelId, + language, + } + + if (include) { + params.include = include.join(",") + } + + getHotelCounter.add(1, { + hotelId, + language, + include, + }) + console.info( + "api.hotels.hotelData start", + JSON.stringify({ query: { hotelId, params } }) + ) + + const apiResponse = await api.get( + api.endpoints.v1.Hotel.Hotels.hotel(hotelId), + { + headers: { + Authorization: `Bearer ${serviceToken}`, + }, + }, + params + ) + + if (!apiResponse.ok) { + const text = await apiResponse.text() + getHotelFailCounter.add(1, { + hotelId, + language, + include, + error_type: "http_error", + error: JSON.stringify({ + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }), + }) + console.error( + "api.hotels.hotelData error", + JSON.stringify({ + query: { hotelId, params }, + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }, + }) + ) + return null + } + + const apiJson = await apiResponse.json() + const validateHotelData = getHotelDataSchema.safeParse(apiJson) + + if (!validateHotelData.success) { + getHotelFailCounter.add(1, { + hotelId, + language, + include, + error_type: "validation_error", + error: JSON.stringify(validateHotelData.error), + }) + + console.error( + "api.hotels.hotelData validation error", + JSON.stringify({ + query: { hotelId, params }, + error: validateHotelData.error, + }) + ) + throw badRequestError() + } + + getHotelSuccessCounter.add(1, { + hotelId, + language, + include, + }) + console.info( + "api.hotels.hotelData success", + JSON.stringify({ + query: { hotelId, params: params }, + }) + ) + + if (isCardOnlyPayment) { + validateHotelData.data.data.attributes.merchantInformationData.alternatePaymentOptions = + [] + } + + return validateHotelData.data +} + export const hotelQueryRouter = router({ get: contentStackUidWithServiceProcedure .input(getHotelInputSchema) @@ -753,104 +858,7 @@ export const hotelQueryRouter = router({ get: serviceProcedure .input(getHotelDataInputSchema) .query(async ({ ctx, input }) => { - const { hotelId, language, include, isCardOnlyPayment } = input - - const params: Record = { - hotelId, - language, - } - - if (include) { - params.include = include.join(",") - } - - getHotelCounter.add(1, { - hotelId, - language, - include, - }) - console.info( - "api.hotels.hotelData start", - JSON.stringify({ query: { hotelId, params } }) - ) - - const apiResponse = await api.get( - api.endpoints.v1.Hotel.Hotels.hotel(hotelId), - { - headers: { - Authorization: `Bearer ${ctx.serviceToken}`, - }, - }, - params - ) - - if (!apiResponse.ok) { - const text = await apiResponse.text() - getHotelFailCounter.add(1, { - hotelId, - language, - include, - error_type: "http_error", - error: JSON.stringify({ - status: apiResponse.status, - statusText: apiResponse.statusText, - text, - }), - }) - console.error( - "api.hotels.hotelData error", - JSON.stringify({ - query: { hotelId, params }, - error: { - status: apiResponse.status, - statusText: apiResponse.statusText, - text, - }, - }) - ) - return null - } - - const apiJson = await apiResponse.json() - const validateHotelData = getHotelDataSchema.safeParse(apiJson) - - if (!validateHotelData.success) { - getHotelFailCounter.add(1, { - hotelId, - language, - include, - error_type: "validation_error", - error: JSON.stringify(validateHotelData.error), - }) - - console.error( - "api.hotels.hotelData validation error", - JSON.stringify({ - query: { hotelId, params }, - error: validateHotelData.error, - }) - ) - throw badRequestError() - } - - getHotelSuccessCounter.add(1, { - hotelId, - language, - include, - }) - console.info( - "api.hotels.hotelData success", - JSON.stringify({ - query: { hotelId, params: params }, - }) - ) - - if (isCardOnlyPayment) { - validateHotelData.data.data.attributes.merchantInformationData.alternatePaymentOptions = - [] - } - - return validateHotelData.data + return getHotelData(input, ctx.serviceToken) }), }), locations: router({ From f79eb7d5a9dde6c9f8a50f2ace487bc86396eff1 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Fri, 1 Nov 2024 10:58:11 +0100 Subject: [PATCH 032/120] fix(sw-453): Correct view of cards and icons when filter --- .../RoomSelection/RoomCard/index.tsx | 21 ++++++++------ .../SelectRate/RoomSelection/index.tsx | 2 ++ .../SelectRate/Rooms/index.tsx | 28 +++++++++++++------ .../hotelReservation/selectRate/roomCard.ts | 4 ++- .../selectRate/roomSelection.ts | 3 +- 5 files changed, 38 insertions(+), 20 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index a045d639a..4eae4293c 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -23,6 +23,7 @@ export default function RoomCard({ rateDefinitions, roomConfiguration, roomCategories, + selectedPackages, handleSelectRate, }: RoomCardProps) { const intl = useIntl() @@ -133,15 +134,17 @@ export default function RoomCard({ >{`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`} )} - {roomConfiguration.features.map((feature) => ( - - {createElement(getIconForFeatureCode(feature.code), { - width: 16, - height: 16, - color: "burgundy", - })} - - ))} + {roomConfiguration.features + .filter((feature) => selectedPackages.includes(feature.code)) + .map((feature) => ( + + {createElement(getIconForFeatureCode(feature.code), { + width: 16, + height: 16, + color: "burgundy", + })} + + ))}
    {/*NOTE: images from the test API are hosted on test3.scandichotels.com, which can't be accessed unless on Scandic's Wifi or using Citrix. */} diff --git a/components/HotelReservation/SelectRate/RoomSelection/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/index.tsx index 4b6613e7e..efe5778d1 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/index.tsx @@ -16,6 +16,7 @@ export default function RoomSelection({ roomCategories, user, packages, + selectedPackages, }: RoomSelectionProps) { const [rateSummary, setRateSummary] = useState(null) @@ -67,6 +68,7 @@ export default function RoomSelection({ roomConfiguration={roomConfiguration} roomCategories={roomCategories} handleSelectRate={setRateSummary} + selectedPackages={selectedPackages} /> ))} diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx index 8f9030149..8c122a9c0 100644 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -9,6 +9,10 @@ import RoomSelection from "../RoomSelection" import styles from "./rooms.module.css" +import { + RoomPackageCodeEnum, + type RoomPackageCodes, +} from "@/types/components/hotelReservation/selectRate/roomFilter" import type { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection" export default function Rooms({ @@ -16,20 +20,25 @@ export default function Rooms({ roomCategories = [], user, packages, -}: RoomSelectionProps) { - const defaultRooms = roomsAvailability.roomConfigurations.filter( - (room) => room.features.length === 0 - ) +}: Omit) { + const defaultRooms = roomsAvailability.roomConfigurations const [rooms, setRooms] = useState({ ...roomsAvailability, roomConfigurations: defaultRooms, }) + const [selectedPackages, setSelectedPackages] = useState( + [] + ) const handleFilter = useCallback( - (filter: Record) => { - const selectedCodes = Object.keys(filter).filter((key) => filter[key]) + (filter: Record) => { + const filteredPackages = Object.keys(filter).filter( + (key) => filter[key as RoomPackageCodeEnum] + ) as RoomPackageCodeEnum[] - if (selectedCodes.length === 0) { + setSelectedPackages(filteredPackages) + + if (filteredPackages.length === 0) { setRooms({ ...roomsAvailability, roomConfigurations: defaultRooms, @@ -39,8 +48,8 @@ export default function Rooms({ const filteredRooms = roomsAvailability.roomConfigurations.filter( (room) => - selectedCodes.every((selectedCode) => - room.features.some((feature) => feature.code === selectedCode) + filteredPackages.every((filteredPackage) => + room.features.some((feature) => feature.code === filteredPackage) ) ) setRooms({ ...roomsAvailability, roomConfigurations: filteredRooms }) @@ -60,6 +69,7 @@ export default function Rooms({ roomCategories={roomCategories} user={user} packages={packages} + selectedPackages={selectedPackages} />
    ) diff --git a/types/components/hotelReservation/selectRate/roomCard.ts b/types/components/hotelReservation/selectRate/roomCard.ts index a6ed91ac1..5af0c4bb1 100644 --- a/types/components/hotelReservation/selectRate/roomCard.ts +++ b/types/components/hotelReservation/selectRate/roomCard.ts @@ -5,11 +5,13 @@ import { import { Rate } from "./selectRate" -import { RoomData } from "@/types/hotel" +import type { RoomData } from "@/types/hotel" +import type { RoomPackageCodes } from "./roomFilter" export type RoomCardProps = { roomConfiguration: RoomConfiguration rateDefinitions: RateDefinition[] roomCategories: RoomData[] + selectedPackages: RoomPackageCodes[] handleSelectRate: (rate: Rate) => void } diff --git a/types/components/hotelReservation/selectRate/roomSelection.ts b/types/components/hotelReservation/selectRate/roomSelection.ts index 8d006779c..3e3a6117e 100644 --- a/types/components/hotelReservation/selectRate/roomSelection.ts +++ b/types/components/hotelReservation/selectRate/roomSelection.ts @@ -1,11 +1,12 @@ import type { RoomData } from "@/types/hotel" import type { SafeUser } from "@/types/user" import type { RoomsAvailability } from "@/server/routers/hotels/output" -import type { RoomPackageData } from "./roomFilter" +import type { RoomPackageCodes, RoomPackageData } from "./roomFilter" export interface RoomSelectionProps { roomsAvailability: RoomsAvailability roomCategories: RoomData[] user: SafeUser packages: RoomPackageData + selectedPackages: RoomPackageCodes[] } From 931828e15ec89c85086e94d7e89aaec3af6b23e7 Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Fri, 1 Nov 2024 10:50:44 +0100 Subject: [PATCH 033/120] fix: coupon rewards are no longer strictly surprises --- server/routers/contentstack/reward/output.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/contentstack/reward/output.ts b/server/routers/contentstack/reward/output.ts index bcb144442..5bd2c7d75 100644 --- a/server/routers/contentstack/reward/output.ts +++ b/server/routers/contentstack/reward/output.ts @@ -34,7 +34,7 @@ const SurpriseReward = z.object({ rewardId: z.string().optional(), redeemLocation: z.string().optional(), autoApplyReward: z.boolean().default(false), - rewardType: z.literal("Surprise"), + rewardType: z.string().optional(), endsAt: z.string().datetime({ offset: true }).optional(), coupons: z.array(Coupon).optional(), }) From 4fe380ad0f7ea0fe0b35799792b664261e5b5c3c Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Fri, 1 Nov 2024 14:20:22 +0100 Subject: [PATCH 034/120] feat(sw-628): Add selectedPackages to URL on submit --- .../HotelReservation/SelectRate/RoomSelection/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/index.tsx index efe5778d1..38745e241 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/index.tsx @@ -43,10 +43,13 @@ export default function RoomSelection({ rateSummary.member.rateCode ) } + if (selectedPackages.length > 0) { + params.set(`room[${index}].packages`, selectedPackages.join(",")) + } }) return params - }, [searchParams, rateSummary]) + }, [searchParams, rateSummary, selectedPackages]) function handleSubmit(e: React.FormEvent) { e.preventDefault() From b035fdd53eb90969421ccdfb83565015774e3ccf Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Thu, 31 Oct 2024 14:16:26 +0100 Subject: [PATCH 035/120] fix: removed useless itemprop and old data attributes removed references to removed classes --- components/Footer/Details/index.tsx | 5 ----- components/Header/MainMenu/index.tsx | 3 --- 2 files changed, 8 deletions(-) diff --git a/components/Footer/Details/index.tsx b/components/Footer/Details/index.tsx index d006f778c..59b9c2488 100644 --- a/components/Footer/Details/index.tsx +++ b/components/Footer/Details/index.tsx @@ -32,10 +32,6 @@ export default async function FooterDetails() { Scandic Hotels logo link.href && ( Date: Tue, 29 Oct 2024 13:35:13 +0100 Subject: [PATCH 036/120] fix: only phrasing content is allowed inside label element --- .../FormContent/Search/index.tsx | 10 +++-- .../FormContent/Voucher/index.tsx | 37 ++++++++++++------- .../Forms/BookingWidget/FormContent/index.tsx | 4 +- .../TempDesignSystem/Form/Checkbox/index.tsx | 8 ++-- 4 files changed, 35 insertions(+), 24 deletions(-) diff --git a/components/Forms/BookingWidget/FormContent/Search/index.tsx b/components/Forms/BookingWidget/FormContent/Search/index.tsx index 12d5e740c..5461426a7 100644 --- a/components/Forms/BookingWidget/FormContent/Search/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Search/index.tsx @@ -128,10 +128,12 @@ export default function Search({ locations }: SearchProps) { }) => (
    diff --git a/components/Forms/BookingWidget/FormContent/Voucher/index.tsx b/components/Forms/BookingWidget/FormContent/Voucher/index.tsx index 067f94c0c..dce678dda 100644 --- a/components/Forms/BookingWidget/FormContent/Voucher/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Voucher/index.tsx @@ -34,8 +34,8 @@ export default function Voucher() { >
    @@ -49,21 +49,30 @@ export default function Voucher() { arrow="left" >
    -
    diff --git a/components/Forms/BookingWidget/FormContent/index.tsx b/components/Forms/BookingWidget/FormContent/index.tsx index 002edb44a..d5dba4d47 100644 --- a/components/Forms/BookingWidget/FormContent/index.tsx +++ b/components/Forms/BookingWidget/FormContent/index.tsx @@ -50,8 +50,8 @@ export default function FormContent({
    diff --git a/components/TempDesignSystem/Form/Checkbox/index.tsx b/components/TempDesignSystem/Form/Checkbox/index.tsx index ee3967d8d..60bd81935 100644 --- a/components/TempDesignSystem/Form/Checkbox/index.tsx +++ b/components/TempDesignSystem/Form/Checkbox/index.tsx @@ -33,12 +33,12 @@ export default function Checkbox({ > {({ isSelected }) => ( <> -
    -
    + + {isSelected && } -
    + {children} -
    + {fieldState.error ? ( From f4d924e45fa83a2ae714f84f8b6fd70aa5800ac8 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Fri, 1 Nov 2024 12:51:08 +0100 Subject: [PATCH 037/120] fix(sw-350): Fixed styling issues and error modal issue --- components/BookingWidget/MobileToggleButton/index.tsx | 4 ++-- components/DatePicker/date-picker.module.css | 1 + .../FormContent/Search/SearchList/index.tsx | 10 ++++++++-- .../Forms/BookingWidget/FormContent/Search/index.tsx | 6 +++++- .../GuestsRoomsPicker/guests-rooms-picker.module.css | 1 + 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/components/BookingWidget/MobileToggleButton/index.tsx b/components/BookingWidget/MobileToggleButton/index.tsx index 9159ef400..80026ff93 100644 --- a/components/BookingWidget/MobileToggleButton/index.tsx +++ b/components/BookingWidget/MobileToggleButton/index.tsx @@ -80,7 +80,7 @@ export default function MobileToggleButton({ {!locationAndDateIsSet && ( <>
    - + {intl.formatMessage({ id: "Where to" })} @@ -91,7 +91,7 @@ export default function MobileToggleButton({
    - + {intl.formatMessage( { id: "booking.nights" }, { totalNights: nights } diff --git a/components/DatePicker/date-picker.module.css b/components/DatePicker/date-picker.module.css index b2e23cd41..ee9034648 100644 --- a/components/DatePicker/date-picker.module.css +++ b/components/DatePicker/date-picker.module.css @@ -14,6 +14,7 @@ outline: none; padding: 0; width: 100%; + text-align: left; } .body { diff --git a/components/Forms/BookingWidget/FormContent/Search/SearchList/index.tsx b/components/Forms/BookingWidget/FormContent/Search/SearchList/index.tsx index a8736e69d..978bf877a 100644 --- a/components/Forms/BookingWidget/FormContent/Search/SearchList/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Search/SearchList/index.tsx @@ -29,17 +29,23 @@ export default function SearchList({ }: SearchListProps) { const intl = useIntl() const [hasMounted, setHasMounted] = useState(false) + const [isFormSubmitted, setIsFormSubmitted] = useState(false) const { clearErrors, - formState: { errors }, + formState: { errors, isSubmitted }, } = useFormContext() const searchError = errors["search"] + useEffect(() => { + setIsFormSubmitted(isSubmitted) // Update form submission state + }, [isSubmitted]) + useEffect(() => { let timeoutID: ReturnType | null = null if (searchError && searchError.message === "Required") { timeoutID = setTimeout(() => { clearErrors("search") + setIsFormSubmitted(false) // magic number originates from animation // 5000ms delay + 120ms exectuion }, 5120) @@ -60,7 +66,7 @@ export default function SearchList({ return null } - if (searchError) { + if (searchError && isFormSubmitted) { if (typeof searchError.message === "string") { if (!isOpen) { if (searchError.message === "Required") { diff --git a/components/Forms/BookingWidget/FormContent/Search/index.tsx b/components/Forms/BookingWidget/FormContent/Search/index.tsx index 5461426a7..c3de24662 100644 --- a/components/Forms/BookingWidget/FormContent/Search/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Search/index.tsx @@ -128,7 +128,11 @@ export default function Search({ locations }: SearchProps) { }) => (
    diff --git a/components/Blocks/DynamicContent/Stays/Previous/EmptyPreviousStays/index.tsx b/components/Blocks/DynamicContent/Stays/Previous/EmptyPreviousStays/index.tsx index e80cab97c..e4da92974 100644 --- a/components/Blocks/DynamicContent/Stays/Previous/EmptyPreviousStays/index.tsx +++ b/components/Blocks/DynamicContent/Stays/Previous/EmptyPreviousStays/index.tsx @@ -7,7 +7,7 @@ export default async function EmptyPreviousStaysBlock() { const { formatMessage } = await getIntl() return (
    - + <Title as="h4" level="h3" color="red" textAlign="center"> {formatMessage({ id: "You have no previous stays.", })} diff --git a/components/Blocks/DynamicContent/Stays/Soonest/EmptyUpcomingStays/index.tsx b/components/Blocks/DynamicContent/Stays/Soonest/EmptyUpcomingStays/index.tsx index 1cd297438..0db81b6ce 100644 --- a/components/Blocks/DynamicContent/Stays/Soonest/EmptyUpcomingStays/index.tsx +++ b/components/Blocks/DynamicContent/Stays/Soonest/EmptyUpcomingStays/index.tsx @@ -14,7 +14,7 @@ export default async function EmptyUpcomingStaysBlock() { return ( <section className={styles.container}> <div className={styles.titleContainer}> - <Title as="h5" level="h3" color="red" className={styles.title}> + <Title as="h4" level="h3" color="red" className={styles.title}> {formatMessage({ id: "You have no upcoming stays." })} <span className={styles.burgundyTitle}> {formatMessage({ id: "Where should you go next?" })} diff --git a/components/Blocks/DynamicContent/Stays/StayCard/index.tsx b/components/Blocks/DynamicContent/Stays/StayCard/index.tsx index 9e63c0f77..59c768156 100644 --- a/components/Blocks/DynamicContent/Stays/StayCard/index.tsx +++ b/components/Blocks/DynamicContent/Stays/StayCard/index.tsx @@ -47,7 +47,7 @@ export default function StayCard({ stay }: StayCardProps) { height={240} /> <footer className={styles.footer}> - <Title as="h5" className={styles.hotel} level="h3"> + <Title as="h4" className={styles.hotel} level="h3"> {hotelInformation.hotelName}
    diff --git a/components/Blocks/DynamicContent/Stays/Upcoming/EmptyUpcomingStays/index.tsx b/components/Blocks/DynamicContent/Stays/Upcoming/EmptyUpcomingStays/index.tsx index 1cd297438..0db81b6ce 100644 --- a/components/Blocks/DynamicContent/Stays/Upcoming/EmptyUpcomingStays/index.tsx +++ b/components/Blocks/DynamicContent/Stays/Upcoming/EmptyUpcomingStays/index.tsx @@ -14,7 +14,7 @@ export default async function EmptyUpcomingStaysBlock() { return (
    - + <Title as="h4" level="h3" color="red" className={styles.title}> {formatMessage({ id: "You have no upcoming stays." })} <span className={styles.burgundyTitle}> {formatMessage({ id: "Where should you go next?" })} diff --git a/components/Current/Header/MyPagesMobileDropdown/index.tsx b/components/Current/Header/MyPagesMobileDropdown/index.tsx index 49c333f34..bf8c5a01e 100644 --- a/components/Current/Header/MyPagesMobileDropdown/index.tsx +++ b/components/Current/Header/MyPagesMobileDropdown/index.tsx @@ -34,7 +34,7 @@ export default function MyPagesMobileDropdown({ <nav className={`${styles.navigationMenu} ${isMyPagesMobileMenuOpen ? styles.navigationMenuIsOpen : ""}`} > - <Title className={styles.heading} textTransform="capitalize" level="h5"> + <Title className={styles.heading} textTransform="capitalize" level="h4"> {navigation.title} {navigation.menuItems.map((menuItem, idx) => ( diff --git a/components/DeprecatedJsonToHtml/renderOptions.tsx b/components/DeprecatedJsonToHtml/renderOptions.tsx index c22f469e0..732e2c321 100644 --- a/components/DeprecatedJsonToHtml/renderOptions.tsx +++ b/components/DeprecatedJsonToHtml/renderOptions.tsx @@ -198,7 +198,7 @@ export const renderOptions: RenderOptions = { ) => { const props = extractPossibleAttributes(node.attrs) return ( - + <Title key={node.uid} {...props} level="h4"> {next(node.children, embeds, fullRenderOptions)} ) diff --git a/components/HotelReservation/SelectRate/SelectionCard/index.tsx b/components/HotelReservation/SelectRate/SelectionCard/index.tsx index bf3b8caeb..e902993a1 100644 --- a/components/HotelReservation/SelectRate/SelectionCard/index.tsx +++ b/components/HotelReservation/SelectRate/SelectionCard/index.tsx @@ -20,7 +20,7 @@ export default function SelectionCard({ return (
    - + <Title className={styles.name} as="h4" level="h3"> {title}
    i
    diff --git a/components/JsonToHtml/renderOptions.tsx b/components/JsonToHtml/renderOptions.tsx index 2ce1afbec..65efe5a7a 100644 --- a/components/JsonToHtml/renderOptions.tsx +++ b/components/JsonToHtml/renderOptions.tsx @@ -221,7 +221,7 @@ export const renderOptions: RenderOptions = { ) => { const props = extractPossibleAttributes(node.attrs) return ( - + <Title key={node.uid} {...props} level="h4"> {next(node.children, embeds, fullRenderOptions)} ) diff --git a/components/TempDesignSystem/LoyaltyCard/index.tsx b/components/TempDesignSystem/LoyaltyCard/index.tsx index ea49366fc..fe26a51c7 100644 --- a/components/TempDesignSystem/LoyaltyCard/index.tsx +++ b/components/TempDesignSystem/LoyaltyCard/index.tsx @@ -35,7 +35,7 @@ export default function LoyaltyCard({ focalPoint={image.focalPoint} /> ) : null} - + <Title as="h4" level="h3" textAlign="center"> {heading} {bodyText ? {bodyText} : null} diff --git a/components/TempDesignSystem/Text/Title/title.module.css b/components/TempDesignSystem/Text/Title/title.module.css index d01be3335..5693c267c 100644 --- a/components/TempDesignSystem/Text/Title/title.module.css +++ b/components/TempDesignSystem/Text/Title/title.module.css @@ -4,7 +4,9 @@ font-weight: 500; } -.h1 { +/* Temporarily remove h1 styling until design tokens är updated */ + +/* .h1 { font-family: var(--typography-Title-1-fontFamily); font-size: clamp( var(--typography-Title-1-Mobile-fontSize), @@ -14,9 +16,9 @@ letter-spacing: var(--typography-Title-1-letterSpacing); line-height: var(--typography-Title-1-lineHeight); text-decoration: var(--typography-Title-1-textDecoration); -} +} */ -.h2 { +.h1 { font-family: var(--typography-Title-2-fontFamily); font-size: clamp( var(--typography-Title-2-Mobile-fontSize), @@ -28,7 +30,7 @@ text-decoration: var(--typography-Title-2-textDecoration); } -.h3 { +.h2 { font-family: var(--typography-Title-3-fontFamily); font-size: clamp( var(--typography-Title-3-Mobile-fontSize), @@ -40,7 +42,7 @@ text-decoration: var(--typography-Title-3-textDecoration); } -.h4 { +.h3 { font-family: var(--typography-Title-4-fontFamily); font-size: clamp( var(--typography-Title-4-Mobile-fontSize), @@ -52,7 +54,7 @@ text-decoration: var(--typography-Title-4-textDecoration); } -.h5 { +.h4 { font-family: var(--typography-Title-5-fontFamily); font-size: clamp( var(--typography-Title-5-Mobile-fontSize), diff --git a/components/TempDesignSystem/Text/Title/title.ts b/components/TempDesignSystem/Text/Title/title.ts index ce7789263..f4ab9eca3 100644 --- a/components/TempDesignSystem/Text/Title/title.ts +++ b/components/TempDesignSystem/Text/Title/title.ts @@ -2,7 +2,7 @@ import { headingVariants } from "./variants" import type { VariantProps } from "class-variance-authority" -type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" +type HeadingLevel = "h1" | "h2" | "h3" | "h4" export interface HeadingProps extends Omit, "color">, diff --git a/components/TempDesignSystem/Text/Title/variants.ts b/components/TempDesignSystem/Text/Title/variants.ts index 641394ceb..291c12486 100644 --- a/components/TempDesignSystem/Text/Title/variants.ts +++ b/components/TempDesignSystem/Text/Title/variants.ts @@ -26,7 +26,6 @@ const config = { h2: styles.h2, h3: styles.h3, h4: styles.h4, - h5: styles.h5, }, }, defaultVariants: { From 7f24dfc703aea5073b2157ce15053c36f424ddf7 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Fri, 1 Nov 2024 16:03:25 +0100 Subject: [PATCH 059/120] feat(SW-762): add font weights --- components/TempDesignSystem/Text/Title/title.module.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/components/TempDesignSystem/Text/Title/title.module.css b/components/TempDesignSystem/Text/Title/title.module.css index 5693c267c..b970de2bb 100644 --- a/components/TempDesignSystem/Text/Title/title.module.css +++ b/components/TempDesignSystem/Text/Title/title.module.css @@ -28,6 +28,7 @@ letter-spacing: var(--typography-Title-2-letterSpacing); line-height: var(--typography-Title-2-lineHeight); text-decoration: var(--typography-Title-2-textDecoration); + font-weight: var(--typography-Title-2-fontWeight); } .h2 { @@ -40,6 +41,7 @@ letter-spacing: var(--typography-Title-3-letterSpacing); line-height: var(--typography-Title-3-lineHeight); text-decoration: var(--typography-Title-3-textDecoration); + font-weight: var(--typography-Title-3-fontWeight); } .h3 { @@ -52,6 +54,7 @@ letter-spacing: var(--typography-Title-4-letterSpacing); line-height: var(--typography-Title-4-lineHeight); text-decoration: var(--typography-Title-4-textDecoration); + font-weight: var(--typography-Title-4-fontWeight); } .h4 { @@ -64,6 +67,7 @@ letter-spacing: var(--typography-Title-5-letterSpacing); line-height: var(--typography-Title-5-lineHeight); text-decoration: var(--typography-Title-5-textDecoration); + font-weight: var(--typography-Title-5-fontWeight); } .capitalize { From f84866da45578721552caca3bdac5b0005e39040 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Mon, 4 Nov 2024 10:17:48 +0100 Subject: [PATCH 060/120] feat(SW-762): add h5 level --- components/Blocks/DynamicContent/LoyaltyLevels/index.tsx | 2 +- components/TempDesignSystem/Text/Title/title.ts | 2 +- components/TempDesignSystem/Text/Title/variants.ts | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/components/Blocks/DynamicContent/LoyaltyLevels/index.tsx b/components/Blocks/DynamicContent/LoyaltyLevels/index.tsx index a2958c3e3..f23130e7d 100644 --- a/components/Blocks/DynamicContent/LoyaltyLevels/index.tsx +++ b/components/Blocks/DynamicContent/LoyaltyLevels/index.tsx @@ -51,7 +51,7 @@ async function LevelCard({ level }: LevelCardProps) { - + <Title textAlign="center" level="h5"> {pointsString} {level.required_nights ? ( <span className={styles.redText}> diff --git a/components/TempDesignSystem/Text/Title/title.ts b/components/TempDesignSystem/Text/Title/title.ts index f4ab9eca3..ce7789263 100644 --- a/components/TempDesignSystem/Text/Title/title.ts +++ b/components/TempDesignSystem/Text/Title/title.ts @@ -2,7 +2,7 @@ import { headingVariants } from "./variants" import type { VariantProps } from "class-variance-authority" -type HeadingLevel = "h1" | "h2" | "h3" | "h4" +type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" export interface HeadingProps extends Omit<React.HTMLAttributes<HTMLHeadingElement>, "color">, diff --git a/components/TempDesignSystem/Text/Title/variants.ts b/components/TempDesignSystem/Text/Title/variants.ts index 291c12486..f374daaf9 100644 --- a/components/TempDesignSystem/Text/Title/variants.ts +++ b/components/TempDesignSystem/Text/Title/variants.ts @@ -26,6 +26,7 @@ const config = { h2: styles.h2, h3: styles.h3, h4: styles.h4, + h5: styles.h4, }, }, defaultVariants: { From 103ea58d987abb6c691c6e7854605896ee34d1fd Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson <fredrik.thorsson@scandichotels.com> Date: Mon, 4 Nov 2024 10:22:41 +0100 Subject: [PATCH 061/120] feat(SW-762): change back heading level --- components/Current/Header/MyPagesMobileDropdown/index.tsx | 2 +- components/DeprecatedJsonToHtml/renderOptions.tsx | 2 +- components/JsonToHtml/renderOptions.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/Current/Header/MyPagesMobileDropdown/index.tsx b/components/Current/Header/MyPagesMobileDropdown/index.tsx index bf8c5a01e..49c333f34 100644 --- a/components/Current/Header/MyPagesMobileDropdown/index.tsx +++ b/components/Current/Header/MyPagesMobileDropdown/index.tsx @@ -34,7 +34,7 @@ export default function MyPagesMobileDropdown({ <nav className={`${styles.navigationMenu} ${isMyPagesMobileMenuOpen ? styles.navigationMenuIsOpen : ""}`} > - <Title className={styles.heading} textTransform="capitalize" level="h4"> + <Title className={styles.heading} textTransform="capitalize" level="h5"> {navigation.title} {navigation.menuItems.map((menuItem, idx) => ( diff --git a/components/DeprecatedJsonToHtml/renderOptions.tsx b/components/DeprecatedJsonToHtml/renderOptions.tsx index 732e2c321..c22f469e0 100644 --- a/components/DeprecatedJsonToHtml/renderOptions.tsx +++ b/components/DeprecatedJsonToHtml/renderOptions.tsx @@ -198,7 +198,7 @@ export const renderOptions: RenderOptions = { ) => { const props = extractPossibleAttributes(node.attrs) return ( - + <Title key={node.uid} {...props} level="h5"> {next(node.children, embeds, fullRenderOptions)} ) diff --git a/components/JsonToHtml/renderOptions.tsx b/components/JsonToHtml/renderOptions.tsx index 65efe5a7a..2ce1afbec 100644 --- a/components/JsonToHtml/renderOptions.tsx +++ b/components/JsonToHtml/renderOptions.tsx @@ -221,7 +221,7 @@ export const renderOptions: RenderOptions = { ) => { const props = extractPossibleAttributes(node.attrs) return ( - + <Title key={node.uid} {...props} level="h5"> {next(node.children, embeds, fullRenderOptions)} ) From 7f9af6c12e88a088d0293e23501c0c58650f4e3f Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Mon, 4 Nov 2024 14:20:46 +0100 Subject: [PATCH 062/120] feat(SW-697): Update package structure on price from API --- .../EnterDetails/Breakfast/index.tsx | 28 +++++------ .../EnterDetails/Summary/index.tsx | 4 +- .../FlexibilityOption/PriceList/index.tsx | 44 ++++++++++++++++-- .../FlexibilityOption/PriceList/utils.ts | 46 +++++++++++++++++++ .../RoomSelection/FlexibilityOption/index.tsx | 7 ++- .../RoomSelection/RateSummary/index.tsx | 4 +- .../RoomSelection/RoomCard/index.tsx | 7 +++ .../SelectRate/RoomSelection/index.tsx | 1 + server/routers/hotels/output.ts | 14 ++++-- server/routers/hotels/query.ts | 4 +- server/routers/hotels/schemas/packages.ts | 42 ++++++++--------- .../selectRate/flexibilityOption.ts | 5 ++ .../hotelReservation/selectRate/roomCard.ts | 19 +++++++- .../hotelReservation/selectRate/roomFilter.ts | 7 ++- 14 files changed, 178 insertions(+), 54 deletions(-) create mode 100644 components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/utils.ts diff --git a/components/HotelReservation/EnterDetails/Breakfast/index.tsx b/components/HotelReservation/EnterDetails/Breakfast/index.tsx index e8868edf7..00d5ab4cc 100644 --- a/components/HotelReservation/EnterDetails/Breakfast/index.tsx +++ b/components/HotelReservation/EnterDetails/Breakfast/index.tsx @@ -76,21 +76,21 @@ export default function Breakfast({ packages }: BreakfastProps) { subtitle={ pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST ? intl.formatMessage( - { id: "breakfast.price.free" }, - { - amount: pkg.originalPrice, - currency: pkg.currency, - free: (str) => {str}, - strikethrough: (str) => {str}, - } - ) + { id: "breakfast.price.free" }, + { + amount: pkg.localPrice.price, + currency: pkg.localPrice.currency, + free: (str) => {str}, + strikethrough: (str) => {str}, + } + ) : intl.formatMessage( - { id: "breakfast.price" }, - { - amount: pkg.packagePrice, - currency: pkg.currency, - } - ) + { id: "breakfast.price" }, + { + amount: pkg.localPrice.price, + currency: pkg.localPrice.currency, + } + ) } text={intl.formatMessage({ id: "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.", diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index 84c6280af..ca2891912 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -150,8 +150,8 @@ export default function Summary({ {intl.formatMessage( { id: "{amount} {currency}" }, { - amount: chosenBreakfast.totalPrice, - currency: chosenBreakfast.currency, + amount: chosenBreakfast.localPrice.price, + currency: chosenBreakfast.localPrice.currency, } )} diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx index dc7ca20fc..6ec68426f 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx @@ -1,9 +1,12 @@ +import { differenceInCalendarDays } from "date-fns" import { useIntl } from "react-intl" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import { calculatePricesPerNight } from "./utils" + import styles from "./priceList.module.css" import { PriceListProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption" @@ -11,6 +14,7 @@ import { PriceListProps } from "@/types/components/hotelReservation/selectRate/f export default function PriceList({ publicPrice = {}, memberPrice = {}, + petRoomPackage, }: PriceListProps) { const intl = useIntl() @@ -19,15 +23,45 @@ export default function PriceList({ const { localPrice: memberLocalPrice, requestedPrice: memberRequestedPrice } = memberPrice + const petRoomLocalPrice = petRoomPackage?.localPrice + const petRoomRequestedPrice = petRoomPackage?.requestedPrice + const showRequestedPrice = publicRequestedPrice && memberRequestedPrice + const searchParams = new URLSearchParams(window.location.search) + const fromDate = searchParams.get("fromDate") + const toDate = searchParams.get("toDate") + + let nights = 1 + + if (fromDate && toDate) { + nights = differenceInCalendarDays(new Date(fromDate), new Date(toDate)) + } + + const { + totalPublicLocalPricePerNight, + totalMemberLocalPricePerNight, + totalPublicRequestedPricePerNight, + totalMemberRequestedPricePerNight, + } = calculatePricesPerNight({ + publicLocalPrice, + memberLocalPrice, + publicRequestedPrice, + memberRequestedPrice, + petRoomLocalPrice, + petRoomRequestedPrice, + nights, + }) + return (
    {intl.formatMessage({ id: "Standard price" })} @@ -36,7 +70,7 @@ export default function PriceList({ {publicLocalPrice ? (
    - {publicLocalPrice.pricePerNight} + {totalPublicLocalPricePerNight} {publicLocalPrice.currency} @@ -63,7 +97,7 @@ export default function PriceList({ {memberLocalPrice ? (
    - {memberLocalPrice.pricePerNight} + {totalMemberLocalPricePerNight} {memberLocalPrice.currency} @@ -91,8 +125,8 @@ export default function PriceList({
    {showRequestedPrice ? ( - {publicRequestedPrice.pricePerNight}/ - {memberRequestedPrice.pricePerNight}{" "} + {totalPublicRequestedPricePerNight}/ + {totalMemberRequestedPricePerNight}{" "} {publicRequestedPrice.currency} ) : ( diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/utils.ts b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/utils.ts new file mode 100644 index 000000000..4043aa652 --- /dev/null +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/utils.ts @@ -0,0 +1,46 @@ +import type { CalculatePricesPerNightProps } from "@/types/components/hotelReservation/selectRate/roomCard" + +export function calculatePricesPerNight({ + publicLocalPrice, + memberLocalPrice, + publicRequestedPrice, + memberRequestedPrice, + petRoomLocalPrice, + petRoomRequestedPrice, + nights, +}: CalculatePricesPerNightProps) { + const totalPublicLocalPricePerNight = publicLocalPrice + ? petRoomLocalPrice + ? Number(publicLocalPrice.pricePerNight) + + Number(petRoomLocalPrice.price) / nights + : Number(publicLocalPrice.pricePerNight) + : undefined + + const totalMemberLocalPricePerNight = memberLocalPrice + ? petRoomLocalPrice + ? Number(memberLocalPrice.pricePerNight) + + Number(petRoomLocalPrice.price) / nights + : Number(memberLocalPrice.pricePerNight) + : undefined + + const totalPublicRequestedPricePerNight = publicRequestedPrice + ? petRoomRequestedPrice + ? Number(publicRequestedPrice.pricePerNight) + + Number(petRoomRequestedPrice.price) / nights + : Number(publicRequestedPrice.pricePerNight) + : undefined + + const totalMemberRequestedPricePerNight = memberRequestedPrice + ? petRoomRequestedPrice + ? Number(memberRequestedPrice.pricePerNight) + + Number(petRoomRequestedPrice.price) / nights + : Number(memberRequestedPrice.pricePerNight) + : undefined + + return { + totalPublicLocalPricePerNight, + totalMemberLocalPricePerNight, + totalPublicRequestedPricePerNight, + totalMemberRequestedPricePerNight, + } +} diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx index a0a92bb66..f1781d978 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx @@ -20,6 +20,7 @@ export default function FlexibilityOption({ roomType, roomTypeCode, features, + petRoomPackage, handleSelectRate, }: FlexibilityOptionProps) { const [rootDiv, setRootDiv] = useState(undefined) @@ -113,7 +114,11 @@ export default function FlexibilityOption({ {name} ({paymentTerm})
    - + pkg.code === RoomPackageCodeEnum.PET_ROOM ) - const petRoomPrice = petRoomPackage?.calculatedPrice ?? null - const petRoomCurrency = petRoomPackage?.currency ?? null + const petRoomPrice = petRoomPackage?.localPrice.totalPrice ?? null + const petRoomCurrency = petRoomPackage?.localPrice.currency ?? null const checkInDate = new Date(roomsAvailability.checkInDate) const checkOutDate = new Date(roomsAvailability.checkOutDate) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index 4eae4293c..530af45da 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -18,12 +18,14 @@ import { getIconForFeatureCode } from "../../utils" import styles from "./roomCard.module.css" import type { RoomCardProps } from "@/types/components/hotelReservation/selectRate/roomCard" +import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" export default function RoomCard({ rateDefinitions, roomConfiguration, roomCategories, selectedPackages, + packages, handleSelectRate, }: RoomCardProps) { const intl = useIntl() @@ -55,6 +57,10 @@ export default function RoomCard({ ?.generalTerms } + const petRoomPackage = packages.find( + (pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM + ) + const selectedRoom = roomCategories.find( (room) => room.name === roomConfiguration.roomType ) @@ -118,6 +124,7 @@ export default function RoomCard({ roomType={roomConfiguration.roomType} roomTypeCode={roomConfiguration.roomTypeCode} features={roomConfiguration.features} + petRoomPackage={petRoomPackage} /> ))}
    diff --git a/components/HotelReservation/SelectRate/RoomSelection/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/index.tsx index 38745e241..112f69f76 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/index.tsx @@ -72,6 +72,7 @@ export default function RoomSelection({ roomCategories={roomCategories} handleSelectRate={setRateSummary} selectedPackages={selectedPackages} + packages={packages} /> ))} diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 1b49b5fbf..dad7abe49 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -488,7 +488,7 @@ export type HotelsAvailability = z.infer export type HotelsAvailabilityPrices = HotelsAvailability["data"][number]["attributes"]["bestPricePerNight"] -const priceSchema = z.object({ +export const priceSchema = z.object({ pricePerNight: z.string(), pricePerStay: z.string(), currency: z.string(), @@ -774,17 +774,21 @@ export const apiLocationsSchema = z.object({ ), }) +const breakfastPackagePriceSchema = z.object({ + currency: z.nativeEnum(CurrencyEnum), + price: z.string(), + totalPrice: z.string(), +}) + export const breakfastPackageSchema = z.object({ code: z.string(), - currency: z.nativeEnum(CurrencyEnum), description: z.string(), - originalPrice: z.number().default(0), - packagePrice: z.number(), + localPrice: breakfastPackagePriceSchema, + requestedPrice: breakfastPackagePriceSchema, packageType: z.enum([ PackageTypeEnum.BreakfastAdult, PackageTypeEnum.BreakfastChildren, ]), - totalPrice: z.number(), }) export const breakfastPackagesSchema = z diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 627ad1f8d..416b22d49 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -1086,8 +1086,8 @@ export const hotelQueryRouter = router({ ) if (freeBreakfastPackage) { if (originalBreakfastPackage) { - freeBreakfastPackage.originalPrice = - originalBreakfastPackage.packagePrice + freeBreakfastPackage.localPrice.price = + originalBreakfastPackage.localPrice.price } return [freeBreakfastPackage] } diff --git a/server/routers/hotels/schemas/packages.ts b/server/routers/hotels/schemas/packages.ts index 2f12f7255..9de3c0574 100644 --- a/server/routers/hotels/schemas/packages.ts +++ b/server/routers/hotels/schemas/packages.ts @@ -11,33 +11,33 @@ export const getRoomPackagesInputSchema = z.object({ packageCodes: z.array(z.string()).optional().default([]), }) -const packagesSchema = z.array( - z.object({ - code: z.enum([ - RoomPackageCodeEnum.PET_ROOM, - RoomPackageCodeEnum.ALLERGY_ROOM, - RoomPackageCodeEnum.ACCESSIBILITY_ROOM, - ]), - itemCode: z.string(), - description: z.string(), - currency: z.string(), - calculatedPrice: z.number(), - inventories: z.array( - z.object({ - date: z.string(), - total: z.number(), - available: z.number(), - }) - ), - }) -) +export const packagePriceSchema = z.object({ + currency: z.string(), + price: z.string(), + totalPrice: z.string(), +}) + +export const packagesSchema = z.object({ + code: z.nativeEnum(RoomPackageCodeEnum), + itemCode: z.string(), + description: z.string(), + localPrice: packagePriceSchema, + requestedPrice: packagePriceSchema, + inventories: z.array( + z.object({ + date: z.string(), + total: z.number(), + available: z.number(), + }) + ), +}) export const getRoomPackagesSchema = z .object({ data: z.object({ attributes: z.object({ hotelId: z.number(), - packages: packagesSchema, + packages: z.array(packagesSchema), }), relationships: z .object({ diff --git a/types/components/hotelReservation/selectRate/flexibilityOption.ts b/types/components/hotelReservation/selectRate/flexibilityOption.ts index 1a432dc32..811afc139 100644 --- a/types/components/hotelReservation/selectRate/flexibilityOption.ts +++ b/types/components/hotelReservation/selectRate/flexibilityOption.ts @@ -1,14 +1,17 @@ import { z } from "zod" import { + priceSchema, Product, productTypePriceSchema, RoomConfiguration, } from "@/server/routers/hotels/output" +import { RoomPackage } from "./roomFilter" import { Rate } from "./selectRate" type ProductPrice = z.output +export type RoomPriceSchema = z.output export type FlexibilityOptionProps = { product: Product | undefined @@ -19,10 +22,12 @@ export type FlexibilityOptionProps = { roomType: RoomConfiguration["roomType"] roomTypeCode: RoomConfiguration["roomTypeCode"] features: RoomConfiguration["features"] + petRoomPackage: RoomPackage | undefined handleSelectRate: (rate: Rate) => void } export interface PriceListProps { publicPrice?: ProductPrice | Record memberPrice?: ProductPrice | Record + petRoomPackage?: RoomPackage | undefined } diff --git a/types/components/hotelReservation/selectRate/roomCard.ts b/types/components/hotelReservation/selectRate/roomCard.ts index 5af0c4bb1..b47a8c5bb 100644 --- a/types/components/hotelReservation/selectRate/roomCard.ts +++ b/types/components/hotelReservation/selectRate/roomCard.ts @@ -1,17 +1,34 @@ +import { z } from "zod" + import { RateDefinition, RoomConfiguration, } from "@/server/routers/hotels/output" +import { packagePriceSchema } from "@/server/routers/hotels/schemas/packages" +import { RoomPriceSchema } from "./flexibilityOption" import { Rate } from "./selectRate" import type { RoomData } from "@/types/hotel" -import type { RoomPackageCodes } from "./roomFilter" +import type { RoomPackageCodes, RoomPackageData } from "./roomFilter" export type RoomCardProps = { roomConfiguration: RoomConfiguration rateDefinitions: RateDefinition[] roomCategories: RoomData[] selectedPackages: RoomPackageCodes[] + packages: RoomPackageData handleSelectRate: (rate: Rate) => void } + +type RoomPackagePriceSchema = z.output + +export type CalculatePricesPerNightProps = { + publicLocalPrice: RoomPriceSchema + memberLocalPrice: RoomPriceSchema + publicRequestedPrice?: RoomPriceSchema + memberRequestedPrice?: RoomPriceSchema + petRoomLocalPrice?: RoomPackagePriceSchema + petRoomRequestedPrice?: RoomPackagePriceSchema + nights: number +} diff --git a/types/components/hotelReservation/selectRate/roomFilter.ts b/types/components/hotelReservation/selectRate/roomFilter.ts index d42669295..8250e7f32 100644 --- a/types/components/hotelReservation/selectRate/roomFilter.ts +++ b/types/components/hotelReservation/selectRate/roomFilter.ts @@ -1,6 +1,9 @@ import { z } from "zod" -import { getRoomPackagesSchema } from "@/server/routers/hotels/schemas/packages" +import { + getRoomPackagesSchema, + packagesSchema, +} from "@/server/routers/hotels/schemas/packages" export enum RoomPackageCodeEnum { PET_ROOM = "PETR", @@ -17,3 +20,5 @@ export interface RoomPackageData extends z.output {} export type RoomPackageCodes = RoomPackageData[number]["code"] + +export type RoomPackage = z.output From 83c1178ba51b50aa4a272721b70b35c6f2efb5cd Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Mon, 4 Nov 2024 14:31:41 +0100 Subject: [PATCH 063/120] feat(sw-697): added language to request --- server/routers/hotels/query.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 416b22d49..f282840da 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -913,11 +913,16 @@ export const hotelQueryRouter = router({ const { hotelId, startDate, endDate, adults, children, packageCodes } = input + const { lang } = ctx + + const apiLang = toApiLang(lang) + const searchParams = new URLSearchParams({ startDate, endDate, adults: adults.toString(), children: children.toString(), + language: apiLang, }) packageCodes.forEach((code) => { @@ -993,11 +998,17 @@ export const hotelQueryRouter = router({ breakfast: safeProtectedServiceProcedure .input(getBreakfastPackageInputSchema) .query(async function ({ ctx, input }) { + const { lang } = ctx + + const apiLang = toApiLang(lang) + const params = { Adults: input.adults, EndDate: dt(input.toDate).format("YYYY-MM-DD"), StartDate: dt(input.fromDate).format("YYYY-MM-DD"), + language: apiLang, } + const metricsData = { ...params, hotelId: input.hotelId } breakfastPackagesCounter.add(1, metricsData) console.info( From 18eeeef51083fba0fa87cba5ce25fb648d5f3b89 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Fri, 25 Oct 2024 09:17:07 +0200 Subject: [PATCH 064/120] feat(SW-663): Updated @contentstack/live-preview-utils --- package-lock.json | 55 +++++++++++++++++++++++++++++++++++++++++++---- package.json | 2 +- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index c1b1e5c7b..36f965c48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "dependencies": { "@azure/monitor-opentelemetry-exporter": "^1.0.0-beta.24", - "@contentstack/live-preview-utils": "^1.4.0", + "@contentstack/live-preview-utils": "^2.0.4", "@hookform/error-message": "^2.0.1", "@hookform/resolvers": "^3.3.4", "@netlify/plugin-nextjs": "^5.1.1", @@ -2459,13 +2459,16 @@ } }, "node_modules/@contentstack/live-preview-utils": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@contentstack/live-preview-utils/-/live-preview-utils-1.4.1.tgz", - "integrity": "sha512-CbC+Wtc+t8NkD3qTTZaxl7KHCzNCrnKtpkPoarxig1ffyGk8GeEZwW5tLyEtqQ7zsvwLA+RIuWY55gKQumJQLQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@contentstack/live-preview-utils/-/live-preview-utils-2.0.4.tgz", + "integrity": "sha512-+56ExpzsOau5MEEg6sy1czaQMzPmEZAyGYx+LIyqeKKCbxFmTHcvg4Ag4vJ7KUf4LCtoGMUUT/Mkf6hqSkCvDA==", "dependencies": { + "goober": "^2.1.14", "just-camel-case": "^4.0.2", + "lodash-es": "^4.17.21", "morphdom": "^2.6.1", "mustache": "^4.2.0", + "post-robot": "8.0.31", "uuid": "^8.3.2" } }, @@ -8750,6 +8753,22 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/cross-domain-safe-weakmap": { + "version": "1.0.29", + "resolved": "https://registry.npmjs.org/cross-domain-safe-weakmap/-/cross-domain-safe-weakmap-1.0.29.tgz", + "integrity": "sha512-VLoUgf2SXnf3+na8NfeUFV59TRZkIJqCIATaMdbhccgtnTlSnHXkyTRwokngEGYdQXx8JbHT9GDYitgR2sdjuA==", + "dependencies": { + "cross-domain-utils": "^2.0.0" + } + }, + "node_modules/cross-domain-utils": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/cross-domain-utils/-/cross-domain-utils-2.0.38.tgz", + "integrity": "sha512-zZfi3+2EIR9l4chrEiXI2xFleyacsJf8YMLR1eJ0Veb5FTMXeJ3DpxDjZkto2FhL/g717WSELqbptNSo85UJDw==", + "dependencies": { + "zalgo-promise": "^1.0.11" + } + }, "node_modules/cross-fetch": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", @@ -11091,6 +11110,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/goober": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -14599,6 +14626,11 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "node_modules/lodash.assignwith": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.assignwith/-/lodash.assignwith-4.2.0.tgz", @@ -16244,6 +16276,16 @@ "node": ">= 0.4" } }, + "node_modules/post-robot": { + "version": "8.0.31", + "resolved": "https://registry.npmjs.org/post-robot/-/post-robot-8.0.31.tgz", + "integrity": "sha512-nUhtKgtmcgyuPm4RnIhUB3gsDYJBHOgFry3TvOxhIHpgfwYY/T69d4oB90tw4YUllFZUUwqLEv1Wgyg6eOoJ7A==", + "dependencies": { + "cross-domain-safe-weakmap": "^1.0.1", + "cross-domain-utils": "^2.0.0", + "zalgo-promise": "^1.0.3" + } + }, "node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -19978,6 +20020,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zalgo-promise": { + "version": "1.0.48", + "resolved": "https://registry.npmjs.org/zalgo-promise/-/zalgo-promise-1.0.48.tgz", + "integrity": "sha512-LLHANmdm53+MucY9aOFIggzYtUdkSBFxUsy4glTTQYNyK6B3uCPWTbfiGvSrEvLojw0mSzyFJ1/RRLv+QMNdzQ==" + }, "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 0f8bd14a2..f5f7ea4cb 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@azure/monitor-opentelemetry-exporter": "^1.0.0-beta.24", - "@contentstack/live-preview-utils": "^1.4.0", + "@contentstack/live-preview-utils": "^2.0.4", "@hookform/error-message": "^2.0.1", "@hookform/resolvers": "^3.3.4", "@netlify/plugin-nextjs": "^5.1.1", From bc93fcaefd641606137e31fc3ae2c87badc09a7f Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Fri, 25 Oct 2024 11:28:55 +0200 Subject: [PATCH 065/120] feat(SW-508): preview for content pages --- app/[lang]/(preview)/layout.tsx | 23 ++--- .../preview/[contentType]/[uid]/page.tsx | 43 ++++++++-- app/[lang]/(preview-current)/layout.tsx | 2 +- .../preview-current/page.tsx | 2 +- .../{Current => }/LivePreview/index.tsx | 2 +- lib/graphql/_request.ts | 83 +++++++++++-------- lib/graphql/previewRequest.ts | 2 +- middlewares/cmsContent.ts | 17 ++-- 8 files changed, 108 insertions(+), 66 deletions(-) rename components/{Current => }/LivePreview/index.tsx (75%) diff --git a/app/[lang]/(preview)/layout.tsx b/app/[lang]/(preview)/layout.tsx index 25727e7d6..948bcceba 100644 --- a/app/[lang]/(preview)/layout.tsx +++ b/app/[lang]/(preview)/layout.tsx @@ -1,26 +1,29 @@ -import InitLivePreview from "@/components/Current/LivePreview" -import { setLang } from "@/i18n/serverContext" +import "@/app/globals.css" +import "@scandic-hotels/design-system/style.css" -import type { Metadata } from "next" +import TrpcProvider from "@/lib/trpc/Provider" + +import InitLivePreview from "@/components/LivePreview" +import { getIntl } from "@/i18n" +import ServerIntlProvider from "@/i18n/Provider" +import { setLang } from "@/i18n/serverContext" import type { LangParams, LayoutArgs } from "@/types/params" -export const metadata: Metadata = { - description: "New web", - title: "Scandic Hotels", -} - -export default function RootLayout({ +export default async function RootLayout({ children, params, }: React.PropsWithChildren>) { setLang(params.lang) + const { defaultLocale, locale, messages } = await getIntl() return ( - {children} + + {children} + ) diff --git a/app/[lang]/(preview)/preview/[contentType]/[uid]/page.tsx b/app/[lang]/(preview)/preview/[contentType]/[uid]/page.tsx index fbdfa4893..26216140e 100644 --- a/app/[lang]/(preview)/preview/[contentType]/[uid]/page.tsx +++ b/app/[lang]/(preview)/preview/[contentType]/[uid]/page.tsx @@ -1,6 +1,13 @@ +import { ContentstackLivePreview } from "@contentstack/live-preview-utils" +import { notFound } from "next/navigation" + +import ContentPage from "@/components/ContentType/ContentPage" +import HotelPage from "@/components/ContentType/HotelPage" +import LoyaltyPage from "@/components/ContentType/LoyaltyPage" +import LoadingSpinner from "@/components/LoadingSpinner" import { setLang } from "@/i18n/serverContext" -import { +import type { ContentTypeParams, LangParams, PageArgs, @@ -13,12 +20,30 @@ export default async function PreviewPage({ }: PageArgs) { setLang(params.lang) - return ( -
    -

    - Preview for {params.contentType}:{params.uid} in {params.lang} with - params

    {JSON.stringify(searchParams, null, 2)}
    goes here -

    -
    - ) + try { + ContentstackLivePreview.setConfigFromParams(searchParams) + + if (!searchParams.live_preview) { + return + } + + switch (params.contentType) { + case "content-page": + return + case "loyalty-page": + return + case "hotel-page": + return + default: + console.log({ PREVIEW: params }) + const type: never = params.contentType + console.error(`Unsupported content type given: ${type}`) + notFound() + } + } catch (error) { + // TODO: throw 500 + console.error("Error in preview page") + console.error(error) + throw new Error("Something went wrong") + } } diff --git a/app/[lang]/(preview-current)/layout.tsx b/app/[lang]/(preview-current)/layout.tsx index 3e98666c8..e5ff698bb 100644 --- a/app/[lang]/(preview-current)/layout.tsx +++ b/app/[lang]/(preview-current)/layout.tsx @@ -1,6 +1,6 @@ import Footer from "@/components/Current/Footer" import LangPopup from "@/components/Current/LangPopup" -import InitLivePreview from "@/components/Current/LivePreview" +import InitLivePreview from "@/components/LivePreview" import SkipToMainContent from "@/components/SkipToMainContent" import { setLang } from "@/i18n/serverContext" diff --git a/app/[lang]/(preview-current)/preview-current/page.tsx b/app/[lang]/(preview-current)/preview-current/page.tsx index f8ca81201..82c1e9c45 100644 --- a/app/[lang]/(preview-current)/preview-current/page.tsx +++ b/app/[lang]/(preview-current)/preview-current/page.tsx @@ -1,4 +1,4 @@ -import ContentstackLivePreview from "@contentstack/live-preview-utils" +import { ContentstackLivePreview } from "@contentstack/live-preview-utils" import { previewRequest } from "@/lib/graphql/previewRequest" import { GetCurrentBlockPage } from "@/lib/graphql/Query/Current/CurrentBlockPage.graphql" diff --git a/components/Current/LivePreview/index.tsx b/components/LivePreview/index.tsx similarity index 75% rename from components/Current/LivePreview/index.tsx rename to components/LivePreview/index.tsx index c75d9bc66..5258da67b 100644 --- a/components/Current/LivePreview/index.tsx +++ b/components/LivePreview/index.tsx @@ -1,6 +1,6 @@ "use client" -import ContentstackLivePreview from "@contentstack/live-preview-utils" +import { ContentstackLivePreview } from "@contentstack/live-preview-utils" import { useEffect } from "react" export default function InitLivePreview() { diff --git a/lib/graphql/_request.ts b/lib/graphql/_request.ts index 4040dfd9e..814fe045c 100644 --- a/lib/graphql/_request.ts +++ b/lib/graphql/_request.ts @@ -1,5 +1,6 @@ import "server-only" +import { ContentstackLivePreview } from "@contentstack/live-preview-utils" import { ClientError, GraphQLClient } from "graphql-request" import { Lang } from "@/constants/languages" @@ -16,40 +17,54 @@ export async function request( params?: RequestInit ): Promise> { try { - if (params?.cache) { - client.requestConfig.cache = params.cache - } - if (params?.headers) { - client.requestConfig.headers = params.headers - } - if (params?.next) { - client.requestConfig.next = params.next - } + client.setHeaders({ + access_token: env.CMS_ACCESS_TOKEN, + "Content-Type": "application/json", + }) - if (env.PRINT_QUERY) { - const print = (await import("graphql/language/printer")).print - const rawResponse = await client.rawRequest( - print(query as DocumentNode), - variables, - { - access_token: env.CMS_ACCESS_TOKEN, - "Content-Type": "application/json", + const previewHash = ContentstackLivePreview.hash + if (previewHash) { + client.setEndpoint(env.CMS_PREVIEW_URL) + client.setHeader("preview_token", env.CMS_PREVIEW_TOKEN) + client.setHeader("live_preview", previewHash) + } else { + if (params?.cache) { + client.requestConfig.cache = params.cache + } + if (params?.headers) { + client.requestConfig.headers = params.headers + } + if (params?.next) { + client.requestConfig.next = params.next + } + + if (env.PRINT_QUERY) { + const print = (await import("graphql/language/printer")).print + const rawResponse = await client.rawRequest( + print(query as DocumentNode), + variables, + { + access_token: env.CMS_ACCESS_TOKEN, + "Content-Type": "application/json", + } + ) + + /** + * TODO: Send to Monitoring (Logging and Metrics) + */ + console.log({ + complexityLimit: rawResponse.headers.get("x-query-complexity"), + }) + console.log({ + referenceDepth: rawResponse.headers.get("x-reference-depth"), + }) + console.log({ + resolverCost: rawResponse.headers.get("x-resolver-cost"), + }) + + return { + data: rawResponse.data, } - ) - - /** - * TODO: Send to Monitoring (Logging and Metrics) - */ - console.log({ - complexityLimit: rawResponse.headers.get("x-query-complexity"), - }) - console.log({ - referenceDepth: rawResponse.headers.get("x-reference-depth"), - }) - console.log({ resolverCost: rawResponse.headers.get("x-resolver-cost") }) - - return { - data: rawResponse.data, } } @@ -78,10 +93,6 @@ export async function request( const response = await client.request({ document: query, - requestHeaders: { - access_token: env.CMS_ACCESS_TOKEN, - "Content-Type": "application/json", - }, variables, }) diff --git a/lib/graphql/previewRequest.ts b/lib/graphql/previewRequest.ts index eadf3fbba..2ea947ad6 100644 --- a/lib/graphql/previewRequest.ts +++ b/lib/graphql/previewRequest.ts @@ -1,6 +1,6 @@ import "server-only" -import ContentstackLivePreview from "@contentstack/live-preview-utils" +import { ContentstackLivePreview } from "@contentstack/live-preview-utils" import { request as graphqlRequest } from "graphql-request" import { env } from "@/env/server" diff --git a/middlewares/cmsContent.ts b/middlewares/cmsContent.ts index 3d931ebbe..d08c12af5 100644 --- a/middlewares/cmsContent.ts +++ b/middlewares/cmsContent.ts @@ -2,7 +2,6 @@ import { NextResponse } from "next/server" import { notFound } from "@/server/errors/next" -import { resolve as resolveEntry } from "@/utils/entry" import { findLang } from "@/utils/languages" import { removeTrailingSlash } from "@/utils/url" @@ -18,17 +17,21 @@ export const middleware: NextMiddleware = async (request) => { const pathWithoutTrailingSlash = removeTrailingSlash(nextUrl.pathname) - const pathNameWithoutLang = pathWithoutTrailingSlash.replace(`/${lang}`, "") + const contentTypePathName = pathWithoutTrailingSlash.replace(`/${lang}`, "") + const isPreview = request.nextUrl.pathname.includes("/preview") + const searchParams = new URLSearchParams(request.nextUrl.searchParams) const { contentType, uid } = await fetchAndCacheEntry( - pathNameWithoutLang, + isPreview + ? contentTypePathName.replace("/preview", "") + : contentTypePathName, lang ) if (!contentType || !uid) { throw notFound( - `Unable to resolve CMS entry for locale "${lang}": ${pathNameWithoutLang}` + `Unable to resolve CMS entry for locale "${lang}": ${contentTypePathName}` ) } const headers = getDefaultRequestHeaders(request) @@ -37,9 +40,9 @@ export const middleware: NextMiddleware = async (request) => { const isCurrent = contentType ? contentType.indexOf("current") >= 0 : false - if (request.nextUrl.pathname.includes("/preview")) { + if (isPreview) { if (isCurrent) { - searchParams.set("uri", pathNameWithoutLang.replace("/preview", "")) + searchParams.set("uri", contentTypePathName.replace("/preview", "")) return NextResponse.rewrite( new URL(`/${lang}/preview-current?${searchParams.toString()}`, nextUrl), { @@ -65,7 +68,7 @@ export const middleware: NextMiddleware = async (request) => { if (isCurrent) { searchParams.set("uid", uid) - searchParams.set("uri", pathNameWithoutLang) + searchParams.set("uri", contentTypePathName) return NextResponse.rewrite( new URL( `/${lang}/current-content-page?${searchParams.toString()}`, From 0854881c2c0ee87d93c121c8d6f137aa9b30e893 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Mon, 4 Nov 2024 13:23:26 +0100 Subject: [PATCH 066/120] fix(SW-217): Styling fixes for teaser-card --- .../preview/[contentType]/[uid]/page.tsx | 7 +++-- components/Sidebar/index.tsx | 2 +- .../TempDesignSystem/TeaserCard/index.tsx | 24 ++++++++-------- .../TeaserCard/teaserCard.module.css | 28 ++++--------------- .../TempDesignSystem/TeaserCard/variants.ts | 4 +-- 5 files changed, 24 insertions(+), 41 deletions(-) diff --git a/app/[lang]/(preview)/preview/[contentType]/[uid]/page.tsx b/app/[lang]/(preview)/preview/[contentType]/[uid]/page.tsx index 26216140e..ff954a31d 100644 --- a/app/[lang]/(preview)/preview/[contentType]/[uid]/page.tsx +++ b/app/[lang]/(preview)/preview/[contentType]/[uid]/page.tsx @@ -1,9 +1,10 @@ import { ContentstackLivePreview } from "@contentstack/live-preview-utils" import { notFound } from "next/navigation" -import ContentPage from "@/components/ContentType/ContentPage" import HotelPage from "@/components/ContentType/HotelPage" import LoyaltyPage from "@/components/ContentType/LoyaltyPage" +import CollectionPage from "@/components/ContentType/StaticPages/CollectionPage" +import ContentPage from "@/components/ContentType/StaticPages/ContentPage" import LoadingSpinner from "@/components/LoadingSpinner" import { setLang } from "@/i18n/serverContext" @@ -32,11 +33,13 @@ export default async function PreviewPage({ return case "loyalty-page": return + case "collection-page": + return case "hotel-page": return default: console.log({ PREVIEW: params }) - const type: never = params.contentType + const type = params.contentType console.error(`Unsupported content type given: ${type}`) notFound() } diff --git a/components/Sidebar/index.tsx b/components/Sidebar/index.tsx index 02e317e54..53e88126e 100644 --- a/components/Sidebar/index.tsx +++ b/components/Sidebar/index.tsx @@ -60,7 +60,7 @@ export default function Sidebar({ blocks }: SidebarProps) { +
    {image && ( -
    - {image.meta?.alt -
    + {image.meta?.alt )}
    diff --git a/components/TempDesignSystem/TeaserCard/teaserCard.module.css b/components/TempDesignSystem/TeaserCard/teaserCard.module.css index d1bc40165..49f084d20 100644 --- a/components/TempDesignSystem/TeaserCard/teaserCard.module.css +++ b/components/TempDesignSystem/TeaserCard/teaserCard.module.css @@ -18,24 +18,17 @@ border: 1px solid var(--Base-Border-Subtle); } -.imageContainer { +.image { width: 100%; - height: 12.58625rem; /* 201.38px / 16 = 12.58625rem */ - overflow: hidden; -} - -.backgroundImage { - width: 100%; - height: 100%; - object-fit: cover; + height: 12.5rem; /* 200px */ } .content { - display: flex; - flex-direction: column; + display: grid; gap: var(--Spacing-x-one-and-half); - align-items: flex-start; padding: var(--Spacing-x2) var(--Spacing-x3); + grid-template-rows: auto 1fr auto; + flex-grow: 1; } .description { @@ -53,17 +46,6 @@ width: 100%; } -.body { - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 3; - line-clamp: 3; - overflow: hidden; - text-overflow: ellipsis; - /* line-height variables are in %, so using the value in rem instead */ - max-height: calc(3 * 1.5rem); -} - @media (min-width: 1367px) { .card:not(.alwaysStack) .ctaContainer { grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); diff --git a/components/TempDesignSystem/TeaserCard/variants.ts b/components/TempDesignSystem/TeaserCard/variants.ts index b6ca7744d..294f6d913 100644 --- a/components/TempDesignSystem/TeaserCard/variants.ts +++ b/components/TempDesignSystem/TeaserCard/variants.ts @@ -4,7 +4,7 @@ import styles from "./teaserCard.module.css" export const teaserCardVariants = cva(styles.card, { variants: { - style: { + intent: { default: styles.default, featured: styles.featured, }, @@ -14,7 +14,7 @@ export const teaserCardVariants = cva(styles.card, { }, }, defaultVariants: { - style: "default", + intent: "default", alwaysStack: false, }, }) From 00d3d1e34fd10fb7917cde3c5cd03a7bc628db4c Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Mon, 4 Nov 2024 15:18:51 +0100 Subject: [PATCH 067/120] feat(image-loader): use correct netlify cdn domain --- components/Image.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/Image.tsx b/components/Image.tsx index dc40069cd..eca148c11 100644 --- a/components/Image.tsx +++ b/components/Image.tsx @@ -25,8 +25,7 @@ function imageLoader({ quality, src, width }: ImageLoaderProps) { return `${src}${hasQS ? "&" : "?"}w=${width}${quality ? "&q=" + quality : ""}` } - return `https://image-scandic-hotels.netlify.app/.netlify/images?url=${src}&w=${width}${quality ? "&q=" + quality : ""}` - //return `https://img.scandichotels.com/.netlify/images?url=${src}&w=${width}${quality ? "&q=" + quality : ""}` //TODO: use this when Daniel fixed DNS in citrix + return `https://img.scandichotels.com/.netlify/images?url=${src}&w=${width}${quality ? "&q=" + quality : ""}` } // Next/Image adds & instead of ? before the params From cac1a6891e7ef1d47dfb783690f1efab93371e2d Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Mon, 4 Nov 2024 15:49:13 +0100 Subject: [PATCH 068/120] fix(sw-350): Fix issue where value where not updated --- .../BookingWidget/FormContent/Search/index.tsx | 18 ++++++++++++------ components/Forms/BookingWidget/index.tsx | 6 +----- types/components/form/bookingwidget.ts | 1 - 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/components/Forms/BookingWidget/FormContent/Search/index.tsx b/components/Forms/BookingWidget/FormContent/Search/index.tsx index c3de24662..109af79a3 100644 --- a/components/Forms/BookingWidget/FormContent/Search/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Search/index.tsx @@ -48,13 +48,10 @@ export default function Search({ locations }: SearchProps) { dispatch({ type: ActionType.CLEAR_HISTORY_LOCATIONS }) } - function handleOnChange( - evt: FormEvent | ChangeEvent - ) { - const value = evt.currentTarget.value - if (value) { + function dispatchInputValue(inputValue: string) { + if (inputValue) { dispatch({ - payload: { search: value }, + payload: { search: inputValue }, type: ActionType.SEARCH_LOCATIONS, }) } else { @@ -62,6 +59,14 @@ export default function Search({ locations }: SearchProps) { } } + function handleOnChange( + evt: FormEvent | ChangeEvent + ) { + const newValue = evt.currentTarget.value + setValue(name, newValue) + dispatchInputValue(value) + } + function handleOnFocus(evt: FocusEvent) { const searchValue = evt.currentTarget.value if (searchValue) { @@ -114,6 +119,7 @@ export default function Search({ locations }: SearchProps) { inputValue={value} itemToString={(value) => (value ? value.name : "")} onSelect={handleOnSelect} + onInputValueChange={(inputValue) => dispatchInputValue(inputValue)} > {({ closeMenu, diff --git a/components/Forms/BookingWidget/index.tsx b/components/Forms/BookingWidget/index.tsx index 8c9ccb48e..731e17033 100644 --- a/components/Forms/BookingWidget/index.tsx +++ b/components/Forms/BookingWidget/index.tsx @@ -65,11 +65,7 @@ export default function Form({ locations, type }: BookingWidgetFormProps) { id={formId} > - +
    ) diff --git a/types/components/form/bookingwidget.ts b/types/components/form/bookingwidget.ts index e655b8b59..997890ca3 100644 --- a/types/components/form/bookingwidget.ts +++ b/types/components/form/bookingwidget.ts @@ -14,7 +14,6 @@ export interface BookingWidgetFormProps { export interface BookingWidgetFormContentProps { locations: Locations formId: string - formState: FormState } export enum ActionType { From 3e41703df13cc7ba8dd13aea609652f9ada720dc Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Fri, 1 Nov 2024 15:10:42 +0100 Subject: [PATCH 069/120] fix: validate member in enter details store --- .../hotelreservation/(standard)/[step]/layout.tsx | 10 ++++++++-- .../HotelReservation/EnterDetails/Details/index.tsx | 4 ++-- .../HotelReservation/EnterDetails/Details/schema.ts | 8 +++++--- .../HotelReservation/EnterDetails/Provider/index.tsx | 7 ++++--- stores/enter-details.ts | 9 +++++++-- .../hotelReservation/enterDetails/details.ts | 4 ++-- .../components/hotelReservation/enterDetails/store.ts | 3 +++ 7 files changed, 31 insertions(+), 14 deletions(-) create mode 100644 types/components/hotelReservation/enterDetails/store.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx index c38349b26..c33d65975 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx @@ -1,3 +1,5 @@ +import { getProfileSafely } from "@/lib/trpc/memoizedRequests" + import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider" import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom" import { setLang } from "@/i18n/serverContext" @@ -20,11 +22,15 @@ export default async function StepLayout({ hotelHeader: React.ReactNode sidePeek: React.ReactNode summary: React.ReactNode - }>) { + } +>) { setLang(params.lang) preload() + + const user = await getProfileSafely() + return ( - +
    {hotelHeader}
    diff --git a/components/HotelReservation/EnterDetails/Details/index.tsx b/components/HotelReservation/EnterDetails/Details/index.tsx index 6b60fbb07..59085c456 100644 --- a/components/HotelReservation/EnterDetails/Details/index.tsx +++ b/components/HotelReservation/EnterDetails/Details/index.tsx @@ -13,7 +13,7 @@ import Input from "@/components/TempDesignSystem/Form/Input" import Phone from "@/components/TempDesignSystem/Form/Phone" import Body from "@/components/TempDesignSystem/Text/Body" -import { detailsSchema, signedInDetailsSchema } from "./schema" +import { guestDetailsSchema, signedInDetailsSchema } from "./schema" import Signup from "./Signup" import styles from "./details.module.css" @@ -53,7 +53,7 @@ export default function Details({ user }: DetailsProps) { }, criteriaMode: "all", mode: "all", - resolver: zodResolver(user ? signedInDetailsSchema : detailsSchema), + resolver: zodResolver(user ? signedInDetailsSchema : guestDetailsSchema), reValidateMode: "onChange", }) diff --git a/components/HotelReservation/EnterDetails/Details/schema.ts b/components/HotelReservation/EnterDetails/Details/schema.ts index 039b22a61..2e01eefe3 100644 --- a/components/HotelReservation/EnterDetails/Details/schema.ts +++ b/components/HotelReservation/EnterDetails/Details/schema.ts @@ -36,15 +36,17 @@ export const joinDetailsSchema = baseDetailsSchema.merge( }) ) -export const detailsSchema = z.discriminatedUnion("join", [ +export const guestDetailsSchema = z.discriminatedUnion("join", [ notJoinDetailsSchema, joinDetailsSchema, ]) +// For signed in users we accept partial or invalid data. Users cannot +// change their info in this flow, so we don't want to validate it. export const signedInDetailsSchema = z.object({ countryCode: z.string().optional(), - email: z.string().email().optional(), + email: z.string().optional(), firstName: z.string().optional(), lastName: z.string().optional(), - phoneNumber: phoneValidator().optional(), + phoneNumber: z.string().optional(), }) diff --git a/components/HotelReservation/EnterDetails/Provider/index.tsx b/components/HotelReservation/EnterDetails/Provider/index.tsx index d5a6ec677..82bfdbd82 100644 --- a/components/HotelReservation/EnterDetails/Provider/index.tsx +++ b/components/HotelReservation/EnterDetails/Provider/index.tsx @@ -8,16 +8,17 @@ import { initEditDetailsState, } from "@/stores/enter-details" -import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step" +import { EnterDetailsProviderProps } from "@/types/components/hotelReservation/enterDetails/store" export default function EnterDetailsProvider({ step, + isMember, children, -}: PropsWithChildren<{ step: StepEnum }>) { +}: PropsWithChildren) { const searchParams = useSearchParams() const initialStore = useRef() if (!initialStore.current) { - initialStore.current = initEditDetailsState(step, searchParams) + initialStore.current = initEditDetailsState(step, searchParams, isMember) } return ( diff --git a/stores/enter-details.ts b/stores/enter-details.ts index 1c2c6b8d8..8b49603e2 100644 --- a/stores/enter-details.ts +++ b/stores/enter-details.ts @@ -5,7 +5,10 @@ import { create, useStore } from "zustand" import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema" import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" -import { detailsSchema } from "@/components/HotelReservation/EnterDetails/Details/schema" +import { + guestDetailsSchema, + signedInDetailsSchema, +} from "@/components/HotelReservation/EnterDetails/Details/schema" import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import type { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData" @@ -39,7 +42,8 @@ interface EnterDetailsState { export function initEditDetailsState( currentStep: StepEnum, - searchParams: ReadonlyURLSearchParams + searchParams: ReadonlyURLSearchParams, + isMember: boolean ) { const isBrowser = typeof window !== "undefined" const sessionData = isBrowser @@ -93,6 +97,7 @@ export function initEditDetailsState( initialData = { ...initialData, ...validatedBreakfast.data } isValid[StepEnum.breakfast] = true } + const detailsSchema = isMember ? signedInDetailsSchema : guestDetailsSchema const validatedDetails = detailsSchema.safeParse(inputUserData) if (validatedDetails.success) { validPaths.push(StepEnum.payment) diff --git a/types/components/hotelReservation/enterDetails/details.ts b/types/components/hotelReservation/enterDetails/details.ts index 55e6864f0..685fad18a 100644 --- a/types/components/hotelReservation/enterDetails/details.ts +++ b/types/components/hotelReservation/enterDetails/details.ts @@ -1,10 +1,10 @@ import { z } from "zod" -import { detailsSchema } from "@/components/HotelReservation/EnterDetails/Details/schema" +import { guestDetailsSchema } from "@/components/HotelReservation/EnterDetails/Details/schema" import type { SafeUser } from "@/types/user" -export type DetailsSchema = z.output +export type DetailsSchema = z.output export interface DetailsProps { user: SafeUser diff --git a/types/components/hotelReservation/enterDetails/store.ts b/types/components/hotelReservation/enterDetails/store.ts new file mode 100644 index 000000000..45dbd5f75 --- /dev/null +++ b/types/components/hotelReservation/enterDetails/store.ts @@ -0,0 +1,3 @@ +import { StepEnum } from "./step" + +export type EnterDetailsProviderProps = { step: StepEnum; isMember: boolean } From 04df824ea194fbf5555a9bf2959cb429b46618b1 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Mon, 4 Nov 2024 16:09:31 +0100 Subject: [PATCH 070/120] feat(697): Made new fields optional --- .../EnterDetails/Breakfast/index.tsx | 8 ++++---- .../HotelReservation/EnterDetails/Summary/index.tsx | 4 ++-- .../FlexibilityOption/PriceList/index.tsx | 4 ++-- .../SelectRate/RoomSelection/RateSummary/index.tsx | 4 ++-- server/routers/hotels/output.ts | 12 +++++++----- server/routers/hotels/query.ts | 7 +++++-- server/routers/hotels/schemas/packages.ts | 12 +++++++----- 7 files changed, 29 insertions(+), 22 deletions(-) diff --git a/components/HotelReservation/EnterDetails/Breakfast/index.tsx b/components/HotelReservation/EnterDetails/Breakfast/index.tsx index 00d5ab4cc..3881340e0 100644 --- a/components/HotelReservation/EnterDetails/Breakfast/index.tsx +++ b/components/HotelReservation/EnterDetails/Breakfast/index.tsx @@ -78,8 +78,8 @@ export default function Breakfast({ packages }: BreakfastProps) { ? intl.formatMessage( { id: "breakfast.price.free" }, { - amount: pkg.localPrice.price, - currency: pkg.localPrice.currency, + amount: pkg.localPrice?.price, + currency: pkg.localPrice?.currency, free: (str) => {str}, strikethrough: (str) => {str}, } @@ -87,8 +87,8 @@ export default function Breakfast({ packages }: BreakfastProps) { : intl.formatMessage( { id: "breakfast.price" }, { - amount: pkg.localPrice.price, - currency: pkg.localPrice.currency, + amount: pkg.localPrice?.price, + currency: pkg.localPrice?.currency, } ) } diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index ca2891912..ba42f1d96 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -150,8 +150,8 @@ export default function Summary({ {intl.formatMessage( { id: "{amount} {currency}" }, { - amount: chosenBreakfast.localPrice.price, - currency: chosenBreakfast.localPrice.currency, + amount: chosenBreakfast.localPrice?.price, + currency: chosenBreakfast.localPrice?.currency, } )} diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx index 6ec68426f..6cb1c647b 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx @@ -1,4 +1,5 @@ import { differenceInCalendarDays } from "date-fns" +import { useSearchParams } from "next/navigation" import { useIntl } from "react-intl" import Body from "@/components/TempDesignSystem/Text/Body" @@ -27,8 +28,7 @@ export default function PriceList({ const petRoomRequestedPrice = petRoomPackage?.requestedPrice const showRequestedPrice = publicRequestedPrice && memberRequestedPrice - - const searchParams = new URLSearchParams(window.location.search) + const searchParams = useSearchParams() const fromDate = searchParams.get("fromDate") const toDate = searchParams.get("toDate") diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx index 9f2dc4a4f..9db162ec5 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx @@ -36,8 +36,8 @@ export default function RateSummary({ (pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM ) - const petRoomPrice = petRoomPackage?.localPrice.totalPrice ?? null - const petRoomCurrency = petRoomPackage?.localPrice.currency ?? null + const petRoomPrice = petRoomPackage?.localPrice?.totalPrice ?? null + const petRoomCurrency = petRoomPackage?.localPrice?.currency ?? null const checkInDate = new Date(roomsAvailability.checkInDate) const checkOutDate = new Date(roomsAvailability.checkOutDate) diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index dad7abe49..aed677b57 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -774,11 +774,13 @@ export const apiLocationsSchema = z.object({ ), }) -const breakfastPackagePriceSchema = z.object({ - currency: z.nativeEnum(CurrencyEnum), - price: z.string(), - totalPrice: z.string(), -}) +const breakfastPackagePriceSchema = z + .object({ + currency: z.nativeEnum(CurrencyEnum), + price: z.string(), + totalPrice: z.string(), + }) + .optional() // TODO: Remove optional when the API change has been deployed export const breakfastPackageSchema = z.object({ code: z.string(), diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index f282840da..a9b382bd0 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -1095,8 +1095,11 @@ export const hotelQueryRouter = router({ const freeBreakfastPackage = breakfastPackages.data.find( (pkg) => pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST ) - if (freeBreakfastPackage) { - if (originalBreakfastPackage) { + if (freeBreakfastPackage && freeBreakfastPackage.localPrice) { + if ( + originalBreakfastPackage && + originalBreakfastPackage.localPrice + ) { freeBreakfastPackage.localPrice.price = originalBreakfastPackage.localPrice.price } diff --git a/server/routers/hotels/schemas/packages.ts b/server/routers/hotels/schemas/packages.ts index 9de3c0574..b4ff7d57e 100644 --- a/server/routers/hotels/schemas/packages.ts +++ b/server/routers/hotels/schemas/packages.ts @@ -11,11 +11,13 @@ export const getRoomPackagesInputSchema = z.object({ packageCodes: z.array(z.string()).optional().default([]), }) -export const packagePriceSchema = z.object({ - currency: z.string(), - price: z.string(), - totalPrice: z.string(), -}) +export const packagePriceSchema = z + .object({ + currency: z.string(), + price: z.string(), + totalPrice: z.string(), + }) + .optional() // TODO: Remove optional when the API change has been deployed export const packagesSchema = z.object({ code: z.nativeEnum(RoomPackageCodeEnum), From 6b5934e2d047c8243c3e58f8250b5bf0ed5c3fb9 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Mon, 4 Nov 2024 16:29:09 +0100 Subject: [PATCH 071/120] feat(sw-697): changed to dt instead of differenceInCalendarDays --- .../RoomSelection/FlexibilityOption/PriceList/index.tsx | 5 +++-- .../SelectRate/RoomSelection/RateSummary/index.tsx | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx index 6cb1c647b..c3791d57f 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx @@ -1,7 +1,8 @@ -import { differenceInCalendarDays } from "date-fns" import { useSearchParams } from "next/navigation" import { useIntl } from "react-intl" +import { dt } from "@/lib/dt" + import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" @@ -35,7 +36,7 @@ export default function PriceList({ let nights = 1 if (fromDate && toDate) { - nights = differenceInCalendarDays(new Date(fromDate), new Date(toDate)) + nights = dt(toDate).diff(dt(fromDate), "days") } const { diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx index 9db162ec5..745f864cc 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx @@ -1,6 +1,7 @@ -import { differenceInCalendarDays } from "date-fns" import { useIntl } from "react-intl" +import { dt } from "@/lib/dt" + import Button from "@/components/TempDesignSystem/Button" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" @@ -41,7 +42,7 @@ export default function RateSummary({ const checkInDate = new Date(roomsAvailability.checkInDate) const checkOutDate = new Date(roomsAvailability.checkOutDate) - const nights = differenceInCalendarDays(checkOutDate, checkInDate) + const nights = dt(checkOutDate).diff(dt(checkInDate), "days") return (
    From b4a015b47a66f7dc927773a1568cb955c0d6d3b3 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Mon, 4 Nov 2024 16:36:24 +0100 Subject: [PATCH 072/120] feat(sw-697): Added default values --- .../HotelReservation/EnterDetails/Breakfast/index.tsx | 8 ++++---- .../SelectRate/RoomSelection/RateSummary/index.tsx | 4 ++-- server/routers/hotels/output.ts | 6 +++++- server/routers/hotels/schemas/packages.ts | 10 ++++++++-- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/components/HotelReservation/EnterDetails/Breakfast/index.tsx b/components/HotelReservation/EnterDetails/Breakfast/index.tsx index 3881340e0..00d5ab4cc 100644 --- a/components/HotelReservation/EnterDetails/Breakfast/index.tsx +++ b/components/HotelReservation/EnterDetails/Breakfast/index.tsx @@ -78,8 +78,8 @@ export default function Breakfast({ packages }: BreakfastProps) { ? intl.formatMessage( { id: "breakfast.price.free" }, { - amount: pkg.localPrice?.price, - currency: pkg.localPrice?.currency, + amount: pkg.localPrice.price, + currency: pkg.localPrice.currency, free: (str) => {str}, strikethrough: (str) => {str}, } @@ -87,8 +87,8 @@ export default function Breakfast({ packages }: BreakfastProps) { : intl.formatMessage( { id: "breakfast.price" }, { - amount: pkg.localPrice?.price, - currency: pkg.localPrice?.currency, + amount: pkg.localPrice.price, + currency: pkg.localPrice.currency, } ) } diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx index 745f864cc..320fccfc5 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx @@ -37,8 +37,8 @@ export default function RateSummary({ (pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM ) - const petRoomPrice = petRoomPackage?.localPrice?.totalPrice ?? null - const petRoomCurrency = petRoomPackage?.localPrice?.currency ?? null + const petRoomPrice = petRoomPackage?.localPrice.totalPrice ?? null + const petRoomCurrency = petRoomPackage?.localPrice.currency ?? null const checkInDate = new Date(roomsAvailability.checkInDate) const checkOutDate = new Date(roomsAvailability.checkOutDate) diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index aed677b57..957ed20ad 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -780,7 +780,11 @@ const breakfastPackagePriceSchema = z price: z.string(), totalPrice: z.string(), }) - .optional() // TODO: Remove optional when the API change has been deployed + .default({ + currency: CurrencyEnum.SEK, + price: "0", + totalPrice: "0", + }) // TODO: Remove optional and default when the API change has been deployed export const breakfastPackageSchema = z.object({ code: z.string(), diff --git a/server/routers/hotels/schemas/packages.ts b/server/routers/hotels/schemas/packages.ts index b4ff7d57e..738da80ce 100644 --- a/server/routers/hotels/schemas/packages.ts +++ b/server/routers/hotels/schemas/packages.ts @@ -1,6 +1,7 @@ import { z } from "zod" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" +import { CurrencyEnum } from "@/types/enums/currency" export const getRoomPackagesInputSchema = z.object({ hotelId: z.string(), @@ -13,11 +14,16 @@ export const getRoomPackagesInputSchema = z.object({ export const packagePriceSchema = z .object({ - currency: z.string(), + currency: z.nativeEnum(CurrencyEnum), price: z.string(), totalPrice: z.string(), }) - .optional() // TODO: Remove optional when the API change has been deployed + .optional() + .default({ + currency: CurrencyEnum.SEK, + price: "0", + totalPrice: "0", + }) // TODO: Remove optional and default when the API change has been deployed export const packagesSchema = z.object({ code: z.nativeEnum(RoomPackageCodeEnum), From 9ab07897951050f727a3e504ccdc40bb33769890 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Mon, 4 Nov 2024 16:38:22 +0100 Subject: [PATCH 073/120] feat(sw-697): removed ? --- components/HotelReservation/EnterDetails/Summary/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index ba42f1d96..ca2891912 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -150,8 +150,8 @@ export default function Summary({ {intl.formatMessage( { id: "{amount} {currency}" }, { - amount: chosenBreakfast.localPrice?.price, - currency: chosenBreakfast.localPrice?.currency, + amount: chosenBreakfast.localPrice.price, + currency: chosenBreakfast.localPrice.currency, } )} From e2e2d786bdeceffceab97f8b19cd86e6b6ec0c91 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Fri, 1 Nov 2024 13:08:00 +0100 Subject: [PATCH 074/120] fix: add correct login button instead of sending everyone to my pages --- .../SignUpVerification/index.tsx | 2 +- components/Current/Header/MainMenu/index.tsx | 2 +- components/Current/Header/TopMenu/index.tsx | 5 ++--- .../MainMenu/MyPagesMenuWrapper/index.tsx | 10 ++++----- .../LoginButton.tsx => LoginButton/index.tsx} | 2 +- components/Sidebar/JoinLoyalty/index.tsx | 2 +- hooks/useLazyPathname.ts | 21 ++++++++++++++----- 7 files changed, 27 insertions(+), 17 deletions(-) rename components/{Current/Header/LoginButton.tsx => LoginButton/index.tsx} (95%) diff --git a/components/Blocks/DynamicContent/SignUpVerification/index.tsx b/components/Blocks/DynamicContent/SignUpVerification/index.tsx index 3755b59b4..2b182d4bf 100644 --- a/components/Blocks/DynamicContent/SignUpVerification/index.tsx +++ b/components/Blocks/DynamicContent/SignUpVerification/index.tsx @@ -3,7 +3,7 @@ import { redirect } from "next/navigation" import { overview } from "@/constants/routes/myPages" import { auth } from "@/auth" -import LoginButton from "@/components/Current/Header/LoginButton" +import LoginButton from "@/components/LoginButton" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" diff --git a/components/Current/Header/MainMenu/index.tsx b/components/Current/Header/MainMenu/index.tsx index e8f41a12e..0303a9448 100644 --- a/components/Current/Header/MainMenu/index.tsx +++ b/components/Current/Header/MainMenu/index.tsx @@ -7,13 +7,13 @@ import { myPages } from "@/constants/routes/myPages" import useDropdownStore from "@/stores/main-menu" import Image from "@/components/Image" +import LoginButton from "@/components/LoginButton" import Avatar from "@/components/MyPages/Avatar" import Link from "@/components/TempDesignSystem/Link" import useLang from "@/hooks/useLang" import { trackClick } from "@/utils/tracking" import BookingButton from "../BookingButton" -import LoginButton from "../LoginButton" import styles from "./mainMenu.module.css" diff --git a/components/Current/Header/TopMenu/index.tsx b/components/Current/Header/TopMenu/index.tsx index 1472461d8..cb7c103f7 100644 --- a/components/Current/Header/TopMenu/index.tsx +++ b/components/Current/Header/TopMenu/index.tsx @@ -2,12 +2,11 @@ import { logout } from "@/constants/routes/handleAuth" import { overview } from "@/constants/routes/myPages" import { getName } from "@/lib/trpc/memoizedRequests" +import LoginButton from "@/components/LoginButton" import Link from "@/components/TempDesignSystem/Link" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" -import LoginButton from "../LoginButton" - import styles from "./topMenu.module.css" import type { TopMenuProps } from "@/types/components/current/header/topMenu" @@ -67,7 +66,7 @@ export default async function TopMenu({ ) : ( {formatMessage({ id: "Log in" })} diff --git a/components/Header/MainMenu/MyPagesMenuWrapper/index.tsx b/components/Header/MainMenu/MyPagesMenuWrapper/index.tsx index 3c2afb9fc..14044b248 100644 --- a/components/Header/MainMenu/MyPagesMenuWrapper/index.tsx +++ b/components/Header/MainMenu/MyPagesMenuWrapper/index.tsx @@ -1,5 +1,4 @@ import { MembershipLevelEnum } from "@/constants/membershipLevels" -import { myPages } from "@/constants/routes/myPages" import { getMembershipLevelSafely, getMyPagesNavigation, @@ -7,7 +6,7 @@ import { } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" -import Link from "@/components/TempDesignSystem/Link" +import LoginButton from "@/components/LoginButton" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" @@ -50,16 +49,17 @@ export default async function MyPagesMenuWrapper() { /> ) : ( - {intl.formatMessage({ id: "Log in/Join" })} - + )} ) diff --git a/components/Current/Header/LoginButton.tsx b/components/LoginButton/index.tsx similarity index 95% rename from components/Current/Header/LoginButton.tsx rename to components/LoginButton/index.tsx index 475640879..54ba17b34 100644 --- a/components/Current/Header/LoginButton.tsx +++ b/components/LoginButton/index.tsx @@ -27,7 +27,7 @@ export default function LoginButton({ variant?: "default" | "signupVerification" }>) { const lang = useLang() - const pathName = useLazyPathname() + const pathName = useLazyPathname({ includeSearchParams: true }) const href = pathName ? `${login[lang]}?redirectTo=${encodeURIComponent(pathName)}` diff --git a/components/Sidebar/JoinLoyalty/index.tsx b/components/Sidebar/JoinLoyalty/index.tsx index 3e353551f..8ee6bbb9d 100644 --- a/components/Sidebar/JoinLoyalty/index.tsx +++ b/components/Sidebar/JoinLoyalty/index.tsx @@ -1,8 +1,8 @@ import { getName } from "@/lib/trpc/memoizedRequests" -import LoginButton from "@/components/Current/Header/LoginButton" import ArrowRight from "@/components/Icons/ArrowRight" import { ScandicFriends } from "@/components/Levels" +import LoginButton from "@/components/LoginButton" import Button from "@/components/TempDesignSystem/Button" import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" diff --git a/hooks/useLazyPathname.ts b/hooks/useLazyPathname.ts index 5f3a53c18..3d387da9e 100644 --- a/hooks/useLazyPathname.ts +++ b/hooks/useLazyPathname.ts @@ -1,16 +1,27 @@ "use client" -import { usePathname } from "next/navigation" +import { usePathname, useSearchParams } from "next/navigation" import { useEffect, useState } from "react" /*** This hook is used to get the current pathname (as reflected in window.location.href) of the page. During ssr, the value from usePathname() * is the value return from NextResponse.rewrite() (e.g. the path from the app directory) instead of the actual pathname from the URL. */ -export function useLazyPathname() { +export function useLazyPathname({ includeSearchParams = false } = {}) { const pathName = usePathname() + const searchParams = useSearchParams() + const [updatedPathName, setUpdatedPathName] = useState(null) useEffect(() => { - setUpdatedPathName(pathName) - }, [pathName]) - return updatedPathName ? updatedPathName : null + if (!includeSearchParams) { + setUpdatedPathName(pathName) + } else { + const updatedPathname = searchParams.size + ? `${pathName}?${searchParams.toString()}` + : pathName + + setUpdatedPathName(updatedPathname) + } + }, [pathName, searchParams, includeSearchParams]) + + return updatedPathName } From a9663c856f1405298e0fa745a77f7472526f159f Mon Sep 17 00:00:00 2001 From: Niclas Edenvin Date: Tue, 5 Nov 2024 09:36:10 +0100 Subject: [PATCH 075/120] Make parking data optional --- server/routers/hotels/output.ts | 92 ++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 41 deletions(-) diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 957ed20ad..a22e2a033 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -225,64 +225,74 @@ export const pointOfInterestSchema = z const parkingPricingSchema = z.object({ freeParking: z.boolean(), - paymentType: z.string(), + paymentType: z.string().optional(), localCurrency: z.object({ - currency: z.string(), + currency: z.string().optional(), range: z.object({ min: z.number().optional(), max: z.number().optional(), }), - ordinary: z.array( - z.object({ - period: z.string(), - amount: z.number().optional(), - startTime: z.string(), - endTime: z.string(), - }) - ), - weekend: z.array( - z.object({ - period: z.string(), - amount: z.number().optional(), - startTime: z.string(), - endTime: z.string(), - }) - ), + ordinary: z + .array( + z.object({ + period: z.string().optional(), + amount: z.number().optional(), + startTime: z.string().optional(), + endTime: z.string().optional(), + }) + ) + .optional(), + weekend: z + .array( + z.object({ + period: z.string().optional(), + amount: z.number().optional(), + startTime: z.string().optional(), + endTime: z.string().optional(), + }) + ) + .optional(), }), requestedCurrency: z .object({ - currency: z.string(), - range: z.object({ - min: z.number(), - max: z.number(), - }), - ordinary: z.array( - z.object({ - period: z.string(), - amount: z.number(), - startTime: z.string(), - endTime: z.string(), + currency: z.string().optional(), + range: z + .object({ + min: z.number().optional(), + max: z.number().optional(), }) - ), - weekend: z.array( - z.object({ - period: z.string(), - amount: z.number(), - startTime: z.string(), - endTime: z.string(), - }) - ), + .optional(), + ordinary: z + .array( + z.object({ + period: z.string().optional(), + amount: z.number().optional(), + startTime: z.string().optional(), + endTime: z.string().optional(), + }) + ) + .optional(), + weekend: z + .array( + z.object({ + period: z.string().optional(), + amount: z.number().optional(), + startTime: z.string().optional(), + endTime: z.string().optional(), + }) + ) + .optional(), }) .optional(), }) export const parkingSchema = z.object({ - type: z.string(), - name: z.string(), + type: z.string().optional(), + name: z.string().optional(), address: z.string().optional(), numberOfParkingSpots: z.number().optional(), numberOfChargingSpaces: z.number().optional(), - distanceToHotel: z.number(), + distanceToHotel: z.number().optional(), canMakeReservation: z.boolean(), pricing: parkingPricingSchema, }) From d8d77479b1960bce6c76a8a21ec1bc7a5066f86c Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Fri, 1 Nov 2024 14:41:56 +0100 Subject: [PATCH 076/120] fix: filter out available bedtypes --- .../(standard)/[step]/@summary/page.tsx | 38 +++++++++---------- .../(standard)/[step]/page.tsx | 18 ++------- server/routers/hotels/input.ts | 2 +- server/routers/hotels/query.ts | 38 +++++++++++++++++++ .../hotelReservation/enterDetails/bedType.ts | 2 +- 5 files changed, 63 insertions(+), 35 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx index ed36a8476..b28dca1a8 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx @@ -17,7 +17,7 @@ export default async function SummaryPage({ getQueryParamsForEnterDetails(selectRoomParams) const availability = await getSelectedRoomAvailability({ - hotelId: parseInt(hotel), + hotelId: hotel, adults, children, roomStayStartDate: fromDate, @@ -35,25 +35,25 @@ export default async function SummaryPage({ const prices = user ? { - local: { - price: availability.memberRate?.localPrice.pricePerStay, - currency: availability.memberRate?.localPrice.currency, - }, - euro: { - price: availability.memberRate?.requestedPrice?.pricePerStay, - currency: availability.memberRate?.requestedPrice?.currency, - }, - } + local: { + price: availability.memberRate?.localPrice.pricePerStay, + currency: availability.memberRate?.localPrice.currency, + }, + euro: { + price: availability.memberRate?.requestedPrice?.pricePerStay, + currency: availability.memberRate?.requestedPrice?.currency, + }, + } : { - local: { - price: availability.publicRate?.localPrice.pricePerStay, - currency: availability.publicRate?.localPrice.currency, - }, - euro: { - price: availability.publicRate?.requestedPrice?.pricePerStay, - currency: availability.publicRate?.requestedPrice?.currency, - }, - } + local: { + price: availability.publicRate?.localPrice.pricePerStay, + currency: availability.publicRate?.localPrice.currency, + }, + euro: { + price: availability.publicRate?.requestedPrice?.pricePerStay, + currency: availability.publicRate?.requestedPrice?.currency, + }, + } return ( room.name === availableRoom) - ?.roomTypes.map((room) => ({ - description: room.mainBed.description, - size: room.mainBed.widthRange, - value: room.code, - })) - return (
    - {/* TODO: How to handle no beds found? */} - {bedTypes ? ( + {roomAvailability.bedTypes ? ( - + ) : null} diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index ecf4d5cad..16518bc9f 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -30,7 +30,7 @@ export const getRoomsAvailabilityInputSchema = z.object({ }) export const getSelectedRoomAvailabilityInputSchema = z.object({ - hotelId: z.number(), + hotelId: z.string(), roomStayStartDate: z.string(), roomStayEndDate: z.string(), adults: z.number(), diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index a9b382bd0..ae27a8498 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -40,6 +40,7 @@ import { getRoomsAvailabilityInputSchema, getSelectedRoomAvailabilityInputSchema, type HotelDataInput, + HotelIncludeEnum, } from "./input" import { breakfastPackagesSchema, @@ -57,6 +58,7 @@ import { } from "./utils" import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities" +import type { BedType } from "@/types/components/hotelReservation/enterDetails/bedType" import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" import { BreakfastPackageEnum } from "@/types/enums/breakfast" import type { RequestOptionsWithOutBody } from "@/types/fetch" @@ -686,6 +688,7 @@ export const hotelQueryRouter = router({ promotionCode, reservationProfileType, attachedProfileId, + language: toApiLang(ctx.lang), } selectedRoomAvailabilityCounter.add(1, { @@ -766,10 +769,26 @@ export const hotelQueryRouter = router({ throw badRequestError() } + const hotelData = await getHotelData( + { + hotelId, + language: ctx.lang, + include: [HotelIncludeEnum.RoomCategories], + }, + ctx.serviceToken + ) + const selectedRoom = validateAvailabilityData.data.roomConfigurations .filter((room) => room.status === "Available") .find((room) => room.roomTypeCode === roomTypeCode) + const availableRoomsInCategory = + validateAvailabilityData.data.roomConfigurations.filter( + (room) => + room.status === "Available" && + room.roomType === selectedRoom?.roomType + ) + if (!selectedRoom) { console.error("No matching room found") return null @@ -793,6 +812,24 @@ export const hotelQueryRouter = router({ (rate) => rate.rateCode === rateCode )?.cancellationText ?? "" + const bedTypes = availableRoomsInCategory + .map((availRoom) => { + const matchingRoom = hotelData?.included + ?.find((room) => room.name === availRoom.roomType) + ?.roomTypes.find( + (roomType) => roomType.code === availRoom.roomTypeCode + ) + + if (matchingRoom) { + return { + description: matchingRoom.mainBed.description, + size: matchingRoom.mainBed.widthRange, + value: matchingRoom.code, + } + } + }) + .filter((bed): bed is BedType => Boolean(bed)) + selectedRoomAvailabilitySuccessCounter.add(1, { hotelId, roomStayStartDate, @@ -815,6 +852,7 @@ export const hotelQueryRouter = router({ cancellationText, memberRate, publicRate, + bedTypes, } }), }), diff --git a/types/components/hotelReservation/enterDetails/bedType.ts b/types/components/hotelReservation/enterDetails/bedType.ts index 35f41ee27..3948fcae3 100644 --- a/types/components/hotelReservation/enterDetails/bedType.ts +++ b/types/components/hotelReservation/enterDetails/bedType.ts @@ -2,7 +2,7 @@ import { z } from "zod" import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema" -type BedType = { +export type BedType = { description: string size: { min: number From c7b61196cbb9d8a0c9d42b202f723ee246094b3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matilda=20Landstr=C3=B6m?= Date: Tue, 5 Nov 2024 09:52:39 +0000 Subject: [PATCH 077/120] Merged in feat/SW-337-hotel-page-ui (pull request #828) Feat(SW-337): HotelPage UI fixes Approved-by: Christian Andolf Approved-by: Erik Tiekstra Approved-by: Fredrik Thorsson --- components/Blocks/CardsGrid.tsx | 2 +- .../AmenitiesList/amenitiesList.module.css | 1 + .../ContentType/HotelPage/AmenitiesList/index.tsx | 11 ++++++++--- .../ContentType/HotelPage/IntroSection/index.tsx | 7 +++---- .../ContentType/HotelPage/Rooms/RoomCard/index.tsx | 4 ++-- .../HotelPage/Rooms/RoomCard/roomCard.module.css | 5 +++-- .../AccordionItem/accordionItem.module.css | 13 +++++-------- .../TempDesignSystem/Accordion/accordion.module.css | 7 +++++++ 8 files changed, 30 insertions(+), 20 deletions(-) diff --git a/components/Blocks/CardsGrid.tsx b/components/Blocks/CardsGrid.tsx index 92d98a61b..81e0b0eff 100644 --- a/components/Blocks/CardsGrid.tsx +++ b/components/Blocks/CardsGrid.tsx @@ -43,7 +43,7 @@ export default function CardsGrid({ return ( {IconComponent && ( - + )} {facility.name}
    @@ -44,7 +49,7 @@ export default async function AmenitiesList({ className={styles.showAllAmenities} > {intl.formatMessage({ id: "Show all amenities" })} - +
    ) diff --git a/components/ContentType/HotelPage/IntroSection/index.tsx b/components/ContentType/HotelPage/IntroSection/index.tsx index 360d0f8df..010530f74 100644 --- a/components/ContentType/HotelPage/IntroSection/index.tsx +++ b/components/ContentType/HotelPage/IntroSection/index.tsx @@ -1,7 +1,6 @@ import { about } from "@/constants/routes/hotelPageParams" -import { ChevronRightIcon } from "@/components/Icons" -import ArrowRight from "@/components/Icons/ArrowRight" +import { ChevronRightSmallIcon } from "@/components/Icons" import TripAdvisorIcon from "@/components/Icons/TripAdvisor" import Link from "@/components/TempDesignSystem/Link" import BiroScript from "@/components/TempDesignSystem/Text/BiroScript" @@ -48,7 +47,7 @@ export default async function IntroSection({
    - {intl.formatMessage({ id: "Welcome to" })}: + {intl.formatMessage({ id: "Welcome to" })} {hotelName}
    @@ -77,7 +76,7 @@ export default async function IntroSection({ scroll={false} > {intl.formatMessage({ id: "Read more about the hotel" })} - +
    diff --git a/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx b/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx index 2f7d33c25..81ab9b720 100644 --- a/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx +++ b/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx @@ -2,7 +2,7 @@ import { useIntl } from "react-intl" -import { ImageIcon } from "@/components/Icons" +import { GalleryIcon } from "@/components/Icons" import Image from "@/components/Image" import RoomSidePeek from "@/components/SidePeeks/RoomSidePeek" import Body from "@/components/TempDesignSystem/Text/Body" @@ -44,7 +44,7 @@ export function RoomCard({ room }: RoomCardProps) { {/* */} {/* )} */} - + {images.length} {/*NOTE: images from the test API are hosted on test3.scandichotels.com, diff --git a/components/ContentType/HotelPage/Rooms/RoomCard/roomCard.module.css b/components/ContentType/HotelPage/Rooms/RoomCard/roomCard.module.css index fcec738e4..5270e9882 100644 --- a/components/ContentType/HotelPage/Rooms/RoomCard/roomCard.module.css +++ b/components/ContentType/HotelPage/Rooms/RoomCard/roomCard.module.css @@ -26,10 +26,11 @@ display: flex; gap: var(--Spacing-x-half); align-items: center; - background-color: var(--Main-Grey-90); + background-color: var(--UI-Grey-90); + opacity: 90%; color: var(--UI-Input-Controls-Fill-Normal); padding: var(--Spacing-x-half) var(--Spacing-x1); - border-radius: var(--Corner-radius-Medium); + border-radius: var(--Corner-radius-Small); } .content { diff --git a/components/TempDesignSystem/Accordion/AccordionItem/accordionItem.module.css b/components/TempDesignSystem/Accordion/AccordionItem/accordionItem.module.css index 7940ee17f..c2dacab26 100644 --- a/components/TempDesignSystem/Accordion/AccordionItem/accordionItem.module.css +++ b/components/TempDesignSystem/Accordion/AccordionItem/accordionItem.module.css @@ -18,16 +18,13 @@ font-weight: var(--typography-Body-Bold-fontWeight); transition: background-color 0.3s; } -.summary:hover, -.accordionItem details[open] .summary { - background-color: var(--Base-Surface-Primary-light-Hover-alt, #f2ece6); +.summary:hover { + background-color: var(--Base-Surface-Primary-light-Hover-alt); } -.accordionItem.light .summary:hover, -.accordionItem.light details[open] .summary { - background-color: var(--Base-Surface-Primary-light-Hover, #f9f6f4); +.accordionItem.light .summary:hover { + background-color: var(--Base-Surface-Primary-light-Hover); } -.accordionItem.subtle .summary:hover, -.accordionItem.subtle details[open] .summary { +.accordionItem.subtle .summary:hover { background-color: var(--Base-Surface-Primary-light-Normal); } diff --git a/components/TempDesignSystem/Accordion/accordion.module.css b/components/TempDesignSystem/Accordion/accordion.module.css index 64f18730c..3c6e0d689 100644 --- a/components/TempDesignSystem/Accordion/accordion.module.css +++ b/components/TempDesignSystem/Accordion/accordion.module.css @@ -16,3 +16,10 @@ .accordion li:last-child { border: none; } + +.accordion details > summary { + list-style: none; +} +.accordion details > summary::-webkit-details-marker { + display: none; +} From 43b1c309cee3f071dfd5aaf8738832f5917c6119 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Tue, 5 Nov 2024 09:07:34 +0100 Subject: [PATCH 078/120] fix(SW-695): apply breadcrumb variant & smaller HomeIcon size --- components/Breadcrumbs/index.tsx | 2 +- components/TempDesignSystem/Link/link.module.css | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/components/Breadcrumbs/index.tsx b/components/Breadcrumbs/index.tsx index 1939e47fa..f962feff1 100644 --- a/components/Breadcrumbs/index.tsx +++ b/components/Breadcrumbs/index.tsx @@ -24,7 +24,7 @@ export default async function Breadcrumbs() { href={homeBreadcrumb.href!} variant="breadcrumb" > - +