Merge branch 'master' into feature/tracking

This commit is contained in:
Linus Flood
2024-11-11 15:47:30 +01:00
253 changed files with 3837 additions and 2268 deletions

View File

@@ -7,7 +7,6 @@ import Summary from "@/components/HotelReservation/EnterDetails/Summary"
import {
generateChildrenString,
getQueryParamsForEnterDetails,
mapChildrenFromString,
} from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
@@ -17,9 +16,11 @@ export default async function SummaryPage({
searchParams,
}: PageArgs<LangParams, SearchParams<SelectRateSearchParams>>) {
const selectRoomParams = new URLSearchParams(searchParams)
const { hotel, adults, children, roomTypeCode, rateCode, fromDate, toDate } =
const { hotel, rooms, fromDate, toDate } =
getQueryParamsForEnterDetails(selectRoomParams)
const { adults, children, roomTypeCode, rateCode } = rooms[0] // TODO: Handle multiple rooms
const availability = await getSelectedRoomAvailability({
hotelId: hotel,
adults,
@@ -37,31 +38,32 @@ export default async function SummaryPage({
return null
}
const prices = user
? {
local: {
price: availability.memberRate?.localPrice.pricePerStay,
currency: availability.memberRate?.localPrice.currency,
},
euro: {
price: availability.memberRate?.requestedPrice?.pricePerStay,
currency: availability.memberRate?.requestedPrice?.currency,
},
}
: {
local: {
price: availability.publicRate?.localPrice.pricePerStay,
currency: availability.publicRate?.localPrice.currency,
},
euro: {
price: availability.publicRate?.requestedPrice?.pricePerStay,
currency: availability.publicRate?.requestedPrice?.currency,
},
}
const prices =
user && availability.memberRate
? {
local: {
price: availability.memberRate?.localPrice.pricePerStay,
currency: availability.memberRate?.localPrice.currency,
},
euro: {
price: availability.memberRate?.requestedPrice?.pricePerStay,
currency: availability.memberRate?.requestedPrice?.currency,
},
}
: {
local: {
price: availability.publicRate?.localPrice.pricePerStay,
currency: availability.publicRate?.localPrice.currency,
},
euro: {
price: availability.publicRate?.requestedPrice?.pricePerStay,
currency: availability.publicRate?.requestedPrice?.currency,
},
}
return (
<Summary
isMember={!!user}
showMemberPrice={!!(user && availability.memberRate)}
room={{
roomType: availability.selectedRoom.roomType,
localPrice: prices.local,

View File

@@ -1,8 +1,14 @@
.layout {
/**
* Due to css import issues with parallel routes we are forced to
* use a regular css file and import it in the page.tsx
* This is addressed in Next 15: https://github.com/vercel/next.js/pull/66300
*/
.enter-details-layout {
background-color: var(--Scandic-Brand-Warm-White);
}
.content {
.enter-details-layout__content {
display: grid;
gap: var(--Spacing-x3) var(--Spacing-x9);
grid-template-columns: 1fr 340px;
@@ -15,12 +21,12 @@
);
}
.summaryContainer {
.enter-details-layout__summaryContainer {
grid-column: 2 / 3;
grid-row: 1/-1;
}
.summary {
.enter-details-layout__summary {
background-color: var(--Main-Grey-White);
border-color: var(--Primary-Light-On-Surface-Divider-subtle);
@@ -31,22 +37,22 @@
z-index: 1;
}
.hider {
.enter-details-layout__hider {
display: none;
}
.shadow {
.enter-details-layout__shadow {
display: none;
}
@media screen and (min-width: 950px) {
.summaryContainer {
.enter-details-layout__summaryContainer {
display: grid;
grid-template-rows: auto auto 1fr;
margin-top: calc(0px - var(--Spacing-x9));
}
.summary {
.enter-details-layout__summary {
position: sticky;
top: calc(
var(--booking-widget-desktop-height) +
@@ -57,7 +63,7 @@
border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0;
}
.hider {
.enter-details-layout__hider {
display: block;
background-color: var(--Scandic-Brand-Warm-White);
position: sticky;
@@ -69,7 +75,7 @@
height: 40px;
}
.shadow {
.enter-details-layout__shadow {
display: block;
background-color: var(--Main-Grey-White);
border-color: var(--Primary-Light-On-Surface-Divider-subtle);
@@ -82,14 +88,14 @@
}
@media screen and (min-width: 1367px) {
.summary {
.enter-details-layout__summary {
top: calc(
var(--booking-widget-desktop-height) + var(--Spacing-x2) +
var(--Spacing-x-half)
);
}
.hider {
.enter-details-layout__hider {
top: calc(var(--booking-widget-desktop-height) - 6px);
}
}

View File

@@ -1,15 +1,10 @@
import {
getCreditCardsSafely,
getProfileSafely,
} from "@/lib/trpc/memoizedRequests"
import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider"
import { setLang } from "@/i18n/serverContext"
import { preload } from "./_preload"
import styles from "./layout.module.css"
import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
import type { LangParams, LayoutArgs } from "@/types/params"
@@ -31,14 +26,14 @@ export default async function StepLayout({
return (
<EnterDetailsProvider step={params.step} isMember={!!user}>
<main className={styles.layout}>
<main className="enter-details-layout__layout">
{hotelHeader}
<div className={styles.content}>
<div className={"enter-details-layout__content"}>
{children}
<aside className={styles.summaryContainer}>
<div className={styles.hider} />
<div className={styles.summary}>{summary}</div>
<div className={styles.shadow} />
<aside className="enter-details-layout__summaryContainer">
<div className="enter-details-layout__hider" />
<div className="enter-details-layout__summary">{summary}</div>
<div className="enter-details-layout__shadow" />
</aside>
</div>
</main>

View File

@@ -1,3 +1,5 @@
import "./enterDetailsLayout.css"
import { notFound } from "next/navigation"
import {
@@ -39,14 +41,13 @@ export default async function StepPage({
const selectRoomParams = new URLSearchParams(searchParams)
const {
hotel: hotelId,
adults,
children,
roomTypeCode,
rateCode,
rooms,
fromDate,
toDate,
} = getQueryParamsForEnterDetails(selectRoomParams)
const { adults, children, roomTypeCode, rateCode } = rooms[0] // TODO: Handle multiple rooms
const childrenAsString = children && generateChildrenString(children)
const breakfastInput = { adults, fromDate, hotelId, toDate }
@@ -97,6 +98,11 @@ export default async function StepPage({
id: "Select payment method",
})
const roomPrice =
user && roomAvailability.memberRate
? roomAvailability.memberRate?.localPrice.pricePerStay
: roomAvailability.publicRate!.localPrice.pricePerStay
return (
<section>
<HistoryStateManager />
@@ -137,7 +143,7 @@ export default async function StepPage({
label={mustBeGuaranteed ? guaranteeWithCard : selectPaymentMethod}
>
<Payment
hotelId={searchParams.hotel}
roomPrice={roomPrice}
otherPaymentOptions={
hotelData.data.attributes.merchantInformationData
.alternatePaymentOptions

View File

@@ -4,15 +4,17 @@ import { env } from "@/env/server"
import { getLocations } from "@/lib/trpc/memoizedRequests"
import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap"
import { getHotelReservationQueryParams } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import {
generateChildrenString,
getHotelReservationQueryParams,
} from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { MapModal } from "@/components/MapModal"
import { setLang } from "@/i18n/serverContext"
import {
fetchAvailableHotels,
generateChildrenString,
getCentralCoordinates,
getPointOfInterests,
getHotelPins,
} from "../../utils"
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
@@ -57,18 +59,19 @@ export default async function SelectHotelMapPage({
children,
})
const pointOfInterests = getPointOfInterests(hotels)
const hotelPins = getHotelPins(hotels)
const centralCoordinates = getCentralCoordinates(pointOfInterests)
const centralCoordinates = getCentralCoordinates(hotelPins)
return (
<MapModal>
<SelectHotelMap
apiKey={googleMapsApiKey}
coordinates={centralCoordinates}
pointsOfInterest={pointOfInterests}
hotelPins={hotelPins}
mapId={googleMapId}
isModal={true}
hotels={hotels}
/>
</MapModal>
)

View File

@@ -9,17 +9,22 @@
margin: 0 auto;
}
.section {
.header {
display: flex;
margin: 0 auto;
padding: var(--Spacing-x4) var(--Spacing-x5) var(--Spacing-x3)
var(--Spacing-x5);
justify-content: space-between;
max-width: var(--max-width);
}
.sideBar {
display: flex;
flex-direction: column;
max-width: 340px;
}
.link {
display: flex;
padding: var(--Spacing-x2) var(--Spacing-x0);
}
.mapContainer {
display: none;
}
@@ -34,11 +39,27 @@
}
@media (min-width: 768px) {
.link {
display: flex;
padding-bottom: var(--Spacing-x6);
}
.mapContainer {
display: block;
display: flex;
flex-direction: column;
background: var(--Base-Surface-Primary-light-Normal);
border-radius: var(--Corner-radius-Medium);
border: 1px solid var(--Base-Border-Subtle);
}
.mapLinkText {
display: flex;
align-items: center;
justify-content: center;
gap: var(--Spacing-x-half);
padding: var(--Spacing-x-one-and-half) var(--Spacing-x0);
}
.main {
flex-direction: row;
gap: var(--Spacing-x5);
}
.buttonContainer {
display: none;

View File

@@ -11,6 +11,7 @@ import {
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
import HotelSorter from "@/components/HotelReservation/SelectHotel/HotelSorter"
import MobileMapButtonContainer from "@/components/HotelReservation/SelectHotel/MobileMapButtonContainer"
import {
generateChildrenString,
@@ -96,34 +97,43 @@ export default async function SelectHotelPage({
}
return (
<main className={styles.main}>
<section className={styles.section}>
<div className={styles.mapContainer}>
<Link href={selectHotelMap[params.lang]} keepSearchParams>
<StaticMap
city={searchParams.city}
width={340}
height={180}
zoomLevel={11}
mapType="roadmap"
altText={`Map of ${searchParams.city} city center`}
/>
</Link>
<>
<header className={styles.header}>
<div>{city.name}</div>
<HotelSorter />
</header>
<main className={styles.main}>
<div className={styles.sideBar}>
<Link
className={styles.link}
color="burgundy"
href={selectHotelMap[params.lang]}
keepSearchParams
>
{intl.formatMessage({ id: "Show map" })}
<ChevronRightIcon color="burgundy" />
<div className={styles.mapContainer}>
<StaticMap
city={searchParams.city}
width={340}
height={180}
zoomLevel={11}
mapType="roadmap"
altText={`Map of ${searchParams.city} city center`}
/>
<div className={styles.mapLinkText}>
{intl.formatMessage({ id: "Show map" })}
<ChevronRightIcon color="burgundy" width={20} height={20} />
</div>
</div>
</Link>
<MobileMapButtonContainer city={searchParams.city} />
<HotelFilter filters={filterList} />
</div>
<MobileMapButtonContainer city={searchParams.city} />
<HotelFilter filters={filterList} />
</section>
<HotelCardListing hotelData={hotels} />
<TrackingSDK pageData={pageTrackingData} hotelInfo={hotelsTrackingData} />
</main>
<HotelCardListing hotelData={hotels} />
<TrackingSDK
pageData={pageTrackingData}
hotelInfo={hotelsTrackingData}
/>
</main>
</>
)
}

View File

@@ -3,16 +3,23 @@ import { serverClient } from "@/lib/trpc/server"
import { getLang } from "@/i18n/serverContext"
import { BedTypeEnum } from "@/types/components/bookingWidget/enums"
import type { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput"
import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
import type { Filter } from "@/types/components/hotelReservation/selectHotel/hotelFilters"
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
import {
type PointOfInterest,
PointOfInterestCategoryNameEnum,
PointOfInterestGroupEnum,
} from "@/types/hotel"
import type {
CategorizedFilters,
Filter,
} from "@/types/components/hotelReservation/selectHotel/hotelFilters"
import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map"
import { HotelListingEnum } from "@/types/enums/hotelListing"
const hotelSurroundingsFilterNames = [
"Hotel surroundings",
"Hotel omgivelser",
"Hotelumgebung",
"Hotellia lähellä",
"Hotellomgivelser",
"Omgivningar",
]
export async function fetchAvailableHotels(
input: AvailabilityInput
@@ -22,7 +29,24 @@ export async function fetchAvailableHotels(
if (!availableHotels) throw new Error()
const language = getLang()
const hotels = availableHotels.availability.map(async (hotel) => {
const hotelMap = new Map<number, any>()
availableHotels.availability.forEach((hotel) => {
const existingHotel = hotelMap.get(hotel.hotelId)
if (existingHotel) {
if (hotel.ratePlanSet === HotelListingEnum.RatePlanSet.PUBLIC) {
existingHotel.bestPricePerNight.regularAmount =
hotel.bestPricePerNight?.regularAmount
} else if (hotel.ratePlanSet === HotelListingEnum.RatePlanSet.MEMBER) {
existingHotel.bestPricePerNight.memberAmount =
hotel.bestPricePerNight?.memberAmount
}
} else {
hotelMap.set(hotel.hotelId, { ...hotel })
}
})
const hotels = Array.from(hotelMap.values()).map(async (hotel) => {
const hotelData = await getHotelData({
hotelId: hotel.hotelId.toString(),
language,
@@ -39,7 +63,7 @@ export async function fetchAvailableHotels(
return await Promise.all(hotels)
}
export function getFiltersFromHotels(hotels: HotelData[]) {
export function getFiltersFromHotels(hotels: HotelData[]): CategorizedFilters {
const filters = hotels.flatMap((hotel) => hotel.hotelData.detailedFacilities)
const uniqueFilterIds = [...new Set(filters.map((filter) => filter.id))]
@@ -47,51 +71,54 @@ export function getFiltersFromHotels(hotels: HotelData[]) {
.map((filterId) => filters.find((filter) => filter.id === filterId))
.filter((filter): filter is Filter => filter !== undefined)
return filterList
return filterList.reduce<CategorizedFilters>(
(acc, filter) => {
if (filter.filter && hotelSurroundingsFilterNames.includes(filter.filter))
return {
facilityFilters: acc.facilityFilters,
surroundingsFilters: [...acc.surroundingsFilters, filter],
}
return {
facilityFilters: [...acc.facilityFilters, filter],
surroundingsFilters: acc.surroundingsFilters,
}
},
{ facilityFilters: [], surroundingsFilters: [] }
)
}
const bedTypeMap: Record<number, string> = {
[BedTypeEnum.IN_ADULTS_BED]: "ParentsBed",
[BedTypeEnum.IN_CRIB]: "Crib",
[BedTypeEnum.IN_EXTRA_BED]: "ExtraBed",
}
export function generateChildrenString(children: Child[]): string {
return `[${children
?.map((child) => {
const age = child.age
const bedType = bedTypeMap[+child.bed]
return `${age}:${bedType}`
})
.join(",")}]`
}
export function getPointOfInterests(hotels: HotelData[]): PointOfInterest[] {
// TODO: this is just a quick transformation to get something there. May need rework
export function getHotelPins(hotels: HotelData[]): HotelPin[] {
return hotels.map((hotel) => ({
coordinates: {
lat: hotel.hotelData.location.latitude,
lng: hotel.hotelData.location.longitude,
},
name: hotel.hotelData.name,
distance: hotel.hotelData.location.distanceToCentre,
categoryName: PointOfInterestCategoryNameEnum.HOTEL,
group: PointOfInterestGroupEnum.LOCATION,
publicPrice: hotel.price?.regularAmount ?? null,
memberPrice: hotel.price?.memberAmount ?? null,
currency: hotel.price?.currency || null,
images: [
hotel.hotelData.hotelContent.images,
...(hotel.hotelData.gallery?.heroImages ?? []),
],
amenities: hotel.hotelData.detailedFacilities.slice(0, 3),
ratings: hotel.hotelData.ratings?.tripAdvisor.rating ?? null,
}))
}
export function getCentralCoordinates(pointOfInterests: PointOfInterest[]) {
const centralCoordinates = pointOfInterests.reduce(
(acc, poi) => {
acc.lat += poi.coordinates.lat
acc.lng += poi.coordinates.lng
export function getCentralCoordinates(hotels: HotelPin[]) {
const centralCoordinates = hotels.reduce(
(acc, pin) => {
acc.lat += pin.coordinates.lat
acc.lng += pin.coordinates.lng
return acc
},
{ lat: 0, lng: 0 }
)
centralCoordinates.lat /= pointOfInterests.length
centralCoordinates.lng /= pointOfInterests.length
centralCoordinates.lat /= hotels.length
centralCoordinates.lng /= hotels.length
return centralCoordinates
}

View File

@@ -49,11 +49,11 @@ export default async function SelectRatePage({
searchParams.fromDate &&
dt(searchParams.fromDate).isAfter(dt().subtract(1, "day"))
? searchParams.fromDate
: dt().utc().format("YYYY-MM-DD")
: dt().utc().format("YYYY-MM-D")
const validToDate =
searchParams.toDate && dt(searchParams.toDate).isAfter(validFromDate)
? searchParams.toDate
: dt().utc().add(1, "day").format("YYYY-MM-DD")
: dt().utc().add(1, "day").format("YYYY-MM-D")
const adults = selectRoomParamsObject.room[0].adults || 1 // TODO: Handle multiple rooms
const childrenCount = selectRoomParamsObject.room[0].child?.length
const children = selectRoomParamsObject.room[0].child