From e78847d743298480d42289313c37ead3559c9153 Mon Sep 17 00:00:00 2001 From: Michael Zetterberg Date: Wed, 16 Oct 2024 11:20:09 +0200 Subject: [PATCH 01/73] chore: update-browserslist-db to latest --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index db6a1ca42..237a64c1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7801,9 +7801,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001608", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001608.tgz", - "integrity": "sha512-cjUJTQkk9fQlJR2s4HMuPMvTiRggl0rAVMtthQuyOlDWuqHXqN8azLq+pi8B2TjwKJ32diHjUqRIKeFX4z1FoA==", + "version": "1.0.30001669", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz", + "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==", "funding": [ { "type": "opencollective", From 05d353e22422819fb3df1e7d3c5eee048fd05c80 Mon Sep 17 00:00:00 2001 From: Hrishikesh Vaipurkar Date: Fri, 25 Oct 2024 08:50:23 +0200 Subject: [PATCH 02/73] sub-task/ SW-695 Prefill Guests data in booking widget --- app/[lang]/(live)/@bookingwidget/page.tsx | 6 +- components/BookingWidget/Client.tsx | 22 +- .../MobileToggleButton/index.tsx | 8 +- .../Forms/BookingWidget/FormContent/index.tsx | 7 +- components/Forms/BookingWidget/index.tsx | 2 +- components/Forms/BookingWidget/schema.ts | 2 +- .../GuestsRoomsPicker/AdultSelector/index.tsx | 6 +- .../ChildSelector/ChildInfoSelector.tsx | 14 +- .../GuestsRoomsPicker/ChildSelector/index.tsx | 22 +- .../Provider/GuestsRoomsProvider.tsx | 26 ++ components/GuestsRoomsPicker/index.tsx | 11 +- .../SelectRate/RoomSelection/utils.ts | 28 +- components/TempDesignSystem/Select/index.tsx | 2 + components/TempDesignSystem/Select/select.ts | 1 + stores/guests-rooms.ts | 319 +++++++++++------- .../bookingWidget/guestsRoomsPicker.ts | 2 +- types/components/bookingWidget/index.ts | 13 +- utils/url.ts | 33 ++ 18 files changed, 342 insertions(+), 182 deletions(-) create mode 100644 components/GuestsRoomsPicker/Provider/GuestsRoomsProvider.tsx diff --git a/app/[lang]/(live)/@bookingwidget/page.tsx b/app/[lang]/(live)/@bookingwidget/page.tsx index 16bc85731..2d43c7773 100644 --- a/app/[lang]/(live)/@bookingwidget/page.tsx +++ b/app/[lang]/(live)/@bookingwidget/page.tsx @@ -3,13 +3,11 @@ import { serverClient } from "@/lib/trpc/server" import BookingWidget, { preload } from "@/components/BookingWidget" -import { BookingWidgetSearchParams } from "@/types/components/bookingWidget" -import { LangParams, PageArgs } from "@/types/params" +import { BookingWidgetPageProps } from "@/types/components/bookingWidget" export default async function BookingWidgetPage({ - params, searchParams, -}: PageArgs) { +}: BookingWidgetPageProps) { if (env.HIDE_FOR_NEXT_RELEASE) { return null } diff --git a/components/BookingWidget/Client.tsx b/components/BookingWidget/Client.tsx index fda8683c9..6a594199e 100644 --- a/components/BookingWidget/Client.tsx +++ b/components/BookingWidget/Client.tsx @@ -9,6 +9,7 @@ import Form from "@/components/Forms/BookingWidget" import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema" 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" @@ -18,6 +19,7 @@ import styles from "./bookingWidget.module.css" import type { BookingWidgetClientProps, BookingWidgetSchema, + BookingWidgetSearchParams, } from "@/types/components/bookingWidget" import type { Location } from "@/types/trpc/routers/hotel/locations" @@ -36,12 +38,14 @@ export default function BookingWidgetClient({ ? JSON.parse(sessionStorageSearchData) : undefined - const bookingWidgetSearchParams = searchParams - ? new URLSearchParams(searchParams) - : undefined - const bookingWidgetSearchData = bookingWidgetSearchParams - ? getHotelReservationQueryParams(bookingWidgetSearchParams) - : undefined + const bookingWidgetSearchData: BookingWidgetSearchParams | undefined = + searchParams + ? (getFormattedUrlQueryParams(new URLSearchParams(searchParams), { + adults: "number", + age: "number", + bed: "number", + }) as BookingWidgetSearchParams) + : undefined const getLocationObj = (destination: string): Location | undefined => { if (destination) { @@ -83,7 +87,7 @@ export default function BookingWidgetClient({ // UTC is required to handle requests from far away timezones https://scandichotels.atlassian.net/browse/SWAP-6375 & PET-507 // This is specifically to handle timezones falling in different dates. fromDate: isDateParamValid - ? bookingWidgetSearchData?.fromDate.toString() + ? bookingWidgetSearchData?.fromDate?.toString() : dt().utc().format("YYYY-MM-DD"), toDate: isDateParamValid ? bookingWidgetSearchData?.toDate?.toString() @@ -92,10 +96,10 @@ export default function BookingWidgetClient({ bookingCode: "", redemption: false, voucher: false, - rooms: [ + rooms: bookingWidgetSearchData?.room ?? [ { adults: 1, - children: [], + child: [], }, ], }, diff --git a/components/BookingWidget/MobileToggleButton/index.tsx b/components/BookingWidget/MobileToggleButton/index.tsx index df84cd65d..3bc438c41 100644 --- a/components/BookingWidget/MobileToggleButton/index.tsx +++ b/components/BookingWidget/MobileToggleButton/index.tsx @@ -54,6 +54,12 @@ export default function MobileToggleButton({ } return acc }, 0) + const totalChildren = rooms.reduce((acc, room) => { + if (room.child) { + acc = acc + room.child.length + } + return acc + }, 0) return (
@@ -62,7 +68,7 @@ export default function MobileToggleButton({ {`${selectedFromDate} - ${selectedToDate} (${intl.formatMessage( { id: "booking.nights" }, { totalNights: nights } - )}) ${intl.formatMessage({ id: "booking.adults" }, { totalAdults })}, ${intl.formatMessage({ id: "booking.rooms" }, { totalRooms })}`} + )}) ${intl.formatMessage({ id: "booking.adults" }, { totalAdults })}, ${intl.formatMessage({ id: "booking.children" }, { totalChildren })}, ${intl.formatMessage({ id: "booking.rooms" }, { totalRooms })}`}
diff --git a/components/Forms/BookingWidget/FormContent/index.tsx b/components/Forms/BookingWidget/FormContent/index.tsx index eca1634d8..002edb44a 100644 --- a/components/Forms/BookingWidget/FormContent/index.tsx +++ b/components/Forms/BookingWidget/FormContent/index.tsx @@ -7,6 +7,7 @@ import { dt } from "@/lib/dt" import DatePicker from "@/components/DatePicker" import GuestsRoomsPickerForm from "@/components/GuestsRoomsPicker" +import GuestsRoomsProvider from "@/components/GuestsRoomsPicker/Provider/GuestsRoomsProvider" import { SearchIcon } from "@/components/Icons" import Button from "@/components/TempDesignSystem/Button" import Caption from "@/components/TempDesignSystem/Text/Caption" @@ -29,6 +30,8 @@ export default function FormContent({ const nights = dt(selectedDate.toDate).diff(dt(selectedDate.fromDate), "days") + const selectedGuests = useWatch({ name: "rooms" }) + return ( <>
@@ -51,7 +54,9 @@ export default function FormContent({ {rooms} - + + +
diff --git a/components/Forms/BookingWidget/index.tsx b/components/Forms/BookingWidget/index.tsx index 270a38b04..8c9ccb48e 100644 --- a/components/Forms/BookingWidget/index.tsx +++ b/components/Forms/BookingWidget/index.tsx @@ -42,7 +42,7 @@ export default function Form({ locations, type }: BookingWidgetFormProps) { data.rooms.forEach((room, index) => { bookingWidgetParams.set(`room[${index}].adults`, room.adults.toString()) - room.children.forEach((child, childIndex) => { + room.child.forEach((child, childIndex) => { bookingWidgetParams.set( `room[${index}].child[${childIndex}].age`, child.age.toString() diff --git a/components/Forms/BookingWidget/schema.ts b/components/Forms/BookingWidget/schema.ts index aa42b542d..973ab6ad6 100644 --- a/components/Forms/BookingWidget/schema.ts +++ b/components/Forms/BookingWidget/schema.ts @@ -4,7 +4,7 @@ import type { Location } from "@/types/trpc/routers/hotel/locations" export const guestRoomSchema = z.object({ adults: z.number().default(1), - children: z.array( + child: z.array( z.object({ age: z.number().nonnegative(), bed: z.number(), diff --git a/components/GuestsRoomsPicker/AdultSelector/index.tsx b/components/GuestsRoomsPicker/AdultSelector/index.tsx index 72d60ebaf..06dbd56c3 100644 --- a/components/GuestsRoomsPicker/AdultSelector/index.tsx +++ b/components/GuestsRoomsPicker/AdultSelector/index.tsx @@ -21,7 +21,7 @@ export default function AdultSelector({ roomIndex = 0 }: AdultSelectorProps) { const intl = useIntl() const adultsLabel = intl.formatMessage({ id: "Adults" }) const { setValue } = useFormContext() - const { adults, children, childrenInAdultsBed } = useGuestsRoomsStore( + const { adults, child, childrenInAdultsBed } = useGuestsRoomsStore( (state) => state.rooms[roomIndex] ) const increaseAdults = useGuestsRoomsStore((state) => state.increaseAdults) @@ -39,13 +39,13 @@ export default function AdultSelector({ roomIndex = 0 }: AdultSelectorProps) { decreaseAdults(roomIndex) setValue(`rooms.${roomIndex}.adults`, adults - 1) if (childrenInAdultsBed > adults) { - const toUpdateIndex = children.findIndex( + const toUpdateIndex = child.findIndex( (child: Child) => child.bed == BedTypeEnum.IN_ADULTS_BED ) if (toUpdateIndex != -1) { setValue( `rooms.${roomIndex}.children.${toUpdateIndex}.bed`, - children[toUpdateIndex].age < 3 + child[toUpdateIndex].age < 3 ? BedTypeEnum.IN_CRIB : BedTypeEnum.IN_EXTRA_BED ) diff --git a/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx b/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx index 107bfd8b6..f219293ab 100644 --- a/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx +++ b/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx @@ -26,7 +26,7 @@ export default function ChildInfoSelector({ const ageLabel = intl.formatMessage({ id: "Age" }) const ageReqdErrMsg = intl.formatMessage({ id: "Child age is required" }) const bedLabel = intl.formatMessage({ id: "Bed" }) - const { setValue, trigger } = useFormContext() + const { setValue } = useFormContext() const { adults, childrenInAdultsBed } = useGuestsRoomsStore( (state) => state.rooms[roomIndex] ) @@ -51,10 +51,11 @@ export default function ChildInfoSelector({ function updateSelectedAge(age: number) { updateChildAge(age, roomIndex, index) - setValue(`rooms.${roomIndex}.children.${index}.age`, age) + setValue(`rooms.${roomIndex}.child.${index}.age`, age, { + shouldValidate: true, + }) const availableBedTypes = getAvailableBeds(age) updateSelectedBed(availableBedTypes[0].value) - trigger("rooms") } function updateSelectedBed(bed: number) { @@ -64,7 +65,7 @@ export default function ChildInfoSelector({ decreaseChildInAdultsBed(roomIndex) } updateChildBed(bed, roomIndex, index) - setValue(`rooms.${roomIndex}.children.${index}.bed`, bed) + setValue(`rooms.${roomIndex}.child.${index}.bed`, bed) } const allBedTypes: ChildBed[] = [ @@ -109,8 +110,9 @@ export default function ChildInfoSelector({ onSelect={(key) => { updateSelectedAge(key as number) }} - name={`rooms.${roomIndex}.children.${index}.age`} + name={`rooms.${roomIndex}.child.${index}.age`} placeholder={ageLabel} + maxHeight={150} />
@@ -123,7 +125,7 @@ export default function ChildInfoSelector({ onSelect={(key) => { updateSelectedBed(key as number) }} - name={`rooms.${roomIndex}.children.${index}.age`} + name={`rooms.${roomIndex}.child.${index}.age`} placeholder={bedLabel} /> ) : null} diff --git a/components/GuestsRoomsPicker/ChildSelector/index.tsx b/components/GuestsRoomsPicker/ChildSelector/index.tsx index 827bcd2e6..22d594397 100644 --- a/components/GuestsRoomsPicker/ChildSelector/index.tsx +++ b/components/GuestsRoomsPicker/ChildSelector/index.tsx @@ -19,9 +19,7 @@ export default function ChildSelector({ roomIndex = 0 }: ChildSelectorProps) { const intl = useIntl() const childrenLabel = intl.formatMessage({ id: "Children" }) const { setValue, trigger } = useFormContext() - const children = useGuestsRoomsStore( - (state) => state.rooms[roomIndex].children - ) + const children = useGuestsRoomsStore((state) => state.rooms[roomIndex].child) const increaseChildren = useGuestsRoomsStore( (state) => state.increaseChildren ) @@ -32,18 +30,22 @@ export default function ChildSelector({ roomIndex = 0 }: ChildSelectorProps) { function increaseChildrenCount(roomIndex: number) { if (children.length < 5) { increaseChildren(roomIndex) - setValue(`rooms.${roomIndex}.children.${children.length}`, { - age: -1, - bed: -1, - }) - trigger("rooms") + setValue( + `rooms.${roomIndex}.child.${children.length}`, + { + age: -1, + bed: -1, + }, + { shouldValidate: true } + ) } } function decreaseChildrenCount(roomIndex: number) { if (children.length > 0) { const newChildrenList = decreaseChildren(roomIndex) - setValue(`rooms.${roomIndex}.children`, newChildrenList) - trigger("rooms") + setValue(`rooms.${roomIndex}.child`, newChildrenList, { + shouldValidate: true, + }) } } diff --git a/components/GuestsRoomsPicker/Provider/GuestsRoomsProvider.tsx b/components/GuestsRoomsPicker/Provider/GuestsRoomsProvider.tsx new file mode 100644 index 000000000..5a85102e7 --- /dev/null +++ b/components/GuestsRoomsPicker/Provider/GuestsRoomsProvider.tsx @@ -0,0 +1,26 @@ +"use client" +import { PropsWithChildren, useRef } from "react" + +import { + GuestsRoomsContext, + type GuestsRoomsStore, + initGuestsRoomsState, +} from "@/stores/guests-rooms" + +import { GuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker" + +export default function GuestsRoomsProvider({ + selectedGuests, + children, +}: PropsWithChildren<{ selectedGuests?: GuestsRoom[] }>) { + const initialStore = useRef() + if (!initialStore.current) { + initialStore.current = initGuestsRoomsState(selectedGuests) + } + + return ( + + {children} + + ) +} diff --git a/components/GuestsRoomsPicker/index.tsx b/components/GuestsRoomsPicker/index.tsx index 8533f79bd..090fc3803 100644 --- a/components/GuestsRoomsPicker/index.tsx +++ b/components/GuestsRoomsPicker/index.tsx @@ -1,6 +1,7 @@ "use client" import { useCallback, useEffect, useRef, useState } from "react" +import { useFormContext } from "react-hook-form" import { useIntl } from "react-intl" import { useGuestsRoomsStore } from "@/stores/guests-rooms" @@ -12,9 +13,14 @@ import GuestsRoomsPicker from "./GuestsRoomsPicker" import styles from "./guests-rooms-picker.module.css" -export default function GuestsRoomsPickerForm() { +export default function GuestsRoomsPickerForm({ + name = "rooms", +}: { + name: string +}) { const intl = useIntl() const [isOpen, setIsOpen] = useState(false) + const { setValue } = useFormContext() const { rooms, adultCount, childCount, setIsValidated } = useGuestsRoomsStore( (state) => ({ rooms: state.rooms, @@ -32,10 +38,11 @@ export default function GuestsRoomsPickerForm() { if (guestRoomsValidData.success) { setIsOpen(false) setIsValidated(false) + setValue(name, guestRoomsValidData.data, { shouldValidate: true }) } else { setIsValidated(true) } - }, [rooms, setIsValidated, setIsOpen]) + }, [rooms, name, setValue, setIsValidated, setIsOpen]) useEffect(() => { function handleClickOutside(evt: Event) { diff --git a/components/HotelReservation/SelectRate/RoomSelection/utils.ts b/components/HotelReservation/SelectRate/RoomSelection/utils.ts index e47a0da70..0b1ab884a 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/utils.ts +++ b/components/HotelReservation/SelectRate/RoomSelection/utils.ts @@ -1,28 +1,12 @@ +import { getFormattedUrlQueryParams } from "@/utils/url" + import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" function getHotelReservationQueryParams(searchParams: URLSearchParams) { - const searchParamsObject: Record = Array.from( - searchParams.entries() - ).reduce>( - (acc, [key, value]) => { - const keys = key.replace(/\]/g, "").split(/\[|\./) // Split keys by '[' or '.' - keys.reduce((nestedAcc, k, i) => { - if (i === keys.length - 1) { - // Convert value to number if the key is 'adults' or 'age' - ;(nestedAcc as Record)[k] = - k === "adults" || k === "age" ? Number(value) : value - } else { - if (!nestedAcc[k]) { - nestedAcc[k] = isNaN(Number(keys[i + 1])) ? {} : [] // Initialize as object or array - } - } - return nestedAcc[k] as Record - }, acc) - return acc - }, - {} as Record - ) - return searchParamsObject as SelectRateSearchParams + return getFormattedUrlQueryParams(searchParams, { + adults: "number", + age: "number", + }) as SelectRateSearchParams } export default getHotelReservationQueryParams diff --git a/components/TempDesignSystem/Select/index.tsx b/components/TempDesignSystem/Select/index.tsx index d506c146c..5575dbd33 100644 --- a/components/TempDesignSystem/Select/index.tsx +++ b/components/TempDesignSystem/Select/index.tsx @@ -34,6 +34,7 @@ export default function Select({ required = false, tabIndex, value, + maxHeight, }: SelectProps) { const [rootDiv, setRootDiv] = useState(undefined) @@ -81,6 +82,7 @@ export default function Select({ * on the container as well as to not overflow it at any time. */ UNSTABLE_portalContainer={rootDiv} + maxHeight={maxHeight} > {items.map((item) => ( diff --git a/components/TempDesignSystem/Select/select.ts b/components/TempDesignSystem/Select/select.ts index cac1e69cc..706ed71fd 100644 --- a/components/TempDesignSystem/Select/select.ts +++ b/components/TempDesignSystem/Select/select.ts @@ -9,6 +9,7 @@ export interface SelectProps onSelect: (key: Key) => void placeholder?: string value?: string | number + maxHeight?: number } export type SelectPortalContainer = HTMLDivElement | undefined diff --git a/stores/guests-rooms.ts b/stores/guests-rooms.ts index 2866184b0..bc305db61 100644 --- a/stores/guests-rooms.ts +++ b/stores/guests-rooms.ts @@ -1,22 +1,28 @@ "use client" import { produce } from "immer" -import { create } from "zustand" +import { createContext, useContext } from "react" +import { create, useStore } from "zustand" import { BedTypeEnum } from "@/types/components/bookingWidget/enums" -import { Child } from "@/types/components/bookingWidget/guestsRoomsPicker" +import { + Child, + GuestsRoom, +} from "@/types/components/bookingWidget/guestsRoomsPicker" -interface GuestsRooms { - rooms: [ - { - adults: number - children: Child[] - childrenInAdultsBed: number - }, - ] +const SESSION_STORAGE_KEY = "guests_rooms" + +interface extendedGuestsRoom extends GuestsRoom { + childrenInAdultsBed: number +} +interface GuestsRoomsState { + rooms: extendedGuestsRoom[] adultCount: number childCount: number isValidated: boolean +} + +interface GuestsRoomsStoreState extends GuestsRoomsState { increaseAdults: (roomIndex: number) => void decreaseAdults: (roomIndex: number) => void increaseChildren: (roomIndex: number) => void @@ -30,115 +36,192 @@ interface GuestsRooms { setIsValidated: (isValidated: boolean) => void } -export const useGuestsRoomsStore = create((set, get) => ({ - rooms: [ - { - adults: 1, - children: [], - childrenInAdultsBed: 0, - }, - ], - adultCount: 1, - childCount: 0, - isValidated: false, - increaseAdults: (roomIndex) => - set( - produce((state: GuestsRooms) => { - state.rooms[roomIndex].adults = state.rooms[roomIndex].adults + 1 - state.adultCount = state.adultCount + 1 - }) - ), - decreaseAdults: (roomIndex) => - set( - produce((state: GuestsRooms) => { - state.rooms[roomIndex].adults = state.rooms[roomIndex].adults - 1 - state.adultCount = state.adultCount - 1 - if ( - state.rooms[roomIndex].childrenInAdultsBed > - state.rooms[roomIndex].adults - ) { - const toUpdateIndex = state.rooms[roomIndex].children.findIndex( - (child) => child.bed == BedTypeEnum.IN_ADULTS_BED - ) - if (toUpdateIndex != -1) { - state.rooms[roomIndex].children[toUpdateIndex].bed = - state.rooms[roomIndex].children[toUpdateIndex].age < 3 - ? BedTypeEnum.IN_CRIB - : BedTypeEnum.IN_EXTRA_BED - state.rooms[roomIndex].childrenInAdultsBed = - state.rooms[roomIndex].adults - } - } - }) - ), - increaseChildren: (roomIndex) => - set( - produce((state: GuestsRooms) => { - state.rooms[roomIndex].children.push({ - age: -1, - bed: -1, +export function validateBedTypes(data: extendedGuestsRoom[]) { + data.forEach((room) => { + room.child.forEach((child) => { + const allowedBedTypes: number[] = [] + if (child.age <= 5 && room.adults >= room.childrenInAdultsBed) { + allowedBedTypes.push(BedTypeEnum.IN_ADULTS_BED) + } else if (child.age <= 5) { + room.childrenInAdultsBed = room.childrenInAdultsBed - 1 + } + if (child.age < 3) { + allowedBedTypes.push(BedTypeEnum.IN_CRIB) + } + if (child.age > 2) { + allowedBedTypes.push(BedTypeEnum.IN_EXTRA_BED) + } + if (!allowedBedTypes.includes(child.bed)) { + child.bed = allowedBedTypes[0] + } + }) + }) +} + +export function initGuestsRoomsState(initData?: GuestsRoom[]) { + const isBrowser = typeof window !== "undefined" + const sessionData = isBrowser + ? sessionStorage.getItem(SESSION_STORAGE_KEY) + : null + + const defaultGuestsData: extendedGuestsRoom = { + adults: 1, + child: [], + childrenInAdultsBed: 0, + } + const defaultData: GuestsRoomsState = { + rooms: [defaultGuestsData], + adultCount: 1, + childCount: 0, + isValidated: false, + } + + let inputData: GuestsRoomsState = defaultData + if (sessionData) { + inputData = JSON.parse(sessionData) + } + if (initData) { + inputData.rooms = initData.map((room) => { + const childrenInAdultsBed = room.child + ? room.child.reduce((acc, child) => { + acc = acc + (child.bed == BedTypeEnum.IN_ADULTS_BED ? 1 : 0) + return acc + }, 0) + : 0 + return { ...defaultGuestsData, ...room, childrenInAdultsBed } + }) as extendedGuestsRoom[] + + inputData.adultCount = initData.reduce((acc, room) => { + acc = acc + room.adults + return acc + }, 0) + inputData.childCount = initData.reduce((acc, room) => { + acc = acc + room.child?.length + return acc + }, 0) + validateBedTypes(inputData.rooms) + } + + return create()((set, get) => ({ + ...inputData, + increaseAdults: (roomIndex) => + set( + produce((state: GuestsRoomsState) => { + state.rooms[roomIndex].adults = state.rooms[roomIndex].adults + 1 + state.adultCount = state.adultCount + 1 }) - state.childCount = state.childCount + 1 - }) - ), - decreaseChildren: (roomIndex) => { - set( - produce((state: GuestsRooms) => { - const roomChildren = state.rooms[roomIndex].children - if ( - roomChildren.length && - roomChildren[roomChildren.length - 1].bed == BedTypeEnum.IN_ADULTS_BED - ) { + ), + decreaseAdults: (roomIndex) => + set( + produce((state: GuestsRoomsState) => { + state.rooms[roomIndex].adults = state.rooms[roomIndex].adults - 1 + state.adultCount = state.adultCount - 1 + if ( + state.rooms[roomIndex].childrenInAdultsBed > + state.rooms[roomIndex].adults + ) { + const toUpdateIndex = state.rooms[roomIndex].child.findIndex( + (child) => child.bed == BedTypeEnum.IN_ADULTS_BED + ) + if (toUpdateIndex != -1) { + state.rooms[roomIndex].child[toUpdateIndex].bed = + state.rooms[roomIndex].child[toUpdateIndex].age < 3 + ? BedTypeEnum.IN_CRIB + : BedTypeEnum.IN_EXTRA_BED + state.rooms[roomIndex].childrenInAdultsBed = + state.rooms[roomIndex].adults + } + } + }) + ), + increaseChildren: (roomIndex) => + set( + produce((state: GuestsRoomsState) => { + state.rooms[roomIndex].child.push({ + age: -1, + bed: -1, + }) + state.childCount = state.childCount + 1 + }) + ), + decreaseChildren: (roomIndex) => { + set( + produce((state: GuestsRoomsState) => { + const roomChildren = state.rooms[roomIndex].child + if ( + roomChildren.length && + roomChildren[roomChildren.length - 1].bed == + BedTypeEnum.IN_ADULTS_BED + ) { + state.rooms[roomIndex].childrenInAdultsBed = + state.rooms[roomIndex].childrenInAdultsBed - 1 + } + state.rooms[roomIndex].child.pop() + state.childCount = state.childCount - 1 + }) + ) + return get().rooms[roomIndex].child + }, + updateChildAge: (age, roomIndex, childIndex) => + set( + produce((state: GuestsRoomsState) => { + state.rooms[roomIndex].child[childIndex].age = age + }) + ), + updateChildBed: (bed, roomIndex, childIndex) => + set( + produce((state: GuestsRoomsState) => { + state.rooms[roomIndex].child[childIndex].bed = bed + }) + ), + increaseChildInAdultsBed: (roomIndex) => + set( + produce((state: GuestsRoomsState) => { + state.rooms[roomIndex].childrenInAdultsBed = + state.rooms[roomIndex].childrenInAdultsBed + 1 + }) + ), + decreaseChildInAdultsBed: (roomIndex) => + set( + produce((state: GuestsRoomsState) => { state.rooms[roomIndex].childrenInAdultsBed = state.rooms[roomIndex].childrenInAdultsBed - 1 - } - state.rooms[roomIndex].children.pop() - state.childCount = state.childCount - 1 - }) - ) - return get().rooms[roomIndex].children - }, - updateChildAge: (age, roomIndex, childIndex) => - set( - produce((state: GuestsRooms) => { - state.rooms[roomIndex].children[childIndex].age = age - }) - ), - updateChildBed: (bed, roomIndex, childIndex) => - set( - produce((state: GuestsRooms) => { - state.rooms[roomIndex].children[childIndex].bed = bed - }) - ), - increaseChildInAdultsBed: (roomIndex) => - set( - produce((state: GuestsRooms) => { - state.rooms[roomIndex].childrenInAdultsBed = - state.rooms[roomIndex].childrenInAdultsBed + 1 - }) - ), - decreaseChildInAdultsBed: (roomIndex) => - set( - produce((state: GuestsRooms) => { - state.rooms[roomIndex].childrenInAdultsBed = - state.rooms[roomIndex].childrenInAdultsBed - 1 - }) - ), - increaseRoom: () => - set( - produce((state: GuestsRooms) => { - state.rooms.push({ - adults: 1, - children: [], - childrenInAdultsBed: 0, }) - }) - ), - decreaseRoom: (roomIndex) => - set( - produce((state: GuestsRooms) => { - state.rooms.splice(roomIndex, 1) - }) - ), - setIsValidated: (isValidated) => set(() => ({ isValidated })), -})) + ), + increaseRoom: () => + set( + produce((state: GuestsRoomsState) => { + state.rooms.push({ + adults: 1, + child: [], + childrenInAdultsBed: 0, + }) + }) + ), + decreaseRoom: (roomIndex) => + set( + produce((state: GuestsRoomsState) => { + state.rooms.splice(roomIndex, 1) + }) + ), + setIsValidated: (isValidated) => set(() => ({ isValidated })), + })) +} + +export type GuestsRoomsStore = ReturnType + +export const GuestsRoomsContext = createContext(null) + +export const useGuestsRoomsStore = ( + selector: (store: GuestsRoomsStoreState) => T +): T => { + const guestsRoomsContextStore = useContext(GuestsRoomsContext) + + if (!guestsRoomsContextStore) { + throw new Error( + `guestsRoomsContextStore must be used within GuestsRoomsContextProvider` + ) + } + + return useStore(guestsRoomsContextStore, selector) +} diff --git a/types/components/bookingWidget/guestsRoomsPicker.ts b/types/components/bookingWidget/guestsRoomsPicker.ts index 5b3b7b3e2..61e8f7d7a 100644 --- a/types/components/bookingWidget/guestsRoomsPicker.ts +++ b/types/components/bookingWidget/guestsRoomsPicker.ts @@ -10,7 +10,7 @@ export type Child = { export type GuestsRoom = { adults: number - children: Child[] + child: Child[] } export interface GuestsRoomsPickerProps { diff --git a/types/components/bookingWidget/index.ts b/types/components/bookingWidget/index.ts index 8652b63da..a6230b0b2 100644 --- a/types/components/bookingWidget/index.ts +++ b/types/components/bookingWidget/index.ts @@ -4,6 +4,8 @@ import { z } from "zod" import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema" import { bookingWidgetVariants } from "@/components/Forms/BookingWidget/variants" +import { GuestsRoom } from "./guestsRoomsPicker" + import type { Locations } from "@/types/trpc/routers/hotel/locations" export type BookingWidgetSchema = z.output @@ -13,22 +15,27 @@ export type BookingWidgetSearchParams = { hotel?: string fromDate?: string toDate?: string - room?: string + room?: GuestsRoom[] + [key: string]: string | string[] | GuestsRoom[] | undefined } export type BookingWidgetType = VariantProps< typeof bookingWidgetVariants >["type"] +export interface BookingWidgetPageProps { + searchParams?: URLSearchParams +} + export interface BookingWidgetProps { type?: BookingWidgetType - searchParams?: BookingWidgetSearchParams + searchParams?: URLSearchParams } export interface BookingWidgetClientProps { locations: Locations type?: BookingWidgetType - searchParams?: BookingWidgetSearchParams + searchParams?: URLSearchParams } export interface BookingWidgetToggleButtonProps { diff --git a/utils/url.ts b/utils/url.ts index d9be2c491..4366fa181 100644 --- a/utils/url.ts +++ b/utils/url.ts @@ -9,3 +9,36 @@ export function removeTrailingSlash(pathname: string) { } return pathname } + +export function getFormattedUrlQueryParams( + searchParams: URLSearchParams, + dataTypes: Record +) { + const searchParamsObject: Record = Array.from( + searchParams.entries() + ).reduce>( + (acc, [key, value]) => { + const keys = key.replace(/\]/g, "").split(/\[|\./) // Split keys by '[' or '.' + keys.reduce((nestedAcc, k, i) => { + if (i === keys.length - 1) { + if (dataTypes[k] == "number") { + ;(nestedAcc as Record)[k] = Number(value) + } else if (dataTypes[k] == "boolean") { + ;(nestedAcc as Record)[k] = + value.toLowerCase() === "true" + } else { + ;(nestedAcc as Record)[k] = value + } + } else { + if (!nestedAcc[k]) { + nestedAcc[k] = isNaN(Number(keys[i + 1])) ? {} : [] // Initialize as object or array + } + } + return nestedAcc[k] as Record + }, acc) + return acc + }, + {} as Record + ) + return searchParamsObject +} From 24384c08f3aa332a1c8aeda08b31d42b29aaa844 Mon Sep 17 00:00:00 2001 From: Hrishikesh Vaipurkar Date: Mon, 28 Oct 2024 12:04:17 +0100 Subject: [PATCH 03/73] sub-task: SW-685 Optimized params --- app/[lang]/(live)/@bookingwidget/page.tsx | 4 ++-- server/routers/contentstack/bookingwidget/query.ts | 2 +- types/components/bookingWidget/index.ts | 5 ----- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/app/[lang]/(live)/@bookingwidget/page.tsx b/app/[lang]/(live)/@bookingwidget/page.tsx index 2d43c7773..6c944ae69 100644 --- a/app/[lang]/(live)/@bookingwidget/page.tsx +++ b/app/[lang]/(live)/@bookingwidget/page.tsx @@ -3,11 +3,11 @@ import { serverClient } from "@/lib/trpc/server" import BookingWidget, { preload } from "@/components/BookingWidget" -import { BookingWidgetPageProps } from "@/types/components/bookingWidget" +import { PageArgs } from "@/types/params" export default async function BookingWidgetPage({ searchParams, -}: BookingWidgetPageProps) { +}: PageArgs<{}, URLSearchParams>) { if (env.HIDE_FOR_NEXT_RELEASE) { return null } diff --git a/server/routers/contentstack/bookingwidget/query.ts b/server/routers/contentstack/bookingwidget/query.ts index c8a64f20e..f87899965 100644 --- a/server/routers/contentstack/bookingwidget/query.ts +++ b/server/routers/contentstack/bookingwidget/query.ts @@ -26,7 +26,7 @@ export const bookingwidgetQueryRouter = router({ const failedResponse = { hideBookingWidget: false } const { contentType, uid, lang } = ctx - // This condition is to handle 404 page case + // This condition is to handle 404 page case and booking flow if (!contentType || !uid) { console.log("No proper params defined: ", contentType, uid) return failedResponse diff --git a/types/components/bookingWidget/index.ts b/types/components/bookingWidget/index.ts index a6230b0b2..c668617da 100644 --- a/types/components/bookingWidget/index.ts +++ b/types/components/bookingWidget/index.ts @@ -16,17 +16,12 @@ export type BookingWidgetSearchParams = { fromDate?: string toDate?: string room?: GuestsRoom[] - [key: string]: string | string[] | GuestsRoom[] | undefined } export type BookingWidgetType = VariantProps< typeof bookingWidgetVariants >["type"] -export interface BookingWidgetPageProps { - searchParams?: URLSearchParams -} - export interface BookingWidgetProps { type?: BookingWidgetType searchParams?: URLSearchParams From 4bd089ab2a8791452f0ec9dfbcbaf76d4cc88779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matilda=20Landstr=C3=B6m?= Date: Tue, 29 Oct 2024 09:20:27 +0100 Subject: [PATCH 04/73] feat(698): fix correct order of blocks from CS --- .../Query/ContentPage/ContentPage.graphql | 3 ++ .../routers/contentstack/contentPage/query.ts | 35 ++++++++++++++----- .../trpc/routers/contentstack/contentPage.ts | 3 +- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/lib/graphql/Query/ContentPage/ContentPage.graphql b/lib/graphql/Query/ContentPage/ContentPage.graphql index 20f910aa9..d98771450 100644 --- a/lib/graphql/Query/ContentPage/ContentPage.graphql +++ b/lib/graphql/Query/ContentPage/ContentPage.graphql @@ -26,6 +26,9 @@ query GetContentPage($locale: String!, $uid: String!) { preamble ...NavigationLinks } + blocks { + __typename + } sidebar { __typename ...ContentSidebar_ContentPage diff --git a/server/routers/contentstack/contentPage/query.ts b/server/routers/contentstack/contentPage/query.ts index dc7659dc8..e7ec7e24c 100644 --- a/server/routers/contentstack/contentPage/query.ts +++ b/server/routers/contentstack/contentPage/query.ts @@ -19,7 +19,11 @@ import { TrackingChannelEnum, type TrackingSDKPageData, } from "@/types/components/tracking" -import type { GetContentPageSchema } from "@/types/trpc/routers/contentstack/contentPage" +import { ContentPageEnum } from "@/types/enums/contentPage" +import type { + GetBlock, + GetContentPageSchema, +} from "@/types/trpc/routers/contentstack/contentPage" export const contentPageQueryRouter = router({ get: contentstackExtendedProcedureUID.query(async ({ ctx }) => { @@ -79,21 +83,36 @@ export const contentPageQueryRouter = router({ ), ]) + const blocksOrder = mainResponse.data.content_page.blocks?.map( + (block) => block.__typename + ) + + let sortedBlocks + if (blocksOrder) { + const blocks = [ + blocksResponse1.data.content_page.blocks, + blocksResponse2.data.content_page.blocks, + ] + .flat(2) + .filter((obj) => !(obj && Object.keys(obj).length < 2)) + // Remove empty objects and objects with only typename + + sortedBlocks = blocksOrder + .map((typename: ContentPageEnum.ContentStack.blocks) => + blocks.find((block) => block?.__typename === typename) + ) + .filter((block): block is GetBlock => !!block) + } + const responseData = { ...mainResponse.data, content_page: { ...mainResponse.data.content_page, - blocks: [ - blocksResponse1.data.content_page.blocks, - blocksResponse2.data.content_page.blocks, - ] - .flat(2) - .filter((obj) => !(obj && Object.keys(obj).length < 2)), // Remove empty objects and objects with only typename + blocks: sortedBlocks, }, } const contentPage = contentPageSchema.safeParse(responseData) - if (!contentPage.success) { console.error( `Failed to validate Contentpage Data - (lang: ${lang}, uid: ${uid})` diff --git a/types/trpc/routers/contentstack/contentPage.ts b/types/trpc/routers/contentstack/contentPage.ts index e3f0f99bb..e1b71bff5 100644 --- a/types/trpc/routers/contentstack/contentPage.ts +++ b/types/trpc/routers/contentstack/contentPage.ts @@ -4,7 +4,6 @@ import { blocksSchema, contentPageRefsSchema, contentPageSchema, - contentPageSchemaBlocks, sidebarSchema, } from "@/server/routers/contentstack/contentPage/output" @@ -21,4 +20,6 @@ export interface ContentPage extends z.output {} export type Block = z.output +export type GetBlock = z.input + export type SidebarBlock = z.output From bc43573d43fbe9939b606ea44efa2cad0ae40668 Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Fri, 25 Oct 2024 16:37:41 +0200 Subject: [PATCH 05/73] fix: remove ids from svgs to prevent duplicate ids when rendering icon multiple times --- components/Icons/Ac.tsx | 21 +---- components/Icons/Accesories.tsx | 21 +---- components/Icons/Accessibility.tsx | 21 +---- components/Icons/Air.tsx | 21 +---- components/Icons/Airplane.tsx | 21 +---- components/Icons/ArrowRight.tsx | 21 +---- components/Icons/Bar.tsx | 21 +---- components/Icons/Bathtub.tsx | 21 +---- components/Icons/BedDouble.tsx | 21 +---- components/Icons/Biking.tsx | 21 +---- components/Icons/Breakfast.tsx | 21 +---- components/Icons/Business.tsx | 21 +---- components/Icons/Calendar.tsx | 21 +---- components/Icons/Camera.tsx | 21 +---- components/Icons/Cellphone.tsx | 21 +---- components/Icons/Chair.tsx | 21 +---- components/Icons/Check.tsx | 21 +---- components/Icons/CheckCircle.tsx | 21 +---- components/Icons/ChevronDown.tsx | 21 +---- components/Icons/ChevronLeft.tsx | 21 +---- components/Icons/ChevronRight.tsx | 21 +---- components/Icons/City.tsx | 21 +---- components/Icons/Close.tsx | 21 +---- components/Icons/CloseLarge.tsx | 21 +---- components/Icons/CoffeeAlt.tsx | 21 +---- components/Icons/Concierge.tsx | 21 +---- components/Icons/ConvenienceStore24h.tsx | 21 +---- components/Icons/Cool.tsx | 21 +---- components/Icons/Copy.tsx | 21 +---- components/Icons/CreditCard.tsx | 21 +---- components/Icons/CrossCircle.tsx | 21 +---- components/Icons/Cultural.tsx | 21 +---- components/Icons/Delete.tsx | 21 +---- components/Icons/Desk.tsx | 21 +---- components/Icons/DoorOpen.tsx | 21 +---- components/Icons/Download.tsx | 21 +---- components/Icons/Dresser.tsx | 21 +---- components/Icons/Edit.tsx | 21 +---- components/Icons/ElectricBike.tsx | 21 +---- components/Icons/ElectricCar.tsx | 21 +---- components/Icons/Email.tsx | 21 +---- components/Icons/ErrorCircle.tsx | 21 +---- components/Icons/EyeHide.tsx | 21 +---- components/Icons/EyeShow.tsx | 21 +---- components/Icons/Fan.tsx | 21 +---- components/Icons/Fitness.tsx | 21 +---- components/Icons/Footstool.tsx | 21 +---- components/Icons/Gallery.tsx | 21 +---- components/Icons/Garage.tsx | 21 +---- components/Icons/Gift.tsx | 21 +---- components/Icons/Globe.tsx | 21 +---- components/Icons/Golf.tsx | 21 +---- components/Icons/Groceries.tsx | 21 +---- components/Icons/Hairdryer.tsx | 57 +++++------- components/Icons/HandSoap.tsx | 32 ++----- components/Icons/Hanger.tsx | 21 +---- components/Icons/HangerAlt.tsx | 21 +---- components/Icons/Heart.tsx | 21 +---- components/Icons/Heat.tsx | 21 +---- components/Icons/House.tsx | 28 ++---- components/Icons/Image.tsx | 21 +---- components/Icons/InfoCircle.tsx | 21 +---- components/Icons/Iron.tsx | 21 +---- components/Icons/Kayaking.tsx | 21 +---- components/Icons/Kettle.tsx | 21 +---- components/Icons/KingBed.tsx | 13 ++- components/Icons/Lamp.tsx | 21 +---- components/Icons/LaundryMachine.tsx | 21 +---- components/Icons/LocalBar.tsx | 21 +---- components/Icons/Location.tsx | 21 +---- components/Icons/Lock.tsx | 21 +---- components/Icons/Map.tsx | 21 +---- components/Icons/Museum.tsx | 21 +---- components/Icons/Nature.tsx | 21 +---- components/Icons/Nightlife.tsx | 21 +---- components/Icons/NoBreakfast.tsx | 21 +---- components/Icons/NoSmoking.tsx | 21 +---- components/Icons/OutdoorFurniture.tsx | 21 +---- components/Icons/Parking.tsx | 21 +---- components/Icons/People2.tsx | 21 +---- components/Icons/Person.tsx | 21 +---- components/Icons/Pets.tsx | 21 +---- components/Icons/Phone.tsx | 21 +---- components/Icons/PlusCircle.tsx | 21 +---- components/Icons/PriceTag.tsx | 21 +---- components/Icons/Printer.tsx | 21 +---- components/Icons/Restaurant.tsx | 21 +---- components/Icons/RoomService.tsx | 21 +---- components/Icons/SafetyBox.tsx | 33 +++---- components/Icons/Sauna.tsx | 21 +---- components/Icons/Search.tsx | 21 +---- components/Icons/Service.tsx | 21 +---- components/Icons/Shopping.tsx | 21 +---- components/Icons/Shower.tsx | 21 +---- components/Icons/Skateboarding.tsx | 21 +---- components/Icons/Smoking.tsx | 21 +---- components/Icons/Spa.tsx | 21 +---- components/Icons/StarFilled.tsx | 21 +---- components/Icons/Street.tsx | 21 +---- components/Icons/Swim.tsx | 21 +---- components/Icons/Thermostat.tsx | 21 +---- components/Icons/Train.tsx | 21 +---- components/Icons/TripAdvisor.tsx | 25 ++---- components/Icons/Tshirt.tsx | 21 +---- components/Icons/TshirtWash.tsx | 21 +---- components/Icons/TvCasting.tsx | 21 +---- components/Icons/WarningTriangle.tsx | 21 +---- components/Icons/Wifi.tsx | 21 +---- components/Icons/WindowCurtainsAlt.tsx | 21 +---- components/Icons/WindowNotAvailable.tsx | 45 ++++------ components/Icons/WineBar.tsx | 21 +---- components/Icons/WoodFloor.tsx | 109 ++++++++++------------- components/Icons/Yard.tsx | 21 +---- 113 files changed, 539 insertions(+), 2008 deletions(-) diff --git a/components/Icons/Ac.tsx b/components/Icons/Ac.tsx index 5449112ca..b8b5a781a 100644 --- a/components/Icons/Ac.tsx +++ b/components/Icons/Ac.tsx @@ -14,23 +14,10 @@ export default function AcIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Accesories.tsx b/components/Icons/Accesories.tsx index 9aaf0c894..4ca631cee 100644 --- a/components/Icons/Accesories.tsx +++ b/components/Icons/Accesories.tsx @@ -18,23 +18,10 @@ export default function AccesoriesIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Accessibility.tsx b/components/Icons/Accessibility.tsx index 60d9a79d4..bf711678d 100644 --- a/components/Icons/Accessibility.tsx +++ b/components/Icons/Accessibility.tsx @@ -18,23 +18,10 @@ export default function AccessibilityIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Air.tsx b/components/Icons/Air.tsx index 239566515..75b19a56c 100644 --- a/components/Icons/Air.tsx +++ b/components/Icons/Air.tsx @@ -14,23 +14,10 @@ export default function AirIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Airplane.tsx b/components/Icons/Airplane.tsx index c68c37ce1..e68a911b2 100644 --- a/components/Icons/Airplane.tsx +++ b/components/Icons/Airplane.tsx @@ -18,23 +18,10 @@ export default function AirplaneIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/ArrowRight.tsx b/components/Icons/ArrowRight.tsx index 5f05e5f09..d21fb311c 100644 --- a/components/Icons/ArrowRight.tsx +++ b/components/Icons/ArrowRight.tsx @@ -20,23 +20,10 @@ export default function ArrowRightIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Bar.tsx b/components/Icons/Bar.tsx index 45796be66..8ab06dcd7 100644 --- a/components/Icons/Bar.tsx +++ b/components/Icons/Bar.tsx @@ -14,23 +14,10 @@ export default function BarIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Bathtub.tsx b/components/Icons/Bathtub.tsx index 26ab4ed54..52c8a0265 100644 --- a/components/Icons/Bathtub.tsx +++ b/components/Icons/Bathtub.tsx @@ -14,23 +14,10 @@ export default function BathtubIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/BedDouble.tsx b/components/Icons/BedDouble.tsx index 6eb59d7b0..be6e87ac8 100644 --- a/components/Icons/BedDouble.tsx +++ b/components/Icons/BedDouble.tsx @@ -18,23 +18,10 @@ export default function BedDoubleIcon({ className={classNames} {...props} > - - - - - - + ) } diff --git a/components/Icons/Biking.tsx b/components/Icons/Biking.tsx index 1cc9b3b9f..2ed9143b5 100644 --- a/components/Icons/Biking.tsx +++ b/components/Icons/Biking.tsx @@ -14,23 +14,10 @@ export default function BikingIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Breakfast.tsx b/components/Icons/Breakfast.tsx index dccfc0c39..6bd0e3705 100644 --- a/components/Icons/Breakfast.tsx +++ b/components/Icons/Breakfast.tsx @@ -18,23 +18,10 @@ export default function BreakfastIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Business.tsx b/components/Icons/Business.tsx index cb6ded53d..57e0e7f9f 100644 --- a/components/Icons/Business.tsx +++ b/components/Icons/Business.tsx @@ -18,23 +18,10 @@ export default function BusinessIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Calendar.tsx b/components/Icons/Calendar.tsx index eaa8f42ac..82f4ba811 100644 --- a/components/Icons/Calendar.tsx +++ b/components/Icons/Calendar.tsx @@ -18,23 +18,10 @@ export default function CalendarIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Camera.tsx b/components/Icons/Camera.tsx index 728a9e1c1..a41729cb6 100644 --- a/components/Icons/Camera.tsx +++ b/components/Icons/Camera.tsx @@ -14,23 +14,10 @@ export default function CameraIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Cellphone.tsx b/components/Icons/Cellphone.tsx index be9d5c0af..d40fdbc8d 100644 --- a/components/Icons/Cellphone.tsx +++ b/components/Icons/Cellphone.tsx @@ -18,23 +18,10 @@ export default function CellphoneIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Chair.tsx b/components/Icons/Chair.tsx index d7cdc81a2..0696cea8f 100644 --- a/components/Icons/Chair.tsx +++ b/components/Icons/Chair.tsx @@ -14,23 +14,10 @@ export default function ChairIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Check.tsx b/components/Icons/Check.tsx index 47c5067a0..b9b266bb0 100644 --- a/components/Icons/Check.tsx +++ b/components/Icons/Check.tsx @@ -14,23 +14,10 @@ export default function CheckIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/CheckCircle.tsx b/components/Icons/CheckCircle.tsx index f8278019b..55f100ceb 100644 --- a/components/Icons/CheckCircle.tsx +++ b/components/Icons/CheckCircle.tsx @@ -18,23 +18,10 @@ export default function CheckCircleIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/ChevronDown.tsx b/components/Icons/ChevronDown.tsx index 1bf541d43..b1370284c 100644 --- a/components/Icons/ChevronDown.tsx +++ b/components/Icons/ChevronDown.tsx @@ -18,23 +18,10 @@ export default function ChevronDownIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/ChevronLeft.tsx b/components/Icons/ChevronLeft.tsx index eb14d07dd..9676480a0 100644 --- a/components/Icons/ChevronLeft.tsx +++ b/components/Icons/ChevronLeft.tsx @@ -18,23 +18,10 @@ export default function ChevronLeftIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/ChevronRight.tsx b/components/Icons/ChevronRight.tsx index 9930ac095..98c1a57a5 100644 --- a/components/Icons/ChevronRight.tsx +++ b/components/Icons/ChevronRight.tsx @@ -18,23 +18,10 @@ export default function ChevronRightIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/City.tsx b/components/Icons/City.tsx index 791242ade..9f101a27d 100644 --- a/components/Icons/City.tsx +++ b/components/Icons/City.tsx @@ -14,23 +14,10 @@ export default function CityIcon({ className, color, ...props }: IconProps) { className={classNames} {...props} > - - - - - - + ) } diff --git a/components/Icons/Close.tsx b/components/Icons/Close.tsx index 6fdb13a67..4cd21c489 100644 --- a/components/Icons/Close.tsx +++ b/components/Icons/Close.tsx @@ -14,23 +14,10 @@ export default function CloseIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/CloseLarge.tsx b/components/Icons/CloseLarge.tsx index e6692f914..2ea89eb5b 100644 --- a/components/Icons/CloseLarge.tsx +++ b/components/Icons/CloseLarge.tsx @@ -18,23 +18,10 @@ export default function CloseLargeIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/CoffeeAlt.tsx b/components/Icons/CoffeeAlt.tsx index e76da5126..1f3051176 100644 --- a/components/Icons/CoffeeAlt.tsx +++ b/components/Icons/CoffeeAlt.tsx @@ -18,23 +18,10 @@ export default function CoffeeAltIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Concierge.tsx b/components/Icons/Concierge.tsx index 69119a969..24362e8c3 100644 --- a/components/Icons/Concierge.tsx +++ b/components/Icons/Concierge.tsx @@ -18,23 +18,10 @@ export default function ConciergeIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/ConvenienceStore24h.tsx b/components/Icons/ConvenienceStore24h.tsx index ace46579c..1c7696aa9 100644 --- a/components/Icons/ConvenienceStore24h.tsx +++ b/components/Icons/ConvenienceStore24h.tsx @@ -18,23 +18,10 @@ export default function ConvenienceStore24hIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Cool.tsx b/components/Icons/Cool.tsx index efa5ae67f..2deb081ad 100644 --- a/components/Icons/Cool.tsx +++ b/components/Icons/Cool.tsx @@ -14,23 +14,10 @@ export default function CoolIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Copy.tsx b/components/Icons/Copy.tsx index 05a5268a5..738456695 100644 --- a/components/Icons/Copy.tsx +++ b/components/Icons/Copy.tsx @@ -14,23 +14,10 @@ export default function CopyIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/CreditCard.tsx b/components/Icons/CreditCard.tsx index ae74314bb..d81ce95f2 100644 --- a/components/Icons/CreditCard.tsx +++ b/components/Icons/CreditCard.tsx @@ -18,23 +18,10 @@ export default function CreditCardIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/CrossCircle.tsx b/components/Icons/CrossCircle.tsx index f4c4c258a..f2c188a75 100644 --- a/components/Icons/CrossCircle.tsx +++ b/components/Icons/CrossCircle.tsx @@ -18,23 +18,10 @@ export default function CrossCircleIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Cultural.tsx b/components/Icons/Cultural.tsx index ede6800b6..af3b9894f 100644 --- a/components/Icons/Cultural.tsx +++ b/components/Icons/Cultural.tsx @@ -18,23 +18,10 @@ export default function CulturalIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Delete.tsx b/components/Icons/Delete.tsx index c91d632a8..2a4150666 100644 --- a/components/Icons/Delete.tsx +++ b/components/Icons/Delete.tsx @@ -14,23 +14,10 @@ export default function DeleteIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Desk.tsx b/components/Icons/Desk.tsx index de9d79ac6..289a227a8 100644 --- a/components/Icons/Desk.tsx +++ b/components/Icons/Desk.tsx @@ -14,23 +14,10 @@ export default function DeskIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/DoorOpen.tsx b/components/Icons/DoorOpen.tsx index 93bc2caf4..27957f2bd 100644 --- a/components/Icons/DoorOpen.tsx +++ b/components/Icons/DoorOpen.tsx @@ -18,23 +18,10 @@ export default function DoorOpenIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Download.tsx b/components/Icons/Download.tsx index 7c1e9017a..3e47c8a5a 100644 --- a/components/Icons/Download.tsx +++ b/components/Icons/Download.tsx @@ -18,23 +18,10 @@ export default function DownloadIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Dresser.tsx b/components/Icons/Dresser.tsx index b81f2bece..2b7d06f1c 100644 --- a/components/Icons/Dresser.tsx +++ b/components/Icons/Dresser.tsx @@ -14,23 +14,10 @@ export default function DresserIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Edit.tsx b/components/Icons/Edit.tsx index 209e16883..b0fa6141e 100644 --- a/components/Icons/Edit.tsx +++ b/components/Icons/Edit.tsx @@ -14,23 +14,10 @@ export default function EditIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/ElectricBike.tsx b/components/Icons/ElectricBike.tsx index 44e796cd8..1e0787c63 100644 --- a/components/Icons/ElectricBike.tsx +++ b/components/Icons/ElectricBike.tsx @@ -18,23 +18,10 @@ export default function ElectricBikeIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/ElectricCar.tsx b/components/Icons/ElectricCar.tsx index 1f9500b64..1af91af32 100644 --- a/components/Icons/ElectricCar.tsx +++ b/components/Icons/ElectricCar.tsx @@ -18,23 +18,10 @@ export default function ElectricCarIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Email.tsx b/components/Icons/Email.tsx index 8915e3991..7f781781f 100644 --- a/components/Icons/Email.tsx +++ b/components/Icons/Email.tsx @@ -20,23 +20,10 @@ export default function EmailIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/ErrorCircle.tsx b/components/Icons/ErrorCircle.tsx index 87b1514d3..4ba0fcde8 100644 --- a/components/Icons/ErrorCircle.tsx +++ b/components/Icons/ErrorCircle.tsx @@ -18,23 +18,10 @@ export default function ErrorCircleIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/EyeHide.tsx b/components/Icons/EyeHide.tsx index cc1db5926..c3da6d258 100644 --- a/components/Icons/EyeHide.tsx +++ b/components/Icons/EyeHide.tsx @@ -14,23 +14,10 @@ export default function EyeHideIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/EyeShow.tsx b/components/Icons/EyeShow.tsx index c3fdaf17d..fc308c352 100644 --- a/components/Icons/EyeShow.tsx +++ b/components/Icons/EyeShow.tsx @@ -14,23 +14,10 @@ export default function EyeShowIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Fan.tsx b/components/Icons/Fan.tsx index 128200104..394d013f2 100644 --- a/components/Icons/Fan.tsx +++ b/components/Icons/Fan.tsx @@ -14,23 +14,10 @@ export default function FanIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Fitness.tsx b/components/Icons/Fitness.tsx index 69b965a7b..113a90fb0 100644 --- a/components/Icons/Fitness.tsx +++ b/components/Icons/Fitness.tsx @@ -14,23 +14,10 @@ export default function FitnessIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Footstool.tsx b/components/Icons/Footstool.tsx index becb0fd80..6d7c6cc42 100644 --- a/components/Icons/Footstool.tsx +++ b/components/Icons/Footstool.tsx @@ -18,23 +18,10 @@ export default function FootstoolIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Gallery.tsx b/components/Icons/Gallery.tsx index 5bf1bc8b6..005f539f2 100644 --- a/components/Icons/Gallery.tsx +++ b/components/Icons/Gallery.tsx @@ -14,23 +14,10 @@ export default function GalleryIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Garage.tsx b/components/Icons/Garage.tsx index b715532cc..597c3e90b 100644 --- a/components/Icons/Garage.tsx +++ b/components/Icons/Garage.tsx @@ -14,23 +14,10 @@ export default function GarageIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Gift.tsx b/components/Icons/Gift.tsx index b07015db5..09914ab54 100644 --- a/components/Icons/Gift.tsx +++ b/components/Icons/Gift.tsx @@ -14,23 +14,10 @@ export default function GiftIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Globe.tsx b/components/Icons/Globe.tsx index 7ea19bf87..16bda388e 100644 --- a/components/Icons/Globe.tsx +++ b/components/Icons/Globe.tsx @@ -14,23 +14,10 @@ export default function GlobeIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Golf.tsx b/components/Icons/Golf.tsx index 027c906d7..a685ea440 100644 --- a/components/Icons/Golf.tsx +++ b/components/Icons/Golf.tsx @@ -14,23 +14,10 @@ export default function GolfIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Groceries.tsx b/components/Icons/Groceries.tsx index 404f09f7b..69410ed29 100644 --- a/components/Icons/Groceries.tsx +++ b/components/Icons/Groceries.tsx @@ -18,23 +18,10 @@ export default function GroceriesIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Hairdryer.tsx b/components/Icons/Hairdryer.tsx index 4d6ca72b2..bca9ef2e7 100644 --- a/components/Icons/Hairdryer.tsx +++ b/components/Icons/Hairdryer.tsx @@ -18,41 +18,28 @@ export default function HairdryerIcon({ className={classNames} {...props} > - - - - - - - - - - + + + + + ) } diff --git a/components/Icons/HandSoap.tsx b/components/Icons/HandSoap.tsx index a6850e4fe..70efe77f0 100644 --- a/components/Icons/HandSoap.tsx +++ b/components/Icons/HandSoap.tsx @@ -18,32 +18,12 @@ export default function HandSoapIcon({ className={classNames} {...props} > - - - - - - - - - - - - - + ) } diff --git a/components/Icons/Hanger.tsx b/components/Icons/Hanger.tsx index 63c869619..e75ac5489 100644 --- a/components/Icons/Hanger.tsx +++ b/components/Icons/Hanger.tsx @@ -14,23 +14,10 @@ export default function HangerIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/HangerAlt.tsx b/components/Icons/HangerAlt.tsx index 732656e60..67167cd8c 100644 --- a/components/Icons/HangerAlt.tsx +++ b/components/Icons/HangerAlt.tsx @@ -18,23 +18,10 @@ export default function HangerAltIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Heart.tsx b/components/Icons/Heart.tsx index 49cbb1171..3f191732e 100644 --- a/components/Icons/Heart.tsx +++ b/components/Icons/Heart.tsx @@ -14,23 +14,10 @@ export default function HeartIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Heat.tsx b/components/Icons/Heat.tsx index ef093e955..f2c2a4318 100644 --- a/components/Icons/Heat.tsx +++ b/components/Icons/Heat.tsx @@ -14,23 +14,10 @@ export default function HeatIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/House.tsx b/components/Icons/House.tsx index 1ddcf0f74..2fa747d57 100644 --- a/components/Icons/House.tsx +++ b/components/Icons/House.tsx @@ -14,28 +14,12 @@ export default function HouseIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - - - + ) } diff --git a/components/Icons/Image.tsx b/components/Icons/Image.tsx index 9fcfe4a71..aa0b875df 100644 --- a/components/Icons/Image.tsx +++ b/components/Icons/Image.tsx @@ -14,23 +14,10 @@ export default function ImageIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/InfoCircle.tsx b/components/Icons/InfoCircle.tsx index f87f5d663..3d9bc82c1 100644 --- a/components/Icons/InfoCircle.tsx +++ b/components/Icons/InfoCircle.tsx @@ -18,23 +18,10 @@ export default function InfoCircleIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Iron.tsx b/components/Icons/Iron.tsx index ac2ac4882..4f5964a90 100644 --- a/components/Icons/Iron.tsx +++ b/components/Icons/Iron.tsx @@ -14,23 +14,10 @@ export default function IronIcon({ className, color, ...props }: IconProps) { className={classNames} {...props} > - - - - - - + ) } diff --git a/components/Icons/Kayaking.tsx b/components/Icons/Kayaking.tsx index 1d4061a3a..1c5e61304 100644 --- a/components/Icons/Kayaking.tsx +++ b/components/Icons/Kayaking.tsx @@ -18,23 +18,10 @@ export default function KayakingIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Kettle.tsx b/components/Icons/Kettle.tsx index 28713719a..c75521398 100644 --- a/components/Icons/Kettle.tsx +++ b/components/Icons/Kettle.tsx @@ -14,23 +14,10 @@ export default function KettleIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/KingBed.tsx b/components/Icons/KingBed.tsx index d4df0f225..5e0f0615d 100644 --- a/components/Icons/KingBed.tsx +++ b/components/Icons/KingBed.tsx @@ -14,14 +14,11 @@ export default function KingBedIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - + ) } diff --git a/components/Icons/Lamp.tsx b/components/Icons/Lamp.tsx index bde8af3dd..785a09dce 100644 --- a/components/Icons/Lamp.tsx +++ b/components/Icons/Lamp.tsx @@ -14,23 +14,10 @@ export default function LampIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/LaundryMachine.tsx b/components/Icons/LaundryMachine.tsx index b4225197d..85f0bbb17 100644 --- a/components/Icons/LaundryMachine.tsx +++ b/components/Icons/LaundryMachine.tsx @@ -18,23 +18,10 @@ export default function LaundryMachineIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/LocalBar.tsx b/components/Icons/LocalBar.tsx index 7a5b3bb51..cd6b2f5dc 100644 --- a/components/Icons/LocalBar.tsx +++ b/components/Icons/LocalBar.tsx @@ -18,23 +18,10 @@ export default function LocalBarIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Location.tsx b/components/Icons/Location.tsx index b5d1d0e64..1b4eba611 100644 --- a/components/Icons/Location.tsx +++ b/components/Icons/Location.tsx @@ -18,23 +18,10 @@ export default function LocationIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Lock.tsx b/components/Icons/Lock.tsx index ce38caa46..5a29f5f3b 100644 --- a/components/Icons/Lock.tsx +++ b/components/Icons/Lock.tsx @@ -14,23 +14,10 @@ export default function LockIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Map.tsx b/components/Icons/Map.tsx index 9571db13b..e8b1b5727 100644 --- a/components/Icons/Map.tsx +++ b/components/Icons/Map.tsx @@ -14,23 +14,10 @@ export default function MapIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Museum.tsx b/components/Icons/Museum.tsx index d9a774908..5cbada97a 100644 --- a/components/Icons/Museum.tsx +++ b/components/Icons/Museum.tsx @@ -14,23 +14,10 @@ export default function MuseumIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Nature.tsx b/components/Icons/Nature.tsx index 4c48c377f..592620ab9 100644 --- a/components/Icons/Nature.tsx +++ b/components/Icons/Nature.tsx @@ -14,23 +14,10 @@ export default function NatureIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Nightlife.tsx b/components/Icons/Nightlife.tsx index 093099cb9..817f2ed5d 100644 --- a/components/Icons/Nightlife.tsx +++ b/components/Icons/Nightlife.tsx @@ -18,23 +18,10 @@ export default function NightlifeIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/NoBreakfast.tsx b/components/Icons/NoBreakfast.tsx index c09af6616..063b5defe 100644 --- a/components/Icons/NoBreakfast.tsx +++ b/components/Icons/NoBreakfast.tsx @@ -18,23 +18,10 @@ export default function NoBreakfastIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/NoSmoking.tsx b/components/Icons/NoSmoking.tsx index bdaa7d3f3..9ca2be0de 100644 --- a/components/Icons/NoSmoking.tsx +++ b/components/Icons/NoSmoking.tsx @@ -18,23 +18,10 @@ export default function NoSmokingIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/OutdoorFurniture.tsx b/components/Icons/OutdoorFurniture.tsx index 3ddac9f38..d0cdb78f3 100644 --- a/components/Icons/OutdoorFurniture.tsx +++ b/components/Icons/OutdoorFurniture.tsx @@ -18,23 +18,10 @@ export default function OutdoorFurnitureIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Parking.tsx b/components/Icons/Parking.tsx index 3680757e4..cbcf30927 100644 --- a/components/Icons/Parking.tsx +++ b/components/Icons/Parking.tsx @@ -14,23 +14,10 @@ export default function ParkingIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/People2.tsx b/components/Icons/People2.tsx index df5319f33..55f16de36 100644 --- a/components/Icons/People2.tsx +++ b/components/Icons/People2.tsx @@ -14,23 +14,10 @@ export default function People2Icon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Person.tsx b/components/Icons/Person.tsx index bc2452ac4..5f2671194 100644 --- a/components/Icons/Person.tsx +++ b/components/Icons/Person.tsx @@ -14,23 +14,10 @@ export default function PersonIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Pets.tsx b/components/Icons/Pets.tsx index db832a5af..679a3afee 100644 --- a/components/Icons/Pets.tsx +++ b/components/Icons/Pets.tsx @@ -14,23 +14,10 @@ export default function PetsIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Phone.tsx b/components/Icons/Phone.tsx index 1dd1c960e..a63adc769 100644 --- a/components/Icons/Phone.tsx +++ b/components/Icons/Phone.tsx @@ -20,23 +20,10 @@ export default function PhoneIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/PlusCircle.tsx b/components/Icons/PlusCircle.tsx index aa2c40afb..3f391672a 100644 --- a/components/Icons/PlusCircle.tsx +++ b/components/Icons/PlusCircle.tsx @@ -18,23 +18,10 @@ export default function PlusCircleIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/PriceTag.tsx b/components/Icons/PriceTag.tsx index d91e28900..fd735a1d3 100644 --- a/components/Icons/PriceTag.tsx +++ b/components/Icons/PriceTag.tsx @@ -18,23 +18,10 @@ export default function PriceTagIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Printer.tsx b/components/Icons/Printer.tsx index d703940da..e82696d37 100644 --- a/components/Icons/Printer.tsx +++ b/components/Icons/Printer.tsx @@ -14,23 +14,10 @@ export default function PrinterIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Restaurant.tsx b/components/Icons/Restaurant.tsx index 4ac06eb32..8fe36e0e6 100644 --- a/components/Icons/Restaurant.tsx +++ b/components/Icons/Restaurant.tsx @@ -18,23 +18,10 @@ export default function RestaurantIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/RoomService.tsx b/components/Icons/RoomService.tsx index 00aadd7d0..b472c7e2a 100644 --- a/components/Icons/RoomService.tsx +++ b/components/Icons/RoomService.tsx @@ -18,23 +18,10 @@ export default function RoomServiceIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/SafetyBox.tsx b/components/Icons/SafetyBox.tsx index 2b79a063f..7dc9e9590 100644 --- a/components/Icons/SafetyBox.tsx +++ b/components/Icons/SafetyBox.tsx @@ -18,29 +18,16 @@ export default function SafetyBoxIcon({ className={classNames} {...props} > - - - - - - - + + ) } diff --git a/components/Icons/Sauna.tsx b/components/Icons/Sauna.tsx index 13288f6d9..f5535c270 100644 --- a/components/Icons/Sauna.tsx +++ b/components/Icons/Sauna.tsx @@ -14,23 +14,10 @@ export default function SaunaIcon({ className, color, ...props }: IconProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Search.tsx b/components/Icons/Search.tsx index aa9f15e52..a849ffcbe 100644 --- a/components/Icons/Search.tsx +++ b/components/Icons/Search.tsx @@ -14,23 +14,10 @@ export default function SearchIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Service.tsx b/components/Icons/Service.tsx index 1f91f7cd8..9ebb97d6a 100644 --- a/components/Icons/Service.tsx +++ b/components/Icons/Service.tsx @@ -14,23 +14,10 @@ export default function ServiceIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Shopping.tsx b/components/Icons/Shopping.tsx index 87b1da6b2..d427f3a38 100644 --- a/components/Icons/Shopping.tsx +++ b/components/Icons/Shopping.tsx @@ -18,23 +18,10 @@ export default function ShoppingIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Shower.tsx b/components/Icons/Shower.tsx index 432b5f346..0f54c4829 100644 --- a/components/Icons/Shower.tsx +++ b/components/Icons/Shower.tsx @@ -14,23 +14,10 @@ export default function ShowerIcon({ className, color, ...props }: IconProps) { className={classNames} {...props} > - - - - - - + ) } diff --git a/components/Icons/Skateboarding.tsx b/components/Icons/Skateboarding.tsx index 6c0106ff3..a94dd0ff2 100644 --- a/components/Icons/Skateboarding.tsx +++ b/components/Icons/Skateboarding.tsx @@ -18,23 +18,10 @@ export default function SkateboardingIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Smoking.tsx b/components/Icons/Smoking.tsx index 58c30abce..e382ccc34 100644 --- a/components/Icons/Smoking.tsx +++ b/components/Icons/Smoking.tsx @@ -14,23 +14,10 @@ export default function SmokingIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Spa.tsx b/components/Icons/Spa.tsx index f3141a4e4..7cc26f820 100644 --- a/components/Icons/Spa.tsx +++ b/components/Icons/Spa.tsx @@ -14,23 +14,10 @@ export default function SpaIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/StarFilled.tsx b/components/Icons/StarFilled.tsx index 4e96f47d9..300924a37 100644 --- a/components/Icons/StarFilled.tsx +++ b/components/Icons/StarFilled.tsx @@ -18,23 +18,10 @@ export default function StarFilledIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Street.tsx b/components/Icons/Street.tsx index 414df197c..b2b0c2dfb 100644 --- a/components/Icons/Street.tsx +++ b/components/Icons/Street.tsx @@ -14,23 +14,10 @@ export default function StreetIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Swim.tsx b/components/Icons/Swim.tsx index abd2bd29a..534bb2068 100644 --- a/components/Icons/Swim.tsx +++ b/components/Icons/Swim.tsx @@ -14,23 +14,10 @@ export default function SwimIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Thermostat.tsx b/components/Icons/Thermostat.tsx index 2fd3ebe97..490f2f042 100644 --- a/components/Icons/Thermostat.tsx +++ b/components/Icons/Thermostat.tsx @@ -18,23 +18,10 @@ export default function ThermostatIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/Train.tsx b/components/Icons/Train.tsx index 79fae85f4..b892a9064 100644 --- a/components/Icons/Train.tsx +++ b/components/Icons/Train.tsx @@ -14,23 +14,10 @@ export default function TrainIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/TripAdvisor.tsx b/components/Icons/TripAdvisor.tsx index a1fc6d416..0b58ca839 100644 --- a/components/Icons/TripAdvisor.tsx +++ b/components/Icons/TripAdvisor.tsx @@ -18,25 +18,12 @@ export default function TripAdvisorIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Tshirt.tsx b/components/Icons/Tshirt.tsx index e6643725b..4d92d2418 100644 --- a/components/Icons/Tshirt.tsx +++ b/components/Icons/Tshirt.tsx @@ -14,23 +14,10 @@ export default function TshirtIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/TshirtWash.tsx b/components/Icons/TshirtWash.tsx index 7b952490f..58dea52a8 100644 --- a/components/Icons/TshirtWash.tsx +++ b/components/Icons/TshirtWash.tsx @@ -18,23 +18,10 @@ export default function TshirtWashIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/TvCasting.tsx b/components/Icons/TvCasting.tsx index ea6e7b90d..5169f41bf 100644 --- a/components/Icons/TvCasting.tsx +++ b/components/Icons/TvCasting.tsx @@ -18,23 +18,10 @@ export default function TvCastingIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/WarningTriangle.tsx b/components/Icons/WarningTriangle.tsx index 78d47d60a..cc4ebb766 100644 --- a/components/Icons/WarningTriangle.tsx +++ b/components/Icons/WarningTriangle.tsx @@ -18,23 +18,10 @@ export default function WarningTriangleIcon({ xmlns="http://www.w3.org/2000/svg" {...props} > - - - - - - + ) } diff --git a/components/Icons/Wifi.tsx b/components/Icons/Wifi.tsx index 80d2d1000..594bbe660 100644 --- a/components/Icons/Wifi.tsx +++ b/components/Icons/Wifi.tsx @@ -14,23 +14,10 @@ export default function BarIcon({ className, color, ...props }: IconProps) { fill="none" {...props} > - - - - - - + ) } diff --git a/components/Icons/WindowCurtainsAlt.tsx b/components/Icons/WindowCurtainsAlt.tsx index f32529566..6fe6dce4c 100644 --- a/components/Icons/WindowCurtainsAlt.tsx +++ b/components/Icons/WindowCurtainsAlt.tsx @@ -18,23 +18,10 @@ export default function WindowCurtainsAltIcon({ className={classNames} {...props} > - - - - - - + ) } diff --git a/components/Icons/WindowNotAvailable.tsx b/components/Icons/WindowNotAvailable.tsx index f064e146d..9e938c1e9 100644 --- a/components/Icons/WindowNotAvailable.tsx +++ b/components/Icons/WindowNotAvailable.tsx @@ -18,35 +18,22 @@ export default function WindowNotAvailableIcon({ className={classNames} {...props} > - - - - - - - - + + + ) } diff --git a/components/Icons/WineBar.tsx b/components/Icons/WineBar.tsx index a0ae499d1..1140d4ee6 100644 --- a/components/Icons/WineBar.tsx +++ b/components/Icons/WineBar.tsx @@ -14,23 +14,10 @@ export default function WineBarIcon({ className, color, ...props }: IconProps) { className={classNames} {...props} > - - - - - - + ) } diff --git a/components/Icons/WoodFloor.tsx b/components/Icons/WoodFloor.tsx index e7eaa4f35..1f57c69ad 100644 --- a/components/Icons/WoodFloor.tsx +++ b/components/Icons/WoodFloor.tsx @@ -18,67 +18,54 @@ export default function WoodFloorIcon({ className={classNames} {...props} > - - - - - - - - - - - - - - - - - + + + + + + + + + + + + ) } diff --git a/components/Icons/Yard.tsx b/components/Icons/Yard.tsx index 9e853b4b0..19c0bd09b 100644 --- a/components/Icons/Yard.tsx +++ b/components/Icons/Yard.tsx @@ -14,23 +14,10 @@ export default function YardIcon({ className, color, ...props }: IconProps) { className={classNames} {...props} > - - - - - - + ) } From 2fddc503c59808d06252bebdb7ceda516eacc590 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Fri, 25 Oct 2024 13:55:08 +0200 Subject: [PATCH 06/73] feat(SW-667): read more section to join loyalty sidebar --- .../Sidebar/JoinLoyalty/ReadMore/index.tsx | 49 +++++++++++++++++++ .../JoinLoyalty/ReadMore/readMore.module.css | 27 ++++++++++ components/Sidebar/JoinLoyalty/index.tsx | 8 +-- .../JoinLoyalty/joinLoyalty.module.css | 7 ++- constants/currentWebHrefs.ts | 18 +++++++ 5 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 components/Sidebar/JoinLoyalty/ReadMore/index.tsx create mode 100644 components/Sidebar/JoinLoyalty/ReadMore/readMore.module.css diff --git a/components/Sidebar/JoinLoyalty/ReadMore/index.tsx b/components/Sidebar/JoinLoyalty/ReadMore/index.tsx new file mode 100644 index 000000000..8fba3390e --- /dev/null +++ b/components/Sidebar/JoinLoyalty/ReadMore/index.tsx @@ -0,0 +1,49 @@ +import { faq, membershipTermsAndConditions } from "@/constants/currentWebHrefs" + +import ArrowRight from "@/components/Icons/ArrowRight" +import Link from "@/components/TempDesignSystem/Link" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import { getIntl } from "@/i18n" +import { getLang } from "@/i18n/serverContext" + +import styles from "./readMore.module.css" + +export default async function ReadMore() { + const [intl, lang] = await Promise.all([getIntl(), getLang()]) + + const links = [ + { href: faq[lang], translationId: "FAQ" }, + { + href: membershipTermsAndConditions[lang], + translationId: "Membership terms and conditions", + }, + ] + + return ( +
+ {intl.formatMessage({ id: "Read more" })} +
+ {links.map((link) => { + const translatedText = intl.formatMessage({ id: link.translationId }) + return ( + + + {translatedText} + + ) + })} +
+
+ ) +} diff --git a/components/Sidebar/JoinLoyalty/ReadMore/readMore.module.css b/components/Sidebar/JoinLoyalty/ReadMore/readMore.module.css new file mode 100644 index 000000000..c7b103996 --- /dev/null +++ b/components/Sidebar/JoinLoyalty/ReadMore/readMore.module.css @@ -0,0 +1,27 @@ +.wrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--Spacing-x2); +} + +.links { + display: grid; + gap: var(--Spacing-x-one-and-half); + justify-items: center; +} + +.link { + display: flex; + align-items: center; +} + +@media screen and (min-width: 1367px) { + .wrapper { + align-items: start; + } + + .links { + justify-items: start; + } +} diff --git a/components/Sidebar/JoinLoyalty/index.tsx b/components/Sidebar/JoinLoyalty/index.tsx index 13b3a91c5..3e353551f 100644 --- a/components/Sidebar/JoinLoyalty/index.tsx +++ b/components/Sidebar/JoinLoyalty/index.tsx @@ -10,6 +10,7 @@ import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" import Contact from "./Contact" +import ReadMore from "./ReadMore" import styles from "./joinLoyalty.module.css" @@ -18,15 +19,15 @@ import type { JoinLoyaltyContactProps } from "@/types/components/sidebar/joinLoy export default async function JoinLoyaltyContact({ block, }: JoinLoyaltyContactProps) { - const intl = await getIntl() - const user = await getName() + const [intl, user] = await Promise.all([getIntl(), getName()]) // Check if we have user, that means we are logged in. if (user) { return null } + return ( -
+
{block.title} @@ -70,6 +71,7 @@ export default async function JoinLoyaltyContact({ </section> </article> {block.contact ? <Contact contactBlock={block.contact} /> : null} + <ReadMore /> </section> ) } diff --git a/components/Sidebar/JoinLoyalty/joinLoyalty.module.css b/components/Sidebar/JoinLoyalty/joinLoyalty.module.css index 8ff913bce..5b7129ebf 100644 --- a/components/Sidebar/JoinLoyalty/joinLoyalty.module.css +++ b/components/Sidebar/JoinLoyalty/joinLoyalty.module.css @@ -1,7 +1,12 @@ +.joinLoyaltyContainer { + display: flex; + flex-direction: column; + gap: var(--Spacing-x5); +} .wrapper { display: grid; gap: var(--Spacing-x3); - padding-bottom: var(--Spacing-x5); + /* padding-bottom: var(--Spacing-x5); */ padding-top: var(--Spacing-x4); justify-items: center; } diff --git a/constants/currentWebHrefs.ts b/constants/currentWebHrefs.ts index 7438ee13a..751e875de 100644 --- a/constants/currentWebHrefs.ts +++ b/constants/currentWebHrefs.ts @@ -18,6 +18,24 @@ export const bookingTermsAndConditions: LangRoute = { sv: `${baseUrl.sv}/kundservice/priser-och-bokningsregler/bokningsregler`, } +export const membershipTermsAndConditions: LangRoute = { + da: `${baseUrl.da}/kundeservice/priser-og-bookingvilkar/vilkar-betingelser-for-medlemsskab`, + de: `${baseUrl.de}/kundenbetreuung/preise-und-richtlinien/scandic-friends-allgemeine-geschaftsbedingungen`, + en: `${baseUrl.en}/customer-service/rates-and-policies/scandic-friends-terms-conditions`, + fi: `${baseUrl.fi}/asiakaspalvelu/hinnat-ja-varausehdot/jasenyyden-ehdot`, + no: `${baseUrl.no}/kundeservice/priser-og-bestillingsvilkar/betingelser-for-medlemskap`, + sv: `${baseUrl.sv}/kundservice/priser-och-bokningsregler/medlemsvillkor`, +} + +export const faq: LangRoute = { + da: `${baseUrl.da}/scandic-friends/hjalp-og-service/ofte-stillede-sporgsmal`, + de: `${baseUrl.de}/scandic-friends/hilfe-und-service/haufig-gestellte-fragen`, + en: `${baseUrl.en}/scandic-friends/help-service/faq`, + fi: `${baseUrl.fi}/scandic-friends/apua-ongelmatilanteissa/usein-kysyttya`, + no: `${baseUrl.no}/scandic-friends/hjelp-og-medlemsservice/ofte-stilte-sporsmal`, + sv: `${baseUrl.sv}/scandic-friends/hjalp-och-service/vanliga-fragor`, +} + export const privacyPolicy: LangRoute = { da: `${baseUrl.da}/kundeservice/priser-og-bookingvilkar/persondatapolitik`, de: `${baseUrl.de}/kundenbetreuung/preise-und-richtlinien/datenschutzrichtlinie`, From 8219ff1931ae5d747b191e640859442ab9c8bf20 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy <chuma.mcphoy@scandichotels.com> Date: Fri, 25 Oct 2024 15:56:01 +0200 Subject: [PATCH 07/73] fix(SW-667): proper contact padding on mobile + translations --- components/Sidebar/JoinLoyalty/Contact/contact.module.css | 3 ++- i18n/dictionaries/da.json | 1 + i18n/dictionaries/de.json | 1 + i18n/dictionaries/en.json | 1 + i18n/dictionaries/fi.json | 1 + i18n/dictionaries/no.json | 1 + i18n/dictionaries/sv.json | 1 + 7 files changed, 8 insertions(+), 1 deletion(-) diff --git a/components/Sidebar/JoinLoyalty/Contact/contact.module.css b/components/Sidebar/JoinLoyalty/Contact/contact.module.css index 72127e707..59827194c 100644 --- a/components/Sidebar/JoinLoyalty/Contact/contact.module.css +++ b/components/Sidebar/JoinLoyalty/Contact/contact.module.css @@ -4,8 +4,8 @@ flex-direction: column; gap: var(--Spacing-x2); justify-content: center; - padding-top: var(--Spacing-x2); align-items: center; + padding-top: var(--Spacing-x5); } .contact { @@ -21,6 +21,7 @@ @media screen and (min-width: 1367px) { .contactContainer { align-items: start; + padding-top: var(--Spacing-x2); } .contact > div { diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index ee5954d7d..889555466 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -173,6 +173,7 @@ "Membership ID": "Medlems-id", "Membership ID copied to clipboard": "Medlems-ID kopieret til udklipsholder", "Membership cards": "Medlemskort", + "Membership terms and conditions": "Medlemsvilkår og -betingelser", "Menu": "Menu", "Modify": "Ændre", "Month": "Måned", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 9eb8bd1ec..e092862df 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -173,6 +173,7 @@ "Membership ID": "Mitglieds-ID", "Membership ID copied to clipboard": "Mitglieds-ID in die Zwischenablage kopiert", "Membership cards": "Mitgliedskarten", + "Membership terms and conditions": "Mitgliedschaftsbedingungen", "Menu": "Menu", "Modify": "Ändern", "Month": "Monat", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index f40909ac9..f52e13d8e 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -184,6 +184,7 @@ "Membership ID": "Membership ID", "Membership ID copied to clipboard": "Membership ID copied to clipboard", "Membership cards": "Membership cards", + "Membership terms and conditions": "Membership terms and conditions", "Menu": "Menu", "Modify": "Modify", "Month": "Month", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index b44c6755d..323dff85f 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -173,6 +173,7 @@ "Membership ID": "Jäsentunnus", "Membership ID copied to clipboard": "Jäsenyystunnus kopioitu leikepöydälle", "Membership cards": "Jäsenkortit", + "Membership terms and conditions": "Jäsenyysehdot", "Menu": "Valikko", "Modify": "Muokkaa", "Month": "Kuukausi", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index ab4143986..eb09018fb 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -171,6 +171,7 @@ "Membership ID": "Medlems-ID", "Membership ID copied to clipboard": "Medlems-ID kopiert til utklippstavlen", "Membership cards": "Medlemskort", + "Membership terms and conditions": "Medlemsvilkår og -betingelser", "Menu": "Menu", "Modify": "Endre", "Month": "Måned", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 9d7c3b95d..be22979a5 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -171,6 +171,7 @@ "Membership ID": "Medlems-ID", "Membership ID copied to clipboard": "Medlems-ID kopierat till urklipp", "Membership cards": "Medlemskort", + "Membership terms and conditions": "Medlemsvillkor", "Menu": "Meny", "Modify": "Ändra", "Month": "Månad", From d15e76de469e6a8bbb4492b2a6665479ea49f792 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy <chuma.mcphoy@scandichotels.com> Date: Fri, 25 Oct 2024 16:00:18 +0200 Subject: [PATCH 08/73] chore(SW-667): remove unused css --- components/Sidebar/JoinLoyalty/joinLoyalty.module.css | 1 - 1 file changed, 1 deletion(-) diff --git a/components/Sidebar/JoinLoyalty/joinLoyalty.module.css b/components/Sidebar/JoinLoyalty/joinLoyalty.module.css index 5b7129ebf..7e85c29bd 100644 --- a/components/Sidebar/JoinLoyalty/joinLoyalty.module.css +++ b/components/Sidebar/JoinLoyalty/joinLoyalty.module.css @@ -6,7 +6,6 @@ .wrapper { display: grid; gap: var(--Spacing-x3); - /* padding-bottom: var(--Spacing-x5); */ padding-top: var(--Spacing-x4); justify-items: center; } From 365cb15f90ec02d3301355756de3b2bd019f6068 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy <chuma.mcphoy@scandichotels.com> Date: Mon, 28 Oct 2024 15:57:33 +0100 Subject: [PATCH 09/73] chore(SW-667): move formatMessage call to array --- .../Sidebar/JoinLoyalty/ReadMore/index.tsx | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/components/Sidebar/JoinLoyalty/ReadMore/index.tsx b/components/Sidebar/JoinLoyalty/ReadMore/index.tsx index 8fba3390e..8890c4172 100644 --- a/components/Sidebar/JoinLoyalty/ReadMore/index.tsx +++ b/components/Sidebar/JoinLoyalty/ReadMore/index.tsx @@ -12,10 +12,13 @@ export default async function ReadMore() { const [intl, lang] = await Promise.all([getIntl(), getLang()]) const links = [ - { href: faq[lang], translationId: "FAQ" }, + { + href: faq[lang], + text: intl.formatMessage({ id: "FAQ" }), + }, { href: membershipTermsAndConditions[lang], - translationId: "Membership terms and conditions", + text: intl.formatMessage({ id: "Membership terms and conditions" }), }, ] @@ -23,26 +26,23 @@ export default async function ReadMore() { <article className={styles.wrapper}> <Subtitle>{intl.formatMessage({ id: "Read more" })}</Subtitle> <div className={styles.links}> - {links.map((link) => { - const translatedText = intl.formatMessage({ id: link.translationId }) - return ( - <Link - key={link.translationId} - size="small" - className={styles.link} + {links.map((link) => ( + <Link + key={link.text} + size="small" + className={styles.link} + color="burgundy" + href={link.href} + > + <ArrowRight color="burgundy" - href={link.href} - > - <ArrowRight - color="burgundy" - className={styles.icon} - height="20" - width="20" - /> - {translatedText} - </Link> - ) - })} + className={styles.icon} + height="20" + width="20" + /> + {link.text} + </Link> + ))} </div> </article> ) From 6c56fbc3134129baf9ece6652e550486e037f4b1 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy <chuma.mcphoy@scandichotels.com> Date: Mon, 28 Oct 2024 16:30:19 +0100 Subject: [PATCH 10/73] fix(SW-667): update translations based on content team requirements --- i18n/dictionaries/fi.json | 4 ++-- i18n/dictionaries/no.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 323dff85f..afe70ea1b 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -101,7 +101,7 @@ "Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin", "Explore nearby": "Tutustu lähialueeseen", "Extras to your booking": "Varauksessa lisäpalveluita", - "FAQ": "UKK", + "FAQ": "Usein kysytyt kysymykset", "Failed to delete credit card, please try again later.": "Luottokortin poistaminen epäonnistui, yritä myöhemmin uudelleen.", "Fair": "Messukeskus", "Find booking": "Etsi varaus", @@ -173,7 +173,7 @@ "Membership ID": "Jäsentunnus", "Membership ID copied to clipboard": "Jäsenyystunnus kopioitu leikepöydälle", "Membership cards": "Jäsenkortit", - "Membership terms and conditions": "Jäsenyysehdot", + "Membership terms and conditions": "Jäsenehdot ja -säännöt", "Menu": "Valikko", "Modify": "Muokkaa", "Month": "Kuukausi", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index eb09018fb..fab5ce670 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -100,7 +100,7 @@ "Explore all levels and benefits": "Utforsk alle nivåer og fordeler", "Explore nearby": "Utforsk i nærheten", "Extras to your booking": "Tilvalg til bestillingen din", - "FAQ": "FAQ", + "FAQ": "Ofte stilte spørsmål", "Failed to delete credit card, please try again later.": "Kunne ikke slette kredittkortet, prøv igjen senere.", "Fair": "Messe", "Find booking": "Finn booking", From aea963740b7be8dd65b23d1c75f790a7240ef2e2 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Thu, 17 Oct 2024 13:47:37 +0200 Subject: [PATCH 11/73] feat(sw-453): Added selected types count --- .../(standard)/select-rate/page.tsx | 10 ++++-- .../SelectRate/RoomFilter/index.tsx | 34 +++++++++++++++++++ .../RoomFilter/roomFilter.module.css | 5 +++ .../SelectRate/RoomSelection/index.tsx | 6 ++-- i18n/dictionaries/da.json | 2 +- i18n/dictionaries/de.json | 1 + i18n/dictionaries/en.json | 6 ++-- i18n/dictionaries/fi.json | 4 ++- i18n/dictionaries/no.json | 4 ++- i18n/dictionaries/sv.json | 4 ++- .../hotelReservation/selectRate/roomFilter.ts | 3 ++ .../selectRate/roomSelection.ts | 2 +- 12 files changed, 67 insertions(+), 14 deletions(-) create mode 100644 components/HotelReservation/SelectRate/RoomFilter/index.tsx create mode 100644 components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css create mode 100644 types/components/hotelReservation/selectRate/roomFilter.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index 2d80356ac..8ef2b4cd4 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -2,6 +2,7 @@ import { getProfileSafely } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard" +import RoomFilter from "@/components/HotelReservation/SelectRate/RoomFilter" import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection" import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { setLang } from "@/i18n/serverContext" @@ -23,7 +24,7 @@ export default async function SelectRatePage({ const adults = selectRoomParamsObject.room[0].adults // TODO: Handle multiple rooms const children = selectRoomParamsObject.room[0].child?.length // TODO: Handle multiple rooms - const [hotelData, roomConfigurations, user] = await Promise.all([ + const [hotelData, roomsAvailability, user] = await Promise.all([ serverClient().hotel.hotelData.get({ hotelId: searchParams.hotel, language: params.lang, @@ -39,7 +40,7 @@ export default async function SelectRatePage({ getProfileSafely(), ]) - if (!roomConfigurations) { + if (!roomsAvailability) { return "No rooms found" // TODO: Add a proper error message } @@ -54,8 +55,11 @@ export default async function SelectRatePage({ <HotelInfoCard hotelData={hotelData} /> <div className={styles.content}> <div className={styles.main}> + <RoomFilter + numberOfRooms={roomsAvailability.roomConfigurations.length} + /> <RoomSelection - roomConfigurations={roomConfigurations} + roomsAvailability={roomsAvailability} roomCategories={roomCategories ?? []} user={user} /> diff --git a/components/HotelReservation/SelectRate/RoomFilter/index.tsx b/components/HotelReservation/SelectRate/RoomFilter/index.tsx new file mode 100644 index 000000000..ff3edc852 --- /dev/null +++ b/components/HotelReservation/SelectRate/RoomFilter/index.tsx @@ -0,0 +1,34 @@ +"use client" + +import { useIntl } from "react-intl" + +import Checkbox from "@/components/TempDesignSystem/Checkbox" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" + +import styles from "./roomFilter.module.css" + +import { RoomFilterProps } from "@/types/components/hotelReservation/selectRate/roomFilter" + +function RoomFilter({ numberOfRooms }: RoomFilterProps) { + const intl = useIntl() + return ( + <div className={styles.container}> + <Body color="uiTextHighContrast"> + {numberOfRooms}{" "} + {intl.formatMessage( + { id: "Room types available" }, + { numberOfRooms: numberOfRooms } + )} + </Body> + <div className={styles.roomsFilter}> + <div> + <Checkbox name="accessibilty" /> + <Caption color="uiTextHighContrast">Accessibility room</Caption> + </div> + </div> + </div> + ) +} + +export default RoomFilter diff --git a/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css b/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css new file mode 100644 index 000000000..06caf5149 --- /dev/null +++ b/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css @@ -0,0 +1,5 @@ +.container { + display: flex; + flex-direction: row; + justify-content: space-between; +} diff --git a/components/HotelReservation/SelectRate/RoomSelection/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/index.tsx index c4c5e2e87..8592e64ea 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/index.tsx @@ -12,7 +12,7 @@ import { RoomSelectionProps } from "@/types/components/hotelReservation/selectRa import { Rate } from "@/types/components/hotelReservation/selectRate/selectRate" export default function RoomSelection({ - roomConfigurations, + roomsAvailability, roomCategories, user, }: RoomSelectionProps) { @@ -54,10 +54,10 @@ export default function RoomSelection({ onSubmit={handleSubmit} > <ul className={styles.roomList}> - {roomConfigurations.roomConfigurations.map((roomConfiguration) => ( + {roomsAvailability.roomConfigurations.map((roomConfiguration) => ( <li key={roomConfiguration.roomType}> <RoomCard - rateDefinitions={roomConfigurations.rateDefinitions} + rateDefinitions={roomsAvailability.rateDefinitions} roomConfiguration={roomConfiguration} roomCategories={roomCategories} handleSelectRate={setRateSummary} diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 889555466..b9c1622c4 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -241,9 +241,9 @@ "Restaurant & Bar": "Restaurant & Bar", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Gentag den nye adgangskode", - "Room": "Værelse", "Room & Terms": "Værelse & Vilkår", "Room facilities": "Værelsesfaciliteter", + "Room types available": "værelse {numberOfRooms, plural, one {# type} other {# types}} tilgængelig", "Rooms": "Værelser", "Rooms & Guests": "Værelser & gæster", "Sauna and gym": "Sauna and gym", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index e092862df..a4df8d378 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -244,6 +244,7 @@ "Room": "Zimmer", "Room & Terms": "Zimmer & Bedingungen", "Room facilities": "Zimmerausstattung", + "Room types available": "zimmer {numberOfRooms, plural, one {# type} other {# types}} verfügbar", "Rooms": "Räume", "Rooms & Guests": "Zimmer & Gäste", "Sauna and gym": "Sauna and gym", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index f52e13d8e..cf5551d18 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -52,8 +52,6 @@ "Check in": "Check in", "Check out": "Check out", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.", - "Check-in": "Check-in", - "Check-out": "Check-out", "Child age is required": "Child age is required", "Children": "Children", "Choose room": "Choose room", @@ -256,9 +254,9 @@ "Restaurant & Bar": "Restaurant & Bar", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Retype new password", - "Room": "Room", "Room & Terms": "Room & Terms", "Room facilities": "Room facilities", + "Room types available": "room {numberOfRooms, plural, one {# type} other {# types}} available", "Rooms": "Rooms", "Rooms & Guests": "Rooms & Guests", "Sauna and gym": "Sauna and gym", @@ -396,6 +394,8 @@ "special character": "special character", "spendable points expiring by": "{points} spendable points expiring by {date}", "to": "to", + "type": "type", + "types": "types", "uppercase letter": "uppercase letter", "{amount} out of {total}": "{amount} out of {total}", "{amount} {currency}": "{amount} {currency}", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index afe70ea1b..e8f430f16 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -241,9 +241,9 @@ "Restaurant & Bar": "Ravintola & Baari", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Kirjoita uusi salasana uudelleen", - "Room": "Huone", "Room & Terms": "Huone & Ehdot", "Room facilities": "Huoneen varustelu", + "Room types available": "huoneen {numberOfRooms, plural, one {# type} other {# types}} saatavilla", "Rooms": "Huoneet", "Rooms & Guests": "Huoneet & Vieraat", "Rooms & Guestss": "Huoneet & Vieraat", @@ -377,6 +377,8 @@ "to": "to", "uppercase letter": "iso kirjain", "{amount} out of {total}": "{amount}/{total}", + "type": "tyyppi", + "types": "tyypit", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index fab5ce670..0cdbf99e9 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -239,9 +239,9 @@ "Restaurant & Bar": "Restaurant & Bar", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Skriv inn nytt passord på nytt", - "Room": "Rom", "Room & Terms": "Rom & Vilkår", "Room facilities": "Romfasiliteter", + "Room types available": "romtyper {numberOfRooms, plural, one {# type} other {# types}} tilgjengelig", "Rooms": "Rom", "Rooms & Guests": "Rom og gjester", "Sauna and gym": "Sauna and gym", @@ -373,6 +373,8 @@ "to": "til", "uppercase letter": "stor bokstav", "{amount} out of {total}": "{amount} av {total}", + "type": "type", + "types": "typer", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index be22979a5..c88675fe9 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -239,9 +239,9 @@ "Restaurant & Bar": "Restaurang & Bar", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Upprepa nytt lösenord", - "Room": "Rum", "Room & Terms": "Rum & Villkor", "Room facilities": "Rumfaciliteter", + "Room types available": "rumstyper {numberOfRooms, plural, one {# type} other {# types}} tillgängliga", "Rooms": "Rum", "Rooms & Guests": "Rum och gäster", "Sauna and gym": "Sauna and gym", @@ -374,6 +374,8 @@ "to": "till", "uppercase letter": "stor bokstav", "{amount} out of {total}": "{amount} av {total}", + "type": "typ", + "types": "typer", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/types/components/hotelReservation/selectRate/roomFilter.ts b/types/components/hotelReservation/selectRate/roomFilter.ts new file mode 100644 index 000000000..3ac0c6be5 --- /dev/null +++ b/types/components/hotelReservation/selectRate/roomFilter.ts @@ -0,0 +1,3 @@ +export interface RoomFilterProps { + numberOfRooms: number +} diff --git a/types/components/hotelReservation/selectRate/roomSelection.ts b/types/components/hotelReservation/selectRate/roomSelection.ts index 9e944f5d8..fd633fa50 100644 --- a/types/components/hotelReservation/selectRate/roomSelection.ts +++ b/types/components/hotelReservation/selectRate/roomSelection.ts @@ -4,7 +4,7 @@ import { RoomData } from "@/types/hotel" import { SafeUser } from "@/types/user" export interface RoomSelectionProps { - roomConfigurations: RoomsAvailability + roomsAvailability: RoomsAvailability roomCategories: RoomData[] user: SafeUser } From fc63a8e5ca251add468f7118fb077ed321cadb93 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Thu, 24 Oct 2024 08:35:34 +0200 Subject: [PATCH 12/73] feat(SW-453) added filter options --- .../(standard)/select-rate/page.tsx | 4 +- .../SelectRate/RoomFilter/index.tsx | 87 ++++++++++++++++--- .../RoomFilter/roomFilter.module.css | 6 ++ .../Checkbox/checkbox.module.css | 40 --------- .../TempDesignSystem/Checkbox/checkbox.ts | 7 -- .../TempDesignSystem/Checkbox/index.tsx | 49 ----------- .../Form/Checkbox/checkbox.module.css | 2 +- i18n/dictionaries/da.json | 8 +- i18n/dictionaries/de.json | 8 +- i18n/dictionaries/en.json | 10 ++- i18n/dictionaries/fi.json | 8 +- i18n/dictionaries/no.json | 8 +- i18n/dictionaries/sv.json | 8 +- server/routers/hotels/query.ts | 1 + server/routers/hotels/schemas/room.ts | 6 ++ .../hotelReservation/selectRate/roomFilter.ts | 6 ++ 16 files changed, 138 insertions(+), 120 deletions(-) delete mode 100644 components/TempDesignSystem/Checkbox/checkbox.module.css delete mode 100644 components/TempDesignSystem/Checkbox/checkbox.ts delete mode 100644 components/TempDesignSystem/Checkbox/index.tsx 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 8ef2b4cd4..05103acf3 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -21,8 +21,8 @@ export default async function SelectRatePage({ const selectRoomParams = new URLSearchParams(searchParams) const selectRoomParamsObject = getHotelReservationQueryParams(selectRoomParams) - const adults = selectRoomParamsObject.room[0].adults // TODO: Handle multiple rooms - const children = selectRoomParamsObject.room[0].child?.length // TODO: Handle multiple rooms + const adults = selectRoomParamsObject.room?.[0].adults // TODO: Handle multiple rooms + const children = selectRoomParamsObject.room?.[0].child?.length // TODO: Handle multiple rooms const [hotelData, roomsAvailability, user] = await Promise.all([ serverClient().hotel.hotelData.get({ diff --git a/components/HotelReservation/SelectRate/RoomFilter/index.tsx b/components/HotelReservation/SelectRate/RoomFilter/index.tsx index ff3edc852..d775f8f46 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/index.tsx +++ b/components/HotelReservation/SelectRate/RoomFilter/index.tsx @@ -1,32 +1,93 @@ "use client" +import { zodResolver } from "@hookform/resolvers/zod" +import { useRef } from "react" +import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" -import Checkbox from "@/components/TempDesignSystem/Checkbox" +import { roomFilterSchema } from "@/server/routers/hotels/schemas/room" + +import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import styles from "./roomFilter.module.css" -import { RoomFilterProps } from "@/types/components/hotelReservation/selectRate/roomFilter" +import { + RoomFilterFormData, + RoomFilterProps, +} from "@/types/components/hotelReservation/selectRate/roomFilter" function RoomFilter({ numberOfRooms }: RoomFilterProps) { const intl = useIntl() + const methods = useForm<RoomFilterFormData>({ + defaultValues: { + allergyFriendly: false, + petFriendly: false, + accessibility: false, + }, + mode: "all", + reValidateMode: "onChange", + resolver: zodResolver(roomFilterSchema), + }) + + const formRef = useRef<HTMLFormElement | null>(null) + const { watch, setValue } = methods + const petFriendly = watch("petFriendly") + const allergyFriendly = watch("allergyFriendly") + + const onSubmit = (data: RoomFilterFormData) => { + if (data.petFriendly) { + setValue("allergyFriendly", false) + } else if (data.allergyFriendly) { + setValue("petFriendly", false) + } + console.log("Form submitted with data:", data) + } + return ( <div className={styles.container}> <Body color="uiTextHighContrast"> - {numberOfRooms}{" "} - {intl.formatMessage( - { id: "Room types available" }, - { numberOfRooms: numberOfRooms } - )} + {intl.formatMessage({ id: "Room types available" }, { numberOfRooms })} </Body> - <div className={styles.roomsFilter}> - <div> - <Checkbox name="accessibilty" /> - <Caption color="uiTextHighContrast">Accessibility room</Caption> - </div> - </div> + <FormProvider {...methods}> + <form ref={formRef} onSubmit={methods.handleSubmit(onSubmit)}> + <div className={styles.roomsFilter}> + <Checkbox + name="accessibility" + onChange={() => formRef.current?.requestSubmit()} + > + <Caption color="uiTextHighContrast"> + {intl.formatMessage({ id: "Accessibility room" })} + </Caption> + </Checkbox> + <Checkbox + name="petFriendly" + onChange={() => { + setValue("petFriendly", !petFriendly) + formRef.current?.requestSubmit() + }} + registerOptions={{ disabled: allergyFriendly }} + > + <Caption color="uiTextHighContrast"> + {intl.formatMessage({ id: "Pet room" })} + </Caption> + </Checkbox> + <Checkbox + name="allergyFriendly" + onChange={() => { + setValue("allergyFriendly", !allergyFriendly) + formRef.current?.requestSubmit() + }} + registerOptions={{ disabled: petFriendly }} + > + <Caption color="uiTextHighContrast"> + {intl.formatMessage({ id: "Allergy room" })} + </Caption> + </Checkbox> + </div> + </form> + </FormProvider> </div> ) } diff --git a/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css b/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css index 06caf5149..3c715f5c3 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css +++ b/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css @@ -3,3 +3,9 @@ flex-direction: row; justify-content: space-between; } + +.roomsFilter { + display: flex; + flex-direction: row; + gap: var(--Spacing-x3); +} diff --git a/components/TempDesignSystem/Checkbox/checkbox.module.css b/components/TempDesignSystem/Checkbox/checkbox.module.css deleted file mode 100644 index c831ba525..000000000 --- a/components/TempDesignSystem/Checkbox/checkbox.module.css +++ /dev/null @@ -1,40 +0,0 @@ -.container { - display: flex; - flex-direction: column; - color: var(--text-color); -} - -.container[data-selected] .checkbox { - border: none; - background: var(--UI-Input-Controls-Fill-Selected); -} - -.checkboxContainer { - display: flex; - align-items: flex-start; - gap: var(--Spacing-x-one-and-half); -} - -.checkbox { - width: 24px; - height: 24px; - min-width: 24px; - background-color: var(--UI-Input-Controls-Surface-Normal); - border: 2px solid var(--UI-Input-Controls-Border-Normal); - border-radius: var(--Corner-radius-Small); - transition: all 200ms; - display: flex; - align-items: center; - justify-content: center; - transition: all 200ms; - forced-color-adjust: none; - cursor: pointer; -} - -.error { - align-items: center; - color: var(--Scandic-Red-60); - display: flex; - gap: var(--Spacing-x-half); - margin-top: var(--Spacing-x1); -} diff --git a/components/TempDesignSystem/Checkbox/checkbox.ts b/components/TempDesignSystem/Checkbox/checkbox.ts deleted file mode 100644 index 8588b7401..000000000 --- a/components/TempDesignSystem/Checkbox/checkbox.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { RegisterOptions } from "react-hook-form" - -export interface CheckboxProps - extends React.InputHTMLAttributes<HTMLInputElement> { - name: string - registerOptions?: RegisterOptions -} diff --git a/components/TempDesignSystem/Checkbox/index.tsx b/components/TempDesignSystem/Checkbox/index.tsx deleted file mode 100644 index fde1742ff..000000000 --- a/components/TempDesignSystem/Checkbox/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Checkbox as AriaCheckbox } from "react-aria-components" -import { useController, useFormContext } from "react-hook-form" - -import { InfoCircleIcon } from "@/components/Icons" -import CheckIcon from "@/components/Icons/Check" -import Caption from "@/components/TempDesignSystem/Text/Caption" - -import { CheckboxProps } from "./checkbox" - -import styles from "./checkbox.module.css" - -export default function Checkbox({ - name, - children, - registerOptions, -}: React.PropsWithChildren<CheckboxProps>) { - const { control } = useFormContext() - const { field, fieldState } = useController({ - control, - name, - rules: registerOptions, - }) - - return ( - <AriaCheckbox - className={styles.container} - isSelected={field.value} - onChange={field.onChange} - data-testid={name} - > - {({ isSelected }) => ( - <> - <div className={styles.checkboxContainer}> - <div className={styles.checkbox}> - {isSelected && <CheckIcon color="white" />} - </div> - {children} - </div> - {children && fieldState.error ? ( - <Caption className={styles.error}> - <InfoCircleIcon color="red" /> - {fieldState.error.message} - </Caption> - ) : null} - </> - )} - </AriaCheckbox> - ) -} diff --git a/components/TempDesignSystem/Form/Checkbox/checkbox.module.css b/components/TempDesignSystem/Form/Checkbox/checkbox.module.css index 99077e212..2e924b226 100644 --- a/components/TempDesignSystem/Form/Checkbox/checkbox.module.css +++ b/components/TempDesignSystem/Form/Checkbox/checkbox.module.css @@ -16,7 +16,7 @@ .checkboxContainer { display: flex; - align-items: flex-start; + align-items: center; gap: var(--Spacing-x-one-and-half); } diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index b9c1622c4..ae660a5fe 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -5,13 +5,16 @@ "A photo of the room": "Et foto af værelset", "About meetings & conferences": "About meetings & conferences", "About the hotel": "About the hotel", + "Accessibility room": "Handicapvenligt værelse", "Activities": "Aktiviteter", "Add code": "Tilføj kode", "Add new card": "Tilføj nyt kort", + "Add room": "Tilføj værelse", "Address": "Adresse", "Adults": "voksne", "Airport": "Lufthavn", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle vores morgenmadsbuffeter tilbyder glutenfrie, veganske og allergivenlige muligheder.", + "Allergy room": "Allergivenligt værelse", "Already a friend?": "Allerede en ven?", "Amenities": "Faciliteter", "Amusement park": "Forlystelsespark", @@ -214,6 +217,7 @@ "Pay later": "Betal senere", "Pay now": "Betal nu", "Payment info": "Betalingsoplysninger", + "Pet room": "Kæledyrsrum", "Phone": "Telefon", "Phone is required": "Telefonnummer er påkrævet", "Phone number": "Telefonnummer", @@ -243,7 +247,7 @@ "Retype new password": "Gentag den nye adgangskode", "Room & Terms": "Værelse & Vilkår", "Room facilities": "Værelsesfaciliteter", - "Room types available": "værelse {numberOfRooms, plural, one {# type} other {# types}} tilgængelig", + "Room types available": "{numberOfRooms, plural, one {# room type} other {# room types}} tilgængelig", "Rooms": "Værelser", "Rooms & Guests": "Værelser & gæster", "Sauna and gym": "Sauna and gym", @@ -376,6 +380,8 @@ "to": "til", "uppercase letter": "stort bogstav", "{amount} out of {total}": "{amount} ud af {total}", + "room type": "værelsestype", + "room types": "værelsestyper", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index a4df8d378..e69b84d40 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -5,13 +5,16 @@ "A photo of the room": "Ein Foto des Zimmers", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Über das Hotel", + "Accessibility room": "Barrierefreies Zimmer", "Activities": "Aktivitäten", "Add code": "Code hinzufügen", "Add new card": "Neue Karte hinzufügen", + "Add room": "Zimmer hinzufügen", "Address": "Adresse", "Adults": "Erwachsene", "Airport": "Flughafen", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle unsere Frühstücksbuffets bieten glutenfreie, vegane und allergikerfreundliche Speisen.", + "Allergy room": "Allergiefreundliches Zimmer", "Already a friend?": "Sind wir schon Freunde?", "Amenities": "Annehmlichkeiten", "Amusement park": "Vergnügungspark", @@ -214,6 +217,7 @@ "Pay later": "Später bezahlen", "Pay now": "Jetzt bezahlen", "Payment info": "Zahlungsinformationen", + "Pet room": "Haustierfreundliches Zimmer", "Phone": "Telefon", "Phone is required": "Telefon ist erforderlich", "Phone number": "Telefonnummer", @@ -244,7 +248,7 @@ "Room": "Zimmer", "Room & Terms": "Zimmer & Bedingungen", "Room facilities": "Zimmerausstattung", - "Room types available": "zimmer {numberOfRooms, plural, one {# type} other {# types}} verfügbar", + "Room types available": "{numberOfRooms, plural, one {# room type} other {# room types}} verfügbar", "Rooms": "Räume", "Rooms & Guests": "Zimmer & Gäste", "Sauna and gym": "Sauna and gym", @@ -377,6 +381,8 @@ "to": "zu", "uppercase letter": "großbuchstabe", "{amount} out of {total}": "{amount} von {total}", + "room type": "zimmerart", + "room types": "zimmerarten", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index cf5551d18..59762c705 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -5,16 +5,19 @@ "A photo of the room": "A photo of the room", "About meetings & conferences": "About meetings & conferences", "About the hotel": "About the hotel", + "Accessibility room": "Accessibility room", "Activities": "Activities", "Add Room": "Add room", "Add code": "Add code", "Add new card": "Add new card", "Add to calendar": "Add to calendar", + "Add room": "Add room", "Address": "Address", "Adults": "Adults", "Age": "Age", "Airport": "Airport", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.", + "Allergy room": "Allergy room", "Already a friend?": "Already a friend?", "Amenities": "Amenities", "Amusement park": "Amusement park", @@ -224,6 +227,7 @@ "Pay now": "Pay now", "Payment info": "Payment info", "Payment received": "Payment received", + "Pet room": "Pet room", "Phone": "Phone", "Phone is required": "Phone is required", "Phone number": "Phone number", @@ -256,7 +260,7 @@ "Retype new password": "Retype new password", "Room & Terms": "Room & Terms", "Room facilities": "Room facilities", - "Room types available": "room {numberOfRooms, plural, one {# type} other {# types}} available", + "Room types available": "{numberOfRooms, plural, one {# room type} other {# room types}} available", "Rooms": "Rooms", "Rooms & Guests": "Rooms & Guests", "Sauna and gym": "Sauna and gym", @@ -391,11 +395,11 @@ "number": "number", "or": "or", "points": "Points", + "room type": "room type", + "room types": "room types", "special character": "special character", "spendable points expiring by": "{points} spendable points expiring by {date}", "to": "to", - "type": "type", - "types": "types", "uppercase letter": "uppercase letter", "{amount} out of {total}": "{amount} out of {total}", "{amount} {currency}": "{amount} {currency}", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index e8f430f16..12fb36018 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -5,13 +5,16 @@ "A photo of the room": "Kuva huoneesta", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Tietoja hotellista", + "Accessibility room": "Esteettömyyshuone", "Activities": "Aktiviteetit", "Add code": "Lisää koodi", "Add new card": "Lisää uusi kortti", + "Add room": "Lisää huone", "Address": "Osoite", "Adults": "Aikuista", "Airport": "Lentokenttä", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Kaikki aamiaisbuffettimme tarjoavat gluteenittomia, vegaanisia ja allergiaystävällisiä vaihtoehtoja.", + "Allergy room": "Allergiahuone", "Already a friend?": "Oletko jo ystävä?", "Amenities": "Mukavuudet", "Amusement park": "Huvipuisto", @@ -214,6 +217,7 @@ "Pay later": "Maksa myöhemmin", "Pay now": "Maksa nyt", "Payment info": "Maksutiedot", + "Pet room": "Lemmikkihuone", "Phone": "Puhelin", "Phone is required": "Puhelin vaaditaan", "Phone number": "Puhelinnumero", @@ -243,7 +247,7 @@ "Retype new password": "Kirjoita uusi salasana uudelleen", "Room & Terms": "Huone & Ehdot", "Room facilities": "Huoneen varustelu", - "Room types available": "huoneen {numberOfRooms, plural, one {# type} other {# types}} saatavilla", + "Room types available": "{numberOfRooms, plural, one {# room type} other {# room types}} saatavilla", "Rooms": "Huoneet", "Rooms & Guests": "Huoneet & Vieraat", "Rooms & Guestss": "Huoneet & Vieraat", @@ -379,6 +383,8 @@ "{amount} out of {total}": "{amount}/{total}", "type": "tyyppi", "types": "tyypit", + "room type": "huonetyyppi", + "room types": "huonetyypit", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 0cdbf99e9..fa02a3b96 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -5,13 +5,16 @@ "A photo of the room": "Et bilde av rommet", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Om hotellet", + "Accessibility room": "Tilgjengelighetsrom", "Activities": "Aktiviteter", "Add code": "Legg til kode", "Add new card": "Legg til nytt kort", + "Add room": "Legg til rom", "Address": "Adresse", "Adults": "Voksne", "Airport": "Flyplass", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle våre frokostbufféer tilbyr glutenfrie, veganske og allergivennlige alternativer.", + "Allergy room": "Allergihus", "Already a friend?": "Allerede Friend?", "Amenities": "Fasiliteter", "Amusement park": "Tivoli", @@ -212,6 +215,7 @@ "Pay later": "Betal senere", "Pay now": "Betal nå", "Payment info": "Betalingsinformasjon", + "Pet room": "Kjæledyrrom", "Phone": "Telefon", "Phone is required": "Telefon kreves", "Phone number": "Telefonnummer", @@ -241,7 +245,7 @@ "Retype new password": "Skriv inn nytt passord på nytt", "Room & Terms": "Rom & Vilkår", "Room facilities": "Romfasiliteter", - "Room types available": "romtyper {numberOfRooms, plural, one {# type} other {# types}} tilgjengelig", + "Room types available": "{numberOfRooms, plural, one {# room type} other {# room types}} tilgjengelig", "Rooms": "Rom", "Rooms & Guests": "Rom og gjester", "Sauna and gym": "Sauna and gym", @@ -375,6 +379,8 @@ "{amount} out of {total}": "{amount} av {total}", "type": "type", "types": "typer", + "room type": "romtype", + "room types": "romtyper", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index c88675fe9..5ee85ccde 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -5,13 +5,16 @@ "A photo of the room": "Ett foto av rummet", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Om hotellet", + "Accessibility room": "Tillgänglighetsrum", "Activities": "Aktiviteter", "Add code": "Lägg till kod", "Add new card": "Lägg till nytt kort", + "Add room": "Lägg till rum", "Address": "Adress", "Adults": "Vuxna", "Airport": "Flygplats", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alla våra frukostbufféer erbjuder glutenfria, veganska och allergivänliga alternativ.", + "Allergy room": "Allergirum", "Already a friend?": "Är du redan en vän?", "Amenities": "Bekvämligheter", "Amusement park": "Nöjespark", @@ -212,6 +215,7 @@ "Pay later": "Betala senare", "Pay now": "Betala nu", "Payment info": "Betalningsinformation", + "Pet room": "Husdjursrum", "Phone": "Telefon", "Phone is required": "Telefonnummer är obligatorisk", "Phone number": "Telefonnummer", @@ -241,7 +245,7 @@ "Retype new password": "Upprepa nytt lösenord", "Room & Terms": "Rum & Villkor", "Room facilities": "Rumfaciliteter", - "Room types available": "rumstyper {numberOfRooms, plural, one {# type} other {# types}} tillgängliga", + "Room types available": "{numberOfRooms, plural, one {# room type} other {# room types}} tillgängliga", "Rooms": "Rum", "Rooms & Guests": "Rum och gäster", "Sauna and gym": "Sauna and gym", @@ -376,6 +380,8 @@ "{amount} out of {total}": "{amount} av {total}", "type": "typ", "types": "typer", + "room type": "rumtyp", + "room types": "rumstyper", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index b5b5ff34b..289429ca7 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -441,6 +441,7 @@ export const hotelQueryRouter = router({ }, params ) + if (!apiResponse.ok) { const text = await apiResponse.text() roomsAvailabilityFailCounter.add(1, { diff --git a/server/routers/hotels/schemas/room.ts b/server/routers/hotels/schemas/room.ts index 19f922db0..af3a3e8bc 100644 --- a/server/routers/hotels/schemas/room.ts +++ b/server/routers/hotels/schemas/room.ts @@ -92,3 +92,9 @@ export const roomSchema = z roomFacilities: data.attributes.roomFacilities, } }) + +export const roomFilterSchema = z.object({ + accessibility: z.boolean(), + petFriendly: z.boolean(), + allergyFriendly: z.boolean(), +}) diff --git a/types/components/hotelReservation/selectRate/roomFilter.ts b/types/components/hotelReservation/selectRate/roomFilter.ts index 3ac0c6be5..ac31cfdbd 100644 --- a/types/components/hotelReservation/selectRate/roomFilter.ts +++ b/types/components/hotelReservation/selectRate/roomFilter.ts @@ -1,3 +1,9 @@ +import { z } from "zod" + +import { roomFilterSchema } from "@/server/routers/hotels/schemas/room" + export interface RoomFilterProps { numberOfRooms: number } + +export interface RoomFilterFormData extends z.output<typeof roomFilterSchema> {} From 260c9096f60ce38ac63b81efee2a5ead3348571d Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Thu, 24 Oct 2024 09:26:50 +0200 Subject: [PATCH 13/73] feat(sw-453): fixed lang files --- i18n/dictionaries/da.json | 7 ++----- i18n/dictionaries/de.json | 7 ++----- i18n/dictionaries/en.json | 2 +- i18n/dictionaries/fi.json | 9 ++------- i18n/dictionaries/no.json | 9 ++------- 5 files changed, 9 insertions(+), 25 deletions(-) diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index ae660a5fe..adf12745e 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -351,14 +351,12 @@ "Zoom in": "Zoom ind", "Zoom out": "Zoom ud", "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.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.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle <termsLink>Vilkår og betingelser</termsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyLink>Scandics Privatlivspolitik</privacyLink>. 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", "by": "inden", "characters": "tegn", "guest": "gæst", @@ -375,13 +373,12 @@ "number": "nummer", "or": "eller", "points": "Point", + "room type": "værelsestype", + "room types": "værelsestyper", "special character": "speciel karakter", "spendable points expiring by": "{points} Brugbare point udløber den {date}", "to": "til", "uppercase letter": "stort bogstav", - "{amount} out of {total}": "{amount} ud af {total}", - "room type": "værelsestype", - "room types": "værelsestyper", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index e69b84d40..e7c3ee819 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -352,14 +352,12 @@ "Zoom in": "Vergrößern", "Zoom out": "Verkleinern", "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.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.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle <termsLink>Vilkår og betingelser</termsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyLink>Scandics Privatlivspolitik</privacyLink>. 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", "by": "bis", "characters": "figuren", "guest": "gast", @@ -376,13 +374,12 @@ "number": "nummer", "or": "oder", "points": "Punkte", + "room type": "zimmerart", + "room types": "zimmerarten", "special character": "sonderzeichen", "spendable points expiring by": "{points} Einlösbare punkte verfallen bis zum {date}", "to": "zu", "uppercase letter": "großbuchstabe", - "{amount} out of {total}": "{amount} von {total}", - "room type": "zimmerart", - "room types": "zimmerarten", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 59762c705..cb2f91ecc 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -10,8 +10,8 @@ "Add Room": "Add room", "Add code": "Add code", "Add new card": "Add new card", - "Add to calendar": "Add to calendar", "Add room": "Add room", + "Add to calendar": "Add to calendar", "Address": "Address", "Adults": "Adults", "Age": "Age", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 12fb36018..9307bd321 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -352,14 +352,12 @@ "Zoom in": "Lähennä", "Zoom out": "Loitonna", "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.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.terms": "Maksamalla minkä tahansa saatavilla olevan maksutavan avulla hyväksyn tämän varauksen ehdot ja yleiset <termsLink>ehdot ja ehtoja</termsLink>, ja ymmärrän, että Scandic käsittelee minun henkilötietoni tässä varauksessa mukaisesti <privacyLink>Scandicin tietosuojavaltuuden</privacyLink> mukaisesti. Hyväksyn myös, että Scandic vaatii validin luottokortin majoituksen ajan, jos jokin jää maksamatta.", - "booking.thisRoomIsEquippedWith": "Tämä huone on varustettu", "by": "mennessä", "characters": "hahmoja", "guest": "Vieras", @@ -376,15 +374,12 @@ "number": "määrä", "or": "tai", "points": "pistettä", + "room type": "huonetyyppi", + "room types": "huonetyypit", "special character": "erikoishahmo", "spendable points expiring by": "{points} pistettä vanhenee {date} mennessä", "to": "to", "uppercase letter": "iso kirjain", - "{amount} out of {total}": "{amount}/{total}", - "type": "tyyppi", - "types": "tyypit", - "room type": "huonetyyppi", - "room types": "huonetyypit", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index fa02a3b96..e7b95db27 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -349,13 +349,11 @@ "Zoom in": "Zoom inn", "Zoom out": "Zoom ut", "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.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.thisRoomIsEquippedWith": "Dette rommet er utstyrt med", "by": "innen", "characters": "tegn", "guest": "gjest", @@ -372,15 +370,12 @@ "number": "antall", "or": "eller", "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", "uppercase letter": "stor bokstav", - "{amount} out of {total}": "{amount} av {total}", - "type": "type", - "types": "typer", - "room type": "romtype", - "room types": "romtyper", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" From 7b36139684d6ebc5978e3e3e87f334c4bfaad34e Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Fri, 25 Oct 2024 12:55:13 +0200 Subject: [PATCH 14/73] feat(sw-453): implemented filter from packages --- .../(standard)/select-rate/page.module.css | 24 ---- .../(standard)/select-rate/page.tsx | 42 ++++--- .../EnterDetails/Payment/index.tsx | 2 +- .../SelectRate/RoomFilter/index.tsx | 115 ++++++++---------- .../SelectRate/Rooms/index.tsx | 53 ++++++++ .../SelectRate/Rooms/rooms.module.css | 8 ++ i18n/dictionaries/da.json | 6 +- i18n/dictionaries/de.json | 6 +- i18n/dictionaries/en.json | 7 +- i18n/dictionaries/fi.json | 6 +- i18n/dictionaries/no.json | 6 +- i18n/dictionaries/sv.json | 6 +- lib/api/endpoints.ts | 1 + lib/api/index.ts | 2 +- server/routers/hotels/query.ts | 97 +++++++++++++++ server/routers/hotels/schemas/packages.ts | 59 +++++++++ server/routers/hotels/schemas/room.ts | 6 - server/tokenManager.ts | 2 +- .../hotelReservation/selectRate/room.ts | 6 + .../hotelReservation/selectRate/roomFilter.ts | 9 +- 20 files changed, 330 insertions(+), 133 deletions(-) delete mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.module.css create mode 100644 components/HotelReservation/SelectRate/Rooms/index.tsx create mode 100644 components/HotelReservation/SelectRate/Rooms/rooms.module.css create mode 100644 server/routers/hotels/schemas/packages.ts create mode 100644 types/components/hotelReservation/selectRate/room.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.module.css deleted file mode 100644 index 464c8ce65..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.module.css +++ /dev/null @@ -1,24 +0,0 @@ -.page { - min-height: 100dvh; - padding-top: var(--Spacing-x6); - padding-left: var(--Spacing-x2); - padding-right: var(--Spacing-x2); - background-color: var(--Scandic-Brand-Warm-White); -} - -.content { - max-width: var(--max-width); - margin: 0 auto; - display: flex; - flex-direction: column; - gap: var(--Spacing-x7); - padding: var(--Spacing-x2); -} - -.main { - flex-grow: 1; -} - -.summary { - max-width: 340px; -} 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 05103acf3..d43330adb 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -1,14 +1,12 @@ import { getProfileSafely } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" +import { RoomPackageCode } from "@/server/routers/hotels/schemas/packages" import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard" -import RoomFilter from "@/components/HotelReservation/SelectRate/RoomFilter" -import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection" +import Rooms from "@/components/HotelReservation/SelectRate/Rooms" import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { setLang } from "@/i18n/serverContext" -import styles from "./page.module.css" - import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import { LangParams, PageArgs } from "@/types/params" @@ -24,7 +22,7 @@ export default async function SelectRatePage({ const adults = selectRoomParamsObject.room?.[0].adults // TODO: Handle multiple rooms const children = selectRoomParamsObject.room?.[0].child?.length // TODO: Handle multiple rooms - const [hotelData, roomsAvailability, user] = await Promise.all([ + const [hotelData, roomsAvailability, packages, user] = await Promise.all([ serverClient().hotel.hotelData.get({ hotelId: searchParams.hotel, language: params.lang, @@ -37,6 +35,18 @@ export default async function SelectRatePage({ adults, children, }), + serverClient().hotel.packages.get({ + hotelId: searchParams.hotel, + startDate: searchParams.fromDate, + endDate: searchParams.toDate, + adults: adults, + children: children, + packageCodes: [ + RoomPackageCode.ACCE, + RoomPackageCode.PETR, + RoomPackageCode.ALLG, + ], + }), getProfileSafely(), ]) @@ -51,20 +61,14 @@ export default async function SelectRatePage({ const roomCategories = hotelData?.included return ( - <div> + <> <HotelInfoCard hotelData={hotelData} /> - <div className={styles.content}> - <div className={styles.main}> - <RoomFilter - numberOfRooms={roomsAvailability.roomConfigurations.length} - /> - <RoomSelection - roomsAvailability={roomsAvailability} - roomCategories={roomCategories ?? []} - user={user} - /> - </div> - </div> - </div> + <Rooms + roomsAvailability={roomsAvailability} + roomCategories={roomCategories ?? []} + user={user} + packages={packages ?? []} + /> + </> ) } diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index 0da2b79e2..ba126c1f2 100644 --- a/components/HotelReservation/EnterDetails/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -22,7 +22,7 @@ import { useEnterDetailsStore } from "@/stores/enter-details" import LoadingSpinner from "@/components/LoadingSpinner" import Button from "@/components/TempDesignSystem/Button" -import Checkbox from "@/components/TempDesignSystem/Checkbox" +import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" diff --git a/components/HotelReservation/SelectRate/RoomFilter/index.tsx b/components/HotelReservation/SelectRate/RoomFilter/index.tsx index d775f8f46..3c3586994 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/index.tsx +++ b/components/HotelReservation/SelectRate/RoomFilter/index.tsx @@ -1,49 +1,60 @@ "use client" import { zodResolver } from "@hookform/resolvers/zod" -import { useRef } from "react" +import { useCallback, useEffect, useMemo } from "react" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" +import { z } from "zod" -import { roomFilterSchema } from "@/server/routers/hotels/schemas/room" +import { RoomPackageCode } from "@/server/routers/hotels/schemas/packages" +import Chip from "@/components/TempDesignSystem/Chip" import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import styles from "./roomFilter.module.css" -import { - RoomFilterFormData, - RoomFilterProps, -} from "@/types/components/hotelReservation/selectRate/roomFilter" +import { RoomFilterProps } from "@/types/components/hotelReservation/selectRate/roomFilter" + +export default function RoomFilter({ + numberOfRooms, + onFilter, + filterOptions, +}: RoomFilterProps) { + const initialFilterValues = useMemo( + () => + filterOptions.reduce( + (acc, option) => { + acc[option.code] = false + return acc + }, + {} as Record<string, boolean | undefined> + ), + [filterOptions] + ) -function RoomFilter({ numberOfRooms }: RoomFilterProps) { const intl = useIntl() - const methods = useForm<RoomFilterFormData>({ - defaultValues: { - allergyFriendly: false, - petFriendly: false, - accessibility: false, - }, + const methods = useForm<Record<string, boolean | undefined>>({ + defaultValues: initialFilterValues, mode: "all", reValidateMode: "onChange", - resolver: zodResolver(roomFilterSchema), + resolver: zodResolver(z.object({})), }) - const formRef = useRef<HTMLFormElement | null>(null) - const { watch, setValue } = methods - const petFriendly = watch("petFriendly") - const allergyFriendly = watch("allergyFriendly") + const { watch, getValues, handleSubmit } = methods + const petFriendly = watch(RoomPackageCode.PETR) + const allergyFriendly = watch(RoomPackageCode.ALLG) - const onSubmit = (data: RoomFilterFormData) => { - if (data.petFriendly) { - setValue("allergyFriendly", false) - } else if (data.allergyFriendly) { - setValue("petFriendly", false) - } - console.log("Form submitted with data:", data) - } + const submitFilter = useCallback(() => { + const data = getValues() + onFilter(data) + }, [onFilter, getValues]) + + useEffect(() => { + const subscription = watch(() => handleSubmit(submitFilter)()) + return () => subscription.unsubscribe() + }, [handleSubmit, watch, submitFilter]) return ( <div className={styles.container}> @@ -51,45 +62,27 @@ function RoomFilter({ numberOfRooms }: RoomFilterProps) { {intl.formatMessage({ id: "Room types available" }, { numberOfRooms })} </Body> <FormProvider {...methods}> - <form ref={formRef} onSubmit={methods.handleSubmit(onSubmit)}> + <form onSubmit={handleSubmit(submitFilter)}> <div className={styles.roomsFilter}> - <Checkbox - name="accessibility" - onChange={() => formRef.current?.requestSubmit()} - > - <Caption color="uiTextHighContrast"> - {intl.formatMessage({ id: "Accessibility room" })} - </Caption> - </Checkbox> - <Checkbox - name="petFriendly" - onChange={() => { - setValue("petFriendly", !petFriendly) - formRef.current?.requestSubmit() - }} - registerOptions={{ disabled: allergyFriendly }} - > - <Caption color="uiTextHighContrast"> - {intl.formatMessage({ id: "Pet room" })} - </Caption> - </Checkbox> - <Checkbox - name="allergyFriendly" - onChange={() => { - setValue("allergyFriendly", !allergyFriendly) - formRef.current?.requestSubmit() - }} - registerOptions={{ disabled: petFriendly }} - > - <Caption color="uiTextHighContrast"> - {intl.formatMessage({ id: "Allergy room" })} - </Caption> - </Checkbox> + {filterOptions.map((option) => ( + <Checkbox + name={option.code} + key={option.code} + registerOptions={{ + required: false, + disabled: + (option.code === RoomPackageCode.PETR && allergyFriendly) || + (option.code === RoomPackageCode.ALLG && petFriendly), + }} + > + <Caption color="uiTextHighContrast"> + {intl.formatMessage({ id: option.description })} + </Caption> + </Checkbox> + ))} </div> </form> </FormProvider> </div> ) } - -export default RoomFilter diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx new file mode 100644 index 000000000..963f266d4 --- /dev/null +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -0,0 +1,53 @@ +"use client" + +import { useState } from "react" + +import { RoomsAvailability } from "@/server/routers/hotels/output" + +import RoomFilter from "../RoomFilter" +import RoomSelection from "../RoomSelection" + +import styles from "./rooms.module.css" + +import { RoomProps } from "@/types/components/hotelReservation/selectRate/room" +import { RoomPackageCodes } from "@/types/components/hotelReservation/selectRate/roomFilter" + +export default function Rooms({ + roomsAvailability, + roomCategories = [], + user, + packages, +}: RoomProps) { + const [rooms, setRooms] = useState<RoomsAvailability>(roomsAvailability) + + function handleFilter(filter: Record<string, boolean | undefined>) { + const selectedCodes = Object.keys(filter).filter((key) => filter[key]) + + if (selectedCodes.length === 0) { + setRooms(roomsAvailability) + return + } + + const filteredRooms = roomsAvailability.roomConfigurations.filter((room) => + room.features.some((feature) => + selectedCodes.includes(feature.code as RoomPackageCodes) + ) + ) + setRooms({ ...roomsAvailability, roomConfigurations: filteredRooms }) + } + + return ( + <div className={styles.content}> + <RoomFilter + numberOfRooms={rooms.roomConfigurations.length} + onFilter={handleFilter} + filterOptions={packages} + /> + <RoomSelection + roomsAvailability={rooms} + roomCategories={roomCategories} + user={user} + /> + </div> + ) +} diff --git a/components/HotelReservation/SelectRate/Rooms/rooms.module.css b/components/HotelReservation/SelectRate/Rooms/rooms.module.css new file mode 100644 index 000000000..a8e573530 --- /dev/null +++ b/components/HotelReservation/SelectRate/Rooms/rooms.module.css @@ -0,0 +1,8 @@ +.content { + max-width: var(--max-width); + margin: 0 auto; + display: flex; + flex-direction: column; + gap: var(--Spacing-x7); + padding: var(--Spacing-x2); +} diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index adf12745e..af4c667b8 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -5,7 +5,7 @@ "A photo of the room": "Et foto af værelset", "About meetings & conferences": "About meetings & conferences", "About the hotel": "About the hotel", - "Accessibility room": "Handicapvenligt værelse", + "Accessible Room": "Tilgængelighedsrum", "Activities": "Aktiviteter", "Add code": "Tilføj kode", "Add new card": "Tilføj nyt kort", @@ -14,7 +14,7 @@ "Adults": "voksne", "Airport": "Lufthavn", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle vores morgenmadsbuffeter tilbyder glutenfrie, veganske og allergivenlige muligheder.", - "Allergy room": "Allergivenligt værelse", + "Allergy Room": "Allergirum", "Already a friend?": "Allerede en ven?", "Amenities": "Faciliteter", "Amusement park": "Forlystelsespark", @@ -217,7 +217,7 @@ "Pay later": "Betal senere", "Pay now": "Betal nu", "Payment info": "Betalingsoplysninger", - "Pet room": "Kæledyrsrum", + "Pet Room": "Kæledyrsrum", "Phone": "Telefon", "Phone is required": "Telefonnummer er påkrævet", "Phone number": "Telefonnummer", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index e7c3ee819..c9483e349 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -5,7 +5,7 @@ "A photo of the room": "Ein Foto des Zimmers", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Über das Hotel", - "Accessibility room": "Barrierefreies Zimmer", + "Accessible Room": "Barrierefreies Zimmer", "Activities": "Aktivitäten", "Add code": "Code hinzufügen", "Add new card": "Neue Karte hinzufügen", @@ -14,7 +14,7 @@ "Adults": "Erwachsene", "Airport": "Flughafen", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle unsere Frühstücksbuffets bieten glutenfreie, vegane und allergikerfreundliche Speisen.", - "Allergy room": "Allergiefreundliches Zimmer", + "Allergy Room": "Allergikerzimmer", "Already a friend?": "Sind wir schon Freunde?", "Amenities": "Annehmlichkeiten", "Amusement park": "Vergnügungspark", @@ -217,7 +217,7 @@ "Pay later": "Später bezahlen", "Pay now": "Jetzt bezahlen", "Payment info": "Zahlungsinformationen", - "Pet room": "Haustierfreundliches Zimmer", + "Pet Room": "Haustierzimmer", "Phone": "Telefon", "Phone is required": "Telefon ist erforderlich", "Phone number": "Telefonnummer", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index cb2f91ecc..0511cf5a9 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -5,7 +5,7 @@ "A photo of the room": "A photo of the room", "About meetings & conferences": "About meetings & conferences", "About the hotel": "About the hotel", - "Accessibility room": "Accessibility room", + "Accessible Room": "Accessibility room", "Activities": "Activities", "Add Room": "Add room", "Add code": "Add code", @@ -17,7 +17,7 @@ "Age": "Age", "Airport": "Airport", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.", - "Allergy room": "Allergy room", + "Allergy Room": "Allergy room", "Already a friend?": "Already a friend?", "Amenities": "Amenities", "Amusement park": "Amusement park", @@ -227,7 +227,7 @@ "Pay now": "Pay now", "Payment info": "Payment info", "Payment received": "Payment received", - "Pet room": "Pet room", + "Pet Room": "Pet room", "Phone": "Phone", "Phone is required": "Phone is required", "Phone number": "Phone number", @@ -258,6 +258,7 @@ "Restaurant & Bar": "Restaurant & Bar", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Retype new password", + "Room": "Room", "Room & Terms": "Room & Terms", "Room facilities": "Room facilities", "Room types available": "{numberOfRooms, plural, one {# room type} other {# room types}} available", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 9307bd321..c8427a306 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -5,7 +5,7 @@ "A photo of the room": "Kuva huoneesta", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Tietoja hotellista", - "Accessibility room": "Esteettömyyshuone", + "Accessible Room": "Esteetön huone", "Activities": "Aktiviteetit", "Add code": "Lisää koodi", "Add new card": "Lisää uusi kortti", @@ -14,7 +14,7 @@ "Adults": "Aikuista", "Airport": "Lentokenttä", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Kaikki aamiaisbuffettimme tarjoavat gluteenittomia, vegaanisia ja allergiaystävällisiä vaihtoehtoja.", - "Allergy room": "Allergiahuone", + "Allergy Room": "Allergiahuone", "Already a friend?": "Oletko jo ystävä?", "Amenities": "Mukavuudet", "Amusement park": "Huvipuisto", @@ -217,7 +217,7 @@ "Pay later": "Maksa myöhemmin", "Pay now": "Maksa nyt", "Payment info": "Maksutiedot", - "Pet room": "Lemmikkihuone", + "Pet Room": "Lemmikkihuone", "Phone": "Puhelin", "Phone is required": "Puhelin vaaditaan", "Phone number": "Puhelinnumero", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index e7b95db27..69a1fbec4 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -5,7 +5,7 @@ "A photo of the room": "Et bilde av rommet", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Om hotellet", - "Accessibility room": "Tilgjengelighetsrom", + "Accessible Room": "Tilgjengelighetsrom", "Activities": "Aktiviteter", "Add code": "Legg til kode", "Add new card": "Legg til nytt kort", @@ -14,7 +14,7 @@ "Adults": "Voksne", "Airport": "Flyplass", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle våre frokostbufféer tilbyr glutenfrie, veganske og allergivennlige alternativer.", - "Allergy room": "Allergihus", + "Allergy Room": "Allergirom", "Already a friend?": "Allerede Friend?", "Amenities": "Fasiliteter", "Amusement park": "Tivoli", @@ -215,7 +215,7 @@ "Pay later": "Betal senere", "Pay now": "Betal nå", "Payment info": "Betalingsinformasjon", - "Pet room": "Kjæledyrrom", + "Pet Room": "Kjæledyrsrom", "Phone": "Telefon", "Phone is required": "Telefon kreves", "Phone number": "Telefonnummer", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 5ee85ccde..c2f90d520 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -5,7 +5,7 @@ "A photo of the room": "Ett foto av rummet", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Om hotellet", - "Accessibility room": "Tillgänglighetsrum", + "Accessible Room": "Tillgänglighetsrum", "Activities": "Aktiviteter", "Add code": "Lägg till kod", "Add new card": "Lägg till nytt kort", @@ -14,7 +14,7 @@ "Adults": "Vuxna", "Airport": "Flygplats", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alla våra frukostbufféer erbjuder glutenfria, veganska och allergivänliga alternativ.", - "Allergy room": "Allergirum", + "Allergy Room": "Allergirum", "Already a friend?": "Är du redan en vän?", "Amenities": "Bekvämligheter", "Amusement park": "Nöjespark", @@ -215,7 +215,7 @@ "Pay later": "Betala senare", "Pay now": "Betala nu", "Payment info": "Betalningsinformation", - "Pet room": "Husdjursrum", + "Pet Room": "Husdjursrum", "Phone": "Telefon", "Phone is required": "Telefonnummer är obligatorisk", "Phone number": "Telefonnummer", diff --git a/lib/api/endpoints.ts b/lib/api/endpoints.ts index 66bc36ec3..be1aee2fd 100644 --- a/lib/api/endpoints.ts +++ b/lib/api/endpoints.ts @@ -23,6 +23,7 @@ export namespace endpoints { rewards = `${profile}/reward`, tierRewards = `${profile}/TierRewards`, subscriberId = `${profile}/SubscriberId`, + packages = "package/v1/packages/hotel", } } diff --git a/lib/api/index.ts b/lib/api/index.ts index 9e32ac0cc..475e0da4e 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -37,7 +37,7 @@ export async function get( const searchParams = new URLSearchParams(params) if (searchParams.size) { searchParams.forEach((value, key) => { - url.searchParams.set(key, value) + url.searchParams.append(key, value) }) url.searchParams.sort() } diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 289429ca7..883b3a3ee 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -25,6 +25,10 @@ import { getHotelPageCounter, validateHotelPageRefs, } from "../contentstack/hotelPage/utils" +import { + getRoomPackagesInputSchema, + getRoomPackagesSchema, +} from "./schemas/packages" import { getHotelInputSchema, getHotelsAvailabilityInputSchema, @@ -57,6 +61,14 @@ const getHotelCounter = meter.createCounter("trpc.hotel.get") const getHotelSuccessCounter = meter.createCounter("trpc.hotel.get-success") const getHotelFailCounter = meter.createCounter("trpc.hotel.get-fail") +const getPackagesCounter = meter.createCounter("trpc.hotel.packages.get") +const getPackagesSuccessCounter = meter.createCounter( + "trpc.hotel.packages.get-success" +) +const getPackagesFailCounter = meter.createCounter( + "trpc.hotel.packages.get-fail" +) + const hotelsAvailabilityCounter = meter.createCounter( "trpc.hotel.availability.hotels" ) @@ -694,4 +706,89 @@ export const hotelQueryRouter = router({ return locations }), }), + packages: router({ + get: serviceProcedure + .input(getRoomPackagesInputSchema) + .query(async ({ input, ctx }) => { + const { hotelId, startDate, endDate, adults, children, packageCodes } = + input + + const searchParams = new URLSearchParams({ + startDate, + endDate, + adults: adults.toString(), + children: children.toString(), + }) + + packageCodes.forEach((code) => { + searchParams.append("packageCodes", code) + }) + + const params = searchParams.toString() + + getPackagesCounter.add(1, { + hotelId, + }) + console.info( + "api.hotels.packages start", + JSON.stringify({ query: { hotelId, params } }) + ) + + const apiResponse = await api.get( + `${api.endpoints.v1.packages}/${hotelId}`, + { + headers: { + Authorization: `Bearer ${ctx.serviceToken}`, + }, + }, + params + ) + + if (!apiResponse.ok) { + getPackagesFailCounter.add(1, { + hotelId, + error_type: "http_error", + error: JSON.stringify({ + status: apiResponse.status, + statusText: apiResponse.statusText, + }), + }) + console.error( + "api.hotels.packages error", + JSON.stringify({ query: { hotelId, params } }) + ) + throw serverErrorByStatus(apiResponse.status, apiResponse) + } + + const apiJson = await apiResponse.json() + const validatedPackagesData = getRoomPackagesSchema.safeParse(apiJson) + + if (!validatedPackagesData.success) { + getHotelFailCounter.add(1, { + hotelId, + error_type: "validation_error", + error: JSON.stringify(validatedPackagesData.error), + }) + + console.error( + "api.hotels.packages validation error", + JSON.stringify({ + query: { hotelId, params }, + error: validatedPackagesData.error, + }) + ) + throw badRequestError() + } + + getPackagesSuccessCounter.add(1, { + hotelId, + }) + console.info( + "api.hotels.packages success", + JSON.stringify({ query: { hotelId, params: params } }) + ) + + return validatedPackagesData.data + }), + }), }) diff --git a/server/routers/hotels/schemas/packages.ts b/server/routers/hotels/schemas/packages.ts new file mode 100644 index 000000000..d7ff6a4e1 --- /dev/null +++ b/server/routers/hotels/schemas/packages.ts @@ -0,0 +1,59 @@ +import { z } from "zod" + +export enum RoomPackageCode { + PETR = "PETR", + ALLG = "ALLG", + ACCE = "ACCE", +} + +export const getRoomPackagesInputSchema = z.object({ + hotelId: z.string(), + startDate: z.string(), + endDate: z.string(), + adults: z.number(), + children: z.number().optional().default(0), + packageCodes: z.array(z.string()).optional().default([]), +}) + +const packagesSchema = z.array( + z.object({ + code: z.enum([ + RoomPackageCode.PETR, + RoomPackageCode.ALLG, + RoomPackageCode.ACCE, + ]), + 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 getRoomPackagesSchema = z + .object({ + data: z.object({ + attributes: z.object({ + hotelId: z.number(), + packages: packagesSchema, + }), + relationships: z + .object({ + links: z.array( + z.object({ + url: z.string(), + type: z.string(), + }) + ), + }) + .optional(), + type: z.string(), + }), + }) + .transform((data) => data.data.attributes.packages) diff --git a/server/routers/hotels/schemas/room.ts b/server/routers/hotels/schemas/room.ts index af3a3e8bc..19f922db0 100644 --- a/server/routers/hotels/schemas/room.ts +++ b/server/routers/hotels/schemas/room.ts @@ -92,9 +92,3 @@ export const roomSchema = z roomFacilities: data.attributes.roomFacilities, } }) - -export const roomFilterSchema = z.object({ - accessibility: z.boolean(), - petFriendly: z.boolean(), - allergyFriendly: z.boolean(), -}) diff --git a/server/tokenManager.ts b/server/tokenManager.ts index 24180d017..980ca071d 100644 --- a/server/tokenManager.ts +++ b/server/tokenManager.ts @@ -74,7 +74,7 @@ export async function getServiceToken() { if (env.HIDE_FOR_NEXT_RELEASE) { scopes = ["profile"] } else { - scopes = ["profile", "hotel", "booking"] + scopes = ["profile", "hotel", "booking", "package"] } const tag = generateServiceTokenTag(scopes) const getCachedJwt = unstable_cache( diff --git a/types/components/hotelReservation/selectRate/room.ts b/types/components/hotelReservation/selectRate/room.ts new file mode 100644 index 000000000..b84e5c667 --- /dev/null +++ b/types/components/hotelReservation/selectRate/room.ts @@ -0,0 +1,6 @@ +import { RoomPackageData } from "./roomFilter" +import { RoomSelectionProps } from "./roomSelection" + +export interface RoomProps extends RoomSelectionProps { + packages: RoomPackageData +} diff --git a/types/components/hotelReservation/selectRate/roomFilter.ts b/types/components/hotelReservation/selectRate/roomFilter.ts index ac31cfdbd..c70f8f1d0 100644 --- a/types/components/hotelReservation/selectRate/roomFilter.ts +++ b/types/components/hotelReservation/selectRate/roomFilter.ts @@ -1,9 +1,14 @@ import { z } from "zod" -import { roomFilterSchema } from "@/server/routers/hotels/schemas/room" +import { getRoomPackagesSchema } from "@/server/routers/hotels/schemas/packages" export interface RoomFilterProps { numberOfRooms: number + onFilter: (filter: Record<string, boolean | undefined>) => void + filterOptions: RoomPackageData } -export interface RoomFilterFormData extends z.output<typeof roomFilterSchema> {} +export interface RoomPackageData + extends z.output<typeof getRoomPackagesSchema> {} + +export type RoomPackageCodes = RoomPackageData[number]["code"] From 8da94fc25973fa516a5d1e25a80bd17ddecc7663 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 28 Oct 2024 10:43:53 +0100 Subject: [PATCH 15/73] feat(SW-453): Fixed new filter buttons and updated price in summary --- .../(standard)/select-rate/page.tsx | 8 +-- .../SelectRate/RoomFilter/index.tsx | 49 +++++++++------- .../RoomFilter/roomFilter.module.css | 10 +++- .../RoomSelection/FlexibilityOption/index.tsx | 2 + .../RoomSelection/RateSummary/index.tsx | 23 ++++++++ .../RateSummary/rateSummary.module.css | 5 ++ .../RoomSelection/RoomCard/index.tsx | 43 +++++++++----- .../RoomCard/roomCard.module.css | 9 ++- .../SelectRate/RoomSelection/index.tsx | 2 + .../RoomSelection/roomSelection.module.css | 1 - .../SelectRate/Rooms/index.tsx | 5 +- .../SelectRate/Rooms/rooms.module.css | 2 +- .../HotelReservation/SelectRate/utils.ts | 19 +++++++ components/Icons/Allergy.tsx | 36 ++++++++++++ components/Icons/Pets.tsx | 2 +- components/Icons/Wheelchair.tsx | 40 +++++++++++++ components/Icons/icon.module.css | 5 ++ components/Icons/index.tsx | 2 + components/Icons/variants.ts | 1 + .../Form/FilterChip/Checkbox.tsx | 7 +++ .../Form/FilterChip/_Chip/chip.module.css | 27 +++++++++ .../Form/FilterChip/_Chip/index.tsx | 57 +++++++++++++++++++ .../Tooltip/tooltip.module.css | 9 ++- i18n/dictionaries/da.json | 1 + i18n/dictionaries/de.json | 1 + i18n/dictionaries/en.json | 1 + i18n/dictionaries/fi.json | 1 + i18n/dictionaries/no.json | 1 + i18n/dictionaries/sv.json | 1 + server/routers/hotels/output.ts | 12 +++- server/routers/hotels/schemas/packages.ts | 12 ++-- types/components/form/filterChip.ts | 16 ++++++ .../selectRate/flexibilityOption.ts | 1 + .../selectRate/rateSummary.ts | 2 + .../hotelReservation/selectRate/room.ts | 6 -- .../hotelReservation/selectRate/roomCard.ts | 1 + .../hotelReservation/selectRate/roomFilter.ts | 5 ++ .../selectRate/roomSelection.ts | 3 + .../hotelReservation/selectRate/selectRate.ts | 1 + 39 files changed, 367 insertions(+), 62 deletions(-) create mode 100644 components/HotelReservation/SelectRate/utils.ts create mode 100644 components/Icons/Allergy.tsx create mode 100644 components/Icons/Wheelchair.tsx create mode 100644 components/TempDesignSystem/Form/FilterChip/Checkbox.tsx create mode 100644 components/TempDesignSystem/Form/FilterChip/_Chip/chip.module.css create mode 100644 components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx create mode 100644 types/components/form/filterChip.ts delete mode 100644 types/components/hotelReservation/selectRate/room.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index d43330adb..603bbbf4a 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -1,12 +1,12 @@ import { getProfileSafely } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" -import { RoomPackageCode } from "@/server/routers/hotels/schemas/packages" import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard" import Rooms from "@/components/HotelReservation/SelectRate/Rooms" import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { setLang } from "@/i18n/serverContext" +import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import { LangParams, PageArgs } from "@/types/params" @@ -42,9 +42,9 @@ export default async function SelectRatePage({ adults: adults, children: children, packageCodes: [ - RoomPackageCode.ACCE, - RoomPackageCode.PETR, - RoomPackageCode.ALLG, + RoomPackageCodeEnum.ACCE, + RoomPackageCodeEnum.PETR, + RoomPackageCodeEnum.ALLG, ], }), getProfileSafely(), diff --git a/components/HotelReservation/SelectRate/RoomFilter/index.tsx b/components/HotelReservation/SelectRate/RoomFilter/index.tsx index 3c3586994..836dd5b49 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/index.tsx +++ b/components/HotelReservation/SelectRate/RoomFilter/index.tsx @@ -6,16 +6,19 @@ import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" import { z } from "zod" -import { RoomPackageCode } from "@/server/routers/hotels/schemas/packages" - -import Chip from "@/components/TempDesignSystem/Chip" -import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" +import { InfoCircleIcon } from "@/components/Icons" +import CheckboxChip from "@/components/TempDesignSystem/Form/FilterChip/Checkbox" import Body from "@/components/TempDesignSystem/Text/Body" -import Caption from "@/components/TempDesignSystem/Text/Caption" +import { Tooltip } from "@/components/TempDesignSystem/Tooltip" + +import { getIconForFeatureCode } from "../utils" import styles from "./roomFilter.module.css" -import { RoomFilterProps } from "@/types/components/hotelReservation/selectRate/roomFilter" +import { + RoomFilterProps, + RoomPackageCodeEnum, +} from "@/types/components/hotelReservation/selectRate/roomFilter" export default function RoomFilter({ numberOfRooms, @@ -43,8 +46,8 @@ export default function RoomFilter({ }) const { watch, getValues, handleSubmit } = methods - const petFriendly = watch(RoomPackageCode.PETR) - const allergyFriendly = watch(RoomPackageCode.ALLG) + const petFriendly = watch(RoomPackageCodeEnum.PETR) + const allergyFriendly = watch(RoomPackageCodeEnum.ALLG) const submitFilter = useCallback(() => { const data = getValues() @@ -65,21 +68,27 @@ export default function RoomFilter({ <form onSubmit={handleSubmit(submitFilter)}> <div className={styles.roomsFilter}> {filterOptions.map((option) => ( - <Checkbox + <CheckboxChip name={option.code} key={option.code} - registerOptions={{ - required: false, - disabled: - (option.code === RoomPackageCode.PETR && allergyFriendly) || - (option.code === RoomPackageCode.ALLG && petFriendly), - }} - > - <Caption color="uiTextHighContrast"> - {intl.formatMessage({ id: option.description })} - </Caption> - </Checkbox> + label={intl.formatMessage({ id: option.description })} + disabled={ + (option.code === RoomPackageCodeEnum.ALLG && petFriendly) || + (option.code === RoomPackageCodeEnum.PETR && allergyFriendly) + } + selected={getValues(option.code)} + Icon={getIconForFeatureCode(option.code)} + /> ))} + <Tooltip + text={intl.formatMessage({ + id: "Pet-friendly rooms have an additional fee of 20 EUR per stay", + })} + position="bottom" + arrow="right" + > + <InfoCircleIcon className={styles.infoIcon} /> + </Tooltip> </div> </form> </FormProvider> diff --git a/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css b/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css index 3c715f5c3..c0eff095a 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css +++ b/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css @@ -2,10 +2,18 @@ display: flex; flex-direction: row; justify-content: space-between; + align-items: center; } .roomsFilter { display: flex; flex-direction: row; - gap: var(--Spacing-x3); + gap: var(--Spacing-x1); + align-items: center; +} + +.roomsFilter .infoIcon, +.roomsFilter .infoIcon path { + stroke: var(--UI-Text-Medium-contrast); + fill: transparent; } diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx index a523305ae..a0a92bb66 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx @@ -19,6 +19,7 @@ export default function FlexibilityOption({ priceInformation, roomType, roomTypeCode, + features, handleSelectRate, }: FlexibilityOptionProps) { const [rootDiv, setRootDiv] = useState<Element | undefined>(undefined) @@ -52,6 +53,7 @@ export default function FlexibilityOption({ priceName: name, public: publicPrice, member: memberPrice, + features, } handleSelectRate(rate) } diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx index b929bfe76..98915efa6 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx @@ -7,15 +7,28 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import styles from "./rateSummary.module.css" import { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary" +import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" export default function RateSummary({ rateSummary, isUserLoggedIn, + packages, }: RateSummaryProps) { const intl = useIntl() const priceToShow = isUserLoggedIn ? rateSummary.member : rateSummary.public + const isPetRoomSelect = rateSummary.features.some( + (feature) => feature.code === RoomPackageCodeEnum.PETR + ) + + const petRoomPackage = packages.find( + (pkg) => pkg.code === RoomPackageCodeEnum.PETR + ) + + const petRoomPrice = petRoomPackage ? petRoomPackage.calculatedPrice : null + const petRoomCurrency = petRoomPackage ? petRoomPackage.currency : null + return ( <div className={styles.summary}> <div className={styles.summaryText}> @@ -34,6 +47,16 @@ export default function RateSummary({ {priceToShow?.requestedPrice?.currency} </Body> </div> + {isPetRoomSelect && ( + <div className={styles.petInfo}> + <Body color="uiTextHighContrast" textTransform="bold"> + + {petRoomPrice} {petRoomCurrency} + </Body> + <Body color="uiTextMediumContrast"> + {intl.formatMessage({ id: "Pet charge" })} + </Body> + </div> + )} <Button type="submit" theme="base"> {intl.formatMessage({ id: "Continue" })} </Button> diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css index c8352efb1..07e9841b4 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css @@ -15,3 +15,8 @@ display: flex; gap: var(--Spacing-x4); } + +.petInfo { + border-left: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); + padding-left: var(--Spacing-x2); +} diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index 1afec6119..3b161d8c7 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -1,5 +1,6 @@ "use client" +import { createElement } from "react" import { useIntl } from "react-intl" import { RateDefinition } from "@/server/routers/hotels/output" @@ -12,6 +13,7 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import ImageGallery from "../../ImageGallery" import RoomSidePeek from "../RoomSidePeek" +import { getIconForFeatureCode } from "../../utils" import styles from "./roomCard.module.css" @@ -47,16 +49,16 @@ export default function RoomCard({ : undefined } - function getPriceForRate( + function getPriceInformationForRate( rate: typeof saveRate | typeof changeRate | typeof flexRate ) { return rateDefinitions.find((def) => def.rateCode === rate?.rateCode) ?.generalTerms } + const selectedRoom = roomCategories.find( (room) => room.name === roomConfiguration.roomType ) - const roomSize = selectedRoom?.roomSize const occupancy = selectedRoom?.occupancy.total const roomDescription = selectedRoom?.descriptions.short @@ -68,7 +70,6 @@ export default function RoomCard({ <div className={styles.cardBody}> <div className={styles.specification}> <Caption color="uiTextMediumContrast" className={styles.guests}> - {/*TODO: Handle pluralisation*/} {intl.formatMessage( { id: "booking.guests", @@ -105,44 +106,58 @@ export default function RoomCard({ value="non-refundable" paymentTerm={intl.formatMessage({ id: "Pay now" })} product={findProductForRate(saveRate)} - priceInformation={getPriceForRate(saveRate)} + priceInformation={getPriceInformationForRate(saveRate)} handleSelectRate={handleSelectRate} roomType={roomConfiguration.roomType} roomTypeCode={roomConfiguration.roomTypeCode} + features={roomConfiguration.features} /> <FlexibilityOption name={intl.formatMessage({ id: "Free rebooking" })} value="free-rebooking" paymentTerm={intl.formatMessage({ id: "Pay now" })} product={findProductForRate(changeRate)} - priceInformation={getPriceForRate(changeRate)} + priceInformation={getPriceInformationForRate(changeRate)} handleSelectRate={handleSelectRate} roomType={roomConfiguration.roomType} roomTypeCode={roomConfiguration.roomTypeCode} + features={roomConfiguration.features} /> <FlexibilityOption name={intl.formatMessage({ id: "Free cancellation" })} value="free-cancellation" paymentTerm={intl.formatMessage({ id: "Pay later" })} product={findProductForRate(flexRate)} - priceInformation={getPriceForRate(flexRate)} + priceInformation={getPriceInformationForRate(flexRate)} handleSelectRate={handleSelectRate} roomType={roomConfiguration.roomType} roomTypeCode={roomConfiguration.roomTypeCode} + features={roomConfiguration.features} /> </div> </div> </div> {mainImage && ( <div className={styles.imageContainer}> - {roomConfiguration.roomsLeft < 5 && ( - <span className={styles.roomsLeft}> - <Footnote - color="burgundy" - textTransform="uppercase" - >{`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`}</Footnote> - </span> - )} + <div className={styles.chipContainer}> + {roomConfiguration.roomsLeft < 5 && ( + <span className={styles.chip}> + <Footnote + color="burgundy" + textTransform="uppercase" + >{`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`}</Footnote> + </span> + )} + {roomConfiguration.features.map((feature) => ( + <span className={styles.chip} key={feature.code}> + {createElement(getIconForFeatureCode(feature.code), { + width: 16, + height: 16, + color: "burgundy", + })} + </span> + ))} + </div> {/*NOTE: images from the test API are hosted on test3.scandichotels.com, which can't be accessed unless on Scandic's Wifi or using Citrix. */} {images && ( diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css index ef5d9b8fc..537c1b30a 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css @@ -64,10 +64,17 @@ gap: var(--Spacing-x2); } -.roomsLeft { +.chipContainer { position: absolute; + z-index: 1; top: 12px; left: 12px; + display: flex; + flex-direction: row; + gap: var(--Spacing-x1); +} + +.chip { background-color: var(--Main-Grey-White); padding: var(--Spacing-x-half) var(--Spacing-x1); border-radius: var(--Corner-radius-Small); diff --git a/components/HotelReservation/SelectRate/RoomSelection/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/index.tsx index 8592e64ea..351efd5b3 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/index.tsx @@ -15,6 +15,7 @@ export default function RoomSelection({ roomsAvailability, roomCategories, user, + packages, }: RoomSelectionProps) { const [rateSummary, setRateSummary] = useState<Rate | null>(null) @@ -69,6 +70,7 @@ export default function RoomSelection({ <RateSummary rateSummary={rateSummary} isUserLoggedIn={isUserLoggedIn} + packages={packages} /> )} </form> diff --git a/components/HotelReservation/SelectRate/RoomSelection/roomSelection.module.css b/components/HotelReservation/SelectRate/RoomSelection/roomSelection.module.css index 66a27302e..1dab63afb 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/roomSelection.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/roomSelection.module.css @@ -3,7 +3,6 @@ } .roomList { - margin-top: var(--Spacing-x4); list-style: none; display: grid; grid-template-columns: 1fr; diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx index 963f266d4..0cd98156e 100644 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -9,15 +9,15 @@ import RoomSelection from "../RoomSelection" import styles from "./rooms.module.css" -import { RoomProps } from "@/types/components/hotelReservation/selectRate/room" import { RoomPackageCodes } from "@/types/components/hotelReservation/selectRate/roomFilter" +import { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection" export default function Rooms({ roomsAvailability, roomCategories = [], user, packages, -}: RoomProps) { +}: RoomSelectionProps) { const [rooms, setRooms] = useState<RoomsAvailability>(roomsAvailability) function handleFilter(filter: Record<string, boolean | undefined>) { @@ -47,6 +47,7 @@ export default function Rooms({ roomsAvailability={rooms} roomCategories={roomCategories} user={user} + packages={packages} /> </div> ) diff --git a/components/HotelReservation/SelectRate/Rooms/rooms.module.css b/components/HotelReservation/SelectRate/Rooms/rooms.module.css index a8e573530..5e2bca00b 100644 --- a/components/HotelReservation/SelectRate/Rooms/rooms.module.css +++ b/components/HotelReservation/SelectRate/Rooms/rooms.module.css @@ -3,6 +3,6 @@ margin: 0 auto; display: flex; flex-direction: column; - gap: var(--Spacing-x7); + gap: var(--Spacing-x2); padding: var(--Spacing-x2); } diff --git a/components/HotelReservation/SelectRate/utils.ts b/components/HotelReservation/SelectRate/utils.ts new file mode 100644 index 000000000..6002b0705 --- /dev/null +++ b/components/HotelReservation/SelectRate/utils.ts @@ -0,0 +1,19 @@ +import { AllergyIcon,PetsIcon, WheelchairIcon } from "@/components/Icons" + +import { + RoomPackageCodeEnum, + RoomPackageCodes, +} from "@/types/components/hotelReservation/selectRate/roomFilter" + +export function getIconForFeatureCode(featureCode: RoomPackageCodes) { + switch (featureCode) { + case RoomPackageCodeEnum.ACCE: + return WheelchairIcon + case RoomPackageCodeEnum.ALLG: + return AllergyIcon + case RoomPackageCodeEnum.PETR: + return PetsIcon + default: + return PetsIcon + } +} diff --git a/components/Icons/Allergy.tsx b/components/Icons/Allergy.tsx new file mode 100644 index 000000000..0fe399445 --- /dev/null +++ b/components/Icons/Allergy.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function AllergyIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + <svg + className={classNames} + width="20" + height="20" + viewBox="0 0 20 20" + fill="none" + xmlns="http://www.w3.org/2000/svg" + {...props} + > + <mask + id="mask0_7488_25219" + style={{ maskType: "alpha" }} + maskUnits="userSpaceOnUse" + x="0" + y="0" + width="20" + height="20" + > + <rect width="20" height="20" fill="#D9D9D9" /> + </mask> + <g mask="url(#mask0_7488_1017)"> + <path + d="M8.83333 18.2812C8.14543 18.2812 7.62276 18.0729 7.26533 17.6562C6.90789 17.2396 6.72569 16.7708 6.71875 16.25C6.71875 15.9028 6.79688 15.5608 6.95312 15.224C7.10938 14.8872 7.35069 14.6111 7.67708 14.3958C7.98887 14.193 8.24043 13.9322 8.43175 13.6135C8.62308 13.2948 8.75694 12.9549 8.83333 12.5938C8.73611 12.5451 8.6441 12.4983 8.55729 12.4531C8.47049 12.408 8.38542 12.3542 8.30208 12.2917L6.35904 12.988C6.11968 13.0724 5.89075 13.1424 5.67225 13.1979C5.45375 13.2535 5.22842 13.2812 4.99627 13.2812C4.13592 13.2812 3.37458 12.9036 2.71225 12.1482C2.04992 11.3928 1.71875 10.2879 1.71875 8.83333C1.71875 8.14583 1.92361 7.62153 2.33333 7.26042C2.74306 6.89931 3.20673 6.71875 3.72435 6.71875C4.07852 6.71875 4.42587 6.79691 4.76642 6.95323C5.10697 7.10956 5.38622 7.35085 5.60417 7.67708C5.80699 7.98887 6.06776 8.24043 6.3865 8.43175C6.70522 8.62308 7.04514 8.75694 7.40625 8.83333C7.45486 8.73611 7.50174 8.6441 7.54688 8.55729C7.59201 8.47049 7.64583 8.38542 7.70833 8.30208L7.01042 6.35417C6.92708 6.1235 6.85764 5.89963 6.80208 5.68254C6.74653 5.46545 6.71875 5.24835 6.71875 5.03125C6.73264 4.16319 7.11534 3.3941 7.86685 2.72396C8.61837 2.05382 9.71831 1.71875 11.1667 1.71875C11.8542 1.71875 12.3785 1.92521 12.7396 2.33812C13.1007 2.75104 13.2812 3.21472 13.2812 3.72917C13.2812 4.07639 13.2031 4.42188 13.0469 4.76562C12.8906 5.10938 12.6493 5.38889 12.3229 5.60417C12.0111 5.80699 11.7596 6.06776 11.5682 6.3865C11.3769 6.70522 11.2431 7.04514 11.1667 7.40625C11.2639 7.45486 11.3559 7.50174 11.4427 7.54688C11.5295 7.59201 11.6146 7.64583 11.6979 7.70833L13.641 6.99115C13.8803 6.90677 14.1058 6.84028 14.3175 6.79167C14.5292 6.74306 14.7511 6.71875 14.9832 6.71875C16.0894 6.71875 16.9156 7.17875 17.4619 8.09875C18.0081 9.01876 18.2812 10.0414 18.2812 11.1667C18.2812 11.8546 18.0642 12.3772 17.6302 12.7347C17.1962 13.0921 16.7153 13.2743 16.1875 13.2812C15.8542 13.2812 15.5269 13.2031 15.2057 13.0468C14.8845 12.8904 14.6145 12.6492 14.3958 12.3229C14.193 12.0111 13.9322 11.7596 13.6135 11.5682C13.2948 11.3769 12.9549 11.2431 12.5938 11.1667C12.5451 11.2639 12.4983 11.3559 12.4531 11.4427C12.408 11.5295 12.3542 11.6146 12.2917 11.6979L12.9896 13.6458C13.066 13.8611 13.1337 14.0747 13.1927 14.2865C13.2517 14.4983 13.2812 14.7083 13.2812 14.9167C13.2743 15.7917 12.8959 16.5712 12.1461 17.2552C11.3963 17.9392 10.2921 18.2812 8.83333 18.2812ZM9.9994 11.3021C10.3609 11.3021 10.6684 11.1755 10.9219 10.9225C11.1753 10.6694 11.3021 10.3621 11.3021 10.0006C11.3021 9.63909 11.1755 9.3316 10.9225 9.07812C10.6694 8.82465 10.3621 8.69792 10.0006 8.69792C9.63909 8.69792 9.3316 8.82445 9.07812 9.07752C8.82465 9.33058 8.69792 9.63787 8.69792 9.9994C8.69792 10.3609 8.82445 10.6684 9.07752 10.9219C9.33058 11.1753 9.63787 11.3021 9.9994 11.3021ZM9.01042 7.3125C9.10764 7.27778 9.20733 7.25 9.30948 7.22917C9.41165 7.20833 9.50988 7.19097 9.60417 7.17708C9.71528 6.59375 9.92708 6.04861 10.2396 5.54167C10.5521 5.03472 10.9618 4.61806 11.4688 4.29167C11.5521 4.22917 11.6146 4.15327 11.6562 4.06398C11.6979 3.9747 11.7188 3.8631 11.7188 3.72917C11.7188 3.60343 11.6727 3.49735 11.5807 3.41092C11.4887 3.32447 11.3507 3.28125 11.1667 3.28125C10.6287 3.28125 10.0202 3.39938 9.34129 3.63565C8.66237 3.87191 8.30903 4.3337 8.28125 5.02102C8.28125 5.1499 8.29861 5.2716 8.33333 5.38615C8.36806 5.5007 8.39931 5.6081 8.42708 5.70833L9.01042 7.3125ZM5 11.7188C5.19444 11.7188 5.43056 11.6701 5.70833 11.5729L7.3125 10.9896C7.27778 10.8924 7.25 10.7927 7.22917 10.6905C7.20833 10.5884 7.19097 10.4901 7.17708 10.3958C6.59375 10.2847 6.04861 10.0729 5.54167 9.76042C5.03472 9.44792 4.61806 9.03819 4.29167 8.53125C4.22917 8.44792 4.14757 8.38542 4.04688 8.34375C3.94618 8.30208 3.84028 8.28125 3.72917 8.28125C3.58772 8.28125 3.47771 8.32726 3.39913 8.41927C3.32054 8.51128 3.28125 8.64931 3.28125 8.83333C3.28125 9.59713 3.42708 10.269 3.71875 10.8489C4.01042 11.4288 4.4375 11.7188 5 11.7188ZM8.83333 16.7188C9.49321 16.7188 10.1425 16.5851 10.7814 16.3177C11.4202 16.0503 11.7326 15.5868 11.7188 14.9271C11.7188 14.8084 11.7031 14.6971 11.6719 14.5933C11.6406 14.4894 11.6076 14.3889 11.5729 14.2917L10.9896 12.6875C10.8924 12.7222 10.7927 12.75 10.6905 12.7708C10.5884 12.7917 10.4901 12.809 10.3958 12.8229C10.2847 13.4062 10.0729 13.9514 9.76042 14.4583C9.44792 14.9653 9.03819 15.3819 8.53125 15.7083C8.44792 15.7708 8.38368 15.8524 8.33854 15.9531C8.2934 16.0538 8.27431 16.1562 8.28125 16.2604C8.29703 16.3826 8.34438 16.4896 8.42329 16.5813C8.50221 16.6729 8.63889 16.7188 8.83333 16.7188ZM16.1833 11.7188C16.325 11.7188 16.4497 11.6804 16.5573 11.6037C16.6649 11.527 16.7188 11.3814 16.7188 11.1667C16.7188 10.6292 16.6046 10.0174 16.3762 9.33146C16.1478 8.64546 15.6838 8.29539 14.9844 8.28125C14.8559 8.28125 14.7326 8.29514 14.6146 8.32292C14.4965 8.35069 14.3877 8.37894 14.2881 8.40765L12.6875 9.01042C12.7222 9.10764 12.75 9.20733 12.7708 9.30948C12.7917 9.41164 12.809 9.50988 12.8229 9.60417C13.4062 9.71528 13.9514 9.92708 14.4583 10.2396C14.9653 10.5521 15.3819 10.9618 15.7083 11.4688C15.7569 11.5451 15.8247 11.6059 15.9115 11.651C15.9983 11.6962 16.0889 11.7188 16.1833 11.7188Z" + fill="#787472" + /> + </g> + </svg> + ) +} diff --git a/components/Icons/Pets.tsx b/components/Icons/Pets.tsx index 679a3afee..e5e475b2e 100644 --- a/components/Icons/Pets.tsx +++ b/components/Icons/Pets.tsx @@ -16,7 +16,7 @@ export default function PetsIcon({ className, color, ...props }: IconProps) { > <path d="M4.6251 12.05C3.95843 12.05 3.39176 11.8166 2.9251 11.35C2.45843 10.8833 2.2251 10.3166 2.2251 9.64995C2.2251 8.98328 2.45843 8.41662 2.9251 7.94995C3.39176 7.48328 3.95843 7.24995 4.6251 7.24995C5.29176 7.24995 5.85843 7.48328 6.3251 7.94995C6.79176 8.41662 7.0251 8.98328 7.0251 9.64995C7.0251 10.3166 6.79176 10.8833 6.3251 11.35C5.85843 11.8166 5.29176 12.05 4.6251 12.05ZM9.0501 8.12495C8.38343 8.12495 7.81676 7.89162 7.3501 7.42495C6.88343 6.95828 6.6501 6.39162 6.6501 5.72495C6.6501 5.05828 6.88343 4.49162 7.3501 4.02495C7.81676 3.55828 8.38343 3.32495 9.0501 3.32495C9.71676 3.32495 10.2834 3.55828 10.7501 4.02495C11.2168 4.49162 11.4501 5.05828 11.4501 5.72495C11.4501 6.39162 11.2168 6.95828 10.7501 7.42495C10.2834 7.89162 9.71676 8.12495 9.0501 8.12495ZM14.9751 8.12495C14.3084 8.12495 13.7418 7.89162 13.2751 7.42495C12.8084 6.95828 12.5751 6.39162 12.5751 5.72495C12.5751 5.05828 12.8084 4.49162 13.2751 4.02495C13.7418 3.55828 14.3084 3.32495 14.9751 3.32495C15.6418 3.32495 16.2084 3.55828 16.6751 4.02495C17.1418 4.49162 17.3751 5.05828 17.3751 5.72495C17.3751 6.39162 17.1418 6.95828 16.6751 7.42495C16.2084 7.89162 15.6418 8.12495 14.9751 8.12495ZM19.4001 12.05C18.7334 12.05 18.1668 11.8166 17.7001 11.35C17.2334 10.8833 17.0001 10.3166 17.0001 9.64995C17.0001 8.98328 17.2334 8.41662 17.7001 7.94995C18.1668 7.48328 18.7334 7.24995 19.4001 7.24995C20.0668 7.24995 20.6334 7.48328 21.1001 7.94995C21.5668 8.41662 21.8001 8.98328 21.8001 9.64995C21.8001 10.3166 21.5668 10.8833 21.1001 11.35C20.6334 11.8166 20.0668 12.05 19.4001 12.05ZM6.7559 21.925C6.0187 21.925 5.40426 21.6474 4.9126 21.0922C4.42093 20.537 4.1751 19.8813 4.1751 19.125C4.1751 18.2666 4.4626 17.5187 5.0376 16.8812C5.6126 16.2437 6.19176 15.615 6.7751 14.995C7.25843 14.4893 7.6751 13.9387 8.0251 13.3432C8.3751 12.7477 8.78343 12.1833 9.2501 11.65C9.60843 11.2416 10.0188 10.8979 10.4813 10.6187C10.9438 10.3395 11.4506 10.2 12.0015 10.2C12.5524 10.2 13.0628 10.3333 13.5327 10.6C14.0026 10.8666 14.4168 11.2083 14.7751 11.625C15.2418 12.15 15.6522 12.7125 16.0063 13.3125C16.3605 13.9125 16.7755 14.4753 17.2513 15.001C17.8255 15.6253 18.4022 16.2541 18.9813 16.8875C19.5605 17.5208 19.8501 18.2666 19.8501 19.125C19.8501 19.8813 19.6043 20.537 19.1126 21.0922C18.6209 21.6474 18.0073 21.925 17.2718 21.925C16.3892 21.925 15.5147 21.85 14.6484 21.7C13.7822 21.55 12.9077 21.475 12.0251 21.475C11.1334 21.475 10.2535 21.55 9.38522 21.7C8.51697 21.85 7.64053 21.925 6.7559 21.925Z" - fill="#4D001B" + fill="#787472" /> </svg> ) diff --git a/components/Icons/Wheelchair.tsx b/components/Icons/Wheelchair.tsx new file mode 100644 index 000000000..991951761 --- /dev/null +++ b/components/Icons/Wheelchair.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function WheelchairIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + <svg + className={classNames} + width="16" + height="16" + viewBox="0 0 16 16" + fill="none" + xmlns="http://www.w3.org/2000/svg" + {...props} + > + <mask + id="mask0_7488_25219" + style={{ maskType: "alpha" }} + maskUnits="userSpaceOnUse" + x="0" + y="0" + width="16" + height="16" + > + <rect width="16" height="16" fill="#D9D9D9" /> + </mask> + <g mask="url(#mask0_7488_25219)"> + <path + d="M5.41602 14.5C4.51046 14.5 3.7424 14.1847 3.11185 13.5541C2.48129 12.9236 2.16602 12.1555 2.16602 11.25C2.16602 10.3444 2.48129 9.57636 3.11185 8.94581C3.7424 8.31525 4.51324 7.99998 5.42435 7.99998V9.24998C4.86324 9.24998 4.38824 9.44442 3.99935 9.83331C3.61046 10.2222 3.41602 10.6944 3.41602 11.25C3.41602 11.8055 3.61046 12.2778 3.99935 12.6666C4.38824 13.0555 4.86046 13.25 5.41602 13.25C5.97157 13.25 6.44379 13.0541 6.83268 12.6625C7.22157 12.2708 7.41602 11.7916 7.41602 11.225H8.66602C8.66602 12.1416 8.35074 12.9166 7.72018 13.55C7.08963 14.1833 6.32157 14.5 5.41602 14.5ZM7.38268 10.6C6.89379 10.6 6.52018 10.3944 6.26185 9.98331C6.00352 9.5722 5.97157 9.14442 6.16602 8.69998L7.36602 6.03331H5.84935L5.64935 6.54998C5.59379 6.70553 5.49102 6.82081 5.34102 6.89581C5.19102 6.97081 5.03546 6.98053 4.87435 6.92498C4.70213 6.86942 4.57574 6.76109 4.49518 6.59998C4.41463 6.43886 4.40768 6.2722 4.47435 6.09998L4.68268 5.54998C4.77713 5.30553 4.92852 5.11664 5.13685 4.98331C5.34518 4.84998 5.5799 4.78331 5.84102 4.78331H9.21601C9.69379 4.78331 10.0618 4.97914 10.3202 5.37081C10.5785 5.76248 10.6105 6.17775 10.416 6.61664L9.31602 9.03331H11.2827C11.6438 9.03331 11.9507 9.1597 12.2035 9.41248C12.4563 9.66525 12.5827 9.9722 12.5827 10.3333V13.2083C12.5827 13.3805 12.5216 13.5278 12.3993 13.65C12.2771 13.7722 12.1299 13.8333 11.9577 13.8333C11.7855 13.8333 11.6382 13.7722 11.516 13.65C11.3938 13.5278 11.3327 13.3805 11.3327 13.2083V10.6H7.38268ZM10.5493 4.44998C10.1882 4.44998 9.88129 4.32359 9.62852 4.07081C9.37574 3.81803 9.24935 3.51109 9.24935 3.14998C9.24935 2.78886 9.37574 2.48192 9.62852 2.22914C9.88129 1.97636 10.1882 1.84998 10.5493 1.84998C10.9105 1.84998 11.2174 1.97636 11.4702 2.22914C11.723 2.48192 11.8493 2.78886 11.8493 3.14998C11.8493 3.51109 11.723 3.81803 11.4702 4.07081C11.2174 4.32359 10.9105 4.44998 10.5493 4.44998Z" + fill="#787472" + /> + </g> + </svg> + ) +} diff --git a/components/Icons/icon.module.css b/components/Icons/icon.module.css index ec5a15fd4..68ace50b3 100644 --- a/components/Icons/icon.module.css +++ b/components/Icons/icon.module.css @@ -76,3 +76,8 @@ .baseButtonTextOnFillNormal * { fill: var(--Base-Button-Text-On-Fill-Normal); } + +.disabled, +.disabled * { + fill: var(--Base-Text-Disabled); +} diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index fc7407ce8..ce23296fe 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -4,6 +4,7 @@ export { default as AccessibilityIcon } from "./Accessibility" export { default as AccountCircleIcon } from "./AccountCircle" export { default as AirIcon } from "./Air" export { default as AirplaneIcon } from "./Airplane" +export { default as AllergyIcon } from "./Allergy" export { default as ArrowRightIcon } from "./ArrowRight" export { default as BarIcon } from "./Bar" export { default as BathtubIcon } from "./Bathtub" @@ -111,6 +112,7 @@ export { default as TshirtIcon } from "./Tshirt" export { default as TshirtWashIcon } from "./TshirtWash" export { default as TvCastingIcon } from "./TvCasting" export { default as WarningTriangle } from "./WarningTriangle" +export { default as WheelchairIcon } from "./Wheelchair" export { default as WifiIcon } from "./Wifi" export { default as WindowCurtainsAltIcon } from "./WindowCurtainsAlt" export { default as WindowNotAvailableIcon } from "./WindowNotAvailable" diff --git a/components/Icons/variants.ts b/components/Icons/variants.ts index d319c466e..09d69364f 100644 --- a/components/Icons/variants.ts +++ b/components/Icons/variants.ts @@ -20,6 +20,7 @@ const config = { white: styles.white, uiTextHighContrast: styles.uiTextHighContrast, uiTextMediumContrast: styles.uiTextMediumContrast, + disabled: styles.disabled, }, }, defaultVariants: { diff --git a/components/TempDesignSystem/Form/FilterChip/Checkbox.tsx b/components/TempDesignSystem/Form/FilterChip/Checkbox.tsx new file mode 100644 index 000000000..c2697d560 --- /dev/null +++ b/components/TempDesignSystem/Form/FilterChip/Checkbox.tsx @@ -0,0 +1,7 @@ +import Chip from "./_Chip" + +import type { FilterChipCheckboxProps } from "@/types/components/form/filterChip" + +export default function CheckboxChip(props: FilterChipCheckboxProps) { + return <Chip {...props} type="checkbox" /> +} diff --git a/components/TempDesignSystem/Form/FilterChip/_Chip/chip.module.css b/components/TempDesignSystem/Form/FilterChip/_Chip/chip.module.css new file mode 100644 index 000000000..19e1db812 --- /dev/null +++ b/components/TempDesignSystem/Form/FilterChip/_Chip/chip.module.css @@ -0,0 +1,27 @@ +.label { + display: flex; + align-items: center; + gap: var(--Spacing-x-half); + padding: var(--Spacing-x1) var(--Spacing-x-one-and-half); + border: 1px solid var(--Base-Border-Subtle); + border-radius: var(--Corner-radius-Small); + background-color: var(--Base-Surface-Secondary-light-Normal); + cursor: pointer; +} + +.label[data-selected="true"], +.label[data-selected="true"]:hover { + background-color: var(--Primary-Light-Surface-Normal); + border-color: var(--Base-Border-Hover); +} + +.label:hover { + background-color: var(--Base-Surface-Primary-light-Hover-alt); + border-color: var(--Base-Border-Subtle); +} + +.label[data-disabled="true"] { + background-color: var(--Base-Button-Primary-Fill-Disabled); + border-color: var(--Base-Button-Primary-Fill-Disabled); + cursor: not-allowed; +} diff --git a/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx b/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx new file mode 100644 index 000000000..c88f17d29 --- /dev/null +++ b/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx @@ -0,0 +1,57 @@ +import { useMemo } from "react" +import { useFormContext } from "react-hook-form" + +import { HeartIcon } from "@/components/Icons" +import Caption from "@/components/TempDesignSystem/Text/Caption" + +import styles from "./chip.module.css" + +import { FilterChipProps } from "@/types/components/form/filterChip" + +export default function FilterChip({ + Icon = HeartIcon, + iconHeight = 20, + iconWidth = 20, + id, + name, + label, + type, + value, + selected, + disabled, +}: FilterChipProps) { + const { register } = useFormContext() + + const color = useMemo(() => { + if (selected) return "burgundy" + if (disabled) return "disabled" + return "uiTextPlaceholder" + }, [selected, disabled]) + + return ( + <label + className={styles.label} + data-selected={selected} + data-disabled={disabled} + > + <Icon + className={styles.icon} + color={color} + height={iconHeight} + width={iconWidth} + /> + <Caption type="bold" color={color}> + {label} + </Caption> + <input + aria-hidden + id={id || name} + hidden + type={type} + value={value} + disabled={disabled} + {...register(name)} + /> + </label> + ) +} diff --git a/components/TempDesignSystem/Tooltip/tooltip.module.css b/components/TempDesignSystem/Tooltip/tooltip.module.css index 2a6b00b43..da8e50cbd 100644 --- a/components/TempDesignSystem/Tooltip/tooltip.module.css +++ b/components/TempDesignSystem/Tooltip/tooltip.module.css @@ -15,6 +15,7 @@ opacity: 0; transition: opacity 0.3s; max-width: 200px; + min-width: 150px; } .tooltipContainer:hover .tooltip { @@ -31,11 +32,15 @@ } .top { - bottom: 100%; + bottom: calc(100% + 8px); } .bottom { - top: 100%; + top: calc(100% + 8px); +} + +.bottom.arrowRight { + right: 0; } .tooltip::before { diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index af4c667b8..74d3e165b 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -218,6 +218,7 @@ "Pay now": "Betal nu", "Payment info": "Betalingsoplysninger", "Pet Room": "Kæledyrsrum", + "Pet-friendly rooms have an additional fee of 20 EUR per stay": "Kæledyrsrum har en ekstra gebyr på 20 EUR per ophold", "Phone": "Telefon", "Phone is required": "Telefonnummer er påkrævet", "Phone number": "Telefonnummer", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index c9483e349..adb01b099 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -218,6 +218,7 @@ "Pay now": "Jetzt bezahlen", "Payment info": "Zahlungsinformationen", "Pet Room": "Haustierzimmer", + "Pet-friendly rooms have an additional fee of 20 EUR per stay": "Haustierzimmer haben einen zusätzlichen Preis von 20 EUR pro Aufenthalt", "Phone": "Telefon", "Phone is required": "Telefon ist erforderlich", "Phone number": "Telefonnummer", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 0511cf5a9..2e92e936a 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -228,6 +228,7 @@ "Payment info": "Payment info", "Payment received": "Payment received", "Pet Room": "Pet room", + "Pet-friendly rooms have an additional fee of 20 EUR per stay": "Pet-friendly rooms have an additional fee of 20 EUR per stay", "Phone": "Phone", "Phone is required": "Phone is required", "Phone number": "Phone number", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index c8427a306..5f464d30f 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -218,6 +218,7 @@ "Pay now": "Maksa nyt", "Payment info": "Maksutiedot", "Pet Room": "Lemmikkihuone", + "Pet-friendly rooms have an additional fee of 20 EUR per stay": "Lemmikkihuoneen lisäkustannus on 20 EUR per majoitus", "Phone": "Puhelin", "Phone is required": "Puhelin vaaditaan", "Phone number": "Puhelinnumero", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 69a1fbec4..8ef615923 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -216,6 +216,7 @@ "Pay now": "Betal nå", "Payment info": "Betalingsinformasjon", "Pet Room": "Kjæledyrsrom", + "Pet-friendly rooms have an additional fee of 20 EUR per stay": "Kjæledyrsrom har en tilleggsavgift på 20 EUR per opphold", "Phone": "Telefon", "Phone is required": "Telefon kreves", "Phone number": "Telefonnummer", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index c2f90d520..df8d576c8 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -216,6 +216,7 @@ "Pay now": "Betala nu", "Payment info": "Betalningsinformation", "Pet Room": "Husdjursrum", + "Pet-friendly rooms have an additional fee of 20 EUR per stay": "Husdjursrum har en extra avgift på 20 EUR per vistelse", "Phone": "Telefon", "Phone is required": "Telefonnummer är obligatorisk", "Phone number": "Telefonnummer", diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 0f545d673..91032ffdc 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -7,6 +7,7 @@ import { imageMetaDataSchema, imageSizesSchema } from "./schemas/image" import { roomSchema } from "./schemas/room" import { getPoiGroupByCategoryName } from "./utils" +import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { AlertTypeEnum } from "@/types/enums/alert" import { FacilityEnum } from "@/types/enums/facilities" import { PointOfInterestCategoryNameEnum } from "@/types/hotel" @@ -545,7 +546,16 @@ const roomConfigurationSchema = z.object({ roomTypeCode: z.string().optional(), roomType: z.string(), roomsLeft: z.number(), - features: z.array(z.object({ inventory: z.number(), code: z.string() })), + features: z.array( + z.object({ + inventory: z.number(), + code: z.enum([ + RoomPackageCodeEnum.PETR, + RoomPackageCodeEnum.ALLG, + RoomPackageCodeEnum.ACCE, + ]), + }) + ), products: z.array(productSchema), }) diff --git a/server/routers/hotels/schemas/packages.ts b/server/routers/hotels/schemas/packages.ts index d7ff6a4e1..a239c6b50 100644 --- a/server/routers/hotels/schemas/packages.ts +++ b/server/routers/hotels/schemas/packages.ts @@ -1,10 +1,6 @@ import { z } from "zod" -export enum RoomPackageCode { - PETR = "PETR", - ALLG = "ALLG", - ACCE = "ACCE", -} +import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" export const getRoomPackagesInputSchema = z.object({ hotelId: z.string(), @@ -18,9 +14,9 @@ export const getRoomPackagesInputSchema = z.object({ const packagesSchema = z.array( z.object({ code: z.enum([ - RoomPackageCode.PETR, - RoomPackageCode.ALLG, - RoomPackageCode.ACCE, + RoomPackageCodeEnum.PETR, + RoomPackageCodeEnum.ALLG, + RoomPackageCodeEnum.ACCE, ]), itemCode: z.string(), description: z.string(), diff --git a/types/components/form/filterChip.ts b/types/components/form/filterChip.ts new file mode 100644 index 000000000..3ff40673d --- /dev/null +++ b/types/components/form/filterChip.ts @@ -0,0 +1,16 @@ +type FilterChipType = "checkbox" | "radio" + +export interface FilterChipProps { + Icon?: React.ElementType + iconHeight?: number + iconWidth?: number + id?: string + label: string + name: string + type: FilterChipType + value?: string + selected?: boolean + disabled?: boolean +} + +export type FilterChipCheckboxProps = Omit<FilterChipProps, "type"> diff --git a/types/components/hotelReservation/selectRate/flexibilityOption.ts b/types/components/hotelReservation/selectRate/flexibilityOption.ts index 1835c3b65..1a432dc32 100644 --- a/types/components/hotelReservation/selectRate/flexibilityOption.ts +++ b/types/components/hotelReservation/selectRate/flexibilityOption.ts @@ -18,6 +18,7 @@ export type FlexibilityOptionProps = { priceInformation?: Array<string> roomType: RoomConfiguration["roomType"] roomTypeCode: RoomConfiguration["roomTypeCode"] + features: RoomConfiguration["features"] handleSelectRate: (rate: Rate) => void } diff --git a/types/components/hotelReservation/selectRate/rateSummary.ts b/types/components/hotelReservation/selectRate/rateSummary.ts index 672df21dd..c9685f180 100644 --- a/types/components/hotelReservation/selectRate/rateSummary.ts +++ b/types/components/hotelReservation/selectRate/rateSummary.ts @@ -1,6 +1,8 @@ +import { RoomPackageData } from "./roomFilter" import { Rate } from "./selectRate" export interface RateSummaryProps { rateSummary: Rate isUserLoggedIn: boolean + packages: RoomPackageData } diff --git a/types/components/hotelReservation/selectRate/room.ts b/types/components/hotelReservation/selectRate/room.ts deleted file mode 100644 index b84e5c667..000000000 --- a/types/components/hotelReservation/selectRate/room.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { RoomPackageData } from "./roomFilter" -import { RoomSelectionProps } from "./roomSelection" - -export interface RoomProps extends RoomSelectionProps { - packages: RoomPackageData -} diff --git a/types/components/hotelReservation/selectRate/roomCard.ts b/types/components/hotelReservation/selectRate/roomCard.ts index a6ed91ac1..99552916d 100644 --- a/types/components/hotelReservation/selectRate/roomCard.ts +++ b/types/components/hotelReservation/selectRate/roomCard.ts @@ -3,6 +3,7 @@ import { RoomConfiguration, } from "@/server/routers/hotels/output" +import { RoomPackageCodes } from "./roomFilter" import { Rate } from "./selectRate" import { RoomData } from "@/types/hotel" diff --git a/types/components/hotelReservation/selectRate/roomFilter.ts b/types/components/hotelReservation/selectRate/roomFilter.ts index c70f8f1d0..aaf807508 100644 --- a/types/components/hotelReservation/selectRate/roomFilter.ts +++ b/types/components/hotelReservation/selectRate/roomFilter.ts @@ -2,6 +2,11 @@ import { z } from "zod" import { getRoomPackagesSchema } from "@/server/routers/hotels/schemas/packages" +export enum RoomPackageCodeEnum { + PETR = "PETR", + ALLG = "ALLG", + ACCE = "ACCE", +} export interface RoomFilterProps { numberOfRooms: number onFilter: (filter: Record<string, boolean | undefined>) => void diff --git a/types/components/hotelReservation/selectRate/roomSelection.ts b/types/components/hotelReservation/selectRate/roomSelection.ts index fd633fa50..03e84245e 100644 --- a/types/components/hotelReservation/selectRate/roomSelection.ts +++ b/types/components/hotelReservation/selectRate/roomSelection.ts @@ -1,5 +1,7 @@ import { RoomsAvailability } from "@/server/routers/hotels/output" +import { RoomPackageData } from "./roomFilter" + import { RoomData } from "@/types/hotel" import { SafeUser } from "@/types/user" @@ -7,4 +9,5 @@ export interface RoomSelectionProps { roomsAvailability: RoomsAvailability roomCategories: RoomData[] user: SafeUser + packages: RoomPackageData } diff --git a/types/components/hotelReservation/selectRate/selectRate.ts b/types/components/hotelReservation/selectRate/selectRate.ts index da8792133..b22c010c0 100644 --- a/types/components/hotelReservation/selectRate/selectRate.ts +++ b/types/components/hotelReservation/selectRate/selectRate.ts @@ -26,4 +26,5 @@ export interface Rate { priceName: string public: Product["productType"]["public"] member: Product["productType"]["member"] + features: RoomConfiguration["features"] } From 20efae18f664a348571256c774b3f268230f8314 Mon Sep 17 00:00:00 2001 From: Christian Andolf <christian@weahead.se> Date: Mon, 28 Oct 2024 17:18:40 +0100 Subject: [PATCH 16/73] fix: remove props that could be set as invalid attributes on svg --- components/DatePicker/Screen/Desktop.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/components/DatePicker/Screen/Desktop.tsx b/components/DatePicker/Screen/Desktop.tsx index d5040a4ae..d3b30dbf8 100644 --- a/components/DatePicker/Screen/Desktop.tsx +++ b/components/DatePicker/Screen/Desktop.tsx @@ -69,7 +69,13 @@ export default function DatePickerDesktop({ weekStartsOn={1} components={{ Chevron(props) { - return <ChevronLeftIcon {...props} height={20} width={20} /> + return ( + <ChevronLeftIcon + className={props.className} + height={20} + width={20} + /> + ) }, Footer(props) { return ( From 917f44f3238082e07893222c00ec9ff17615515c Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 28 Oct 2024 13:11:24 +0100 Subject: [PATCH 17/73] feat(sw-453): fixed mobile view and some improvements --- .../(standard)/select-rate/page.tsx | 2 + .../MobileToggleButton/index.tsx | 9 +- .../SelectRate/RoomFilter/index.tsx | 45 +++++++-- .../RoomFilter/roomFilter.module.css | 24 +++++ .../FlexibilityOption/PriceList/index.tsx | 6 ++ .../PriceList/priceList.module.css | 5 + .../RoomSelection/RateSummary/index.tsx | 65 +++++++++++-- .../RateSummary/rateSummary.module.css | 42 +++++++- .../RoomSelection/RoomCard/index.tsx | 95 ++++++++----------- .../SelectRate/RoomSelection/index.tsx | 26 +++-- .../SelectRate/Rooms/index.tsx | 30 +++--- .../Form/FilterChip/_Chip/chip.module.css | 10 ++ .../Form/FilterChip/_Chip/index.tsx | 2 +- i18n/dictionaries/da.json | 5 + i18n/dictionaries/de.json | 5 + i18n/dictionaries/en.json | 5 + i18n/dictionaries/fi.json | 5 + i18n/dictionaries/no.json | 5 + i18n/dictionaries/sv.json | 13 ++- .../selectRate/rateSummary.ts | 3 + 20 files changed, 299 insertions(+), 103 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index 603bbbf4a..48a0b30b1 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -58,6 +58,8 @@ export default async function SelectRatePage({ return "No hotel data found" // TODO: Add a proper error message } + console.log(selectRoomParamsObject) + const roomCategories = hotelData?.included return ( diff --git a/components/BookingWidget/MobileToggleButton/index.tsx b/components/BookingWidget/MobileToggleButton/index.tsx index 3bc438c41..a58bdd1b2 100644 --- a/components/BookingWidget/MobileToggleButton/index.tsx +++ b/components/BookingWidget/MobileToggleButton/index.tsx @@ -68,7 +68,14 @@ export default function MobileToggleButton({ {`${selectedFromDate} - ${selectedToDate} (${intl.formatMessage( { id: "booking.nights" }, { totalNights: nights } - )}) ${intl.formatMessage({ id: "booking.adults" }, { totalAdults })}, ${intl.formatMessage({ id: "booking.children" }, { totalChildren })}, ${intl.formatMessage({ id: "booking.rooms" }, { totalRooms })}`} + )}) ${intl.formatMessage({ id: "booking.adults" }, { totalAdults })}, ${ + totalChildren > 0 + ? intl.formatMessage( + { id: "booking.children" }, + { totalChildren } + ) + ", " + : "" + }${intl.formatMessage({ id: "booking.rooms" }, { totalRooms })}`} </Caption> </div> <div className={styles.icon}> diff --git a/components/HotelReservation/SelectRate/RoomFilter/index.tsx b/components/HotelReservation/SelectRate/RoomFilter/index.tsx index 836dd5b49..19fd4d8b7 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/index.tsx +++ b/components/HotelReservation/SelectRate/RoomFilter/index.tsx @@ -9,6 +9,7 @@ import { z } from "zod" import { InfoCircleIcon } from "@/components/Icons" import CheckboxChip from "@/components/TempDesignSystem/Form/FilterChip/Checkbox" import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" import { Tooltip } from "@/components/TempDesignSystem/Tooltip" import { getIconForFeatureCode } from "../utils" @@ -49,6 +50,12 @@ export default function RoomFilter({ const petFriendly = watch(RoomPackageCodeEnum.PETR) const allergyFriendly = watch(RoomPackageCodeEnum.ALLG) + const selectedFilters = useMemo(() => getValues(), [getValues]) + + const tooltipText = intl.formatMessage({ + id: "Pet-friendly rooms have an additional fee of 20 EUR per stay", + }) + const submitFilter = useCallback(() => { const data = getValues() onFilter(data) @@ -61,9 +68,33 @@ export default function RoomFilter({ return ( <div className={styles.container}> - <Body color="uiTextHighContrast"> - {intl.formatMessage({ id: "Room types available" }, { numberOfRooms })} - </Body> + <div className={styles.infoDesktop}> + <Body color="uiTextHighContrast"> + {intl.formatMessage( + { id: "Room types available" }, + { numberOfRooms } + )} + </Body> + </div> + <div className={styles.infoMobile}> + <div className={styles.filterInfo}> + <Caption type="label" color="burgundy" textTransform="uppercase"> + {intl.formatMessage({ id: "Filter" })} + </Caption> + <Caption type="label" color="burgundy"> + {Object.entries(selectedFilters) + .filter(([_, value]) => value) + .map(([key]) => intl.formatMessage({ id: key })) + .join(", ")} + </Caption> + </div> + <Caption color="uiTextHighContrast"> + {intl.formatMessage( + { id: "Room types available" }, + { numberOfRooms } + )} + </Caption> + </div> <FormProvider {...methods}> <form onSubmit={handleSubmit(submitFilter)}> <div className={styles.roomsFilter}> @@ -80,13 +111,7 @@ export default function RoomFilter({ Icon={getIconForFeatureCode(option.code)} /> ))} - <Tooltip - text={intl.formatMessage({ - id: "Pet-friendly rooms have an additional fee of 20 EUR per stay", - })} - position="bottom" - arrow="right" - > + <Tooltip text={tooltipText} position="bottom" arrow="right"> <InfoCircleIcon className={styles.infoIcon} /> </Tooltip> </div> diff --git a/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css b/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css index c0eff095a..9cce04e43 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css +++ b/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css @@ -17,3 +17,27 @@ stroke: var(--UI-Text-Medium-contrast); fill: transparent; } +.filterInfo { + display: flex; + flex-direction: row; + gap: var(--Spacing-x-half); + align-items: flex-end; +} + +.infoDesktop { + display: none; +} + +.infoMobile { + display: block; +} + +@media (min-width: 768px) { + .infoDesktop { + display: block; + } + + .infoMobile { + display: none; + } +} diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx index 76f366adb..dc7ca20fc 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx @@ -40,6 +40,9 @@ export default function PriceList({ </Subtitle> <Body color="uiTextHighContrast" textTransform="bold"> {publicLocalPrice.currency} + <span className={styles.perNight}> + /{intl.formatMessage({ id: "night" })} + </span> </Body> </div> ) : ( @@ -64,6 +67,9 @@ export default function PriceList({ </Subtitle> <Body color="red" textTransform="bold"> {memberLocalPrice.currency} + <span className={styles.perNight}> + /{intl.formatMessage({ id: "night" })} + </span> </Body> </div> ) : ( diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/priceList.module.css b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/priceList.module.css index 7320cf1be..4f3431525 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/priceList.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/priceList.module.css @@ -12,3 +12,8 @@ display: flex; gap: var(--Spacing-x-half); } + +.perNight { + font-weight: 400; + font-size: var(--typography-Caption-Regular-fontSize); +} diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx index 98915efa6..3b9f6dbf6 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx @@ -2,6 +2,8 @@ import { useIntl } from "react-intl" import Button from "@/components/TempDesignSystem/Button" import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import styles from "./rateSummary.module.css" @@ -13,12 +15,19 @@ export default function RateSummary({ rateSummary, isUserLoggedIn, packages, + roomsAvailability, }: RateSummaryProps) { const intl = useIntl() + const { + member, + public: publicRate, + features, + roomType, + priceName, + } = rateSummary + const priceToShow = isUserLoggedIn ? member : publicRate - const priceToShow = isUserLoggedIn ? rateSummary.member : rateSummary.public - - const isPetRoomSelect = rateSummary.features.some( + const isPetRoomSelect = features.some( (feature) => feature.code === RoomPackageCodeEnum.PETR ) @@ -26,17 +35,23 @@ export default function RateSummary({ (pkg) => pkg.code === RoomPackageCodeEnum.PETR ) - const petRoomPrice = petRoomPackage ? petRoomPackage.calculatedPrice : null - const petRoomCurrency = petRoomPackage ? petRoomPackage.currency : null + const petRoomPrice = petRoomPackage?.calculatedPrice ?? null + const petRoomCurrency = petRoomPackage?.currency ?? null + + const checkInDate = new Date(roomsAvailability.checkInDate) + const checkOutDate = new Date(roomsAvailability.checkOutDate) + const nights = Math.ceil( + (checkOutDate.getTime() - checkInDate.getTime()) / (1000 * 60 * 60 * 24) + ) return ( <div className={styles.summary}> <div className={styles.summaryText}> - <Subtitle color="uiTextHighContrast">{rateSummary.roomType}</Subtitle> - <Body color="uiTextMediumContrast">{rateSummary.priceName}</Body> + <Subtitle color="uiTextHighContrast">{roomType}</Subtitle> + <Body color="uiTextMediumContrast">{priceName}</Body> </div> <div className={styles.summaryPrice}> - <div className={styles.summaryPriceText}> + <div className={styles.summaryPriceTextDesktop}> <Subtitle color={isUserLoggedIn ? "red" : "uiTextHighContrast"}> {priceToShow?.localPrice.pricePerStay}{" "} {priceToShow?.localPrice.currency} @@ -47,6 +62,38 @@ export default function RateSummary({ {priceToShow?.requestedPrice?.currency} </Body> </div> + <div className={styles.summaryPriceTextMobile}> + <Caption color="uiTextHighContrast"> + {intl.formatMessage({ id: "Total price" })} + </Caption> + <Subtitle color={isUserLoggedIn ? "red" : "uiTextHighContrast"}> + {priceToShow?.localPrice.pricePerStay}{" "} + {priceToShow?.localPrice.currency} + </Subtitle> + <Footnote + color="uiTextMediumContrast" + className={styles.summaryPriceTextMobile} + > + {intl.formatMessage( + { id: "booking.nights" }, + { totalNights: nights } + )} + ,{" "} + {intl.formatMessage( + { id: "booking.adults" }, + { totalAdults: roomsAvailability.occupancy?.adults } + )} + {roomsAvailability.occupancy?.children && ( + <> + ,{" "} + {intl.formatMessage( + { id: "booking.children" }, + { totalChildren: roomsAvailability.occupancy.children } + )} + </> + )} + </Footnote> + </div> {isPetRoomSelect && ( <div className={styles.petInfo}> <Body color="uiTextHighContrast" textTransform="bold"> @@ -57,7 +104,7 @@ export default function RateSummary({ </Body> </div> )} - <Button type="submit" theme="base"> + <Button type="submit" theme="base" className={styles.continueButton}> {intl.formatMessage({ id: "Continue" })} </Button> </div> diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css index 07e9841b4..5cb5a4229 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css @@ -5,7 +5,7 @@ left: 0; right: 0; background-color: var(--Base-Surface-Primary-light-Normal); - padding: var(--Spacing-x3) var(--Spacing-x7) var(--Spacing-x5); + padding: var(--Spacing-x2) var(--Spacing-x3) var(--Spacing-x5); display: flex; justify-content: space-between; align-items: center; @@ -13,10 +13,50 @@ .summaryPrice { display: flex; + width: 100%; gap: var(--Spacing-x4); } .petInfo { border-left: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); padding-left: var(--Spacing-x2); + display: none; +} + +.summaryText { + display: none; +} + +.summaryPriceTextDesktop { + display: none; +} + +.continueButton { + margin-left: auto; + height: fit-content; + width: 100%; +} + +.summaryPriceTextMobile { + white-space: nowrap; +} + +@media (min-width: 768px) { + .summary { + padding: var(--Spacing-x3) var(--Spacing-x7) var(--Spacing-x5); + } + .petInfo, + .summaryText, + .summaryPriceTextDesktop { + display: block; + } + .summaryPriceTextMobile { + display: none; + } + .summaryPrice { + width: auto; + } + .continueButton { + width: auto; + } } diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index 3b161d8c7..d1ab92174 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -12,8 +12,8 @@ import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import ImageGallery from "../../ImageGallery" -import RoomSidePeek from "../RoomSidePeek" import { getIconForFeatureCode } from "../../utils" +import RoomSidePeek from "../RoomSidePeek" import styles from "./roomCard.module.css" @@ -26,17 +26,19 @@ export default function RoomCard({ handleSelectRate, }: RoomCardProps) { const intl = useIntl() - const saveRate = rateDefinitions.find( - // TODO: Update string when API has decided - (rate) => rate.cancellationRule === "NonCancellable" - ) - const changeRate = rateDefinitions.find( - // TODO: Update string when API has decided - (rate) => rate.cancellationRule === "Modifiable" - ) - const flexRate = rateDefinitions.find( - // TODO: Update string when API has decided - (rate) => rate.cancellationRule === "CancellableBefore6PM" + + // TODO: Update string when API has decided + const rateTypes = { + saveRate: "NonCancellable", + changeRate: "Modifiable", + flexRate: "CancellableBefore6PM", + } + + const rates = Object.fromEntries( + Object.entries(rateTypes).map(([key, rule]) => [ + key, + rateDefinitions.find((rate) => rate.cancellationRule === rule), + ]) ) function findProductForRate(rate: RateDefinition | undefined) { @@ -49,9 +51,7 @@ export default function RoomCard({ : undefined } - function getPriceInformationForRate( - rate: typeof saveRate | typeof changeRate | typeof flexRate - ) { + function getPriceInformationForRate(rate: RateDefinition | undefined) { return rateDefinitions.find((def) => def.rateCode === rate?.rateCode) ?.generalTerms } @@ -59,10 +59,7 @@ export default function RoomCard({ const selectedRoom = roomCategories.find( (room) => room.name === roomConfiguration.roomType ) - const roomSize = selectedRoom?.roomSize - const occupancy = selectedRoom?.occupancy.total - const roomDescription = selectedRoom?.descriptions.short - const images = selectedRoom?.images + const { roomSize, occupancy, descriptions, images } = selectedRoom || {} const mainImage = images?.[0] return ( @@ -74,7 +71,7 @@ export default function RoomCard({ { id: "booking.guests", }, - { nrOfGuests: occupancy } + { nrOfGuests: occupancy?.total } )} </Caption> <Caption color="uiTextMediumContrast"> @@ -93,7 +90,7 @@ export default function RoomCard({ <Subtitle className={styles.name} type="two"> {roomConfiguration.roomType} </Subtitle> - <Body>{roomDescription}</Body> + <Body>{descriptions?.short}</Body> </div> <Caption color="uiTextHighContrast"> {intl.formatMessage({ @@ -101,39 +98,29 @@ export default function RoomCard({ })} </Caption> <div className={styles.flexibilityOptions}> - <FlexibilityOption - name={intl.formatMessage({ id: "Non-refundable" })} - value="non-refundable" - paymentTerm={intl.formatMessage({ id: "Pay now" })} - product={findProductForRate(saveRate)} - priceInformation={getPriceInformationForRate(saveRate)} - handleSelectRate={handleSelectRate} - roomType={roomConfiguration.roomType} - roomTypeCode={roomConfiguration.roomTypeCode} - features={roomConfiguration.features} - /> - <FlexibilityOption - name={intl.formatMessage({ id: "Free rebooking" })} - value="free-rebooking" - paymentTerm={intl.formatMessage({ id: "Pay now" })} - product={findProductForRate(changeRate)} - priceInformation={getPriceInformationForRate(changeRate)} - handleSelectRate={handleSelectRate} - roomType={roomConfiguration.roomType} - roomTypeCode={roomConfiguration.roomTypeCode} - features={roomConfiguration.features} - /> - <FlexibilityOption - name={intl.formatMessage({ id: "Free cancellation" })} - value="free-cancellation" - paymentTerm={intl.formatMessage({ id: "Pay later" })} - product={findProductForRate(flexRate)} - priceInformation={getPriceInformationForRate(flexRate)} - handleSelectRate={handleSelectRate} - roomType={roomConfiguration.roomType} - roomTypeCode={roomConfiguration.roomTypeCode} - features={roomConfiguration.features} - /> + {Object.entries(rates).map(([key, rate]) => ( + <FlexibilityOption + key={key} + name={intl.formatMessage({ + id: + key === "flexRate" + ? "Free cancellation" + : key === "saveRate" + ? "Non-refundable" + : "Free rebooking", + })} + value={key.toLowerCase()} + paymentTerm={intl.formatMessage({ + id: key === "flexRate" ? "Pay later" : "Pay now", + })} + product={findProductForRate(rate)} + priceInformation={getPriceInformationForRate(rate)} + handleSelectRate={handleSelectRate} + roomType={roomConfiguration.roomType} + roomTypeCode={roomConfiguration.roomTypeCode} + features={roomConfiguration.features} + /> + ))} </div> </div> </div> diff --git a/components/HotelReservation/SelectRate/RoomSelection/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/index.tsx index 351efd5b3..3c0305190 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/index.tsx @@ -1,6 +1,6 @@ "use client" import { useRouter, useSearchParams } from "next/navigation" -import { useState } from "react" +import { useMemo,useState } from "react" import RateSummary from "./RateSummary" import RoomCard from "./RoomCard" @@ -23,27 +23,32 @@ export default function RoomSelection({ const searchParams = useSearchParams() const isUserLoggedIn = !!user - function handleSubmit(e: React.FormEvent<HTMLFormElement>) { - e.preventDefault() - const searchParamsObject = getHotelReservationQueryParams(searchParams) + const { roomConfigurations, rateDefinitions } = roomsAvailability - const queryParams = new URLSearchParams(searchParams) + const queryParams = useMemo(() => { + const params = new URLSearchParams(searchParams) + const searchParamsObject = getHotelReservationQueryParams(searchParams) searchParamsObject.room.forEach((item, index) => { if (rateSummary?.roomTypeCode) { - queryParams.set(`room[${index}].roomtype`, rateSummary.roomTypeCode) + params.set(`room[${index}].roomtype`, rateSummary.roomTypeCode) } if (rateSummary?.public?.rateCode) { - queryParams.set(`room[${index}].ratecode`, rateSummary.public.rateCode) + params.set(`room[${index}].ratecode`, rateSummary.public.rateCode) } if (rateSummary?.member?.rateCode) { - queryParams.set( + params.set( `room[${index}].counterratecode`, rateSummary.member.rateCode ) } }) + return params + }, [searchParams, rateSummary]) + + function handleSubmit(e: React.FormEvent<HTMLFormElement>) { + e.preventDefault() router.push(`select-bed?${queryParams}`) } @@ -55,10 +60,10 @@ export default function RoomSelection({ onSubmit={handleSubmit} > <ul className={styles.roomList}> - {roomsAvailability.roomConfigurations.map((roomConfiguration) => ( + {roomConfigurations.map((roomConfiguration) => ( <li key={roomConfiguration.roomType}> <RoomCard - rateDefinitions={roomsAvailability.rateDefinitions} + rateDefinitions={rateDefinitions} roomConfiguration={roomConfiguration} roomCategories={roomCategories} handleSelectRate={setRateSummary} @@ -71,6 +76,7 @@ export default function RoomSelection({ rateSummary={rateSummary} isUserLoggedIn={isUserLoggedIn} packages={packages} + roomsAvailability={roomsAvailability} /> )} </form> diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx index 0cd98156e..74fe0e811 100644 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -1,6 +1,6 @@ "use client" -import { useState } from "react" +import { useCallback,useState } from "react" import { RoomsAvailability } from "@/server/routers/hotels/output" @@ -20,21 +20,25 @@ export default function Rooms({ }: RoomSelectionProps) { const [rooms, setRooms] = useState<RoomsAvailability>(roomsAvailability) - function handleFilter(filter: Record<string, boolean | undefined>) { - const selectedCodes = Object.keys(filter).filter((key) => filter[key]) + const handleFilter = useCallback( + (filter: Record<string, boolean | undefined>) => { + const selectedCodes = Object.keys(filter).filter((key) => filter[key]) - if (selectedCodes.length === 0) { - setRooms(roomsAvailability) - return - } + if (selectedCodes.length === 0) { + setRooms(roomsAvailability) + return + } - const filteredRooms = roomsAvailability.roomConfigurations.filter((room) => - room.features.some((feature) => - selectedCodes.includes(feature.code as RoomPackageCodes) + const filteredRooms = roomsAvailability.roomConfigurations.filter( + (room) => + room.features.some((feature) => + selectedCodes.includes(feature.code as RoomPackageCodes) + ) ) - ) - setRooms({ ...roomsAvailability, roomConfigurations: filteredRooms }) - } + setRooms({ ...roomsAvailability, roomConfigurations: filteredRooms }) + }, + [roomsAvailability] + ) return ( <div className={styles.content}> diff --git a/components/TempDesignSystem/Form/FilterChip/_Chip/chip.module.css b/components/TempDesignSystem/Form/FilterChip/_Chip/chip.module.css index 19e1db812..44fa78a14 100644 --- a/components/TempDesignSystem/Form/FilterChip/_Chip/chip.module.css +++ b/components/TempDesignSystem/Form/FilterChip/_Chip/chip.module.css @@ -25,3 +25,13 @@ border-color: var(--Base-Button-Primary-Fill-Disabled); cursor: not-allowed; } + +.caption { + display: none; +} + +@media (min-width: 768px) { + .caption { + display: block; + } +} diff --git a/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx b/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx index c88f17d29..528469df1 100644 --- a/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx +++ b/components/TempDesignSystem/Form/FilterChip/_Chip/index.tsx @@ -40,7 +40,7 @@ export default function FilterChip({ height={iconHeight} width={iconWidth} /> - <Caption type="bold" color={color}> + <Caption type="bold" color={color} className={styles.caption}> {label} </Caption> <input diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 74d3e165b..625646fb8 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -3,6 +3,8 @@ "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/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", + "ALLG": "Allergi", "About meetings & conferences": "About meetings & conferences", "About the hotel": "About the hotel", "Accessible Room": "Tilgængelighedsrum", @@ -107,6 +109,7 @@ "FAQ": "Ofte stillede spørgsmål", "Failed to delete credit card, please try again later.": "Kunne ikke slette kreditkort. Prøv venligst igen senere.", "Fair": "Messe", + "Filter": "Filter", "Find booking": "Find booking", "Find hotels": "Find hotel", "First name": "Fornavn", @@ -211,6 +214,7 @@ "Open menu": "Åbn menuen", "Open my pages menu": "Åbn mine sider menuen", "Overview": "Oversigt", + "PETR": "Kæledyr", "Parking": "Parkering", "Parking / Garage": "Parkering / Garage", "Password": "Adgangskode", @@ -300,6 +304,7 @@ "Things nearby HOTEL_NAME": "Ting i nærheden af {hotelName}", "Total Points": "Samlet antal point", "Total incl VAT": "Inkl. moms", + "Total price": "Samlet pris", "Tourist": "Turist", "Transaction date": "Overførselsdato", "Transactions": "Transaktioner", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index adb01b099..0029444d2 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -3,6 +3,8 @@ "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/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", + "ALLG": "Allergie", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Über das Hotel", "Accessible Room": "Barrierefreies Zimmer", @@ -107,6 +109,7 @@ "FAQ": "Häufig gestellte Fragen", "Failed to delete credit card, please try again later.": "Kreditkarte konnte nicht gelöscht werden. Bitte versuchen Sie es später noch einmal.", "Fair": "Messe", + "Filter": "Filter", "Find booking": "Buchung finden", "Find hotels": "Hotels finden", "First name": "Vorname", @@ -211,6 +214,7 @@ "Open menu": "Menü öffnen", "Open my pages menu": "Meine Seiten Menü öffnen", "Overview": "Übersicht", + "PETR": "Haustier", "Parking": "Parken", "Parking / Garage": "Parken / Garage", "Password": "Passwort", @@ -301,6 +305,7 @@ "Things nearby HOTEL_NAME": "Dinge in der Nähe von {hotelName}", "Total Points": "Gesamtpunktzahl", "Total incl VAT": "Gesamt inkl. MwSt.", + "Total price": "Gesamtpreis", "Tourist": "Tourist", "Transaction date": "Transaktionsdatum", "Transactions": "Transaktionen", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 2e92e936a..8aebd5004 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -3,6 +3,8 @@ "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/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", + "ALLG": "Allergy", "About meetings & conferences": "About meetings & conferences", "About the hotel": "About the hotel", "Accessible Room": "Accessibility room", @@ -114,6 +116,7 @@ "FAQ": "FAQ", "Failed to delete credit card, please try again later.": "Failed to delete credit card, please try again later.", "Fair": "Fair", + "Filter": "Filter", "Find booking": "Find booking", "Find hotels": "Find hotels", "First name": "First name", @@ -220,6 +223,7 @@ "Open menu": "Open menu", "Open my pages menu": "Open my pages menu", "Overview": "Overview", + "PETR": "Pet", "Parking": "Parking", "Parking / Garage": "Parking / Garage", "Password": "Password", @@ -315,6 +319,7 @@ "Total Points": "Total Points", "Total cost": "Total cost", "Total incl VAT": "Total incl VAT", + "Total price": "Total price", "Tourist": "Tourist", "Transaction date": "Transaction date", "Transactions": "Transactions", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 5f464d30f..bb417d8c8 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -3,6 +3,8 @@ "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/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", + "ALLG": "Allergia", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Tietoja hotellista", "Accessible Room": "Esteetön huone", @@ -107,6 +109,7 @@ "FAQ": "Usein kysytyt kysymykset", "Failed to delete credit card, please try again later.": "Luottokortin poistaminen epäonnistui, yritä myöhemmin uudelleen.", "Fair": "Messukeskus", + "Filter": "Suodatin", "Find booking": "Etsi varaus", "Find hotels": "Etsi hotelleja", "First name": "Etunimi", @@ -211,6 +214,7 @@ "Open menu": "Avaa valikko", "Open my pages menu": "Avaa omat sivut -valikko", "Overview": "Yleiskatsaus", + "PETR": "Lemmikki", "Parking": "Pysäköinti", "Parking / Garage": "Pysäköinti / Autotalli", "Password": "Salasana", @@ -301,6 +305,7 @@ "Things nearby HOTEL_NAME": "Lähellä olevia asioita {hotelName}", "Total Points": "Kokonaispisteet", "Total incl VAT": "Yhteensä sis. alv", + "Total price": "Kokonaishinta", "Tourist": "Turisti", "Transaction date": "Tapahtuman päivämäärä", "Transactions": "Tapahtumat", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 8ef615923..76d2745db 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -3,6 +3,8 @@ "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/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", + "ALLG": "Allergi", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Om hotellet", "Accessible Room": "Tilgjengelighetsrom", @@ -106,6 +108,7 @@ "FAQ": "Ofte stilte spørsmål", "Failed to delete credit card, please try again later.": "Kunne ikke slette kredittkortet, prøv igjen senere.", "Fair": "Messe", + "Filter": "Filter", "Find booking": "Finn booking", "Find hotels": "Finn hotell", "First name": "Fornavn", @@ -209,6 +212,7 @@ "Open menu": "Åpne menyen", "Open my pages menu": "Åpne mine sider menyen", "Overview": "Oversikt", + "PETR": "Kjæledyr", "Parking": "Parkering", "Parking / Garage": "Parkering / Garasje", "Password": "Passord", @@ -298,6 +302,7 @@ "Things nearby HOTEL_NAME": "Ting i nærheten av {hotelName}", "Total Points": "Totale poeng", "Total incl VAT": "Sum inkl mva", + "Total price": "Totalpris", "Tourist": "Turist", "Transaction date": "Transaksjonsdato", "Transactions": "Transaksjoner", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index df8d576c8..dd1b443e2 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -3,6 +3,8 @@ "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/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", + "ALLG": "Allergi", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Om hotellet", "Accessible Room": "Tillgänglighetsrum", @@ -106,6 +108,7 @@ "FAQ": "FAQ", "Failed to delete credit card, please try again later.": "Det gick inte att ta bort kreditkortet, försök igen senare.", "Fair": "Mässa", + "Filter": "Filter", "Find booking": "Hitta bokning", "Find hotels": "Hitta hotell", "First name": "Förnamn", @@ -209,6 +212,7 @@ "Open menu": "Öppna menyn", "Open my pages menu": "Öppna mina sidor menyn", "Overview": "Översikt", + "PETR": "Husdjur", "Parking": "Parkering", "Parking / Garage": "Parkering / Garage", "Password": "Lösenord", @@ -298,6 +302,7 @@ "Things nearby HOTEL_NAME": "Saker i närheten av {hotelName}", "Total Points": "Poäng totalt", "Total incl VAT": "Totalt inkl moms", + "Total price": "Totalpris", "Tourist": "Turist", "Transaction date": "Transaktionsdatum", "Transactions": "Transaktioner", @@ -374,15 +379,15 @@ "number": "nummer", "or": "eller", "points": "poäng", + "room type": "rumtyp", + "room types": "rumstyper", "special character": "speciell karaktär", "spendable points expiring by": "{points} poäng förfaller {date}", "to": "till", - "uppercase letter": "stor bokstav", - "{amount} out of {total}": "{amount} av {total}", "type": "typ", "types": "typer", - "room type": "rumtyp", - "room types": "rumstyper", + "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" diff --git a/types/components/hotelReservation/selectRate/rateSummary.ts b/types/components/hotelReservation/selectRate/rateSummary.ts index c9685f180..73af97078 100644 --- a/types/components/hotelReservation/selectRate/rateSummary.ts +++ b/types/components/hotelReservation/selectRate/rateSummary.ts @@ -1,3 +1,5 @@ +import { RoomsAvailability } from "@/server/routers/hotels/output" + import { RoomPackageData } from "./roomFilter" import { Rate } from "./selectRate" @@ -5,4 +7,5 @@ export interface RateSummaryProps { rateSummary: Rate isUserLoggedIn: boolean packages: RoomPackageData + roomsAvailability: RoomsAvailability } From 3b2b39fdc6f0fe633d0d96a0376c3cbb0912b362 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 28 Oct 2024 13:13:09 +0100 Subject: [PATCH 18/73] feat(sw-453): removed log --- .../(public)/hotelreservation/(standard)/select-rate/page.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index 48a0b30b1..603bbbf4a 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -58,8 +58,6 @@ export default async function SelectRatePage({ return "No hotel data found" // TODO: Add a proper error message } - console.log(selectRoomParamsObject) - const roomCategories = hotelData?.included return ( From c14b413a34eb9284079bbb3d79bd7bbddf18c0fa Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 28 Oct 2024 13:58:37 +0100 Subject: [PATCH 19/73] feat(sw-453): fixed filter bug with key --- .../HotelReservation/SelectRate/RoomSelection/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/index.tsx index 3c0305190..b6e686faa 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/index.tsx @@ -1,6 +1,6 @@ "use client" import { useRouter, useSearchParams } from "next/navigation" -import { useMemo,useState } from "react" +import { useMemo, useState } from "react" import RateSummary from "./RateSummary" import RoomCard from "./RoomCard" @@ -61,7 +61,7 @@ export default function RoomSelection({ > <ul className={styles.roomList}> {roomConfigurations.map((roomConfiguration) => ( - <li key={roomConfiguration.roomType}> + <li key={roomConfiguration.roomTypeCode}> <RoomCard rateDefinitions={rateDefinitions} roomConfiguration={roomConfiguration} From 62cf5dab80dcb885baf639d6344bafcb25054cb5 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 28 Oct 2024 17:04:24 +0100 Subject: [PATCH 20/73] feat(sw-453): fixed pr comments and default filter --- .../(standard)/select-rate/page.tsx | 18 +++++++++---- .../SelectRate/RoomFilter/index.tsx | 12 +++++---- .../RoomSelection/RateSummary/index.tsx | 13 +++++----- .../RoomSelection/RoomCard/index.tsx | 22 +++++++--------- .../SelectRate/RoomSelection/index.tsx | 4 +-- .../SelectRate/Rooms/index.tsx | 26 ++++++++++++------- .../HotelReservation/SelectRate/utils.ts | 10 +++---- i18n/dictionaries/da.json | 4 +++ i18n/dictionaries/de.json | 4 +++ i18n/dictionaries/en.json | 1 + i18n/dictionaries/fi.json | 4 +++ i18n/dictionaries/no.json | 4 +++ i18n/dictionaries/sv.json | 1 + server/routers/hotels/output.ts | 6 ++--- server/routers/hotels/schemas/packages.ts | 6 ++--- .../hotelReservation/selectRate/roomCard.ts | 1 - .../hotelReservation/selectRate/roomFilter.ts | 6 ++--- 17 files changed, 87 insertions(+), 55 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index 603bbbf4a..85c546997 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -1,3 +1,5 @@ +import { notFound } from "next/navigation" + import { getProfileSafely } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" @@ -17,10 +19,16 @@ export default async function SelectRatePage({ setLang(params.lang) const selectRoomParams = new URLSearchParams(searchParams) + console.log(selectRoomParams) const selectRoomParamsObject = getHotelReservationQueryParams(selectRoomParams) - const adults = selectRoomParamsObject.room?.[0].adults // TODO: Handle multiple rooms - const children = selectRoomParamsObject.room?.[0].child?.length // TODO: Handle multiple rooms + + if (!selectRoomParamsObject.room) { + return notFound() + } + + const adults = selectRoomParamsObject.room[0].adults // TODO: Handle multiple rooms + const children = selectRoomParamsObject.room[0].child?.length // TODO: Handle multiple rooms const [hotelData, roomsAvailability, packages, user] = await Promise.all([ serverClient().hotel.hotelData.get({ @@ -42,9 +50,9 @@ export default async function SelectRatePage({ adults: adults, children: children, packageCodes: [ - RoomPackageCodeEnum.ACCE, - RoomPackageCodeEnum.PETR, - RoomPackageCodeEnum.ALLG, + RoomPackageCodeEnum.ACCESSIBILITY_ROOM, + RoomPackageCodeEnum.PET_ROOM, + RoomPackageCodeEnum.ALLERGY_ROOM, ], }), getProfileSafely(), diff --git a/components/HotelReservation/SelectRate/RoomFilter/index.tsx b/components/HotelReservation/SelectRate/RoomFilter/index.tsx index 19fd4d8b7..ab3bd5626 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/index.tsx +++ b/components/HotelReservation/SelectRate/RoomFilter/index.tsx @@ -17,7 +17,7 @@ import { getIconForFeatureCode } from "../utils" import styles from "./roomFilter.module.css" import { - RoomFilterProps, + type RoomFilterProps, RoomPackageCodeEnum, } from "@/types/components/hotelReservation/selectRate/roomFilter" @@ -47,8 +47,8 @@ export default function RoomFilter({ }) const { watch, getValues, handleSubmit } = methods - const petFriendly = watch(RoomPackageCodeEnum.PETR) - const allergyFriendly = watch(RoomPackageCodeEnum.ALLG) + const petFriendly = watch(RoomPackageCodeEnum.PET_ROOM) + const allergyFriendly = watch(RoomPackageCodeEnum.ALLERGY_ROOM) const selectedFilters = useMemo(() => getValues(), [getValues]) @@ -104,8 +104,10 @@ export default function RoomFilter({ key={option.code} label={intl.formatMessage({ id: option.description })} disabled={ - (option.code === RoomPackageCodeEnum.ALLG && petFriendly) || - (option.code === RoomPackageCodeEnum.PETR && allergyFriendly) + (option.code === RoomPackageCodeEnum.ALLERGY_ROOM && + petFriendly) || + (option.code === RoomPackageCodeEnum.PET_ROOM && + allergyFriendly) } selected={getValues(option.code)} Icon={getIconForFeatureCode(option.code)} diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx index 3b9f6dbf6..cc594aed1 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx @@ -1,3 +1,4 @@ +import { differenceInCalendarDays } from "date-fns" import { useIntl } from "react-intl" import Button from "@/components/TempDesignSystem/Button" @@ -27,12 +28,12 @@ export default function RateSummary({ } = rateSummary const priceToShow = isUserLoggedIn ? member : publicRate - const isPetRoomSelect = features.some( - (feature) => feature.code === RoomPackageCodeEnum.PETR + const isPetRoomSelected = features.some( + (feature) => feature.code === RoomPackageCodeEnum.PET_ROOM ) const petRoomPackage = packages.find( - (pkg) => pkg.code === RoomPackageCodeEnum.PETR + (pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM ) const petRoomPrice = petRoomPackage?.calculatedPrice ?? null @@ -40,9 +41,7 @@ export default function RateSummary({ const checkInDate = new Date(roomsAvailability.checkInDate) const checkOutDate = new Date(roomsAvailability.checkOutDate) - const nights = Math.ceil( - (checkOutDate.getTime() - checkInDate.getTime()) / (1000 * 60 * 60 * 24) - ) + const nights = differenceInCalendarDays(checkOutDate, checkInDate) return ( <div className={styles.summary}> @@ -94,7 +93,7 @@ export default function RateSummary({ )} </Footnote> </div> - {isPetRoomSelect && ( + {isPetRoomSelected && ( <div className={styles.petInfo}> <Body color="uiTextHighContrast" textTransform="bold"> + {petRoomPrice} {petRoomCurrency} diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index d1ab92174..665d91b31 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -27,20 +27,18 @@ export default function RoomCard({ }: RoomCardProps) { const intl = useIntl() - // TODO: Update string when API has decided - const rateTypes = { - saveRate: "NonCancellable", - changeRate: "Modifiable", - flexRate: "CancellableBefore6PM", + const rates = { + saveRate: rateDefinitions.find( + (rate) => rate.cancellationRule === "NonCancellable" + ), + changeRate: rateDefinitions.find( + (rate) => rate.cancellationRule === "Modifiable" + ), + flexRate: rateDefinitions.find( + (rate) => rate.cancellationRule === "CancellableBefore6PM" + ), } - const rates = Object.fromEntries( - Object.entries(rateTypes).map(([key, rule]) => [ - key, - rateDefinitions.find((rate) => rate.cancellationRule === rule), - ]) - ) - function findProductForRate(rate: RateDefinition | undefined) { return rate ? roomConfiguration.products.find( diff --git a/components/HotelReservation/SelectRate/RoomSelection/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/index.tsx index b6e686faa..9929a7451 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/index.tsx @@ -8,8 +8,8 @@ import getHotelReservationQueryParams from "./utils" import styles from "./roomSelection.module.css" -import { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection" -import { Rate } from "@/types/components/hotelReservation/selectRate/selectRate" +import type { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection" +import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate" export default function RoomSelection({ roomsAvailability, diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx index 74fe0e811..ea0901f44 100644 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -1,6 +1,6 @@ "use client" -import { useCallback,useState } from "react" +import { useCallback, useState } from "react" import { RoomsAvailability } from "@/server/routers/hotels/output" @@ -9,8 +9,7 @@ import RoomSelection from "../RoomSelection" import styles from "./rooms.module.css" -import { RoomPackageCodes } from "@/types/components/hotelReservation/selectRate/roomFilter" -import { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection" +import type { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection" export default function Rooms({ roomsAvailability, @@ -18,26 +17,35 @@ export default function Rooms({ user, packages, }: RoomSelectionProps) { - const [rooms, setRooms] = useState<RoomsAvailability>(roomsAvailability) + const defaultRooms = roomsAvailability.roomConfigurations.filter( + (room) => room.features.length === 0 + ) + const [rooms, setRooms] = useState<RoomsAvailability>({ + ...roomsAvailability, + roomConfigurations: defaultRooms, + }) + + console.log(rooms) const handleFilter = useCallback( (filter: Record<string, boolean | undefined>) => { const selectedCodes = Object.keys(filter).filter((key) => filter[key]) if (selectedCodes.length === 0) { - setRooms(roomsAvailability) + setRooms({ + ...roomsAvailability, + roomConfigurations: defaultRooms, + }) return } const filteredRooms = roomsAvailability.roomConfigurations.filter( (room) => - room.features.some((feature) => - selectedCodes.includes(feature.code as RoomPackageCodes) - ) + room.features.some((feature) => selectedCodes.includes(feature.code)) ) setRooms({ ...roomsAvailability, roomConfigurations: filteredRooms }) }, - [roomsAvailability] + [roomsAvailability, defaultRooms] ) return ( diff --git a/components/HotelReservation/SelectRate/utils.ts b/components/HotelReservation/SelectRate/utils.ts index 6002b0705..d91476bb5 100644 --- a/components/HotelReservation/SelectRate/utils.ts +++ b/components/HotelReservation/SelectRate/utils.ts @@ -1,17 +1,17 @@ -import { AllergyIcon,PetsIcon, WheelchairIcon } from "@/components/Icons" +import { AllergyIcon, PetsIcon, WheelchairIcon } from "@/components/Icons" import { RoomPackageCodeEnum, - RoomPackageCodes, + type RoomPackageCodes, } from "@/types/components/hotelReservation/selectRate/roomFilter" export function getIconForFeatureCode(featureCode: RoomPackageCodes) { switch (featureCode) { - case RoomPackageCodeEnum.ACCE: + case RoomPackageCodeEnum.ACCESSIBILITY_ROOM: return WheelchairIcon - case RoomPackageCodeEnum.ALLG: + case RoomPackageCodeEnum.ALLERGY_ROOM: return AllergyIcon - case RoomPackageCodeEnum.PETR: + case RoomPackageCodeEnum.PET_ROOM: return PetsIcon default: return PetsIcon diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 625646fb8..e47484466 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -7,6 +7,7 @@ "ALLG": "Allergi", "About meetings & conferences": "About meetings & conferences", "About the hotel": "About the hotel", + "Accessibility": "Tilgængelighed", "Accessible Room": "Tilgængelighedsrum", "Activities": "Aktiviteter", "Add code": "Tilføj kode", @@ -357,12 +358,14 @@ "Zoom in": "Zoom ind", "Zoom out": "Zoom ud", "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.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.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle <termsLink>Vilkår og betingelser</termsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyLink>Scandics Privatlivspolitik</privacyLink>. 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", "by": "inden", "characters": "tegn", "guest": "gæst", @@ -385,6 +388,7 @@ "spendable points expiring by": "{points} Brugbare point udløber den {date}", "to": "til", "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" diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 0029444d2..88a5092e5 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -7,6 +7,7 @@ "ALLG": "Allergie", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Über das Hotel", + "Accessibility": "Zugänglichkeit", "Accessible Room": "Barrierefreies Zimmer", "Activities": "Aktivitäten", "Add code": "Code hinzufügen", @@ -358,12 +359,14 @@ "Zoom in": "Vergrößern", "Zoom out": "Verkleinern", "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.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.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle <termsLink>Vilkår og betingelser</termsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyLink>Scandics Privatlivspolitik</privacyLink>. 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", "by": "bis", "characters": "figuren", "guest": "gast", @@ -386,6 +389,7 @@ "spendable points expiring by": "{points} Einlösbare punkte verfallen bis zum {date}", "to": "zu", "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" diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 8aebd5004..75fbbd805 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -7,6 +7,7 @@ "ALLG": "Allergy", "About meetings & conferences": "About meetings & conferences", "About the hotel": "About the hotel", + "Accessibility": "Accessibility", "Accessible Room": "Accessibility room", "Activities": "Activities", "Add Room": "Add room", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index bb417d8c8..6f2f28b63 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -7,6 +7,7 @@ "ALLG": "Allergia", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Tietoja hotellista", + "Accessibility": "Saavutettavuus", "Accessible Room": "Esteetön huone", "Activities": "Aktiviteetit", "Add code": "Lisää koodi", @@ -358,12 +359,14 @@ "Zoom in": "Lähennä", "Zoom out": "Loitonna", "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.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.terms": "Maksamalla minkä tahansa saatavilla olevan maksutavan avulla hyväksyn tämän varauksen ehdot ja yleiset <termsLink>ehdot ja ehtoja</termsLink>, ja ymmärrän, että Scandic käsittelee minun henkilötietoni tässä varauksessa mukaisesti <privacyLink>Scandicin tietosuojavaltuuden</privacyLink> mukaisesti. Hyväksyn myös, että Scandic vaatii validin luottokortin majoituksen ajan, jos jokin jää maksamatta.", + "booking.thisRoomIsEquippedWith": "Tämä huone on varustettu", "by": "mennessä", "characters": "hahmoja", "guest": "Vieras", @@ -386,6 +389,7 @@ "spendable points expiring by": "{points} pistettä vanhenee {date} mennessä", "to": "to", "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" diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 76d2745db..0b45a4673 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -7,6 +7,7 @@ "ALLG": "Allergi", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Om hotellet", + "Accessibility": "Tilgjengelighet", "Accessible Room": "Tilgjengelighetsrom", "Activities": "Aktiviteter", "Add code": "Legg til kode", @@ -355,11 +356,13 @@ "Zoom in": "Zoom inn", "Zoom out": "Zoom ut", "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.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.thisRoomIsEquippedWith": "Dette rommet er utstyrt med", "by": "innen", "characters": "tegn", "guest": "gjest", @@ -382,6 +385,7 @@ "spendable points expiring by": "{points} Brukbare poeng utløper innen {date}", "to": "til", "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" diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index dd1b443e2..406aa4cb4 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -7,6 +7,7 @@ "ALLG": "Allergi", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Om hotellet", + "Accessibility": "Tillgänglighet", "Accessible Room": "Tillgänglighetsrum", "Activities": "Aktiviteter", "Add code": "Lägg till kod", diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 91032ffdc..182575c2d 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -550,9 +550,9 @@ const roomConfigurationSchema = z.object({ z.object({ inventory: z.number(), code: z.enum([ - RoomPackageCodeEnum.PETR, - RoomPackageCodeEnum.ALLG, - RoomPackageCodeEnum.ACCE, + RoomPackageCodeEnum.PET_ROOM, + RoomPackageCodeEnum.ALLERGY_ROOM, + RoomPackageCodeEnum.ACCESSIBILITY_ROOM, ]), }) ), diff --git a/server/routers/hotels/schemas/packages.ts b/server/routers/hotels/schemas/packages.ts index a239c6b50..2f12f7255 100644 --- a/server/routers/hotels/schemas/packages.ts +++ b/server/routers/hotels/schemas/packages.ts @@ -14,9 +14,9 @@ export const getRoomPackagesInputSchema = z.object({ const packagesSchema = z.array( z.object({ code: z.enum([ - RoomPackageCodeEnum.PETR, - RoomPackageCodeEnum.ALLG, - RoomPackageCodeEnum.ACCE, + RoomPackageCodeEnum.PET_ROOM, + RoomPackageCodeEnum.ALLERGY_ROOM, + RoomPackageCodeEnum.ACCESSIBILITY_ROOM, ]), itemCode: z.string(), description: z.string(), diff --git a/types/components/hotelReservation/selectRate/roomCard.ts b/types/components/hotelReservation/selectRate/roomCard.ts index 99552916d..a6ed91ac1 100644 --- a/types/components/hotelReservation/selectRate/roomCard.ts +++ b/types/components/hotelReservation/selectRate/roomCard.ts @@ -3,7 +3,6 @@ import { RoomConfiguration, } from "@/server/routers/hotels/output" -import { RoomPackageCodes } from "./roomFilter" import { Rate } from "./selectRate" import { RoomData } from "@/types/hotel" diff --git a/types/components/hotelReservation/selectRate/roomFilter.ts b/types/components/hotelReservation/selectRate/roomFilter.ts index aaf807508..d42669295 100644 --- a/types/components/hotelReservation/selectRate/roomFilter.ts +++ b/types/components/hotelReservation/selectRate/roomFilter.ts @@ -3,9 +3,9 @@ import { z } from "zod" import { getRoomPackagesSchema } from "@/server/routers/hotels/schemas/packages" export enum RoomPackageCodeEnum { - PETR = "PETR", - ALLG = "ALLG", - ACCE = "ACCE", + PET_ROOM = "PETR", + ALLERGY_ROOM = "ALLG", + ACCESSIBILITY_ROOM = "ACCE", } export interface RoomFilterProps { numberOfRooms: number From c679fa83e2ff71a0f7bdc61fca61eccc989b216c Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 28 Oct 2024 17:06:42 +0100 Subject: [PATCH 21/73] feat(sw-453): removed log --- .../(public)/hotelreservation/(standard)/select-rate/page.tsx | 1 - 1 file changed, 1 deletion(-) 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 85c546997..2da2d78a6 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -19,7 +19,6 @@ export default async function SelectRatePage({ setLang(params.lang) const selectRoomParams = new URLSearchParams(searchParams) - console.log(selectRoomParams) const selectRoomParamsObject = getHotelReservationQueryParams(selectRoomParams) From 153822b738520fff36d523355a71b29f69983707 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 28 Oct 2024 17:07:27 +0100 Subject: [PATCH 22/73] feat(sw-453): removed log --- components/HotelReservation/SelectRate/Rooms/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx index ea0901f44..923bfa35c 100644 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -25,8 +25,6 @@ export default function Rooms({ roomConfigurations: defaultRooms, }) - console.log(rooms) - const handleFilter = useCallback( (filter: Record<string, boolean | undefined>) => { const selectedCodes = Object.keys(filter).filter((key) => filter[key]) From 83cd9cf5183f941ecb299da1bb745f3e5fcbcb88 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 28 Oct 2024 17:22:21 +0100 Subject: [PATCH 23/73] feat(sw-453): import types --- .../hotelreservation/(standard)/select-rate/page.tsx | 2 +- .../SelectRate/RoomSelection/RateSummary/index.tsx | 2 +- .../HotelReservation/SelectRate/RoomSelection/utils.ts | 2 +- .../hotelReservation/selectRate/rateSummary.ts | 7 +++---- .../hotelReservation/selectRate/roomSelection.ts | 10 ++++------ 5 files changed, 10 insertions(+), 13 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index 2da2d78a6..b361aaa10 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -9,7 +9,7 @@ import getHotelReservationQueryParams from "@/components/HotelReservation/Select import { setLang } from "@/i18n/serverContext" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" -import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" +import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import { LangParams, PageArgs } from "@/types/params" export default async function SelectRatePage({ diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx index cc594aed1..b7ecc3c63 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx @@ -9,7 +9,7 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import styles from "./rateSummary.module.css" -import { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary" +import type { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" export default function RateSummary({ diff --git a/components/HotelReservation/SelectRate/RoomSelection/utils.ts b/components/HotelReservation/SelectRate/RoomSelection/utils.ts index 0b1ab884a..1ae94cc9c 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/utils.ts +++ b/components/HotelReservation/SelectRate/RoomSelection/utils.ts @@ -1,6 +1,6 @@ import { getFormattedUrlQueryParams } from "@/utils/url" -import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" +import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" function getHotelReservationQueryParams(searchParams: URLSearchParams) { return getFormattedUrlQueryParams(searchParams, { diff --git a/types/components/hotelReservation/selectRate/rateSummary.ts b/types/components/hotelReservation/selectRate/rateSummary.ts index 73af97078..f6c0f03b6 100644 --- a/types/components/hotelReservation/selectRate/rateSummary.ts +++ b/types/components/hotelReservation/selectRate/rateSummary.ts @@ -1,7 +1,6 @@ -import { RoomsAvailability } from "@/server/routers/hotels/output" - -import { RoomPackageData } from "./roomFilter" -import { Rate } from "./selectRate" +import type { RoomsAvailability } from "@/server/routers/hotels/output" +import type { RoomPackageData } from "./roomFilter" +import type { Rate } from "./selectRate" export interface RateSummaryProps { rateSummary: Rate diff --git a/types/components/hotelReservation/selectRate/roomSelection.ts b/types/components/hotelReservation/selectRate/roomSelection.ts index 03e84245e..8d006779c 100644 --- a/types/components/hotelReservation/selectRate/roomSelection.ts +++ b/types/components/hotelReservation/selectRate/roomSelection.ts @@ -1,9 +1,7 @@ -import { RoomsAvailability } from "@/server/routers/hotels/output" - -import { RoomPackageData } from "./roomFilter" - -import { RoomData } from "@/types/hotel" -import { SafeUser } from "@/types/user" +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" export interface RoomSelectionProps { roomsAvailability: RoomsAvailability From f35ccbd9978281785a7305c5e7dff189518c2f39 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Tue, 29 Oct 2024 08:34:52 +0100 Subject: [PATCH 24/73] feat(sw-453): correct filter --- components/HotelReservation/SelectRate/Rooms/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx index 923bfa35c..8f9030149 100644 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -39,7 +39,9 @@ export default function Rooms({ const filteredRooms = roomsAvailability.roomConfigurations.filter( (room) => - room.features.some((feature) => selectedCodes.includes(feature.code)) + selectedCodes.every((selectedCode) => + room.features.some((feature) => feature.code === selectedCode) + ) ) setRooms({ ...roomsAvailability, roomConfigurations: filteredRooms }) }, From 3705ff0120e7dfeb13c318f2ce8a4d8ae23f0b1a Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Tue, 29 Oct 2024 11:09:29 +0100 Subject: [PATCH 25/73] feat(sw-453): removed useMemo on getValues --- .gitignore | 3 +++ components/HotelReservation/SelectRate/RoomFilter/index.tsx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4663988fb..8a6f4e73e 100644 --- a/.gitignore +++ b/.gitignore @@ -42,5 +42,8 @@ certificates #vscode .vscode/ +#cursor +.cursorrules + # localfile with all the CSS variables exported from design system variables.css \ No newline at end of file diff --git a/components/HotelReservation/SelectRate/RoomFilter/index.tsx b/components/HotelReservation/SelectRate/RoomFilter/index.tsx index ab3bd5626..eb7c442b3 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/index.tsx +++ b/components/HotelReservation/SelectRate/RoomFilter/index.tsx @@ -50,7 +50,7 @@ export default function RoomFilter({ const petFriendly = watch(RoomPackageCodeEnum.PET_ROOM) const allergyFriendly = watch(RoomPackageCodeEnum.ALLERGY_ROOM) - const selectedFilters = useMemo(() => getValues(), [getValues]) + const selectedFilters = getValues() const tooltipText = intl.formatMessage({ id: "Pet-friendly rooms have an additional fee of 20 EUR per stay", From 1e92348fb497150c8d355ddcd47a6b2591461f90 Mon Sep 17 00:00:00 2001 From: Michael Zetterberg <michael.zetterberg@scandichotels.com> Date: Tue, 22 Oct 2024 07:09:26 +0200 Subject: [PATCH 26/73] fix(SW-126): remove feature flag communications settings in profile --- app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx index 13512d701..73496fe4e 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx @@ -1,5 +1,3 @@ -import { env } from "@/env/server" - import Divider from "@/components/TempDesignSystem/Divider" import type { ProfileLayoutProps } from "@/types/components/myPages/myProfile/layout" @@ -17,7 +15,7 @@ export default function ProfileLayout({ {profile} <Divider color="burgundy" opacity={8} /> {creditCards} - {env.HIDE_FOR_NEXT_RELEASE ? null : communication} + {communication} </section> </main> ) From 62ddad5730827bb741685911c296abd1c205158f Mon Sep 17 00:00:00 2001 From: Michael Zetterberg <michael.zetterberg@scandichotels.com> Date: Tue, 29 Oct 2024 04:51:35 +0100 Subject: [PATCH 27/73] fix(SW-714): rendering of RTE nested links/references --- components/JsonToHtml/renderOptions.tsx | 41 ++++++++++++++++--------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/components/JsonToHtml/renderOptions.tsx b/components/JsonToHtml/renderOptions.tsx index 219981529..2ce1afbec 100644 --- a/components/JsonToHtml/renderOptions.tsx +++ b/components/JsonToHtml/renderOptions.tsx @@ -42,6 +42,17 @@ import type { import { RTEMarkType } from "@/types/transitionTypes/rte/node" import type { RenderOptions } from "@/types/transitionTypes/rte/option" +function noNestedLinksOrReferences(node: RTENode) { + if ("type" in node) { + if (node.type === RTETypeEnum.reference) { + return node.children + } else if (node.type === RTETypeEnum.a) { + return node.children + } + } + return node +} + function extractPossibleAttributes(attrs: Attributes | undefined) { if (!attrs) return {} const props: Record<string, any> = {} @@ -82,7 +93,13 @@ export const renderOptions: RenderOptions = { variant="underscored" color="burgundy" > - {next(node.children, embeds, fullRenderOptions)} + {next( + // Sometimes editors happen to nest a reference inside a link and vice versa. + // In that case use the outermost link, i.e. ignore nested links. + node.children.flatMap(noNestedLinksOrReferences), + embeds, + fullRenderOptions + )} </Link> ) } @@ -340,28 +357,22 @@ export const renderOptions: RenderOptions = { ) { // If entry is not an ImageContainer, it is a page and we return it as a link const props = extractPossibleAttributes(node.attrs) - let href = "" - if (entry?.node.__typename === ContentEnum.blocks.AccountPage) { - href = removeMultipleSlashes( - `/${entry.node.system.locale}${entry.node.url}` - ) - } else { - href = - entry.node?.web?.original_url || - removeMultipleSlashes( - `/${entry.node.system.locale}${entry.node.url}` - ) - } return ( <Link {...props} - href={href} + href={entry.node.url} key={node.uid} variant="underscored" color="burgundy" > - {next(node.children, embeds, fullRenderOptions)} + {next( + // Sometimes editors happen to nest a reference inside a link and vice versa. + // In that case use the outermost link, i.e. ignore nested links. + node.children.flatMap(noNestedLinksOrReferences), + embeds, + fullRenderOptions + )} </Link> ) } From fc8844eb96dc4c1392ecff32adeb55cec319950c Mon Sep 17 00:00:00 2001 From: Bianca Widstam <bianca.widstam@scandichotels.com> Date: Tue, 29 Oct 2024 13:54:12 +0000 Subject: [PATCH 28/73] feat/SW-689-image-gallery-sizes (pull request #781) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Feat/SW-689 image gallery sizes * feat(SW-689): initial gallery changes * feat(SW-689): remove console.log * feat(SW-689): remove unneccessary code * feat(SW-689): change sizes * feat(SW-689): change size * feat(SW-689): add design for ipad for fullview * feat(SW-689): fix import type * feat(SW-689): fix tripAdvisor placement * feat(SW-689): fix image gallery type * feat(SW-689): fix check gallery length Approved-by: Christian Andolf Approved-by: Matilda Landström --- .../HotelPage/PreviewImages/index.tsx | 6 +-- components/ContentType/HotelPage/index.tsx | 4 +- .../SelectRate/HotelInfoCard/index.tsx | 17 ++++----- .../SelectRate/ImageGallery/index.tsx | 9 +---- components/Lightbox/FullView.tsx | 10 +++-- components/Lightbox/Gallery.tsx | 38 ++++++++++--------- components/Lightbox/Lightbox.module.css | 19 ++++++++++ components/Lightbox/index.tsx | 2 +- server/routers/hotels/output.ts | 26 ++++--------- server/routers/hotels/query.ts | 3 +- server/routers/utils/hotels.ts | 30 --------------- types/components/hotelPage/previewImages.ts | 4 +- .../selectRate/imageGallery.ts | 4 +- types/components/lightbox/lightbox.ts | 16 +++----- types/hotel.ts | 5 +-- 15 files changed, 82 insertions(+), 111 deletions(-) delete mode 100644 server/routers/utils/hotels.ts diff --git a/components/ContentType/HotelPage/PreviewImages/index.tsx b/components/ContentType/HotelPage/PreviewImages/index.tsx index 99704a61b..2a0074c5c 100644 --- a/components/ContentType/HotelPage/PreviewImages/index.tsx +++ b/components/ContentType/HotelPage/PreviewImages/index.tsx @@ -21,9 +21,9 @@ export default async function PreviewImages({ {images.slice(0, 3).map((image, index) => ( <Image key={index} - src={image.url} - alt={image.alt} - title={image.title} + src={image.imageSizes.medium} + alt={image.metaData.altText} + title={image.metaData.title} width={index === 0 ? 752 : 292} height={index === 0 ? 540 : 266} className={styles.image} diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index d10244040..f5718a7f2 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -63,7 +63,9 @@ export default async function HotelPage() { return ( <div className={styles.pageContainer}> <div className={styles.hotelImages}> - <PreviewImages images={hotelImages} hotelName={hotelName} /> + {hotelImages?.length && ( + <PreviewImages images={hotelImages} hotelName={hotelName} /> + )} </div> <TabNavigation restaurantTitle={getRestaurantHeading(hotelDetailedFacilities)} diff --git a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx index 120c0385d..b48220795 100644 --- a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx +++ b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx @@ -3,7 +3,6 @@ import { useIntl } from "react-intl" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import TripAdvisorIcon from "@/components/Icons/TripAdvisor" -import Image from "@/components/Image" import Divider from "@/components/TempDesignSystem/Divider" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" @@ -29,14 +28,6 @@ export default function HotelInfoCard({ hotelData }: HotelInfoCardProps) { {hotelAttributes && ( <section className={styles.wrapper}> <div className={styles.imageWrapper}> - {hotelAttributes.ratings?.tripAdvisor && ( - <div className={styles.tripAdvisor}> - <TripAdvisorIcon color="burgundy" /> - <Caption color="burgundy"> - {hotelAttributes.ratings.tripAdvisor.rating} - </Caption> - </div> - )} {hotelAttributes.gallery && ( <ImageGallery title={hotelAttributes.name} @@ -46,6 +37,14 @@ export default function HotelInfoCard({ hotelData }: HotelInfoCardProps) { ]} /> )} + {hotelAttributes.ratings?.tripAdvisor && ( + <div className={styles.tripAdvisor}> + <TripAdvisorIcon color="burgundy" /> + <Caption color="burgundy"> + {hotelAttributes.ratings.tripAdvisor.rating} + </Caption> + </div> + )} </div> <div className={styles.hotelContent}> <div className={styles.hotelInformation}> diff --git a/components/HotelReservation/SelectRate/ImageGallery/index.tsx b/components/HotelReservation/SelectRate/ImageGallery/index.tsx index 1d4500cdb..4ff21af9f 100644 --- a/components/HotelReservation/SelectRate/ImageGallery/index.tsx +++ b/components/HotelReservation/SelectRate/ImageGallery/index.tsx @@ -9,14 +9,7 @@ import type { ImageGalleryProps } from "@/types/components/hotelReservation/sele export default function ImageGallery({ images, title }: ImageGalleryProps) { return ( - <Lightbox - images={images.map((image) => ({ - url: image.imageSizes.small, - alt: image.metaData.altText, - title: image.metaData.title, - }))} - dialogTitle={title} - > + <Lightbox images={images} dialogTitle={title}> <div className={styles.triggerArea} id="lightboxTrigger"> <Image src={images[0].imageSizes.medium} diff --git a/components/Lightbox/FullView.tsx b/components/Lightbox/FullView.tsx index 5541d365b..360b7a276 100644 --- a/components/Lightbox/FullView.tsx +++ b/components/Lightbox/FullView.tsx @@ -40,7 +40,7 @@ export default function FullView({ <div className={styles.fullViewImageContainer}> <AnimatePresence initial={false} custom={currentIndex}> <motion.div - key={image.url} + key={image.imageSizes.medium} custom={currentIndex} initial={{ opacity: 0, x: 300 }} animate={{ opacity: 1, x: 0 }} @@ -49,14 +49,16 @@ export default function FullView({ className={styles.fullViewImage} > <Image - alt={image.alt} + alt={image.metaData.altText} fill - src={image.url} + src={image.imageSizes.medium} style={{ objectFit: "cover" }} /> <div className={styles.fullViewFooter}> - {image.title && <Body color="white">{image.title}</Body>} + {image.metaData.title && ( + <Body color="white">{image.metaData.title}</Body> + )} </div> </motion.div> </AnimatePresence> diff --git a/components/Lightbox/Gallery.tsx b/components/Lightbox/Gallery.tsx index 23149d3de..578cc961e 100644 --- a/components/Lightbox/Gallery.tsx +++ b/components/Lightbox/Gallery.tsx @@ -20,7 +20,7 @@ export default function Gallery({ selectedImage, }: GalleryProps) { const mainImage = selectedImage || images[0] - const mainImageIndex = images.findIndex((img) => img.url === mainImage.url) + const mainImageIndex = images.findIndex((img) => img === mainImage) function getThumbImages() { const thumbs = [] @@ -55,16 +55,18 @@ export default function Gallery({ {/* Desktop Gallery */} <div className={styles.desktopGallery}> <div className={styles.galleryHeader}> - {mainImage.title && ( + {mainImage.metaData.title && ( <div className={styles.imageCaption}> - <Caption color="textMediumContrast">{mainImage.title}</Caption> + <Caption color="textMediumContrast"> + {mainImage.metaData.title} + </Caption> </div> )} </div> <div className={styles.mainImageWrapper}> <AnimatePresence initial={false} mode="wait"> <motion.div - key={mainImage.url} + key={mainImage.imageSizes.medium} className={styles.mainImageContainer} initial={{ opacity: 0, x: 300 }} animate={{ opacity: 1, x: 0 }} @@ -72,10 +74,10 @@ export default function Gallery({ transition={{ duration: 0.3 }} > <Image - src={mainImage.url} - alt={mainImage.alt} - layout="fill" - objectFit="cover" + src={mainImage.imageSizes.medium} + alt={mainImage.metaData.altText} + fill + className={styles.image} onClick={onImageClick} /> </motion.div> @@ -100,7 +102,7 @@ export default function Gallery({ <AnimatePresence initial={false}> {getThumbImages().map((image, index) => ( <motion.div - key={image.url} + key={image.imageSizes.tiny} className={styles.thumbnailContainer} onClick={() => onSelectImage(image)} initial={{ opacity: 0, x: 50 }} @@ -109,10 +111,10 @@ export default function Gallery({ transition={{ duration: 0.2, delay: index * 0.05 }} > <Image - src={image.url} - alt={image.alt} - layout="fill" - objectFit="cover" + src={image.imageSizes.tiny} + alt={image.metaData.altText} + fill + className={styles.image} /> </motion.div> ))} @@ -139,7 +141,7 @@ export default function Gallery({ <div className={styles.thumbnailGrid}> {images.map((image, index) => ( <motion.div - key={image.url} + key={image.imageSizes.small} className={`${styles.thumbnailContainer} ${index % 3 === 0 ? styles.fullWidthImage : ""}`} onClick={() => { onSelectImage(image) @@ -150,10 +152,10 @@ export default function Gallery({ transition={{ duration: 0.3, delay: index * 0.05 }} > <Image - src={image.url} - alt={image.alt} - layout="fill" - objectFit="cover" + src={image.imageSizes.small} + alt={image.metaData.altText} + fill + className={styles.image} /> </motion.div> ))} diff --git a/components/Lightbox/Lightbox.module.css b/components/Lightbox/Lightbox.module.css index ae46b5a0e..c98745601 100644 --- a/components/Lightbox/Lightbox.module.css +++ b/components/Lightbox/Lightbox.module.css @@ -184,6 +184,25 @@ max-width: 548px; } +.image { + object-fit: cover; +} + +@media (min-width: 768px) and (max-width: 1367px) { + .fullViewContainer { + grid-template-columns: 1fr; + justify-items: center; + padding: var(--Spacing-x5); + } + + .fullViewImageContainer { + position: relative; + width: 100%; + height: 100%; + max-height: 35rem; + } +} + @media (min-width: 1367px) { .mobileGallery, .thumbnailGrid { diff --git a/components/Lightbox/index.tsx b/components/Lightbox/index.tsx index f76063848..e9e5d8fa1 100644 --- a/components/Lightbox/index.tsx +++ b/components/Lightbox/index.tsx @@ -97,7 +97,7 @@ export default function Lightbox({ onClose={() => setIsOpen(false)} onSelectImage={(image) => { setSelectedImageIndex( - images.findIndex((img) => img.url === image.url) + images.findIndex((img) => img === image) ) }} onImageClick={() => setIsFullView(true)} diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 182575c2d..e4c90c5dc 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -163,30 +163,20 @@ export const facilitySchema = z.object({ ), }) +export const imageSchema = z.object({ + metaData: imageMetaDataSchema, + imageSizes: imageSizesSchema, +}) + export const gallerySchema = z.object({ - heroImages: z.array( - z.object({ - metaData: imageMetaDataSchema, - imageSizes: imageSizesSchema, - }) - ), - smallerImages: z.array( - z.object({ - metaData: imageMetaDataSchema, - imageSizes: imageSizesSchema, - }) - ), + heroImages: z.array(imageSchema), + smallerImages: z.array(imageSchema), }) const healthFacilitySchema = z.object({ type: z.string(), content: z.object({ - images: z.array( - z.object({ - metaData: imageMetaDataSchema, - imageSizes: imageSizesSchema, - }) - ), + images: z.array(imageSchema), texts: z.object({ facilityInformation: z.string().optional(), surroundingInformation: z.string().optional(), diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 883b3a3ee..c7a1da08f 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -9,7 +9,6 @@ import { notFound, serverErrorByStatus, } from "@/server/errors/trpc" -import { extractHotelImages } from "@/server/routers/utils/hotels" import { contentStackUidWithServiceProcedure, publicProcedure, @@ -230,7 +229,7 @@ export const hotelQueryRouter = router({ const included = validatedHotelData.data.included || [] const hotelAttributes = validatedHotelData.data.data.attributes - const images = extractHotelImages(hotelAttributes) + const images = hotelAttributes.gallery?.smallerImages const hotelAlerts = hotelAttributes.meta?.specialAlerts || [] const roomCategories = included diff --git a/server/routers/utils/hotels.ts b/server/routers/utils/hotels.ts deleted file mode 100644 index 37ae167cb..000000000 --- a/server/routers/utils/hotels.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { ImageItem } from "@/types/components/lightbox/lightbox" -import type { Hotel } from "@/types/hotel" - -export function extractHotelImages(hotelData: Hotel): ImageItem[] { - const images: ImageItem[] = [] - - if (hotelData.hotelContent?.images) { - images.push({ - url: hotelData.hotelContent.images.imageSizes.large, - alt: hotelData.hotelContent.images.metaData.altText, - title: - hotelData.hotelContent.images.metaData.title || - hotelData.hotelContent.images.metaData.altText, - }) - } - - if (hotelData.healthFacilities) { - hotelData.healthFacilities.forEach((facility) => { - facility.content.images.forEach((image) => { - images.push({ - url: image.imageSizes.large, - alt: image.metaData.altText, - title: image.metaData.title || image.metaData.altText, - }) - }) - }) - } - - return images -} diff --git a/types/components/hotelPage/previewImages.ts b/types/components/hotelPage/previewImages.ts index 204a23fd4..d808fdcf7 100644 --- a/types/components/hotelPage/previewImages.ts +++ b/types/components/hotelPage/previewImages.ts @@ -1,6 +1,6 @@ -import type { ImageItem } from "@/types/components/lightbox/lightbox" +import type { GalleryImage } from "@/types/hotel" export type PreviewImagesProps = { - images: ImageItem[] + images: GalleryImage[] hotelName: string } diff --git a/types/components/hotelReservation/selectRate/imageGallery.ts b/types/components/hotelReservation/selectRate/imageGallery.ts index 333ff2d94..5d75189fa 100644 --- a/types/components/hotelReservation/selectRate/imageGallery.ts +++ b/types/components/hotelReservation/selectRate/imageGallery.ts @@ -1,3 +1,3 @@ -import type { GalleryImages } from "@/types/hotel" +import type { GalleryImage } from "@/types/hotel" -export type ImageGalleryProps = { images: GalleryImages; title: string } +export type ImageGalleryProps = { images: GalleryImage[]; title: string } diff --git a/types/components/lightbox/lightbox.ts b/types/components/lightbox/lightbox.ts index fc8d11066..af592bca6 100644 --- a/types/components/lightbox/lightbox.ts +++ b/types/components/lightbox/lightbox.ts @@ -1,26 +1,22 @@ -export interface ImageItem { - url: string - alt: string - title: string -} +import type { GalleryImage } from "@/types/hotel" export interface LightboxProps { - images: ImageItem[] + images: GalleryImage[] dialogTitle: string /* Accessible title for dialog screen readers */ children: React.ReactNode } export interface GalleryProps { - images: ImageItem[] + images: GalleryImage[] dialogTitle: string onClose: () => void - onSelectImage: (image: ImageItem) => void + onSelectImage: (image: GalleryImage) => void onImageClick: () => void - selectedImage: ImageItem | null + selectedImage: GalleryImage | null } export interface FullViewProps { - image: ImageItem + image: GalleryImage onClose: () => void onNext: () => void onPrev: () => void diff --git a/types/hotel.ts b/types/hotel.ts index 972c6459e..9c95a9067 100644 --- a/types/hotel.ts +++ b/types/hotel.ts @@ -2,8 +2,8 @@ import { z } from "zod" import { facilitySchema, - gallerySchema, getHotelDataSchema, + imageSchema, parkingSchema, pointOfInterestSchema, } from "@/server/routers/hotels/output" @@ -22,8 +22,7 @@ export type HotelTripAdvisor = | undefined export type RoomData = z.infer<typeof roomSchema> -export type GallerySchema = z.infer<typeof gallerySchema> -export type GalleryImages = GallerySchema["heroImages"] +export type GalleryImage = z.infer<typeof imageSchema> export type PointOfInterest = z.output<typeof pointOfInterestSchema> From 62f549e85d44ab6251dfd09e8007eddf4904debb Mon Sep 17 00:00:00 2001 From: Simon Emanuelsson <simon.emanuelsson@scandichotels.com> Date: Mon, 28 Oct 2024 10:12:03 +0100 Subject: [PATCH 29/73] feat: get breakfast package from API --- actions/editProfile.ts | 2 +- actions/registerUser.ts | 2 +- actions/registerUserBookingFlow.ts | 2 +- .../[step]/@hotelHeader/[...paths]/page.tsx | 1 + .../[step]/@hotelHeader/loading.tsx | 5 + .../(standard)/[step]/@hotelHeader/page.tsx | 22 ++ .../[step]/@sidePeek/[...paths]/page.tsx | 1 + .../(standard)/[step]/@sidePeek/loading.tsx | 5 + .../(standard)/[step]/@sidePeek/page.tsx | 21 ++ .../(standard)/[step]/layout.tsx | 40 ++-- .../(standard)/[step]/page.tsx | 23 +- .../EnterDetails/BedType/index.tsx | 10 +- .../EnterDetails/BedType/schema.ts | 4 +- .../EnterDetails/Breakfast/index.tsx | 92 +++++--- .../EnterDetails/Breakfast/schema.ts | 14 +- .../Form/ChoiceCard/_Card/card.module.css | 4 + .../Form/ChoiceCard/_Card/card.ts | 9 + .../Form/ChoiceCard/_Card/index.tsx | 105 +++++---- constants/booking.ts | 15 +- 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 +- lib/api/endpoints.ts | 205 ++++++++++++++++-- lib/api/index.ts | 2 +- lib/trpc/memoizedRequests/index.ts | 6 + lib/trpc/server.ts | 1 - server/context.ts | 5 +- server/routers/booking/mutation.ts | 2 +- server/routers/booking/query.ts | 4 +- server/routers/contentstack/reward/query.ts | 6 +- server/routers/hotels/input.ts | 4 + server/routers/hotels/output.ts | 31 ++- server/routers/hotels/query.ts | 132 ++++++++++- server/routers/hotels/utils.ts | 6 +- server/routers/user/mutation.ts | 31 +-- server/routers/user/query.ts | 35 +-- server/routers/user/utils.ts | 2 +- server/trpc.ts | 5 +- stores/enter-details.ts | 15 +- types/components/enterDetails/breakfast.ts | 20 +- types/enums/bedType.ts | 2 +- types/enums/breakfast.ts | 5 +- types/enums/currency.ts | 7 + types/enums/packages.ts | 7 + 47 files changed, 718 insertions(+), 210 deletions(-) create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/[...paths]/page.tsx create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/loading.tsx create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/[...paths]/page.tsx create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/loading.tsx create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/page.tsx create mode 100644 types/enums/currency.ts create mode 100644 types/enums/packages.ts diff --git a/actions/editProfile.ts b/actions/editProfile.ts index 8f0791c48..acf5d1b12 100644 --- a/actions/editProfile.ts +++ b/actions/editProfile.ts @@ -148,7 +148,7 @@ export const editProfile = protectedServerActionProcedure ) } - const apiResponse = await api.patch(api.endpoints.v1.profile, { + const apiResponse = await api.patch(api.endpoints.v1.Profile.profile, { body, cache: "no-store", headers: { diff --git a/actions/registerUser.ts b/actions/registerUser.ts index e65fc357f..ecd2318b4 100644 --- a/actions/registerUser.ts +++ b/actions/registerUser.ts @@ -55,7 +55,7 @@ export const registerUser = serviceServerActionProcedure let apiResponse try { - apiResponse = await api.post(api.endpoints.v1.profile, { + apiResponse = await api.post(api.endpoints.v1.Profile.profile, { body: parsedPayload.data, headers: { Authorization: `Bearer ${ctx.serviceToken}`, diff --git a/actions/registerUserBookingFlow.ts b/actions/registerUserBookingFlow.ts index a0539d351..a34cad231 100644 --- a/actions/registerUserBookingFlow.ts +++ b/actions/registerUserBookingFlow.ts @@ -33,7 +33,7 @@ export const registerUserBookingFlow = serviceServerActionProcedure // TODO: Consume the API to register the user as soon as passwordless signup is enabled. // let apiResponse // try { - // apiResponse = await api.post(api.endpoints.v1.profile, { + // apiResponse = await api.post(api.endpoints.v1.Profile.profile, { // body: payload, // headers: { // Authorization: `Bearer ${ctx.serviceToken}`, diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/[...paths]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/[...paths]/page.tsx new file mode 100644 index 000000000..03a82e5f5 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/[...paths]/page.tsx @@ -0,0 +1 @@ +export { default } from "../page" diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/loading.tsx new file mode 100644 index 000000000..0fad268cc --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/loading.tsx @@ -0,0 +1,5 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function LoadingHotelHeader() { + return <LoadingSpinner /> +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx new file mode 100644 index 000000000..58a216006 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx @@ -0,0 +1,22 @@ +import { redirect } from "next/navigation" + +import { getHotelData } from "@/lib/trpc/memoizedRequests" + +import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader" + +import type { LangParams, PageArgs } from "@/types/params" + +export default async function HotelHeader({ + params, + searchParams, +}: PageArgs<LangParams, { hotel: string }>) { + const home = `/${params.lang}` + if (!searchParams.hotel) { + redirect(home) + } + const hotel = await getHotelData(searchParams.hotel, params.lang) + if (!hotel?.data) { + redirect(home) + } + return <HotelSelectionHeader hotel={hotel.data.attributes} /> +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/[...paths]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/[...paths]/page.tsx new file mode 100644 index 000000000..03a82e5f5 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/[...paths]/page.tsx @@ -0,0 +1 @@ +export { default } from "../page" diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/loading.tsx new file mode 100644 index 000000000..67515d4f5 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/loading.tsx @@ -0,0 +1,5 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function LoadingHotelSidePeek() { + return <LoadingSpinner /> +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/page.tsx new file mode 100644 index 000000000..13b770699 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/page.tsx @@ -0,0 +1,21 @@ +import { redirect } from "next/navigation" + +import { getHotelData } from "@/lib/trpc/memoizedRequests" + +import SidePeek from "@/components/HotelReservation/EnterDetails/SidePeek" + +import type { LangParams, PageArgs } from "@/types/params" + +export default async function HotelSidePeek({ + params, + searchParams, +}: PageArgs<LangParams, { hotel: string }>) { + if (!searchParams.hotel) { + redirect(`/${params.lang}`) + } + const hotel = await getHotelData(searchParams.hotel, params.lang) + if (!hotel?.data) { + redirect(`/${params.lang}`) + } + return <SidePeek hotel={hotel.data.attributes} /> +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx index 0e8edd50e..271d19e6d 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx @@ -1,46 +1,32 @@ -import { redirect } from "next/navigation" - -import { - getCreditCardsSafely, - getHotelData, - getProfileSafely, -} from "@/lib/trpc/memoizedRequests" - import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider" import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom" -import SidePeek from "@/components/HotelReservation/EnterDetails/SidePeek" import Summary from "@/components/HotelReservation/EnterDetails/Summary" -import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader" import { setLang } from "@/i18n/serverContext" +import { preload } from "./page" + import styles from "./layout.module.css" import { StepEnum } from "@/types/components/enterDetails/step" import type { LangParams, LayoutArgs } from "@/types/params" -function preload(id: string, lang: string) { - void getHotelData(id, lang) - void getProfileSafely() - void getCreditCardsSafely() -} - export default async function StepLayout({ children, + hotelHeader, params, -}: React.PropsWithChildren<LayoutArgs<LangParams & { step: StepEnum }>>) { - setLang(params.lang) - preload("811", params.lang) - - const hotel = await getHotelData("811", params.lang) - - if (!hotel?.data) { - redirect(`/${params.lang}`) + sidePeek, +}: React.PropsWithChildren< + LayoutArgs<LangParams & { step: StepEnum }> & { + hotelHeader: React.ReactNode + sidePeek: React.ReactNode } - +>) { + setLang(params.lang) + preload() return ( <EnterDetailsProvider step={params.step}> <main className={styles.layout}> - <HotelSelectionHeader hotel={hotel.data.attributes} /> + {hotelHeader} <div className={styles.content}> <SelectedRoom /> {children} @@ -48,7 +34,7 @@ export default async function StepLayout({ <Summary /> </aside> </div> - <SidePeek hotel={hotel.data.attributes} /> + {sidePeek} </main> </EnterDetailsProvider> ) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 8e8d3c891..77ec20203 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -1,6 +1,7 @@ -import { notFound } from "next/navigation" +import { notFound, redirect } from "next/navigation" import { + getBreakfastPackages, getCreditCardsSafely, getHotelData, getProfileSafely, @@ -17,22 +18,32 @@ import { getIntl } from "@/i18n" import { StepEnum } from "@/types/components/enterDetails/step" import type { LangParams, PageArgs } from "@/types/params" +export function preload() { + void getProfileSafely() + void getCreditCardsSafely() +} + function isValidStep(step: string): step is StepEnum { return Object.values(StepEnum).includes(step as StepEnum) } export default async function StepPage({ params, -}: PageArgs<LangParams & { step: StepEnum }>) { - const { step, lang } = params + searchParams, +}: PageArgs<LangParams & { step: StepEnum }, { hotel: string }>) { + if (!searchParams.hotel) { + redirect(`/${params.lang}`) + } + void getBreakfastPackages(searchParams.hotel) const intl = await getIntl() - const hotel = await getHotelData("811", lang) + const hotel = await getHotelData(searchParams.hotel, params.lang) const user = await getProfileSafely() const savedCreditCards = await getCreditCardsSafely() + const breakfastPackages = await getBreakfastPackages(searchParams.hotel) - if (!isValidStep(step) || !hotel) { + if (!isValidStep(params.step) || !hotel) { return notFound() } @@ -51,7 +62,7 @@ export default async function StepPage({ step={StepEnum.breakfast} label={intl.formatMessage({ id: "Select breakfast options" })} > - <Breakfast /> + <Breakfast packages={breakfastPackages} /> </SectionAccordion> <SectionAccordion header="Details" diff --git a/components/HotelReservation/EnterDetails/BedType/index.tsx b/components/HotelReservation/EnterDetails/BedType/index.tsx index 106aaa80a..e73eacf87 100644 --- a/components/HotelReservation/EnterDetails/BedType/index.tsx +++ b/components/HotelReservation/EnterDetails/BedType/index.tsx @@ -15,7 +15,7 @@ import { bedTypeSchema } from "./schema" import styles from "./bedOptions.module.css" import type { BedTypeSchema } from "@/types/components/enterDetails/bedType" -import { bedTypeEnum } from "@/types/enums/bedType" +import { BedTypeEnum } from "@/types/enums/bedType" export default function BedType() { const intl = useIntl() @@ -61,7 +61,7 @@ export default function BedType() { <RadioCard Icon={KingBedIcon} iconWidth={46} - id={bedTypeEnum.KING} + id={BedTypeEnum.KING} name="bedType" subtitle={intl.formatMessage( { id: "{width} cm × {length} cm" }, @@ -72,12 +72,12 @@ export default function BedType() { )} text={text} title={intl.formatMessage({ id: "King bed" })} - value={bedTypeEnum.KING} + value={BedTypeEnum.KING} /> <RadioCard Icon={KingBedIcon} iconWidth={46} - id={bedTypeEnum.QUEEN} + id={BedTypeEnum.QUEEN} name="bedType" subtitle={intl.formatMessage( { id: "{width} cm × {length} cm" }, @@ -88,7 +88,7 @@ export default function BedType() { )} text={text} title={intl.formatMessage({ id: "Queen bed" })} - value={bedTypeEnum.QUEEN} + value={BedTypeEnum.QUEEN} /> </form> </FormProvider> diff --git a/components/HotelReservation/EnterDetails/BedType/schema.ts b/components/HotelReservation/EnterDetails/BedType/schema.ts index d9f52a407..8f77ba768 100644 --- a/components/HotelReservation/EnterDetails/BedType/schema.ts +++ b/components/HotelReservation/EnterDetails/BedType/schema.ts @@ -1,7 +1,7 @@ import { z } from "zod" -import { bedTypeEnum } from "@/types/enums/bedType" +import { BedTypeEnum } from "@/types/enums/bedType" export const bedTypeSchema = z.object({ - bedType: z.nativeEnum(bedTypeEnum), + bedType: z.nativeEnum(BedTypeEnum), }) diff --git a/components/HotelReservation/EnterDetails/Breakfast/index.tsx b/components/HotelReservation/EnterDetails/Breakfast/index.tsx index c2ea00754..603a3aaad 100644 --- a/components/HotelReservation/EnterDetails/Breakfast/index.tsx +++ b/components/HotelReservation/EnterDetails/Breakfast/index.tsx @@ -7,36 +7,50 @@ import { useIntl } from "react-intl" import { useEnterDetailsStore } from "@/stores/enter-details" -import { BreakfastIcon, NoBreakfastIcon } from "@/components/Icons" +import { Highlight } from "@/components/TempDesignSystem/Form/ChoiceCard/_Card" import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio" -import { breakfastSchema } from "./schema" +import { breakfastFormSchema } from "./schema" import styles from "./breakfast.module.css" -import type { BreakfastSchema } from "@/types/components/enterDetails/breakfast" -import { breakfastEnum } from "@/types/enums/breakfast" +import type { + BreakfastFormSchema, + BreakfastProps, +} from "@/types/components/enterDetails/breakfast" +import { BreakfastPackageEnum } from "@/types/enums/breakfast" -export default function Breakfast() { +export default function Breakfast({ packages }: BreakfastProps) { const intl = useIntl() const breakfast = useEnterDetailsStore((state) => state.data.breakfast) - const methods = useForm<BreakfastSchema>({ - defaultValues: breakfast ? { breakfast } : undefined, + let defaultValues = undefined + if (breakfast === BreakfastPackageEnum.NO_BREAKFAST) { + defaultValues = { breakfast: BreakfastPackageEnum.NO_BREAKFAST } + } else if (breakfast?.code) { + defaultValues = { breakfast: breakfast.code } + } + const methods = useForm<BreakfastFormSchema>({ + defaultValues, criteriaMode: "all", mode: "all", - resolver: zodResolver(breakfastSchema), + resolver: zodResolver(breakfastFormSchema), reValidateMode: "onChange", }) const completeStep = useEnterDetailsStore((state) => state.completeStep) const onSubmit = useCallback( - (values: BreakfastSchema) => { - completeStep(values) + (values: BreakfastFormSchema) => { + const pkg = packages?.find((p) => p.code === values.breakfast) + if (pkg) { + completeStep({ breakfast: pkg }) + } else { + completeStep({ breakfast: BreakfastPackageEnum.NO_BREAKFAST }) + } }, - [completeStep] + [completeStep, packages] ) useEffect(() => { @@ -47,30 +61,46 @@ export default function Breakfast() { return () => subscription.unsubscribe() }, [methods, onSubmit]) + if (!packages) { + return null + } + return ( <FormProvider {...methods}> <form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}> - <RadioCard - Icon={BreakfastIcon} - id={breakfastEnum.BREAKFAST} - name="breakfast" - subtitle={intl.formatMessage<React.ReactNode>( - { id: "<b>{amount} {currency}</b>/night per adult" }, - { - amount: "150", - b: (str) => <b>{str}</b>, - currency: "SEK", + {packages.map((pkg) => ( + <RadioCard + key={pkg.code} + id={pkg.code} + name="breakfast" + subtitle={ + pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST + ? intl.formatMessage<React.ReactNode>( + { id: "breakfast.price.free" }, + { + amount: pkg.originalPrice, + currency: pkg.currency, + free: (str) => <Highlight>{str}</Highlight>, + strikethrough: (str) => <s>{str}</s>, + } + ) + : intl.formatMessage( + { id: "breakfast.price" }, + { + amount: pkg.packagePrice, + currency: pkg.currency, + } + ) } - )} - text={intl.formatMessage({ - id: "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.", - })} - title={intl.formatMessage({ id: "Breakfast buffet" })} - value={breakfastEnum.BREAKFAST} - /> + text={intl.formatMessage({ + id: "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.", + })} + title={intl.formatMessage({ id: "Breakfast buffet" })} + value={pkg.code} + /> + ))} <RadioCard - Icon={NoBreakfastIcon} - id={breakfastEnum.NO_BREAKFAST} + id={BreakfastPackageEnum.NO_BREAKFAST} name="breakfast" subtitle={intl.formatMessage( { id: "{amount} {currency}" }, @@ -83,7 +113,7 @@ export default function Breakfast() { id: "You can always change your mind later and add breakfast at the hotel.", })} title={intl.formatMessage({ id: "No breakfast" })} - value={breakfastEnum.NO_BREAKFAST} + value={BreakfastPackageEnum.NO_BREAKFAST} /> </form> </FormProvider> diff --git a/components/HotelReservation/EnterDetails/Breakfast/schema.ts b/components/HotelReservation/EnterDetails/Breakfast/schema.ts index 34cc5efca..5f8c1f354 100644 --- a/components/HotelReservation/EnterDetails/Breakfast/schema.ts +++ b/components/HotelReservation/EnterDetails/Breakfast/schema.ts @@ -1,7 +1,15 @@ import { z } from "zod" -import { breakfastEnum } from "@/types/enums/breakfast" +import { breakfastPackageSchema } from "@/server/routers/hotels/output" -export const breakfastSchema = z.object({ - breakfast: z.nativeEnum(breakfastEnum), +import { BreakfastPackageEnum } from "@/types/enums/breakfast" + +export const breakfastStoreSchema = z.object({ + breakfast: breakfastPackageSchema.or( + z.literal(BreakfastPackageEnum.NO_BREAKFAST) + ), +}) + +export const breakfastFormSchema = z.object({ + breakfast: z.string().or(z.literal(BreakfastPackageEnum.NO_BREAKFAST)), }) diff --git a/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css index 1044596f6..fa7d6d13a 100644 --- a/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css +++ b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css @@ -70,3 +70,7 @@ .listItem:nth-of-type(n + 2) { margin-top: var(--Spacing-x-quarter); } + +.highlight { + color: var(--Scandic-Brand-Scandic-Red); +} diff --git a/components/TempDesignSystem/Form/ChoiceCard/_Card/card.ts b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.ts index d1a961150..145116409 100644 --- a/components/TempDesignSystem/Form/ChoiceCard/_Card/card.ts +++ b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.ts @@ -34,3 +34,12 @@ export type CheckboxProps = export type RadioProps = | Omit<ListCardProps, "type"> | Omit<TextCardProps, "type"> + +export interface ListProps extends Pick<ListCardProps, "declined"> { + list?: ListCardProps["list"] +} + +export interface SubtitleProps + extends Pick<BaseCardProps, "highlightSubtitle" | "subtitle"> {} + +export interface TextProps extends Pick<TextCardProps, "text"> {} diff --git a/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx b/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx index c57d56cc4..2a3faf57b 100644 --- a/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx +++ b/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx @@ -2,16 +2,16 @@ import { useFormContext } from "react-hook-form" -import { CheckIcon, CloseIcon, HeartIcon } from "@/components/Icons" +import { CheckIcon, CloseIcon } from "@/components/Icons" import Caption from "@/components/TempDesignSystem/Text/Caption" import Footnote from "@/components/TempDesignSystem/Text/Footnote" import styles from "./card.module.css" -import type { CardProps } from "./card" +import type { CardProps, ListProps, SubtitleProps, TextProps } from "./card" export default function Card({ - Icon = HeartIcon, + Icon, iconHeight = 32, iconWidth = 32, declined = false, @@ -26,56 +26,79 @@ export default function Card({ value, }: CardProps) { const { register } = useFormContext() - return ( <label className={styles.label} data-declined={declined} tabIndex={0}> - <Caption className={styles.title} type="label" uppercase> + <Caption className={styles.title} color="burgundy" type="label" uppercase> {title} </Caption> - {subtitle ? ( - <Caption - className={styles.subtitle} - color={highlightSubtitle ? "baseTextAccent" : "uiTextHighContrast"} - type="regular" - > - {subtitle} - </Caption> - ) : null} - <Icon - className={styles.icon} - color="uiTextHighContrast" - height={iconHeight} - width={iconWidth} - /> - {list - ? list.map((listItem) => ( - <span key={listItem.title} className={styles.listItem}> - {declined ? ( - <CloseIcon - color="uiTextMediumContrast" - height={20} - width={20} - /> - ) : ( - <CheckIcon color="baseIconLowContrast" height={20} width={20} /> - )} - <Footnote color="uiTextMediumContrast">{listItem.title}</Footnote> - </span> - )) - : null} - {text ? ( - <Footnote className={styles.text} color="uiTextMediumContrast"> - {text} - </Footnote> + <Subtitle highlightSubtitle={highlightSubtitle} subtitle={subtitle} /> + {Icon ? ( + <Icon + className={styles.icon} + color="uiTextHighContrast" + height={iconHeight} + width={iconWidth} + /> ) : null} + <List declined={declined} list={list} /> + <Text text={text} /> <input + {...register(name)} aria-hidden id={id || name} hidden type={type} value={value} - {...register(name)} /> </label> ) } + +function List({ declined, list }: ListProps) { + if (!list) { + return null + } + + return list.map((listItem) => ( + <span key={listItem.title} className={styles.listItem}> + {declined ? ( + <CloseIcon color="uiTextMediumContrast" height={20} width={20} /> + ) : ( + <CheckIcon color="baseIconLowContrast" height={20} width={20} /> + )} + <Footnote color="uiTextMediumContrast">{listItem.title}</Footnote> + </span> + )) +} + +function Subtitle({ highlightSubtitle, subtitle }: SubtitleProps) { + if (!subtitle) { + return null + } + + return ( + <Caption + className={styles.subtitle} + color={highlightSubtitle ? "baseTextAccent" : "uiTextMediumContrast"} + type="label" + uppercase + > + {subtitle} + </Caption> + ) +} + +function Text({ text }: TextProps) { + if (!text) { + return null + } + return ( + <Footnote className={styles.text} color="uiTextMediumContrast"> + {text} + </Footnote> + ) +} + +export function Highlight({ children }: React.PropsWithChildren) { + return <span className={styles.highlight}>{children}</span> +} diff --git a/constants/booking.ts b/constants/booking.ts index 8f5acb120..da6b30695 100644 --- a/constants/booking.ts +++ b/constants/booking.ts @@ -1,7 +1,18 @@ export enum BookingStatusEnum { - CreatedInOhip = "CreatedInOhip", - PaymentRegistered = "PaymentRegistered", BookingCompleted = "BookingCompleted", + Cancelled = "Cancelled", + CheckedOut = "CheckedOut", + ConfirmedInScorpio = "ConfirmedInScorpio", + CreatedInOhip = "CreatedInOhip", + PaymentAuthorized = "PaymentAuthorized", + PaymentCancelled = "PaymentCancelled", + PaymentError = "PaymentError", + PaymentFailed = "PaymentFailed", + PaymentRegistered = "PaymentRegistered", + PaymentSucceeded = "PaymentSucceeded", + PendingAcceptPriceChange = "PendingAcceptPriceChange", + PendingGuarantee = "PendingGuarantee", + PendingPayment = "PendingPayment", } export enum BedTypeEnum { diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index e47484466..c66448f17 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -1,6 +1,5 @@ { "<b>Included</b> (based on availability)": "<b>Inkluderet</b> (baseret på tilgængelighed)", - "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/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", @@ -366,6 +365,8 @@ "booking.rooms": "{totalRooms, plural, one {# værelse} other {# værelser}}", "booking.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle <termsLink>Vilkår og betingelser</termsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyLink>Scandics Privatlivspolitik</privacyLink>. 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", + "breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/nat", "by": "inden", "characters": "tegn", "guest": "gæst", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 88a5092e5..1959939ee 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -1,6 +1,5 @@ { "<b>Included</b> (based on availability)": "<b>Inbegriffen</b> (je nach Verfügbarkeit)", - "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/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", @@ -367,6 +366,8 @@ "booking.rooms": "{totalRooms, plural, one {# zimmer} other {# räume}}", "booking.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle <termsLink>Vilkår og betingelser</termsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyLink>Scandics Privatlivspolitik</privacyLink>. 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", + "breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/Nacht", "by": "bis", "characters": "figuren", "guest": "gast", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 75fbbd805..2fa6793d7 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -1,6 +1,5 @@ { "<b>Included</b> (based on availability)": "<b>Included</b> (based on availability)", - "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/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", @@ -385,6 +384,8 @@ "booking.rooms": "{totalRooms, plural, one {# room} other {# rooms}}", "booking.terms": "By paying with any of the payment methods available, I accept the terms for this booking and the general <termsLink>Terms & Conditions</termsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyLink>Scandic's Privacy policy</privacyLink>. 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", + "breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/night", "by": "by", "characters": "characters", "from": "from", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 6f2f28b63..883b9d277 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -1,6 +1,5 @@ { "<b>Included</b> (based on availability)": "<b>Sisältyy</b> (saatavuuden mukaan)", - "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/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", @@ -367,6 +366,8 @@ "booking.rooms": "{totalRooms, plural, one {# huone} other {# sviitti}}", "booking.terms": "Maksamalla minkä tahansa saatavilla olevan maksutavan avulla hyväksyn tämän varauksen ehdot ja yleiset <termsLink>ehdot ja ehtoja</termsLink>, ja ymmärrän, että Scandic käsittelee minun henkilötietoni tässä varauksessa mukaisesti <privacyLink>Scandicin tietosuojavaltuuden</privacyLink> 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ö", + "breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/yö", "by": "mennessä", "characters": "hahmoja", "guest": "Vieras", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 0b45a4673..075f84783 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -1,6 +1,5 @@ { "<b>Included</b> (based on availability)": "<b>Inkludert</b> (basert på tilgjengelighet)", - "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/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", @@ -363,6 +362,8 @@ "booking.nights": "{totalNights, plural, one {# natt} other {# netter}}", "booking.rooms": "{totalRooms, plural, one {# rom} other {# rom}}", "booking.thisRoomIsEquippedWith": "Dette rommet er utstyrt med", + "breakfast.price": "{amount} {currency}/natt", + "breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/natt", "by": "innen", "characters": "tegn", "guest": "gjest", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 406aa4cb4..0b48022d3 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -1,6 +1,5 @@ { "<b>Included</b> (based on availability)": "<b>Ingår</b> (baserat på tillgänglighet)", - "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/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", @@ -364,6 +363,8 @@ "booking.rooms": "{totalRooms, plural, one {# rum} other {# rum}}", "booking.terms": "Genom att betala med någon av de tillgängliga betalningsmetoderna accepterar jag villkoren för denna bokning och de generella <termsLink>Villkoren och villkoren</termsLink>, och förstår att Scandic kommer att behandla min personliga data i samband med denna bokning i enlighet med <privacyLink>Scandics integritetspolicy</privacyLink>. 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", + "breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/natt", "by": "innan", "characters": "tecken", "guest": "gäst", diff --git a/lib/api/endpoints.ts b/lib/api/endpoints.ts index be1aee2fd..100fb7518 100644 --- a/lib/api/endpoints.ts +++ b/lib/api/endpoints.ts @@ -2,29 +2,190 @@ * Nested enum requires namespace */ export namespace endpoints { - export const enum v0 { - profile = "profile/v0/Profile", + namespace base { + export const enum path { + availability = "availability", + booking = "booking", + hotel = "hotel", + package = "package", + profile = "profile", + } + + export const enum enitity { + Ancillary = "Ancillary", + Availabilities = "availabilities", + Bookings = "Bookings", + Breakfast = "breakfast", + Cities = "Cities", + Countries = "Countries", + Hotels = "Hotels", + Locations = "Locations", + Packages = "packages", + Profile = "Profile", + Reward = "Reward", + Stays = "Stays", + Transaction = "Transaction", + } } - export const enum v1 { - hotelsAvailability = "availability/v1/availabilities/city", - roomsAvailability = "availability/v1/availabilities/hotel", - profile = "profile/v1/Profile", - booking = "booking/v1/Bookings", - creditCards = `${profile}/creditCards`, - city = "hotel/v1/Cities", - citiesCountry = `${city}/country`, - countries = "hotel/v1/Countries", - friendTransactions = "profile/v1/Transaction/friendTransactions", - hotels = "hotel/v1/Hotels", - initiateSaveCard = `${creditCards}/initiateSaveCard`, - locations = "hotel/v1/Locations", - previousStays = "booking/v1/Stays/past", - upcomingStays = "booking/v1/Stays/future", - rewards = `${profile}/reward`, - tierRewards = `${profile}/TierRewards`, - subscriberId = `${profile}/SubscriberId`, - packages = "package/v1/packages/hotel", + + export namespace v1 { + const version = "v1" + /** + * availability (Swagger) + * https://tstapi.scandichotels.com/availability/swagger/v1/index.html + */ + export namespace Availability { + export function city(cityId: string) { + return `${base.path.availability}/${version}/${base.enitity.Availabilities}/city/${cityId}` + } + export function hotel(hotelId: string) { + return `${base.path.availability}/${version}/${base.enitity.Availabilities}/hotel/${hotelId}` + } + } + + /** + * booking (Swagger) + * https://tstapi.scandichotels.com/booking/swagger/v1/index.html + */ + export namespace Booking { + export const bookings = `${base.path.booking}/${version}/${base.enitity.Bookings}` + + export function booking(confirmationNumber: string) { + return `${bookings}/${confirmationNumber}` + } + export function cancel(confirmationNumber: string) { + return `${bookings}/${confirmationNumber}/cancel` + } + export function status(confirmationNumber: string) { + return `${bookings}/${confirmationNumber}/status` + } + + export const enum Stays { + future = `${base.path.booking}/${version}/${base.enitity.Stays}/future`, + past = `${base.path.booking}/${version}/${base.enitity.Stays}/past`, + } + } + + /** + * hotel (Swagger) + * https://tstapi.scandichotels.com/hotel/swagger/v1/index.html + */ + export namespace Hotel { + export const cities = `${base.path.hotel}/${version}/${base.enitity.Cities}` + export namespace Cities { + export function city(cityId: string) { + return `${cities}/${cityId}` + } + export function country(countryId: string) { + return `${cities}/country/${countryId}` + } + export function hotel(hotelId: string) { + return `${cities}/hotel/${hotelId}` + } + } + + export const countries = `${base.path.hotel}/${version}/${base.enitity.Countries}` + export namespace Countries { + export function country(countryId: string) { + return `${countries}/${countryId}` + } + } + + export const hotels = `${base.path.hotel}/${version}/${base.enitity.Hotels}` + export namespace Hotels { + export function hotel(hotelId: string) { + return `${hotels}/${hotelId}` + } + export function meetingRooms(hotelId: string) { + return `${hotels}/${hotelId}/meetingRooms` + } + export function merchantInformation(hotelId: string) { + return `${hotels}/${hotelId}/merchantInformation` + } + export function nearbyHotels(hotelId: string) { + return `${hotels}/${hotelId}/nearbyHotels` + } + export function restaurants(hotelId: string) { + return `${hotels}/${hotelId}/restaurants` + } + export function roomCategories(hotelId: string) { + return `${hotels}/${hotelId}/roomCategories` + } + } + + export const locations = `${base.path.hotel}/${version}/${base.enitity.Locations}` + } + + /** + * package (Swagger) + * https://tstapi.scandichotels.com/package/swagger/v1/index.html + */ + export namespace Package { + export namespace Ancillary { + export function hotel(hotelId: string) { + return `${base.path.package}/${version}/${base.enitity.Ancillary}/hotel/${hotelId}` + } + export function hotelAncillaries(hotelId: string) { + return `${base.path.package}/${version}/${base.enitity.Ancillary}/hotel/${hotelId}/ancillaries` + } + } + + export namespace Breakfast { + export function hotel(hotelId: string) { + return `${base.path.package}/${version}/${base.enitity.Breakfast}/hotel/${hotelId}` + } + } + + export namespace Packages { + export function hotel(hotelId: string) { + return `${base.path.package}/${version}/${base.enitity.Packages}/hotel/${hotelId}` + } + } + } + + /** + * profile (Swagger) + * https://tstapi.scandichotels.com/profile/swagger/v1/index.html + */ + export namespace Profile { + export const invalidateSessions = `${base.path.profile}/${version}/${base.enitity.Profile}/invalidateSessions` + export const membership = `${base.path.profile}/${version}/${base.enitity.Profile}/membership` + export const profile = `${base.path.profile}/${version}/${base.enitity.Profile}` + export const reward = `${base.path.profile}/${version}/${base.enitity.Profile}/reward` + export const subscriberId = `${base.path.profile}/${version}/${base.enitity.Profile}/SubscriberId` + export const tierRewards = `${base.path.profile}/${version}/${base.enitity.Profile}/tierRewards` + + export function deleteProfile(profileId: string) { + return `${profile}/${profileId}` + } + + export const creditCards = `${base.path.profile}/${version}/${base.enitity.Profile}/creditCards` + export namespace CreditCards { + export const initiateSaveCard = `${creditCards}/initiateSaveCard` + + export function deleteCreditCard(creditCardId: string) { + return `${creditCards}/${creditCardId}` + } + export function transaction(transactionId: string) { + return `${creditCards}/${transactionId}` + } + } + + export namespace Reward { + export const allTiers = `${base.path.profile}/${version}/${base.enitity.Reward}/AllTiers` + export const reward = `${base.path.profile}/${version}/${base.enitity.Reward}` + export const unwrap = `${base.path.profile}/${version}/${base.enitity.Reward}/Unwrap` + + export function claim(rewardId: string) { + return `${base.path.profile}/${version}/${base.enitity.Reward}/Claim/${rewardId}` + } + } + + export const enum Transaction { + friendTransactions = `${base.path.profile}/${version}/${base.enitity.Transaction}/friendTransactions`, + } + } } } -export type Endpoint = endpoints.v0 | endpoints.v1 +export type Endpoint = string diff --git a/lib/api/index.ts b/lib/api/index.ts index 475e0da4e..46bae9f88 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -28,7 +28,7 @@ const wrappedFetch = fetchRetry(fetch, { }) export async function get( - endpoint: Endpoint | `${Endpoint}/${string}`, + endpoint: Endpoint, options: RequestOptionsWithOutBody, params = {} ) { diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index d09349a8c..e34f241ce 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -89,3 +89,9 @@ export const getLanguageSwitcher = cache( export const getSiteConfig = cache(async function getMemoizedSiteConfig() { return serverClient().contentstack.base.siteConfig() }) + +export const getBreakfastPackages = cache(async function getMemoizedPackages( + hotelId: string +) { + return serverClient().hotel.packages.breakfast({ hotelId }) +}) diff --git a/lib/trpc/server.ts b/lib/trpc/server.ts index 46c0f8840..5d8539a8a 100644 --- a/lib/trpc/server.ts +++ b/lib/trpc/server.ts @@ -6,7 +6,6 @@ import { login } from "@/constants/routes/handleAuth" import { webviews } from "@/constants/routes/webviews" import { appRouter } from "@/server" import { createContext } from "@/server/context" -import { internalServerError } from "@/server/errors/next" import { createCallerFactory } from "@/server/trpc" const createCaller = createCallerFactory(appRouter) diff --git a/server/context.ts b/server/context.ts index 76ea0ba34..f33d6b1c9 100644 --- a/server/context.ts +++ b/server/context.ts @@ -1,5 +1,6 @@ import { cookies, headers } from "next/headers" import { type Session } from "next-auth" +import { cache } from "react" import { Lang } from "@/constants/languages" @@ -37,7 +38,7 @@ export function createContextInner(opts: CreateContextOptions) { * This is the actual context you'll use in your router * @link https://trpc.io/docs/context **/ -export function createContext() { +export const createContext = cache(function () { const h = headers() const cookie = cookies() @@ -66,6 +67,6 @@ export function createContext() { webToken: webviewTokenCookie?.value, contentType: h.get("x-contenttype")!, }) -} +}) export type Context = ReturnType<typeof createContext> diff --git a/server/routers/booking/mutation.ts b/server/routers/booking/mutation.ts index 9e5677d32..2edbd5bdd 100644 --- a/server/routers/booking/mutation.ts +++ b/server/routers/booking/mutation.ts @@ -62,7 +62,7 @@ export const bookingMutationRouter = router({ Authorization: `Bearer ${ctx.serviceToken}`, } - const apiResponse = await api.post(api.endpoints.v1.booking, { + const apiResponse = await api.post(api.endpoints.v1.Booking.bookings, { headers, body: input, }) diff --git a/server/routers/booking/query.ts b/server/routers/booking/query.ts index 76b874ba3..5c72bb284 100644 --- a/server/routers/booking/query.ts +++ b/server/routers/booking/query.ts @@ -33,7 +33,7 @@ export const bookingQueryRouter = router({ getBookingConfirmationCounter.add(1, { confirmationNumber }) const apiResponse = await api.get( - `${api.endpoints.v1.booking}/${confirmationNumber}`, + api.endpoints.v1.Booking.booking(confirmationNumber), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, @@ -142,7 +142,7 @@ export const bookingQueryRouter = router({ getBookingStatusCounter.add(1, { confirmationNumber }) const apiResponse = await api.get( - `${api.endpoints.v1.booking}/${confirmationNumber}/status`, + api.endpoints.v1.Booking.status(confirmationNumber), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, diff --git a/server/routers/contentstack/reward/query.ts b/server/routers/contentstack/reward/query.ts index 419e0a2c2..b4e0e54b2 100644 --- a/server/routers/contentstack/reward/query.ts +++ b/server/routers/contentstack/reward/query.ts @@ -75,7 +75,7 @@ function getUniqueRewardIds(rewardIds: string[]) { const getAllCachedApiRewards = unstable_cache( async function (token) { - const apiResponse = await api.get(api.endpoints.v1.tierRewards, { + const apiResponse = await api.get(api.endpoints.v1.Profile.tierRewards, { headers: { Authorization: `Bearer ${token}`, }, @@ -194,7 +194,7 @@ export const rewardQueryRouter = router({ const { limit, cursor } = input - const apiResponse = await api.get(api.endpoints.v1.rewards, { + const apiResponse = await api.get(api.endpoints.v1.Profile.reward, { cache: undefined, // override defaultOptions headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, @@ -393,7 +393,7 @@ export const rewardQueryRouter = router({ surprises: contentStackBaseWithProtectedProcedure.query(async ({ ctx }) => { getCurrentRewardCounter.add(1) - const apiResponse = await api.get(api.endpoints.v1.rewards, { + const apiResponse = await api.get(api.endpoints.v1.Profile.reward, { cache: undefined, // override defaultOptions headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index 6deff63a6..61927c3d2 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -39,3 +39,7 @@ export const getlHotelDataInputSchema = z.object({ .array(z.enum(["RoomCategories", "NearbyHotels", "Restaurants", "City"])) .optional(), }) + +export const getBreakfastPackageInput = z.object({ + hotelId: z.string().min(1, { message: "hotelId is required" }), +}) diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index e4c90c5dc..f8e2b8a6e 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -9,7 +9,9 @@ import { getPoiGroupByCategoryName } from "./utils" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { AlertTypeEnum } from "@/types/enums/alert" +import { CurrencyEnum } from "@/types/enums/currency" import { FacilityEnum } from "@/types/enums/facilities" +import { PackageTypeEnum } from "@/types/enums/packages" import { PointOfInterestCategoryNameEnum } from "@/types/hotel" const ratingsSchema = z @@ -653,7 +655,7 @@ export const apiCountriesSchema = z.object({ name: z.string(), }), hotelInformationSystemId: z.number().optional(), - id: z.string().optional(), + id: z.string().optional().default(""), language: z.string().optional(), type: z.literal("countries"), }) @@ -794,3 +796,30 @@ export const apiLocationsSchema = z.object({ }) ), }) + +export const breakfastPackageSchema = z.object({ + code: z.string(), + currency: z.nativeEnum(CurrencyEnum), + description: z.string(), + originalPrice: z.number().default(0), + packagePrice: z.number(), + packageType: z.enum([ + PackageTypeEnum.BreakfastAdult, + PackageTypeEnum.BreakfastChildren, + ]), + totalPrice: z.number(), +}) + +export const breakfastPackagesSchema = z + .object({ + data: z.object({ + attributes: z.object({ + hotelId: z.number(), + packages: z.array(breakfastPackageSchema), + }), + type: z.literal("breakfastpackage"), + }), + }) + .transform(({ data }) => + data.attributes.packages.filter((pkg) => pkg.code.match(/^(BRF\d+)$/gm)) + ) diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index c7a1da08f..a7b6c220e 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -13,6 +13,7 @@ import { contentStackUidWithServiceProcedure, publicProcedure, router, + safeProtectedServiceProcedure, serviceProcedure, } from "@/server/trpc" import { toApiLang } from "@/server/utils" @@ -24,11 +25,13 @@ import { getHotelPageCounter, validateHotelPageRefs, } from "../contentstack/hotelPage/utils" +import { getVerifiedUser, parsedUser } from "../user/query" import { getRoomPackagesInputSchema, getRoomPackagesSchema, } from "./schemas/packages" import { + getBreakfastPackageInput, getHotelInputSchema, getHotelsAvailabilityInputSchema, getlHotelDataInputSchema, @@ -36,6 +39,7 @@ import { getRoomsAvailabilityInputSchema, } from "./input" import { + breakfastPackagesSchema, getHotelDataSchema, getHotelsAvailabilitySchema, getRatesSchema, @@ -51,6 +55,7 @@ import { import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities" import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" +import { BreakfastPackageEnum } from "@/types/enums/breakfast" import type { RequestOptionsWithOutBody } from "@/types/fetch" import type { Facility } from "@/types/hotel" import type { GetHotelPageData } from "@/types/trpc/routers/contentstack/hotelPage" @@ -88,6 +93,14 @@ const roomsAvailabilityFailCounter = meter.createCounter( "trpc.hotel.availability.rooms-fail" ) +const breakfastPackagesCounter = meter.createCounter("trpc.package.breakfast") +const breakfastPackagesSuccessCounter = meter.createCounter( + "trpc.package.breakfast-success" +) +const breakfastPackagesFailCounter = meter.createCounter( + "trpc.package.breakfast-fail" +) + async function getContentstackData(lang: Lang, uid?: string | null) { if (!uid) { return null @@ -169,7 +182,7 @@ export const hotelQueryRouter = router({ }) ) const apiResponse = await api.get( - `${api.endpoints.v1.hotels}/${hotelId}`, + api.endpoints.v1.Hotel.Hotels.hotel(hotelId), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, @@ -320,7 +333,7 @@ export const hotelQueryRouter = router({ JSON.stringify({ query: { cityId, params } }) ) const apiResponse = await api.get( - `${api.endpoints.v1.hotelsAvailability}/${cityId}`, + api.endpoints.v1.Availability.city(cityId), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, @@ -444,7 +457,7 @@ export const hotelQueryRouter = router({ JSON.stringify({ query: { hotelId, params } }) ) const apiResponse = await api.get( - `${api.endpoints.v1.roomsAvailability}/${hotelId}`, + api.endpoints.v1.Availability.hotel(hotelId.toString()), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, @@ -587,7 +600,7 @@ export const hotelQueryRouter = router({ ) const apiResponse = await api.get( - `${api.endpoints.v1.hotels}/${hotelId}`, + api.endpoints.v1.Hotel.Hotels.hotel(hotelId), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, @@ -734,7 +747,7 @@ export const hotelQueryRouter = router({ ) const apiResponse = await api.get( - `${api.endpoints.v1.packages}/${hotelId}`, + api.endpoints.v1.Package.Packages.hotel(hotelId), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, @@ -789,5 +802,114 @@ export const hotelQueryRouter = router({ return validatedPackagesData.data }), + breakfast: safeProtectedServiceProcedure + .input(getBreakfastPackageInput) + .query(async function ({ ctx, input }) { + const params = { + Adults: 2, + EndDate: "2024-10-28", + StartDate: "2024-10-25", + } + const metricsData = { ...input, ...params } + breakfastPackagesCounter.add(1, metricsData) + console.info( + "api.package.breakfast start", + JSON.stringify({ query: metricsData }) + ) + + const apiResponse = await api.get( + api.endpoints.v1.Package.Breakfast.hotel(input.hotelId), + { + cache: undefined, + headers: { + Authorization: `Bearer ${ctx.serviceToken}`, + }, + next: { + revalidate: 60, + }, + }, + params + ) + + if (!apiResponse.ok) { + const text = await apiResponse.text() + breakfastPackagesFailCounter.add(1, { + ...metricsData, + error_type: "http_error", + error: JSON.stringify({ + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }), + }) + console.error( + "api.hotels.hotelsAvailability error", + JSON.stringify({ + query: metricsData, + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }, + }) + ) + return null + } + + const apiJson = await apiResponse.json() + const breakfastPackages = breakfastPackagesSchema.safeParse(apiJson) + if (!breakfastPackages.success) { + hotelsAvailabilityFailCounter.add(1, { + ...metricsData, + error_type: "validation_error", + error: JSON.stringify(breakfastPackages.error), + }) + console.error( + "api.package.breakfast validation error", + JSON.stringify({ + query: metricsData, + error: breakfastPackages.error, + }) + ) + return null + } + + breakfastPackagesSuccessCounter.add(1, metricsData) + console.info( + "api.package.breakfast success", + JSON.stringify({ + query: metricsData, + }) + ) + + if (ctx.session?.token) { + const apiUser = await getVerifiedUser({ session: ctx.session }) + if (apiUser && !("error" in apiUser)) { + const user = parsedUser(apiUser.data, false) + if ( + user.membership && + ["L6", "L7"].includes(user.membership.membershipLevel) + ) { + const originalBreakfastPackage = breakfastPackages.data.find( + (pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST + ) + const freeBreakfastPackage = breakfastPackages.data.find( + (pkg) => pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST + ) + if (freeBreakfastPackage) { + if (originalBreakfastPackage) { + freeBreakfastPackage.originalPrice = + originalBreakfastPackage.packagePrice + } + return [freeBreakfastPackage] + } + } + } + } + + return breakfastPackages.data.filter( + (pkg) => pkg.code !== BreakfastPackageEnum.FREE_MEMBER_BREAKFAST + ) + }), }), }) diff --git a/server/routers/hotels/utils.ts b/server/routers/hotels/utils.ts index 84020bfaa..6d384b3cf 100644 --- a/server/routers/hotels/utils.ts +++ b/server/routers/hotels/utils.ts @@ -96,7 +96,7 @@ export async function getCountries( return unstable_cache( async function (searchParams) { const countryResponse = await api.get( - api.endpoints.v1.countries, + api.endpoints.v1.Hotel.countries, options, searchParams ) @@ -136,7 +136,7 @@ export async function getCitiesByCountry( await Promise.all( searchedCountries.data.map(async (country) => { const countryResponse = await api.get( - `${api.endpoints.v1.citiesCountry}/${country.name}`, + api.endpoints.v1.Hotel.Cities.country(country.name), options, searchParams ) @@ -182,7 +182,7 @@ export async function getLocations( groupedCitiesByCountry: CitiesGroupedByCountry | null ) { const apiResponse = await api.get( - api.endpoints.v1.locations, + api.endpoints.v1.Hotel.locations, options, searchParams ) diff --git a/server/routers/user/mutation.ts b/server/routers/user/mutation.ts index 005941090..b03e6a68e 100644 --- a/server/routers/user/mutation.ts +++ b/server/routers/user/mutation.ts @@ -35,16 +35,19 @@ export const userMutationRouter = router({ "api.user.creditCard.add start", JSON.stringify({ query: { language: input.language } }) ) - const apiResponse = await api.post(api.endpoints.v1.initiateSaveCard, { - headers: { - Authorization: `Bearer ${ctx.session.token.access_token}`, - }, - body: { - language: input.language, - mobileToken: false, - redirectUrl: `api/web/add-card-callback/${input.language}`, - }, - }) + const apiResponse = await api.post( + api.endpoints.v1.Profile.CreditCards.initiateSaveCard, + { + headers: { + Authorization: `Bearer ${ctx.session.token.access_token}`, + }, + body: { + language: input.language, + mobileToken: false, + redirectUrl: `api/web/add-card-callback/${input.language}`, + }, + } + ) if (!apiResponse.ok) { const text = await apiResponse.text() @@ -85,7 +88,7 @@ export const userMutationRouter = router({ .mutation(async function ({ ctx, input }) { console.info("api.user.creditCard.save start", JSON.stringify({})) const apiResponse = await api.post( - `${api.endpoints.v1.creditCards}/${input.transactionId}`, + api.endpoints.v1.Profile.CreditCards.transaction(input.transactionId), { headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, @@ -118,7 +121,9 @@ export const userMutationRouter = router({ JSON.stringify({ query: {} }) ) const apiResponse = await api.remove( - `${api.endpoints.v1.creditCards}/${input.creditCardId}`, + api.endpoints.v1.Profile.CreditCards.deleteCreditCard( + input.creditCardId + ), { headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, @@ -149,7 +154,7 @@ export const userMutationRouter = router({ ctx, }) { generatePreferencesLinkCounter.add(1) - const apiResponse = await api.get(api.endpoints.v1.subscriberId, { + const apiResponse = await api.get(api.endpoints.v1.Profile.subscriberId, { headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, }, diff --git a/server/routers/user/query.ts b/server/routers/user/query.ts index 224f03198..a0d7face0 100644 --- a/server/routers/user/query.ts +++ b/server/routers/user/query.ts @@ -89,7 +89,7 @@ export const getVerifiedUser = cache( } getVerifiedUserCounter.add(1) console.info("api.user.profile getVerifiedUser start", JSON.stringify({})) - const apiResponse = await api.get(api.endpoints.v1.profile, { + const apiResponse = await api.get(api.endpoints.v1.Profile.profile, { headers: { Authorization: `Bearer ${session.token.access_token}`, }, @@ -163,7 +163,7 @@ export const getVerifiedUser = cache( } ) -function parsedUser(data: User, isMFA: boolean) { +export function parsedUser(data: User, isMFA: boolean) { const country = countries.find((c) => c.code === data.address.countryCode) const user = { @@ -211,7 +211,7 @@ function parsedUser(data: User, isMFA: boolean) { async function getCreditCards(session: Session) { getCreditCardsCounter.add(1) console.info("api.profile.creditCards start", JSON.stringify({})) - const apiResponse = await api.get(api.endpoints.v1.creditCards, { + const apiResponse = await api.get(api.endpoints.v1.Profile.creditCards, { headers: { Authorization: `Bearer ${session.token.access_token}`, }, @@ -354,7 +354,7 @@ export const userQueryRouter = router({ JSON.stringify({ query: { params } }) ) const previousStaysResponse = await api.get( - api.endpoints.v1.previousStays, + api.endpoints.v1.Booking.Stays.past, { headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, @@ -430,7 +430,7 @@ export const userQueryRouter = router({ ) const apiResponse = await api.get( - api.endpoints.v1.previousStays, + api.endpoints.v1.Booking.Stays.past, { headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, @@ -492,7 +492,7 @@ export const userQueryRouter = router({ ) const nextCursor = verifiedData.data.links && - verifiedData.data.links.offset < verifiedData.data.links.totalCount + verifiedData.data.links.offset < verifiedData.data.links.totalCount ? verifiedData.data.links.offset : undefined @@ -525,7 +525,7 @@ export const userQueryRouter = router({ JSON.stringify({ query: { params } }) ) const apiResponse = await api.get( - api.endpoints.v1.upcomingStays, + api.endpoints.v1.Booking.Stays.future, { headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, @@ -585,7 +585,7 @@ export const userQueryRouter = router({ }) const nextCursor = verifiedData.data.links && - verifiedData.data.links.offset < verifiedData.data.links.totalCount + verifiedData.data.links.offset < verifiedData.data.links.totalCount ? verifiedData.data.links.offset : undefined @@ -611,13 +611,16 @@ export const userQueryRouter = router({ "api.transaction.friendTransactions start", JSON.stringify({}) ) - const apiResponse = await api.get(api.endpoints.v1.friendTransactions, { - cache: undefined, // override defaultOptions - headers: { - Authorization: `Bearer ${ctx.session.token.access_token}`, - }, - next: { revalidate: 30 * 60 * 1000 }, - }) + const apiResponse = await api.get( + api.endpoints.v1.Profile.Transaction.friendTransactions, + { + cache: undefined, // override defaultOptions + headers: { + Authorization: `Bearer ${ctx.session.token.access_token}`, + }, + next: { revalidate: 30 * 60 * 1000 }, + } + ) if (!apiResponse.ok) { // switch (apiResponse.status) { @@ -740,7 +743,7 @@ export const userQueryRouter = router({ membershipCards: protectedProcedure.query(async function ({ ctx }) { getProfileCounter.add(1) console.info("api.profile start", JSON.stringify({})) - const apiResponse = await api.get(api.endpoints.v1.profile, { + const apiResponse = await api.get(api.endpoints.v1.Profile.profile, { cache: "no-store", headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, diff --git a/server/routers/user/utils.ts b/server/routers/user/utils.ts index cde50270f..c720195b1 100644 --- a/server/routers/user/utils.ts +++ b/server/routers/user/utils.ts @@ -35,7 +35,7 @@ async function updateStaysBookingUrl( // Temporary API call needed till we have user name in ctx session data getProfileCounter.add(1) console.info("api.user.profile updatebookingurl start", JSON.stringify({})) - const apiResponse = await api.get(api.endpoints.v1.profile, { + const apiResponse = await api.get(api.endpoints.v1.Profile.profile, { cache: "no-store", headers: { Authorization: `Bearer ${token}`, diff --git a/server/trpc.ts b/server/trpc.ts index 0079a1ff6..688ea01cf 100644 --- a/server/trpc.ts +++ b/server/trpc.ts @@ -176,7 +176,7 @@ export const protectedServerActionProcedure = serverActionProcedure.use( } ) -// NOTE: This is actually save to use, just the implementation could change +// NOTE: This is actually safe to use, just the implementation could change // in minor version bumps. Please read: https://trpc.io/docs/faq#unstable export const contentStackUidWithServiceProcedure = contentstackExtendedProcedureUID.unstable_concat(serviceProcedure) @@ -186,3 +186,6 @@ export const contentStackBaseWithServiceProcedure = export const contentStackBaseWithProtectedProcedure = contentstackBaseProcedure.unstable_concat(protectedProcedure) + +export const safeProtectedServiceProcedure = + safeProtectedProcedure.unstable_concat(serviceProcedure) diff --git a/stores/enter-details.ts b/stores/enter-details.ts index d4d2666d1..b5c862d36 100644 --- a/stores/enter-details.ts +++ b/stores/enter-details.ts @@ -3,21 +3,22 @@ import { createContext, useContext } from "react" import { create, useStore } from "zustand" import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema" -import { breakfastSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" +import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" import { detailsSchema } from "@/components/HotelReservation/EnterDetails/Details/schema" +import { BreakfastPackage } from "@/types/components/enterDetails/breakfast" import { DetailsSchema } from "@/types/components/enterDetails/details" import { SidePeekEnum } from "@/types/components/enterDetails/sidePeek" import { StepEnum } from "@/types/components/enterDetails/step" -import { bedTypeEnum } from "@/types/enums/bedType" -import { breakfastEnum } from "@/types/enums/breakfast" +import { BedTypeEnum } from "@/types/enums/bedType" +import { BreakfastPackageEnum } from "@/types/enums/breakfast" const SESSION_STORAGE_KEY = "enterDetails" interface EnterDetailsState { data: { - bedType: bedTypeEnum | undefined - breakfast: breakfastEnum | undefined + bedType: BedTypeEnum | undefined + breakfast: BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST | undefined } & DetailsSchema steps: StepEnum[] currentStep: StepEnum @@ -26,7 +27,7 @@ interface EnterDetailsState { completeStep: (updatedData: Partial<EnterDetailsState["data"]>) => void navigate: ( step: StepEnum, - updatedData?: Record<string, string | boolean> + updatedData?: Record<string, string | boolean | BreakfastPackage> ) => void setCurrentStep: (step: StepEnum) => void openSidePeek: (key: SidePeekEnum | null) => void @@ -75,7 +76,7 @@ export function initEditDetailsState(currentStep: StepEnum) { initialData = { ...initialData, ...validatedBedType.data } isValid[StepEnum.selectBed] = true } - const validatedBreakfast = breakfastSchema.safeParse(inputData) + const validatedBreakfast = breakfastStoreSchema.safeParse(inputData) if (validatedBreakfast.success) { validPaths.push(StepEnum.details) initialData = { ...initialData, ...validatedBreakfast.data } diff --git a/types/components/enterDetails/breakfast.ts b/types/components/enterDetails/breakfast.ts index 868bc96a1..21ba37bd0 100644 --- a/types/components/enterDetails/breakfast.ts +++ b/types/components/enterDetails/breakfast.ts @@ -1,5 +1,21 @@ import { z } from "zod" -import { breakfastSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" +import { + breakfastPackageSchema, + breakfastPackagesSchema, +} from "@/server/routers/hotels/output" -export interface BreakfastSchema extends z.output<typeof breakfastSchema> {} +import { breakfastFormSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" + +export interface BreakfastFormSchema + extends z.output<typeof breakfastFormSchema> {} + +export interface BreakfastPackages + extends z.output<typeof breakfastPackagesSchema> {} + +export interface BreakfastPackage + extends z.output<typeof breakfastPackageSchema> {} + +export interface BreakfastProps { + packages: BreakfastPackages | null +} diff --git a/types/enums/bedType.ts b/types/enums/bedType.ts index 0b4ba284d..2feb6d980 100644 --- a/types/enums/bedType.ts +++ b/types/enums/bedType.ts @@ -1,4 +1,4 @@ -export enum bedTypeEnum { +export enum BedTypeEnum { KING = "KING", QUEEN = "QUEEN", } diff --git a/types/enums/breakfast.ts b/types/enums/breakfast.ts index 567db2860..81ff51a2e 100644 --- a/types/enums/breakfast.ts +++ b/types/enums/breakfast.ts @@ -1,4 +1,5 @@ -export enum breakfastEnum { - BREAKFAST = "BREAKFAST", +export enum BreakfastPackageEnum { + FREE_MEMBER_BREAKFAST = "BRF0", + REGULAR_BREAKFAST = "BRF1", NO_BREAKFAST = "NO_BREAKFAST", } diff --git a/types/enums/currency.ts b/types/enums/currency.ts new file mode 100644 index 000000000..c04ed8450 --- /dev/null +++ b/types/enums/currency.ts @@ -0,0 +1,7 @@ +export enum CurrencyEnum { + DKK = "DKK", + EUR = "EUR", + NOK = "NOK", + PLN = "PLN", + SEK = "SEK", +} diff --git a/types/enums/packages.ts b/types/enums/packages.ts new file mode 100644 index 000000000..f030ccaab --- /dev/null +++ b/types/enums/packages.ts @@ -0,0 +1,7 @@ +export enum PackageTypeEnum { + AccessibleFriendlyRoom = "AccessibleFriendlyRoom", + AllergyRoom = "AllergyRoom", + BreakfastAdult = "BreakfastAdult", + BreakfastChildren = "BreakfastChildren", + PetRoom = "PetRoom", +} From bbcaa1cbf4c450669c969ae04f254f8cfcca491b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matilda=20Landstr=C3=B6m?= <matilda.landstrom@scandichotels.com> Date: Tue, 29 Oct 2024 14:43:41 +0100 Subject: [PATCH 30/73] fix: hide empty hotel faq --- components/ContentType/HotelPage/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index f5718a7f2..8eb441e01 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -70,7 +70,7 @@ export default async function HotelPage() { <TabNavigation restaurantTitle={getRestaurantHeading(hotelDetailedFacilities)} hasActivities={!!activitiesCard} - hasFAQ={!!faq} + hasFAQ={!!faq.accordions.length} /> <main className={styles.mainSection}> <div id={HotelHashValues.overview} className={styles.overview}> @@ -100,7 +100,7 @@ export default async function HotelPage() { </div> <Rooms rooms={roomCategories} /> <Facilities facilities={facilities} activitiesCard={activitiesCard} /> - {faq && ( + {faq.accordions.length > 0 && ( <AccordionSection accordion={faq.accordions} title={faq.title} /> )} </main> From eff0d122cd94b7b9f32e9f86564ea760933ff4e9 Mon Sep 17 00:00:00 2001 From: Bianca Widstam <bianca.widstam@scandichotels.com> Date: Wed, 30 Oct 2024 09:34:54 +0000 Subject: [PATCH 31/73] feat/SW-711-update-children-params (pull request #791) Feat/SW-711 update children params * feat(SW-711): add new child params for availability * feat(SW-711): fix children schema * feat(SW-711): fix optional values * feat(SW-711): add children as parameter iff not undefined * feat(SW-711): add bedType enum * feat(SW-711): remove optional number type * feat(SW-711): fix wrong slash * feat(SW-711): remove optional Approved-by: Hrishikesh Vaipurkar --- .../(standard)/select-hotel/page.tsx | 5 ++++- .../(standard)/select-hotel/utils.ts | 18 ++++++++++++++++++ .../(standard)/select-rate/page.tsx | 11 ++++++++--- .../RoomSelection/RateSummary/index.tsx | 4 ++-- server/routers/hotels/input.ts | 4 ++-- server/routers/hotels/output.ts | 8 +++++++- server/routers/hotels/query.ts | 5 ++--- .../selectHotel/availabilityInput.ts | 2 +- .../hotelReservation/selectRate/selectRate.ts | 2 +- 9 files changed, 45 insertions(+), 14 deletions(-) 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 1591e811b..36fbd59c7 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -5,6 +5,7 @@ import { getLocations } from "@/lib/trpc/memoizedRequests" import { fetchAvailableHotels, + generateChildrenString, getFiltersFromHotels, } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils" import HotelCardListing from "@/components/HotelReservation/HotelCardListing" @@ -42,7 +43,9 @@ export default async function SelectHotelPage({ const selectHotelParamsObject = getHotelReservationQueryParams(selectHotelParams) const adults = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms - const children = selectHotelParamsObject.room[0].child?.length // TODO: Handle multiple rooms + const children = selectHotelParamsObject.room[0].child + ? generateChildrenString(selectHotelParamsObject.room[0].child) + : undefined // TODO: Handle multiple rooms const hotels = await fetchAvailableHotels({ cityId: city.id, diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts index 8dfc76a1d..a6a48e12f 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts @@ -2,9 +2,11 @@ import { serverClient } from "@/lib/trpc/server" import { getLang } from "@/i18n/serverContext" +import { BedTypeEnum } from "@/types/components/bookingWidget/enums" import { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput" import { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" import { Filter } from "@/types/components/hotelReservation/selectHotel/hotelFilters" +import { Child } from "@/types/components/hotelReservation/selectRate/selectRate" export async function fetchAvailableHotels( input: AvailabilityInput @@ -41,3 +43,19 @@ export function getFiltersFromHotels(hotels: HotelData[]) { return filterList } + +const bedTypeMap: Record<number, string> = { + [BedTypeEnum.IN_ADULTS_BED]: "ParentsBed", + [BedTypeEnum.IN_CRIB]: "Crib", + [BedTypeEnum.IN_EXTRA_BED]: "ExtraBed", +} + +export function generateChildrenString(children: Child[]): string { + return `[${children + ?.map((child) => { + const age = child.age + const bedType = bedTypeMap[+child.bed] + return `${age}:${bedType}` + }) + .join(",")}]` +} 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 b361aaa10..35a940f0b 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -8,6 +8,8 @@ import Rooms from "@/components/HotelReservation/SelectRate/Rooms" import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { setLang } from "@/i18n/serverContext" +import { generateChildrenString } from "../select-hotel/utils" + import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import { LangParams, PageArgs } from "@/types/params" @@ -27,7 +29,10 @@ export default async function SelectRatePage({ } const adults = selectRoomParamsObject.room[0].adults // TODO: Handle multiple rooms - const children = selectRoomParamsObject.room[0].child?.length // TODO: Handle multiple rooms + const childrenCount = selectRoomParamsObject.room[0].child?.length + const children = selectRoomParamsObject.room[0].child + ? generateChildrenString(selectRoomParamsObject.room[0].child) + : undefined // TODO: Handle multiple rooms const [hotelData, roomsAvailability, packages, user] = await Promise.all([ serverClient().hotel.hotelData.get({ @@ -46,8 +51,8 @@ export default async function SelectRatePage({ hotelId: searchParams.hotel, startDate: searchParams.fromDate, endDate: searchParams.toDate, - adults: adults, - children: children, + adults, + children: childrenCount, packageCodes: [ RoomPackageCodeEnum.ACCESSIBILITY_ROOM, RoomPackageCodeEnum.PET_ROOM, diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx index b7ecc3c63..827a1d280 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx @@ -82,12 +82,12 @@ export default function RateSummary({ { id: "booking.adults" }, { totalAdults: roomsAvailability.occupancy?.adults } )} - {roomsAvailability.occupancy?.children && ( + {roomsAvailability.occupancy?.children?.length && ( <> ,{" "} {intl.formatMessage( { id: "booking.children" }, - { totalChildren: roomsAvailability.occupancy.children } + { totalChildren: roomsAvailability.occupancy.children.length } )} </> )} diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index 61927c3d2..194a16496 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -11,7 +11,7 @@ export const getHotelsAvailabilityInputSchema = z.object({ roomStayStartDate: z.string(), roomStayEndDate: z.string(), adults: z.number(), - children: z.number().optional().default(0), + children: z.string().optional(), promotionCode: z.string().optional().default(""), reservationProfileType: z.string().optional().default(""), attachedProfileId: z.string().optional().default(""), @@ -22,7 +22,7 @@ export const getRoomsAvailabilityInputSchema = z.object({ roomStayStartDate: z.string(), roomStayEndDate: z.string(), adults: z.number(), - children: z.number().optional().default(0), + children: z.string().optional(), promotionCode: z.string().optional(), reservationProfileType: z.string().optional().default(""), attachedProfileId: z.string().optional().default(""), diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index f8e2b8a6e..84ab77171 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -1,5 +1,6 @@ import { z } from "zod" +import { BedTypeEnum } from "@/constants/booking" import { dt } from "@/lib/dt" import { toLang } from "@/server/utils" @@ -458,9 +459,14 @@ export const getHotelDataSchema = z.object({ included: z.array(roomSchema).optional(), }) +export const childrenSchema = z.object({ + age: z.number(), + bedType: z.nativeEnum(BedTypeEnum), +}) + const occupancySchema = z.object({ adults: z.number(), - children: z.number(), + children: z.array(childrenSchema), }) const bestPricePerStaySchema = z.object({ diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index a7b6c220e..c783d0be5 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -312,13 +312,12 @@ export const hotelQueryRouter = router({ roomStayStartDate, roomStayEndDate, adults, - children, + ...(children && { children }), promotionCode, reservationProfileType, attachedProfileId, language: apiLang, } - hotelsAvailabilityCounter.add(1, { cityId, roomStayStartDate, @@ -437,7 +436,7 @@ export const hotelQueryRouter = router({ roomStayStartDate, roomStayEndDate, adults, - children, + ...(children && { children }), promotionCode, reservationProfileType, attachedProfileId, diff --git a/types/components/hotelReservation/selectHotel/availabilityInput.ts b/types/components/hotelReservation/selectHotel/availabilityInput.ts index ff25984b9..5b3a51b93 100644 --- a/types/components/hotelReservation/selectHotel/availabilityInput.ts +++ b/types/components/hotelReservation/selectHotel/availabilityInput.ts @@ -3,7 +3,7 @@ export type AvailabilityInput = { roomStayStartDate: string roomStayEndDate: string adults: number - children?: number + children?: string promotionCode?: string reservationProfileType?: string attachedProfileId?: string diff --git a/types/components/hotelReservation/selectRate/selectRate.ts b/types/components/hotelReservation/selectRate/selectRate.ts index b22c010c0..553d09827 100644 --- a/types/components/hotelReservation/selectRate/selectRate.ts +++ b/types/components/hotelReservation/selectRate/selectRate.ts @@ -1,6 +1,6 @@ import { Product, RoomConfiguration } from "@/server/routers/hotels/output" -interface Child { +export interface Child { bed: string age: number } From 6523e2329b2792f2d8f3fb7425e44e73dc97e403 Mon Sep 17 00:00:00 2001 From: Tobias Johansson <tobias.johansson@scandichotels.com> Date: Wed, 23 Oct 2024 09:57:45 +0200 Subject: [PATCH 32/73] feat(SW-614): added i18n strings --- i18n/dictionaries/da.json | 9 +++++++++ i18n/dictionaries/de.json | 11 +++++++++-- i18n/dictionaries/en.json | 9 +++++++++ i18n/dictionaries/fi.json | 11 +++++++++-- i18n/dictionaries/no.json | 13 +++++++++++-- i18n/dictionaries/sv.json | 13 +++++++++++-- 6 files changed, 58 insertions(+), 8 deletions(-) diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index c66448f17..cbfec3e69 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -68,6 +68,7 @@ "Coming up": "Er lige om hjørnet", "Compare all levels": "Sammenlign alle niveauer", "Complete booking & go to payment": "Udfyld booking & gå til betaling", + "Complete the booking": "Fuldfør bookingen", "Contact information": "Kontaktoplysninger", "Contact us": "Kontakt os", "Continue": "Blive ved", @@ -125,6 +126,7 @@ "Go back to edit": "Gå tilbage til redigering", "Go back to overview": "Gå tilbage til oversigten", "Go to My Benefits": "Gå til ‘Mine fordele’", + "Guarantee booking with credit card": "Garantere booking med kreditkort", "Guest information": "Gæsteinformation", "Guests & Rooms": "Gæster & værelser", "Hi": "Hei", @@ -227,6 +229,7 @@ "Phone is required": "Telefonnummer er påkrævet", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Indtast venligst et gyldigt telefonnummer", + "Please note that this is mandatory, and that your card will only be charged in the event of a no-show.": "Vær opmærksom på, at dette er påkrævet, og at dit kort kun vil blive opkrævet i tilfælde af en no-show.", "Points": "Point", "Points being calculated": "Point udregnes", "Points earned prior to May 1, 2021": "Point optjent inden 1. maj 2021", @@ -237,6 +240,7 @@ "Previous victories": "Tidligere sejre", "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", @@ -302,6 +306,7 @@ "Theatre": "Teater", "There are no transactions to display": "Der er ingen transaktioner at vise", "Things nearby HOTEL_NAME": "Ting i nærheden af {hotelName}", + "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For at sikre din reservation, beder vi om at du giver os dine betalingsoplysninger. Du kan så være sikker på, at ingen gebyrer vil blive opkrævet på dette tidspunkt.", "Total Points": "Samlet antal point", "Total incl VAT": "Inkl. moms", "Total price": "Samlet pris", @@ -330,7 +335,9 @@ "Welcome": "Velkommen", "Welcome to": "Velkommen til", "Wellness & Exercise": "Velvære & Motion", + "What you have to do to guarantee booking": "Hvad du skal gøre for at garantere booking", "When": "Hvornår", + "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "Når du garanterer din booking, vil vi holde bookingen indtil 07:00 til dagen efter check-in. Dette vil give dig som gæst tilføjet fleksibilitet til check-in-tider.", "Where should you go next?": "Find inspiration til dit næste ophold", "Where to": "Hvor", "Which room class suits you the best?": "Hvilken rumklasse passer bedst til dig", @@ -369,6 +376,7 @@ "breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/nat", "by": "inden", "characters": "tegn", + "guaranteeing": "garanti", "guest": "gæst", "guests": "gæster", "hotelPages.rooms.roomCard.person": "person", @@ -382,6 +390,7 @@ "nights": "nætter", "number": "nummer", "or": "eller", + "paying": "betaler ", "points": "Point", "room type": "værelsestype", "room types": "værelsestyper", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 1959939ee..901d47949 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -68,6 +68,7 @@ "Coming up": "Demnächst", "Compare all levels": "Vergleichen Sie alle Levels", "Complete booking & go to payment": "Buchung abschließen & zur Bezahlung gehen", + "Complete the booking": "Buchung abschließen", "Contact information": "Kontaktinformationen", "Contact us": "Kontaktieren Sie uns", "Continue": "Weitermachen", @@ -125,6 +126,7 @@ "Go back to edit": "Zurück zum Bearbeiten", "Go back to overview": "Zurück zur Übersicht", "Go to My Benefits": "Gehen Sie zu „Meine Vorteile“", + "Guarantee booking with credit card": "Buchung mit Kreditkarte garantieren", "Guest information": "Informationen für Gäste", "Guests & Rooms": "Gäste & Zimmer", "Hi": "Hallo", @@ -179,8 +181,6 @@ "Membership ID": "Mitglieds-ID", "Membership ID copied to clipboard": "Mitglieds-ID in die Zwischenablage kopiert", "Membership cards": "Mitgliedskarten", - "Membership terms and conditions": "Mitgliedschaftsbedingungen", - "Menu": "Menu", "Modify": "Ändern", "Month": "Monat", "Museum": "Museum", @@ -227,6 +227,7 @@ "Phone is required": "Telefon ist erforderlich", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Bitte geben Sie eine gültige Telefonnummer ein", + "Please note that this is mandatory, and that your card will only be charged in the event of a no-show.": "Bitte beachten Sie, dass dies erforderlich ist und dass Ihr Kreditkartenkonto nur in einem No-Show-Fall belastet wird.", "Points": "Punkte", "Points being calculated": "Punkte werden berechnet", "Points earned prior to May 1, 2021": "Zusammengeführte Punkte vor dem 1. Mai 2021", @@ -237,6 +238,7 @@ "Previous victories": "Bisherige Siege", "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", @@ -303,6 +305,7 @@ "Theatre": "Theater", "There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden", "Things nearby HOTEL_NAME": "Dinge in der Nähe von {hotelName}", + "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Um Ihre Reservierung zu sichern, bitten wir Sie, Ihre Zahlungskarteninformationen zu geben. Sie können sicher sein, dass keine Gebühren zu diesem Zeitpunkt erhoben werden.", "Total Points": "Gesamtpunktzahl", "Total incl VAT": "Gesamt inkl. MwSt.", "Total price": "Gesamtpreis", @@ -331,7 +334,9 @@ "Welcome": "Willkommen", "Welcome to": "Willkommen zu", "Wellness & Exercise": "Wellness & Bewegung", + "What you have to do to guarantee booking": "Was Sie tun müssen, um eine Buchung zu garantieren", "When": "Wann", + "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "Wenn Sie Ihre Buchung garantieren, halten wir die Buchung bis 07:00 am Tag nach dem Check-in. Dies wird Ihnen als Gast zusätzliche Flexibilität für die Check-in-Zeiten gewähren.", "Where should you go next?": "Wo geht es als Nächstes hin?", "Where to": "Wohin", "Which room class suits you the best?": "Welche Zimmerklasse passt am besten zu Ihnen?", @@ -370,6 +375,7 @@ "breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/Nacht", "by": "bis", "characters": "figuren", + "guaranteeing": "garantiert", "guest": "gast", "guests": "gäste", "hotelPages.rooms.roomCard.person": "person", @@ -383,6 +389,7 @@ "nights": "Nächte", "number": "nummer", "or": "oder", + "paying": "bezahlt", "points": "Punkte", "room type": "zimmerart", "room types": "zimmerarten", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 2fa6793d7..fb7daa428 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -74,6 +74,7 @@ "Coming up": "Coming up", "Compare all levels": "Compare all levels", "Complete booking & go to payment": "Complete booking & go to payment", + "Complete the booking": "Complete the booking", "Contact information": "Contact information", "Contact us": "Contact us", "Continue": "Continue", @@ -134,6 +135,7 @@ "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 information": "Guest information", "Guests & Rooms": "Guests & Rooms", "Hi": "Hi", @@ -237,6 +239,7 @@ "Phone is required": "Phone is required", "Phone number": "Phone number", "Please enter a valid phone number": "Please enter a valid phone number", + "Please note that this is mandatory, and that your card will only be charged in the event of a no-show.": "Please note that this is mandatory, and that your card will only be charged in the event of a no-show.", "Points": "Points", "Points being calculated": "Points being calculated", "Points earned prior to May 1, 2021": "Points earned prior to May 1, 2021", @@ -248,6 +251,7 @@ "Print confirmation": "Print confirmation", "Proceed to login": "Proceed to login", "Proceed to payment method": "Proceed to payment method", + "Provide a payment card in the next step": "Provide a payment card in the next step", "Public price from": "Public price from", "Public transport": "Public transport", "Queen bed": "Queen bed", @@ -318,6 +322,7 @@ "Things nearby HOTEL_NAME": "Things nearby {hotelName}", "Total Points": "Total Points", "Total cost": "Total cost", + "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 incl VAT": "Total incl VAT", "Total price": "Total price", "Tourist": "Tourist", @@ -346,7 +351,9 @@ "Welcome": "Welcome", "Welcome to": "Welcome to", "Wellness & Exercise": "Wellness & Exercise", + "What you have to do to guarantee booking": "What you have to do to guarantee booking", "When": "When", + "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.", "Where should you go next?": "Where should you go next?", "Where to": "Where to", "Which room class suits you the best?": "Which room class suits you the best?", @@ -389,6 +396,7 @@ "by": "by", "characters": "characters", "from": "from", + "guaranteeing": "guaranteeing", "guest": "guest", "guests": "guests", "hotelPages.rooms.roomCard.person": "person", @@ -403,6 +411,7 @@ "nights": "nights", "number": "number", "or": "or", + "paying": "paying", "points": "Points", "room type": "room type", "room types": "room types", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 883b9d277..6ae282cd1 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -68,6 +68,7 @@ "Coming up": "Tulossa", "Compare all levels": "Vertaa kaikkia tasoja", "Complete booking & go to payment": "Täydennä varaus & siirry maksamaan", + "Complete the booking": "Täydennä varaus", "Contact information": "Yhteystiedot", "Contact us": "Ota meihin yhteyttä", "Continue": "Jatkaa", @@ -125,6 +126,7 @@ "Go back to edit": "Palaa muokkaamaan", "Go back to overview": "Palaa yleiskatsaukseen", "Go to My Benefits": "Siirry kohtaan ‘Omat edut’", + "Guarantee booking with credit card": "Varmista varaus luottokortilla", "Guest information": "Vieraan tiedot", "Guests & Rooms": "Vieraat & Huoneet", "Hi": "Hi", @@ -227,6 +229,7 @@ "Phone is required": "Puhelin vaaditaan", "Phone number": "Puhelinnumero", "Please enter a valid phone number": "Ole hyvä ja näppäile voimassaoleva puhelinnumero", + "Please note that this is mandatory, and that your card will only be charged in the event of a no-show.": "Huomaa, että tämä on pakollinen, ja että maksukorttiisi kirjataan vain, jos varausmyyntiä ei tapahtu.", "Points": "Pisteet", "Points being calculated": "Pisteitä lasketaan", "Points earned prior to May 1, 2021": "Pisteet, jotka ansaittu ennen 1.5.2021", @@ -237,6 +240,7 @@ "Previous victories": "Edelliset voitot", "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", @@ -303,6 +307,7 @@ "Theatre": "Teatteri", "There are no transactions to display": "Näytettäviä tapahtumia ei ole", "Things nearby HOTEL_NAME": "Lähellä olevia asioita {hotelName}", + "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Varmistaaksesi varauksen, pyydämme sinua antamaan meille maksukortin tiedot. Varmista, että ei veloiteta maksusi tällä hetkellä.", "Total Points": "Kokonaispisteet", "Total incl VAT": "Yhteensä sis. alv", "Total price": "Kokonaishinta", @@ -331,7 +336,9 @@ "Welcome": "Tervetuloa", "Welcome to": "Tervetuloa", "Wellness & Exercise": "Hyvinvointi & Liikunta", + "What you have to do to guarantee booking": "Mitä sinun on tehtävä varmistaaksesi varauksen", "When": "Kun", + "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "Jos varaat varauksen, pidämme varauksen 07:00 päivän jälkeen tarkistuspäivän jälkeen. Tämä tarjoaa sinulle lisään tarkistuspäivän aikaan.", "Where should you go next?": "Mihin menisit seuraavaksi?", "Where to": "Minne", "Which room class suits you the best?": "Mikä huoneluokka sopii sinulle parhaiten?", @@ -344,8 +351,6 @@ "You have <b>#</b> gifts waiting for you!": "Sinulla on <b>{amount}</b> lahjaa odottamassa sinua!", "You have no previous stays.": "Sinulla ei ole aiempia majoituksia.", "You have no upcoming stays.": "Sinulla ei ole tulevia majoituksia.", - "You'll find all your gifts in 'My benefits'": "Löydät kaikki lahjasi kohdasta ‘Omat edut’", - "Your Challenges Conquer & Earn!": "Voita ja ansaitse haasteesi!", "Your card was successfully removed!": "Korttisi poistettiin onnistuneesti!", "Your card was successfully saved!": "Korttisi tallennettu onnistuneesti!", "Your current level": "Nykyinen tasosi", @@ -370,6 +375,7 @@ "breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/yö", "by": "mennessä", "characters": "hahmoja", + "guaranteeing": "varmistetaan", "guest": "Vieras", "guests": "Vieraita", "hotelPages.rooms.roomCard.person": "henkilö", @@ -383,6 +389,7 @@ "nights": "yötä", "number": "määrä", "or": "tai", + "paying": "maksaa", "points": "pistettä", "room type": "huonetyyppi", "room types": "huonetyypit", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 075f84783..5041708dc 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -68,6 +68,7 @@ "Coming up": "Kommer opp", "Compare all levels": "Sammenlign alle nivåer", "Complete booking & go to payment": "Fullfør bestilling & gå til betaling", + "Complete the booking": "Fullfør reservasjonen", "Contact information": "Kontaktinformasjon", "Contact us": "Kontakt oss", "Continue": "Fortsette", @@ -124,6 +125,7 @@ "Go back to edit": "Gå tilbake til redigering", "Go back to overview": "Gå tilbake til oversikten", "Go to My Benefits": "Gå til ‘Mine fordeler’", + "Guarantee booking with credit card": "Garantere booking med kredittkort", "Guest information": "Informasjon til gjester", "Guests & Rooms": "Gjester & rom", "Hi": "Hei", @@ -225,6 +227,7 @@ "Phone is required": "Telefon kreves", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Vennligst oppgi et gyldig telefonnummer", + "Please note that this is mandatory, and that your card will only be charged in the event of a no-show.": "Vær oppmerksom på at dette er påkrevd, og at ditt kredittkort kun vil bli belastet i tilfelle av en no-show.", "Points": "Poeng", "Points being calculated": "Poeng beregnes", "Points earned prior to May 1, 2021": "Opptjente poeng før 1. mai 2021", @@ -235,6 +238,7 @@ "Previous victories": "Tidligere seire", "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", @@ -300,9 +304,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", @@ -328,7 +333,9 @@ "Welcome": "Velkommen", "Welcome to": "Velkommen til", "Wellness & Exercise": "Velvære & Trening", + "What you have to do to guarantee booking": "Hva du må gjøre for å garantere reservasjonen", "When": "Når", + "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "Når du garanterer din reservasjon, vil vi holde reservasjonen til 07:00 til dagen etter check-in. Dette vil gi deg som gjest tilføjet fleksibilitet for check-in-tider.", "Where should you go next?": "Hvor ønsker du å reise neste gang?", "Where to": "Hvor skal du", "Which room class suits you the best?": "Hvilken romklasse passer deg best?", @@ -366,6 +373,7 @@ "breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/natt", "by": "innen", "characters": "tegn", + "guaranteeing": "garantiert", "guest": "gjest", "guests": "gjester", "hotelPages.rooms.roomCard.person": "person", @@ -379,9 +387,10 @@ "nights": "netter", "number": "antall", "or": "eller", - "points": "poeng", "room type": "romtype", "room types": "romtyper", + "paying": "betaler", + "points": "poeng", "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 0b48022d3..8f3f0eeda 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -68,6 +68,7 @@ "Coming up": "Kommer härnäst", "Compare all levels": "Jämför alla nivåer", "Complete booking & go to payment": "Fullför bokning & gå till betalning", + "Complete the booking": "Slutför bokningen", "Contact information": "Kontaktinformation", "Contact us": "Kontakta oss", "Continue": "Fortsätt", @@ -124,6 +125,7 @@ "Go back to edit": "Gå tillbaka till redigeringen", "Go back to overview": "Gå tillbaka till översikten", "Go to My Benefits": "Gå till ‘Mina förmåner’", + "Guarantee booking with credit card": "Garantera bokning med kreditkort", "Guest information": "Information till gästerna", "Guests & Rooms": "Gäster & rum", "Hi": "Hej", @@ -225,6 +227,7 @@ "Phone is required": "Telefonnummer är obligatorisk", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Var vänlig och ange ett giltigt telefonnummer", + "Please note that this is mandatory, and that your card will only be charged in the event of a no-show.": "Vänligen notera att detta är obligatoriskt, och att ditt kreditkort endast debiteras i händelse av en no-show.", "Points": "Poäng", "Points being calculated": "Poäng beräknas", "Points earned prior to May 1, 2021": "Intjänade poäng före den 1 maj 2021", @@ -235,6 +238,7 @@ "Previous victories": "Tidigare segrar", "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", @@ -300,9 +304,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", @@ -328,7 +333,9 @@ "Welcome": "Välkommen", "Welcome to": "Välkommen till", "Wellness & Exercise": "Hälsa & Träning", + "What you have to do to guarantee booking": "Vad du måste göra för att garantera bokningen", "When": "När", + "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "När du garanterar din bokning kommer vi att hålla bokningen till 07:00 till dagen efter check-in. Detta ger dig som gäst extra flexibilitet för check-in-tider.", "Where should you go next?": "Låter inte en spontanweekend härligt?", "Where to": "Vart", "Which room class suits you the best?": "Vilken rumsklass passar dig bäst?", @@ -367,6 +374,7 @@ "breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/natt", "by": "innan", "characters": "tecken", + "guaranteeing": "garanterar", "guest": "gäst", "guests": "gäster", "hotelPages.rooms.roomCard.person": "person", @@ -388,8 +396,9 @@ "to": "till", "type": "typ", "types": "typer", - "uppercase letter": "stor bokstav", "{amount} out of {total}": "{amount} av {total}", + "paying": "betalar", + "uppercase letter": "stor bokstav", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" From a690750c361b54ba9c1c91ac0bad3bc4a8dc8c29 Mon Sep 17 00:00:00 2001 From: Tobias Johansson <tobias.johansson@scandichotels.com> Date: Wed, 23 Oct 2024 13:22:49 +0200 Subject: [PATCH 33/73] feat(SW-614): Add mustBeGuaranteed flag and update content based on this --- .../(standard)/[step]/page.tsx | 30 ++++++++--- .../guaranteeDetails.module.css | 26 ++++++++++ .../Payment/GuaranteeDetails/index.tsx | 50 +++++++++++++++++++ .../EnterDetails/Payment/index.tsx | 17 +++++++ i18n/dictionaries/da.json | 2 +- i18n/dictionaries/de.json | 2 +- i18n/dictionaries/en.json | 2 +- i18n/dictionaries/fi.json | 2 +- i18n/dictionaries/no.json | 2 +- i18n/dictionaries/sv.json | 2 +- lib/trpc/memoizedRequests/index.ts | 20 ++++++++ .../hotelReservation/selectRate/section.ts | 1 + 12 files changed, 144 insertions(+), 12 deletions(-) create mode 100644 components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/guaranteeDetails.module.css create mode 100644 components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/index.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 77ec20203..45191398d 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -5,6 +5,7 @@ import { getCreditCardsSafely, getHotelData, getProfileSafely, + getRoomAvailability, } from "@/lib/trpc/memoizedRequests" import BedType from "@/components/HotelReservation/EnterDetails/BedType" @@ -21,6 +22,7 @@ import type { LangParams, PageArgs } from "@/types/params" export function preload() { void getProfileSafely() void getCreditCardsSafely() + void getRoomAvailability("811", 1, "2024-11-01", "2024-11-02") } function isValidStep(step: string): step is StepEnum { @@ -43,10 +45,19 @@ export default async function StepPage({ const savedCreditCards = await getCreditCardsSafely() const breakfastPackages = await getBreakfastPackages(searchParams.hotel) - if (!isValidStep(params.step) || !hotel) { + const roomAvailability = await getRoomAvailability( + searchParams.hotel, + Number(searchParams.adults), + searchParams.checkIn, + searchParams.checkOut + ) + + if (!isValidStep(params.step) || !hotel || !roomAvailability) { return notFound() } + const mustBeGuaranteed = false + return ( <section> <HistoryStateManager /> @@ -72,17 +83,24 @@ export default async function StepPage({ <Details user={user} /> </SectionAccordion> <SectionAccordion - header="Payment" + header={mustBeGuaranteed ? "Payment Guarantee" : "Payment"} step={StepEnum.payment} - label={intl.formatMessage({ id: "Select payment method" })} + label={ + mustBeGuaranteed + ? intl.formatMessage({ id: "Guarantee booking with credit card" }) + : intl.formatMessage({ id: "Select payment method" }) + } > <Payment - hotelId={hotel.data.attributes.operaId} + hotelId={searchParams.hotel} otherPaymentOptions={ - hotel.data.attributes.merchantInformationData - .alternatePaymentOptions + mustBeGuaranteed + ? [] + : hotel.data.attributes.merchantInformationData + .alternatePaymentOptions } savedCreditCards={savedCreditCards} + mustBeGuaranteed={mustBeGuaranteed} /> </SectionAccordion> </section> diff --git a/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/guaranteeDetails.module.css b/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/guaranteeDetails.module.css new file mode 100644 index 000000000..a14bc3473 --- /dev/null +++ b/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/guaranteeDetails.module.css @@ -0,0 +1,26 @@ +.content { + display: flex; + flex-direction: column; + gap: var(--Spacing-x1); + padding-top: var(--Spacing-x2); +} + +.content ol { + margin: 0; +} + +.summary { + list-style: none; + display: flex; + align-items: center; + gap: var(--Spacing-x-half); +} + +.summary::-webkit-details-marker, +.summary::marker { + display: none; +} + +.summary .icon { + height: 16px; +} diff --git a/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/index.tsx b/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/index.tsx new file mode 100644 index 000000000..99021715a --- /dev/null +++ b/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/index.tsx @@ -0,0 +1,50 @@ +import { useIntl } from "react-intl" + +import ChevronDown from "@/components/Icons/ChevronDown" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" + +import styles from "./guaranteeDetails.module.css" + +export default function GuaranteeDetails() { + const intl = useIntl() + return ( + <details> + <Caption color="burgundy" type="bold" asChild> + <summary className={styles.summary}> + {intl.formatMessage({ id: "How it works" })} + <ChevronDown color="burgundy" className={styles.icon} /> + </summary> + </Caption> + <section className={styles.content}> + <Body> + {intl.formatMessage({ + id: "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.", + })} + </Body> + <Body> + {intl.formatMessage({ + id: "What you have to do to guarantee booking:", + })} + </Body> + <ol> + <Body asChild> + <li>{intl.formatMessage({ id: "Complete the booking" })}</li> + </Body> + <Body asChild> + <li> + {intl.formatMessage({ + id: "Provide a payment card in the next step", + })} + </li> + </Body> + </ol> + <Body> + {intl.formatMessage({ + id: "Please note that this is mandatory, and that your card will only be charged in the event of a no-show.", + })} + </Body> + </section> + </details> + ) +} diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index ba126c1f2..f31ea816b 100644 --- a/components/HotelReservation/EnterDetails/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -30,6 +30,7 @@ import { toast } from "@/components/TempDesignSystem/Toasts" import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus" import useLang from "@/hooks/useLang" +import GuaranteeDetails from "./GuaranteeDetails" import PaymentOption from "./PaymentOption" import { PaymentFormData, paymentSchema } from "./schema" @@ -48,6 +49,7 @@ export default function Payment({ hotelId, otherPaymentOptions, savedCreditCards, + mustBeGuaranteed, }: PaymentProps) { const router = useRouter() const lang = useLang() @@ -169,12 +171,26 @@ export default function Payment({ return <LoadingSpinner /> } + const paymentVerb = mustBeGuaranteed + ? intl.formatMessage({ id: "guaranteeing" }) + : intl.formatMessage({ id: "paying" }) + return ( <FormProvider {...methods}> <form className={styles.paymentContainer} onSubmit={methods.handleSubmit(handleSubmit)} > + {mustBeGuaranteed ? ( + <section className={styles.section}> + <Body> + {intl.formatMessage({ + id: "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.", + })} + </Body> + <GuaranteeDetails /> + </section> + ) : null} {savedCreditCards?.length ? ( <section className={styles.section}> <Body color="uiTextHighContrast" textTransform="bold"> @@ -238,6 +254,7 @@ export default function Payment({ id: "booking.terms", }, { + paymentVerb, termsLink: (str) => ( <Link className={styles.link} diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index cbfec3e69..5e0b2446f 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -335,7 +335,7 @@ "Welcome": "Velkommen", "Welcome to": "Velkommen til", "Wellness & Exercise": "Velvære & Motion", - "What you have to do to guarantee booking": "Hvad du skal gøre for at garantere booking", + "What you have to do to guarantee booking:": "Hvad du skal gøre for at garantere booking:", "When": "Hvornår", "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "Når du garanterer din booking, vil vi holde bookingen indtil 07:00 til dagen efter check-in. Dette vil give dig som gæst tilføjet fleksibilitet til check-in-tider.", "Where should you go next?": "Find inspiration til dit næste ophold", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 901d47949..aaf57329f 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -334,7 +334,7 @@ "Welcome": "Willkommen", "Welcome to": "Willkommen zu", "Wellness & Exercise": "Wellness & Bewegung", - "What you have to do to guarantee booking": "Was Sie tun müssen, um eine Buchung zu garantieren", + "What you have to do to guarantee booking:": "Was Sie tun müssen, um eine Buchung zu garantieren:", "When": "Wann", "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "Wenn Sie Ihre Buchung garantieren, halten wir die Buchung bis 07:00 am Tag nach dem Check-in. Dies wird Ihnen als Gast zusätzliche Flexibilität für die Check-in-Zeiten gewähren.", "Where should you go next?": "Wo geht es als Nächstes hin?", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index fb7daa428..24d888259 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -351,7 +351,7 @@ "Welcome": "Welcome", "Welcome to": "Welcome to", "Wellness & Exercise": "Wellness & Exercise", - "What you have to do to guarantee booking": "What you have to do to guarantee booking", + "What you have to do to guarantee booking:": "What you have to do to guarantee booking:", "When": "When", "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.", "Where should you go next?": "Where should you go next?", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 6ae282cd1..fbd1682d6 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -336,7 +336,7 @@ "Welcome": "Tervetuloa", "Welcome to": "Tervetuloa", "Wellness & Exercise": "Hyvinvointi & Liikunta", - "What you have to do to guarantee booking": "Mitä sinun on tehtävä varmistaaksesi varauksen", + "What you have to do to guarantee booking:": "Mitä sinun on tehtävä varmistaaksesi varauksen:", "When": "Kun", "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "Jos varaat varauksen, pidämme varauksen 07:00 päivän jälkeen tarkistuspäivän jälkeen. Tämä tarjoaa sinulle lisään tarkistuspäivän aikaan.", "Where should you go next?": "Mihin menisit seuraavaksi?", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 5041708dc..c15bf403f 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -333,7 +333,7 @@ "Welcome": "Velkommen", "Welcome to": "Velkommen til", "Wellness & Exercise": "Velvære & Trening", - "What you have to do to guarantee booking": "Hva du må gjøre for å garantere reservasjonen", + "What you have to do to guarantee booking:": "Hva du må gjøre for å garantere reservasjonen:", "When": "Når", "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "Når du garanterer din reservasjon, vil vi holde reservasjonen til 07:00 til dagen etter check-in. Dette vil gi deg som gjest tilføjet fleksibilitet for check-in-tider.", "Where should you go next?": "Hvor ønsker du å reise neste gang?", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 8f3f0eeda..6ce4280fb 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -333,7 +333,7 @@ "Welcome": "Välkommen", "Welcome to": "Välkommen till", "Wellness & Exercise": "Hälsa & Träning", - "What you have to do to guarantee booking": "Vad du måste göra för att garantera bokningen", + "What you have to do to guarantee booking:": "Vad du måste göra för att garantera bokningen:", "When": "När", "When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.": "När du garanterar din bokning kommer vi att hålla bokningen till 07:00 till dagen efter check-in. Detta ger dig som gäst extra flexibilitet för check-in-tider.", "Where should you go next?": "Låter inte en spontanweekend härligt?", diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index e34f241ce..5aa915497 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -60,6 +60,26 @@ export const getHotelData = cache(async function getMemoizedHotelData( }) }) +export const getRoomAvailability = cache( + async function getMemoizedRoomAvailability( + hotelId: string, + adults: number, + roomStayStartDate: string, + roomStayEndDate: string, + children?: number, + promotionCode?: string + ) { + return serverClient().hotel.availability.rooms({ + hotelId: parseInt(hotelId), + adults, + roomStayStartDate, + roomStayEndDate, + children, + promotionCode, + }) + } +) + export const getFooter = cache(async function getMemoizedFooter() { return serverClient().contentstack.base.footer() }) diff --git a/types/components/hotelReservation/selectRate/section.ts b/types/components/hotelReservation/selectRate/section.ts index a02107caf..85dc2a55d 100644 --- a/types/components/hotelReservation/selectRate/section.ts +++ b/types/components/hotelReservation/selectRate/section.ts @@ -31,6 +31,7 @@ export interface PaymentProps { hotelId: string otherPaymentOptions: string[] savedCreditCards: CreditCard[] | null + mustBeGuaranteed: boolean } export interface SectionPageProps { From 7819db2bb213627d9267041e132a37215ab67282 Mon Sep 17 00:00:00 2001 From: Tobias Johansson <tobias.johansson@scandichotels.com> Date: Thu, 24 Oct 2024 14:45:25 +0200 Subject: [PATCH 34/73] fix(SW-614): update enter details strings --- .../(standard)/[step]/page.tsx | 27 ++++++++++++------- .../EnterDetails/Payment/index.tsx | 6 ++--- i18n/dictionaries/da.json | 5 ++++ i18n/dictionaries/de.json | 5 ++++ i18n/dictionaries/en.json | 7 ++++- i18n/dictionaries/fi.json | 5 ++++ i18n/dictionaries/no.json | 5 ++++ i18n/dictionaries/sv.json | 5 ++++ 8 files changed, 52 insertions(+), 13 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 45191398d..689a4d8de 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -58,38 +58,47 @@ export default async function StepPage({ const mustBeGuaranteed = false + const paymentGuarantee = intl.formatMessage({ + id: "Payment Guarantee", + }) + const payment = intl.formatMessage({ + id: "Payment", + }) + const guaranteeWithCard = intl.formatMessage({ + id: "Guarantee booking with credit card", + }) + const selectPaymentMethod = intl.formatMessage({ + id: "Select payment method", + }) + return ( <section> <HistoryStateManager /> <SectionAccordion - header="Select bed" + header={intl.formatMessage({ id: "Select bed" })} step={StepEnum.selectBed} label={intl.formatMessage({ id: "Request bedtype" })} > <BedType /> </SectionAccordion> <SectionAccordion - header="Food options" + header={intl.formatMessage({ id: "Food options" })} step={StepEnum.breakfast} label={intl.formatMessage({ id: "Select breakfast options" })} > <Breakfast packages={breakfastPackages} /> </SectionAccordion> <SectionAccordion - header="Details" + header={intl.formatMessage({ id: "Details" })} step={StepEnum.details} label={intl.formatMessage({ id: "Enter your details" })} > <Details user={user} /> </SectionAccordion> <SectionAccordion - header={mustBeGuaranteed ? "Payment Guarantee" : "Payment"} + header={mustBeGuaranteed ? paymentGuarantee : payment} step={StepEnum.payment} - label={ - mustBeGuaranteed - ? intl.formatMessage({ id: "Guarantee booking with credit card" }) - : intl.formatMessage({ id: "Select payment method" }) - } + label={mustBeGuaranteed ? guaranteeWithCard : selectPaymentMethod} > <Payment hotelId={searchParams.hotel} diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index f31ea816b..e29692993 100644 --- a/components/HotelReservation/EnterDetails/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -171,9 +171,9 @@ export default function Payment({ return <LoadingSpinner /> } - const paymentVerb = mustBeGuaranteed - ? intl.formatMessage({ id: "guaranteeing" }) - : intl.formatMessage({ id: "paying" }) + const guaranteeing = intl.formatMessage({ id: "guaranteeing" }) + const paying = intl.formatMessage({ id: "paying" }) + const paymentVerb = mustBeGuaranteed ? guaranteeing : paying return ( <FormProvider {...methods}> diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 5e0b2446f..67755749a 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -86,6 +86,7 @@ "Description": "Beskrivelse", "Destination": "Destination", "Destinations & hotels": "Destinationer & hoteller", + "Details": "Detaljer", "Disabled booking options header": "Vi beklager", "Disabled booking options text": "Koder, checks og bonusnætter er endnu ikke tilgængelige på den nye hjemmeside.", "Discard changes": "Kassér ændringer", @@ -116,6 +117,7 @@ "First name": "Fornavn", "Flexibility": "Fleksibilitet", "Follow us": "Følg os", + "Food options": "Madvalg", "Former Scandic Hotel": "Tidligere Scandic Hotel", "Free cancellation": "Gratis afbestilling", "Free rebooking": "Gratis ombooking", @@ -222,6 +224,8 @@ "Password": "Adgangskode", "Pay later": "Betal senere", "Pay now": "Betal nu", + "Payment": "Betaling", + "Payment Guarantee": "Garanti betaling", "Payment info": "Betalingsoplysninger", "Pet Room": "Kæledyrsrum", "Pet-friendly rooms have an additional fee of 20 EUR per stay": "Kæledyrsrum har en ekstra gebyr på 20 EUR per ophold", @@ -272,6 +276,7 @@ "See room details": "Se værelsesdetaljer", "See rooms": "Se værelser", "Select a country": "Vælg et land", + "Select bed": "Vælg seng", "Select breakfast options": "Vælg morgenmadsmuligheder", "Select country of residence": "Vælg bopælsland", "Select date of birth": "Vælg fødselsdato", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index aaf57329f..008d7654e 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -86,6 +86,7 @@ "Description": "Beschreibung", "Destination": "Bestimmungsort", "Destinations & hotels": "Reiseziele & Hotels", + "Details": "Details", "Disabled booking options header": "Es tut uns leid", "Disabled booking options text": "Codes, Schecks und Bonusnächte sind auf der neuen Website noch nicht verfügbar.", "Discard changes": "Änderungen verwerfen", @@ -116,6 +117,7 @@ "First name": "Vorname", "Flexibility": "Flexibilität", "Follow us": "Folgen Sie uns", + "Food options": "Speisen & Getränke", "Former Scandic Hotel": "Ehemaliges Scandic Hotel", "Free cancellation": "Kostenlose Stornierung", "Free rebooking": "Kostenlose Umbuchung", @@ -220,6 +222,8 @@ "Password": "Passwort", "Pay later": "Später bezahlen", "Pay now": "Jetzt bezahlen", + "Payment": "Zahlung", + "Payment Guarantee": "Zahlungsgarantie", "Payment info": "Zahlungsinformationen", "Pet Room": "Haustierzimmer", "Pet-friendly rooms have an additional fee of 20 EUR per stay": "Haustierzimmer haben einen zusätzlichen Preis von 20 EUR pro Aufenthalt", @@ -271,6 +275,7 @@ "See room details": "Zimmerdetails ansehen", "See rooms": "Zimmer ansehen", "Select a country": "Wähle ein Land", + "Select bed": "Betttyp auswählen", "Select breakfast options": "Wählen Sie Frühstücksoptionen", "Select country of residence": "Wählen Sie das Land Ihres Wohnsitzes aus", "Select date of birth": "Geburtsdatum auswählen", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 24d888259..e7443b158 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -92,6 +92,7 @@ "Description": "Description", "Destination": "Destination", "Destinations & hotels": "Destinations & hotels", + "Details": "Details", "Disabled booking options header": "We're sorry", "Disabled booking options text": "Codes, cheques and reward nights aren't available on the new website yet.", "Discard changes": "Discard changes", @@ -123,6 +124,7 @@ "First name": "First name", "Flexibility": "Flexibility", "Follow us": "Follow us", + "Food options": "Food options", "Former Scandic Hotel": "Former Scandic Hotel", "Free cancellation": "Free cancellation", "Free rebooking": "Free rebooking", @@ -231,6 +233,8 @@ "Password": "Password", "Pay later": "Pay later", "Pay now": "Pay now", + "Payment": "Payment", + "Payment Guarantee": "Payment Guarantee", "Payment info": "Payment info", "Payment received": "Payment received", "Pet Room": "Pet room", @@ -286,6 +290,7 @@ "See room details": "See room details", "See rooms": "See rooms", "Select a country": "Select a country", + "Select bed": "Select bed", "Select breakfast options": "Select breakfast options", "Select country of residence": "Select country of residence", "Select date of birth": "Select date of birth", @@ -320,9 +325,9 @@ "Theatre": "Theatre", "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", - "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 incl VAT": "Total incl VAT", "Total price": "Total price", "Tourist": "Tourist", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index fbd1682d6..65f7529b1 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -86,6 +86,7 @@ "Description": "Kuvaus", "Destination": "Kohde", "Destinations & hotels": "Kohteet ja hotellit", + "Details": "Tiedot", "Disabled booking options header": "Olemme pahoillamme", "Disabled booking options text": "Koodit, sekit ja palkintoillat eivät ole vielä saatavilla uudella verkkosivustolla.", "Discard changes": "Hylkää muutokset", @@ -116,6 +117,7 @@ "First name": "Etunimi", "Flexibility": "Joustavuus", "Follow us": "Seuraa meitä", + "Food options": "Ruokavalio", "Former Scandic Hotel": "Entinen Scandic-hotelli", "Free cancellation": "Ilmainen peruutus", "Free rebooking": "Ilmainen uudelleenvaraus", @@ -222,6 +224,8 @@ "Password": "Salasana", "Pay later": "Maksa myöhemmin", "Pay now": "Maksa nyt", + "Payment": "Maksu", + "Payment Guarantee": "Varmistusmaksu", "Payment info": "Maksutiedot", "Pet Room": "Lemmikkihuone", "Pet-friendly rooms have an additional fee of 20 EUR per stay": "Lemmikkihuoneen lisäkustannus on 20 EUR per majoitus", @@ -273,6 +277,7 @@ "See room details": "Katso huoneen tiedot", "See rooms": "Katso huoneet", "Select a country": "Valitse maa", + "Select bed": "Valitse vuodetyyppi", "Select breakfast options": "Valitse aamiaisvaihtoehdot", "Select country of residence": "Valitse asuinmaa", "Select date of birth": "Valitse syntymäaika", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index c15bf403f..f13a25268 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -85,6 +85,7 @@ "Description": "Beskrivelse", "Destination": "Destinasjon", "Destinations & hotels": "Destinasjoner og hoteller", + "Details": "Detaljer", "Disabled booking options header": "Vi beklager", "Disabled booking options text": "Koder, checks og belønningsnætter er enda ikke tilgjengelige på den nye nettsiden.", "Discard changes": "Forkaste endringer", @@ -115,6 +116,7 @@ "First name": "Fornavn", "Flexibility": "Fleksibilitet", "Follow us": "Følg oss", + "Food options": "Matvalg", "Former Scandic Hotel": "Tidligere Scandic-hotell", "Free cancellation": "Gratis avbestilling", "Free rebooking": "Gratis ombooking", @@ -220,6 +222,8 @@ "Password": "Passord", "Pay later": "Betal senere", "Pay now": "Betal nå", + "Payment": "Betaling", + "Payment Guarantee": "Garantera betalning", "Payment info": "Betalingsinformasjon", "Pet Room": "Kjæledyrsrom", "Pet-friendly rooms have an additional fee of 20 EUR per stay": "Kjæledyrsrom har en tilleggsavgift på 20 EUR per opphold", @@ -270,6 +274,7 @@ "See room details": "Se detaljer om rommet", "See rooms": "Se rom", "Select a country": "Velg et land", + "Select bed": "Vælg seng", "Select breakfast options": "Velg frokostalternativer", "Select country of residence": "Velg bostedsland", "Select date of birth": "Velg fødselsdato", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 6ce4280fb..b74e19c0b 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -85,6 +85,7 @@ "Description": "Beskrivning", "Destination": "Destination", "Destinations & hotels": "Destinationer & hotell", + "Details": "Detaljer", "Disabled booking options header": "Vi beklagar", "Disabled booking options text": "Koder, bonuscheckar och belöningsnätter är inte tillgängliga på den nya webbplatsen än.", "Discard changes": "Ignorera ändringar", @@ -115,6 +116,7 @@ "First name": "Förnamn", "Flexibility": "Flexibilitet", "Follow us": "Följ oss", + "Food options": "Matval", "Former Scandic Hotel": "Tidigare Scandichotell", "Free cancellation": "Fri avbokning", "Free rebooking": "Fri ombokning", @@ -220,6 +222,8 @@ "Password": "Lösenord", "Pay later": "Betala senare", "Pay now": "Betala nu", + "Payment": "Betalning", + "Payment Guarantee": "Garantera betalning", "Payment info": "Betalningsinformation", "Pet Room": "Husdjursrum", "Pet-friendly rooms have an additional fee of 20 EUR per stay": "Husdjursrum har en extra avgift på 20 EUR per vistelse", @@ -270,6 +274,7 @@ "See room details": "Se rumsdetaljer", "See rooms": "Se rum", "Select a country": "Välj ett land", + "Select bed": "Välj säng", "Select breakfast options": "Välj frukostalternativ", "Select country of residence": "Välj bosättningsland", "Select date of birth": "Välj födelsedatum", From 60ceeaf9c347080994976910438a0adc7016956b Mon Sep 17 00:00:00 2001 From: Tobias Johansson <tobias.johansson@scandichotels.com> Date: Mon, 28 Oct 2024 11:06:47 +0100 Subject: [PATCH 35/73] fix(SW-614): move filtering logic to routes --- .../(standard)/[step]/page.tsx | 25 +++++++++-------- lib/trpc/memoizedRequests/index.ts | 28 +++++++++++++------ server/routers/hotels/input.ts | 2 ++ server/routers/hotels/output.ts | 1 + server/routers/hotels/query.ts | 16 ++++++++++- 5 files changed, 52 insertions(+), 20 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 689a4d8de..7532c6673 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -22,7 +22,12 @@ import type { LangParams, PageArgs } from "@/types/params" export function preload() { void getProfileSafely() void getCreditCardsSafely() - void getRoomAvailability("811", 1, "2024-11-01", "2024-11-02") + void getRoomAvailability({ + hotelId: "811", + adults: 1, + roomStayStartDate: "2024-11-01", + roomStayEndDate: "2024-11-02", + }) } function isValidStep(step: string): step is StepEnum { @@ -45,12 +50,12 @@ export default async function StepPage({ const savedCreditCards = await getCreditCardsSafely() const breakfastPackages = await getBreakfastPackages(searchParams.hotel) - const roomAvailability = await getRoomAvailability( - searchParams.hotel, - Number(searchParams.adults), - searchParams.checkIn, - searchParams.checkOut - ) + const roomAvailability = await getRoomAvailability({ + hotelId: searchParams.hotel, + adults: Number(searchParams.adults), + roomStayStartDate: searchParams.checkIn, + roomStayEndDate: searchParams.checkOut, + }) if (!isValidStep(params.step) || !hotel || !roomAvailability) { return notFound() @@ -103,10 +108,8 @@ export default async function StepPage({ <Payment hotelId={searchParams.hotel} otherPaymentOptions={ - mustBeGuaranteed - ? [] - : hotel.data.attributes.merchantInformationData - .alternatePaymentOptions + hotel.data.attributes.merchantInformationData + .alternatePaymentOptions } savedCreditCards={savedCreditCards} mustBeGuaranteed={mustBeGuaranteed} diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index 5aa915497..742fb2606 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -52,23 +52,34 @@ export const getUserTracking = cache(async function getMemoizedUserTracking() { export const getHotelData = cache(async function getMemoizedHotelData( hotelId: string, - language: string + language: string, + isCardOnlyPayment?: boolean ) { return serverClient().hotel.hotelData.get({ hotelId, language, + isCardOnlyPayment, }) }) export const getRoomAvailability = cache( - async function getMemoizedRoomAvailability( - hotelId: string, - adults: number, - roomStayStartDate: string, - roomStayEndDate: string, - children?: number, + async function getMemoizedRoomAvailability({ + hotelId, + adults, + roomStayStartDate, + roomStayEndDate, + children, + promotionCode, + rateCode, + }: { + hotelId: string + adults: number + roomStayStartDate: string + roomStayEndDate: string + children?: number promotionCode?: string - ) { + rateCode?: string + }) { return serverClient().hotel.availability.rooms({ hotelId: parseInt(hotelId), adults, @@ -76,6 +87,7 @@ export const getRoomAvailability = cache( roomStayEndDate, children, promotionCode, + rateCode, }) } ) diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index 194a16496..2d69ab642 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -26,6 +26,7 @@ export const getRoomsAvailabilityInputSchema = z.object({ promotionCode: z.string().optional(), reservationProfileType: z.string().optional().default(""), attachedProfileId: z.string().optional().default(""), + rateCode: z.string().optional(), }) export const getRatesInputSchema = z.object({ @@ -35,6 +36,7 @@ export const getRatesInputSchema = z.object({ export const getlHotelDataInputSchema = z.object({ hotelId: z.string(), language: z.string(), + isCardOnlyPayment: z.boolean().optional(), include: z .array(z.enum(["RoomCategories", "NearbyHotels", "Restaurants", "City"])) .optional(), diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 84ab77171..b69207a7d 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -578,6 +578,7 @@ const roomsAvailabilitySchema = z hotelId: z.number(), roomConfigurations: z.array(roomConfigurationSchema), rateDefinitions: z.array(rateDefinitionSchema), + mustBeGuaranteed: z.boolean().optional(), }), relationships: linksSchema.optional(), type: z.string().optional(), diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index c783d0be5..6ad27655a 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -430,6 +430,7 @@ export const hotelQueryRouter = router({ promotionCode, reservationProfileType, attachedProfileId, + rateCode, } = input const params: Record<string, string | number | undefined> = { @@ -534,6 +535,14 @@ export const hotelQueryRouter = router({ query: { hotelId, params: params }, }) ) + + if (rateCode) { + validateAvailabilityData.data.mustBeGuaranteed = + validateAvailabilityData.data.rateDefinitions.filter( + (rate) => rate.rateCode === rateCode + )[0].mustBeGuaranteed + } + return validateAvailabilityData.data }), }), @@ -577,7 +586,7 @@ export const hotelQueryRouter = router({ get: serviceProcedure .input(getlHotelDataInputSchema) .query(async ({ ctx, input }) => { - const { hotelId, language, include } = input + const { hotelId, language, include, isCardOnlyPayment } = input const params: Record<string, string> = { hotelId, @@ -669,6 +678,11 @@ export const hotelQueryRouter = router({ }) ) + if (isCardOnlyPayment) { + validateHotelData.data.data.attributes.merchantInformationData.alternatePaymentOptions = + [] + } + return validateHotelData.data }), }), From b6bec433fb4505cf702fe5702dcc72e1ab9bc2ce Mon Sep 17 00:00:00 2001 From: Tobias Johansson <tobias.johansson@scandichotels.com> Date: Mon, 28 Oct 2024 12:56:22 +0100 Subject: [PATCH 36/73] fix(SW-614): use height prop on icon --- .../Payment/GuaranteeDetails/guaranteeDetails.module.css | 4 ---- .../EnterDetails/Payment/GuaranteeDetails/index.tsx | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/guaranteeDetails.module.css b/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/guaranteeDetails.module.css index a14bc3473..32e678fbe 100644 --- a/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/guaranteeDetails.module.css +++ b/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/guaranteeDetails.module.css @@ -20,7 +20,3 @@ .summary::marker { display: none; } - -.summary .icon { - height: 16px; -} diff --git a/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/index.tsx b/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/index.tsx index 99021715a..f1f8f6783 100644 --- a/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/index.tsx @@ -13,7 +13,7 @@ export default function GuaranteeDetails() { <Caption color="burgundy" type="bold" asChild> <summary className={styles.summary}> {intl.formatMessage({ id: "How it works" })} - <ChevronDown color="burgundy" className={styles.icon} /> + <ChevronDown color="burgundy" height={16} /> </summary> </Caption> <section className={styles.content}> From a5fad308ef225be91a610e5b5439c819f5c45ea1 Mon Sep 17 00:00:00 2001 From: Tobias Johansson <tobias.johansson@scandichotels.com> Date: Wed, 30 Oct 2024 10:06:59 +0100 Subject: [PATCH 37/73] fix(SW-614): remove room availabiltiy from preload --- .../hotelreservation/(standard)/[step]/page.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 7532c6673..264f5b04d 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -22,12 +22,6 @@ import type { LangParams, PageArgs } from "@/types/params" export function preload() { void getProfileSafely() void getCreditCardsSafely() - void getRoomAvailability({ - hotelId: "811", - adults: 1, - roomStayStartDate: "2024-11-01", - roomStayEndDate: "2024-11-02", - }) } function isValidStep(step: string): step is StepEnum { @@ -42,6 +36,12 @@ export default async function StepPage({ redirect(`/${params.lang}`) } void getBreakfastPackages(searchParams.hotel) + void getRoomAvailability({ + hotelId: searchParams.hotel, + adults: Number(searchParams.adults), + roomStayStartDate: searchParams.checkIn, + roomStayEndDate: searchParams.checkOut, + }) const intl = await getIntl() @@ -55,13 +55,14 @@ export default async function StepPage({ adults: Number(searchParams.adults), roomStayStartDate: searchParams.checkIn, roomStayEndDate: searchParams.checkOut, + rateCode: searchParams.rateCode, }) if (!isValidStep(params.step) || !hotel || !roomAvailability) { return notFound() } - const mustBeGuaranteed = false + const mustBeGuaranteed = roomAvailability?.mustBeGuaranteed ?? false const paymentGuarantee = intl.formatMessage({ id: "Payment Guarantee", From 795eae89bc14a038dd9b8b631b0f9ad8e3f4151c Mon Sep 17 00:00:00 2001 From: Tobias Johansson <tobias.johansson@scandichotels.com> Date: Wed, 30 Oct 2024 11:36:43 +0100 Subject: [PATCH 38/73] fix: type issue --- lib/trpc/memoizedRequests/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index 742fb2606..ec361b96c 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -76,7 +76,7 @@ export const getRoomAvailability = cache( adults: number roomStayStartDate: string roomStayEndDate: string - children?: number + children?: string promotionCode?: string rateCode?: string }) { From b5dce01fd3deb7f8336edeface595eebb128bd7b Mon Sep 17 00:00:00 2001 From: Bianca Widstam <bianca.widstam@scandichotels.com> Date: Wed, 30 Oct 2024 14:31:18 +0000 Subject: [PATCH 39/73] 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 <christel.westerberg@scandichotels.com> Date: Tue, 22 Oct 2024 16:19:15 +0200 Subject: [PATCH 40/73] 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<LangParams & { step: StepEnum }> & { hotelHeader: React.ReactNode sidePeek: React.ReactNode - } ->) { + }>) { setLang(params.lang) preload() return ( - <EnterDetailsProvider step={params.step}> + <EnterDetailsProvider step={params.step} > <main className={styles.layout}> {hotelHeader} <div className={styles.content}> <SelectedRoom /> {children} <aside className={styles.summary}> - <Summary /> + <Summary isMember={false} /> </aside> </div> {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 ( <Body textTransform="bold" className={classNames}> {isCalculated - ? formatter.format(awardPoints) + ? formatNumber(awardPoints) : intl.formatMessage({ id: "Points being calculated" })} </Body> ) 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({ <div className={styles.secondaryNavigation}> {appDownloads && ( <nav className={styles.secondaryNavigationGroup}> - <Body color="peach80" textTransform="uppercase"> + <Body color="baseTextMediumContrast" textTransform="uppercase"> {appDownloads.title} </Body> <ul className={styles.secondaryNavigationList}> @@ -50,7 +50,7 @@ export default function FooterSecondaryNav({ )} {secondaryLinks.map((link) => ( <nav className={styles.secondaryNavigationGroup} key={link.title}> - <Body color="peach80" textTransform="uppercase"> + <Body color="baseTextMediumContrast" textTransform="uppercase"> {link.title} </Body> <ul className={styles.secondaryNavigationList}> diff --git a/components/HotelReservation/EnterDetails/BedType/index.tsx b/components/HotelReservation/EnterDetails/BedType/index.tsx index e73eacf87..4811a17d5 100644 --- a/components/HotelReservation/EnterDetails/BedType/index.tsx +++ b/components/HotelReservation/EnterDetails/BedType/index.tsx @@ -14,18 +14,17 @@ import { bedTypeSchema } from "./schema" import styles from "./bedOptions.module.css" -import type { BedTypeSchema } from "@/types/components/enterDetails/bedType" -import { BedTypeEnum } from "@/types/enums/bedType" +import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" export default function BedType() { const intl = useIntl() - const bedType = useEnterDetailsStore((state) => state.data.bedType) + const bedType = useEnterDetailsStore((state) => state.userData.bedType) const methods = useForm<BedTypeSchema>({ defaultValues: bedType ? { - bedType, - } + bedType, + } : undefined, criteriaMode: "all", mode: "all", diff --git a/components/HotelReservation/EnterDetails/Breakfast/index.tsx b/components/HotelReservation/EnterDetails/Breakfast/index.tsx index 603a3aaad..e8868edf7 100644 --- a/components/HotelReservation/EnterDetails/Breakfast/index.tsx +++ b/components/HotelReservation/EnterDetails/Breakfast/index.tsx @@ -17,13 +17,13 @@ import styles from "./breakfast.module.css" import type { BreakfastFormSchema, BreakfastProps, -} from "@/types/components/enterDetails/breakfast" +} from "@/types/components/hotelReservation/enterDetails/breakfast" import { BreakfastPackageEnum } from "@/types/enums/breakfast" export default function Breakfast({ packages }: BreakfastProps) { const intl = useIntl() - const breakfast = useEnterDetailsStore((state) => state.data.breakfast) + const breakfast = useEnterDetailsStore((state) => state.userData.breakfast) let defaultValues = undefined if (breakfast === BreakfastPackageEnum.NO_BREAKFAST) { @@ -76,21 +76,21 @@ export default function Breakfast({ packages }: BreakfastProps) { subtitle={ pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST ? intl.formatMessage<React.ReactNode>( - { id: "breakfast.price.free" }, - { - amount: pkg.originalPrice, - currency: pkg.currency, - free: (str) => <Highlight>{str}</Highlight>, - strikethrough: (str) => <s>{str}</s>, - } - ) + { id: "breakfast.price.free" }, + { + amount: pkg.originalPrice, + currency: pkg.currency, + free: (str) => <Highlight>{str}</Highlight>, + strikethrough: (str) => <s>{str}</s>, + } + ) : intl.formatMessage( - { id: "breakfast.price" }, - { - amount: pkg.packagePrice, - currency: pkg.currency, - } - ) + { id: "breakfast.price" }, + { + amount: pkg.packagePrice, + currency: pkg.currency, + } + ) } text={intl.formatMessage({ id: "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.", diff --git a/components/HotelReservation/EnterDetails/Details/index.tsx b/components/HotelReservation/EnterDetails/Details/index.tsx index 35c16304b..1d5031aa8 100644 --- a/components/HotelReservation/EnterDetails/Details/index.tsx +++ b/components/HotelReservation/EnterDetails/Details/index.tsx @@ -22,21 +22,21 @@ import styles from "./details.module.css" import type { DetailsProps, DetailsSchema, -} from "@/types/components/enterDetails/details" +} from "@/types/components/hotelReservation/enterDetails/details" const formID = "enter-details" export default function Details({ user }: DetailsProps) { const intl = useIntl() const initialData = useEnterDetailsStore((state) => ({ - countryCode: state.data.countryCode, - email: state.data.email, - firstName: state.data.firstName, - lastName: state.data.lastName, - phoneNumber: state.data.phoneNumber, - join: state.data.join, - dateOfBirth: state.data.dateOfBirth, - zipCode: state.data.zipCode, - termsAccepted: state.data.termsAccepted, + countryCode: state.userData.countryCode, + email: state.userData.email, + firstName: state.userData.firstName, + lastName: state.userData.lastName, + phoneNumber: state.userData.phoneNumber, + join: state.userData.join, + dateOfBirth: state.userData.dateOfBirth, + zipCode: state.userData.zipCode, + termsAccepted: state.userData.termsAccepted, })) const methods = useForm<DetailsSchema>({ diff --git a/components/HotelReservation/EnterDetails/Provider/index.tsx b/components/HotelReservation/EnterDetails/Provider/index.tsx index 6e4d6cc2c..d5a6ec677 100644 --- a/components/HotelReservation/EnterDetails/Provider/index.tsx +++ b/components/HotelReservation/EnterDetails/Provider/index.tsx @@ -1,4 +1,5 @@ "use client" +import { useSearchParams } from "next/navigation" import { PropsWithChildren, useRef } from "react" import { @@ -7,15 +8,16 @@ import { initEditDetailsState, } from "@/stores/enter-details" -import { StepEnum } from "@/types/components/enterDetails/step" +import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step" export default function EnterDetailsProvider({ step, children, }: PropsWithChildren<{ step: StepEnum }>) { + const searchParams = useSearchParams() const initialStore = useRef<EnterDetailsStore>() if (!initialStore.current) { - initialStore.current = initEditDetailsState(step) + initialStore.current = initEditDetailsState(step, searchParams) } return ( diff --git a/components/HotelReservation/EnterDetails/SidePeek/index.tsx b/components/HotelReservation/EnterDetails/SidePeek/index.tsx index f7d280fed..257f7dbe8 100644 --- a/components/HotelReservation/EnterDetails/SidePeek/index.tsx +++ b/components/HotelReservation/EnterDetails/SidePeek/index.tsx @@ -14,7 +14,7 @@ import styles from "./enterDetailsSidePeek.module.css" import { SidePeekEnum, SidePeekProps, -} from "@/types/components/enterDetails/sidePeek" +} from "@/types/components/hotelReservation/enterDetails/sidePeek" export default function EnterDetailsSidePeek({ hotel }: SidePeekProps) { const activeSidePeek = useEnterDetailsStore((state) => state.activeSidePeek) diff --git a/components/HotelReservation/EnterDetails/Summary/ToggleSidePeek.tsx b/components/HotelReservation/EnterDetails/Summary/ToggleSidePeek.tsx index 2f022f857..771a5e75b 100644 --- a/components/HotelReservation/EnterDetails/Summary/ToggleSidePeek.tsx +++ b/components/HotelReservation/EnterDetails/Summary/ToggleSidePeek.tsx @@ -7,7 +7,7 @@ import { useEnterDetailsStore } from "@/stores/enter-details" import { ChevronRightSmallIcon } from "@/components/Icons" import Button from "@/components/TempDesignSystem/Button" -import { SidePeekEnum } from "@/types/components/enterDetails/sidePeek" +import { SidePeekEnum } from "@/types/components/hotelReservation/enterDetails/sidePeek" export default function ToggleSidePeek() { const intl = useIntl() diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index 18121dad3..363585848 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -1,154 +1,206 @@ +"use client" + +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 { getIntl } from "@/i18n" -import { getLang } from "@/i18n/serverContext" - -import ToggleSidePeek from "./ToggleSidePeek" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import useLang from "@/hooks/useLang" +import { formatNumber } from "@/utils/format" import styles from "./summary.module.css" -// TEMP -const rooms = [ - { - adults: 1, - type: "Cozy cabin", - }, -] +import { RoomsData } from "@/types/components/hotelReservation/enterDetails/bookingData" -export default async function Summary() { - const intl = await getIntl() - const lang = getLang() - const fromDate = dt().locale(lang).format("ddd, D MMM") - const toDate = dt().add(1, "day").locale(lang).format("ddd, D MMM") - const diff = dt(toDate).diff(fromDate, "days") +export default function Summary({ isMember }: { isMember: boolean }) { + const intl = useIntl() + const lang = useLang() + const { fromDate, toDate, rooms, hotel, 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 adults = intl.formatMessage( - { id: "booking.adults" }, - { totalAdults: totalAdults } + 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") + const nights = intl.formatMessage( { id: "booking.nights" }, { totalNights: diff } ) - const addOns = [ - { - price: intl.formatMessage({ id: "Included" }), - title: intl.formatMessage({ id: "King bed" }), - }, - { - price: intl.formatMessage({ id: "Included" }), - title: intl.formatMessage({ id: "Breakfast buffet" }), - }, - ] + if (isLoading) { + return <LoadingSpinner /> + } + 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 - const mappedRooms = Array.from( - rooms - .reduce((acc, room) => { - const currentRoom = acc.get(room.type) - acc.set(room.type, { - total: currentRoom ? currentRoom.total + 1 : 1, - type: room.type, - }) - return acc - }, new Map()) - .values() - ) + 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) return ( <section className={styles.summary}> <header> - <Body textTransform="bold"> - {mappedRooms.map( - (room, idx) => - `${room.total} x ${room.type}${mappedRooms.length > 1 && idx + 1 !== mappedRooms.length ? ", " : ""}` - )} + <Subtitle type="two">{intl.formatMessage({ id: "Summary" })}</Subtitle> + <Body className={styles.date} color="baseTextMediumContrast"> + {dt(fromDate).locale(lang).format("ddd, D MMM")} + <ArrowRightIcon color="peach80" height={15} width={15} /> + {dt(toDate).locale(lang).format("ddd, D MMM")} ({nights}) </Body> - <Body className={styles.date} color="textMediumContrast"> - {fromDate} - <ArrowRightIcon color="uiTextMediumContrast" height={15} width={15} /> - {toDate} - </Body> - - <ToggleSidePeek /> </header> <Divider color="primaryLightSubtle" /> <div className={styles.addOns}> - <div className={styles.entry}> - <Caption color="uiTextMediumContrast"> - {`${nights}, ${adults}`} - </Caption> - <Caption color="uiTextHighContrast"> - {intl.formatMessage( - { id: "{amount} {currency}" }, - { amount: "4536", currency: "SEK" } - )} - </Caption> - </div> - {addOns.map((addOn) => ( - <div className={styles.entry} key={addOn.title}> - <Caption color="uiTextMediumContrast">{addOn.title}</Caption> - <Caption color="uiTextHighContrast">{addOn.price}</Caption> - </div> + {populatedRooms.map((room, idx) => ( + <RoomBreakdown key={idx} room={room} isMember={isMember} /> ))} + + {bedType ? ( + <div className={styles.entry}> + <Body color="textHighContrast">{bedType}</Body> + <Caption color="red"> + {intl.formatMessage( + { id: "{amount} {currency}" }, + { amount: "0", currency: "SEK" } + )} + </Caption> + </div> + ) : null} + {breakfast ? ( + <div className={styles.entry}> + <Body color="textHighContrast">{breakfast}</Body> + <Caption color="red"> + {intl.formatMessage( + { id: "{amount} {currency}" }, + { amount: "0", currency: "SEK" } + )} + </Caption> + </div> + ) : null} </div> <Divider color="primaryLightSubtle" /> <div className={styles.total}> - <div> - <div className={styles.entry}> - <Body textTransform="bold"> - {intl.formatMessage({ id: "Total incl VAT" })} - </Body> - <Body textTransform="bold"> - {intl.formatMessage( - { id: "{amount} {currency}" }, - { amount: "4686", currency: "SEK" } - )} - </Body> - </div> - <div className={styles.entry}> - <Caption color="uiTextMediumContrast"> - {intl.formatMessage({ id: "Approx." })} - </Caption> - <Caption color="uiTextMediumContrast"> - {intl.formatMessage( - { id: "{amount} {currency}" }, - { amount: "455", currency: "EUR" } - )} - </Caption> - </div> + <div className={styles.entry}> + <Body textTransform="bold"> + {intl.formatMessage({ id: "Total price (incl VAT)" })} + </Body> + <Body textTransform="bold"> + {intl.formatMessage( + { id: "{amount} {currency}" }, + { amount: "4686", currency: "SEK" } + )} + </Body> </div> - <div> - <div className={styles.entry}> - <Body color="red" textTransform="bold"> - {intl.formatMessage({ id: "Member price" })} - </Body> - <Body color="red" textTransform="bold"> - {intl.formatMessage( - { id: "{amount} {currency}" }, - { amount: "4219", currency: "SEK" } - )} - </Body> - </div> - <div className={styles.entry}> - <Caption color="uiTextMediumContrast"> - {intl.formatMessage({ id: "Approx." })} - </Caption> - <Caption color="uiTextMediumContrast"> - {intl.formatMessage( - { id: "{amount} {currency}" }, - { amount: "412", currency: "EUR" } - )} - </Caption> - </div> + <div className={styles.entry}> + <Caption color="uiTextMediumContrast"> + {intl.formatMessage({ id: "Approx." })} + </Caption> + <Caption color="uiTextMediumContrast"> + {intl.formatMessage( + { id: "{amount} {currency}" }, + { amount: "455", currency: "EUR" } + )} + </Caption> </div> </div> </section> ) } + +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 ( + <div> + <div className={styles.entry}> + <Body color="textHighContrast">{room.roomType}</Body> + <Caption color={color}> + {intl.formatMessage( + { id: "{amount} {currency}" }, + { amount: price, currency: "SEK" } + )} + </Caption> + </div> + <Caption color="uiTextMediumContrast"> + {intl.formatMessage( + { id: "booking.adults" }, + { totalAdults: room.adults } + )} + </Caption> + {room.children?.length ? ( + <Caption color="uiTextMediumContrast"> + {intl.formatMessage( + { id: "booking.children" }, + { totalChildren: room.children.length } + )} + </Caption> + ) : null} + <Caption color="uiTextMediumContrast">{room.cancellationText}</Caption> + <Link color="burgundy" href="#" variant="underscored" size="small"> + {intl.formatMessage({ id: "Rate details" })} + </Link> + </div> + ) +} diff --git a/components/HotelReservation/EnterDetails/Summary/summary.module.css b/components/HotelReservation/EnterDetails/Summary/summary.module.css index 16ca412c5..472508e79 100644 --- a/components/HotelReservation/EnterDetails/Summary/summary.module.css +++ b/components/HotelReservation/EnterDetails/Summary/summary.module.css @@ -22,7 +22,7 @@ .addOns { display: flex; flex-direction: column; - gap: var(--Spacing-x1); + gap: var(--Spacing-x-one-and-half); } .entry { diff --git a/components/TempDesignSystem/Text/Body/body.module.css b/components/TempDesignSystem/Text/Body/body.module.css index 85b286b3a..e9c342d2b 100644 --- a/components/TempDesignSystem/Text/Body/body.module.css +++ b/components/TempDesignSystem/Text/Body/body.module.css @@ -92,7 +92,7 @@ color: var(--Primary-Dark-On-Surface-Accent); } -.peach80 { +.baseTextMediumContrast { color: var(--Base-Text-Medium-contrast); } diff --git a/components/TempDesignSystem/Text/Body/variants.ts b/components/TempDesignSystem/Text/Body/variants.ts index 324d1baef..3cf9a1376 100644 --- a/components/TempDesignSystem/Text/Body/variants.ts +++ b/components/TempDesignSystem/Text/Body/variants.ts @@ -16,7 +16,7 @@ const config = { textHighContrast: styles.textHighContrast, white: styles.white, peach50: styles.peach50, - peach80: styles.peach80, + 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 67755749a..fadfdedea 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -313,8 +313,8 @@ "Things nearby HOTEL_NAME": "Ting i nærheden af {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.": "For at sikre din reservation, beder vi om at du giver os dine betalingsoplysninger. Du kan så være sikker på, at ingen gebyrer vil blive opkrævet på dette tidspunkt.", "Total Points": "Samlet antal point", - "Total incl VAT": "Inkl. moms", "Total price": "Samlet pris", + "Total price (incl VAT)": "Samlet pris (inkl. moms)", "Tourist": "Turist", "Transaction date": "Overførselsdato", "Transactions": "Transaktioner", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 008d7654e..f7223efae 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -312,8 +312,8 @@ "Things nearby HOTEL_NAME": "Dinge in der Nähe von {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.": "Um Ihre Reservierung zu sichern, bitten wir Sie, Ihre Zahlungskarteninformationen zu geben. Sie können sicher sein, dass keine Gebühren zu diesem Zeitpunkt erhoben werden.", "Total Points": "Gesamtpunktzahl", - "Total incl VAT": "Gesamt inkl. MwSt.", "Total price": "Gesamtpreis", + "Total price (incl VAT)": "Gesamtpreis (inkl. MwSt.)", "Tourist": "Tourist", "Transaction date": "Transaktionsdatum", "Transactions": "Transaktionen", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index e7443b158..6cd2128c0 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -326,10 +326,10 @@ "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 incl VAT": "Total incl VAT", "Total price": "Total price", + "Total Points": "Total Points", + "Total price (incl VAT)": "Total price (incl VAT)", "Tourist": "Tourist", "Transaction date": "Transaction date", "Transactions": "Transactions", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 65f7529b1..f46700b16 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -314,8 +314,8 @@ "Things nearby HOTEL_NAME": "Lähellä olevia asioita {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.": "Varmistaaksesi varauksen, pyydämme sinua antamaan meille maksukortin tiedot. Varmista, että ei veloiteta maksusi tällä hetkellä.", "Total Points": "Kokonaispisteet", - "Total incl VAT": "Yhteensä sis. alv", "Total price": "Kokonaishinta", + "Total price (incl VAT)": "Kokonaishinta (sis. ALV)", "Tourist": "Turisti", "Transaction date": "Tapahtuman päivämäärä", "Transactions": "Tapahtumat", diff --git a/stores/enter-details.ts b/stores/enter-details.ts index b5c862d36..44289cc21 100644 --- a/stores/enter-details.ts +++ b/stores/enter-details.ts @@ -1,30 +1,34 @@ import { produce } from "immer" +import { ReadonlyURLSearchParams } from "next/navigation" import { createContext, useContext } from "react" 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 { BreakfastPackage } from "@/types/components/enterDetails/breakfast" -import { DetailsSchema } from "@/types/components/enterDetails/details" -import { SidePeekEnum } from "@/types/components/enterDetails/sidePeek" -import { StepEnum } from "@/types/components/enterDetails/step" +import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast" +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 { - data: { + userData: { bedType: BedTypeEnum | undefined breakfast: BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST | undefined } & DetailsSchema + roomData: BookingData steps: StepEnum[] currentStep: StepEnum activeSidePeek: SidePeekEnum | null isValid: Record<StepEnum, boolean> - completeStep: (updatedData: Partial<EnterDetailsState["data"]>) => void + completeStep: (updatedData: Partial<EnterDetailsState["userData"]>) => void navigate: ( step: StepEnum, updatedData?: Record<string, string | boolean | BreakfastPackage> @@ -34,13 +38,60 @@ interface EnterDetailsState { closeSidePeek: () => void } -export function initEditDetailsState(currentStep: StepEnum) { +function getUpdatedValue<T>( + 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 +) { const isBrowser = typeof window !== "undefined" const sessionData = isBrowser ? sessionStorage.getItem(SESSION_STORAGE_KEY) : null - const defaultData: EnterDetailsState["data"] = { + 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 + ), + })) + } + + const defaultUserData: EnterDetailsState["userData"] = { bedType: undefined, breakfast: undefined, countryCode: "", @@ -54,14 +105,14 @@ export function initEditDetailsState(currentStep: StepEnum) { termsAccepted: false, } - let inputData = {} + let inputUserData = {} if (sessionData) { - inputData = JSON.parse(sessionData) + inputUserData = JSON.parse(sessionData) } const validPaths = [StepEnum.selectBed] - let initialData: EnterDetailsState["data"] = defaultData + let initialData: EnterDetailsState["userData"] = defaultUserData const isValid = { [StepEnum.selectBed]: false, @@ -70,19 +121,19 @@ export function initEditDetailsState(currentStep: StepEnum) { [StepEnum.payment]: false, } - const validatedBedType = bedTypeSchema.safeParse(inputData) + const validatedBedType = bedTypeSchema.safeParse(inputUserData) if (validatedBedType.success) { validPaths.push(StepEnum.breakfast) initialData = { ...initialData, ...validatedBedType.data } isValid[StepEnum.selectBed] = true } - const validatedBreakfast = breakfastStoreSchema.safeParse(inputData) + const validatedBreakfast = breakfastStoreSchema.safeParse(inputUserData) if (validatedBreakfast.success) { validPaths.push(StepEnum.details) initialData = { ...initialData, ...validatedBreakfast.data } isValid[StepEnum.breakfast] = true } - const validatedDetails = detailsSchema.safeParse(inputData) + const validatedDetails = detailsSchema.safeParse(inputUserData) if (validatedDetails.success) { validPaths.push(StepEnum.payment) initialData = { ...initialData, ...validatedDetails.data } @@ -101,7 +152,8 @@ export function initEditDetailsState(currentStep: StepEnum) { } return create<EnterDetailsState>()((set, get) => ({ - data: initialData, + userData: initialData, + roomData, steps: Object.values(StepEnum), setCurrentStep: (step) => set({ currentStep: step }), navigate: (step, updatedData) => @@ -129,14 +181,17 @@ export function initEditDetailsState(currentStep: StepEnum) { isValid, completeStep: (updatedData) => set( - produce((state) => { + produce((state: EnterDetailsState) => { state.isValid[state.currentStep] = true const nextStep = state.steps[state.steps.indexOf(state.currentStep) + 1] - state.data = { ...state.data, ...updatedData } - + // @ts-expect-error: ts has a hard time understanding that "false | true" equals "boolean" + state.userData = { + ...state.userData, + ...updatedData, + } state.currentStep = nextStep get().navigate(nextStep, updatedData) }) diff --git a/types/components/enterDetails/bedType.ts b/types/components/hotelReservation/enterDetails/bedType.ts similarity index 100% rename from types/components/enterDetails/bedType.ts rename to types/components/hotelReservation/enterDetails/bedType.ts diff --git a/types/components/hotelReservation/enterDetails/bookingData.ts b/types/components/hotelReservation/enterDetails/bookingData.ts new file mode 100644 index 000000000..4b4a72898 --- /dev/null +++ b/types/components/hotelReservation/enterDetails/bookingData.ts @@ -0,0 +1,27 @@ +interface Child { + bed: string + age: number +} + +interface Room { + adults: number + roomtypecode: string + ratecode: string + child: Child[] +} + +export interface BookingData { + hotel: string + fromdate: string + todate: string + room: Room[] +} + +export type RoomsData = { + roomType: string + memberPrice: string | undefined + publicPrice: string | undefined + adults: number + children: Child[] + cancellationText: string | undefined +} diff --git a/types/components/enterDetails/breakfast.ts b/types/components/hotelReservation/enterDetails/breakfast.ts similarity index 100% rename from types/components/enterDetails/breakfast.ts rename to types/components/hotelReservation/enterDetails/breakfast.ts diff --git a/types/components/enterDetails/details.ts b/types/components/hotelReservation/enterDetails/details.ts similarity index 100% rename from types/components/enterDetails/details.ts rename to types/components/hotelReservation/enterDetails/details.ts diff --git a/types/components/enterDetails/sidePeek.ts b/types/components/hotelReservation/enterDetails/sidePeek.ts similarity index 100% rename from types/components/enterDetails/sidePeek.ts rename to types/components/hotelReservation/enterDetails/sidePeek.ts diff --git a/types/components/enterDetails/step.ts b/types/components/hotelReservation/enterDetails/step.ts similarity index 100% rename from types/components/enterDetails/step.ts rename to types/components/hotelReservation/enterDetails/step.ts diff --git a/types/components/hotelReservation/selectRate/sectionAccordion.ts b/types/components/hotelReservation/selectRate/sectionAccordion.ts index 7e85bf768..46b194db3 100644 --- a/types/components/hotelReservation/selectRate/sectionAccordion.ts +++ b/types/components/hotelReservation/selectRate/sectionAccordion.ts @@ -1,4 +1,4 @@ -import { StepEnum } from "../../enterDetails/step" +import { StepEnum } from "../enterDetails/step" export interface SectionAccordionProps { header: string diff --git a/utils/format.ts b/utils/format.ts new file mode 100644 index 000000000..7a731f09e --- /dev/null +++ b/utils/format.ts @@ -0,0 +1,8 @@ +import { Lang } from "@/constants/languages" + +/// Function to format numbers with space as thousands separator +export function formatNumber(num: number) { + // sv hardcoded to force space on thousands + const formatter = new Intl.NumberFormat(Lang.sv) + return formatter.format(num) +} From 7954c704d939cfff9e7e72600e4d43e1a6b39d27 Mon Sep 17 00:00:00 2001 From: Christel Westerberg <christel.westerberg@scandichotels.com> Date: Thu, 24 Oct 2024 10:53:05 +0200 Subject: [PATCH 41/73] feat: consume serach params in summary and step page --- .../(standard)/[step]/@summary/page.tsx | 77 ++++++ .../(standard)/[step]/layout.tsx | 7 +- .../(standard)/[step]/page.tsx | 91 ++++--- .../(standard)/select-hotel/page.tsx | 2 +- .../(standard)/select-rate/page.tsx | 5 +- components/BookingWidget/Client.tsx | 15 +- .../EnterDetails/BedType/index.tsx | 64 +++-- .../EnterDetails/BedType/schema.ts | 2 +- .../EnterDetails/Payment/index.tsx | 2 +- .../EnterDetails/Summary/index.tsx | 226 ++++++++---------- .../EnterDetails/Summary/summary.module.css | 8 +- .../SelectRate/RoomSelection/index.tsx | 2 +- .../SelectRate/RoomSelection/utils.ts | 15 +- .../TempDesignSystem/Text/Body/variants.ts | 1 - i18n/dictionaries/da.json | 8 +- i18n/dictionaries/de.json | 8 +- i18n/dictionaries/en.json | 9 +- i18n/dictionaries/fi.json | 8 +- i18n/dictionaries/no.json | 8 +- i18n/dictionaries/sv.json | 8 +- lib/trpc/memoizedRequests/index.ts | 20 +- server/routers/hotels/input.ts | 17 +- server/routers/hotels/query.ts | 4 +- server/routers/hotels/schemas/room.ts | 3 + stores/enter-details.ts | 9 +- .../enterDetails/bookingData.ts | 18 +- .../hotelReservation/selectRate/selectRate.ts | 2 +- 27 files changed, 376 insertions(+), 263 deletions(-) create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx new file mode 100644 index 000000000..4a15e3994 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx @@ -0,0 +1,77 @@ +import { + getHotelData, + getProfileSafely, + getRoomAvailability, +} 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" + +export default async function SummaryPage({ + params, + searchParams, +}: PageArgs<LangParams, SearchParams<SelectRateSearchParams>>) { + const selectRoomParams = new URLSearchParams(searchParams) + 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, + }) + + if (!hotelData?.data || !hotelData?.included || !availability) { + console.error("No hotel or availability data", hotelData, availability) + + // TODO: handle this case + return null + } + + const chosenRoom = availability.roomConfigurations.find( + (availRoom) => availRoom.roomTypeCode === roomTypeCode + ) + + if (!chosenRoom) { + // TODO: handle this case + console.error("No chosen room", chosenRoom) + return null + } + + const cancellationText = + availability?.rateDefinitions.find((rate) => rate.rateCode === rateCode) + ?.cancellationText ?? "" + + const memberPrice = + chosenRoom.products.find( + (rate) => rate.productType.member?.rateCode === rateCode + )?.productType.member?.localPrice.pricePerStay ?? "0" + + const publicPrice = + chosenRoom.products.find( + (rate) => rate.productType.public?.rateCode === rateCode + )?.productType.public?.localPrice.pricePerStay ?? "0" + + const price = user ? memberPrice : publicPrice + + return ( + <Summary + isMember={!!user} + room={{ + roomType: chosenRoom.roomType, + price: formatNumber(parseInt(price)), + adults, + cancellationText, + }} + /> + ) +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx index 28a2bc570..85c4c7c39 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx @@ -1,6 +1,5 @@ import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider" import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom" -import Summary from "@/components/HotelReservation/EnterDetails/Summary" import { setLang } from "@/i18n/serverContext" import { preload } from "./page" @@ -11,6 +10,7 @@ import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step" import type { LangParams, LayoutArgs } from "@/types/params" export default async function StepLayout({ + summary, children, hotelHeader, params, @@ -19,6 +19,7 @@ export default async function StepLayout({ LayoutArgs<LangParams & { step: StepEnum }> & { hotelHeader: React.ReactNode sidePeek: React.ReactNode + summary: React.ReactNode }>) { setLang(params.lang) preload() @@ -29,9 +30,7 @@ export default async function StepLayout({ <div className={styles.content}> <SelectedRoom /> {children} - <aside className={styles.summary}> - <Summary isMember={false} /> - </aside> + <aside className={styles.summary}>{summary}</aside> </div> {sidePeek} </main> 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<LangParams & { step: StepEnum }, { hotel: string }>) { +}: 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 ( <section> <HistoryStateManager /> - <SectionAccordion - header={intl.formatMessage({ id: "Select bed" })} - step={StepEnum.selectBed} - label={intl.formatMessage({ id: "Request bedtype" })} - > - <BedType /> - </SectionAccordion> + + {/* TODO: How to handle no beds found? */} + {roomTypes ? ( + <SectionAccordion + header="Select bed" + step={StepEnum.selectBed} + label={intl.formatMessage({ id: "Request bedtype" })} + > + <BedType roomTypes={roomTypes} /> + </SectionAccordion> + ) : null} + <SectionAccordion header={intl.formatMessage({ id: "Food options" })} step={StepEnum.breakfast} @@ -111,13 +146,13 @@ export default async function StepPage({ <Payment hotelId={searchParams.hotel} otherPaymentOptions={ - hotel.data.attributes.merchantInformationData + hotelData.data.attributes.merchantInformationData .alternatePaymentOptions } savedCreditCards={savedCreditCards} mustBeGuaranteed={mustBeGuaranteed} /> </SectionAccordion> - </section> + </section > ) } 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<BookingWidgetSchema>({ 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 ( <FormProvider {...methods}> <form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}> - <RadioCard - Icon={KingBedIcon} - iconWidth={46} - id={BedTypeEnum.KING} - name="bedType" - subtitle={intl.formatMessage( - { id: "{width} cm × {length} cm" }, - { - length: "210", - width: "180", - } - )} - text={text} - title={intl.formatMessage({ id: "King bed" })} - value={BedTypeEnum.KING} - /> - <RadioCard - Icon={KingBedIcon} - iconWidth={46} - id={BedTypeEnum.QUEEN} - name="bedType" - subtitle={intl.formatMessage( - { id: "{width} cm × {length} cm" }, - { - length: "200", - width: "160", - } - )} - text={text} - title={intl.formatMessage({ id: "Queen bed" })} - value={BedTypeEnum.QUEEN} - /> + {roomTypes.map((roomType) => { + const width = + roomType.size.max === roomType.size.min + ? roomType.size.max + : `${roomType.size.min} cm - ${roomType.size.max} cm` + return ( + <RadioCard + key={roomType.value} + Icon={KingBedIcon} + iconWidth={46} + id={roomType.value} + name="bedType" + subtitle={width} + text={text} + title={roomType.description} + value={roomType.description} + /> + ) + })} </form> </FormProvider> ) 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<string>("") const methods = useForm<PaymentFormData>({ 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<string | undefined>() + const [chosenBreakfast, setCosenBreakfast] = useState<string | undefined>() 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 <LoadingSpinner /> + 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 ( <section className={styles.summary}> @@ -100,14 +69,48 @@ export default function Summary({ isMember }: { isMember: boolean }) { </header> <Divider color="primaryLightSubtle" /> <div className={styles.addOns}> - {populatedRooms.map((room, idx) => ( - <RoomBreakdown key={idx} room={room} isMember={isMember} /> - ))} - - {bedType ? ( + <div> <div className={styles.entry}> - <Body color="textHighContrast">{bedType}</Body> - <Caption color="red"> + <Body color="textHighContrast">{room.roomType}</Body> + <Caption color={color}> + {intl.formatMessage( + { id: "{amount} {currency}" }, + { amount: room.price, currency: "SEK" } + )} + </Caption> + </div> + <Caption color="uiTextMediumContrast"> + {intl.formatMessage( + { id: "booking.adults" }, + { totalAdults: room.adults } + )} + </Caption> + {room.children?.length ? ( + <Caption color="uiTextMediumContrast"> + {intl.formatMessage( + { id: "booking.children" }, + { totalChildren: room.children.length } + )} + </Caption> + ) : null} + <Caption color="uiTextMediumContrast"> + {room.cancellationText} + </Caption> + <Link color="burgundy" href="#" variant="underscored" size="small"> + {intl.formatMessage({ id: "Rate details" })} + </Link> + </div> + + {chosenBed ? ( + <div className={styles.entry}> + <div> + <Body color="textHighContrast">{chosenBed}</Body> + <Caption color="uiTextMediumContrast"> + {intl.formatMessage({ id: "Based on availability" })} + </Caption> + </div> + + <Caption color="uiTextMediumContrast"> {intl.formatMessage( { id: "{amount} {currency}" }, { amount: "0", currency: "SEK" } @@ -115,10 +118,11 @@ export default function Summary({ isMember }: { isMember: boolean }) { </Caption> </div> ) : null} - {breakfast ? ( + + {chosenBreakfast ? ( <div className={styles.entry}> - <Body color="textHighContrast">{breakfast}</Body> - <Caption color="red"> + <Body color="textHighContrast">{chosenBreakfast}</Body> + <Caption color="uiTextMediumContrast"> {intl.formatMessage( { id: "{amount} {currency}" }, { amount: "0", currency: "SEK" } @@ -130,77 +134,35 @@ export default function Summary({ isMember }: { isMember: boolean }) { <Divider color="primaryLightSubtle" /> <div className={styles.total}> <div className={styles.entry}> - <Body textTransform="bold"> - {intl.formatMessage({ id: "Total price (incl VAT)" })} - </Body> - <Body textTransform="bold"> - {intl.formatMessage( - { id: "{amount} {currency}" }, - { amount: "4686", currency: "SEK" } - )} - </Body> - </div> - <div className={styles.entry}> - <Caption color="uiTextMediumContrast"> - {intl.formatMessage({ id: "Approx." })} - </Caption> - <Caption color="uiTextMediumContrast"> - {intl.formatMessage( - { id: "{amount} {currency}" }, - { amount: "455", currency: "EUR" } - )} - </Caption> + <div> + <Body> + {intl.formatMessage<React.ReactNode>( + { id: "<b>Total price</b> (incl VAT)" }, + { b: (str) => <b>{str}</b> } + )} + </Body> + <Link color="burgundy" href="#" variant="underscored" size="small"> + {intl.formatMessage({ id: "Price details" })} + </Link> + </div> + <div> + <Body textTransform="bold"> + {intl.formatMessage( + { id: "{amount} {currency}" }, + { amount: room.price, currency: "SEK" } // TODO: calculate total price + )} + </Body> + <Caption color="uiTextMediumContrast"> + {intl.formatMessage({ id: "Approx." })}{" "} + {intl.formatMessage( + { id: "{amount} {currency}" }, + { amount: "455", currency: "EUR" } + )} + </Caption> + </div> </div> + <Divider color="primaryLightSubtle" /> </div> </section> ) } - -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 ( - <div> - <div className={styles.entry}> - <Body color="textHighContrast">{room.roomType}</Body> - <Caption color={color}> - {intl.formatMessage( - { id: "{amount} {currency}" }, - { amount: price, currency: "SEK" } - )} - </Caption> - </div> - <Caption color="uiTextMediumContrast"> - {intl.formatMessage( - { id: "booking.adults" }, - { totalAdults: room.adults } - )} - </Caption> - {room.children?.length ? ( - <Caption color="uiTextMediumContrast"> - {intl.formatMessage( - { id: "booking.children" }, - { totalChildren: room.children.length } - )} - </Caption> - ) : null} - <Caption color="uiTextMediumContrast">{room.cancellationText}</Caption> - <Link color="burgundy" href="#" variant="underscored" size="small"> - {intl.formatMessage({ id: "Rate details" })} - </Link> - </div> - ) -} 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 @@ { "<b>Included</b> (based on availability)": "<b>Inkluderet</b> (baseret på tilgængelighed)", + "<b>Total price</b> (incl VAT)": "<b>Samlet pris</b> (inkl. moms)", + "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/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 @@ { "<b>Included</b> (based on availability)": "<b>Inbegriffen</b> (je nach Verfügbarkeit)", + "<b>Total price</b> (incl VAT)": "<b>Gesamtpreis</b> (inkl. MwSt.)", + "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/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 @@ { "<b>Included</b> (based on availability)": "<b>Included</b> (based on availability)", + "<b>Total price</b> (incl VAT)": "<b>Total price</b> (incl VAT)", + "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/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 @@ { "<b>Included</b> (based on availability)": "<b>Sisältyy</b> (saatavuuden mukaan)", + "<b>Total price</b> (incl VAT)": "<b>Kokonaishinta</b> (sis. ALV)", + "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/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 @@ { "<b>Included</b> (based on availability)": "<b>Inkludert</b> (basert på tilgjengelighet)", + "<b>Total price</b> (incl VAT)": "<b>Totalpris</b> (inkl. mva)", + "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/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 @@ { "<b>Included</b> (based on availability)": "<b>Ingår</b> (baserat på tillgänglighet)", + "<b>Total price</b> (incl VAT)": "<b>Totalpris</b> (inkl moms)", + "<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/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<z.output<typeof roomSchema>, "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 <christel.westerberg@scandichotels.com> Date: Fri, 25 Oct 2024 11:09:03 +0200 Subject: [PATCH 42/73] 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 ( <Summary isMember={!!user} room={{ roomType: chosenRoom.roomType, - price: formatNumber(parseInt(price)), + localPrice: formatNumber(parseInt(prices.local ?? "0")), + euroPrice: formatNumber(parseInt(prices.euro ?? "0")), adults, cancellationText, }} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.module.css index 4f337ccb2..296eea04d 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.module.css @@ -1,5 +1,4 @@ .layout { - min-height: 100dvh; background-color: var(--Scandic-Brand-Warm-White); } @@ -9,7 +8,6 @@ grid-template-columns: 1fr 340px; grid-template-rows: auto 1fr; margin: var(--Spacing-x5) auto 0; - padding-top: var(--Spacing-x6); /* simulates padding on viewport smaller than --max-width-navigation */ width: min( calc(100dvw - (var(--Spacing-x2) * 2)), @@ -17,8 +15,81 @@ ); } -.summary { - align-self: flex-start; +.summaryContainer { grid-column: 2 / 3; grid-row: 1/-1; } + +.summary { + background-color: var(--Main-Grey-White); + + border-color: var(--Primary-Light-On-Surface-Divider-subtle); + border-style: solid; + border-width: 1px; + border-radius: var(--Corner-radius-Large); + + z-index: 1; +} + +.hider { + display: none; +} + +.shadow { + display: none; +} + +@media screen and (min-width: 950px) { + .summaryContainer { + display: grid; + grid-template-rows: auto auto 1fr; + margin-top: calc(0px - var(--Spacing-x9)); + } + + .summary { + position: sticky; + top: calc( + var(--booking-widget-desktop-height) + + var(--booking-widget-desktop-height) + var(--Spacing-x-one-and-half) + ); + margin-top: calc(0px - var(--Spacing-x9)); + border-bottom: none; + border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; + } + + .hider { + display: block; + background-color: var(--Scandic-Brand-Warm-White); + position: sticky; + margin-top: var(--Spacing-x4); + top: calc( + var(--booking-widget-desktop-height) + + var(--booking-widget-desktop-height) - 6px + ); + height: 40px; + } + + .shadow { + display: block; + background-color: var(--Main-Grey-White); + border-color: var(--Primary-Light-On-Surface-Divider-subtle); + border-style: solid; + border-left-width: 1px; + border-right-width: 1px; + border-top: none; + border-bottom: none; + } +} + +@media screen and (min-width: 1367px) { + .summary { + top: calc( + var(--booking-widget-desktop-height) + var(--Spacing-x2) + + var(--Spacing-x-half) + ); + } + + .hider { + top: calc(var(--booking-widget-desktop-height) - 6px); + } +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx index 85c4c7c39..c38349b26 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx @@ -30,7 +30,11 @@ export default async function StepLayout({ <div className={styles.content}> <SelectedRoom /> {children} - <aside className={styles.summary}>{summary}</aside> + <aside className={styles.summaryContainer}> + <div className={styles.hider} /> + <div className={styles.summary}>{summary}</div> + <div className={styles.shadow} /> + </aside> </div> {sidePeek} </main> 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<LangParams & { step: StepEnum }, SelectRateSearchParams>) { + 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} /> </SectionAccordion> - </section > + </section> ) } 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<React.ReactNode>( - { id: "<b>Included</b> (based on availability)" }, - { b: (str) => <b>{str}</b> } - ) 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 ( <RadioCard @@ -81,7 +77,6 @@ export default function BedType({ id={roomType.value} name="bedType" subtitle={width} - text={text} title={roomType.description} value={roomType.description} /> 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<string | undefined>() - const [chosenBreakfast, setCosenBreakfast] = useState<string | undefined>() + const [chosenBed, setChosenBed] = useState<string>() + const [chosenBreakfast, setCosenBreakfast] = useState<string>() 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({ <Caption color={color}> {intl.formatMessage( { id: "{amount} {currency}" }, - { amount: room.price, currency: "SEK" } + { amount: room.localPrice, currency: "SEK" } )} </Caption> </div> @@ -121,7 +126,9 @@ export default function Summary({ {chosenBreakfast ? ( <div className={styles.entry}> - <Body color="textHighContrast">{chosenBreakfast}</Body> + <Body color="textHighContrast"> + {intl.formatMessage({ id: chosenBreakfast })} + </Body> <Caption color="uiTextMediumContrast"> {intl.formatMessage( { id: "{amount} {currency}" }, @@ -149,14 +156,14 @@ export default function Summary({ <Body textTransform="bold"> {intl.formatMessage( { id: "{amount} {currency}" }, - { amount: room.price, currency: "SEK" } // TODO: calculate total price + { amount: room.localPrice, currency: "SEK" } // TODO: calculate total price )} </Body> <Caption color="uiTextMediumContrast"> {intl.formatMessage({ id: "Approx." })}{" "} {intl.formatMessage( { id: "{amount} {currency}" }, - { amount: "455", currency: "EUR" } + { amount: room.euroPrice, currency: "EUR" } )} </Caption> </div> 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<T>( - 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 <christel.westerberg@scandichotels.com> Date: Wed, 30 Oct 2024 15:21:51 +0100 Subject: [PATCH 43/73] 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 ( <Summary isMember={!!user} room={{ - roomType: chosenRoom.roomType, - localPrice: formatNumber(parseInt(prices.local ?? "0")), - euroPrice: formatNumber(parseInt(prices.euro ?? "0")), + roomType: availability.selectedRoom.roomType, + localPrice: prices.local, + euroPrice: prices.euro, adults, - cancellationText, + cancellationText: availability.cancellationText, }} /> ) 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<LangParams & { step: StepEnum }, SelectRateSearchParams>) { 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({ <HistoryStateManager /> {/* TODO: How to handle no beds found? */} - {roomTypes ? ( + {bedTypes ? ( <SectionAccordion header="Select bed" step={StepEnum.selectBed} label={intl.formatMessage({ id: "Request bedtype" })} > - <BedType roomTypes={roomTypes} /> + <BedType bedTypes={bedTypes} /> </SectionAccordion> ) : 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<BedTypeSchema>({ defaultValues: bedType ? { - bedType, - } + bedType, + } : undefined, criteriaMode: "all", mode: "all", @@ -64,7 +56,7 @@ export default function BedType({ return ( <FormProvider {...methods}> <form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}> - {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<string>() - const [chosenBreakfast, setCosenBreakfast] = useState<string>() + 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({ <Caption color={color}> {intl.formatMessage( { id: "{amount} {currency}" }, - { amount: room.localPrice, currency: "SEK" } + { + amount: formatNumber(parseInt(room.localPrice.price ?? "0")), + currency: room.localPrice.currency, + } )} </Caption> </div> @@ -118,24 +122,41 @@ export default function Summary({ <Caption color="uiTextMediumContrast"> {intl.formatMessage( { id: "{amount} {currency}" }, - { amount: "0", currency: "SEK" } + { amount: "0", currency: room.localPrice.currency } )} </Caption> </div> ) : null} {chosenBreakfast ? ( - <div className={styles.entry}> - <Body color="textHighContrast"> - {intl.formatMessage({ id: chosenBreakfast })} - </Body> - <Caption color="uiTextMediumContrast"> - {intl.formatMessage( - { id: "{amount} {currency}" }, - { amount: "0", currency: "SEK" } - )} - </Caption> - </div> + chosenBreakfast === BreakfastPackageEnum.NO_BREAKFAST ? ( + <div className={styles.entry}> + <Body color="textHighContrast"> + {intl.formatMessage({ id: "No breakfast" })} + </Body> + <Caption color="uiTextMediumContrast"> + {intl.formatMessage( + { id: "{amount} {currency}" }, + { amount: "0", currency: room.localPrice.currency } + )} + </Caption> + </div> + ) : ( + <div className={styles.entry}> + <Body color="textHighContrast"> + {intl.formatMessage({ id: "Breakfast buffet" })} + </Body> + <Caption color="uiTextMediumContrast"> + {intl.formatMessage( + { id: "{amount} {currency}" }, + { + amount: chosenBreakfast.totalPrice, + currency: chosenBreakfast.currency, + } + )} + </Caption> + </div> + ) ) : null} </div> <Divider color="primaryLightSubtle" /> @@ -156,14 +177,20 @@ export default function Summary({ <Body textTransform="bold"> {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, + } )} </Body> <Caption color="uiTextMediumContrast"> {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, + } )} </Caption> </div> 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<ListCardProps, "type"> @@ -34,6 +39,7 @@ export type CheckboxProps = export type RadioProps = | Omit<ListCardProps, "type"> | Omit<TextCardProps, "type"> + | Omit<CleanCardProps, "type"> export interface ListProps extends Pick<ListCardProps, "declined"> { 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<string, string | number | undefined> = { + 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<typeof bedTypeSchema> {} 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 <christel.westerberg@scandichotels.com> Date: Wed, 30 Oct 2024 16:39:04 +0100 Subject: [PATCH 44/73] 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?= <matilda.landstrom@scandichotels.com> Date: Wed, 30 Oct 2024 15:08:47 +0100 Subject: [PATCH 45/73] 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 <michael.zetterberg@scandichotels.com> Date: Wed, 30 Oct 2024 16:58:08 +0100 Subject: [PATCH 46/73] 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 <LoadingSpinner /> -} From 3b75231eeefc78fe4f4ed5d91e24f3ee704d82af Mon Sep 17 00:00:00 2001 From: Erik Tiekstra <erik.tiekstra@scandichotels.com> Date: Thu, 24 Oct 2024 09:32:47 +0200 Subject: [PATCH 47/73] 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 ( - <aside - className={`${styles.sidebar} ${ - isFullScreenSidebar ? styles.fullscreen : "" - }`} - > - <Button - theme="base" - intent="text" - className={styles.sidebarToggle} - onClick={toggleFullScreenSidebar} + <> + <aside + className={`${styles.sidebar} ${ + isFullScreenSidebar ? styles.fullscreen : "" + }`} > - <Body textTransform="bold" color="textMediumContrast" asChild> - <span> - {intl.formatMessage({ - id: isFullScreenSidebar ? "View as map" : "View as list", - })} - </span> - </Body> - </Button> - <div className={styles.sidebarContent}> - <Title as="h4" level="h2" textTransform="regular"> - {intl.formatMessage( - { id: "Things nearby HOTEL_NAME" }, - { hotelName } - )} - + +
+ + {intl.formatMessage( + { id: "Things nearby HOTEL_NAME" }, + { hotelName } + )} + - {poisInGroups.map(({ group, pois }) => - pois.length ? ( -
- -

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

- -
    - {pois.map((poi) => ( -
  • - -
  • - ))} -
-
- ) : null - )} -
- + {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 48/73] 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 49/73] 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 50/73] 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 51/73] 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 58/73] 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 59/73] 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 60/73] 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 61/73] 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 62/73] 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 63/73] 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 64/73] 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 65/73] 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 66/73] 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 67/73] 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 68/73] 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 69/73] 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 70/73] 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 71/73] 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 72/73] 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 e64179752213ac27a74bab2f85cfba1f3ff6d6b4 Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Fri, 1 Nov 2024 07:53:24 +0100 Subject: [PATCH 73/73] feat (#SW-751): added fallback as font-display to avoid flash of unstyled text --- app/globals.css | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/globals.css b/app/globals.css index 539f9827a..4a8b655c1 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,5 +1,5 @@ @font-face { - font-display: swap; + font-display: fallback; font-family: "biro script plus"; font-style: normal; font-weight: 400; @@ -7,7 +7,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "brandon text"; font-weight: 700; src: @@ -16,7 +16,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "brandon text"; font-weight: 900; src: @@ -25,7 +25,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira mono"; font-style: normal; font-weight: 400; @@ -33,7 +33,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira mono"; font-style: normal; font-weight: 500; @@ -41,7 +41,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira mono"; font-style: normal; font-weight: 700; @@ -49,7 +49,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira sans"; font-style: normal; font-weight: 300; @@ -57,7 +57,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira sans"; font-style: normal; font-weight: 400; @@ -65,7 +65,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira sans"; font-style: normal; font-weight: 500; @@ -73,7 +73,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira sans"; font-style: normal; font-weight: 600; @@ -81,7 +81,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira sans"; font-style: normal; font-weight: 700; @@ -89,7 +89,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira sans"; font-style: normal; font-weight: 900;