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:
Linus Flood
2025-04-02 11:37:22 +00:00
parent 961e8aea91
commit e4907d4b47
34 changed files with 381 additions and 290 deletions

View File

@@ -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>
))}

View File

@@ -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()

View File

@@ -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>

View File

@@ -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

View File

@@ -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,
}
}

View File

@@ -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

View File

@@ -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>
))}

View File

@@ -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)

View File

@@ -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,
}
}

View File

@@ -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"

View File

@@ -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 (
<>

View File

@@ -2,4 +2,4 @@
margin: 0 auto;
padding: var(--Spacing-x-one-and-half);
width: 100%;
}
}

View File

@@ -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
}
})
}
}

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -5,4 +5,4 @@
gap: var(--Spacing-x1);
margin: 0;
padding: var(--Spacing-x2);
}
}

View File

@@ -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(() => {

View File

@@ -5,6 +5,6 @@
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
}
.roomList>li {
.roomList > li {
width: 100%;
}
}

View File

@@ -62,4 +62,4 @@
.Border-Divider-Default {
background-color: var(--Border-Divider-Default);
}
}