diff --git a/.env.local.example b/.env.local.example index e939323ef..5043646c9 100644 --- a/.env.local.example +++ b/.env.local.example @@ -61,3 +61,5 @@ ENABLE_BOOKING_WIDGET_HOTELRESERVATION_PATH="false" SHOW_SITE_WIDE_ALERT="false" SHOW_SIGNUP_FLOW="true" USE_NEW_REWARDS_ENDPOINT="true" + +USE_NEW_REWARD_MODEL="true" diff --git a/.env.test b/.env.test index 0355b15cc..e48b5dc41 100644 --- a/.env.test +++ b/.env.test @@ -7,6 +7,7 @@ CMS_API_KEY="test" CMS_PREVIEW_TOKEN="test" CMS_PREVIEW_URL="test" CMS_URL="test" +CMS_BRANCH="development" CURITY_CLIENT_ID_SERVICE="test" CURITY_CLIENT_SECRET_SERVICE="test" CURITY_CLIENT_ID_USER="test" @@ -44,6 +45,7 @@ GOOGLE_DYNAMIC_MAP_ID="test" HIDE_FOR_NEXT_RELEASE="true" SALESFORCE_PREFERENCE_BASE_URL="test" USE_NEW_REWARDS_ENDPOINT="true" +USE_NEW_REWARD_MODEL="true" TZ=UTC ENABLE_BOOKING_FLOW="false" diff --git a/.eslintrc.json b/.eslintrc.json index 630a0576f..9eeb76fca 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,6 +1,7 @@ { "extends": ["next/core-web-vitals", "plugin:import/recommended"], - "plugins": ["simple-import-sort"], + "plugins": ["simple-import-sort", "@typescript-eslint"], + "parser": "@typescript-eslint/parser", "rules": { "react/function-component-definition": "error", "simple-import-sort/imports": [ @@ -52,6 +53,12 @@ "simple-import-sort/exports": "error", "import/first": "error", "import/newline-after-import": "error", - "import/no-duplicates": "error" + "import/no-duplicates": [ + "error", + { + "prefer-inline": true + } + ], + "@typescript-eslint/consistent-type-imports": "error" } } diff --git a/app/[lang]/(live)/(protected)/logout/route.ts b/app/[lang]/(live)/(protected)/logout/route.ts index db32e1c48..c6f7f809f 100644 --- a/app/[lang]/(live)/(protected)/logout/route.ts +++ b/app/[lang]/(live)/(protected)/logout/route.ts @@ -1,4 +1,4 @@ -import { NextRequest, NextResponse } from "next/server" +import { type NextRequest, NextResponse } from "next/server" import { AuthError } from "next-auth" import { Lang } from "@/constants/languages" diff --git a/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx b/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx index 048cf9e5f..dde4eb4b7 100644 --- a/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx @@ -4,7 +4,7 @@ import Breadcrumbs from "@/components/Breadcrumbs" import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton" import { setLang } from "@/i18n/serverContext" -import { LangParams, PageArgs } from "@/types/params" +import type { LangParams, PageArgs } from "@/types/params" export default function AllBreadcrumbs({ params }: PageArgs) { setLang(params.lang) diff --git a/app/[lang]/(live)/(protected)/my-pages/layout.module.css b/app/[lang]/(live)/(protected)/my-pages/layout.module.css index 199a19362..fdde0e3af 100644 --- a/app/[lang]/(live)/(protected)/my-pages/layout.module.css +++ b/app/[lang]/(live)/(protected)/my-pages/layout.module.css @@ -23,7 +23,7 @@ @media screen and (min-width: 1367px) { .content { gap: var(--Spacing-x5); - grid-template-columns: max(360px) 1fr; + grid-template-columns: max(340px) 1fr; padding-left: var(--Spacing-x5); padding-right: var(--Spacing-x5); } diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/@communication/page.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/@communication/page.tsx index 4341b3ed0..767c27815 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/@communication/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/@communication/page.tsx @@ -6,7 +6,7 @@ import { setLang } from "@/i18n/serverContext" import styles from "./page.module.css" -import { LangParams, PageArgs } from "@/types/params" +import type { LangParams, PageArgs } from "@/types/params" export default async function CommunicationSlot({ params, diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.tsx index ea54e71fd..84ade3e0e 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.tsx @@ -9,7 +9,7 @@ import { setLang } from "@/i18n/serverContext" import styles from "./page.module.css" -import { LangParams, PageArgs } from "@/types/params" +import type { LangParams, PageArgs } from "@/types/params" export default async function CreditCardSlot({ params }: PageArgs) { setLang(params.lang) diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/@membershipCards/page.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/@membershipCards/page.tsx index 913cde993..050da40f8 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/@membershipCards/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/@membershipCards/page.tsx @@ -9,7 +9,7 @@ import { setLang } from "@/i18n/serverContext" import styles from "./page.module.css" -import { LangParams, PageArgs } from "@/types/params" +import type { LangParams, PageArgs } from "@/types/params" export default async function MembershipCardSlot({ params, diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/@profile/edit/page.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/@profile/edit/page.tsx index d4cee27f1..2e2386b9d 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/@profile/edit/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/@profile/edit/page.tsx @@ -3,7 +3,7 @@ import { getProfile } from "@/lib/trpc/memoizedRequests" import Form from "@/components/Forms/Edit/Profile" import { setLang } from "@/i18n/serverContext" -import { LangParams, PageArgs } from "@/types/params" +import type { LangParams, PageArgs } from "@/types/params" export default async function EditProfileSlot({ params, diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/@profile/page.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/@profile/page.tsx index c12b61810..720306478 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/@profile/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/@profile/page.tsx @@ -20,7 +20,7 @@ import { setLang } from "@/i18n/serverContext" import styles from "./page.module.css" -import { LangParams, PageArgs } from "@/types/params" +import type { LangParams, PageArgs } from "@/types/params" export default async function Profile({ params }: PageArgs) { setLang(params.lang) diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/page.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/page.tsx index 749065fc5..33c1ceb0c 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/page.tsx @@ -5,7 +5,7 @@ import { serverClient } from "@/lib/trpc/server" import TrackingSDK from "@/components/TrackingSDK" import { setLang } from "@/i18n/serverContext" -import { LangParams, PageArgs } from "@/types/params" +import type { LangParams, PageArgs } from "@/types/params" export { generateMetadata } from "@/utils/generateMetadata" diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/page.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/page.tsx index f8024a816..587268de3 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/page.tsx +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/page.tsx @@ -4,7 +4,7 @@ import Breadcrumbs from "@/components/Breadcrumbs" import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton" import { setLang } from "@/i18n/serverContext" -import { LangParams, PageArgs } from "@/types/params" +import type { LangParams, PageArgs } from "@/types/params" export default function PageBreadcrumbs({ params }: PageArgs) { setLang(params.lang) diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/@preview/page.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/@preview/page.tsx index 6a29974b3..3c6e14e9e 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/@preview/page.tsx +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/@preview/page.tsx @@ -2,7 +2,7 @@ import { setPreviewData } from "@/lib/previewContext" import InitLivePreview from "@/components/LivePreview" -import { PageArgs, UIDParams } from "@/types/params" +import type { PageArgs, UIDParams } from "@/types/params" export default function PreviewPage({ searchParams, diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/layout.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/layout.tsx index 8ffc3c82d..afe86aa0d 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/layout.tsx +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/layout.tsx @@ -1,6 +1,6 @@ import styles from "./layout.module.css" -import { +import type { ContentTypeParams, LangParams, LayoutArgs, diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx index f81aa2edf..31fcd6add 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx @@ -11,7 +11,7 @@ import CollectionPage from "@/components/ContentType/StaticPages/CollectionPage" import ContentPage from "@/components/ContentType/StaticPages/ContentPage" import { setLang } from "@/i18n/serverContext" -import { +import type { ContentTypeParams, LangParams, PageArgs, diff --git a/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.tsx index 056db9936..e0ce4da7c 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.tsx @@ -1,6 +1,6 @@ import styles from "./layout.module.css" -import { LangParams, LayoutArgs } from "@/types/params" +import type { LangParams, LayoutArgs } from "@/types/params" export default function PaymentCallbackLayout({ children, diff --git a/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx index 364f50349..43374e32e 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx @@ -12,7 +12,7 @@ import { serverClient } from "@/lib/trpc/server" import PaymentCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback" -import { LangParams, PageArgs } from "@/types/params" +import type { LangParams, PageArgs } from "@/types/params" export default async function PaymentCallbackPage({ params, diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx index 66352fe80..9a2755a3e 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx @@ -1,6 +1,6 @@ import styles from "./layout.module.css" -import { LangParams, LayoutArgs } from "@/types/params" +import type { LangParams, LayoutArgs } from "@/types/params" export default function HotelReservationLayout({ children, diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/loading.tsx deleted file mode 100644 index 8f6f8657c..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import LoadingSpinner from "@/components/LoadingSpinner" - -export default function LoadingModal() { - return -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/default.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/default.tsx deleted file mode 100644 index 86b9e9a38..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/default.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Default() { - return null -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.module.css index b86e58a72..f955dbffc 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.module.css @@ -3,3 +3,9 @@ background-color: var(--Base-Background-Primary-Normal); position: relative; } + +@media screen and (min-width: 768px) { + .layout { + z-index: 0; + } +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.tsx index 907b02c2a..7b1b1bbd4 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.tsx @@ -1,17 +1,9 @@ import styles from "./layout.module.css" -import { LangParams, LayoutArgs } from "@/types/params" +import type { LangParams, LayoutArgs } from "@/types/params" export default function HotelReservationLayout({ children, - modal, -}: React.PropsWithChildren< - LayoutArgs & { modal: React.ReactNode } ->) { - return ( -
- {children} - {modal} -
- ) +}: React.PropsWithChildren>) { + return
{children}
} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx index bfd164880..9737ea82a 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx @@ -1 +1,63 @@ -export { default } from "../@modal/(.)map/page" +import { notFound } from "next/navigation" +import { Suspense } from "react" + +import { getLocations } from "@/lib/trpc/memoizedRequests" + +import { SelectHotelMapContainer } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer" +import { SelectHotelMapContainerSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainerSkeleton" +import { + generateChildrenString, + getHotelReservationQueryParams, +} from "@/components/HotelReservation/SelectRate/RoomSelection/utils" +import { MapContainer } from "@/components/MapContainer" +import { setLang } from "@/i18n/serverContext" + +import styles from "./page.module.css" + +import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" +import type { LangParams, PageArgs } from "@/types/params" + +export default async function SelectHotelMapPage({ + params, + searchParams, +}: PageArgs) { + setLang(params.lang) + const locations = await getLocations() + + if (!locations || "error" in locations) { + return null + } + const city = locations.data.find( + (location) => + location.name.toLowerCase() === searchParams.city.toLowerCase() + ) + if (!city) return notFound() + + const selectHotelParams = new URLSearchParams(searchParams) + const selectHotelParamsObject = + getHotelReservationQueryParams(selectHotelParams) + const adultsInRoom = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms + const childrenInRoom = selectHotelParamsObject.room[0].child + ? generateChildrenString(selectHotelParamsObject.room[0].child) + : undefined // TODO: Handle multiple rooms + const child = selectHotelParamsObject.room[0].child // TODO: Handle multiple rooms + + 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 f248a25ca..9888a05f6 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -1,51 +1,18 @@ -import { differenceInCalendarDays, format, isWeekend } from "date-fns" import { notFound } from "next/navigation" import { Suspense } from "react" -import { Lang } from "@/constants/languages" -import { - selectHotel, - selectHotelMap, -} from "@/constants/routes/hotelReservation" import { getLocations } from "@/lib/trpc/memoizedRequests" -import { - fetchAvailableHotels, - getFiltersFromHotels, -} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils" -import HotelCardListing from "@/components/HotelReservation/HotelCardListing" -import HotelCount from "@/components/HotelReservation/SelectHotel/HotelCount" -import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter" -import HotelSorter from "@/components/HotelReservation/SelectHotel/HotelSorter" -import MobileMapButtonContainer from "@/components/HotelReservation/SelectHotel/MobileMapButtonContainer" +import SelectHotel from "@/components/HotelReservation/SelectHotel" +import { SelectHotelSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelSkeleton" import { generateChildrenString, getHotelReservationQueryParams, } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" -import { ChevronRightIcon } from "@/components/Icons" -import StaticMap from "@/components/Maps/StaticMap" -import Alert from "@/components/TempDesignSystem/Alert" -import Breadcrumbs from "@/components/TempDesignSystem/Breadcrumbs" -import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton" -import Button from "@/components/TempDesignSystem/Button" -import Link from "@/components/TempDesignSystem/Link" -import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import TrackingSDK from "@/components/TrackingSDK" -import { getIntl } from "@/i18n" import { setLang } from "@/i18n/serverContext" -import styles from "./page.module.css" - -import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" -import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" -import { - TrackingChannelEnum, - TrackingSDKHotelInfo, - TrackingSDKPageData, -} from "@/types/components/tracking" -import { AlertTypeEnum } from "@/types/enums/alert" -import { LangParams, PageArgs } from "@/types/params" +import type { LangParams, PageArgs } from "@/types/params" export default async function SelectHotelPage({ params, @@ -64,10 +31,6 @@ export default async function SelectHotelPage({ if (!city) return notFound() - const isCityWithCountry = (city: any): city is { country: string } => - "country" in city - - const intl = await getIntl() const selectHotelParams = new URLSearchParams(searchParams) const selectHotelParamsObject = getHotelReservationQueryParams(selectHotelParams) @@ -79,157 +42,30 @@ export default async function SelectHotelPage({ return notFound() } - const adults = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms + const adultsParams = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms + const childrenParams = selectHotelParamsObject.room[0].child + ? generateChildrenString(selectHotelParamsObject.room[0].child) + : undefined // TODO: Handle multiple rooms const child = selectHotelParamsObject.room[0].child // TODO: Handle multiple rooms - const children = child ? generateChildrenString(child) : undefined - const hotels = await fetchAvailableHotels({ - cityId: city.id, - roomStayStartDate: searchParams.fromDate, - roomStayEndDate: searchParams.toDate, - adults, - children, - }) - - const arrivalDate = new Date(searchParams.fromDate) - const departureDate = new Date(searchParams.toDate) - - const validHotels = hotels.filter( - (hotel): hotel is HotelData => hotel !== null - ) - - const filterList = getFiltersFromHotels(validHotels) - const breadcrumbs = [ - { - title: intl.formatMessage({ id: "Home" }), - href: `/${params.lang}`, - uid: "home-page", - }, - { - title: intl.formatMessage({ id: "Hotel reservation" }), - href: `/${params.lang}/hotelreservation`, - uid: "hotel-reservation", - }, - { - title: intl.formatMessage({ id: "Select hotel" }), - href: `${selectHotel(params.lang)}/?${selectHotelParams}`, - uid: "select-hotel", - }, - { - title: city.name, - uid: city.id, - }, - ] - - const isAllUnavailable = hotels.every((hotel) => hotel.price === undefined) - - const pageTrackingData: TrackingSDKPageData = { - pageId: "select-hotel", - domainLanguage: params.lang as Lang, - channel: TrackingChannelEnum["hotelreservation"], - pageName: "hotelreservation|select-hotel", - siteSections: "hotelreservation|select-hotel", - pageType: "bookinghotelspage", - siteVersion: "new-web", - } - - const hotelsTrackingData: TrackingSDKHotelInfo = { - availableResults: hotels.length, - searchTerm: searchParams.city, - arrivalDate: format(arrivalDate, "yyyy-MM-dd"), - departureDate: format(departureDate, "yyyy-MM-dd"), - noOfAdults: adults, - noOfChildren: child?.length, - ageOfChildren: child?.map((c) => c.age).join(","), - childBedPreference: child?.map((c) => ChildBedMapEnum[c.bed]).join("|"), - noOfRooms: 1, // // TODO: Handle multiple rooms - duration: differenceInCalendarDays(departureDate, arrivalDate), - leadTime: differenceInCalendarDays(arrivalDate, new Date()), - searchType: "destination", - bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday", - country: validHotels?.[0].hotelData.address.country, - region: validHotels?.[0].hotelData.address.city, + const reservationParams = { + selectHotelParams, + searchParams, + adultsParams, + childrenParams, + child, } return ( - <> -
- }> - - -
-
- {city.name} - -
-
- -
-
- -
-
-
- {hotels.length > 0 ? ( // TODO: Temp fix until API returns hotels that are not available - -
- - -
- - ) : ( -
- -
- )} - -
-
- {isAllUnavailable && ( - - )} - -
- - - -
- + } + > + + ) } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/getValidDates.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/getValidDates.ts index d9bcbf09e..eb879052d 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/getValidDates.ts +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/getValidDates.ts @@ -1,7 +1,7 @@ -import { Dayjs } from "dayjs" - import { dt } from "@/lib/dt" +import type { Dayjs } from "dayjs" + /** * Get valid dates from stringFromDate and stringToDate making sure that they are not in the past and chronologically correct * @example const { fromDate, toDate} = getValidDates("2021-01-01", "2021-01-02") diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.module.css index d807e2633..a9e612141 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.module.css @@ -17,12 +17,10 @@ @media screen and (min-width: 1367px) { .container { - width: var(--max-width-page); - grid-template-columns: 1fr 340px; grid-template-rows: auto 1fr; + width: var(--max-width-page); margin: var(--Spacing-x5) auto 0; - /* simulates padding on viewport smaller than --max-width-navigation */ } .content { diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx index 79bbd6375..e6428d9af 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx @@ -35,12 +35,11 @@ import EnterDetailsTracking from "./enterDetailsTracking" import styles from "./page.module.css" import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" -import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" -import { +import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" +import type { TrackingChannelEnum, TrackingSDKHotelInfo, - TrackingSDKPageData, -} from "@/types/components/tracking" + TrackingSDKPageData} from "@/types/components/tracking"; import { StepEnum } from "@/types/enums/step" import type { LangParams, PageArgs } from "@/types/params" diff --git a/app/[lang]/(live)/(public)/login/route.ts b/app/[lang]/(live)/(public)/login/route.ts index 2d91c0d5f..03933e003 100644 --- a/app/[lang]/(live)/(public)/login/route.ts +++ b/app/[lang]/(live)/(public)/login/route.ts @@ -1,4 +1,4 @@ -import { NextRequest, NextResponse } from "next/server" +import { type NextRequest, NextResponse } from "next/server" import { AuthError } from "next-auth" import { Lang } from "@/constants/languages" @@ -117,7 +117,7 @@ export async function GET( /** Record is next-auth typings */ const params: Record = { ui_locales: context.params.lang, - scope: ["openid", "profile"], + scope: ["openid", "profile", "booking"], /** * The `acr_values` param is used to make Curity display the proper login * page for Scandic. Without the parameter Curity presents some choices diff --git a/app/[lang]/(live)/(public)/verifymagiclink/route.ts b/app/[lang]/(live)/(public)/verifymagiclink/route.ts index 81fa277c3..adac7a8d4 100644 --- a/app/[lang]/(live)/(public)/verifymagiclink/route.ts +++ b/app/[lang]/(live)/(public)/verifymagiclink/route.ts @@ -1,14 +1,13 @@ -import { NextRequest, NextResponse } from "next/server" +import { type NextRequest, NextResponse } from "next/server" import { AuthError } from "next-auth" -import { Lang } from "@/constants/languages" -import { login } from "@/constants/routes/handleAuth" -import { env } from "@/env/server" import { badRequest, internalServerError } from "@/server/errors/next" import { getPublicURL } from "@/server/utils" import { signIn } from "@/auth" +import type { Lang } from "@/constants/languages" + export async function GET( request: NextRequest, context: { params: { lang: Lang } } diff --git a/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx b/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx index d3ec595df..a6d940572 100644 --- a/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx +++ b/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx @@ -2,7 +2,7 @@ import { env } from "@/env/server" import BookingWidget, { preload } from "@/components/BookingWidget" -import { PageArgs } from "@/types/params" +import type { PageArgs } from "@/types/params" export default async function BookingWidgetPage({ searchParams, diff --git a/app/[lang]/(live)/@bookingwidget/page.tsx b/app/[lang]/(live)/@bookingwidget/page.tsx index 794218e52..93e81d617 100644 --- a/app/[lang]/(live)/@bookingwidget/page.tsx +++ b/app/[lang]/(live)/@bookingwidget/page.tsx @@ -3,7 +3,7 @@ import { serverClient } from "@/lib/trpc/server" import BookingWidget, { preload } from "@/components/BookingWidget" -import { PageArgs } from "@/types/params" +import type { PageArgs } from "@/types/params" export default async function BookingWidgetPage({ searchParams, diff --git a/app/[lang]/(live)/@footer/page.tsx b/app/[lang]/(live)/@footer/page.tsx index 4edbe0b0d..90fc74d02 100644 --- a/app/[lang]/(live)/@footer/page.tsx +++ b/app/[lang]/(live)/@footer/page.tsx @@ -4,7 +4,7 @@ import CurrentFooter from "@/components/Current/Footer" import Footer, { preload } from "@/components/Footer" import { setLang } from "@/i18n/serverContext" -import { LangParams, PageArgs } from "@/types/params" +import type { LangParams, PageArgs } from "@/types/params" export default function FooterSlot({ params }: PageArgs) { setLang(params.lang) diff --git a/app/[lang]/(live)/@header/page.tsx b/app/[lang]/(live)/@header/page.tsx index 51e91035d..59c67fc02 100644 --- a/app/[lang]/(live)/@header/page.tsx +++ b/app/[lang]/(live)/@header/page.tsx @@ -7,7 +7,7 @@ import HeaderFallback from "@/components/Current/Header/HeaderFallback" import Header from "@/components/Header" import { setLang } from "@/i18n/serverContext" -import { LangParams, PageArgs } from "@/types/params" +import type { LangParams, PageArgs } from "@/types/params" export default function HeaderPage({ params }: PageArgs) { setLang(params.lang) diff --git a/app/[lang]/(live)/error.tsx b/app/[lang]/(live)/error.tsx index a39495eb7..b347ce19b 100644 --- a/app/[lang]/(live)/error.tsx +++ b/app/[lang]/(live)/error.tsx @@ -16,7 +16,7 @@ import { findLang } from "@/utils/languages" import styles from "./error.module.css" -import { LangParams } from "@/types/params" +import type { LangParams } from "@/types/params" export default function Error({ error, diff --git a/app/[lang]/(live)/layout.tsx b/app/[lang]/(live)/layout.tsx index 5bf769386..7d0397a0b 100644 --- a/app/[lang]/(live)/layout.tsx +++ b/app/[lang]/(live)/layout.tsx @@ -3,7 +3,6 @@ import "@scandic-hotels/design-system/style.css" import Script from "next/script" -import { env } from "@/env/server" import TrpcProvider from "@/lib/trpc/Provider" import TokenRefresher from "@/components/Auth/TokenRefresher" diff --git a/app/[lang]/(live)/middleware-error/404/page.tsx b/app/[lang]/(live)/middleware-error/404/page.tsx index f70f34995..416f2ed72 100644 --- a/app/[lang]/(live)/middleware-error/404/page.tsx +++ b/app/[lang]/(live)/middleware-error/404/page.tsx @@ -1,7 +1,7 @@ import NotFound from "@/components/Current/NotFound" import { setLang } from "@/i18n/serverContext" -import { LangParams, PageArgs } from "@/types/params" +import type { LangParams, PageArgs } from "@/types/params" export default function NotFoundPage({ params }: PageArgs) { setLang(params.lang) diff --git a/app/[lang]/(live)/middleware-error/[status]/page.tsx b/app/[lang]/(live)/middleware-error/[status]/page.tsx index 177b62af4..ecbb159a1 100644 --- a/app/[lang]/(live)/middleware-error/[status]/page.tsx +++ b/app/[lang]/(live)/middleware-error/[status]/page.tsx @@ -2,7 +2,7 @@ import { setLang } from "@/i18n/serverContext" import styles from "./page.module.css" -import { LangParams, LayoutArgs, StatusParams } from "@/types/params" +import type { LangParams, LayoutArgs, StatusParams } from "@/types/params" export default function MiddlewareError({ params, diff --git a/app/[lang]/(live-current)/@header/current-content-page/page.tsx b/app/[lang]/(live-current)/@header/current-content-page/page.tsx index 98738064a..b41776b58 100644 --- a/app/[lang]/(live-current)/@header/current-content-page/page.tsx +++ b/app/[lang]/(live-current)/@header/current-content-page/page.tsx @@ -1,7 +1,7 @@ import Header from "@/components/Current/Header" import { setLang } from "@/i18n/serverContext" -import { LangParams, PageArgs } from "@/types/params" +import type { LangParams, PageArgs } from "@/types/params" export default async function HeaderPage({ params }: PageArgs) { setLang(params.lang) diff --git a/app/[lang]/webview/[contentType]/[uid]/page.tsx b/app/[lang]/webview/[contentType]/[uid]/page.tsx index ab64c4916..54b5fa91b 100644 --- a/app/[lang]/webview/[contentType]/[uid]/page.tsx +++ b/app/[lang]/webview/[contentType]/[uid]/page.tsx @@ -7,7 +7,7 @@ import AccountPage from "@/components/Webviews/AccountPage" import LoyaltyPage from "@/components/Webviews/LoyaltyPage" import { setLang } from "@/i18n/serverContext" -import { +import type { ContentTypeWebviewParams, LangParams, PageArgs, diff --git a/app/[lang]/webview/refresh/page.tsx b/app/[lang]/webview/refresh/page.tsx index 2777f1c6d..ad258094b 100644 --- a/app/[lang]/webview/refresh/page.tsx +++ b/app/[lang]/webview/refresh/page.tsx @@ -3,7 +3,7 @@ import { setLang } from "@/i18n/serverContext" import styles from "./page.module.css" -import { LangParams, PageArgs } from "@/types/params" +import type { LangParams, PageArgs } from "@/types/params" export default function Refresh({ params }: PageArgs) { setLang(params.lang) diff --git a/app/api/web/add-card-callback/[lang]/route.ts b/app/api/web/add-card-callback/[lang]/route.ts index e664124ed..bcbeff297 100644 --- a/app/api/web/add-card-callback/[lang]/route.ts +++ b/app/api/web/add-card-callback/[lang]/route.ts @@ -1,4 +1,3 @@ -import { NextRequest } from "next/server" import { env } from "process" import { Lang } from "@/constants/languages" @@ -6,6 +5,8 @@ import { profile } from "@/constants/routes/myPages" import { serverClient } from "@/lib/trpc/server" import { getPublicURL } from "@/server/utils" +import type { NextRequest } from "next/server" + export async function GET( request: NextRequest, { params }: { params: { lang: string } } diff --git a/app/api/web/check-headers/route.ts b/app/api/web/check-headers/route.ts index 49be1b0c6..7b7cef40c 100644 --- a/app/api/web/check-headers/route.ts +++ b/app/api/web/check-headers/route.ts @@ -1,6 +1,4 @@ -import { NextResponse } from "next/server" - -import type { NextRequest } from "next/server" +import { type NextRequest, NextResponse } from "next/server" export async function GET(request: NextRequest) { return NextResponse.json(Object.fromEntries(request.headers.entries())) diff --git a/app/api/web/revalidate/loyaltyConfig/route.ts b/app/api/web/revalidate/loyaltyConfig/route.ts index 131b583d1..b63cf9559 100644 --- a/app/api/web/revalidate/loyaltyConfig/route.ts +++ b/app/api/web/revalidate/loyaltyConfig/route.ts @@ -1,6 +1,5 @@ import { revalidateTag } from "next/cache" import { headers } from "next/headers" -import { NextRequest } from "next/server" import { z } from "zod" import { Lang } from "@/constants/languages" @@ -9,6 +8,8 @@ import { badRequest, internalServerError, notFound } from "@/server/errors/next" import { generateLoyaltyConfigTag } from "@/utils/generateTag" +import type { NextRequest } from "next/server" + enum LoyaltyConfigContentTypes { loyalty_level = "loyalty_level", reward = "reward", diff --git a/app/api/web/revalidate/manually/route.ts b/app/api/web/revalidate/manually/route.ts index ac9ffad50..a4b3bfdc9 100644 --- a/app/api/web/revalidate/manually/route.ts +++ b/app/api/web/revalidate/manually/route.ts @@ -1,12 +1,13 @@ import { revalidateTag } from "next/cache" import { headers } from "next/headers" -import { Lang } from "@/constants/languages" import { env } from "@/env/server" import { badRequest, internalServerError } from "@/server/errors/next" import { generateTag } from "@/utils/generateTag" +import type { Lang } from "@/constants/languages" + // This file is primarily to be used locally to test // purging your cache for new (and old) requests export async function POST() { diff --git a/app/api/web/revalidate/route.ts b/app/api/web/revalidate/route.ts index 63a7b157b..3e5038b7d 100644 --- a/app/api/web/revalidate/route.ts +++ b/app/api/web/revalidate/route.ts @@ -1,6 +1,5 @@ import { revalidateTag } from "next/cache" import { headers } from "next/headers" -import { NextRequest } from "next/server" import { z } from "zod" import { Lang } from "@/constants/languages" @@ -17,6 +16,8 @@ import { generateTag, } from "@/utils/generateTag" +import type { NextRequest } from "next/server" + const validateJsonBody = z.object({ data: z.object({ content_type: z.object({ diff --git a/components/Blocks/CardsGrid.tsx b/components/Blocks/CardsGrid.tsx index 81e0b0eff..aaebd9df9 100644 --- a/components/Blocks/CardsGrid.tsx +++ b/components/Blocks/CardsGrid.tsx @@ -43,7 +43,7 @@ export default function CardsGrid({ return ( , "color">, diff --git a/components/Blocks/DynamicContent/Overview/Friend/Hero/index.tsx b/components/Blocks/DynamicContent/Overview/Friend/Hero/index.tsx index 3ec3083ba..7b783e485 100644 --- a/components/Blocks/DynamicContent/Overview/Friend/Hero/index.tsx +++ b/components/Blocks/DynamicContent/Overview/Friend/Hero/index.tsx @@ -1,6 +1,7 @@ -import { HeroProps } from "./hero" import { heroVariants } from "./heroVariants" +import type { HeroProps } from "./hero" + export default function Hero({ className, color, children }: HeroProps) { const classNames = heroVariants({ className, color }) return
{children}
diff --git a/components/Blocks/DynamicContent/Overview/Stats/Points/index.tsx b/components/Blocks/DynamicContent/Overview/Stats/Points/index.tsx index 1da672402..d06581ff7 100644 --- a/components/Blocks/DynamicContent/Overview/Stats/Points/index.tsx +++ b/components/Blocks/DynamicContent/Overview/Stats/Points/index.tsx @@ -7,7 +7,7 @@ import { getMembership } from "@/utils/user" import PointsContainer from "./Container" import { NextLevelPointsColumn, YourPointsColumn } from "./PointsColumn" -import { UserProps } from "@/types/components/myPages/user" +import type { UserProps } from "@/types/components/myPages/user" export default async function Points({ user }: UserProps) { const intl = await getIntl() diff --git a/components/Blocks/DynamicContent/OverviewTable/Client.tsx b/components/Blocks/DynamicContent/OverviewTable/Client.tsx index 1754fce72..ce1d9b8bd 100644 --- a/components/Blocks/DynamicContent/OverviewTable/Client.tsx +++ b/components/Blocks/DynamicContent/OverviewTable/Client.tsx @@ -4,7 +4,7 @@ import { useReducer } from "react" import { useIntl } from "react-intl" import { - MembershipLevel, + type MembershipLevel, MembershipLevelEnum, } from "@/constants/membershipLevels" @@ -22,8 +22,8 @@ import styles from "./overviewTable.module.css" import type { Key } from "react-aria-components" import { - ComparisonLevel, - DesktopSelectColumns, + type ComparisonLevel, + type DesktopSelectColumns, type MobileColumnHeaderProps, OverviewTableActionsEnum, type OverviewTableClientProps, diff --git a/components/Blocks/DynamicContent/OverviewTable/index.tsx b/components/Blocks/DynamicContent/OverviewTable/index.tsx index f39fb9a25..062f7f32e 100644 --- a/components/Blocks/DynamicContent/OverviewTable/index.tsx +++ b/components/Blocks/DynamicContent/OverviewTable/index.tsx @@ -4,7 +4,7 @@ import { serverClient } from "@/lib/trpc/server" import SectionWrapper from "../SectionWrapper" import OverviewTableClient from "./Client" -import { OverviewTableProps } from "@/types/components/blocks/dynamicContent" +import type { OverviewTableProps } from "@/types/components/blocks/dynamicContent" export default async function OverviewTable({ dynamic_content, diff --git a/components/Blocks/DynamicContent/OverviewTable/reducer.ts b/components/Blocks/DynamicContent/OverviewTable/reducer.ts index b2fd7372d..d6e3f1483 100644 --- a/components/Blocks/DynamicContent/OverviewTable/reducer.ts +++ b/components/Blocks/DynamicContent/OverviewTable/reducer.ts @@ -9,7 +9,7 @@ import { type LevelWithRewards, OverviewTableActionsEnum, type OverviewTableClientProps, - OverviewTableReducerAction, + type OverviewTableReducerAction, } from "@/types/components/overviewTable" export function getLevel( diff --git a/components/Blocks/DynamicContent/Points/EarnAndBurn/JourneyTable/Client.tsx b/components/Blocks/DynamicContent/Points/EarnAndBurn/JourneyTable/Client.tsx index eca762942..7bd58dd2e 100644 --- a/components/Blocks/DynamicContent/Points/EarnAndBurn/JourneyTable/Client.tsx +++ b/components/Blocks/DynamicContent/Points/EarnAndBurn/JourneyTable/Client.tsx @@ -6,11 +6,11 @@ import { useState } from "react" import { trpc } from "@/lib/trpc/client" import LoadingSpinner from "@/components/LoadingSpinner" +import Pagination from "@/components/MyPages/Pagination" import ClientTable from "./ClientTable" -import Pagination from "./Pagination" -import { Transactions } from "@/types/components/myPages/myPage/earnAndBurn" +import type { Transactions } from "@/types/components/myPages/myPage/earnAndBurn" export default function TransactionTable({ initialJourneyTransactions, diff --git a/components/Blocks/DynamicContent/Points/ExpiringPoints/index.tsx b/components/Blocks/DynamicContent/Points/ExpiringPoints/index.tsx index 8b0206351..2614615eb 100644 --- a/components/Blocks/DynamicContent/Points/ExpiringPoints/index.tsx +++ b/components/Blocks/DynamicContent/Points/ExpiringPoints/index.tsx @@ -5,7 +5,7 @@ import SectionHeader from "@/components/Section/Header" import ExpiringPointsTable from "./ExpiringPointsTable" -import { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage" +import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage" export default async function ExpiringPoints({ link, diff --git a/components/Blocks/DynamicContent/Points/Overview/Points/index.tsx b/components/Blocks/DynamicContent/Points/Overview/Points/index.tsx index dc42f86de..b11699391 100644 --- a/components/Blocks/DynamicContent/Points/Overview/Points/index.tsx +++ b/components/Blocks/DynamicContent/Points/Overview/Points/index.tsx @@ -12,8 +12,8 @@ import { YourPointsColumn, } from "../../../Overview/Stats/Points/PointsColumn" -import { UserProps } from "@/types/components/myPages/user" -import { LangParams } from "@/types/params" +import type { UserProps } from "@/types/components/myPages/user" +import type { LangParams } from "@/types/params" /* TODO */ export default async function Points({ user, lang }: UserProps & LangParams) { diff --git a/components/Blocks/DynamicContent/Rewards/CurrentLevel/Client.tsx b/components/Blocks/DynamicContent/Rewards/CurrentLevel/Client.tsx deleted file mode 100644 index a57e5e160..000000000 --- a/components/Blocks/DynamicContent/Rewards/CurrentLevel/Client.tsx +++ /dev/null @@ -1,76 +0,0 @@ -"use client" - -import { trpc } from "@/lib/trpc/client" -import { Reward } from "@/server/routers/contentstack/reward/output" - -import LoadingSpinner from "@/components/LoadingSpinner" -import Grids from "@/components/TempDesignSystem/Grids" -import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton" -import Title from "@/components/TempDesignSystem/Text/Title" -import useLang from "@/hooks/useLang" - -import styles from "./current.module.css" - -type CurrentRewardsClientProps = { - initialCurrentRewards: { rewards: Reward[]; nextCursor: number | undefined } -} -export default function ClientCurrentRewards({ - initialCurrentRewards, -}: CurrentRewardsClientProps) { - const lang = useLang() - const { data, isFetching, fetchNextPage, hasNextPage, isLoading } = - trpc.contentstack.rewards.current.useInfiniteQuery( - { - limit: 3, - lang, - }, - { - getNextPageParam: (lastPage) => lastPage?.nextCursor, - initialData: { - pageParams: [undefined, 1], - pages: [initialCurrentRewards], - }, - } - ) - function loadMoreData() { - if (hasNextPage) { - fetchNextPage() - } - } - const filteredRewards = - data?.pages.filter((page) => page && page.rewards) ?? [] - const rewards = filteredRewards.flatMap((page) => page?.rewards) as Reward[] - - if (isLoading) { - return - } - - if (!rewards.length) { - return null - } - - return ( -
- - {rewards.map((reward, idx) => ( -
- - {reward.label} - -
- ))} -
- {hasNextPage && - (isFetching ? ( - - ) : ( - - ))} -
- ) -} diff --git a/components/Blocks/DynamicContent/Rewards/CurrentLevel/current.module.css b/components/Blocks/DynamicContent/Rewards/CurrentLevel/current.module.css deleted file mode 100644 index 64e65574b..000000000 --- a/components/Blocks/DynamicContent/Rewards/CurrentLevel/current.module.css +++ /dev/null @@ -1,12 +0,0 @@ -.card { - align-items: center; - background-color: var(--UI-Opacity-White-100); - border: 1px solid var(--Base-Border-Subtle); - border-radius: var(--Corner-radius-Medium); - display: flex; - flex-direction: column; - gap: var(--Spacing-x1); - justify-content: center; - min-height: 280px; - padding: var(--Spacing-x7) var(--Spacing-x3); -} diff --git a/components/Blocks/DynamicContent/Rewards/CurrentRewards/Client.tsx b/components/Blocks/DynamicContent/Rewards/CurrentRewards/Client.tsx new file mode 100644 index 000000000..761682e83 --- /dev/null +++ b/components/Blocks/DynamicContent/Rewards/CurrentRewards/Client.tsx @@ -0,0 +1,73 @@ +"use client" + +import { useRef, useState } from "react" + +import { RewardIcon } from "@/components/Blocks/DynamicContent/Rewards/RewardIcon" +import Pagination from "@/components/MyPages/Pagination" +import Grids from "@/components/TempDesignSystem/Grids" +import Title from "@/components/TempDesignSystem/Text/Title" + +import Redeem from "./Redeem" + +import styles from "./current.module.css" + +import type { CurrentRewardsClientProps } from "@/types/components/myPages/myPage/accountPage" + +export default function ClientCurrentRewards({ + rewards, + pageSize, + showRedeem, +}: CurrentRewardsClientProps) { + const containerRef = useRef(null) + const [currentPage, setCurrentPage] = useState(1) + + const totalPages = Math.ceil(rewards.length / pageSize) + const startIndex = (currentPage - 1) * pageSize + const endIndex = startIndex + pageSize + const currentRewards = rewards.slice(startIndex, endIndex) + + function handlePageChange(page: number) { + requestAnimationFrame(() => { + setCurrentPage(page) + containerRef.current?.scrollIntoView({ + behavior: "smooth", + block: "start", + inline: "nearest", + }) + }) + } + + return ( +
+ + {currentRewards.map((reward, idx) => ( +
+
+ + + {reward.label} + +
+ {showRedeem && "redeem_description" in reward && ( +
+ +
+ )} +
+ ))} +
+ {totalPages > 1 && ( + + )} +
+ ) +} diff --git a/components/Blocks/DynamicContent/Rewards/CurrentRewards/Redeem.tsx b/components/Blocks/DynamicContent/Rewards/CurrentRewards/Redeem.tsx new file mode 100644 index 000000000..5686ad8a0 --- /dev/null +++ b/components/Blocks/DynamicContent/Rewards/CurrentRewards/Redeem.tsx @@ -0,0 +1,191 @@ +"use client" + +import { motion } from "framer-motion" +import { useState } from "react" +import { + Dialog, + DialogTrigger, + Modal, + ModalOverlay, +} from "react-aria-components" +import { useIntl } from "react-intl" + +import { trpc } from "@/lib/trpc/client" + +import Countdown from "@/components/Countdown" +import { CheckCircleIcon, CloseLargeIcon } from "@/components/Icons" +import Button from "@/components/TempDesignSystem/Button" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Title from "@/components/TempDesignSystem/Text/Title" + +import { RewardIcon } from "../RewardIcon" + +import styles from "./current.module.css" + +import type { + RedeemModalState, + RedeemProps, + RedeemStep, +} from "@/types/components/myPages/myPage/accountPage" + +const MotionOverlay = motion(ModalOverlay) +const MotionModal = motion(Modal) + +export default function Redeem({ reward }: RedeemProps) { + const [animation, setAnimation] = useState("unmounted") + const intl = useIntl() + const update = trpc.contentstack.rewards.redeem.useMutation() + const [redeemStep, setRedeemStep] = useState("initial") + + function onProceed() { + if (reward.id) { + update.mutate( + { rewardId: reward.id }, + { + onSuccess() { + setRedeemStep("redeemed") + }, + onError(error) { + console.error("Failed to redeem", error) + }, + } + ) + } + } + + function modalStateHandler(newAnimationState: RedeemModalState) { + setAnimation((currentAnimationState) => + newAnimationState === "hidden" && currentAnimationState === "hidden" + ? "unmounted" + : currentAnimationState + ) + if (newAnimationState === "unmounted") { + setRedeemStep("initial") + } + } + + return ( + setAnimation(isOpen ? "visible" : "hidden")} + > + + + + + {({ close }) => ( + <> +
+ +
+
+ {redeemStep === "redeemed" && ( +
+
+ + + {intl.formatMessage({ + id: "Redeemed & valid through:", + })} + +
+ +
+ )} + + + {reward.label} + + + {redeemStep === "initial" && ( + {reward.description} + )} + + {redeemStep === "confirmation" && + "redeem_description" in reward && ( + + {reward.redeem_description} + + )} +
+ {redeemStep === "initial" && ( +
+ +
+ )} + + {redeemStep === "confirmation" && ( +
+ + +
+ )} + + )} +
+
+
+
+ ) +} + +const variants = { + fade: { + hidden: { + opacity: 0, + transition: { duration: 0.4, ease: "easeInOut" }, + }, + visible: { + opacity: 1, + transition: { duration: 0.4, ease: "easeInOut" }, + }, + }, + + slideInOut: { + hidden: { + opacity: 0, + y: 32, + transition: { duration: 0.4, ease: "easeInOut" }, + }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.4, ease: "easeInOut" }, + }, + }, +} diff --git a/components/Blocks/DynamicContent/Rewards/CurrentRewards/current.module.css b/components/Blocks/DynamicContent/Rewards/CurrentRewards/current.module.css new file mode 100644 index 000000000..da85887a0 --- /dev/null +++ b/components/Blocks/DynamicContent/Rewards/CurrentRewards/current.module.css @@ -0,0 +1,132 @@ +.container { + display: flex; + flex-direction: column; + gap: var(--Spacing-x4); + position: relative; + scroll-margin-top: calc(var(--current-mobile-site-header-height) * 2); +} + +.card { + background-color: var(--UI-Opacity-White-100); + border: 1px solid var(--Base-Border-Subtle); + border-radius: var(--Corner-radius-Medium); + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.content { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); + align-items: center; + justify-content: center; + padding: var(--Spacing-x3); +} + +.btnContainer { + padding: 0 var(--Spacing-x3) var(--Spacing-x3); +} + +.badge { + border-radius: var(--Small, 4px); + border: 1px solid var(--Base-Border-Subtle); + display: flex; + padding: var(--Spacing-x1) var(--Spacing-x2); + flex-direction: column; + justify-content: center; + align-items: center; +} + +.redeemed { + display: flex; + justify-content: center; + align-items: center; + gap: var(--Spacing-x-half); + align-self: stretch; +} + +.overlay { + background: rgba(0, 0, 0, 0.5); + height: var(--visual-viewport-height); + position: fixed; + top: 0; + left: 0; + width: 100vw; + z-index: 100; +} + +@media screen and (min-width: 768px) { + .overlay { + display: flex; + justify-content: center; + align-items: center; + } +} + +.modal { + background-color: var(--Base-Surface-Primary-light-Normal); + border-radius: var(--Corner-radius-Medium); + box-shadow: 0px 4px 24px 0px rgba(38, 32, 30, 0.08); + width: 100%; + position: absolute; + left: 0; + bottom: 0; + z-index: 101; +} + +@media screen and (min-width: 768px) { + .modal { + left: auto; + bottom: auto; + width: 400px; + } +} + +.dialog { + display: flex; + flex-direction: column; + padding-bottom: var(--Spacing-x3); +} + +.modalHeader { + --button-height: 32px; + box-sizing: content-box; + display: flex; + align-items: center; + height: var(--button-height); + position: relative; + justify-content: center; + padding: var(--Spacing-x3) var(--Spacing-x2) 0; +} + +.modalContent { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--Spacing-x2); + padding: 0 var(--Spacing-x3) var(--Spacing-x3); +} + +.modalFooter { + display: flex; + flex-direction: column; + padding: 0 var(--Spacing-x3) var(--Spacing-x1); + gap: var(--Spacing-x-one-and-half); +} + +.modalFooter > button { + flex: 1 0 100%; +} + +.modalClose { + background: none; + border: none; + cursor: pointer; + position: absolute; + right: var(--Spacing-x2); + width: 32px; + height: var(--button-height); + display: flex; + align-items: center; +} diff --git a/components/Blocks/DynamicContent/Rewards/CurrentLevel/index.tsx b/components/Blocks/DynamicContent/Rewards/CurrentRewards/index.tsx similarity index 67% rename from components/Blocks/DynamicContent/Rewards/CurrentLevel/index.tsx rename to components/Blocks/DynamicContent/Rewards/CurrentRewards/index.tsx index cb970e5e7..8dd47b612 100644 --- a/components/Blocks/DynamicContent/Rewards/CurrentLevel/index.tsx +++ b/components/Blocks/DynamicContent/Rewards/CurrentRewards/index.tsx @@ -1,3 +1,4 @@ +import { env } from "@/env/server" import { serverClient } from "@/lib/trpc/server" import SectionContainer from "@/components/Section/Container" @@ -13,19 +14,20 @@ export default async function CurrentRewardsBlock({ subtitle, link, }: AccountPageComponentProps) { - const initialCurrentRewards = - await serverClient().contentstack.rewards.current({ - limit: 3, - }) + const rewardsResponse = await serverClient().contentstack.rewards.current() - if (!initialCurrentRewards) { + if (!rewardsResponse?.rewards.length) { return null } return ( - + ) diff --git a/components/Blocks/DynamicContent/Rewards/RewardIcon/data.ts b/components/Blocks/DynamicContent/Rewards/RewardIcon/data.ts new file mode 100644 index 000000000..fcb388b64 --- /dev/null +++ b/components/Blocks/DynamicContent/Rewards/RewardIcon/data.ts @@ -0,0 +1,68 @@ +import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name" +import { isValidRewardId } from "@/utils/rewards" + +import type { FC } from "react" + +import { IconName, type IconProps } from "@/types/components/icon" +import { RewardId } from "@/types/enums/rewards" + +function getIconForRewardId(rewardId: RewardId): IconName { + switch (rewardId) { + // Food & beverage + case RewardId.TenPercentFood: + case RewardId.FifteenPercentFood: + return IconName.CroissantCoffeeEgg + case RewardId.TwoForOneBreakfast: + return IconName.CutleryTwo + case RewardId.FreeBreakfast: + return IconName.CutleryOne + case RewardId.FreeKidsDrink: + return IconName.KidsMocktail + + // Monetary vouchers + case RewardId.Bonus50SEK: + case RewardId.Bonus75SEK: + case RewardId.Bonus100SEK: + case RewardId.Bonus150SEK: + case RewardId.Bonus200SEK: + return IconName.Voucher + + // Hotel perks + case RewardId.EarlyCheckin: + return IconName.HandKey + case RewardId.LateCheckout: + return IconName.HotelNight + case RewardId.FreeUpgrade: + return IconName.MagicWand + case RewardId.RoomGuarantee48H: + return IconName.Bed + + // Earnings + case RewardId.EarnRate25Percent: + case RewardId.EarnRate50Percent: + return IconName.MoneyHand + case RewardId.StayBoostForKids: + return IconName.Kids + case RewardId.MemberRate: + return IconName.Coin + + // Special + case RewardId.YearlyExclusiveGift: + return IconName.GiftOpen + + default: { + const unhandledRewardId: never = rewardId + return IconName.GiftOpen + } + } +} + +export function mapRewardToIcon(rewardId: string): FC | null { + if (!isValidRewardId(rewardId)) { + // TODO: Update once UX has decided on fallback icon. + return getIconByIconName(IconName.GiftOpen) + } + + const iconName = getIconForRewardId(rewardId) + return getIconByIconName(iconName) +} diff --git a/components/Blocks/DynamicContent/Rewards/RewardIcon/index.tsx b/components/Blocks/DynamicContent/Rewards/RewardIcon/index.tsx new file mode 100644 index 000000000..082d20f54 --- /dev/null +++ b/components/Blocks/DynamicContent/Rewards/RewardIcon/index.tsx @@ -0,0 +1,27 @@ +import { mapRewardToIcon } from "./data" + +import type { RewardIconProps } from "@/types/components/myPages/rewards" + +// Original SVG aspect ratio is 358:202 (≈1.77:1) +const sizeMap = { + small: { width: 120, height: 68 }, // 40% of card width + medium: { width: 180, height: 102 }, // 60% of card width + large: { width: 240, height: 135 }, // 80% of card width +} as const + +export function RewardIcon({ + rewardId, + size = "medium", + ...props +}: RewardIconProps) { + const IconComponent = mapRewardToIcon(rewardId) + if (!IconComponent) return null + + return ( + + ) +} diff --git a/components/Blocks/DynamicContent/SignupFormWrapper/index.tsx b/components/Blocks/DynamicContent/SignupFormWrapper/index.tsx index 7daf85588..e93785b0e 100644 --- a/components/Blocks/DynamicContent/SignupFormWrapper/index.tsx +++ b/components/Blocks/DynamicContent/SignupFormWrapper/index.tsx @@ -6,7 +6,7 @@ import { getProfileSafely } from "@/lib/trpc/memoizedRequests" import SignupForm from "@/components/Forms/Signup" import { getLang } from "@/i18n/serverContext" -import { SignupFormWrapperProps } from "@/types/components/blocks/dynamicContent" +import type { SignupFormWrapperProps } from "@/types/components/blocks/dynamicContent" export default async function SignupFormWrapper({ dynamic_content, diff --git a/components/Blocks/DynamicContent/Stays/Soonest/index.tsx b/components/Blocks/DynamicContent/Stays/Soonest/index.tsx index 0aaa2316c..c0a16f04a 100644 --- a/components/Blocks/DynamicContent/Stays/Soonest/index.tsx +++ b/components/Blocks/DynamicContent/Stays/Soonest/index.tsx @@ -8,7 +8,7 @@ import Grids from "@/components/TempDesignSystem/Grids" import StayCard from "../StayCard" import EmptyUpcomingStaysBlock from "./EmptyUpcomingStays" -import { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage" +import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage" export default async function SoonestStays({ title, diff --git a/components/Blocks/DynamicContent/index.tsx b/components/Blocks/DynamicContent/index.tsx index a05f5551c..3c9849c57 100644 --- a/components/Blocks/DynamicContent/index.tsx +++ b/components/Blocks/DynamicContent/index.tsx @@ -9,7 +9,7 @@ import OverviewTable from "@/components/Blocks/DynamicContent/OverviewTable" import EarnAndBurn from "@/components/Blocks/DynamicContent/Points/EarnAndBurn" import ExpiringPoints from "@/components/Blocks/DynamicContent/Points/ExpiringPoints" import PointsOverview from "@/components/Blocks/DynamicContent/Points/Overview" -import CurrentRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/CurrentLevel" +import CurrentRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/CurrentRewards" import NextLevelRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/NextLevel" import SignupFormWrapper from "@/components/Blocks/DynamicContent/SignupFormWrapper" import SignUpVerification from "@/components/Blocks/DynamicContent/SignUpVerification" diff --git a/components/Blocks/UspGrid/utils.ts b/components/Blocks/UspGrid/utils.ts index 700482909..47d5ce76c 100644 --- a/components/Blocks/UspGrid/utils.ts +++ b/components/Blocks/UspGrid/utils.ts @@ -1,4 +1,4 @@ -import { UspIcon } from "@/types/components/blocks/uspGrid" +import type { UspIcon } from "@/types/components/blocks/uspGrid" import { IconName } from "@/types/components/icon" export function getUspIconName(icon?: UspIcon | null) { diff --git a/components/BookingWidget/Client.tsx b/components/BookingWidget/Client.tsx index 84ac08cba..3c17caf8e 100644 --- a/components/BookingWidget/Client.tsx +++ b/components/BookingWidget/Client.tsx @@ -114,9 +114,9 @@ export default function BookingWidgetClient({ rooms: defaultRoomsData, }, shouldFocusError: false, - mode: "all", + mode: "onSubmit", resolver: zodResolver(bookingWidgetSchema), - reValidateMode: "onChange", + reValidateMode: "onSubmit", }) function closeMobileSearch() { diff --git a/components/ContentType/HotelPage/AmenitiesList/index.tsx b/components/ContentType/HotelPage/AmenitiesList/index.tsx index 03244ea7a..b55332f6a 100644 --- a/components/ContentType/HotelPage/AmenitiesList/index.tsx +++ b/components/ContentType/HotelPage/AmenitiesList/index.tsx @@ -36,7 +36,7 @@ export default async function AmenitiesList({ height={20} /> )} - {facility.name} + {facility.name} ) })} diff --git a/components/ContentType/HotelPage/Facilities/CardGrid/ActivitiesCardGrid.tsx b/components/ContentType/HotelPage/Facilities/CardGrid/ActivitiesCardGrid.tsx index dd865227e..a2fdbc391 100644 --- a/components/ContentType/HotelPage/Facilities/CardGrid/ActivitiesCardGrid.tsx +++ b/components/ContentType/HotelPage/Facilities/CardGrid/ActivitiesCardGrid.tsx @@ -23,6 +23,7 @@ export default function ActivitiesCardGrid(activitiesCard: ActivityCard) { href: `?s=${activities[lang]}`, title: activitiesCard.ctaText, isExternal: false, + scrollOnClick: false, } : undefined, secondaryButton: hasImage @@ -31,6 +32,7 @@ export default function ActivitiesCardGrid(activitiesCard: ActivityCard) { href: `?s=${activities[lang]}`, title: activitiesCard.ctaText, isExternal: false, + scrollOnClick: false, }, } return ( diff --git a/components/ContentType/HotelPage/IntroSection/index.tsx b/components/ContentType/HotelPage/IntroSection/index.tsx index 7a5349ff0..79f29adbd 100644 --- a/components/ContentType/HotelPage/IntroSection/index.tsx +++ b/components/ContentType/HotelPage/IntroSection/index.tsx @@ -51,7 +51,7 @@ export default async function IntroSection({ {hotelName} - {formattedLocationText} + {formattedLocationText} {hasTripAdvisorData && ( setLightboxIsOpen(true)} className={styles.image} /> ))} diff --git a/components/ContentType/HotelPage/PreviewImages/previewImages.module.css b/components/ContentType/HotelPage/PreviewImages/previewImages.module.css index 064d99dce..edbf90ca7 100644 --- a/components/ContentType/HotelPage/PreviewImages/previewImages.module.css +++ b/components/ContentType/HotelPage/PreviewImages/previewImages.module.css @@ -13,6 +13,7 @@ width: 100%; height: 100%; max-height: 30vh; + cursor: pointer; } .imageWrapper > :nth-child(2), diff --git a/components/ContentType/HotelPage/Rooms/index.tsx b/components/ContentType/HotelPage/Rooms/index.tsx index e976ee8af..54c17bfa7 100644 --- a/components/ContentType/HotelPage/Rooms/index.tsx +++ b/components/ContentType/HotelPage/Rooms/index.tsx @@ -4,9 +4,9 @@ import { useRef, useState } from "react" import { useIntl } from "react-intl" import SectionContainer from "@/components/Section/Container" -import SectionHeader from "@/components/Section/Header" import Grids from "@/components/TempDesignSystem/Grids" import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton" +import Title from "@/components/TempDesignSystem/Text/Title" import { RoomCard } from "./RoomCard" @@ -35,19 +35,19 @@ export function Rooms({ rooms }: RoomsProps) { className={styles.roomsContainer} >
- + + {intl.formatMessage({ id: "Rooms" })} + - {rooms.map((room) => ( -
- -
- ))} + {rooms + .sort((a, b) => a.sortOrder - b.sortOrder) + .map((room) => ( +
+ +
+ ))}
{showToggleButton ? ( diff --git a/components/ContentType/HotelPage/SidePeeks/AboutTheHotel/index.tsx b/components/ContentType/HotelPage/SidePeeks/AboutTheHotel/index.tsx index 0537fda83..272f0db36 100644 --- a/components/ContentType/HotelPage/SidePeeks/AboutTheHotel/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/AboutTheHotel/index.tsx @@ -40,6 +40,7 @@ export default async function AboutTheHotelSidePeek({ {descriptions.descriptions.medium} {descriptions.facilityInformation} + {descriptions.surroundingInformation} ) diff --git a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Accessibility/index.tsx b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Accessibility/index.tsx index 5313bb50a..d922114bd 100644 --- a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Accessibility/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Accessibility/index.tsx @@ -17,6 +17,7 @@ export default async function AccessibilityAmenity({
{accessibility?.description && ( diff --git a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Breakfast/index.tsx b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Breakfast/index.tsx index c731bb86a..d19dc93c9 100644 --- a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Breakfast/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Breakfast/index.tsx @@ -9,6 +9,7 @@ export default async function BreakfastAmenity() { {/* TODO: breakfast to be implemented */} diff --git a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/CheckIn/index.tsx b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/CheckIn/index.tsx index 21cc2b964..95585165a 100644 --- a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/CheckIn/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/CheckIn/index.tsx @@ -14,6 +14,7 @@ export default async function CheckInAmenity({ {intl.formatMessage({ id: "Times" })} {`${intl.formatMessage({ id: "Check in from" })}: ${checkInTime}`} diff --git a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx index 697527048..a2ef9680c 100644 --- a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx @@ -25,6 +25,7 @@ export default async function ParkingAmenity({
{parking.map((data) => ( diff --git a/components/ContentType/HotelPage/SidePeeks/Amenities/FilteredAmenities/filteredAmenities.module.css b/components/ContentType/HotelPage/SidePeeks/Amenities/FilteredAmenities/filteredAmenities.module.css index b6d3ba651..81dc4f43b 100644 --- a/components/ContentType/HotelPage/SidePeeks/Amenities/FilteredAmenities/filteredAmenities.module.css +++ b/components/ContentType/HotelPage/SidePeeks/Amenities/FilteredAmenities/filteredAmenities.module.css @@ -1,10 +1,10 @@ .wrapper { - padding: var(--Spacing-x1); + padding: var(--Spacing-x1) var(--Spacing-x0); border-bottom: 1px solid var(--Base-Border-Subtle); } .amenity { display: flex; gap: var(--Spacing-x1); - padding: var(--Spacing-x-one-and-half) var(--Spacing-x2); + padding: var(--Spacing-x-one-and-half) var(--Spacing-x1); } diff --git a/components/ContentType/HotelPage/SidePeeks/Amenities/FilteredAmenities/index.tsx b/components/ContentType/HotelPage/SidePeeks/Amenities/FilteredAmenities/index.tsx index f121893e5..92df26178 100644 --- a/components/ContentType/HotelPage/SidePeeks/Amenities/FilteredAmenities/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/Amenities/FilteredAmenities/index.tsx @@ -1,5 +1,5 @@ import { HeartIcon } from "@/components/Icons" -import Body from "@/components/TempDesignSystem/Text/Body" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import { mapFacilityToIcon } from "../../../data" @@ -15,16 +15,18 @@ export default function FilteredAmenities({ {filteredAmenities?.map((amenity) => { const Icon = mapFacilityToIcon(amenity.id) return ( -
+
  • {Icon ? ( ) : ( )} - {amenity.name} + + {amenity.name} +
    -
  • + ) })} diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx index 3357765ad..ab1114dbb 100644 --- a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx @@ -6,7 +6,7 @@ import { getIntl } from "@/i18n" import styles from "./facility.module.css" -import { FacilityProps } from "@/types/components/hotelPage/sidepeek/facility" +import type { FacilityProps } from "@/types/components/hotelPage/sidepeek/facility" export default async function Facility({ data }: FacilityProps) { const intl = await getIntl() diff --git a/components/ContentType/HotelPage/TabNavigation/tabNavigation.module.css b/components/ContentType/HotelPage/TabNavigation/tabNavigation.module.css index 4c8cbaca1..31e41abee 100644 --- a/components/ContentType/HotelPage/TabNavigation/tabNavigation.module.css +++ b/components/ContentType/HotelPage/TabNavigation/tabNavigation.module.css @@ -14,6 +14,7 @@ justify-content: flex-start; padding: 0 var(--Spacing-x2); width: 100%; + overflow-x: auto; } @media screen and (min-width: 768px) { @@ -26,5 +27,6 @@ .tabsContainer { padding: 0 var(--Spacing-x5); max-width: calc(100% - var(--hotel-page-map-desktop-width)); + overflow-x: visible; } } diff --git a/components/ContentType/HotelPage/data.ts b/components/ContentType/HotelPage/data.ts index 81ae2b438..3d7faad2f 100644 --- a/components/ContentType/HotelPage/data.ts +++ b/components/ContentType/HotelPage/data.ts @@ -1,8 +1,8 @@ -import { FC } from "react" - import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name" -import { IconName, IconProps } from "@/types/components/icon" +import type { FC } from "react" + +import { IconName, type IconProps } from "@/types/components/icon" import { FacilityEnum } from "@/types/enums/facilities" const facilityToIconMap: Record = { diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index 38002af81..13030f130 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -78,7 +78,7 @@ export default async function HotelPage({ hotelId }: HotelPageProps) { const roomCategories = hotelData.included?.filter((item) => item.type === "roomcategories") || [] const images = gallery?.smallerImages - const description = hotelContent.texts.descriptions.short + const description = hotelContent.texts.descriptions.medium const activitiesCard = content?.[0]?.upcoming_activities_card || null const facilities: Facility[] = [ diff --git a/components/ContentType/StaticPages/staticPage.ts b/components/ContentType/StaticPages/staticPage.ts index 5a3bf84b4..d23d7b525 100644 --- a/components/ContentType/StaticPages/staticPage.ts +++ b/components/ContentType/StaticPages/staticPage.ts @@ -1,10 +1,9 @@ -import { staticPageVariants } from "./variants" - import type { VariantProps } from "class-variance-authority" import type { TrackingSDKPageData } from "@/types/components/tracking" import type { CollectionPage } from "@/types/trpc/routers/contentstack/collectionPage" import type { ContentPage } from "@/types/trpc/routers/contentstack/contentPage" +import type { staticPageVariants } from "./variants" export interface StaticPageProps extends Omit, "content">, diff --git a/components/Countdown/index.tsx b/components/Countdown/index.tsx new file mode 100644 index 000000000..d87cc3c20 --- /dev/null +++ b/components/Countdown/index.tsx @@ -0,0 +1,34 @@ +"use client" + +import { useState } from "react" +import { useInterval } from "usehooks-ts" + +import { dt } from "@/lib/dt" + +import Title from "@/components/TempDesignSystem/Text/Title" + +import type { CountdownProps } from "@/types/components/countdown" + +export default function Countdown({ + minutes = 30, + seconds = 0, +}: CountdownProps) { + const [time, setTime] = useState(dt.duration({ minutes, seconds })) + const timeSeconds = time.asSeconds() + + useInterval( + () => { + setTime((currentTime) => { + const newTime = currentTime.asMilliseconds() - 1000 + return dt.duration(newTime) + }) + }, + timeSeconds > 0 ? 1000 : null + ) + + return ( + + <time dateTime={time.toISOString()}>{time.format("m:ss")}</time> + + ) +} diff --git a/components/Current/Blocks/List/ListItem.tsx b/components/Current/Blocks/List/ListItem.tsx index 2bbd470f4..12fdcd476 100644 --- a/components/Current/Blocks/List/ListItem.tsx +++ b/components/Current/Blocks/List/ListItem.tsx @@ -2,8 +2,7 @@ import { cva } from "class-variance-authority" import styles from "./list.module.css" -import type { ListItem } from "@/types/requests/blocks/list" -import { BlockListItemsEnum } from "@/types/requests/blocks/list" +import { BlockListItemsEnum, type ListItem } from "@/types/requests/blocks/list" const config = { variants: { diff --git a/components/Current/Header/LanguageSwitcher/Desktop/index.tsx b/components/Current/Header/LanguageSwitcher/Desktop/index.tsx index 10ca94c15..a2a81c060 100644 --- a/components/Current/Header/LanguageSwitcher/Desktop/index.tsx +++ b/components/Current/Header/LanguageSwitcher/Desktop/index.tsx @@ -1,7 +1,7 @@ "use client" import { useCallback, useEffect, useRef, useState } from "react" -import { Lang, languages } from "@/constants/languages" +import { type Lang, languages } from "@/constants/languages" import Link from "@/components/TempDesignSystem/Link" import useLang from "@/hooks/useLang" diff --git a/components/Current/Header/LanguageSwitcher/Mobile/index.tsx b/components/Current/Header/LanguageSwitcher/Mobile/index.tsx index d8081227d..45be5f078 100644 --- a/components/Current/Header/LanguageSwitcher/Mobile/index.tsx +++ b/components/Current/Header/LanguageSwitcher/Mobile/index.tsx @@ -1,7 +1,7 @@ "use client" import { useState } from "react" -import { Lang, languages } from "@/constants/languages" +import { type Lang, languages } from "@/constants/languages" import useLang from "@/hooks/useLang" diff --git a/components/Current/Header/LanguageSwitcher/index.tsx b/components/Current/Header/LanguageSwitcher/index.tsx index bac3be427..0aa160255 100644 --- a/components/Current/Header/LanguageSwitcher/index.tsx +++ b/components/Current/Header/LanguageSwitcher/index.tsx @@ -1,7 +1,7 @@ import Desktop from "./Desktop" import Mobile from "./Mobile" -import { LanguageSwitcherData } from "@/types/requests/languageSwitcher" +import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher" type LanguageSwitcherProps = { urls: LanguageSwitcherData } diff --git a/components/Current/Header/MyPagesMobileDropdown/index.tsx b/components/Current/Header/MyPagesMobileDropdown/index.tsx index be58e53b5..753a87594 100644 --- a/components/Current/Header/MyPagesMobileDropdown/index.tsx +++ b/components/Current/Header/MyPagesMobileDropdown/index.tsx @@ -3,7 +3,6 @@ import { Fragment } from "react" import { useIntl } from "react-intl" import { logout } from "@/constants/routes/handleAuth" -import { navigationQueryRouter } from "@/server/routers/contentstack/myPages/navigation/query" import useDropdownStore from "@/stores/main-menu" import Divider from "@/components/TempDesignSystem/Divider" @@ -14,6 +13,7 @@ import useLang from "@/hooks/useLang" import styles from "./my-pages-mobile-dropdown.module.css" import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown" +import type { navigationQueryRouter } from "@/server/routers/contentstack/myPages/navigation/query" type Navigation = Awaited> diff --git a/components/Current/NotFound/Texts.ts b/components/Current/NotFound/Texts.ts index cb1fd017a..c07b406ba 100644 --- a/components/Current/NotFound/Texts.ts +++ b/components/Current/NotFound/Texts.ts @@ -1,4 +1,4 @@ -import { Lang } from "@/constants/languages" +import type { Lang } from "@/constants/languages" type Texts = { title: string diff --git a/components/Current/Tracking.tsx b/components/Current/Tracking.tsx index de366aae5..8c2d33490 100644 --- a/components/Current/Tracking.tsx +++ b/components/Current/Tracking.tsx @@ -3,7 +3,7 @@ import { usePathname, useSearchParams } from "next/navigation" import { useEffect, useState } from "react" -import { +import type { SiteSectionObject, TrackingData, TrackingProps, diff --git a/components/Current/currentRenderOptions.tsx b/components/Current/currentRenderOptions.tsx index 2360e1805..c184f5497 100644 --- a/components/Current/currentRenderOptions.tsx +++ b/components/Current/currentRenderOptions.tsx @@ -7,13 +7,13 @@ import type { EmbedByUid } from "@/types/components/deprecatedjsontohtml" import { EmbedEnum } from "@/types/requests/utils/embeds" import type { Attributes } from "@/types/rte/attrs" import { RTEItemTypeEnum, RTETypeEnum } from "@/types/rte/enums" -import type { - RTEDefaultNode, - RTENext, - RTENode, - RTERegularNode, +import { + type RTEDefaultNode, + RTEMarkType, + type RTENext, + type RTENode, + type RTERegularNode, } from "@/types/rte/node" -import { RTEMarkType } from "@/types/rte/node" import type { RenderOptions } from "@/types/rte/option" function extractPossibleAttributes(attrs: Attributes | undefined) { diff --git a/components/DeprecatedJsonToHtml/renderOptions.tsx b/components/DeprecatedJsonToHtml/renderOptions.tsx index c22f469e0..148993ded 100644 --- a/components/DeprecatedJsonToHtml/renderOptions.tsx +++ b/components/DeprecatedJsonToHtml/renderOptions.tsx @@ -17,7 +17,7 @@ import { hasAvailableParagraphFormat, hasAvailableULFormat } from "./utils" import styles from "./jsontohtml.module.css" import type { EmbedByUid } from "@/types/components/deprecatedjsontohtml" -import { ImageVaultAsset } from "@/types/components/imageVault" +import type { ImageVaultAsset } from "@/types/components/imageVault" import { EmbedEnum } from "@/types/requests/utils/embeds" import type { Attributes, RTEImageVaultAttrs } from "@/types/rte/attrs" import { @@ -25,15 +25,15 @@ import { RTEItemTypeEnum, RTETypeEnum, } from "@/types/rte/enums" -import type { - RTEDefaultNode, - RTEImageNode, - RTENext, - RTENode, - RTERegularNode, - RTETextNode, +import { + type RTEDefaultNode, + type RTEImageNode, + RTEMarkType, + type RTENext, + type RTENode, + type RTERegularNode, + type RTETextNode, } from "@/types/rte/node" -import { RTEMarkType } from "@/types/rte/node" import type { RenderOptions } from "@/types/rte/option" function extractPossibleAttributes(attrs: Attributes | undefined) { diff --git a/components/DeprecatedJsonToHtml/utils.tsx b/components/DeprecatedJsonToHtml/utils.tsx index 347a23106..c87a26a96 100644 --- a/components/DeprecatedJsonToHtml/utils.tsx +++ b/components/DeprecatedJsonToHtml/utils.tsx @@ -8,12 +8,13 @@ import { AvailableULFormatEnum, RTETypeEnum, } from "@/types/rte/enums" -import type { - RTENode, - RTERenderOptionComponent, - RTETextNode, +import { + RTEMarkType, + type RTENode, + type RTERenderMark, + type RTERenderOptionComponent, + type RTETextNode, } from "@/types/rte/node" -import { RTEMarkType, RTERenderMark } from "@/types/rte/node" import type { RenderOptions } from "@/types/rte/option" export function groupEmbedsByUid(embedsArray: Node[]) { diff --git a/components/Footer/Details/index.tsx b/components/Footer/Details/index.tsx index b95c20cca..7827f6043 100644 --- a/components/Footer/Details/index.tsx +++ b/components/Footer/Details/index.tsx @@ -12,7 +12,7 @@ import { getLang } from "@/i18n/serverContext" import styles from "./details.module.css" import type { SocialIconsProps } from "@/types/components/footer/socialIcons" -import { IconName } from "@/types/components/icon" +import type { IconName } from "@/types/components/icon" function SocialIcon({ iconName }: SocialIconsProps) { const SocialIcon = getIconByIconName(iconName as IconName) diff --git a/components/Forms/BookingWidget/FormContent/Input/index.tsx b/components/Forms/BookingWidget/FormContent/Input/index.tsx index 7a592f594..dfa5e018a 100644 --- a/components/Forms/BookingWidget/FormContent/Input/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Input/index.tsx @@ -1,4 +1,5 @@ -import React, { forwardRef, InputHTMLAttributes } from "react" +import React, { forwardRef, type InputHTMLAttributes } from "react" +import { Input as InputRAC } from "react-aria-components" import Body from "@/components/TempDesignSystem/Text/Body" @@ -10,7 +11,7 @@ const Input = forwardRef< >(function InputComponent(props, ref) { return ( - + ) }) diff --git a/components/Forms/BookingWidget/FormContent/Search/SearchList/List/index.tsx b/components/Forms/BookingWidget/FormContent/Search/SearchList/List/index.tsx index 946fb2e28..4ed6998bb 100644 --- a/components/Forms/BookingWidget/FormContent/Search/SearchList/List/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Search/SearchList/List/index.tsx @@ -23,7 +23,7 @@ export default function List({ getItemProps={getItemProps} highlightedIndex={highlightedIndex} index={initialIndex + index} - key={location.id} + key={location.id + index} location={location} /> ))} diff --git a/components/Forms/BookingWidget/FormContent/Search/SearchList/index.tsx b/components/Forms/BookingWidget/FormContent/Search/SearchList/index.tsx index c66c8e10b..cce0e571f 100644 --- a/components/Forms/BookingWidget/FormContent/Search/SearchList/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Search/SearchList/index.tsx @@ -29,23 +29,17 @@ export default function SearchList({ }: SearchListProps) { const intl = useIntl() const [hasMounted, setHasMounted] = useState(false) - const [isFormSubmitted, setIsFormSubmitted] = useState(false) const { clearErrors, formState: { errors, isSubmitted }, } = useFormContext() const searchError = errors["search"] - useEffect(() => { - setIsFormSubmitted(isSubmitted) - }, [isSubmitted]) - useEffect(() => { let timeoutID: ReturnType | null = null - if (searchError && searchError.message === "Required") { + if (searchError) { timeoutID = setTimeout(() => { clearErrors("search") - setIsFormSubmitted(false) // magic number originates from animation // 5000ms delay + 120ms exectuion }, 5120) @@ -66,7 +60,7 @@ export default function SearchList({ return null } - if (searchError && isFormSubmitted) { + if (searchError && isSubmitted) { if (typeof searchError.message === "string") { if (!isOpen) { if (searchError.message === "Required") { @@ -87,6 +81,24 @@ export default function SearchList({ ) + } else if (searchError.type === "custom") { + return ( + + + + {intl.formatMessage({ id: "No results" })} + + + {intl.formatMessage({ + id: "We couldn't find a matching location for your search.", + })} + + + ) } } } diff --git a/components/Forms/BookingWidget/FormContent/Search/index.tsx b/components/Forms/BookingWidget/FormContent/Search/index.tsx index 860ecffde..28b53bb1b 100644 --- a/components/Forms/BookingWidget/FormContent/Search/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Search/index.tsx @@ -1,9 +1,9 @@ "use client" import Downshift from "downshift" import { - ChangeEvent, - FocusEvent, - FormEvent, + type ChangeEvent, + type FocusEvent, + type FormEvent, useCallback, useEffect, useReducer, @@ -26,8 +26,10 @@ import type { SearchProps } from "@/types/components/search" import type { Location } from "@/types/trpc/routers/hotel/locations" const name = "search" -export default function Search({ locations }: SearchProps) { - const { register, setValue, trigger } = useFormContext() + +export default function Search({ locations, handlePressEnter }: SearchProps) { + const { register, setValue, unregister } = + useFormContext() const intl = useIntl() const value = useWatch({ name }) const [state, dispatch] = useReducer( @@ -88,7 +90,6 @@ export default function Search({ locations }: SearchProps) { setValue("location", encodeURIComponent(stringified)) sessionStorage.setItem(sessionStorageKey, stringified) setValue(name, selectedItem.name) - trigger() const searchHistoryMap = new Map() searchHistoryMap.set(selectedItem.name, selectedItem) @@ -135,6 +136,27 @@ export default function Search({ locations }: SearchProps) { } }, [dispatch]) + const stayType = state.searchData?.type === "cities" ? "city" : "hotel" + const stayValue = + (value === state.searchData?.name && + ((state.searchData?.type === "cities" && state.searchData?.name) || + state.searchData?.id)) || + "" + + useEffect(() => { + if (stayType === "city") { + unregister("hotel") + setValue(stayType, stayValue, { + shouldValidate: true, + }) + } else { + unregister("city") + setValue(stayType, Number(stayValue), { + shouldValidate: true, + }) + } + }, [stayType, stayValue, unregister, setValue]) + return ( (value ? value.name : "")} onSelect={handleOnSelect} onInputValueChange={(inputValue) => dispatchInputValue(inputValue)} + defaultHighlightedIndex={0} > {({ - closeMenu, getInputProps, getItemProps, getLabelProps, @@ -155,6 +177,10 @@ export default function Search({ locations }: SearchProps) { openMenu, }) => (
    + {value ? ( + // Adding hidden input to define hotel or city based on destination selection for basic form submit. + + ) : null}
    + {!showMemberPrice && memberPrice ? ( + + ) : null} ) } diff --git a/components/HotelReservation/HotelCard/HotelCardSkeleton.module.css b/components/HotelReservation/HotelCard/HotelCardSkeleton.module.css new file mode 100644 index 000000000..1d757cebf --- /dev/null +++ b/components/HotelReservation/HotelCard/HotelCardSkeleton.module.css @@ -0,0 +1,63 @@ +.card { + font-size: 14px; + display: flex; + flex-direction: column; + background-color: #fff; + border-radius: var(--Corner-radius-Large); + border: 1px solid var(--Base-Border-Subtle); + position: relative; + height: 100%; + justify-content: space-between; + min-height: 200px; + flex: 1; + overflow: hidden; +} + +.imageContainer { + aspect-ratio: 16/9; + width: 100%; + height: 200px; +} + +.priceVariants { + display: flex; + flex-direction: column; + gap: var(--Spacing-x1); + padding: var(--Spacing-x2); + flex: 1; +} + +.content { + display: flex; + flex-direction: column; + flex: 1; + gap: var(--Spacing-x1); + padding: var(--Spacing-x2); +} + +.text { + display: none; +} + +@media (min-width: 1367px) { + .content { + padding: var(--Spacing-x2) 0 var(--Spacing-x2) var(--Spacing-x2); + } + + .text { + gap: 10px; + display: flex; + flex-direction: column; + } + + .card { + flex-direction: row; + } + .imageContainer { + width: 315px; + height: 100%; + } + .priceVariants { + max-width: 260px; + } +} diff --git a/components/HotelReservation/HotelCard/HotelCardSkeleton.tsx b/components/HotelReservation/HotelCard/HotelCardSkeleton.tsx new file mode 100644 index 000000000..341857247 --- /dev/null +++ b/components/HotelReservation/HotelCard/HotelCardSkeleton.tsx @@ -0,0 +1,34 @@ +import SkeletonShimmer from "@/components/SkeletonShimmer" + +import styles from "./HotelCardSkeleton.module.css" + +export function HotelCardSkeleton() { + return ( +
    + {/* image container */} +
    + +
    + +
    + +
    + + + + +
    + + +
    + +
    + {/* price variants */} + {Array.from({ length: 2 }).map((_, index) => ( + + ))} + +
    +
    + ) +} diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 5c2f788ee..89d0c7243 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -3,8 +3,8 @@ import { useParams } from "next/dist/client/components/navigation" import { memo, useCallback } from "react" import { useIntl } from "react-intl" -import { Lang } from "@/constants/languages" -import { selectHotelMap, selectRate } from "@/constants/routes/hotelReservation" +import { selectRate } from "@/constants/routes/hotelReservation" +import { useHotelsMapStore } from "@/stores/hotels-map" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import ImageGallery from "@/components/ImageGallery" @@ -27,31 +27,33 @@ import styles from "./hotelCard.module.css" import { HotelCardListingTypeEnum } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" import type { HotelCardProps } from "@/types/components/hotelReservation/selectHotel/hotelCardProps" +import type { Lang } from "@/constants/languages" function HotelCard({ hotel, type = HotelCardListingTypeEnum.PageListing, state = "default", - onHotelCardHover, }: HotelCardProps) { const params = useParams() const lang = params.lang as Lang const intl = useIntl() + const { setActiveHotelPin, setActiveHotelCard } = useHotelsMapStore() const { hotelData } = hotel const { price } = hotel const handleMouseEnter = useCallback(() => { - if (onHotelCardHover && hotelData) { - onHotelCardHover(hotelData.name) + if (hotelData) { + setActiveHotelPin(hotelData.name) } - }, [onHotelCardHover, hotelData]) + }, [setActiveHotelPin, hotelData]) const handleMouseLeave = useCallback(() => { - if (onHotelCardHover) { - onHotelCardHover(null) + if (hotelData) { + setActiveHotelPin(null) + setActiveHotelCard(null) } - }, [onHotelCardHover]) + }, [setActiveHotelPin, hotelData, setActiveHotelCard]) if (!hotel || !hotelData) return null @@ -96,15 +98,20 @@ function HotelCard({ {hotelData.address.streetAddress}, {hotelData.address.city} - - + + {hotelData.address.streetAddress}, {hotelData.address.city} - - + +
    diff --git a/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css b/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css index 7d607d3ad..30cd03385 100644 --- a/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css +++ b/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css @@ -106,8 +106,7 @@ } @media (min-width: 768px) { - .facilities, - .memberPrice { + .facilities { display: none; } .dialog { diff --git a/components/HotelReservation/HotelCardDialog/index.tsx b/components/HotelReservation/HotelCardDialog/index.tsx index e6a5e2f02..1c35836b2 100644 --- a/components/HotelReservation/HotelCardDialog/index.tsx +++ b/components/HotelReservation/HotelCardDialog/index.tsx @@ -4,7 +4,6 @@ import { useParams } from "next/navigation" import { useState } from "react" import { useIntl } from "react-intl" -import { Lang } from "@/constants/languages" import { selectRate } from "@/constants/routes/hotelReservation" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" @@ -22,6 +21,7 @@ import NoPriceAvailableCard from "../HotelCard/NoPriceAvailableCard" import styles from "./hotelCardDialog.module.css" import type { HotelCardDialogProps } from "@/types/components/hotelReservation/selectHotel/map" +import type { Lang } from "@/constants/languages" export default function HotelCardDialog({ data, @@ -103,12 +103,14 @@ export default function HotelCardDialog({ {intl.formatMessage({ id: "From" })} - - {publicPrice} {currency} - - /{intl.formatMessage({ id: "night" })} - - + {publicPrice && ( + + {publicPrice} {currency} + + /{intl.formatMessage({ id: "night" })} + + + )} {memberPrice && ( (null) const observerRef = useRef(null) const dialogRef = useRef(null) const isMobile = useMediaQuery("(max-width: 768px)") + const { activeHotelCard, setActiveHotelCard, setActiveHotelPin } = + useHotelsMapStore() - useClickOutside(dialogRef, !!activeCard && isMobile, () => { - onActiveCardChange(null) - }) + function handleClose() { + setActiveHotelCard(null) + setActiveHotelPin(null) + } + + useClickOutside(dialogRef, !!activeHotelCard && isMobile, handleClose) const handleIntersection = useCallback( (entries: IntersectionObserverEntry[]) => { @@ -33,12 +38,12 @@ export default function HotelCardDialogListing({ if (entry.isIntersecting) { const cardName = entry.target.getAttribute("data-name") if (cardName) { - onActiveCardChange(cardName) + setActiveHotelCard(cardName) } } }) }, - [onActiveCardChange] + [setActiveHotelCard] ) useEffect(() => { @@ -73,13 +78,13 @@ export default function HotelCardDialogListing({ elements.forEach((el) => observerRef.current?.observe(el)) }, 1000) } - }, [activeCard]) + }, [activeHotelCard]) return (
    {!!hotelsPinData?.length && hotelsPinData.map((data) => { - const isActive = data.name === activeCard + const isActive = data.name === activeHotelCard return (
    onActiveCardChange(null)} + isOpen={!!activeHotelCard} + handleClose={handleClose} />
    ) diff --git a/components/HotelReservation/HotelCardDialogListing/utils.ts b/components/HotelReservation/HotelCardDialogListing/utils.ts index 1a0e05ad8..fb658adc0 100644 --- a/components/HotelReservation/HotelCardDialogListing/utils.ts +++ b/components/HotelReservation/HotelCardDialogListing/utils.ts @@ -12,7 +12,10 @@ export function getHotelPins(hotels: HotelData[]): HotelPin[] { name: hotel.hotelData.name, publicPrice: hotel.price?.public?.localPrice.pricePerNight ?? null, memberPrice: hotel.price?.member?.localPrice.pricePerNight ?? null, - currency: hotel.price?.public?.localPrice.currency || null, + currency: + hotel.price?.public?.localPrice.currency || + hotel.price?.member?.localPrice.currency || + null, images: [ hotel.hotelData.hotelContent.images, ...(hotel.hotelData.gallery?.heroImages ?? []), @@ -25,5 +28,8 @@ export function getHotelPins(hotels: HotelData[]): HotelPin[] { .slice(0, 3), ratings: hotel.hotelData.ratings?.tripAdvisor.rating ?? null, operaId: hotel.hotelData.operaId, + facilityIds: hotel.hotelData.detailedFacilities.map( + (facility) => facility.id + ), })) } diff --git a/components/HotelReservation/HotelCardListing/index.tsx b/components/HotelReservation/HotelCardListing/index.tsx index d9d0b4f30..c7687362b 100644 --- a/components/HotelReservation/HotelCardListing/index.tsx +++ b/components/HotelReservation/HotelCardListing/index.tsx @@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react" import { useIntl } from "react-intl" import { useHotelFilterStore } from "@/stores/hotel-filters" +import { useHotelsMapStore } from "@/stores/hotels-map" import Alert from "@/components/TempDesignSystem/Alert" import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton" @@ -24,14 +25,13 @@ import { AlertTypeEnum } from "@/types/enums/alert" export default function HotelCardListing({ hotelData, type = HotelCardListingTypeEnum.PageListing, - activeCard, - onHotelCardHover, }: HotelCardListingProps) { const searchParams = useSearchParams() const activeFilters = useHotelFilterStore((state) => state.activeFilters) const setResultCount = useHotelFilterStore((state) => state.setResultCount) const [showBackToTop, setShowBackToTop] = useState(false) const intl = useIntl() + const { activeHotelCard } = useHotelsMapStore() const sortBy = useMemo( () => searchParams.get("sort") ?? DEFAULT_SORT, @@ -111,13 +111,16 @@ export default function HotelCardListing({ hotels.map((hotel) => (
    )) @@ -128,7 +131,9 @@ export default function HotelCardListing({ text={intl.formatMessage({ id: "filters.nohotel.text" })} /> ) : null} - {showBackToTop && } + {showBackToTop && ( + + )} ) } diff --git a/components/HotelReservation/ReadMore/index.tsx b/components/HotelReservation/ReadMore/index.tsx index 5b55f59ca..dbbc4312c 100644 --- a/components/HotelReservation/ReadMore/index.tsx +++ b/components/HotelReservation/ReadMore/index.tsx @@ -7,7 +7,7 @@ import Button from "@/components/TempDesignSystem/Button" import styles from "./readMore.module.css" -import { ReadMoreProps } from "@/types/components/hotelReservation/selectHotel/selectHotel" +import type { ReadMoreProps } from "@/types/components/hotelReservation/selectHotel/selectHotel" import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek" export default function ReadMore({ label, hotelId, showCTA }: ReadMoreProps) { diff --git a/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx b/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx index 8a68a8105..f1d3d9751 100644 --- a/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx +++ b/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx @@ -1,5 +1,10 @@ "use client" +import { + usePathname, + useSearchParams, +} from "next/dist/client/components/navigation" +import { useCallback, useState } from "react" import { Dialog as AriaDialog, DialogTrigger, @@ -13,25 +18,69 @@ import { useHotelFilterStore } from "@/stores/hotel-filters" import { CloseLargeIcon, FilterIcon } from "@/components/Icons" import Button from "@/components/TempDesignSystem/Button" import Divider from "@/components/TempDesignSystem/Divider" +import Select from "@/components/TempDesignSystem/Select" import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useInitializeFiltersFromUrl from "@/hooks/useInitializeFiltersFromUrl" import HotelFilter from "../HotelFilter" -import HotelSorter from "../HotelSorter" +import { DEFAULT_SORT } from "../HotelSorter" import styles from "./filterAndSortModal.module.css" import type { FilterAndSortModalProps } from "@/types/components/hotelReservation/selectHotel/filterAndSortModal" +import { + type SortItem, + SortOrder, +} from "@/types/components/hotelReservation/selectHotel/hotelSorter" export default function FilterAndSortModal({ filters, }: FilterAndSortModalProps) { const intl = useIntl() useInitializeFiltersFromUrl() + const searchParams = useSearchParams() + const pathname = usePathname() const resultCount = useHotelFilterStore((state) => state.resultCount) const setFilters = useHotelFilterStore((state) => state.setFilters) const activeFilters = useHotelFilterStore((state) => state.activeFilters) + const [sort, setSort] = useState(searchParams.get("sort") ?? DEFAULT_SORT) + + const sortItems: SortItem[] = [ + { + label: intl.formatMessage({ id: "Distance to city centre" }), + value: SortOrder.Distance, + }, + { label: intl.formatMessage({ id: "Name" }), value: SortOrder.Name }, + { label: intl.formatMessage({ id: "Price" }), value: SortOrder.Price }, + { + label: intl.formatMessage({ id: "TripAdvisor rating" }), + value: SortOrder.TripAdvisorRating, + }, + ] + + const handleSortSelect = useCallback((value: string | number) => { + setSort(value.toString()) + }, []) + + const handleApplyFiltersAndSorting = useCallback( + (close: () => void) => { + if (sort === searchParams.get("sort")) { + close() + } + + const newSearchParams = new URLSearchParams(searchParams) + newSearchParams.set("sort", sort) + + window.history.replaceState( + null, + "", + `${pathname}?${newSearchParams.toString()}` + ) + close() + }, + [pathname, searchParams, sort] + ) return ( <> @@ -65,7 +114,16 @@ export default function FilterAndSortModal({
    - +