Merged in feat/SW-1457-city-dynamic-map (pull request #1320)

feat(SW-1457): Added map and fetching hotels by cityIdentifier

* feat(SW-1457): Added map and fetching hotels by cityIdentifier


Approved-by: Fredrik Thorsson
Approved-by: Matilda Landström
This commit is contained in:
Erik Tiekstra
2025-02-12 13:02:19 +00:00
parent 019a5db549
commit 1532898c23
10 changed files with 106 additions and 88 deletions

View File

@@ -0,0 +1,33 @@
import { env } from "@/env/server"
import { getHotelsByCityIdentifier } from "@/lib/trpc/memoizedRequests"
import Title from "@/components/TempDesignSystem/Text/Title"
import Map from "../../Map"
import type { CityLocation } from "@/types/trpc/routers/hotel/locations"
interface CityMapProps {
city: CityLocation
cityIdentifier: string
}
export function preloadHotels(cityIdentifier: string) {
void getHotelsByCityIdentifier(cityIdentifier)
}
export default async function CityMap({ city, cityIdentifier }: CityMapProps) {
const hotels = await getHotelsByCityIdentifier(cityIdentifier)
return (
<Map
hotels={hotels}
mapId={env.GOOGLE_DYNAMIC_MAP_ID}
apiKey={env.GOOGLE_STATIC_MAP_KEY}
>
<div>
<Title level="h2" as="h3">
{city.name}
</Title>
</div>
</Map>
)
}

View File

@@ -16,6 +16,7 @@ import SidebarContentWrapper from "../SidebarContentWrapper"
import DestinationPageSidePeek from "../Sidepeek" import DestinationPageSidePeek from "../Sidepeek"
import StaticMap from "../StaticMap" import StaticMap from "../StaticMap"
import TopImages from "../TopImages" import TopImages from "../TopImages"
import CityMap, { preloadHotels } from "./CityMap"
import styles from "./destinationCityPage.module.css" import styles from "./destinationCityPage.module.css"
@@ -28,7 +29,7 @@ export default async function DestinationCityPage() {
return null return null
} }
const { tracking, destinationCityPage, cityIdentifier } = pageData const { tracking, destinationCityPage, cityIdentifier, city } = pageData
const { const {
blocks, blocks,
images, images,
@@ -41,6 +42,8 @@ export default async function DestinationCityPage() {
destination_settings, destination_settings,
} = destinationCityPage } = destinationCityPage
preloadHotels(cityIdentifier)
return ( return (
<> <>
<div className={styles.pageContainer}> <div className={styles.pageContainer}>
@@ -48,11 +51,7 @@ export default async function DestinationCityPage() {
<Suspense fallback={<BreadcrumbsSkeleton />}> <Suspense fallback={<BreadcrumbsSkeleton />}>
<Breadcrumbs variant={PageContentTypeEnum.destinationCityPage} /> <Breadcrumbs variant={PageContentTypeEnum.destinationCityPage} />
</Suspense> </Suspense>
{/* TODO: fetch translated city name from API when fetching hotel listing */} <TopImages images={images} destinationName={city.name} />
<TopImages
images={images}
destinationName={destination_settings.city}
/>
</header> </header>
<main className={styles.mainContent}> <main className={styles.mainContent}>
<Suspense fallback={<HotelListingSkeleton />}> <Suspense fallback={<HotelListingSkeleton />}>
@@ -78,6 +77,7 @@ export default async function DestinationCityPage() {
</SidebarContentWrapper> </SidebarContentWrapper>
</aside> </aside>
</div> </div>
<CityMap city={city} cityIdentifier={cityIdentifier} />
<Suspense fallback={null}> <Suspense fallback={null}>
<TrackingSDK pageData={tracking} /> <TrackingSDK pageData={tracking} />
</Suspense> </Suspense>

View File

@@ -1,23 +0,0 @@
.countryMap {
--destination-map-height: 100dvh;
position: absolute;
top: 0;
left: 0;
height: var(--destination-map-height);
width: 100dvw;
z-index: var(--hotel-dynamic-map-z-index);
display: flex;
background-color: var(--Base-Surface-Primary-light-Normal);
}
.wrapper {
position: absolute;
top: 0;
left: 0;
}
.closeButton {
pointer-events: initial;
box-shadow: var(--button-box-shadow);
gap: var(--Spacing-x-half);
}

View File

@@ -1,4 +1,4 @@
import { getHotelListDataByCityIdentifier } from "@/lib/trpc/memoizedRequests" import { getHotelsByCityIdentifier } from "@/lib/trpc/memoizedRequests"
import HotelListingClient from "./Client" import HotelListingClient from "./Client"
@@ -9,7 +9,7 @@ interface HotelListingProps {
export default async function HotelListing({ export default async function HotelListing({
cityIdentifier, cityIdentifier,
}: HotelListingProps) { }: HotelListingProps) {
const hotels = await getHotelListDataByCityIdentifier(cityIdentifier) const hotels = await getHotelsByCityIdentifier(cityIdentifier)
if (!hotels.length) { if (!hotels.length) {
return null return null

View File

@@ -49,14 +49,12 @@ export default function DynamicMap({
function zoomIn() { function zoomIn() {
const currentZoom = map && map.getZoom() const currentZoom = map && map.getZoom()
console.log(currentZoom)
if (currentZoom) { if (currentZoom) {
map.setZoom(currentZoom + 1) map.setZoom(currentZoom + 1)
} }
} }
function zoomOut() { function zoomOut() {
const currentZoom = map && map.getZoom() const currentZoom = map && map.getZoom()
console.log(currentZoom)
if (currentZoom) { if (currentZoom) {
map.setZoom(currentZoom - 1) map.setZoom(currentZoom - 1)
} }
@@ -65,6 +63,7 @@ export default function DynamicMap({
const mapOptions: MapProps = { const mapOptions: MapProps = {
defaultCenter: markers[0].coordinates, // Default center will be overridden by the bounds defaultCenter: markers[0].coordinates, // Default center will be overridden by the bounds
minZoom: 3, minZoom: 3,
maxZoom: 18,
defaultZoom: 8, defaultZoom: 8,
disableDefaultUI: true, disableDefaultUI: true,
clickableIcons: false, clickableIcons: false,

View File

@@ -213,20 +213,18 @@ export const getHotelsByCountry = cache(
}) })
} }
) )
export const getHotelsByCityIdentifier = cache(
async function getMemoizedHotelsByCityIdentifier(cityIdentifier: string) {
return serverClient().hotel.hotels.byCityIdentifier.get({
cityIdentifier,
})
}
)
export const getDestinationCityPage = cache( export const getDestinationCityPage = cache(
async function getMemoizedDestinationCityPage() { async function getMemoizedDestinationCityPage() {
return serverClient().contentstack.destinationCityPage.get() return serverClient().contentstack.destinationCityPage.get()
} }
) )
export const getHotelListDataByCityIdentifier = cache(
async function getMemoizedHotelListDataByCityIdentifier(
cityIdentifier: string
) {
return serverClient().contentstack.destinationCityPage.hotelList({
cityIdentifier,
})
}
)
export const getStartPage = cache(async function getMemoizedStartPage() { export const getStartPage = cache(async function getMemoizedStartPage() {
return serverClient().contentstack.startPage.get() return serverClient().contentstack.startPage.get()

View File

@@ -8,8 +8,7 @@ import { contentStackUidWithServiceProcedure, router } from "@/server/trpc"
import { generateTag } from "@/utils/generateTag" import { generateTag } from "@/utils/generateTag"
import { getHotelListData } from "../../hotels/utils" import { getCityByCityIdentifier } from "../../hotels/utils"
import { getHotelListDataInput } from "./input"
import { import {
destinationCityPageRefsSchema, destinationCityPageRefsSchema,
destinationCityPageSchema, destinationCityPageSchema,
@@ -148,6 +147,27 @@ export const destinationCityPageQueryRouter = router({
) )
return null return null
} }
const cityIdentifier =
validatedResponse.data.destinationCityPage.destination_settings.city
const city = await getCityByCityIdentifier(cityIdentifier, serviceToken)
if (!city) {
getDestinationCityPageFailCounter.add(1, {
lang,
uid: `${uid}`,
error_type: "not_found",
error: `Couldn't find city with cityIdentifier: ${cityIdentifier}`,
})
console.error(
"contentstack.destinationCityPage not found error",
JSON.stringify({
query: { lang, uid },
error: `Couldn't find city with cityIdentifier: ${cityIdentifier}`,
})
)
return null
}
getDestinationCityPageSuccessCounter.add(1, { lang, uid: `${uid}` }) getDestinationCityPageSuccessCounter.add(1, { lang, uid: `${uid}` })
console.info( console.info(
@@ -159,18 +179,8 @@ export const destinationCityPageQueryRouter = router({
return { return {
...validatedResponse.data, ...validatedResponse.data,
cityIdentifier: cityIdentifier,
validatedResponse.data.destinationCityPage.destination_settings.city, city,
} }
}), }),
hotelList: contentStackUidWithServiceProcedure
.input(getHotelListDataInput)
.query(async ({ ctx, input }) => {
const { lang, serviceToken } = ctx
const { cityIdentifier } = input
const hotels = await getHotelListData(lang, serviceToken, cityIdentifier)
return hotels
}),
}) })

View File

@@ -121,3 +121,7 @@ export const getAdditionalDataInputSchema = z.object({
export const getHotelsByCountryInput = z.object({ export const getHotelsByCountryInput = z.object({
country: z.nativeEnum(Country), country: z.nativeEnum(Country),
}) })
export const getHotelsByCityIdentifierInput = z.object({
cityIdentifier: z.string(),
})

View File

@@ -24,6 +24,7 @@ import {
breakfastPackageInputSchema, breakfastPackageInputSchema,
cityCoordinatesInputSchema, cityCoordinatesInputSchema,
getAdditionalDataInputSchema, getAdditionalDataInputSchema,
getHotelsByCityIdentifierInput,
getHotelsByCountryInput, getHotelsByCountryInput,
getHotelsByCSFilterInput, getHotelsByCSFilterInput,
getHotelsByHotelIdsAvailabilityInputSchema, getHotelsByHotelIdsAvailabilityInputSchema,
@@ -51,7 +52,9 @@ import {
getCitiesByCountry, getCitiesByCountry,
getCountries, getCountries,
getHotelIdsByCityId, getHotelIdsByCityId,
getHotelIdsByCityIdentifier,
getHotelIdsByCountry, getHotelIdsByCountry,
getHotelsByHotelIds,
getLocations, getLocations,
} from "./utils" } from "./utils"
@@ -861,21 +864,22 @@ export const hotelQueryRouter = router({
hotelIdsParams hotelIdsParams
) )
const hotels = await Promise.all( return await getHotelsByHotelIds(hotelIds, lang, serviceToken)
hotelIds.map(async (hotelId) => { }),
const [hotelData, url] = await Promise.all([ }),
getHotel( byCityIdentifier: router({
{ hotelId, isCardOnlyPayment: false, language: lang }, get: contentStackBaseWithServiceProcedure
ctx.serviceToken .input(getHotelsByCityIdentifierInput)
), .query(async ({ ctx, input }) => {
getHotelPageUrl(lang, hotelId), const { lang, serviceToken } = ctx
]) const { cityIdentifier } = input
return hotelData ? { ...hotelData, url } : null const hotelIds = await getHotelIdsByCityIdentifier(
}) cityIdentifier,
serviceToken
) )
return hotels.filter((hotel): hotel is HotelDataWithUrl => !!hotel) return await getHotelsByHotelIds(hotelIds, lang, serviceToken)
}), }),
}), }),
byCSFilter: router({ byCSFilter: router({

View File

@@ -20,7 +20,7 @@ import { getHotel } from "./query"
import type { Country } from "@/types/enums/country" import type { Country } from "@/types/enums/country"
import { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest" import { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest"
import type { RequestOptionsWithOutBody } from "@/types/fetch" import type { RequestOptionsWithOutBody } from "@/types/fetch"
import type { HotelData } from "@/types/hotel" import type { HotelDataWithUrl } from "@/types/hotel"
import type { import type {
CitiesGroupedByCountry, CitiesGroupedByCountry,
CityLocation, CityLocation,
@@ -423,15 +423,15 @@ export async function getHotelIdsByCityIdentifier(
serviceToken: string serviceToken: string
) { ) {
const apiLang = toApiLang(Lang.en) const apiLang = toApiLang(Lang.en)
const cityId = await getCityIdByCityIdentifier(cityIdentifier, serviceToken) const city = await getCityByCityIdentifier(cityIdentifier, serviceToken)
if (!cityId) { if (!city) {
return [] return []
} }
const hotelIdsParams = new URLSearchParams({ const hotelIdsParams = new URLSearchParams({
language: apiLang, language: apiLang,
city: cityId, city: city.id,
}) })
const options: RequestOptionsWithOutBody = { const options: RequestOptionsWithOutBody = {
// needs to clear default option as only // needs to clear default option as only
@@ -444,11 +444,11 @@ export async function getHotelIdsByCityIdentifier(
revalidate: env.CACHE_TIME_HOTELS, revalidate: env.CACHE_TIME_HOTELS,
}, },
} }
const hotelIds = await getHotelIdsByCityId(cityId, options, hotelIdsParams) const hotelIds = await getHotelIdsByCityId(city.id, options, hotelIdsParams)
return hotelIds return hotelIds
} }
export async function getCityIdByCityIdentifier( export async function getCityByCityIdentifier(
cityIdentifier: string, cityIdentifier: string,
serviceToken: string serviceToken: string
) { ) {
@@ -473,23 +473,18 @@ export async function getCityIdByCityIdentifier(
return null return null
} }
const cityId = locations const city = locations
.filter((loc): loc is CityLocation => loc.type === "cities") .filter((loc): loc is CityLocation => loc.type === "cities")
.find((loc) => loc.cityIdentifier === cityIdentifier)?.id .find((loc) => loc.cityIdentifier === cityIdentifier)
return cityId ?? null return city ?? null
} }
export async function getHotelListData( export async function getHotelsByHotelIds(
hotelIds: string[],
lang: Lang, lang: Lang,
serviceToken: string, serviceToken: string
cityIdentifier: string
) { ) {
const hotelIds = await getHotelIdsByCityIdentifier(
cityIdentifier,
serviceToken
)
const hotels = await Promise.all( const hotels = await Promise.all(
hotelIds.map(async (hotelId) => { hotelIds.map(async (hotelId) => {
const [hotelData, url] = await Promise.all([ const [hotelData, url] = await Promise.all([
@@ -504,7 +499,5 @@ export async function getHotelListData(
}) })
) )
return hotels.filter( return hotels.filter((hotel): hotel is HotelDataWithUrl => !!hotel)
(hotel): hotel is HotelData & { url: string | null } => !!hotel
)
} }