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/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)/(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} {creditCards} - {env.HIDE_FOR_NEXT_RELEASE ? null : communication} + {communication} ) 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 +} 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) { + const home = `/${params.lang}` + if (!searchParams.hotel) { + redirect(home) + } + const hotel = await getHotelData(searchParams.hotel, params.lang) + if (!hotel?.data) { + redirect(home) + } + return +} 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 +} 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) { + if (!searchParams.hotel) { + redirect(`/${params.lang}`) + } + const hotel = await getHotelData(searchParams.hotel, params.lang) + if (!hotel?.data) { + redirect(`/${params.lang}`) + } + return +} 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>) { - setLang(params.lang) - preload("811", params.lang) - - const hotel = await getHotelData("811", params.lang) - - if (!hotel?.data) { - redirect(`/${params.lang}`) + sidePeek, +}: React.PropsWithChildren< + LayoutArgs & { + hotelHeader: React.ReactNode + sidePeek: React.ReactNode } - +>) { + setLang(params.lang) + preload() return (
- + {hotelHeader}
{children} @@ -48,7 +34,7 @@ export default async function StepLayout({
- + {sidePeek}
) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 8e8d3c891..264f5b04d 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -1,9 +1,11 @@ -import { notFound } from "next/navigation" +import { notFound, redirect } from "next/navigation" import { + getBreakfastPackages, getCreditCardsSafely, getHotelData, getProfileSafely, + getRoomAvailability, } from "@/lib/trpc/memoizedRequests" import BedType from "@/components/HotelReservation/EnterDetails/BedType" @@ -17,61 +19,101 @@ 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) { - const { step, lang } = params + searchParams, +}: PageArgs) { + if (!searchParams.hotel) { + 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() - 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) { + const roomAvailability = await getRoomAvailability({ + hotelId: searchParams.hotel, + adults: Number(searchParams.adults), + roomStayStartDate: searchParams.checkIn, + roomStayEndDate: searchParams.checkOut, + rateCode: searchParams.rateCode, + }) + + if (!isValidStep(params.step) || !hotel || !roomAvailability) { return notFound() } + const mustBeGuaranteed = roomAvailability?.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 (
- +
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 1a2bb5118..fe98723e8 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -7,6 +7,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" @@ -50,7 +51,8 @@ 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 child = selectHotelParamsObject.room[0].child + const children = child ? generateChildrenString(child) : undefined // TODO: Handle multiple rooms const hotels = await fetchAvailableHotels({ cityId: city.id, @@ -80,7 +82,7 @@ export default async function SelectHotelPage({ arrivalDate: format(arrivalDate, "yyyy-MM-dd"), departureDate: format(departureDate, "yyyy-MM-dd"), noOfAdults: adults, - noOfChildren: children, + noOfChildren: child?.length, noOfRooms: 1, // // TODO: Handle multiple rooms duration: differenceInCalendarDays(departureDate, arrivalDate), leadTime: differenceInCalendarDays(arrivalDate, new Date()), 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 = { + [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.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 2d80356ac..35a940f0b 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,17 @@ +import { notFound } from "next/navigation" + import { getProfileSafely } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard" -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 { generateChildrenString } from "../select-hotel/utils" -import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" +import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" +import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import { LangParams, PageArgs } from "@/types/params" export default async function SelectRatePage({ @@ -20,10 +23,18 @@ 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 [hotelData, roomConfigurations, user] = await Promise.all([ + if (!selectRoomParamsObject.room) { + return notFound() + } + + const adults = selectRoomParamsObject.room[0].adults // 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({ hotelId: searchParams.hotel, language: params.lang, @@ -36,10 +47,22 @@ export default async function SelectRatePage({ adults, children, }), + serverClient().hotel.packages.get({ + hotelId: searchParams.hotel, + startDate: searchParams.fromDate, + endDate: searchParams.toDate, + adults, + children: childrenCount, + packageCodes: [ + RoomPackageCodeEnum.ACCESSIBILITY_ROOM, + RoomPackageCodeEnum.PET_ROOM, + RoomPackageCodeEnum.ALLERGY_ROOM, + ], + }), getProfileSafely(), ]) - if (!roomConfigurations) { + if (!roomsAvailability) { return "No rooms found" // TODO: Add a proper error message } @@ -50,17 +73,14 @@ export default async function SelectRatePage({ const roomCategories = hotelData?.included return ( -
+ <> -
-
- -
-
-
+ + ) } diff --git a/app/[lang]/(live)/@bookingwidget/page.tsx b/app/[lang]/(live)/@bookingwidget/page.tsx index 16bc85731..6c944ae69 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 { PageArgs } from "@/types/params" export default async function BookingWidgetPage({ - params, searchParams, -}: PageArgs) { +}: PageArgs<{}, URLSearchParams>) { 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..a58bdd1b2 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,14 @@ 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 })}, ${ + totalChildren > 0 + ? intl.formatMessage( + { id: "booking.children" }, + { totalChildren } + ) + ", " + : "" + }${intl.formatMessage({ id: "booking.rooms" }, { totalRooms })}`}
diff --git a/components/ContentType/ContentPage/HotelListingItem/hotelListingItem.module.css b/components/ContentType/ContentPage/HotelListingItem/hotelListingItem.module.css new file mode 100644 index 000000000..5a7bf6f7d --- /dev/null +++ b/components/ContentType/ContentPage/HotelListingItem/hotelListingItem.module.css @@ -0,0 +1,49 @@ +.container { + background-color: var(--Base-Surface-Primary-light-Normal); + border: 1px solid var(--Base-Border-Subtle); + border-radius: var(--Corner-radius-Medium); + overflow: hidden; +} + +.image { + width: 100%; + max-height: 200px; + object-fit: cover; +} + +.content { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); + padding: var(--Spacing-x2) var(--Spacing-x3); +} + +.intro { + display: flex; + flex-direction: column; + gap: var(--Spacing-x-half); +} + +.dividerContainer { + padding: 0 var(--Spacing-x1); +} + +.captions { + display: flex; +} + +@media screen and (min-width: 768px) { + .container { + display: grid; + grid-template-columns: minmax(250px, 350px) auto; + } + + .image { + max-height: none; + height: 100%; + } + + .button { + width: min(100%, 200px); + } +} diff --git a/components/ContentType/ContentPage/HotelListingItem/index.tsx b/components/ContentType/ContentPage/HotelListingItem/index.tsx new file mode 100644 index 000000000..2f2d0dd81 --- /dev/null +++ b/components/ContentType/ContentPage/HotelListingItem/index.tsx @@ -0,0 +1,70 @@ +import { ScandicLogoIcon } from "@/components/Icons" +import Image from "@/components/Image" +import Button from "@/components/TempDesignSystem/Button" +import Divider from "@/components/TempDesignSystem/Divider" +import Link from "@/components/TempDesignSystem/Link" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import Title from "@/components/TempDesignSystem/Text/Title" +import { getIntl } from "@/i18n" + +import styles from "./hotelListingItem.module.css" + +import type { HotelListingItemProps } from "@/types/components/contentPage/hotelListingItem" + +export default async function HotelListingItem({ + imageUrl, + altText, + name, + address, + distanceToCentre, + description, + link, +}: HotelListingItemProps) { + const intl = await getIntl() + + return ( +
+ {altText} +
+
+ + + {name} + +
+ {address} +
+ +
+ + {intl.formatMessage( + { id: "Distance to city centre" }, + { number: distanceToCentre } + )} + +
+
+ {description} + +
+
+ ) +} 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.alt}
- + {hotelImages?.length && ( + + )}
@@ -98,7 +100,7 @@ export default async function HotelPage() {
- {faq && ( + {faq.accordions.length > 0 && ( )}
diff --git a/components/DatePicker/Screen/Desktop.tsx b/components/DatePicker/Screen/Desktop.tsx index 9056f9183..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 + return ( + + ) }, Footer(props) { return ( @@ -82,8 +88,8 @@ export default function DatePickerDesktop({ size="small" theme="base" > - - {intl.formatMessage({ id: "Select dates" })} + + {intl.formatMessage({ id: "Select dates" })} diff --git a/components/DatePicker/Screen/Mobile.tsx b/components/DatePicker/Screen/Mobile.tsx index 0407f9b63..6e68ba1fc 100644 --- a/components/DatePicker/Screen/Mobile.tsx +++ b/components/DatePicker/Screen/Mobile.tsx @@ -94,8 +94,8 @@ export default function DatePickerMobile({ size="large" theme="base" > - - {intl.formatMessage({ id: "Select dates" })} + + {intl.formatMessage({ id: "Select dates" })}
diff --git a/components/DatePicker/index.tsx b/components/DatePicker/index.tsx index a68f137ea..b18a4d9ca 100644 --- a/components/DatePicker/index.tsx +++ b/components/DatePicker/index.tsx @@ -89,8 +89,10 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { return (
diff --git a/components/Forms/BookingWidget/FormContent/Search/SearchList/ClearSearchButton/index.tsx b/components/Forms/BookingWidget/FormContent/Search/SearchList/ClearSearchButton/index.tsx index 3b4391998..ae1f8f4d0 100644 --- a/components/Forms/BookingWidget/FormContent/Search/SearchList/ClearSearchButton/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Search/SearchList/ClearSearchButton/index.tsx @@ -33,8 +33,8 @@ export default function ClearSearchButton({ type="button" > - - {intl.formatMessage({ id: "Clear searches" })} + + {intl.formatMessage({ id: "Clear searches" })} ) diff --git a/components/Forms/BookingWidget/FormContent/index.tsx b/components/Forms/BookingWidget/FormContent/index.tsx index bd7a65ba6..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} - + + +
@@ -65,12 +70,17 @@ export default function FormContent({ theme="base" type="submit" > - - {intl.formatMessage({ id: "Search" })} + + {intl.formatMessage({ id: "Search" })} -
+ -
+
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 f12ecebde..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) { @@ -53,23 +60,25 @@ export default function GuestsRoomsPickerForm() { return (
diff --git a/components/Header/MainMenu/MobileMenu/index.tsx b/components/Header/MainMenu/MobileMenu/index.tsx index 1f2660770..ae79675dd 100644 --- a/components/Header/MainMenu/MobileMenu/index.tsx +++ b/components/Header/MainMenu/MobileMenu/index.tsx @@ -68,7 +68,7 @@ export default function MobileMenu({ })} onClick={() => toggleDropdown(DropdownTypeEnum.HamburgerMenu)} > - + toggleDropdown(DropdownTypeEnum.MyPagesMenu)} > - - {intl.formatMessage({ id: "Hi" })} {user.firstName}! + + + {intl.formatMessage({ id: "Hi" })} {user.firstName}! + toggleMegaMenu(false)} > - - {title} + + {title}
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() { 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({ - 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({ + 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 (
- ( - { id: "{amount} {currency}/night per adult" }, - { - amount: "150", - b: (str) => {str}, - currency: "SEK", + {packages.map((pkg) => ( + ( + { id: "breakfast.price.free" }, + { + amount: pkg.originalPrice, + currency: pkg.currency, + free: (str) => {str}, + strikethrough: (str) => {str}, + } + ) + : 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} + /> + ))}
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/HotelReservation/EnterDetails/Payment/GuaranteeDetails/guaranteeDetails.module.css b/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/guaranteeDetails.module.css new file mode 100644 index 000000000..32e678fbe --- /dev/null +++ b/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/guaranteeDetails.module.css @@ -0,0 +1,22 @@ +.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; +} diff --git a/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/index.tsx b/components/HotelReservation/EnterDetails/Payment/GuaranteeDetails/index.tsx new file mode 100644 index 000000000..f1f8f6783 --- /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 ( +
+ + + {intl.formatMessage({ id: "How it works" })} + + + +
+ + {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.", + })} + + + {intl.formatMessage({ + id: "What you have to do to guarantee booking:", + })} + +
    + +
  1. {intl.formatMessage({ id: "Complete the booking" })}
  2. + + +
  3. + {intl.formatMessage({ + id: "Provide a payment card in the next step", + })} +
  4. + +
+ + {intl.formatMessage({ + id: "Please note that this is mandatory, and that your card will only be charged in the event of a no-show.", + })} + +
+
+ ) +} diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index 0da2b79e2..e29692993 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" @@ -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 } + const guaranteeing = intl.formatMessage({ id: "guaranteeing" }) + const paying = intl.formatMessage({ id: "paying" }) + const paymentVerb = mustBeGuaranteed ? guaranteeing : paying + return (
+ {mustBeGuaranteed ? ( +
+ + {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.", + })} + + +
+ ) : null} {savedCreditCards?.length ? (
@@ -238,6 +254,7 @@ export default function Payment({ id: "booking.terms", }, { + paymentVerb, termsLink: (str) => (
- {hotelAttributes.ratings?.tripAdvisor && ( -
- - - {hotelAttributes.ratings.tripAdvisor.rating} - -
- )} {hotelAttributes.gallery && ( )} + {hotelAttributes.ratings?.tripAdvisor && ( +
+ + + {hotelAttributes.ratings.tripAdvisor.rating} + +
+ )}
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 ( - ({ - url: image.imageSizes.small, - alt: image.metaData.altText, - title: image.metaData.title, - }))} - dialogTitle={title} - > +
+ filterOptions.reduce( + (acc, option) => { + acc[option.code] = false + return acc + }, + {} as Record + ), + [filterOptions] + ) + + const intl = useIntl() + const methods = useForm>({ + defaultValues: initialFilterValues, + mode: "all", + reValidateMode: "onChange", + resolver: zodResolver(z.object({})), + }) + + const { watch, getValues, handleSubmit } = methods + const petFriendly = watch(RoomPackageCodeEnum.PET_ROOM) + const allergyFriendly = watch(RoomPackageCodeEnum.ALLERGY_ROOM) + + const selectedFilters = 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) + }, [onFilter, getValues]) + + useEffect(() => { + const subscription = watch(() => handleSubmit(submitFilter)()) + return () => subscription.unsubscribe() + }, [handleSubmit, watch, submitFilter]) + + return ( +
+
+ + {intl.formatMessage( + { id: "Room types available" }, + { numberOfRooms } + )} + +
+
+
+ + {intl.formatMessage({ id: "Filter" })} + + + {Object.entries(selectedFilters) + .filter(([_, value]) => value) + .map(([key]) => intl.formatMessage({ id: key })) + .join(", ")} + +
+ + {intl.formatMessage( + { id: "Room types available" }, + { numberOfRooms } + )} + +
+ + +
+ {filterOptions.map((option) => ( + + ))} + + + +
+ +
+
+ ) +} diff --git a/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css b/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css new file mode 100644 index 000000000..9cce04e43 --- /dev/null +++ b/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css @@ -0,0 +1,43 @@ +.container { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +} + +.roomsFilter { + display: flex; + flex-direction: row; + gap: var(--Spacing-x1); + align-items: center; +} + +.roomsFilter .infoIcon, +.roomsFilter .infoIcon path { + 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({ {publicLocalPrice.currency} + + /{intl.formatMessage({ id: "night" })} +
) : ( @@ -64,6 +67,9 @@ export default function PriceList({ {memberLocalPrice.currency} + + /{intl.formatMessage({ id: "night" })} +
) : ( 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/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(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..827a1d280 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx @@ -1,29 +1,56 @@ +import { differenceInCalendarDays } from "date-fns" 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" -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({ 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 isPetRoomSelected = features.some( + (feature) => feature.code === RoomPackageCodeEnum.PET_ROOM + ) + + const petRoomPackage = packages.find( + (pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM + ) + + const petRoomPrice = petRoomPackage?.calculatedPrice ?? null + const petRoomCurrency = petRoomPackage?.currency ?? null + + const checkInDate = new Date(roomsAvailability.checkInDate) + const checkOutDate = new Date(roomsAvailability.checkOutDate) + const nights = differenceInCalendarDays(checkOutDate, checkInDate) return (
- {rateSummary.roomType} - {rateSummary.priceName} + {roomType} + {priceName}
-
+
{priceToShow?.localPrice.pricePerStay}{" "} {priceToShow?.localPrice.currency} @@ -34,7 +61,49 @@ export default function RateSummary({ {priceToShow?.requestedPrice?.currency}
-
diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/rateSummary.module.css index c8352efb1..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,5 +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 1afec6119..665d91b31 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" @@ -11,6 +12,7 @@ import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import ImageGallery from "../../ImageGallery" +import { getIconForFeatureCode } from "../../utils" import RoomSidePeek from "../RoomSidePeek" import styles from "./roomCard.module.css" @@ -24,18 +26,18 @@ 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" - ) + + const rates = { + saveRate: rateDefinitions.find( + (rate) => rate.cancellationRule === "NonCancellable" + ), + changeRate: rateDefinitions.find( + (rate) => rate.cancellationRule === "Modifiable" + ), + flexRate: rateDefinitions.find( + (rate) => rate.cancellationRule === "CancellableBefore6PM" + ), + } function findProductForRate(rate: RateDefinition | undefined) { return rate @@ -47,20 +49,15 @@ export default function RoomCard({ : undefined } - function getPriceForRate( - rate: typeof saveRate | typeof changeRate | typeof flexRate - ) { + function getPriceInformationForRate(rate: RateDefinition | undefined) { 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 - const images = selectedRoom?.images + const { roomSize, occupancy, descriptions, images } = selectedRoom || {} const mainImage = images?.[0] return ( @@ -68,12 +65,11 @@ export default function RoomCard({
- {/*TODO: Handle pluralisation*/} {intl.formatMessage( { id: "booking.guests", }, - { nrOfGuests: occupancy } + { nrOfGuests: occupancy?.total } )} @@ -92,7 +88,7 @@ export default function RoomCard({ {roomConfiguration.roomType} - {roomDescription} + {descriptions?.short}
{intl.formatMessage({ @@ -100,49 +96,53 @@ export default function RoomCard({ })}
- - - + {Object.entries(rates).map(([key, rate]) => ( + + ))}
{mainImage && (
- {roomConfiguration.roomsLeft < 5 && ( - - {`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`} - - )} +
+ {roomConfiguration.roomsLeft < 5 && ( + + {`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`} + + )} + {roomConfiguration.features.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. */} {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 c4c5e2e87..9929a7451 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" @@ -8,13 +8,14 @@ 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({ - roomConfigurations, + roomsAvailability, roomCategories, user, + packages, }: RoomSelectionProps) { const [rateSummary, setRateSummary] = useState(null) @@ -22,27 +23,32 @@ export default function RoomSelection({ const searchParams = useSearchParams() const isUserLoggedIn = !!user - function handleSubmit(e: React.FormEvent) { - 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) { + e.preventDefault() router.push(`select-bed?${queryParams}`) } @@ -54,10 +60,10 @@ export default function RoomSelection({ onSubmit={handleSubmit} >
    - {roomConfigurations.roomConfigurations.map((roomConfiguration) => ( -
  • + {roomConfigurations.map((roomConfiguration) => ( +
  • )} 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/RoomSelection/utils.ts b/components/HotelReservation/SelectRate/RoomSelection/utils.ts index e47a0da70..1ae94cc9c 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/utils.ts +++ b/components/HotelReservation/SelectRate/RoomSelection/utils.ts @@ -1,28 +1,12 @@ -import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" +import { getFormattedUrlQueryParams } from "@/utils/url" + +import type { 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/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx new file mode 100644 index 000000000..8f9030149 --- /dev/null +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -0,0 +1,66 @@ +"use client" + +import { useCallback, 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 type { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection" + +export default function Rooms({ + roomsAvailability, + roomCategories = [], + user, + packages, +}: RoomSelectionProps) { + const defaultRooms = roomsAvailability.roomConfigurations.filter( + (room) => room.features.length === 0 + ) + const [rooms, setRooms] = useState({ + ...roomsAvailability, + roomConfigurations: defaultRooms, + }) + + const handleFilter = useCallback( + (filter: Record) => { + const selectedCodes = Object.keys(filter).filter((key) => filter[key]) + + if (selectedCodes.length === 0) { + setRooms({ + ...roomsAvailability, + roomConfigurations: defaultRooms, + }) + return + } + + const filteredRooms = roomsAvailability.roomConfigurations.filter( + (room) => + selectedCodes.every((selectedCode) => + room.features.some((feature) => feature.code === selectedCode) + ) + ) + setRooms({ ...roomsAvailability, roomConfigurations: filteredRooms }) + }, + [roomsAvailability, defaultRooms] + ) + + return ( +
    + + +
    + ) +} diff --git a/components/HotelReservation/SelectRate/Rooms/rooms.module.css b/components/HotelReservation/SelectRate/Rooms/rooms.module.css new file mode 100644 index 000000000..5e2bca00b --- /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-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..d91476bb5 --- /dev/null +++ b/components/HotelReservation/SelectRate/utils.ts @@ -0,0 +1,19 @@ +import { AllergyIcon, PetsIcon, WheelchairIcon } from "@/components/Icons" + +import { + RoomPackageCodeEnum, + type RoomPackageCodes, +} from "@/types/components/hotelReservation/selectRate/roomFilter" + +export function getIconForFeatureCode(featureCode: RoomPackageCodes) { + switch (featureCode) { + case RoomPackageCodeEnum.ACCESSIBILITY_ROOM: + return WheelchairIcon + case RoomPackageCodeEnum.ALLERGY_ROOM: + return AllergyIcon + case RoomPackageCodeEnum.PET_ROOM: + return PetsIcon + default: + return PetsIcon + } +} 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/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 ( + + + + + + + + + ) +} 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..e5e475b2e 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/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 ( + + + + + + + + + ) +} 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} > - - - - - - + ) } 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/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 = {} @@ -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 + )} ) } @@ -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 ( - {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 + )} ) } diff --git a/components/LanguageSwitcher/LanguageSwitcherContainer/index.tsx b/components/LanguageSwitcher/LanguageSwitcherContainer/index.tsx index ada415172..0eafafa29 100644 --- a/components/LanguageSwitcher/LanguageSwitcherContainer/index.tsx +++ b/components/LanguageSwitcher/LanguageSwitcherContainer/index.tsx @@ -35,10 +35,12 @@ export default function LanguageSwitcherContainer({ onClick={() => toggleDropdown(position)} > - - {intl.formatMessage({ - id: "Main menu", - })} + + + {intl.formatMessage({ + id: "Main menu", + })} +
@@ -53,7 +55,7 @@ export default function LanguageSwitcherContainer({ })} onClick={() => toggleDropdown(position)} > - +
) : null} 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({
{image.alt}
- {image.title && {image.title}} + {image.metaData.title && ( + {image.metaData.title} + )}
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 */}
- {mainImage.title && ( + {mainImage.metaData.title && (
- {mainImage.title} + + {mainImage.metaData.title} +
)}
{mainImage.alt} @@ -100,7 +102,7 @@ export default function Gallery({ {getThumbImages().map((image, index) => ( onSelectImage(image)} initial={{ opacity: 0, x: 50 }} @@ -109,10 +111,10 @@ export default function Gallery({ transition={{ duration: 0.2, delay: index * 0.05 }} > {image.alt} ))} @@ -139,7 +141,7 @@ export default function Gallery({
{images.map((image, index) => ( { onSelectImage(image) @@ -150,10 +152,10 @@ export default function Gallery({ transition={{ duration: 0.3, delay: index * 0.05 }} > {image.alt} ))} 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/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/components/Sidebar/JoinLoyalty/ReadMore/index.tsx b/components/Sidebar/JoinLoyalty/ReadMore/index.tsx new file mode 100644 index 000000000..8890c4172 --- /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], + text: intl.formatMessage({ id: "FAQ" }), + }, + { + href: membershipTermsAndConditions[lang], + text: intl.formatMessage({ id: "Membership terms and conditions" }), + }, + ] + + return ( +
+ {intl.formatMessage({ id: "Read more" })} +
+ {links.map((link) => ( + + + {link.text} + + ))} +
+
+ ) +} 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..7e85c29bd 100644 --- a/components/Sidebar/JoinLoyalty/joinLoyalty.module.css +++ b/components/Sidebar/JoinLoyalty/joinLoyalty.module.css @@ -1,7 +1,11 @@ +.joinLoyaltyContainer { + display: flex; + flex-direction: column; + gap: var(--Spacing-x5); +} .wrapper { display: grid; gap: var(--Spacing-x3); - padding-bottom: var(--Spacing-x5); padding-top: var(--Spacing-x4); justify-items: center; } 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/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/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..44fa78a14 --- /dev/null +++ b/components/TempDesignSystem/Form/FilterChip/_Chip/chip.module.css @@ -0,0 +1,37 @@ +.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; +} + +.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 new file mode 100644 index 000000000..528469df1 --- /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} className={styles.caption}> + {label} + </Caption> + <input + aria-hidden + id={id || name} + hidden + type={type} + value={value} + disabled={disabled} + {...register(name)} + /> + </label> + ) +} diff --git a/components/TempDesignSystem/Form/Phone/index.tsx b/components/TempDesignSystem/Form/Phone/index.tsx index d27e31986..ffe66a67e 100644 --- a/components/TempDesignSystem/Form/Phone/index.tsx +++ b/components/TempDesignSystem/Form/Phone/index.tsx @@ -91,7 +91,7 @@ export default function Phone({ <Label required={!!registerOptions.required} size="small"> {formatMessage({ id: "Country code" })} </Label> - <div className={styles.selectContainer}> + <span className={styles.selectContainer}> {props.children} <Body asChild fontOnly> <DialCodePreview @@ -106,7 +106,7 @@ export default function Phone({ height={18} width={18} /> - </div> + </span> </button> )} /> diff --git a/components/TempDesignSystem/Select/index.tsx b/components/TempDesignSystem/Select/index.tsx index 64f3fc335..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<SelectPortalContainer>(undefined) @@ -60,12 +61,12 @@ export default function Select({ > <Body asChild fontOnly> <Button className={styles.input} data-testid={name}> - <div className={styles.inputContentWrapper} tabIndex={tabIndex}> + <span className={styles.inputContentWrapper} tabIndex={tabIndex}> <Label required={required} size="small"> {label} </Label> <SelectValue /> - </div> + </span> <SelectChevron /> </Button> </Body> @@ -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} > <ListBox className={styles.listBox}> {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/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/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/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`, diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index d77219384..ff47c978c 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -1,17 +1,22 @@ { "<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", + "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", "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": "Allergirum", "Already a friend?": "Allerede en ven?", "Amenities": "Faciliteter", "Amusement park": "Forlystelsespark", @@ -63,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", @@ -80,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", @@ -104,11 +111,13 @@ "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", "Flexibility": "Fleksibilitet", "Follow us": "Følg os", + "Food options": "Madvalg", "Former Scandic Hotel": "Tidligere Scandic Hotel", "Free cancellation": "Gratis afbestilling", "Free rebooking": "Gratis ombooking", @@ -118,6 +127,8 @@ "Gift(s) added to your benefits": "{amount, plural, one {Gave} other {Gaver}} tilføjet til dine fordele", "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", @@ -172,6 +183,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", @@ -206,16 +218,22 @@ "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", "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", "Phone": "Telefon", "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", @@ -226,6 +244,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", @@ -239,9 +258,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": "{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", @@ -257,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", @@ -289,8 +309,10 @@ "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", "Tourist": "Turist", "Transaction date": "Overførselsdato", "Transactions": "Transaktioner", @@ -316,7 +338,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", @@ -340,7 +364,42 @@ "Zoo": "Zoo", "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", + "breakfast.price": "{amount} {currency}/nat", + "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", + "hotelPages.rooms.roomCard.persons": "personer", + "hotelPages.rooms.roomCard.seeRoomDetails": "Se værelsesdetaljer", + "km to city center": "km til byens centrum", + "lowercase letter": "lille bogstav", + "n/a": "n/a", + "next level:": "Næste niveau:", + "night": "nat", + "nights": "nætter", + "number": "nummer", + "or": "eller", + "paying": "betaler ", + "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}", "{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 9eb8bd1ec..008d7654e 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -1,17 +1,22 @@ { "<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", + "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", "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": "Allergikerzimmer", "Already a friend?": "Sind wir schon Freunde?", "Amenities": "Annehmlichkeiten", "Amusement park": "Vergnügungspark", @@ -63,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", @@ -80,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", @@ -104,11 +111,13 @@ "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", "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", @@ -119,6 +128,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", @@ -173,7 +183,6 @@ "Membership ID": "Mitglieds-ID", "Membership ID copied to clipboard": "Mitglieds-ID in die Zwischenablage kopiert", "Membership cards": "Mitgliedskarten", - "Menu": "Menu", "Modify": "Ändern", "Month": "Monat", "Museum": "Museum", @@ -207,16 +216,22 @@ "Open menu": "Menü öffnen", "Open my pages menu": "Meine Seiten Menü öffnen", "Overview": "Übersicht", + "PETR": "Haustier", "Parking": "Parken", "Parking / Garage": "Parken / Garage", "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", "Phone": "Telefon", "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", @@ -227,6 +242,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", @@ -243,6 +259,7 @@ "Room": "Zimmer", "Room & Terms": "Zimmer & Bedingungen", "Room facilities": "Zimmerausstattung", + "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", @@ -258,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", @@ -292,8 +310,10 @@ "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", "Tourist": "Tourist", "Transaction date": "Transaktionsdatum", "Transactions": "Transaktionen", @@ -319,7 +339,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?", @@ -354,8 +376,11 @@ "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", + "guaranteeing": "garantiert", "guest": "gast", "guests": "gäste", "hotelPages.rooms.roomCard.person": "person", @@ -369,7 +394,10 @@ "nights": "Nächte", "number": "nummer", "or": "oder", + "paying": "bezahlt", "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", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index f40909ac9..3679a2f4e 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -1,20 +1,25 @@ { "<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", + "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", "Add code": "Add code", "Add new card": "Add new card", + "Add room": "Add room", "Add to calendar": "Add to calendar", "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", @@ -52,8 +57,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", @@ -71,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", @@ -88,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", @@ -113,11 +118,13 @@ "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", "Flexibility": "Flexibility", "Follow us": "Follow us", + "Food options": "Food options", "Former Scandic Hotel": "Former Scandic Hotel", "Free cancellation": "Free cancellation", "Free rebooking": "Free rebooking", @@ -129,6 +136,7 @@ "Go back to edit": "Go back to edit", "Go back to overview": "Go back to overview", "Go to My Benefits": "Go to My Benefits", + "Guarantee booking with credit card": "Guarantee booking with credit card", "Guest": "Guest", "Guest information": "Guest information", "Guests & Rooms": "Guests & Rooms", @@ -184,6 +192,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", @@ -218,17 +227,23 @@ "Open menu": "Open menu", "Open my pages menu": "Open my pages menu", "Overview": "Overview", + "PETR": "Pet", "Parking": "Parking", "Parking / Garage": "Parking / Garage", "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", + "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", "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", @@ -240,6 +255,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", @@ -258,6 +274,7 @@ "Room": "Room", "Room & Terms": "Room & Terms", "Room facilities": "Room facilities", + "Room types available": "{numberOfRooms, plural, one {# room type} other {# room types}} available", "Rooms": "Rooms", "Rooms & Guests": "Rooms & Guests", "Sauna and gym": "Sauna and gym", @@ -273,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", @@ -307,9 +325,11 @@ "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", "Total incl VAT": "Total incl VAT", + "Total price": "Total price", "Tourist": "Tourist", "Transaction date": "Transaction date", "Transactions": "Transactions", @@ -336,7 +356,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?", @@ -374,9 +396,12 @@ "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", + "guaranteeing": "guaranteeing", "guest": "guest", "guests": "guests", "hotelPages.rooms.roomCard.person": "person", @@ -391,7 +416,10 @@ "nights": "nights", "number": "number", "or": "or", + "paying": "paying", "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", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index b44c6755d..65f7529b1 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -1,17 +1,22 @@ { "<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", + "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", "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", @@ -63,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", @@ -80,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", @@ -101,14 +108,16 @@ "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", + "Filter": "Suodatin", "Find booking": "Etsi varaus", "Find hotels": "Etsi hotelleja", "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", @@ -119,6 +128,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", @@ -173,6 +183,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äsenehdot ja -säännöt", "Menu": "Valikko", "Modify": "Muokkaa", "Month": "Kuukausi", @@ -207,16 +218,22 @@ "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", "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", "Phone": "Puhelin", "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", @@ -227,6 +244,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", @@ -240,9 +258,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": "{numberOfRooms, plural, one {# room type} other {# room types}} saatavilla", "Rooms": "Huoneet", "Rooms & Guests": "Huoneet & Vieraat", "Rooms & Guestss": "Huoneet & Vieraat", @@ -259,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", @@ -293,8 +312,10 @@ "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", "Tourist": "Turisti", "Transaction date": "Tapahtuman päivämäärä", "Transactions": "Tapahtumat", @@ -320,7 +341,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?", @@ -333,8 +356,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", @@ -355,8 +376,11 @@ "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", + "guaranteeing": "varmistetaan", "guest": "Vieras", "guests": "Vieraita", "hotelPages.rooms.roomCard.person": "henkilö", @@ -370,7 +394,10 @@ "nights": "yötä", "number": "määrä", "or": "tai", + "paying": "maksaa", "points": "pistettä", + "room type": "huonetyyppi", + "room types": "huonetyypit", "special character": "erikoishahmo", "spendable points expiring by": "{points} pistettä vanhenee {date} mennessä", "to": "to", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index ab4143986..9ac8d540a 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -1,17 +1,22 @@ { "<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", + "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", "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": "Allergirom", "Already a friend?": "Allerede Friend?", "Amenities": "Fasiliteter", "Amusement park": "Tivoli", @@ -63,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", @@ -79,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", @@ -100,14 +107,16 @@ "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", + "Filter": "Filter", "Find booking": "Finn booking", "Find hotels": "Finn hotell", "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", @@ -118,6 +127,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", @@ -171,6 +181,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", @@ -205,16 +216,22 @@ "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", "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", "Phone": "Telefon", "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", @@ -225,6 +242,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", @@ -238,9 +256,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": "{numberOfRooms, plural, one {# room type} other {# room types}} tilgjengelig", "Rooms": "Rom", "Rooms & Guests": "Rom og gjester", "Sauna and gym": "Sauna and gym", @@ -256,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", @@ -290,8 +309,10 @@ "Theatre": "Teater", "There are no transactions to display": "Det er ingen transaksjoner å vise", "Things nearby HOTEL_NAME": "Ting i nærheten av {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 å 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", @@ -317,7 +338,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?", @@ -351,8 +374,11 @@ "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", + "guaranteeing": "garantiert", "guest": "gjest", "guests": "gjester", "hotelPages.rooms.roomCard.person": "person", @@ -366,7 +392,10 @@ "nights": "netter", "number": "antall", "or": "eller", + "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 9d7c3b95d..3c2023eed 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -1,17 +1,22 @@ { "<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", + "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", "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", @@ -63,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", @@ -79,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", @@ -103,11 +110,13 @@ "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", "Flexibility": "Flexibilitet", "Follow us": "Följ oss", + "Food options": "Matval", "Former Scandic Hotel": "Tidigare Scandichotell", "Free cancellation": "Fri avbokning", "Free rebooking": "Fri ombokning", @@ -118,6 +127,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", @@ -171,6 +181,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", @@ -205,16 +216,22 @@ "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", "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", "Phone": "Telefon", "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", @@ -225,6 +242,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", @@ -238,9 +256,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": "{numberOfRooms, plural, one {# room type} other {# room types}} tillgängliga", "Rooms": "Rum", "Rooms & Guests": "Rum och gäster", "Sauna and gym": "Sauna and gym", @@ -256,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", @@ -290,8 +309,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}", + "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", @@ -317,7 +338,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?", @@ -352,8 +375,11 @@ "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", + "guaranteeing": "garanterar", "guest": "gäst", "guests": "gäster", "hotelPages.rooms.roomCard.person": "person", @@ -367,10 +393,15 @@ "nights": "nätter", "number": "nummer", "or": "eller", + "paying": "betalar", "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", + "type": "typ", + "types": "typer", "uppercase letter": "stor bokstav", "{amount} out of {total}": "{amount} av {total}", "{amount} {currency}": "{amount} {currency}", diff --git a/lib/api/endpoints.ts b/lib/api/endpoints.ts index 66bc36ec3..100fb7518 100644 --- a/lib/api/endpoints.ts +++ b/lib/api/endpoints.ts @@ -2,28 +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`, + + 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 9e32ac0cc..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 = {} ) { @@ -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/lib/graphql/Query/ContentPage/ContentPage.graphql b/lib/graphql/Query/ContentPage/ContentPage.graphql index 06174504a..c67738c05 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/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index d09349a8c..ec361b96c 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -52,14 +52,46 @@ 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, + adults, + roomStayStartDate, + roomStayEndDate, + children, + promotionCode, + rateCode, + }: { + hotelId: string + adults: number + roomStayStartDate: string + roomStayEndDate: string + children?: string + promotionCode?: string + rateCode?: string + }) { + return serverClient().hotel.availability.rooms({ + hotelId: parseInt(hotelId), + adults, + roomStayStartDate, + roomStayEndDate, + children, + promotionCode, + rateCode, + }) + } +) + export const getFooter = cache(async function getMemoizedFooter() { return serverClient().contentstack.base.footer() }) @@ -89,3 +121,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/package-lock.json b/package-lock.json index 9cd999fd9..e1cd6777e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8034,9 +8034,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", 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/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/server/routers/contentstack/contentPage/query.ts b/server/routers/contentstack/contentPage/query.ts index a7e5d53f0..44a765c49 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/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..2d69ab642 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,10 +22,11 @@ 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(""), + rateCode: z.string().optional(), }) export const getRatesInputSchema = z.object({ @@ -35,7 +36,12 @@ 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(), }) + +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 0f545d673..b69207a7d 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" @@ -7,8 +8,11 @@ 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 { 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 @@ -162,30 +166,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(), @@ -465,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({ @@ -545,7 +544,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.PET_ROOM, + RoomPackageCodeEnum.ALLERGY_ROOM, + RoomPackageCodeEnum.ACCESSIBILITY_ROOM, + ]), + }) + ), products: z.array(productSchema), }) @@ -570,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(), @@ -653,7 +662,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 +803,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 b5b5ff34b..6ad27655a 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -9,11 +9,11 @@ import { notFound, serverErrorByStatus, } from "@/server/errors/trpc" -import { extractHotelImages } from "@/server/routers/utils/hotels" import { contentStackUidWithServiceProcedure, publicProcedure, router, + safeProtectedServiceProcedure, serviceProcedure, } from "@/server/trpc" import { toApiLang } from "@/server/utils" @@ -25,7 +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, @@ -33,6 +39,7 @@ import { getRoomsAvailabilityInputSchema, } from "./input" import { + breakfastPackagesSchema, getHotelDataSchema, getHotelsAvailabilitySchema, getRatesSchema, @@ -48,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" @@ -57,6 +65,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" ) @@ -77,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 @@ -158,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}`, @@ -218,7 +242,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 @@ -288,13 +312,12 @@ export const hotelQueryRouter = router({ roomStayStartDate, roomStayEndDate, adults, - children, + ...(children && { children }), promotionCode, reservationProfileType, attachedProfileId, language: apiLang, } - hotelsAvailabilityCounter.add(1, { cityId, roomStayStartDate, @@ -309,7 +332,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}`, @@ -407,13 +430,14 @@ export const hotelQueryRouter = router({ promotionCode, reservationProfileType, attachedProfileId, + rateCode, } = input const params: Record<string, string | number | undefined> = { roomStayStartDate, roomStayEndDate, adults, - children, + ...(children && { children }), promotionCode, reservationProfileType, attachedProfileId, @@ -433,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}`, @@ -441,6 +465,7 @@ export const hotelQueryRouter = router({ }, params ) + if (!apiResponse.ok) { const text = await apiResponse.text() roomsAvailabilityFailCounter.add(1, { @@ -510,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 }), }), @@ -553,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, @@ -575,7 +608,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}`, @@ -645,6 +678,11 @@ export const hotelQueryRouter = router({ }) ) + if (isCardOnlyPayment) { + validateHotelData.data.data.attributes.merchantInformationData.alternatePaymentOptions = + [] + } + return validateHotelData.data }), }), @@ -693,4 +731,198 @@ 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.Package.Packages.hotel(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 + }), + 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/schemas/packages.ts b/server/routers/hotels/schemas/packages.ts new file mode 100644 index 000000000..2f12f7255 --- /dev/null +++ b/server/routers/hotels/schemas/packages.ts @@ -0,0 +1,55 @@ +import { z } from "zod" + +import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" + +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([ + RoomPackageCodeEnum.PET_ROOM, + RoomPackageCodeEnum.ALLERGY_ROOM, + RoomPackageCodeEnum.ACCESSIBILITY_ROOM, + ]), + itemCode: z.string(), + description: z.string(), + currency: z.string(), + calculatedPrice: z.number(), + inventories: z.array( + z.object({ + date: z.string(), + total: z.number(), + available: z.number(), + }) + ), + }) +) + +export const 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/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..10e1a4668 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}`, @@ -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}`, @@ -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/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/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/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/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<GuestsRooms>((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<GuestsRoomsStoreState>()((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<typeof initGuestsRoomsState> + +export const GuestsRoomsContext = createContext<GuestsRoomsStore | null>(null) + +export const useGuestsRoomsStore = <T>( + 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..c668617da 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<typeof bookingWidgetSchema> @@ -13,7 +15,7 @@ export type BookingWidgetSearchParams = { hotel?: string fromDate?: string toDate?: string - room?: string + room?: GuestsRoom[] } export type BookingWidgetType = VariantProps< @@ -22,13 +24,13 @@ export type BookingWidgetType = VariantProps< 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/types/components/contentPage/hotelListingItem.ts b/types/components/contentPage/hotelListingItem.ts new file mode 100644 index 000000000..1065d1c5f --- /dev/null +++ b/types/components/contentPage/hotelListingItem.ts @@ -0,0 +1,9 @@ +export type HotelListingItemProps = { + imageUrl: string + altText: string + name: string + address: string + distanceToCentre: number + description: string + link: string +} 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/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/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/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/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/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/hotelReservation/selectRate/rateSummary.ts b/types/components/hotelReservation/selectRate/rateSummary.ts index 672df21dd..f6c0f03b6 100644 --- a/types/components/hotelReservation/selectRate/rateSummary.ts +++ b/types/components/hotelReservation/selectRate/rateSummary.ts @@ -1,6 +1,10 @@ -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 isUserLoggedIn: boolean + packages: RoomPackageData + roomsAvailability: RoomsAvailability } diff --git a/types/components/hotelReservation/selectRate/roomFilter.ts b/types/components/hotelReservation/selectRate/roomFilter.ts new file mode 100644 index 000000000..d42669295 --- /dev/null +++ b/types/components/hotelReservation/selectRate/roomFilter.ts @@ -0,0 +1,19 @@ +import { z } from "zod" + +import { getRoomPackagesSchema } from "@/server/routers/hotels/schemas/packages" + +export enum RoomPackageCodeEnum { + PET_ROOM = "PETR", + ALLERGY_ROOM = "ALLG", + ACCESSIBILITY_ROOM = "ACCE", +} +export interface RoomFilterProps { + numberOfRooms: number + onFilter: (filter: Record<string, boolean | undefined>) => void + filterOptions: RoomPackageData +} + +export interface RoomPackageData + extends z.output<typeof getRoomPackagesSchema> {} + +export type RoomPackageCodes = RoomPackageData[number]["code"] diff --git a/types/components/hotelReservation/selectRate/roomSelection.ts b/types/components/hotelReservation/selectRate/roomSelection.ts index 9e944f5d8..8d006779c 100644 --- a/types/components/hotelReservation/selectRate/roomSelection.ts +++ b/types/components/hotelReservation/selectRate/roomSelection.ts @@ -1,10 +1,11 @@ -import { RoomsAvailability } from "@/server/routers/hotels/output" - -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 { - roomConfigurations: RoomsAvailability + roomsAvailability: RoomsAvailability roomCategories: RoomData[] user: SafeUser + packages: RoomPackageData } 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 { diff --git a/types/components/hotelReservation/selectRate/selectRate.ts b/types/components/hotelReservation/selectRate/selectRate.ts index da8792133..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 } @@ -26,4 +26,5 @@ export interface Rate { priceName: string public: Product["productType"]["public"] member: Product["productType"]["member"] + features: RoomConfiguration["features"] } 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/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", +} 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> 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<typeof contentPageSchema> {} export type Block = z.output<typeof blocksSchema> +export type GetBlock = z.input<typeof blocksSchema> + export type SidebarBlock = z.output<typeof sidebarSchema> 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<string, unknown> +) { + const searchParamsObject: Record<string, unknown> = Array.from( + searchParams.entries() + ).reduce<Record<string, unknown>>( + (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<string, unknown>)[k] = Number(value) + } else if (dataTypes[k] == "boolean") { + ;(nestedAcc as Record<string, unknown>)[k] = + value.toLowerCase() === "true" + } else { + ;(nestedAcc as Record<string, unknown>)[k] = value + } + } else { + if (!nestedAcc[k]) { + nestedAcc[k] = isNaN(Number(keys[i + 1])) ? {} : [] // Initialize as object or array + } + } + return nestedAcc[k] as Record<string, unknown> + }, acc) + return acc + }, + {} as Record<string, unknown> + ) + return searchParamsObject +}