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
This commit is contained in:
@@ -46,6 +46,17 @@ export async function fetchAvailableHotels(
|
|||||||
return enhanceHotels(availableHotels)
|
return enhanceHotels(availableHotels)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchBookingCodeAvailableHotels(
|
||||||
|
input: AvailabilityInput
|
||||||
|
): Promise<NullableHotelData[]> {
|
||||||
|
const availableHotels =
|
||||||
|
await serverClient().hotel.availability.hotelsByCityWithBookingCode(input)
|
||||||
|
|
||||||
|
if (!availableHotels) return []
|
||||||
|
|
||||||
|
return enhanceHotels(availableHotels)
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchAlternativeHotels(
|
export async function fetchAlternativeHotels(
|
||||||
hotelId: string,
|
hotelId: string,
|
||||||
input: AlternativeHotelsAvailabilityInput
|
input: AlternativeHotelsAvailabilityInput
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ interface HotelSearchDetails<T> {
|
|||||||
adultsInRoom: number[]
|
adultsInRoom: number[]
|
||||||
childrenInRoomString?: string
|
childrenInRoomString?: string
|
||||||
childrenInRoom?: Child[]
|
childrenInRoom?: Child[]
|
||||||
|
bookingCode?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getHotelSearchDetails<
|
export async function getHotelSearchDetails<
|
||||||
@@ -101,5 +102,6 @@ export async function getHotelSearchDetails<
|
|||||||
adultsInRoom,
|
adultsInRoom,
|
||||||
childrenInRoomString,
|
childrenInRoomString,
|
||||||
childrenInRoom,
|
childrenInRoom,
|
||||||
|
bookingCode: selectHotelParams.bookingCode ?? undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export default function TabletCodeInput({
|
|||||||
{...register("bookingCode.value", {
|
{...register("bookingCode.value", {
|
||||||
onChange: (e) => updateValue(e.target.value),
|
onChange: (e) => updateValue(e.target.value),
|
||||||
})}
|
})}
|
||||||
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ export default function BookingCode() {
|
|||||||
id="booking-code"
|
id="booking-code"
|
||||||
onChange={(event) => updateBookingCodeFormValue(event.target.value)}
|
onChange={(event) => updateBookingCodeFormValue(event.target.value)}
|
||||||
defaultValue={bookingCode?.value}
|
defaultValue={bookingCode?.value}
|
||||||
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
{codeError?.message ? (
|
{codeError?.message ? (
|
||||||
<Caption color="red" className={styles.error}>
|
<Caption color="red" className={styles.error}>
|
||||||
|
|||||||
@@ -92,6 +92,15 @@
|
|||||||
gap: var(--Spacing-x-one-and-half);
|
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) {
|
@media screen and (min-width: 1367px) {
|
||||||
.card.pageListing {
|
.card.pageListing {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { selectRate } from "@/constants/routes/hotelReservation"
|
|||||||
import { useHotelsMapStore } from "@/stores/hotels-map"
|
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||||
|
|
||||||
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||||
|
import { PriceTagIcon } from "@/components/Icons"
|
||||||
import HotelLogo from "@/components/Icons/Logos"
|
import HotelLogo from "@/components/Icons/Logos"
|
||||||
import ImageGallery from "@/components/ImageGallery"
|
import ImageGallery from "@/components/ImageGallery"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
@@ -36,6 +37,7 @@ function HotelCard({
|
|||||||
isUserLoggedIn,
|
isUserLoggedIn,
|
||||||
state = "default",
|
state = "default",
|
||||||
type = HotelCardListingTypeEnum.PageListing,
|
type = HotelCardListingTypeEnum.PageListing,
|
||||||
|
bookingCode = "",
|
||||||
}: HotelCardProps) {
|
}: HotelCardProps) {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const lang = params.lang as Lang
|
const lang = params.lang as Lang
|
||||||
@@ -71,6 +73,7 @@ function HotelCard({
|
|||||||
const galleryImages = mapApiImagesToGalleryImages(
|
const galleryImages = mapApiImagesToGalleryImages(
|
||||||
hotelData.galleryImages || []
|
hotelData.galleryImages || []
|
||||||
)
|
)
|
||||||
|
const fullPrice = hotel.price?.public?.rateType?.toLowerCase() === "regular"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
@@ -161,7 +164,16 @@ function HotelCard({
|
|||||||
<NoPriceAvailableCard />
|
<NoPriceAvailableCard />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{!isUserLoggedIn && price.public && (
|
{bookingCode && (
|
||||||
|
<span
|
||||||
|
className={`${styles.bookingCode} ${fullPrice ? styles.strikedText : ""}`}
|
||||||
|
>
|
||||||
|
<PriceTagIcon height={20} width={20} />
|
||||||
|
{bookingCode}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{(!isUserLoggedIn || (bookingCode && !fullPrice)) &&
|
||||||
|
price.public && (
|
||||||
<HotelPriceCard productTypePrices={price.public} />
|
<HotelPriceCard productTypePrices={price.public} />
|
||||||
)}
|
)}
|
||||||
{price.member && (
|
{price.member && (
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export function getHotelPins(hotels: HotelData[]): HotelPin[] {
|
|||||||
name: hotel.hotelData.name,
|
name: hotel.hotelData.name,
|
||||||
publicPrice: hotel.price?.public?.localPrice.pricePerNight ?? null,
|
publicPrice: hotel.price?.public?.localPrice.pricePerNight ?? null,
|
||||||
memberPrice: hotel.price?.member?.localPrice.pricePerNight ?? null,
|
memberPrice: hotel.price?.member?.localPrice.pricePerNight ?? null,
|
||||||
|
rateType:
|
||||||
|
hotel.price?.public?.rateType ?? hotel.price?.member?.rateType ?? null,
|
||||||
currency:
|
currency:
|
||||||
hotel.price?.public?.localPrice.currency ||
|
hotel.price?.public?.localPrice.currency ||
|
||||||
hotel.price?.member?.localPrice.currency ||
|
hotel.price?.member?.localPrice.currency ||
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useSession } from "next-auth/react"
|
|||||||
import { useEffect, useMemo } from "react"
|
import { useEffect, useMemo } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { useBookingCodeFilterStore } from "@/stores/bookingCode-filter"
|
||||||
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
||||||
import { useHotelsMapStore } from "@/stores/hotels-map"
|
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||||
|
|
||||||
@@ -36,27 +37,40 @@ export default function HotelCardListing({
|
|||||||
const { activeHotelCard } = useHotelsMapStore()
|
const { activeHotelCard } = useHotelsMapStore()
|
||||||
const { showBackToTop, scrollToTop } = useScrollToTop({ threshold: 490 })
|
const { showBackToTop, scrollToTop } = useScrollToTop({ threshold: 490 })
|
||||||
|
|
||||||
const sortBy = useMemo(
|
const sortBy = searchParams.get("sort") ?? DEFAULT_SORT
|
||||||
() => searchParams.get("sort") ?? DEFAULT_SORT,
|
|
||||||
[searchParams]
|
const bookingCode = searchParams.get("bookingCode")
|
||||||
|
const activeCodeFilter = useBookingCodeFilterStore(
|
||||||
|
(state) => state.activeCodeFilter
|
||||||
)
|
)
|
||||||
|
|
||||||
const sortedHotels = useMemo(() => {
|
const sortedHotels = useMemo(() => {
|
||||||
if (!hotelData) return []
|
if (!hotelData) return []
|
||||||
return getSortedHotels({ hotels: hotelData, sortBy })
|
return getSortedHotels({ hotels: hotelData, sortBy, bookingCode })
|
||||||
}, [hotelData, sortBy])
|
}, [hotelData, sortBy, bookingCode])
|
||||||
|
|
||||||
const hotels = useMemo(() => {
|
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) =>
|
activeFilters.every((appliedFilterId) =>
|
||||||
hotel.hotelData.detailedFacilities.some(
|
hotel.hotelData.detailedFacilities.some(
|
||||||
(facility) => facility.id.toString() === appliedFilterId
|
(facility) => facility.id.toString() === appliedFilterId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}, [activeFilters, sortedHotels])
|
}, [activeFilters, sortedHotels, bookingCode, activeCodeFilter])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setResultCount(hotels?.length ?? 0)
|
setResultCount(hotels?.length ?? 0)
|
||||||
@@ -79,6 +93,7 @@ export default function HotelCardListing({
|
|||||||
hotel.hotelData.name === activeHotelCard ? "active" : "default"
|
hotel.hotelData.name === activeHotelCard ? "active" : "default"
|
||||||
}
|
}
|
||||||
type={type}
|
type={type}
|
||||||
|
bookingCode={bookingCode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -4,14 +4,18 @@ import { SortOrder } from "@/types/components/hotelReservation/selectHotel/hotel
|
|||||||
export function getSortedHotels({
|
export function getSortedHotels({
|
||||||
hotels,
|
hotels,
|
||||||
sortBy,
|
sortBy,
|
||||||
|
bookingCode,
|
||||||
}: {
|
}: {
|
||||||
hotels: HotelData[]
|
hotels: HotelData[]
|
||||||
sortBy: string
|
sortBy: string
|
||||||
|
bookingCode: string | null
|
||||||
}) {
|
}) {
|
||||||
const getPricePerNight = (hotel: HotelData): number =>
|
const getPricePerNight = (hotel: HotelData): number =>
|
||||||
hotel.price?.member?.localPrice?.pricePerNight ??
|
hotel.price?.member?.localPrice?.pricePerNight ??
|
||||||
hotel.price?.public?.localPrice?.pricePerNight ??
|
hotel.price?.public?.localPrice?.pricePerNight ??
|
||||||
Infinity
|
Infinity
|
||||||
|
const availableHotels = hotels.filter((hotel) => !!hotel?.price)
|
||||||
|
const unAvailableHotels = hotels.filter((hotel) => !hotel?.price)
|
||||||
|
|
||||||
const sortingStrategies: Record<
|
const sortingStrategies: Record<
|
||||||
string,
|
string,
|
||||||
@@ -29,7 +33,27 @@ export function getSortedHotels({
|
|||||||
b.hotelData.location.distanceToCentre,
|
b.hotelData.location.distanceToCentre,
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...hotels].sort(
|
const sortStrategy =
|
||||||
sortingStrategies[sortBy] ?? sortingStrategies[SortOrder.Distance]
|
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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<div className={styles.bookingCodeFilter}>
|
||||||
|
<Select
|
||||||
|
aria-label="Booking Code Filter"
|
||||||
|
className={styles.bookingCodeFilterSelect}
|
||||||
|
name="bookingCodeFilter"
|
||||||
|
onSelect={updateFilter}
|
||||||
|
label=""
|
||||||
|
items={bookingCodeFilterItems}
|
||||||
|
defaultSelectedKey={activeCodeFilter}
|
||||||
|
optionsIcon={<PriceTagIcon />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -55,6 +55,7 @@ export default function HotelSorter({ discreet }: HotelSorterProps) {
|
|||||||
items={sortItems}
|
items={sortItems}
|
||||||
defaultSelectedKey={searchParams.get("sort") ?? DEFAULT_SORT}
|
defaultSelectedKey={searchParams.get("sort") ?? DEFAULT_SORT}
|
||||||
label={intl.formatMessage({ id: "Sort by" })}
|
label={intl.formatMessage({ id: "Sort by" })}
|
||||||
|
aria-label={intl.formatMessage({ id: "Sort by" })}
|
||||||
name="sort"
|
name="sort"
|
||||||
showRadioButton
|
showRadioButton
|
||||||
discreet={discreet}
|
discreet={discreet}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { getCityCoordinates } from "@/lib/trpc/memoizedRequests"
|
|||||||
import {
|
import {
|
||||||
fetchAlternativeHotels,
|
fetchAlternativeHotels,
|
||||||
fetchAvailableHotels,
|
fetchAvailableHotels,
|
||||||
|
fetchBookingCodeAvailableHotels,
|
||||||
getFiltersFromHotels,
|
getFiltersFromHotels,
|
||||||
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
||||||
import { getHotelSearchDetails } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils"
|
import { getHotelSearchDetails } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils"
|
||||||
@@ -19,10 +20,7 @@ import { getHotelPins } from "../../HotelCardDialogListing/utils"
|
|||||||
import SelectHotelMap from "."
|
import SelectHotelMap from "."
|
||||||
|
|
||||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||||
import type {
|
import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||||
HotelData,
|
|
||||||
NullableHotelData,
|
|
||||||
} from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
|
||||||
import type { SelectHotelMapContainerProps } from "@/types/components/hotelReservation/selectHotel/map"
|
import type { SelectHotelMapContainerProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||||
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
||||||
import {
|
import {
|
||||||
@@ -60,6 +58,7 @@ export async function SelectHotelMapContainer({
|
|||||||
childrenInRoom,
|
childrenInRoom,
|
||||||
childrenInRoomString,
|
childrenInRoomString,
|
||||||
hotel: isAlternativeFor,
|
hotel: isAlternativeFor,
|
||||||
|
bookingCode,
|
||||||
} = searchDetails
|
} = searchDetails
|
||||||
|
|
||||||
if (!city) return notFound()
|
if (!city) return notFound()
|
||||||
@@ -71,6 +70,18 @@ export async function SelectHotelMapContainer({
|
|||||||
roomStayEndDate: selectHotelParams.toDate,
|
roomStayEndDate: selectHotelParams.toDate,
|
||||||
adults: adultsInRoom[0],
|
adults: adultsInRoom[0],
|
||||||
children: childrenInRoomString,
|
children: childrenInRoomString,
|
||||||
|
bookingCode,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
: bookingCode
|
||||||
|
? safeTry(
|
||||||
|
fetchBookingCodeAvailableHotels({
|
||||||
|
cityId: city.id,
|
||||||
|
roomStayStartDate: selectHotelParams.fromDate,
|
||||||
|
roomStayEndDate: selectHotelParams.toDate,
|
||||||
|
adults: adultsInRoom[0],
|
||||||
|
children: childrenInRoomString,
|
||||||
|
bookingCode,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
: safeTry(
|
: safeTry(
|
||||||
@@ -142,6 +153,7 @@ export async function SelectHotelMapContainer({
|
|||||||
hotels={validHotels}
|
hotels={validHotels}
|
||||||
filterList={filterList}
|
filterList={filterList}
|
||||||
cityCoordinates={cityCoordinates}
|
cityCoordinates={cityCoordinates}
|
||||||
|
bookingCode={bookingCode ?? ""}
|
||||||
/>
|
/>
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<TrackingSDK
|
<TrackingSDK
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useIntl } from "react-intl"
|
|||||||
import { useMediaQuery } from "usehooks-ts"
|
import { useMediaQuery } from "usehooks-ts"
|
||||||
|
|
||||||
import { selectHotel } from "@/constants/routes/hotelReservation"
|
import { selectHotel } from "@/constants/routes/hotelReservation"
|
||||||
|
import { useBookingCodeFilterStore } from "@/stores/bookingCode-filter"
|
||||||
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
||||||
import { useHotelsMapStore } from "@/stores/hotels-map"
|
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ import useLang from "@/hooks/useLang"
|
|||||||
import { useScrollToTop } from "@/hooks/useScrollToTop"
|
import { useScrollToTop } from "@/hooks/useScrollToTop"
|
||||||
import { debounce } from "@/utils/debounce"
|
import { debounce } from "@/utils/debounce"
|
||||||
|
|
||||||
|
import BookingCodeFilter from "../../BookingCodeFilter"
|
||||||
import FilterAndSortModal from "../../FilterAndSortModal"
|
import FilterAndSortModal from "../../FilterAndSortModal"
|
||||||
import HotelListing from "../HotelListing"
|
import HotelListing from "../HotelListing"
|
||||||
import { getVisibleHotels } from "./utils"
|
import { getVisibleHotels } from "./utils"
|
||||||
@@ -35,6 +37,7 @@ export default function SelectHotelContent({
|
|||||||
mapId,
|
mapId,
|
||||||
hotels,
|
hotels,
|
||||||
filterList,
|
filterList,
|
||||||
|
bookingCode,
|
||||||
}: Omit<SelectHotelMapProps, "apiKey">) {
|
}: Omit<SelectHotelMapProps, "apiKey">) {
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
@@ -53,6 +56,9 @@ export default function SelectHotelContent({
|
|||||||
elementRef: listingContainerRef,
|
elementRef: listingContainerRef,
|
||||||
refScrollable: true,
|
refScrollable: true,
|
||||||
})
|
})
|
||||||
|
const activeCodeFilter = useBookingCodeFilterStore(
|
||||||
|
(state) => state.activeCodeFilter
|
||||||
|
)
|
||||||
|
|
||||||
const coordinates = useMemo(
|
const coordinates = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -72,15 +78,23 @@ export default function SelectHotelContent({
|
|||||||
}
|
}
|
||||||
}, [activeHotelCard, activeHotelPin])
|
}, [activeHotelCard, activeHotelPin])
|
||||||
|
|
||||||
const filteredHotelPins = useMemo(
|
const filteredHotelPins = useMemo(() => {
|
||||||
() =>
|
const updatedHotelsList = bookingCode
|
||||||
hotelPins.filter((hotel) =>
|
? hotelPins.filter(
|
||||||
|
(hotel) =>
|
||||||
|
!hotel.publicPrice ||
|
||||||
|
activeCodeFilter === "all" ||
|
||||||
|
(activeCodeFilter === "discounted" &&
|
||||||
|
hotel.rateType?.toLowerCase() !== "regular") ||
|
||||||
|
activeCodeFilter === hotel.rateType?.toLowerCase()
|
||||||
|
)
|
||||||
|
: hotelPins
|
||||||
|
return updatedHotelsList.filter((hotel) =>
|
||||||
activeFilters.every((filterId) =>
|
activeFilters.every((filterId) =>
|
||||||
hotel.facilityIds.includes(Number(filterId))
|
hotel.facilityIds.includes(Number(filterId))
|
||||||
)
|
)
|
||||||
),
|
|
||||||
[activeFilters, hotelPins]
|
|
||||||
)
|
)
|
||||||
|
}, [activeFilters, hotelPins, bookingCode, activeCodeFilter])
|
||||||
|
|
||||||
const getHotelCards = useCallback(() => {
|
const getHotelCards = useCallback(() => {
|
||||||
const visibleHotels = getVisibleHotels(hotels, filteredHotelPins, map)
|
const visibleHotels = getVisibleHotels(hotels, filteredHotelPins, map)
|
||||||
@@ -143,6 +157,7 @@ export default function SelectHotelContent({
|
|||||||
filters={filterList}
|
filters={filterList}
|
||||||
setShowSkeleton={setShowSkeleton}
|
setShowSkeleton={setShowSkeleton}
|
||||||
/>
|
/>
|
||||||
|
{bookingCode ? <BookingCodeFilter /> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showSkeleton ? (
|
{showSkeleton ? (
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export default function SelectHotelMap({
|
|||||||
hotels,
|
hotels,
|
||||||
filterList,
|
filterList,
|
||||||
cityCoordinates,
|
cityCoordinates,
|
||||||
|
bookingCode,
|
||||||
}: SelectHotelMapProps) {
|
}: SelectHotelMapProps) {
|
||||||
return (
|
return (
|
||||||
<APIProvider apiKey={apiKey}>
|
<APIProvider apiKey={apiKey}>
|
||||||
@@ -22,6 +23,7 @@ export default function SelectHotelMap({
|
|||||||
mapId={mapId}
|
mapId={mapId}
|
||||||
hotels={hotels}
|
hotels={hotels}
|
||||||
filterList={filterList}
|
filterList={filterList}
|
||||||
|
bookingCode={bookingCode}
|
||||||
/>
|
/>
|
||||||
</APIProvider>
|
</APIProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
fetchAlternativeHotels,
|
fetchAlternativeHotels,
|
||||||
fetchAvailableHotels,
|
fetchAvailableHotels,
|
||||||
|
fetchBookingCodeAvailableHotels,
|
||||||
getFiltersFromHotels,
|
getFiltersFromHotels,
|
||||||
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
||||||
import { getHotelSearchDetails } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils"
|
import { getHotelSearchDetails } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils"
|
||||||
@@ -27,6 +28,7 @@ import { safeTry } from "@/utils/safeTry"
|
|||||||
import { convertObjToSearchParams } from "@/utils/url"
|
import { convertObjToSearchParams } from "@/utils/url"
|
||||||
|
|
||||||
import HotelCardListing from "../HotelCardListing"
|
import HotelCardListing from "../HotelCardListing"
|
||||||
|
import BookingCodeFilter from "./BookingCodeFilter"
|
||||||
import HotelCount from "./HotelCount"
|
import HotelCount from "./HotelCount"
|
||||||
import HotelFilter from "./HotelFilter"
|
import HotelFilter from "./HotelFilter"
|
||||||
import HotelSorter from "./HotelSorter"
|
import HotelSorter from "./HotelSorter"
|
||||||
@@ -74,6 +76,7 @@ export default async function SelectHotel({
|
|||||||
childrenInRoomString,
|
childrenInRoomString,
|
||||||
childrenInRoom,
|
childrenInRoom,
|
||||||
hotel: isAlternativeFor,
|
hotel: isAlternativeFor,
|
||||||
|
bookingCode,
|
||||||
} = searchDetails
|
} = searchDetails
|
||||||
|
|
||||||
if (!city) return notFound()
|
if (!city) return notFound()
|
||||||
@@ -85,6 +88,18 @@ export default async function SelectHotel({
|
|||||||
roomStayEndDate: selectHotelParams.toDate,
|
roomStayEndDate: selectHotelParams.toDate,
|
||||||
adults: adultsInRoom[0],
|
adults: adultsInRoom[0],
|
||||||
children: childrenInRoomString,
|
children: childrenInRoomString,
|
||||||
|
bookingCode,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
: bookingCode
|
||||||
|
? safeTry(
|
||||||
|
fetchBookingCodeAvailableHotels({
|
||||||
|
cityId: city.id,
|
||||||
|
roomStayStartDate: selectHotelParams.fromDate,
|
||||||
|
roomStayEndDate: selectHotelParams.toDate,
|
||||||
|
adults: adultsInRoom[0],
|
||||||
|
children: childrenInRoomString,
|
||||||
|
bookingCode,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
: safeTry(
|
: safeTry(
|
||||||
@@ -204,6 +219,7 @@ export default async function SelectHotel({
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
|
{bookingCode ? <BookingCodeFilter /> : null}
|
||||||
<div className={styles.sideBar}>
|
<div className={styles.sideBar}>
|
||||||
{hotels && hotels.length > 0 ? ( // TODO: Temp fix until API returns hotels that are not available
|
{hotels && hotels.length > 0 ? ( // TODO: Temp fix until API returns hotels that are not available
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -75,6 +75,9 @@
|
|||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.main {
|
.main {
|
||||||
padding: var(--Spacing-x5) 0;
|
padding: var(--Spacing-x5) 0;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--Spacing-x5);
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.headerContent {
|
.headerContent {
|
||||||
@@ -125,10 +128,6 @@
|
|||||||
border-radius: var(--Corner-radius-Medium);
|
border-radius: var(--Corner-radius-Medium);
|
||||||
border: 1px solid var(--Base-Border-Subtle);
|
border: 1px solid var(--Base-Border-Subtle);
|
||||||
}
|
}
|
||||||
.main {
|
|
||||||
flex-direction: row;
|
|
||||||
gap: var(--Spacing-x5);
|
|
||||||
}
|
|
||||||
.buttonContainer {
|
.buttonContainer {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export default function Select({
|
|||||||
showRadioButton = false,
|
showRadioButton = false,
|
||||||
discreet = false,
|
discreet = false,
|
||||||
isNestedInModal = false,
|
isNestedInModal = false,
|
||||||
|
optionsIcon,
|
||||||
}: SelectProps) {
|
}: SelectProps) {
|
||||||
const [rootDiv, setRootDiv] = useState<SelectPortalContainer>(undefined)
|
const [rootDiv, setRootDiv] = useState<SelectPortalContainer>(undefined)
|
||||||
const setOverflowVisible = useSetOverflowVisibleOnRA(isNestedInModal)
|
const setOverflowVisible = useSetOverflowVisibleOnRA(isNestedInModal)
|
||||||
@@ -89,7 +90,12 @@ export default function Select({
|
|||||||
{label}
|
{label}
|
||||||
{discreet && `:`}
|
{discreet && `:`}
|
||||||
</Label>
|
</Label>
|
||||||
{selectedText && <Body>{selectedText}</Body>}
|
{selectedText && (
|
||||||
|
<Body className={optionsIcon ? styles.iconLabel : ""}>
|
||||||
|
{optionsIcon ? optionsIcon : null}
|
||||||
|
{selectedText}
|
||||||
|
</Body>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</SelectValue>
|
</SelectValue>
|
||||||
@@ -114,11 +120,12 @@ export default function Select({
|
|||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<ListBoxItem
|
<ListBoxItem
|
||||||
aria-label={item.label}
|
aria-label={item.label}
|
||||||
className={`${styles.listBoxItem} ${showRadioButton && styles.showRadioButton}`}
|
className={`${styles.listBoxItem} ${showRadioButton && styles.showRadioButton} ${optionsIcon && styles.iconLabel}`}
|
||||||
id={item.value}
|
id={item.value}
|
||||||
key={item.label}
|
key={item.label}
|
||||||
data-testid={item.label}
|
data-testid={item.label}
|
||||||
>
|
>
|
||||||
|
{optionsIcon ? optionsIcon : null}
|
||||||
{item.label}
|
{item.label}
|
||||||
</ListBoxItem>
|
</ListBoxItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -40,6 +40,11 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.iconLabel {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Spacing-x-half);
|
||||||
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--UI-Opacity-White-100);
|
background-color: var(--UI-Opacity-White-100);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ReactElement } from "react"
|
||||||
import type { Key } from "react-aria-components"
|
import type { Key } from "react-aria-components"
|
||||||
|
|
||||||
export interface SelectProps
|
export interface SelectProps
|
||||||
@@ -12,6 +13,7 @@ export interface SelectProps
|
|||||||
showRadioButton?: boolean
|
showRadioButton?: boolean
|
||||||
discreet?: boolean
|
discreet?: boolean
|
||||||
isNestedInModal?: boolean
|
isNestedInModal?: boolean
|
||||||
|
optionsIcon?: ReactElement
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SelectPortalContainer = HTMLDivElement | undefined
|
export type SelectPortalContainer = HTMLDivElement | undefined
|
||||||
|
|||||||
@@ -161,6 +161,7 @@
|
|||||||
"Dimensions": "Dimensioner",
|
"Dimensions": "Dimensioner",
|
||||||
"Discard changes": "Kassér ændringer",
|
"Discard changes": "Kassér ændringer",
|
||||||
"Discard unsaved changes?": "Slette ændringer, der ikke er gemt?",
|
"Discard unsaved changes?": "Slette ændringer, der ikke er gemt?",
|
||||||
|
"Discounted rooms": "Værelser med rabat",
|
||||||
"Discover": "Opdag",
|
"Discover": "Opdag",
|
||||||
"Discover the little extra touches to make your upcoming stay even more unforgettable.": "Discover the little extra touches to make your upcoming stay even more unforgettable.",
|
"Discover the little extra touches to make your upcoming stay even more unforgettable.": "Discover the little extra touches to make your upcoming stay even more unforgettable.",
|
||||||
"Discover {name}": "Opdag {name}",
|
"Discover {name}": "Opdag {name}",
|
||||||
@@ -222,6 +223,7 @@
|
|||||||
"Friends with Benefits": "Friends with Benefits",
|
"Friends with Benefits": "Friends with Benefits",
|
||||||
"From": "Fra",
|
"From": "Fra",
|
||||||
"Full circle": "Full circle",
|
"Full circle": "Full circle",
|
||||||
|
"Full price rooms": "Fuld pris værelser",
|
||||||
"Garage": "Garage",
|
"Garage": "Garage",
|
||||||
"Get inspired": "Bliv inspireret",
|
"Get inspired": "Bliv inspireret",
|
||||||
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.": "Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.": "Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
||||||
@@ -485,6 +487,7 @@
|
|||||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||||
"Search": "Søge",
|
"Search": "Søge",
|
||||||
|
"See all": "Se alle",
|
||||||
"See all photos": "Se alle billeder",
|
"See all photos": "Se alle billeder",
|
||||||
"See alternative hotels": "See alternative hotels",
|
"See alternative hotels": "See alternative hotels",
|
||||||
"See destination": "Se destination",
|
"See destination": "Se destination",
|
||||||
|
|||||||
@@ -162,6 +162,7 @@
|
|||||||
"Dimensions": "Abmessungen",
|
"Dimensions": "Abmessungen",
|
||||||
"Discard changes": "Änderungen verwerfen",
|
"Discard changes": "Änderungen verwerfen",
|
||||||
"Discard unsaved changes?": "Nicht gespeicherte Änderungen verwerfen?",
|
"Discard unsaved changes?": "Nicht gespeicherte Änderungen verwerfen?",
|
||||||
|
"Discounted rooms": "Zimmer mit Rabatt",
|
||||||
"Discover": "Entdecken",
|
"Discover": "Entdecken",
|
||||||
"Discover the little extra touches to make your upcoming stay even more unforgettable.": "Discover the little extra touches to make your upcoming stay even more unforgettable.",
|
"Discover the little extra touches to make your upcoming stay even more unforgettable.": "Discover the little extra touches to make your upcoming stay even more unforgettable.",
|
||||||
"Discover {name}": "Entdecken Sie {name}",
|
"Discover {name}": "Entdecken Sie {name}",
|
||||||
@@ -223,6 +224,7 @@
|
|||||||
"Friends with Benefits": "Friends with Benefits",
|
"Friends with Benefits": "Friends with Benefits",
|
||||||
"From": "Fromm",
|
"From": "Fromm",
|
||||||
"Full circle": "Full circle",
|
"Full circle": "Full circle",
|
||||||
|
"Full price rooms": "Zimmer zum vollen Preis",
|
||||||
"Garage": "Garage",
|
"Garage": "Garage",
|
||||||
"Get inspired": "Lassen Sie sich inspieren",
|
"Get inspired": "Lassen Sie sich inspieren",
|
||||||
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.": "Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.": "Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
||||||
@@ -486,6 +488,7 @@
|
|||||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||||
"Search": "Suchen",
|
"Search": "Suchen",
|
||||||
|
"See all": "Alle anzeigen",
|
||||||
"See all photos": "Alle Fotos ansehen",
|
"See all photos": "Alle Fotos ansehen",
|
||||||
"See alternative hotels": "See alternative hotels",
|
"See alternative hotels": "See alternative hotels",
|
||||||
"See destination": "Siehe Ziel",
|
"See destination": "Siehe Ziel",
|
||||||
|
|||||||
@@ -165,6 +165,7 @@
|
|||||||
"Dimensions": "Dimensions",
|
"Dimensions": "Dimensions",
|
||||||
"Discard changes": "Discard changes",
|
"Discard changes": "Discard changes",
|
||||||
"Discard unsaved changes?": "Discard unsaved changes?",
|
"Discard unsaved changes?": "Discard unsaved changes?",
|
||||||
|
"Discounted rooms": "Discounted rooms",
|
||||||
"Discover": "Discover",
|
"Discover": "Discover",
|
||||||
"Discover the little extra touches to make your upcoming stay even more unforgettable.": "Discover the little extra touches to make your upcoming stay even more unforgettable.",
|
"Discover the little extra touches to make your upcoming stay even more unforgettable.": "Discover the little extra touches to make your upcoming stay even more unforgettable.",
|
||||||
"Discover {name}": "Discover {name}",
|
"Discover {name}": "Discover {name}",
|
||||||
@@ -226,6 +227,7 @@
|
|||||||
"Friends with Benefits": "Friends with Benefits",
|
"Friends with Benefits": "Friends with Benefits",
|
||||||
"From": "From",
|
"From": "From",
|
||||||
"Full circle": "Full circle",
|
"Full circle": "Full circle",
|
||||||
|
"Full price rooms": "Full price rooms",
|
||||||
"Garage": "Garage",
|
"Garage": "Garage",
|
||||||
"Get inspired": "Get inspired",
|
"Get inspired": "Get inspired",
|
||||||
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.": "Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.": "Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
||||||
@@ -491,6 +493,7 @@
|
|||||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||||
"Search": "Search",
|
"Search": "Search",
|
||||||
|
"See all": "See all",
|
||||||
"See all photos": "See all photos",
|
"See all photos": "See all photos",
|
||||||
"See alternative hotels": "See alternative hotels",
|
"See alternative hotels": "See alternative hotels",
|
||||||
"See destination": "See destination",
|
"See destination": "See destination",
|
||||||
|
|||||||
@@ -161,6 +161,7 @@
|
|||||||
"Dimensions": "Mitat",
|
"Dimensions": "Mitat",
|
||||||
"Discard changes": "Hylkää muutokset",
|
"Discard changes": "Hylkää muutokset",
|
||||||
"Discard unsaved changes?": "Hylkäätkö tallentamattomat muutokset?",
|
"Discard unsaved changes?": "Hylkäätkö tallentamattomat muutokset?",
|
||||||
|
"Discounted rooms": "Alennetut huoneet",
|
||||||
"Discover": "Löydä",
|
"Discover": "Löydä",
|
||||||
"Discover the little extra touches to make your upcoming stay even more unforgettable.": "Discover the little extra touches to make your upcoming stay even more unforgettable.",
|
"Discover the little extra touches to make your upcoming stay even more unforgettable.": "Discover the little extra touches to make your upcoming stay even more unforgettable.",
|
||||||
"Discover {name}": "Tutustu {name}",
|
"Discover {name}": "Tutustu {name}",
|
||||||
@@ -222,6 +223,7 @@
|
|||||||
"Friends with Benefits": "Friends with Benefits",
|
"Friends with Benefits": "Friends with Benefits",
|
||||||
"From": "From",
|
"From": "From",
|
||||||
"Full circle": "Full circle",
|
"Full circle": "Full circle",
|
||||||
|
"Full price rooms": "Täyshintaiset huoneet",
|
||||||
"Garage": "Autotalli",
|
"Garage": "Autotalli",
|
||||||
"Get inspired": "Inspiroidu",
|
"Get inspired": "Inspiroidu",
|
||||||
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.": "Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.": "Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
||||||
@@ -486,6 +488,7 @@
|
|||||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||||
"Search": "Haku",
|
"Search": "Haku",
|
||||||
|
"See all": "Katso kaikki",
|
||||||
"See all photos": "Katso kaikki kuvat",
|
"See all photos": "Katso kaikki kuvat",
|
||||||
"See alternative hotels": "See alternative hotels",
|
"See alternative hotels": "See alternative hotels",
|
||||||
"See destination": "Katso kohde",
|
"See destination": "Katso kohde",
|
||||||
|
|||||||
@@ -160,6 +160,7 @@
|
|||||||
"Dimensions": "Dimensjoner",
|
"Dimensions": "Dimensjoner",
|
||||||
"Discard changes": "Forkaste endringer",
|
"Discard changes": "Forkaste endringer",
|
||||||
"Discard unsaved changes?": "Forkaste endringer som ikke er lagret?",
|
"Discard unsaved changes?": "Forkaste endringer som ikke er lagret?",
|
||||||
|
"Discounted rooms": "Rabatterte rom",
|
||||||
"Discover": "Oppdag",
|
"Discover": "Oppdag",
|
||||||
"Discover the little extra touches to make your upcoming stay even more unforgettable.": "Discover the little extra touches to make your upcoming stay even more unforgettable.",
|
"Discover the little extra touches to make your upcoming stay even more unforgettable.": "Discover the little extra touches to make your upcoming stay even more unforgettable.",
|
||||||
"Discover {name}": "Oppdag {name}",
|
"Discover {name}": "Oppdag {name}",
|
||||||
@@ -221,6 +222,7 @@
|
|||||||
"Friends with Benefits": "Friends with Benefits",
|
"Friends with Benefits": "Friends with Benefits",
|
||||||
"From": "Fra",
|
"From": "Fra",
|
||||||
"Full circle": "Full circle",
|
"Full circle": "Full circle",
|
||||||
|
"Full price rooms": "Full pris rom",
|
||||||
"Garage": "Garasje",
|
"Garage": "Garasje",
|
||||||
"Get inspired": "Bli inspirert",
|
"Get inspired": "Bli inspirert",
|
||||||
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.": "Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.": "Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
||||||
@@ -484,6 +486,7 @@
|
|||||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||||
"Search": "Søk",
|
"Search": "Søk",
|
||||||
|
"See all": "Se alle",
|
||||||
"See all photos": "Se alle bilder",
|
"See all photos": "Se alle bilder",
|
||||||
"See alternative hotels": "See alternative hotels",
|
"See alternative hotels": "See alternative hotels",
|
||||||
"See destination": "Se destinasjon",
|
"See destination": "Se destinasjon",
|
||||||
|
|||||||
@@ -160,6 +160,7 @@
|
|||||||
"Dimensions": "Dimensioner",
|
"Dimensions": "Dimensioner",
|
||||||
"Discard changes": "Ignorera ändringar",
|
"Discard changes": "Ignorera ändringar",
|
||||||
"Discard unsaved changes?": "Vill du ignorera ändringar som inte har sparats?",
|
"Discard unsaved changes?": "Vill du ignorera ändringar som inte har sparats?",
|
||||||
|
"Discounted rooms": "Rabatterade rum",
|
||||||
"Discover": "Upptäck",
|
"Discover": "Upptäck",
|
||||||
"Discover the little extra touches to make your upcoming stay even more unforgettable.": "Discover the little extra touches to make your upcoming stay even more unforgettable.",
|
"Discover the little extra touches to make your upcoming stay even more unforgettable.": "Discover the little extra touches to make your upcoming stay even more unforgettable.",
|
||||||
"Discover {name}": "Upptäck {name}",
|
"Discover {name}": "Upptäck {name}",
|
||||||
@@ -221,6 +222,7 @@
|
|||||||
"Friends with Benefits": "Friends with Benefits",
|
"Friends with Benefits": "Friends with Benefits",
|
||||||
"From": "Från",
|
"From": "Från",
|
||||||
"Full circle": "Full circle",
|
"Full circle": "Full circle",
|
||||||
|
"Full price rooms": "Fullpris rum",
|
||||||
"Garage": "Garage",
|
"Garage": "Garage",
|
||||||
"Get inspired": "Bli inspirerad",
|
"Get inspired": "Bli inspirerad",
|
||||||
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.": "Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.": "Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
||||||
@@ -484,6 +486,7 @@
|
|||||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||||
"Search": "Sök",
|
"Search": "Sök",
|
||||||
|
"See all": "Se alla",
|
||||||
"See all photos": "Se alla foton",
|
"See all photos": "Se alla foton",
|
||||||
"See alternative hotels": "See alternative hotels",
|
"See alternative hotels": "See alternative hotels",
|
||||||
"See destination": "Se destination",
|
"See destination": "Se destination",
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export namespace endpoints {
|
|||||||
return `${base.path.availability}/${version}/${base.enitity.Availabilities}/city/${cityId}`
|
return `${base.path.availability}/${version}/${base.enitity.Availabilities}/city/${cityId}`
|
||||||
}
|
}
|
||||||
export function hotels() {
|
export function hotels() {
|
||||||
return `${base.path.availability}/${version}/${base.enitity.Availabilities}/hotel`
|
return `${base.path.availability}/${version}/${base.enitity.Availabilities}/hotels`
|
||||||
}
|
}
|
||||||
export function hotel(hotelId: string) {
|
export function hotel(hotelId: string) {
|
||||||
return `${base.path.availability}/${version}/${base.enitity.Availabilities}/hotel/${hotelId}`
|
return `${base.path.availability}/${version}/${base.enitity.Availabilities}/hotel/${hotelId}`
|
||||||
|
|||||||
@@ -32,6 +32,15 @@ export const metrics = {
|
|||||||
fail: meter.createCounter("trpc.hotel.availability.hotels-fail"),
|
fail: meter.createCounter("trpc.hotel.availability.hotels-fail"),
|
||||||
success: meter.createCounter("trpc.hotel.availability.hotels-success"),
|
success: meter.createCounter("trpc.hotel.availability.hotels-success"),
|
||||||
},
|
},
|
||||||
|
hotelsAvailabilityBookingCode: {
|
||||||
|
counter: meter.createCounter("trpc.hotel.availability.hotels-booking-code"),
|
||||||
|
fail: meter.createCounter(
|
||||||
|
"trpc.hotel.availability.hotels-booking-code-fail"
|
||||||
|
),
|
||||||
|
success: meter.createCounter(
|
||||||
|
"trpc.hotel.availability.hotels-booking-code-success"
|
||||||
|
),
|
||||||
|
},
|
||||||
hotelsByHotelIdAvailability: {
|
hotelsByHotelIdAvailability: {
|
||||||
counter: meter.createCounter("trpc.hotel.availability.hotels-by-hotel-id"),
|
counter: meter.createCounter("trpc.hotel.availability.hotels-by-hotel-id"),
|
||||||
fail: meter.createCounter(
|
fail: meter.createCounter(
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
|||||||
import { HotelTypeEnum } from "@/types/enums/hotelType"
|
import { HotelTypeEnum } from "@/types/enums/hotelType"
|
||||||
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
||||||
import type { HotelDataWithUrl } from "@/types/hotel"
|
import type { HotelDataWithUrl } from "@/types/hotel"
|
||||||
|
import type {
|
||||||
|
HotelsAvailabilityInputSchema,
|
||||||
|
HotelsByHotelIdsAvailabilityInputSchema,
|
||||||
|
} from "@/types/trpc/routers/hotel/availability"
|
||||||
import type { HotelInput } from "@/types/trpc/routers/hotel/hotel"
|
import type { HotelInput } from "@/types/trpc/routers/hotel/hotel"
|
||||||
import type { CityLocation } from "@/types/trpc/routers/hotel/locations"
|
import type { CityLocation } from "@/types/trpc/routers/hotel/locations"
|
||||||
|
|
||||||
@@ -203,13 +207,11 @@ export const getHotel = cache(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const hotelQueryRouter = router({
|
export const getHotelsAvailabilityByCity = async (
|
||||||
availability: router({
|
input: HotelsAvailabilityInputSchema,
|
||||||
hotelsByCity: serviceProcedure
|
apiLang: string,
|
||||||
.input(hotelsAvailabilityInputSchema)
|
serviceToken: string
|
||||||
.query(async ({ input, ctx }) => {
|
) => {
|
||||||
const { lang } = ctx
|
|
||||||
const apiLang = toApiLang(lang)
|
|
||||||
const {
|
const {
|
||||||
cityId,
|
cityId,
|
||||||
roomStayStartDate,
|
roomStayStartDate,
|
||||||
@@ -244,7 +246,7 @@ export const hotelQueryRouter = router({
|
|||||||
{
|
{
|
||||||
cache: undefined,
|
cache: undefined,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
Authorization: `Bearer ${serviceToken}`,
|
||||||
},
|
},
|
||||||
next: {
|
next: {
|
||||||
revalidate: env.CACHE_TIME_CITY_SEARCH,
|
revalidate: env.CACHE_TIME_CITY_SEARCH,
|
||||||
@@ -282,8 +284,7 @@ export const hotelQueryRouter = router({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const apiJson = await apiResponse.json()
|
const apiJson = await apiResponse.json()
|
||||||
const validateAvailabilityData =
|
const validateAvailabilityData = hotelsAvailabilitySchema.safeParse(apiJson)
|
||||||
hotelsAvailabilitySchema.safeParse(apiJson)
|
|
||||||
if (!validateAvailabilityData.success) {
|
if (!validateAvailabilityData.success) {
|
||||||
metrics.hotelsAvailability.fail.add(1, {
|
metrics.hotelsAvailability.fail.add(1, {
|
||||||
cityId,
|
cityId,
|
||||||
@@ -323,12 +324,13 @@ export const hotelQueryRouter = router({
|
|||||||
(hotels) => hotels.attributes
|
(hotels) => hotels.attributes
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
hotelsByHotelIds: serviceProcedure
|
|
||||||
.input(getHotelsByHotelIdsAvailabilityInputSchema)
|
export const getHotelsAvailabilityByHotelIds = async (
|
||||||
.query(async ({ input, ctx }) => {
|
input: HotelsByHotelIdsAvailabilityInputSchema,
|
||||||
const { lang } = ctx
|
apiLang: string,
|
||||||
const apiLang = toApiLang(lang)
|
serviceToken: string
|
||||||
|
) => {
|
||||||
const {
|
const {
|
||||||
hotelIds,
|
hotelIds,
|
||||||
roomStayStartDate,
|
roomStayStartDate,
|
||||||
@@ -338,15 +340,21 @@ export const hotelQueryRouter = router({
|
|||||||
bookingCode,
|
bookingCode,
|
||||||
} = input
|
} = input
|
||||||
|
|
||||||
const params: Record<string, string | number | number[]> = {
|
/**
|
||||||
hotelIds,
|
* Since API expects the params appended and not just
|
||||||
roomStayStartDate,
|
* a comma separated string we need to initialize the
|
||||||
roomStayEndDate,
|
* SearchParams with a sequence of pairs
|
||||||
adults,
|
* (hotelIds=810&hotelIds=879&hotelIds=222 etc.)
|
||||||
...(children && { children }),
|
**/
|
||||||
...(bookingCode && { bookingCode }),
|
const params = new URLSearchParams([
|
||||||
language: apiLang,
|
["roomStayStartDate", roomStayStartDate],
|
||||||
}
|
["roomStayEndDate", roomStayEndDate],
|
||||||
|
["adults", adults.toString()],
|
||||||
|
["children", children ?? ""],
|
||||||
|
["bookingCode", bookingCode],
|
||||||
|
["language", apiLang],
|
||||||
|
])
|
||||||
|
hotelIds.forEach((hotelId) => params.append("hotelIds", hotelId.toString()))
|
||||||
metrics.hotelsByHotelIdAvailability.counter.add(1, {
|
metrics.hotelsByHotelIdAvailability.counter.add(1, {
|
||||||
hotelIds,
|
hotelIds,
|
||||||
roomStayStartDate,
|
roomStayStartDate,
|
||||||
@@ -364,7 +372,7 @@ export const hotelQueryRouter = router({
|
|||||||
{
|
{
|
||||||
cache: undefined,
|
cache: undefined,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
Authorization: `Bearer ${serviceToken}`,
|
||||||
},
|
},
|
||||||
next: {
|
next: {
|
||||||
revalidate: env.CACHE_TIME_CITY_SEARCH,
|
revalidate: env.CACHE_TIME_CITY_SEARCH,
|
||||||
@@ -402,8 +410,7 @@ export const hotelQueryRouter = router({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const apiJson = await apiResponse.json()
|
const apiJson = await apiResponse.json()
|
||||||
const validateAvailabilityData =
|
const validateAvailabilityData = hotelsAvailabilitySchema.safeParse(apiJson)
|
||||||
hotelsAvailabilitySchema.safeParse(apiJson)
|
|
||||||
if (!validateAvailabilityData.success) {
|
if (!validateAvailabilityData.success) {
|
||||||
metrics.hotelsByHotelIdAvailability.fail.add(1, {
|
metrics.hotelsByHotelIdAvailability.fail.add(1, {
|
||||||
hotelIds,
|
hotelIds,
|
||||||
@@ -443,6 +450,23 @@ export const hotelQueryRouter = router({
|
|||||||
(hotels) => hotels.attributes
|
(hotels) => hotels.attributes
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hotelQueryRouter = router({
|
||||||
|
availability: router({
|
||||||
|
hotelsByCity: serviceProcedure
|
||||||
|
.input(hotelsAvailabilityInputSchema)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const { lang } = ctx
|
||||||
|
const apiLang = toApiLang(lang)
|
||||||
|
return getHotelsAvailabilityByCity(input, apiLang, ctx.serviceToken)
|
||||||
|
}),
|
||||||
|
hotelsByHotelIds: serviceProcedure
|
||||||
|
.input(getHotelsByHotelIdsAvailabilityInputSchema)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const { lang } = ctx
|
||||||
|
const apiLang = toApiLang(lang)
|
||||||
|
return getHotelsAvailabilityByHotelIds(input, apiLang, ctx.serviceToken)
|
||||||
}),
|
}),
|
||||||
rooms: serviceProcedure
|
rooms: serviceProcedure
|
||||||
.input(roomsAvailabilityInputSchema)
|
.input(roomsAvailabilityInputSchema)
|
||||||
@@ -793,6 +817,70 @@ export const hotelQueryRouter = router({
|
|||||||
bedTypes,
|
bedTypes,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
hotelsByCityWithBookingCode: serviceProcedure
|
||||||
|
.input(hotelsAvailabilityInputSchema)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const { lang } = ctx
|
||||||
|
const apiLang = toApiLang(lang)
|
||||||
|
|
||||||
|
metrics.hotelsAvailabilityBookingCode.counter.add(1, {
|
||||||
|
...input,
|
||||||
|
})
|
||||||
|
const bookingCodeAvailabilityResponse =
|
||||||
|
await getHotelsAvailabilityByCity(input, apiLang, ctx.serviceToken)
|
||||||
|
if (!bookingCodeAvailabilityResponse) {
|
||||||
|
metrics.hotelsAvailabilityBookingCode.fail.add(1, {
|
||||||
|
...input,
|
||||||
|
error_type: "unknown",
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get regular availability of hotels which don't have availability with booking code.
|
||||||
|
const unavailableHotelIds =
|
||||||
|
bookingCodeAvailabilityResponse?.availability
|
||||||
|
.filter((hotel) => {
|
||||||
|
return hotel.status === "NotAvailable"
|
||||||
|
})
|
||||||
|
.flatMap((hotel) => {
|
||||||
|
return hotel.hotelId
|
||||||
|
})
|
||||||
|
|
||||||
|
// All hotels have availability with booking code no need to fetch regular prices.
|
||||||
|
// return response as is without any filtering as below.
|
||||||
|
if (!unavailableHotelIds || !unavailableHotelIds.length) {
|
||||||
|
return bookingCodeAvailabilityResponse
|
||||||
|
}
|
||||||
|
const unavailableHotelsInput = {
|
||||||
|
...input,
|
||||||
|
bookingCode: "",
|
||||||
|
hotelIds: unavailableHotelIds,
|
||||||
|
}
|
||||||
|
const unavailableHotels = await getHotelsAvailabilityByHotelIds(
|
||||||
|
unavailableHotelsInput,
|
||||||
|
apiLang,
|
||||||
|
ctx.serviceToken
|
||||||
|
)
|
||||||
|
|
||||||
|
metrics.hotelsAvailabilityBookingCode.success.add(1, {
|
||||||
|
...input,
|
||||||
|
})
|
||||||
|
console.info("api.hotels.hotelsAvailabilityBookingCode success")
|
||||||
|
|
||||||
|
// No regular rates available due to network or API failure (no need to filter & merge).
|
||||||
|
if (!unavailableHotels) {
|
||||||
|
return bookingCodeAvailabilityResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtering the response hotels to merge bookingCode rates and regular rates in single response.
|
||||||
|
return {
|
||||||
|
availability: bookingCodeAvailabilityResponse.availability
|
||||||
|
.filter((hotel) => {
|
||||||
|
return hotel.status === "Available"
|
||||||
|
})
|
||||||
|
.concat(unavailableHotels.availability),
|
||||||
|
}
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
rates: router({
|
rates: router({
|
||||||
get: publicProcedure
|
get: publicProcedure
|
||||||
|
|||||||
13
stores/bookingCode-filter.ts
Normal file
13
stores/bookingCode-filter.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { create } from "zustand"
|
||||||
|
|
||||||
|
interface BookingCodeFilterState {
|
||||||
|
activeCodeFilter: string
|
||||||
|
setFilter: (filter: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useBookingCodeFilterStore = create<BookingCodeFilterState>(
|
||||||
|
(set) => ({
|
||||||
|
activeCodeFilter: "discounted",
|
||||||
|
setFilter: (filter) => set({ activeCodeFilter: filter }),
|
||||||
|
})
|
||||||
|
)
|
||||||
@@ -8,4 +8,5 @@ export type HotelCardProps = {
|
|||||||
isUserLoggedIn: boolean
|
isUserLoggedIn: boolean
|
||||||
type?: HotelCardListingTypeEnum
|
type?: HotelCardListingTypeEnum
|
||||||
state?: "default" | "active"
|
state?: "default" | "active"
|
||||||
|
bookingCode?: string | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface SelectHotelMapProps {
|
|||||||
hotels: HotelData[]
|
hotels: HotelData[]
|
||||||
filterList: CategorizedFilters
|
filterList: CategorizedFilters
|
||||||
cityCoordinates: Coordinates
|
cityCoordinates: Coordinates
|
||||||
|
bookingCode: string | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageSizes = z.infer<typeof imageSchema>["imageSizes"]
|
type ImageSizes = z.infer<typeof imageSchema>["imageSizes"]
|
||||||
@@ -32,6 +33,7 @@ export type HotelPin = {
|
|||||||
coordinates: Coordinates
|
coordinates: Coordinates
|
||||||
publicPrice: number | null
|
publicPrice: number | null
|
||||||
memberPrice: number | null
|
memberPrice: number | null
|
||||||
|
rateType: string | null
|
||||||
currency: string
|
currency: string
|
||||||
images: {
|
images: {
|
||||||
imageSizes: ImageSizes
|
imageSizes: ImageSizes
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export interface SelectHotelSearchParams {
|
|||||||
fromDate: string
|
fromDate: string
|
||||||
toDate: string
|
toDate: string
|
||||||
rooms: Pick<Room, "adults" | "childrenInRoom">[]
|
rooms: Pick<Room, "adults" | "childrenInRoom">[]
|
||||||
|
bookingCode: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AlternativeHotelsSearchParams {
|
export interface AlternativeHotelsSearchParams {
|
||||||
@@ -12,4 +13,5 @@ export interface AlternativeHotelsSearchParams {
|
|||||||
fromDate: string
|
fromDate: string
|
||||||
toDate: string
|
toDate: string
|
||||||
rooms: Pick<Room, "adults" | "childrenInRoom">[]
|
rooms: Pick<Room, "adults" | "childrenInRoom">[]
|
||||||
|
bookingCode: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export interface SelectRateSearchParams {
|
|||||||
fromDate: string
|
fromDate: string
|
||||||
toDate: string
|
toDate: string
|
||||||
rooms: Room[]
|
rooms: Room[]
|
||||||
|
bookingCode?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Rate {
|
export interface Rate {
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
import type { z } from "zod"
|
import type { z } from "zod"
|
||||||
|
|
||||||
|
import type {
|
||||||
|
getHotelsByHotelIdsAvailabilityInputSchema,
|
||||||
|
hotelsAvailabilityInputSchema,
|
||||||
|
} from "@/server/routers/hotels/input"
|
||||||
import type { hotelsAvailabilitySchema } from "@/server/routers/hotels/output"
|
import type { hotelsAvailabilitySchema } from "@/server/routers/hotels/output"
|
||||||
import type { productTypeSchema } from "@/server/routers/hotels/schemas/availability/productType"
|
import type { productTypeSchema } from "@/server/routers/hotels/schemas/availability/productType"
|
||||||
import type { productTypePriceSchema } from "@/server/routers/hotels/schemas/productTypePrice"
|
import type { productTypePriceSchema } from "@/server/routers/hotels/schemas/productTypePrice"
|
||||||
|
|
||||||
export type HotelsAvailability = z.output<typeof hotelsAvailabilitySchema>
|
export type HotelsAvailability = z.output<typeof hotelsAvailabilitySchema>
|
||||||
|
export type HotelsAvailabilityInputSchema = z.output<
|
||||||
|
typeof hotelsAvailabilityInputSchema
|
||||||
|
>
|
||||||
|
export type HotelsByHotelIdsAvailabilityInputSchema = z.output<
|
||||||
|
typeof getHotelsByHotelIdsAvailabilityInputSchema
|
||||||
|
>
|
||||||
export type ProductType = z.output<typeof productTypeSchema>
|
export type ProductType = z.output<typeof productTypeSchema>
|
||||||
export type ProductTypePrices = z.output<typeof productTypePriceSchema>
|
export type ProductTypePrices = z.output<typeof productTypePriceSchema>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user