Merged in fix/destinations-speed-test (pull request #1704)
Feat(destination pages): Performance improvements * fix/destinations: try cache full response * Added more caching * Removed unsed env car * wip * merge master * wip * wip * wip * Renaming Approved-by: Michael Zetterberg
This commit is contained in:
@@ -25,8 +25,9 @@ export async function GET(request: NextRequest) {
|
||||
throw new Error("[WARMUP] Invalid language provided")
|
||||
}
|
||||
|
||||
const hotels = await serverClient().hotel.hotels.getAllHotels.get({
|
||||
const hotels = await serverClient().hotel.hotels.getDestinationsMapData({
|
||||
lang: parsedLang.data,
|
||||
warmup: true,
|
||||
})
|
||||
return NextResponse.json(hotels)
|
||||
} catch (error) {
|
||||
|
||||
@@ -19,13 +19,15 @@ import { getVisibleHotels } from "./utils"
|
||||
import styles from "./hotelList.module.css"
|
||||
|
||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||
import type { HotelDataWithUrl } from "@/types/hotel"
|
||||
import type { DestinationPagesHotelData } from "@/types/hotel"
|
||||
|
||||
export default function HotelList() {
|
||||
const intl = useIntl()
|
||||
const map = useMap()
|
||||
const coreLib = useMapsLibrary("core")
|
||||
const [visibleHotels, setVisibleHotels] = useState<HotelDataWithUrl[]>([])
|
||||
const [visibleHotels, setVisibleHotels] = useState<
|
||||
DestinationPagesHotelData[]
|
||||
>([])
|
||||
const { activeHotels, isLoading } = useDestinationDataStore((state) => ({
|
||||
activeHotels: state.activeHotels,
|
||||
isLoading: state.isLoading,
|
||||
@@ -80,7 +82,7 @@ export default function HotelList() {
|
||||
<HotelCardCarousel visibleHotels={visibleHotels} />
|
||||
<ul className={styles.hotelList}>
|
||||
{visibleHotels.map(({ hotel, url }) => (
|
||||
<li key={hotel.operaId}>
|
||||
<li key={hotel.id}>
|
||||
<HotelListItem hotel={hotel} url={url} />
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { HotelDataWithUrl } from "@/types/hotel"
|
||||
import type { DestinationPagesHotelData } from "@/types/hotel"
|
||||
|
||||
export function getVisibleHotels(
|
||||
hotels: HotelDataWithUrl[],
|
||||
hotels: DestinationPagesHotelData[],
|
||||
map: google.maps.Map | null
|
||||
) {
|
||||
const bounds = map?.getBounds()
|
||||
|
||||
@@ -19,15 +19,11 @@ import { getSingleDecimal } from "@/utils/numberFormatting"
|
||||
|
||||
import styles from "./hotelListItem.module.css"
|
||||
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
import type { DestinationPagesHotelData } from "@/types/hotel"
|
||||
|
||||
interface HotelListItemProps {
|
||||
hotel: Hotel
|
||||
url: string | null
|
||||
}
|
||||
|
||||
export default function HotelListItem({ hotel, url }: HotelListItemProps) {
|
||||
export default function HotelListItem(data: DestinationPagesHotelData) {
|
||||
const intl = useIntl()
|
||||
const { hotel, url } = data
|
||||
const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || [])
|
||||
const amenities = hotel.detailedFacilities.slice(0, 5)
|
||||
|
||||
@@ -35,7 +31,7 @@ export default function HotelListItem({ hotel, url }: HotelListItemProps) {
|
||||
const { setHoveredMarker, activeMarker } = useDestinationPageHotelsMapStore()
|
||||
|
||||
useEffect(() => {
|
||||
if (activeMarker === hotel.operaId) {
|
||||
if (activeMarker === hotel.id) {
|
||||
const element = itemRef.current
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
@@ -45,13 +41,13 @@ export default function HotelListItem({ hotel, url }: HotelListItemProps) {
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [activeMarker, hotel.operaId])
|
||||
}, [activeMarker, hotel.id])
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
if (hotel.operaId) {
|
||||
setHoveredMarker(hotel.operaId)
|
||||
if (hotel.id) {
|
||||
setHoveredMarker(hotel.id)
|
||||
}
|
||||
}, [setHoveredMarker, hotel.operaId])
|
||||
}, [setHoveredMarker, hotel.id])
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
setHoveredMarker(null)
|
||||
@@ -62,7 +58,7 @@ export default function HotelListItem({ hotel, url }: HotelListItemProps) {
|
||||
ref={itemRef}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
className={`${styles.hotelListItem} ${activeMarker === hotel.operaId ? styles.activeCard : ""}`}
|
||||
className={`${styles.hotelListItem} ${activeMarker === hotel.id ? styles.activeCard : ""}`}
|
||||
>
|
||||
<div className={styles.imageWrapper}>
|
||||
<ImageGallery
|
||||
@@ -74,19 +70,17 @@ export default function HotelListItem({ hotel, url }: HotelListItemProps) {
|
||||
{ title: hotel.name }
|
||||
)}
|
||||
/>
|
||||
{hotel.ratings?.tripAdvisor.rating && (
|
||||
{hotel.tripadvisor && (
|
||||
<div className={styles.tripAdvisor}>
|
||||
<TripadvisorIcon color="Icon/Interactive/Default" />
|
||||
<Caption color="burgundy">
|
||||
{hotel.ratings.tripAdvisor.rating}
|
||||
</Caption>
|
||||
<Caption color="burgundy">{hotel.tripadvisor}</Caption>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.intro}>
|
||||
<div className={styles.logo}>
|
||||
<HotelLogo hotelId={hotel.operaId} hotelType={hotel.hotelType} />
|
||||
<HotelLogo hotelId={hotel.id} hotelType={hotel.hotelType} />
|
||||
</div>
|
||||
<Subtitle type="one" asChild>
|
||||
<h3>{hotel.name}</h3>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { env } from "@/env/server"
|
||||
import { getAllHotels } from "@/lib/trpc/memoizedRequests"
|
||||
import { getDestinationsMapData } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import DynamicMap from "../../Map/DynamicMap"
|
||||
import MapContent from "../../Map/MapContent"
|
||||
@@ -8,7 +8,7 @@ import { getHotelMapMarkers, mapMarkerDataToGeoJson } from "../../Map/utils"
|
||||
import ActiveMapCard from "./ActiveMapCard"
|
||||
|
||||
export default async function OverviewMapContainer() {
|
||||
const hotelData = await getAllHotels()
|
||||
const hotelData = await getDestinationsMapData()
|
||||
|
||||
if (!hotelData) {
|
||||
return null
|
||||
|
||||
@@ -11,10 +11,10 @@ import HotelMapCard from "../HotelMapCard"
|
||||
|
||||
import styles from "./hotelCardCarousel.module.css"
|
||||
|
||||
import type { Hotel, HotelDataWithUrl } from "@/types/hotel"
|
||||
import type { DestinationPagesHotelData } from "@/types/hotel"
|
||||
|
||||
interface MapCardCarouselProps {
|
||||
visibleHotels: HotelDataWithUrl[] | []
|
||||
visibleHotels: DestinationPagesHotelData[]
|
||||
}
|
||||
export default function HotelCardCarousel({
|
||||
visibleHotels,
|
||||
@@ -22,13 +22,13 @@ export default function HotelCardCarousel({
|
||||
const { activeMarker, setActiveMarker } = useDestinationPageHotelsMapStore()
|
||||
|
||||
const selectedHotelIdx = visibleHotels.findIndex(
|
||||
({ hotel }) => hotel.operaId === activeMarker
|
||||
({ hotel }) => hotel.id === activeMarker
|
||||
)
|
||||
|
||||
const handleScrollSelect = useCallback(
|
||||
(idx: number) => {
|
||||
if (selectedHotelIdx !== -1) {
|
||||
setActiveMarker(visibleHotels[idx]?.hotel.operaId)
|
||||
setActiveMarker(visibleHotels[idx]?.hotel.id)
|
||||
}
|
||||
},
|
||||
[setActiveMarker, visibleHotels, selectedHotelIdx]
|
||||
@@ -44,15 +44,15 @@ export default function HotelCardCarousel({
|
||||
>
|
||||
<Carousel.Content className={styles.carouselContent}>
|
||||
{visibleHotels.map(({ hotel, url }) => (
|
||||
<Carousel.Item key={hotel.operaId} className={styles.item}>
|
||||
<Carousel.Item key={hotel.id} className={styles.item}>
|
||||
<HotelMapCard
|
||||
className={cx(styles.carouselCard, {
|
||||
[styles.noActiveHotel]: !activeMarker,
|
||||
})}
|
||||
tripadvisorRating={hotel.ratings?.tripAdvisor.rating}
|
||||
tripadvisorRating={hotel.tripadvisor}
|
||||
hotelName={hotel.name}
|
||||
url={url}
|
||||
image={getImage(hotel)}
|
||||
image={getImage({ hotel, url })}
|
||||
amenities={hotel.detailedFacilities.slice(0, 3)}
|
||||
/>
|
||||
</Carousel.Item>
|
||||
@@ -62,11 +62,11 @@ export default function HotelCardCarousel({
|
||||
)
|
||||
}
|
||||
|
||||
function getImage(hotel: Hotel) {
|
||||
function getImage(hotel: DestinationPagesHotelData) {
|
||||
return {
|
||||
src: hotel.galleryImages[0].imageSizes.medium,
|
||||
src: hotel.hotel.galleryImages?.[0]?.imageSizes.medium,
|
||||
alt:
|
||||
hotel.galleryImages[0].metaData.altText ||
|
||||
hotel.galleryImages[0].metaData.altText_En,
|
||||
hotel.hotel.galleryImages?.[0]?.metaData.altText ||
|
||||
hotel.hotel.galleryImages?.[0]?.metaData.altText_En,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,19 +25,12 @@ import { getSingleDecimal } from "@/utils/numberFormatting"
|
||||
|
||||
import styles from "./hotelListingItem.module.css"
|
||||
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
import type { DestinationPagesHotelData } from "@/types/hotel"
|
||||
|
||||
interface HotelListingItemProps {
|
||||
hotel: Hotel
|
||||
url: string | null
|
||||
}
|
||||
|
||||
export default function HotelListingItem({
|
||||
hotel,
|
||||
url,
|
||||
}: HotelListingItemProps) {
|
||||
export default function HotelListingItem(data: DestinationPagesHotelData) {
|
||||
const intl = useIntl()
|
||||
const params = useParams()
|
||||
const { hotel, url } = data
|
||||
const { setActiveMarker } = useDestinationPageHotelsMapStore()
|
||||
const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || [])
|
||||
const amenities = hotel.detailedFacilities.slice(0, 5)
|
||||
@@ -61,18 +54,16 @@ export default function HotelListingItem({
|
||||
{ title: hotel.name }
|
||||
)}
|
||||
/>
|
||||
{hotel.ratings?.tripAdvisor.rating && (
|
||||
{hotel.tripadvisor && (
|
||||
<div className={styles.tripAdvisor}>
|
||||
<TripadvisorIcon color="Icon/Interactive/Default" />
|
||||
<Caption color="burgundy">
|
||||
{hotel.ratings.tripAdvisor.rating}
|
||||
</Caption>
|
||||
<Caption color="burgundy">{hotel.tripadvisor}</Caption>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.intro}>
|
||||
<HotelLogo hotelId={hotel.operaId} hotelType={hotel.hotelType} />
|
||||
<HotelLogo hotelId={hotel.id} hotelType={hotel.hotelType} />
|
||||
<Subtitle type="one" asChild>
|
||||
<h3>{hotel.name}</h3>
|
||||
</Subtitle>
|
||||
@@ -93,7 +84,7 @@ export default function HotelListingItem({
|
||||
</Caption>
|
||||
</div>
|
||||
</div>
|
||||
<Body>{hotel.hotelContent.texts.descriptions?.short}</Body>
|
||||
<Body>{hotel.hotelContent?.texts.descriptions?.short}</Body>
|
||||
<ul className={styles.amenityList}>
|
||||
{amenities.map((amenity) => {
|
||||
const Icon = (
|
||||
@@ -112,7 +103,7 @@ export default function HotelListingItem({
|
||||
<Link
|
||||
href={mapUrl}
|
||||
scroll={true}
|
||||
onClick={() => setActiveMarker(hotel.operaId)}
|
||||
onClick={() => setActiveMarker(hotel.id)}
|
||||
>
|
||||
{intl.formatMessage({ id: "See on map" })}
|
||||
<MaterialIcon
|
||||
|
||||
@@ -87,7 +87,7 @@ export default function HotelListing() {
|
||||
<>
|
||||
<ul className={styles.hotelList}>
|
||||
{activeHotels.map(({ hotel, url }) => (
|
||||
<li key={hotel.name}>
|
||||
<li key={hotel.id}>
|
||||
<HotelListingItem hotel={hotel} url={url} />
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -29,10 +29,10 @@ import { getHotelMapMarkers, mapMarkerDataToGeoJson } from "./utils"
|
||||
import styles from "./map.module.css"
|
||||
|
||||
import type { MapLocation } from "@/types/components/mapLocation"
|
||||
import type { HotelDataWithUrl } from "@/types/hotel"
|
||||
import type { DestinationPagesHotelData } from "@/types/hotel"
|
||||
|
||||
interface MapProps {
|
||||
hotels: HotelDataWithUrl[]
|
||||
hotels: DestinationPagesHotelData[]
|
||||
mapId: string
|
||||
apiKey: string
|
||||
pageType: "city" | "country"
|
||||
@@ -55,9 +55,7 @@ export default function Map({
|
||||
() => searchParams.get("view") === "map",
|
||||
[searchParams]
|
||||
)
|
||||
const activeHotel = hotels.find(
|
||||
({ hotel }) => hotel.operaId === activeHotelId
|
||||
)
|
||||
const activeHotel = hotels.find(({ hotel }) => hotel.id === activeHotelId)
|
||||
const rootDiv = useRef<HTMLDivElement | null>(null)
|
||||
const [mapHeight, setMapHeight] = useState("0px")
|
||||
const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0)
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
MarkerFeature,
|
||||
MarkerGeojson,
|
||||
} from "@/types/components/maps/destinationMarkers"
|
||||
import type { HotelDataWithUrl } from "@/types/hotel"
|
||||
import type { DestinationPagesHotelData } from "@/types/hotel"
|
||||
|
||||
export function mapMarkerDataToGeoJson(markers: DestinationMarker[]) {
|
||||
const features = markers.map<MarkerFeature>(
|
||||
@@ -28,7 +28,7 @@ export function mapMarkerDataToGeoJson(markers: DestinationMarker[]) {
|
||||
return geoJson
|
||||
}
|
||||
|
||||
export function getHotelMapMarkers(hotels: HotelDataWithUrl[]) {
|
||||
export function getHotelMapMarkers(hotels: DestinationPagesHotelData[]) {
|
||||
const markers = hotels
|
||||
.map(({ hotel, url }) => ({
|
||||
id: hotel.id,
|
||||
@@ -41,20 +41,21 @@ export function getHotelMapMarkers(hotels: HotelDataWithUrl[]) {
|
||||
}
|
||||
: null,
|
||||
url: url,
|
||||
tripadvisor: hotel.ratings?.tripAdvisor.rating,
|
||||
amenities: hotel.detailedFacilities.slice(0, 3),
|
||||
image:
|
||||
hotel.galleryImages && hotel.galleryImages[0]
|
||||
? {
|
||||
src: hotel.galleryImages[0].imageSizes.medium,
|
||||
alt:
|
||||
hotel.galleryImages[0].metaData.altText ||
|
||||
hotel.galleryImages[0].metaData.altText_En,
|
||||
}
|
||||
: null,
|
||||
tripadvisor: hotel.tripadvisor,
|
||||
amenities: hotel.detailedFacilities,
|
||||
image: getImage({ hotel, url }),
|
||||
}))
|
||||
|
||||
.filter((item): item is DestinationMarker => !!item.coordinates)
|
||||
|
||||
return markers
|
||||
}
|
||||
|
||||
function getImage(hotel: DestinationPagesHotelData) {
|
||||
return {
|
||||
src: hotel.hotel.galleryImages?.[0]?.imageSizes.medium,
|
||||
alt:
|
||||
hotel.hotel.galleryImages?.[0]?.metaData.altText ||
|
||||
hotel.hotel.galleryImages?.[0]?.metaData.altText_En,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
|
||||
import { trackAddAncillary } from "@/utils/tracking/myStay"
|
||||
|
||||
import { type AncillaryQuantityFormData,quantitySchema } from "../../schema"
|
||||
import { type AncillaryQuantityFormData, quantitySchema } from "../../schema"
|
||||
|
||||
import styles from "./actionButtons.module.css"
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function PriceDetails({
|
||||
|
||||
const totalPrice = isBreakfast
|
||||
? breakfastData!.priceAdult * breakfastData!.nrOfAdults +
|
||||
breakfastData!.priceChild * breakfastData!.nrOfPayingChildren
|
||||
breakfastData!.priceChild * breakfastData!.nrOfPayingChildren
|
||||
: quantityWithCard && selectedAncillary
|
||||
? selectedAncillary.price.total * quantityWithCard
|
||||
: null
|
||||
@@ -101,15 +101,15 @@ export default function PriceDetails({
|
||||
const items = isBreakfast
|
||||
? getBreakfastItems(selectedAncillary, breakfastData)
|
||||
: [
|
||||
{
|
||||
title: selectedAncillary.title,
|
||||
totalPrice: selectedAncillary.price.total,
|
||||
currency: selectedAncillary.price.currency,
|
||||
points: selectedAncillary.points,
|
||||
quantityWithCard,
|
||||
quantityWithPoints,
|
||||
},
|
||||
]
|
||||
{
|
||||
title: selectedAncillary.title,
|
||||
totalPrice: selectedAncillary.price.total,
|
||||
currency: selectedAncillary.price.currency,
|
||||
points: selectedAncillary.points,
|
||||
quantityWithCard,
|
||||
quantityWithPoints,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
margin: 0 auto;
|
||||
padding: var(--Spacing-x-one-and-half);
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,4 +24,4 @@
|
||||
|
||||
.name {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { Product, RateDefinition } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||
import type {
|
||||
Product,
|
||||
RateDefinition,
|
||||
} from "@/types/trpc/routers/hotel/roomAvailability"
|
||||
|
||||
/**
|
||||
* Get terms and rate title from the rate definitions when booking code rate
|
||||
@@ -12,7 +15,7 @@ export function getRateDefinition(
|
||||
product: Product,
|
||||
rateDefinitions: RateDefinition[],
|
||||
isUserLoggedIn: boolean,
|
||||
isMainRoom: boolean,
|
||||
isMainRoom: boolean
|
||||
) {
|
||||
return rateDefinitions.find((rateDefinition) => {
|
||||
if ("member" in product && product.member && isUserLoggedIn && isMainRoom) {
|
||||
@@ -28,4 +31,4 @@ export function getRateDefinition(
|
||||
return rateDefinition.rateCode === product.public.rateCode
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
export function isSelectedPriceProduct(
|
||||
product: PriceProduct,
|
||||
selectedRate: SelectedRate | null,
|
||||
roomTypeCode: string,
|
||||
roomTypeCode: string
|
||||
) {
|
||||
if (!selectedRate) {
|
||||
return false
|
||||
@@ -25,15 +25,15 @@ export function isSelectedPriceProduct(
|
||||
selectedRatePublic = selectedRate.product.public
|
||||
}
|
||||
|
||||
const selectedRateIsMember = (
|
||||
member && selectedRateMember &&
|
||||
(member.rateCode === selectedRateMember.rateCode)
|
||||
)
|
||||
const selectedRateIsMember =
|
||||
member &&
|
||||
selectedRateMember &&
|
||||
member.rateCode === selectedRateMember.rateCode
|
||||
|
||||
const selectedRateIsPublic = (
|
||||
standard && selectedRatePublic &&
|
||||
(standard.rateCode === selectedRatePublic.rateCode)
|
||||
)
|
||||
const selectedRateIsPublic =
|
||||
standard &&
|
||||
selectedRatePublic &&
|
||||
standard.rateCode === selectedRatePublic.rateCode
|
||||
return !!(
|
||||
(selectedRateIsMember || selectedRateIsPublic) &&
|
||||
selectedRate.roomTypeCode === roomTypeCode
|
||||
@@ -43,15 +43,15 @@ export function isSelectedPriceProduct(
|
||||
export function isSelectedCorporateCheque(
|
||||
product: CorporateChequeProduct,
|
||||
selectedRate: SelectedRate | null,
|
||||
roomTypeCode: string,
|
||||
roomTypeCode: string
|
||||
) {
|
||||
if (!selectedRate || !("corporateCheque" in selectedRate.product)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const isSameRateCode = (
|
||||
product.corporateCheque.rateCode === selectedRate.product.corporateCheque.rateCode
|
||||
)
|
||||
const isSameRateCode =
|
||||
product.corporateCheque.rateCode ===
|
||||
selectedRate.product.corporateCheque.rateCode
|
||||
const isSameRoomTypeCode = selectedRate.roomTypeCode === roomTypeCode
|
||||
return isSameRateCode && isSameRoomTypeCode
|
||||
}
|
||||
@@ -59,15 +59,14 @@ export function isSelectedCorporateCheque(
|
||||
export function isSelectedVoucher(
|
||||
product: VoucherProduct,
|
||||
selectedRate: SelectedRate | null,
|
||||
roomTypeCode: string,
|
||||
roomTypeCode: string
|
||||
) {
|
||||
if (!selectedRate || !("voucher" in selectedRate.product)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const isSameRateCode = (
|
||||
const isSameRateCode =
|
||||
product.voucher.rateCode === selectedRate.product.voucher.rateCode
|
||||
)
|
||||
const isSameRoomTypeCode = selectedRate.roomTypeCode === roomTypeCode
|
||||
return isSameRateCode && isSameRoomTypeCode
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
import type {
|
||||
RoomPackage,
|
||||
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
import type { RoomPackage } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
|
||||
export function calculatePricePerNightPriceProduct(
|
||||
pricePerNight: number,
|
||||
requestedPricePerNight: number | undefined,
|
||||
nights: number,
|
||||
petRoomPackage?: RoomPackage,
|
||||
petRoomPackage?: RoomPackage
|
||||
) {
|
||||
const totalPrice = petRoomPackage?.localPrice
|
||||
? Math.floor(
|
||||
pricePerNight + (petRoomPackage.localPrice.price / nights)
|
||||
)
|
||||
? Math.floor(pricePerNight + petRoomPackage.localPrice.price / nights)
|
||||
: Math.floor(pricePerNight)
|
||||
|
||||
let totalRequestedPrice = undefined
|
||||
if (requestedPricePerNight) {
|
||||
if (petRoomPackage?.requestedPrice) {
|
||||
totalRequestedPrice = Math.floor(
|
||||
requestedPricePerNight +
|
||||
(petRoomPackage.requestedPrice.price / nights)
|
||||
requestedPricePerNight + petRoomPackage.requestedPrice.price / nights
|
||||
)
|
||||
} else {
|
||||
totalRequestedPrice = Math.floor(requestedPricePerNight)
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
gap: var(--Spacing-x1);
|
||||
margin: 0;
|
||||
padding: var(--Spacing-x2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ import { useRatesStore } from "@/stores/select-rate"
|
||||
import styles from "./rooms.module.css"
|
||||
|
||||
export default function ScrollToList() {
|
||||
const { isSingleRoomAndHasSelection } = useRatesStore(state => ({
|
||||
isSingleRoomAndHasSelection: state.booking.rooms.length === 1 && !!state.rateSummary.length,
|
||||
const { isSingleRoomAndHasSelection } = useRatesStore((state) => ({
|
||||
isSingleRoomAndHasSelection:
|
||||
state.booking.rooms.length === 1 && !!state.rateSummary.length,
|
||||
}))
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
}
|
||||
|
||||
.roomList>li {
|
||||
.roomList > li {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,4 +62,4 @@
|
||||
|
||||
.Border-Divider-Default {
|
||||
background-color: var(--Border-Divider-Default);
|
||||
}
|
||||
}
|
||||
|
||||
8
apps/scandic-web/env/server.ts
vendored
8
apps/scandic-web/env/server.ts
vendored
@@ -168,13 +168,6 @@ export const env = createEnv({
|
||||
// transform to boolean
|
||||
.transform((s) => s === "true")
|
||||
.default("false"),
|
||||
|
||||
CACHE_TIME_HOTELDATA: z
|
||||
.number()
|
||||
.default(30 * 60)
|
||||
.transform((val) =>
|
||||
process.env.CMS_ENVIRONMENT === "test" ? 5 * 60 : val
|
||||
),
|
||||
CACHE_TIME_HOTELS: z
|
||||
.number()
|
||||
.default(TWENTYFOUR_HOURS)
|
||||
@@ -284,7 +277,6 @@ export const env = createEnv({
|
||||
SAS_ENABLED: process.env.SAS,
|
||||
SAS_POINT_TRANSFER_ENABLED: process.env.SAS_POINT_TRANSFER_ENABLED,
|
||||
|
||||
CACHE_TIME_HOTELDATA: process.env.CACHE_TIME_HOTELDATA,
|
||||
CACHE_TIME_HOTELS: process.env.CACHE_TIME_HOTELS,
|
||||
CACHE_TIME_CITY_SEARCH: process.env.CACHE_TIME_CITY_SEARCH,
|
||||
|
||||
|
||||
@@ -21,4 +21,4 @@ export default function useRateTitles() {
|
||||
},
|
||||
noPriceAvailable: intl.formatMessage({ id: "No prices available" }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,9 +131,9 @@
|
||||
"Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}": "Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}",
|
||||
"Breakfast Restaurant": "Breakfast Restaurant",
|
||||
"Breakfast buffet": "Breakfast buffet",
|
||||
"Breakfast can only be added for the entire duration of the stay and for all guests.": "Breakfast can only be added for the entire duration of the stay and for all guests.",
|
||||
"Breakfast can be added after booking for an additional fee.": "Breakfast can be added after booking for an additional fee.",
|
||||
"Breakfast can be added after booking for an extra cost for adults and kids ages 4 and up.": "Breakfast can be added after booking for an extra cost for adults and kids ages 4 and up.",
|
||||
"Breakfast can only be added for the entire duration of the stay and for all guests.": "Breakfast can only be added for the entire duration of the stay and for all guests.",
|
||||
"Breakfast can only be added for the entire duration of the stay
and for all guests.": "Breakfast can only be added for the entire duration of the stay
and for all guests.",
|
||||
"Breakfast charge": "Breakfast charge",
|
||||
"Breakfast deal can be purchased at the hotel.": "Breakfast deal can be purchased at the hotel.",
|
||||
|
||||
@@ -209,9 +209,11 @@ export const getHotelsByCityIdentifier = cache(
|
||||
})
|
||||
}
|
||||
)
|
||||
export const getAllHotels = cache(async function getMemoizedAllHotels() {
|
||||
return serverClient().hotel.hotels.getAllHotels.get()
|
||||
})
|
||||
export const getDestinationsMapData = cache(
|
||||
async function getMemoizedDestinationsMapData() {
|
||||
return serverClient().hotel.hotels.getDestinationsMapData()
|
||||
}
|
||||
)
|
||||
export const getDestinationCityPage = cache(
|
||||
async function getMemoizedDestinationCityPage() {
|
||||
return serverClient().contentstack.destinationCityPage.get()
|
||||
|
||||
@@ -96,9 +96,10 @@ export const nearbyHotelIdsInput = z.object({
|
||||
hotelId: z.string(),
|
||||
})
|
||||
|
||||
export const getAllHotelsInput = z
|
||||
export const getDestinationsMapDataInput = z
|
||||
.object({
|
||||
lang: z.nativeEnum(Lang),
|
||||
warmup: z.boolean().optional(),
|
||||
})
|
||||
.optional()
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ import {
|
||||
includedSchema,
|
||||
relationshipsSchema as hotelRelationshipsSchema,
|
||||
} from "./schemas/hotel"
|
||||
import { addressSchema } from "./schemas/hotel/address"
|
||||
import { detailedFacilitiesSchema } from "./schemas/hotel/detailedFacility"
|
||||
import { locationSchema } from "./schemas/hotel/location"
|
||||
import { imageSchema } from "./schemas/image"
|
||||
import { locationCitySchema } from "./schemas/location/city"
|
||||
import { locationHotelSchema } from "./schemas/location/hotel"
|
||||
import {
|
||||
@@ -657,3 +661,40 @@ export const roomFeaturesSchema = z
|
||||
.transform((data) => {
|
||||
return data.data.attributes.roomFeatures
|
||||
})
|
||||
|
||||
export const destinationPagesHotelDataSchema = z
|
||||
.object({
|
||||
data: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
location: locationSchema,
|
||||
cityIdentifier: z.string().optional(),
|
||||
tripadvisor: z.number().optional(),
|
||||
detailedFacilities: detailedFacilitiesSchema,
|
||||
galleryImages: z
|
||||
.array(imageSchema)
|
||||
.nullish()
|
||||
.transform((arr) => (arr ? arr.filter(Boolean) : [])),
|
||||
address: addressSchema,
|
||||
hotelType: z.string(),
|
||||
type: z.literal("hotels"), // No enum here but the standard return appears to be "hotels".
|
||||
url: z.string().optional(),
|
||||
hotelContent: z
|
||||
.object({
|
||||
texts: z.object({
|
||||
descriptions: z.object({
|
||||
short: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
})
|
||||
.transform(({ data: { ...data } }) => {
|
||||
return {
|
||||
hotel: {
|
||||
...data,
|
||||
},
|
||||
url: data.url ?? "",
|
||||
}
|
||||
})
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
breakfastPackageInputSchema,
|
||||
cityCoordinatesInputSchema,
|
||||
getAdditionalDataInputSchema,
|
||||
getAllHotelsInput,
|
||||
getDestinationsMapDataInput,
|
||||
getHotelsByCityIdentifierInput,
|
||||
getHotelsByCountryInput,
|
||||
getHotelsByCSFilterInput,
|
||||
@@ -70,7 +70,7 @@ import type { BedTypeSelection } from "@/types/components/hotelReservation/enter
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
import { HotelTypeEnum } from "@/types/enums/hotelType"
|
||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||
import type { HotelDataWithUrl } from "@/types/hotel"
|
||||
import type { DestinationPagesHotelData, HotelDataWithUrl } from "@/types/hotel"
|
||||
import type {
|
||||
HotelsAvailabilityInputSchema,
|
||||
HotelsByHotelIdsAvailabilityInputSchema,
|
||||
@@ -1292,45 +1292,59 @@ export const hotelQueryRouter = router({
|
||||
return hotels.filter((hotel): hotel is HotelDataWithUrl => !!hotel)
|
||||
}),
|
||||
}),
|
||||
getAllHotels: router({
|
||||
get: serviceProcedure.input(getAllHotelsInput).query(async function ({
|
||||
input,
|
||||
ctx,
|
||||
}) {
|
||||
getDestinationsMapData: serviceProcedure
|
||||
.input(getDestinationsMapDataInput)
|
||||
.query(async function ({ input, ctx }) {
|
||||
const lang = input?.lang ?? ctx.lang
|
||||
const countries = await getCountries({
|
||||
// Countries need to be in English regardless of incoming lang because
|
||||
// we use the names as input for API endpoints.
|
||||
lang: Lang.en,
|
||||
serviceToken: ctx.serviceToken,
|
||||
})
|
||||
const warmup = input?.warmup ?? false
|
||||
|
||||
if (!countries) {
|
||||
throw new Error("Unable to fetch countries")
|
||||
const fetchHotels = async () => {
|
||||
const countries = await getCountries({
|
||||
// Countries need to be in English regardless of incoming lang because
|
||||
// we use the names as input for API endpoints.
|
||||
lang: Lang.en,
|
||||
serviceToken: ctx.serviceToken,
|
||||
})
|
||||
|
||||
if (!countries) {
|
||||
throw new Error("Unable to fetch countries")
|
||||
}
|
||||
|
||||
const countryNames = countries.data.map((country) => country.name)
|
||||
const hotelData: DestinationPagesHotelData[] = (
|
||||
await Promise.all(
|
||||
countryNames.map(async (country) => {
|
||||
const hotelIds = await getHotelIdsByCountry({
|
||||
country,
|
||||
serviceToken: ctx.serviceToken,
|
||||
})
|
||||
|
||||
const hotels = await getHotelsByHotelIds({
|
||||
hotelIds,
|
||||
lang: lang,
|
||||
serviceToken: ctx.serviceToken,
|
||||
})
|
||||
return hotels
|
||||
})
|
||||
)
|
||||
).flat()
|
||||
|
||||
return hotelData
|
||||
}
|
||||
|
||||
const countryNames = countries.data.map((country) => country.name)
|
||||
const hotelData: HotelDataWithUrl[] = (
|
||||
await Promise.all(
|
||||
countryNames.map(async (country) => {
|
||||
const hotelIds = await getHotelIdsByCountry({
|
||||
country,
|
||||
serviceToken: ctx.serviceToken,
|
||||
})
|
||||
if (warmup) {
|
||||
return await fetchHotels()
|
||||
}
|
||||
|
||||
const hotels = await getHotelsByHotelIds({
|
||||
hotelIds,
|
||||
lang: lang,
|
||||
serviceToken: ctx.serviceToken,
|
||||
})
|
||||
return hotels
|
||||
})
|
||||
)
|
||||
).flat()
|
||||
return hotelData
|
||||
const cacheClient = await getCacheClient()
|
||||
return await cacheClient.cacheOrGet(
|
||||
`${lang}:getDestinationsMapData`,
|
||||
fetchHotels,
|
||||
"max"
|
||||
)
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
||||
nearbyHotelIds: serviceProcedure
|
||||
.input(nearbyHotelIdsInput)
|
||||
.query(async function ({ ctx, input }) {
|
||||
@@ -1341,74 +1355,81 @@ export const hotelQueryRouter = router({
|
||||
const params: Record<string, string | number> = {
|
||||
language: apiLang,
|
||||
}
|
||||
metrics.nearbyHotelIds.counter.add(1, {
|
||||
hotelId,
|
||||
})
|
||||
console.info(
|
||||
"api.hotels.nearbyHotelIds start",
|
||||
JSON.stringify({ query: { hotelId, params } })
|
||||
)
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Hotel.Hotels.nearbyHotels(hotelId),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
metrics.nearbyHotelIds.fail.add(1, {
|
||||
hotelId,
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.hotels.nearbyHotelIds error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
const cacheClient = await getCacheClient()
|
||||
return cacheClient.cacheOrGet(
|
||||
`${apiLang}:nearbyHotels:${hotelId}`,
|
||||
async () => {
|
||||
metrics.nearbyHotelIds.counter.add(1, {
|
||||
hotelId,
|
||||
})
|
||||
console.info(
|
||||
"api.hotels.nearbyHotelIds start",
|
||||
JSON.stringify({ query: { hotelId, params } })
|
||||
)
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Hotel.Hotels.nearbyHotels(hotelId),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
metrics.nearbyHotelIds.fail.add(1, {
|
||||
hotelId,
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.hotels.nearbyHotelIds error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
const apiJson = await apiResponse.json()
|
||||
const validateHotelData = getNearbyHotelIdsSchema.safeParse(apiJson)
|
||||
if (!validateHotelData.success) {
|
||||
metrics.nearbyHotelIds.fail.add(1, {
|
||||
hotelId,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validateHotelData.error),
|
||||
})
|
||||
console.error(
|
||||
"api.hotels.nearbyHotelIds validation error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
error: validateHotelData.error,
|
||||
})
|
||||
)
|
||||
throw badRequestError()
|
||||
}
|
||||
metrics.nearbyHotelIds.success.add(1, {
|
||||
hotelId,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
const apiJson = await apiResponse.json()
|
||||
const validateHotelData = getNearbyHotelIdsSchema.safeParse(apiJson)
|
||||
if (!validateHotelData.success) {
|
||||
metrics.nearbyHotelIds.fail.add(1, {
|
||||
hotelId,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validateHotelData.error),
|
||||
})
|
||||
console.error(
|
||||
"api.hotels.nearbyHotelIds validation error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
error: validateHotelData.error,
|
||||
})
|
||||
)
|
||||
throw badRequestError()
|
||||
}
|
||||
metrics.nearbyHotelIds.success.add(1, {
|
||||
hotelId,
|
||||
})
|
||||
console.info(
|
||||
"api.hotels.nearbyHotelIds success",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
})
|
||||
)
|
||||
console.info(
|
||||
"api.hotels.nearbyHotelIds success",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
})
|
||||
)
|
||||
|
||||
return validateHotelData.data.map((id: string) => parseInt(id, 10))
|
||||
return validateHotelData.data.map((id: string) => parseInt(id, 10))
|
||||
},
|
||||
env.CACHE_TIME_HOTELS
|
||||
)
|
||||
}),
|
||||
locations: router({
|
||||
get: serviceProcedure.input(getLocationsInput).query(async function ({
|
||||
@@ -1459,22 +1480,29 @@ export const hotelQueryRouter = router({
|
||||
const { city, hotel } = input
|
||||
|
||||
async function fetchCoordinates(address: string) {
|
||||
const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${apiKey}`
|
||||
const response = await fetch(url)
|
||||
const data = await response.json()
|
||||
const cacheClient = await getCacheClient()
|
||||
return await cacheClient.cacheOrGet(
|
||||
`coordinates:${address}`,
|
||||
async function () {
|
||||
const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${apiKey}`
|
||||
const response = await fetch(url)
|
||||
const data = await response.json()
|
||||
|
||||
if (data.status !== "OK") {
|
||||
console.error(`Geocode error: ${data.status}`)
|
||||
return null
|
||||
}
|
||||
if (data.status !== "OK") {
|
||||
console.error(`Geocode error: ${data.status}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const location = data.results[0]?.geometry?.location
|
||||
if (!location) {
|
||||
console.error("No location found in geocode response")
|
||||
return null
|
||||
}
|
||||
const location = data.results[0]?.geometry?.location
|
||||
if (!location) {
|
||||
console.error("No location found in geocode response")
|
||||
return null
|
||||
}
|
||||
|
||||
return location
|
||||
return location
|
||||
},
|
||||
"1d"
|
||||
)
|
||||
}
|
||||
|
||||
let location = await fetchCoordinates(city)
|
||||
|
||||
@@ -24,7 +24,7 @@ import { getHotel } from "./query"
|
||||
import type { z } from "zod"
|
||||
|
||||
import { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest"
|
||||
import type { HotelDataWithUrl } from "@/types/hotel"
|
||||
import type { DestinationPagesHotelData } from "@/types/hotel"
|
||||
import type {
|
||||
CitiesGroupedByCountry,
|
||||
CityLocation,
|
||||
@@ -529,19 +529,54 @@ export async function getHotelsByHotelIds({
|
||||
lang: Lang
|
||||
serviceToken: string
|
||||
}) {
|
||||
const hotelPages = await getHotelPageUrls(lang)
|
||||
const hotels = await Promise.all(
|
||||
hotelIds.map(async (hotelId) => {
|
||||
const hotelData = await getHotel(
|
||||
{ hotelId, language: lang, isCardOnlyPayment: false },
|
||||
serviceToken
|
||||
)
|
||||
const hotelPage = hotelPages.find((page) => page.hotelId === hotelId)
|
||||
return hotelData ? { ...hotelData, url: hotelPage?.url ?? null } : null
|
||||
})
|
||||
)
|
||||
const cacheClient = await getCacheClient()
|
||||
const cacheKey = `${lang}:getHotelsByHotelIds:hotels:${hotelIds.sort().join(",")}`
|
||||
|
||||
return hotels.filter((hotel): hotel is HotelDataWithUrl => !!hotel)
|
||||
return await cacheClient.cacheOrGet(
|
||||
cacheKey,
|
||||
async () => {
|
||||
const hotelPages = await getHotelPageUrls(lang)
|
||||
const hotels = await Promise.all(
|
||||
hotelIds.map(async (hotelId) => {
|
||||
const hotelResponse = await getHotel(
|
||||
{ hotelId, language: lang, isCardOnlyPayment: false },
|
||||
serviceToken
|
||||
)
|
||||
|
||||
if (!hotelResponse) {
|
||||
throw new Error(`Hotel not found: ${hotelId}`)
|
||||
}
|
||||
|
||||
const hotelPage = hotelPages.find((page) => page.hotelId === hotelId)
|
||||
const { hotel, cities } = hotelResponse
|
||||
const data: DestinationPagesHotelData = {
|
||||
hotel: {
|
||||
id: hotel.id,
|
||||
galleryImages: hotel.galleryImages?.length
|
||||
? [hotel.galleryImages[0]]
|
||||
: [],
|
||||
name: hotel.name,
|
||||
tripadvisor: hotel.ratings?.tripAdvisor?.rating,
|
||||
detailedFacilities: hotel.detailedFacilities?.slice(0, 3) || [],
|
||||
location: hotel.location,
|
||||
hotelType: hotel.hotelType,
|
||||
type: hotel.type,
|
||||
address: hotel.address,
|
||||
cityIdentifier: cities?.[0]?.cityIdentifier,
|
||||
},
|
||||
url: hotelPage?.url ?? "",
|
||||
} satisfies DestinationPagesHotelData
|
||||
|
||||
return { ...data, url: hotelPage?.url ?? null }
|
||||
})
|
||||
)
|
||||
|
||||
return hotels.filter(
|
||||
(hotel): hotel is DestinationPagesHotelData => !!hotel
|
||||
)
|
||||
},
|
||||
"1d"
|
||||
)
|
||||
}
|
||||
|
||||
function findProduct(product: Products, rateDefinition: RateDefinition) {
|
||||
@@ -697,10 +732,13 @@ export async function getSelectedRoomAvailability(
|
||||
}
|
||||
|
||||
if (Array.isArray(product)) {
|
||||
const redemptionProduct = userPoints ? product.find(
|
||||
(r) => r.redemption.rateCode === rateDefinition.rateCode &&
|
||||
r.redemption.localPrice.pointsPerStay <= userPoints
|
||||
) : undefined
|
||||
const redemptionProduct = userPoints
|
||||
? product.find(
|
||||
(r) =>
|
||||
r.redemption.rateCode === rateDefinition.rateCode &&
|
||||
r.redemption.localPrice.pointsPerStay <= userPoints
|
||||
)
|
||||
: undefined
|
||||
if (!redemptionProduct) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -4,20 +4,20 @@ import type {
|
||||
SortItem,
|
||||
} from "@/types/components/destinationFilterAndSort"
|
||||
import { SortOption } from "@/types/enums/destinationFilterAndSort"
|
||||
import type { HotelDataWithUrl } from "@/types/hotel"
|
||||
import type { DestinationPagesHotelData } from "@/types/hotel"
|
||||
import type { DestinationCityListItem } from "@/types/trpc/routers/contentstack/destinationCityPage"
|
||||
|
||||
const HOTEL_SORTING_STRATEGIES: Partial<
|
||||
Record<SortOption, (a: HotelDataWithUrl, b: HotelDataWithUrl) => number>
|
||||
Record<
|
||||
SortOption,
|
||||
(a: DestinationPagesHotelData, b: DestinationPagesHotelData) => number
|
||||
>
|
||||
> = {
|
||||
[SortOption.Name]: function (a, b) {
|
||||
return a.hotel.name.localeCompare(b.hotel.name)
|
||||
},
|
||||
[SortOption.TripAdvisorRating]: function (a, b) {
|
||||
return (
|
||||
(b.hotel.ratings?.tripAdvisor.rating ?? 0) -
|
||||
(a.hotel.ratings?.tripAdvisor.rating ?? 0)
|
||||
)
|
||||
return (b.hotel.tripadvisor ?? 0) - (a.hotel.tripadvisor ?? 0)
|
||||
},
|
||||
[SortOption.Distance]: function (a, b) {
|
||||
return a.hotel.location.distanceToCentre - b.hotel.location.distanceToCentre
|
||||
@@ -48,7 +48,7 @@ const CITY_SORTING_STRATEGIES: Partial<
|
||||
}
|
||||
|
||||
export function getFilteredHotels(
|
||||
hotels: HotelDataWithUrl[],
|
||||
hotels: DestinationPagesHotelData[],
|
||||
filters: string[]
|
||||
) {
|
||||
if (filters.length) {
|
||||
@@ -62,11 +62,11 @@ export function getFilteredHotels(
|
||||
}
|
||||
|
||||
export function getFilteredCities(
|
||||
filteredHotels: HotelDataWithUrl[],
|
||||
filteredHotels: DestinationPagesHotelData[],
|
||||
cities: DestinationCityListItem[]
|
||||
) {
|
||||
const filteredCityIdentifiers = filteredHotels.map(
|
||||
(hotel) => hotel.cities[0].cityIdentifier
|
||||
(hotel) => hotel.hotel.cityIdentifier
|
||||
)
|
||||
|
||||
return cities.filter((city) =>
|
||||
@@ -83,7 +83,7 @@ export function getSortedCities(
|
||||
}
|
||||
|
||||
export function getSortedHotels(
|
||||
hotels: HotelDataWithUrl[],
|
||||
hotels: DestinationPagesHotelData[],
|
||||
sortOption: SortOption
|
||||
) {
|
||||
const sortFn = HOTEL_SORTING_STRATEGIES[sortOption]
|
||||
@@ -116,7 +116,7 @@ const HOTEL_FACILITIES_FILTER_TYPE_NAMES = [
|
||||
]
|
||||
|
||||
export function getFiltersFromHotels(
|
||||
hotels: HotelDataWithUrl[]
|
||||
hotels: DestinationPagesHotelData[]
|
||||
): CategorizedFilters {
|
||||
if (hotels.length === 0) {
|
||||
return { facilityFilters: [], surroundingsFilters: [] }
|
||||
|
||||
@@ -10,10 +10,7 @@ export type RoomListItemProps = {
|
||||
|
||||
export type RoomListItemImageProps = Pick<
|
||||
RoomConfiguration,
|
||||
| "features"
|
||||
| "roomType"
|
||||
| "roomTypeCode"
|
||||
| "roomsLeft"
|
||||
"features" | "roomType" | "roomTypeCode" | "roomsLeft"
|
||||
>
|
||||
|
||||
type RoomPackagePriceSchema = z.output<typeof packagePriceSchema>
|
||||
@@ -30,9 +27,9 @@ export type CalculatePricesPerNightProps = {
|
||||
|
||||
export interface RoomSizeProps {
|
||||
roomSize:
|
||||
| {
|
||||
max: number
|
||||
min: number
|
||||
}
|
||||
| undefined
|
||||
| {
|
||||
max: number
|
||||
min: number
|
||||
}
|
||||
| undefined
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { z } from "zod"
|
||||
|
||||
import type { hotelSchema } from "@/server/routers/hotels/output"
|
||||
import type {
|
||||
destinationPagesHotelDataSchema,
|
||||
hotelSchema,
|
||||
} from "@/server/routers/hotels/output"
|
||||
import type { citySchema } from "@/server/routers/hotels/schemas/city"
|
||||
import type { attributesSchema } from "@/server/routers/hotels/schemas/hotel"
|
||||
import type { addressSchema } from "@/server/routers/hotels/schemas/hotel/address"
|
||||
@@ -75,3 +78,7 @@ export type AdditionalData = ReturnType<typeof transformAdditionalData>
|
||||
export type ExtraPageSchema = z.output<typeof extraPageSchema>
|
||||
|
||||
export type HotelDataWithUrl = HotelData & { url: string }
|
||||
|
||||
export type DestinationPagesHotelData = z.output<
|
||||
typeof destinationPagesHotelDataSchema
|
||||
> & { url: string }
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { HotelDataWithUrl } from "@/types/hotel"
|
||||
import type { DestinationPagesHotelData } from "@/types/hotel"
|
||||
import type { SortItem } from "../components/destinationFilterAndSort"
|
||||
import type { DestinationCityListItem } from "../trpc/routers/contentstack/destinationCityPage"
|
||||
|
||||
export interface DestinationDataProviderProps extends React.PropsWithChildren {
|
||||
allHotels: HotelDataWithUrl[]
|
||||
allHotels: DestinationPagesHotelData[]
|
||||
allCities?: DestinationCityListItem[]
|
||||
sortItems: SortItem[]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
SortItem,
|
||||
} from "../components/destinationFilterAndSort"
|
||||
import type { SortOption } from "../enums/destinationFilterAndSort"
|
||||
import type { HotelDataWithUrl } from "../hotel"
|
||||
import type { DestinationPagesHotelData } from "../hotel"
|
||||
import type { DestinationCityListItem } from "../trpc/routers/contentstack/destinationCityPage"
|
||||
|
||||
interface Actions {
|
||||
@@ -25,8 +25,8 @@ export interface DestinationDataState {
|
||||
actions: Actions
|
||||
allCities: DestinationCityListItem[]
|
||||
activeCities: DestinationCityListItem[]
|
||||
allHotels: HotelDataWithUrl[]
|
||||
activeHotels: HotelDataWithUrl[]
|
||||
allHotels: DestinationPagesHotelData[]
|
||||
activeHotels: DestinationPagesHotelData[]
|
||||
pendingSort: SortOption
|
||||
activeSort: SortOption
|
||||
defaultSort: SortOption
|
||||
|
||||
Reference in New Issue
Block a user