From eabe45b73c7b2597d4b2dcb107d46b482ef048f2 Mon Sep 17 00:00:00 2001 From: Hrishikesh Vaipurkar Date: Thu, 13 Feb 2025 09:24:47 +0000 Subject: [PATCH] Merged in feat/SW-1557-implement-booking-code-select (pull request #1304) feat: SW-1577 Implemented booking code city search * feat: SW-1577 Implemented booking code city search * feat: SW-1557 Strict comparison * feat: SW-1557 Review comments fix Approved-by: Michael Zetterberg Approved-by: Pontus Dreij --- .../(standard)/select-hotel/utils.ts | 11 + .../hotelreservation/(standard)/utils.ts | 2 + .../BookingCode/TabletCodeInput/index.tsx | 1 + .../FormContent/BookingCode/index.tsx | 1 + .../HotelCard/hotelCard.module.css | 9 + .../HotelReservation/HotelCard/index.tsx | 16 +- .../HotelCardDialogListing/utils.ts | 2 + .../HotelCardListing/index.tsx | 31 +- .../HotelCardListing/utils.ts | 28 +- .../bookingCodeFilter.module.css | 15 + .../SelectHotel/BookingCodeFilter/index.tsx | 56 ++ .../SelectHotel/HotelSorter/index.tsx | 1 + .../SelectHotelMapContainer.tsx | 38 +- .../SelectHotelMapContent/index.tsx | 31 +- .../SelectHotel/SelectHotelMap/index.tsx | 2 + .../HotelReservation/SelectHotel/index.tsx | 34 +- .../SelectHotel/selectHotel.module.css | 7 +- components/TempDesignSystem/Select/index.tsx | 11 +- .../TempDesignSystem/Select/select.module.css | 5 + components/TempDesignSystem/Select/select.ts | 2 + i18n/dictionaries/da.json | 3 + i18n/dictionaries/de.json | 3 + i18n/dictionaries/en.json | 3 + i18n/dictionaries/fi.json | 3 + i18n/dictionaries/no.json | 3 + i18n/dictionaries/sv.json | 3 + lib/api/endpoints.ts | 2 +- server/routers/hotels/metrics.ts | 9 + server/routers/hotels/query.ts | 542 ++++++++++-------- stores/bookingCode-filter.ts | 13 + .../selectHotel/hotelCardProps.ts | 1 + .../hotelReservation/selectHotel/map.ts | 2 + .../selectHotel/selectHotelSearchParams.ts | 2 + .../hotelReservation/selectRate/selectRate.ts | 1 + types/trpc/routers/hotel/availability.ts | 10 + 35 files changed, 627 insertions(+), 276 deletions(-) create mode 100644 components/HotelReservation/SelectHotel/BookingCodeFilter/bookingCodeFilter.module.css create mode 100644 components/HotelReservation/SelectHotel/BookingCodeFilter/index.tsx create mode 100644 stores/bookingCode-filter.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts index 3edbe088f..cd0be320c 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts @@ -46,6 +46,17 @@ export async function fetchAvailableHotels( return enhanceHotels(availableHotels) } +export async function fetchBookingCodeAvailableHotels( + input: AvailabilityInput +): Promise { + const availableHotels = + await serverClient().hotel.availability.hotelsByCityWithBookingCode(input) + + if (!availableHotels) return [] + + return enhanceHotels(availableHotels) +} + export async function fetchAlternativeHotels( hotelId: string, input: AlternativeHotelsAvailabilityInput diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils.ts index d948eaaab..755275dcb 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils.ts +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils.ts @@ -26,6 +26,7 @@ interface HotelSearchDetails { adultsInRoom: number[] childrenInRoomString?: string childrenInRoom?: Child[] + bookingCode?: string } export async function getHotelSearchDetails< @@ -101,5 +102,6 @@ export async function getHotelSearchDetails< adultsInRoom, childrenInRoomString, childrenInRoom, + bookingCode: selectHotelParams.bookingCode ?? undefined, } } diff --git a/components/Forms/BookingWidget/FormContent/BookingCode/TabletCodeInput/index.tsx b/components/Forms/BookingWidget/FormContent/BookingCode/TabletCodeInput/index.tsx index 25410023c..10a7830dc 100644 --- a/components/Forms/BookingWidget/FormContent/BookingCode/TabletCodeInput/index.tsx +++ b/components/Forms/BookingWidget/FormContent/BookingCode/TabletCodeInput/index.tsx @@ -20,6 +20,7 @@ export default function TabletCodeInput({ {...register("bookingCode.value", { onChange: (e) => updateValue(e.target.value), })} + autoComplete="off" /> ) } diff --git a/components/Forms/BookingWidget/FormContent/BookingCode/index.tsx b/components/Forms/BookingWidget/FormContent/BookingCode/index.tsx index 280b7e75d..a8b2ea698 100644 --- a/components/Forms/BookingWidget/FormContent/BookingCode/index.tsx +++ b/components/Forms/BookingWidget/FormContent/BookingCode/index.tsx @@ -170,6 +170,7 @@ export default function BookingCode() { id="booking-code" onChange={(event) => updateBookingCodeFormValue(event.target.value)} defaultValue={bookingCode?.value} + autoComplete="off" /> {codeError?.message ? ( diff --git a/components/HotelReservation/HotelCard/hotelCard.module.css b/components/HotelReservation/HotelCard/hotelCard.module.css index 0ea50cad2..06b8e50c5 100644 --- a/components/HotelReservation/HotelCard/hotelCard.module.css +++ b/components/HotelReservation/HotelCard/hotelCard.module.css @@ -92,6 +92,15 @@ gap: var(--Spacing-x-one-and-half); } +.strikedText { + text-decoration: line-through; +} + +@media screen and (min-width: 768px) and (max-width: 1024px) { + .imageContainer { + height: 180px; + } +} @media screen and (min-width: 1367px) { .card.pageListing { flex-direction: row; diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 7062718d0..b24073985 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -8,6 +8,7 @@ import { selectRate } from "@/constants/routes/hotelReservation" import { useHotelsMapStore } from "@/stores/hotels-map" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" +import { PriceTagIcon } from "@/components/Icons" import HotelLogo from "@/components/Icons/Logos" import ImageGallery from "@/components/ImageGallery" import Button from "@/components/TempDesignSystem/Button" @@ -36,6 +37,7 @@ function HotelCard({ isUserLoggedIn, state = "default", type = HotelCardListingTypeEnum.PageListing, + bookingCode = "", }: HotelCardProps) { const params = useParams() const lang = params.lang as Lang @@ -71,6 +73,7 @@ function HotelCard({ const galleryImages = mapApiImagesToGalleryImages( hotelData.galleryImages || [] ) + const fullPrice = hotel.price?.public?.rateType?.toLowerCase() === "regular" return (
) : ( <> - {!isUserLoggedIn && price.public && ( - + {bookingCode && ( + + + {bookingCode} + )} + {(!isUserLoggedIn || (bookingCode && !fullPrice)) && + price.public && ( + + )} {price.member && ( searchParams.get("sort") ?? DEFAULT_SORT, - [searchParams] + const sortBy = searchParams.get("sort") ?? DEFAULT_SORT + + const bookingCode = searchParams.get("bookingCode") + const activeCodeFilter = useBookingCodeFilterStore( + (state) => state.activeCodeFilter ) const sortedHotels = useMemo(() => { if (!hotelData) return [] - return getSortedHotels({ hotels: hotelData, sortBy }) - }, [hotelData, sortBy]) + return getSortedHotels({ hotels: hotelData, sortBy, bookingCode }) + }, [hotelData, sortBy, bookingCode]) const hotels = useMemo(() => { - if (activeFilters.length === 0) return sortedHotels + const updatedHotelsList = bookingCode + ? sortedHotels.filter( + (hotel) => + !hotel.price || + activeCodeFilter === "all" || + (activeCodeFilter === "discounted" && + hotel.price?.public?.rateType?.toLowerCase() !== "regular") || + activeCodeFilter === hotel.price?.public?.rateType?.toLowerCase() + ) + : sortedHotels - return sortedHotels.filter((hotel) => + if (activeFilters.length === 0) return updatedHotelsList + + return updatedHotelsList.filter((hotel) => activeFilters.every((appliedFilterId) => hotel.hotelData.detailedFacilities.some( (facility) => facility.id.toString() === appliedFilterId ) ) ) - }, [activeFilters, sortedHotels]) + }, [activeFilters, sortedHotels, bookingCode, activeCodeFilter]) useEffect(() => { setResultCount(hotels?.length ?? 0) @@ -79,6 +93,7 @@ export default function HotelCardListing({ hotel.hotelData.name === activeHotelCard ? "active" : "default" } type={type} + bookingCode={bookingCode} /> )) diff --git a/components/HotelReservation/HotelCardListing/utils.ts b/components/HotelReservation/HotelCardListing/utils.ts index 1e57a6721..3e048bc0e 100644 --- a/components/HotelReservation/HotelCardListing/utils.ts +++ b/components/HotelReservation/HotelCardListing/utils.ts @@ -4,14 +4,18 @@ import { SortOrder } from "@/types/components/hotelReservation/selectHotel/hotel export function getSortedHotels({ hotels, sortBy, + bookingCode, }: { hotels: HotelData[] sortBy: string + bookingCode: string | null }) { const getPricePerNight = (hotel: HotelData): number => hotel.price?.member?.localPrice?.pricePerNight ?? hotel.price?.public?.localPrice?.pricePerNight ?? Infinity + const availableHotels = hotels.filter((hotel) => !!hotel?.price) + const unAvailableHotels = hotels.filter((hotel) => !hotel?.price) const sortingStrategies: Record< string, @@ -29,7 +33,27 @@ export function getSortedHotels({ b.hotelData.location.distanceToCentre, } - return [...hotels].sort( + const sortStrategy = sortingStrategies[sortBy] ?? sortingStrategies[SortOrder.Distance] - ) + + if (bookingCode) { + const bookingCodeHotels = hotels.filter( + (hotel) => + (hotel?.price?.public?.rateType?.toLowerCase() !== "regular" || + hotel?.price?.member?.rateType?.toLowerCase() !== "regular") && + !!hotel?.price + ) + const regularHotels = hotels.filter( + (hotel) => hotel?.price?.public?.rateType?.toLowerCase() === "regular" + ) + + return [...bookingCodeHotels] + .sort(sortStrategy) + .concat([...regularHotels].sort(sortStrategy)) + .concat([...unAvailableHotels].sort(sortStrategy)) + } + + return [...availableHotels] + .sort(sortStrategy) + .concat([...unAvailableHotels].sort(sortStrategy)) } diff --git a/components/HotelReservation/SelectHotel/BookingCodeFilter/bookingCodeFilter.module.css b/components/HotelReservation/SelectHotel/BookingCodeFilter/bookingCodeFilter.module.css new file mode 100644 index 000000000..0dd8e71b7 --- /dev/null +++ b/components/HotelReservation/SelectHotel/BookingCodeFilter/bookingCodeFilter.module.css @@ -0,0 +1,15 @@ +.bookingCodeFilter { + display: flex; + justify-content: flex-end; + width: 100%; +} + +.bookingCodeFilterSelect { + min-width: 200px; +} + +@media screen and (max-width: 767px) { + .bookingCodeFilter { + margin-bottom: var(--Spacing-x3); + } +} diff --git a/components/HotelReservation/SelectHotel/BookingCodeFilter/index.tsx b/components/HotelReservation/SelectHotel/BookingCodeFilter/index.tsx new file mode 100644 index 000000000..94ff93c62 --- /dev/null +++ b/components/HotelReservation/SelectHotel/BookingCodeFilter/index.tsx @@ -0,0 +1,56 @@ +"use client" + +import { useIntl } from "react-intl" + +import { useBookingCodeFilterStore } from "@/stores/bookingCode-filter" + +import { PriceTagIcon } from "@/components/Icons" +import Select from "@/components/TempDesignSystem/Select" + +import styles from "./bookingCodeFilter.module.css" + +import type { Key } from "react" + +export default function BookingCodeFilter() { + const intl = useIntl() + const activeCodeFilter = useBookingCodeFilterStore( + (state) => state.activeCodeFilter + ) + const setFilter = useBookingCodeFilterStore((state) => state.setFilter) + + const bookingCodeFilterItems = [ + { + label: intl.formatMessage({ id: "Discounted rooms" }), + value: "discounted", + }, + { + label: intl.formatMessage({ id: "Full price rooms" }), + value: "regular", + }, + { + label: intl.formatMessage({ id: "See all" }), + value: "all", + }, + ] + + function updateFilter(selectedFilter: Key) { + setFilter(selectedFilter as string) + } + + return ( + <> +
+