Merge branch 'master' into feature/tracking

This commit is contained in:
Linus Flood
2024-11-18 12:20:13 +01:00
240 changed files with 5429 additions and 2717 deletions

View File

@@ -1,5 +1,5 @@
import LoadingSpinner from "@/components/LoadingSpinner"
export default function Loading() {
return <LoadingSpinner />
return <LoadingSpinner fullPage />
}

View File

@@ -1,5 +1,7 @@
import { headers } from "next/headers"
import { notFound } from "next/navigation"
import { isSignupPage } from "@/constants/routes/signup"
import { env } from "@/env/server"
import HotelPage from "@/components/ContentType/HotelPage"
@@ -22,17 +24,33 @@ export default function ContentTypePage({
}: PageArgs<LangParams & ContentTypeParams & UIDParams, {}>) {
setLang(params.lang)
const pathname = headers().get("x-pathname") || ""
switch (params.contentType) {
case "collection-page":
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()
}
return <CollectionPage />
case "content-page":
case "content-page": {
const isSignupRoute = isSignupPage(pathname)
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()
// Hide content pages for next release for non-signup routes.
if (!isSignupRoute) {
return notFound()
}
}
if (!env.SHOW_SIGNUP_FLOW) {
// Hide content pages for signup routes when signup flow is disabled.
if (isSignupRoute) {
return notFound()
}
}
return <ContentPage />
}
case "loyalty-page":
return <LoyaltyPage />
case "hotel-page":

View File

@@ -0,0 +1,68 @@
.mobileSummary {
display: block;
}
.desktopSummary {
display: none;
}
.summary {
background-color: var(--Main-Grey-White);
border-color: var(--Primary-Light-On-Surface-Divider-subtle);
border-style: solid;
border-width: 1px;
border-bottom: none;
z-index: 10;
}
.hider {
display: none;
}
.shadow {
display: none;
}
@media screen and (min-width: 1367px) {
.mobileSummary {
display: none;
}
.desktopSummary {
display: grid;
grid-template-rows: auto auto 1fr;
margin-top: calc(0px - var(--Spacing-x9));
}
.summary {
position: sticky;
top: calc(
var(--booking-widget-desktop-height) + var(--Spacing-x2) +
var(--Spacing-x-half)
);
z-index: 10;
border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0;
margin-top: calc(0px - var(--Spacing-x9));
}
.shadow {
display: block;
background-color: var(--Main-Grey-White);
border-color: var(--Primary-Light-On-Surface-Divider-subtle);
border-style: solid;
border-left-width: 1px;
border-right-width: 1px;
border-top: none;
border-bottom: none;
}
.hider {
display: block;
background-color: var(--Scandic-Brand-Warm-White);
position: sticky;
top: calc(var(--booking-widget-desktop-height) - 6px);
margin-top: var(--Spacing-x4);
height: 40px;
}
}

View File

@@ -1,25 +1,39 @@
import { redirect } from "next/navigation"
import { selectRate } from "@/constants/routes/hotelReservation"
import {
getPackages,
getProfileSafely,
getSelectedRoomAvailability,
} from "@/lib/trpc/memoizedRequests"
import Summary from "@/components/HotelReservation/EnterDetails/Summary"
import { SummaryBottomSheet } from "@/components/HotelReservation/EnterDetails/Summary/BottomSheet"
import {
generateChildrenString,
getQueryParamsForEnterDetails,
} from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import styles from "./page.module.css"
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import { LangParams, PageArgs, SearchParams } from "@/types/params"
export default async function SummaryPage({
params,
searchParams,
}: PageArgs<LangParams, SearchParams<SelectRateSearchParams>>) {
const selectRoomParams = new URLSearchParams(searchParams)
const { hotel, rooms, fromDate, toDate } =
getQueryParamsForEnterDetails(selectRoomParams)
const { adults, children, roomTypeCode, rateCode } = rooms[0] // TODO: Handle multiple rooms
const {
adults,
children,
roomTypeCode,
rateCode,
packages: packageCodes,
} = rooms[0] // TODO: Handle multiple rooms
const availability = await getSelectedRoomAvailability({
hotelId: hotel,
@@ -29,49 +43,88 @@ export default async function SummaryPage({
roomStayEndDate: toDate,
rateCode,
roomTypeCode,
packageCodes,
})
const user = await getProfileSafely()
if (!availability) {
const packages = packageCodes
? await getPackages({
hotelId: hotel,
startDate: fromDate,
endDate: toDate,
adults,
children: children?.length,
packageCodes,
})
: null
if (!availability || !availability.selectedRoom) {
console.error("No hotel or availability data", availability)
// TODO: handle this case
return null
redirect(selectRate[params.lang])
}
const prices =
user && availability.memberRate
? {
local: {
price: availability.memberRate?.localPrice.pricePerStay,
currency: availability.memberRate?.localPrice.currency,
price: availability.memberRate.localPrice.pricePerStay,
currency: availability.memberRate.localPrice.currency,
},
euro: {
price: availability.memberRate?.requestedPrice?.pricePerStay,
currency: availability.memberRate?.requestedPrice?.currency,
price: availability.memberRate.requestedPrice.pricePerStay,
currency: availability.memberRate.requestedPrice.currency,
},
}
: {
local: {
price: availability.publicRate?.localPrice.pricePerStay,
currency: availability.publicRate?.localPrice.currency,
price: availability.publicRate.localPrice.pricePerStay,
currency: availability.publicRate.localPrice.currency,
},
euro: {
price: availability.publicRate?.requestedPrice?.pricePerStay,
currency: availability.publicRate?.requestedPrice?.currency,
price: availability.publicRate.requestedPrice.pricePerStay,
currency: availability.publicRate.requestedPrice.currency,
},
}
return (
<Summary
showMemberPrice={!!(user && availability.memberRate)}
room={{
roomType: availability.selectedRoom.roomType,
localPrice: prices.local,
euroPrice: prices.euro,
adults,
children,
cancellationText: availability.cancellationText,
}}
/>
<>
<div className={styles.mobileSummary}>
<SummaryBottomSheet>
<div className={styles.summary}>
<Summary
showMemberPrice={!!(user && availability.memberRate)}
room={{
roomType: availability.selectedRoom.roomType,
localPrice: prices.local,
euroPrice: prices.euro,
adults,
children,
cancellationText: availability.cancellationText,
packages,
}}
/>
</div>
</SummaryBottomSheet>
</div>
<div className={styles.desktopSummary}>
<div className={styles.hider} />
<div className={styles.summary}>
<Summary
showMemberPrice={!!(user && availability.memberRate)}
room={{
roomType: availability.selectedRoom.roomType,
localPrice: prices.local,
euroPrice: prices.euro,
adults,
children,
cancellationText: availability.cancellationText,
packages,
}}
/>
</div>
<div className={styles.shadow} />
</div>
</>
)
}

View File

@@ -8,94 +8,38 @@
background-color: var(--Scandic-Brand-Warm-White);
}
.enter-details-layout__content {
.enter-details-layout__container {
display: grid;
gap: var(--Spacing-x3) var(--Spacing-x9);
grid-template-columns: 1fr 340px;
grid-template-rows: auto 1fr;
margin: var(--Spacing-x5) auto 0;
/* simulates padding on viewport smaller than --max-width-navigation */
width: min(
calc(100dvw - (var(--Spacing-x2) * 2)),
var(--max-width-navigation)
);
}
.enter-details-layout__content {
margin: var(--Spacing-x3) var(--Spacing-x2) 0;
}
.enter-details-layout__summaryContainer {
grid-column: 2 / 3;
grid-row: 1/-1;
}
.enter-details-layout__summary {
background-color: var(--Main-Grey-White);
border-color: var(--Primary-Light-On-Surface-Divider-subtle);
border-style: solid;
border-width: 1px;
border-radius: var(--Corner-radius-Large);
z-index: 1;
}
.enter-details-layout__hider {
display: none;
}
.enter-details-layout__shadow {
display: none;
}
@media screen and (min-width: 950px) {
.enter-details-layout__summaryContainer {
display: grid;
grid-template-rows: auto auto 1fr;
margin-top: calc(0px - var(--Spacing-x9));
}
.enter-details-layout__summary {
position: sticky;
top: calc(
var(--booking-widget-desktop-height) +
var(--booking-widget-desktop-height) + var(--Spacing-x-one-and-half)
);
margin-top: calc(0px - var(--Spacing-x9));
border-bottom: none;
border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0;
}
.enter-details-layout__hider {
display: block;
background-color: var(--Scandic-Brand-Warm-White);
position: sticky;
margin-top: var(--Spacing-x4);
top: calc(
var(--booking-widget-desktop-height) +
var(--booking-widget-desktop-height) - 6px
);
height: 40px;
}
.enter-details-layout__shadow {
display: block;
background-color: var(--Main-Grey-White);
border-color: var(--Primary-Light-On-Surface-Divider-subtle);
border-style: solid;
border-left-width: 1px;
border-right-width: 1px;
border-top: none;
border-bottom: none;
}
position: sticky;
bottom: 0;
left: 0;
right: 0;
}
@media screen and (min-width: 1367px) {
.enter-details-layout__summary {
top: calc(
var(--booking-widget-desktop-height) + var(--Spacing-x2) +
var(--Spacing-x-half)
.enter-details-layout__container {
grid-template-columns: 1fr 340px;
grid-template-rows: auto 1fr;
margin: var(--Spacing-x5) auto 0;
width: min(
calc(100dvw - (var(--Spacing-x2) * 2)),
var(--max-width-navigation)
);
}
.enter-details-layout__hider {
top: calc(var(--booking-widget-desktop-height) - 6px);
.enter-details-layout__summaryContainer {
position: static;
display: grid;
grid-column: 2/3;
grid-row: 1/-1;
}
}

View File

@@ -28,12 +28,10 @@ export default async function StepLayout({
<EnterDetailsProvider step={params.step} isMember={!!user}>
<main className="enter-details-layout__layout">
{hotelHeader}
<div className={"enter-details-layout__content"}>
{children}
<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" />
<div className={"enter-details-layout__container"}>
<div className={"enter-details-layout__content"}>{children}</div>
<aside className={"enter-details-layout__summaryContainer"}>
{summary}
</aside>
</div>
</main>

View File

@@ -46,7 +46,13 @@ export default async function StepPage({
toDate,
} = getQueryParamsForEnterDetails(selectRoomParams)
const { adults, children, roomTypeCode, rateCode } = rooms[0] // TODO: Handle multiple rooms
const {
adults,
children,
roomTypeCode,
rateCode,
packages: packageCodes,
} = rooms[0] // TODO: Handle multiple rooms
const childrenAsString = children && generateChildrenString(children)
@@ -60,12 +66,9 @@ export default async function StepPage({
roomStayEndDate: toDate,
rateCode,
roomTypeCode,
packageCodes,
})
const hotelData = await getHotelData({
hotelId,
language: lang,
})
const roomAvailability = await getSelectedRoomAvailability({
hotelId,
adults,
@@ -74,6 +77,12 @@ export default async function StepPage({
roomStayEndDate: toDate,
rateCode,
roomTypeCode,
packageCodes,
})
const hotelData = await getHotelData({
hotelId,
language: lang,
isCardOnlyPayment: roomAvailability?.mustBeGuaranteed,
})
const breakfastPackages = await getBreakfastPackages(breakfastInput)
const user = await getProfileSafely()

View File

@@ -3,6 +3,7 @@ import { notFound } from "next/navigation"
import { env } from "@/env/server"
import { getLocations } from "@/lib/trpc/memoizedRequests"
import { getHotelPins } from "@/components/HotelReservation/HotelCardDialogListing/utils"
import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap"
import {
generateChildrenString,
@@ -11,11 +12,7 @@ import {
import { MapModal } from "@/components/MapModal"
import { setLang } from "@/i18n/serverContext"
import {
fetchAvailableHotels,
getCentralCoordinates,
getHotelPins,
} from "../../utils"
import { fetchAvailableHotels } from "../../utils"
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
import type { LangParams, PageArgs } from "@/types/params"
@@ -61,16 +58,12 @@ export default async function SelectHotelMapPage({
const hotelPins = getHotelPins(hotels)
const centralCoordinates = getCentralCoordinates(hotelPins)
return (
<MapModal>
<SelectHotelMap
apiKey={googleMapsApiKey}
coordinates={centralCoordinates}
hotelPins={hotelPins}
mapId={googleMapId}
isModal={true}
hotels={hotels}
/>
</MapModal>

View File

@@ -1,21 +1,23 @@
.main {
display: flex;
gap: var(--Spacing-x3);
padding: var(--Spacing-x4) var(--Spacing-x4) 0 var(--Spacing-x4);
padding: 0 var(--Spacing-x2) var(--Spacing-x3) var(--Spacing-x2);
background-color: var(--Scandic-Brand-Warm-White);
min-height: 100dvh;
flex-direction: column;
max-width: var(--max-width);
margin: 0 auto;
}
.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);
flex-direction: column;
gap: var(--Spacing-x2);
padding: var(--Spacing-x3) var(--Spacing-x2) 0 var(--Spacing-x2);
}
.cityInformation {
display: flex;
flex-wrap: wrap;
gap: var(--Spacing-x1);
}
.sideBar {
@@ -38,7 +40,31 @@
flex: 1;
}
.hotelList {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--Spacing-x3);
}
@media (min-width: 768px) {
.main {
padding: var(--Spacing-x5);
}
.header {
display: block;
background-color: var(--Base-Surface-Subtle-Normal);
padding: var(--Spacing-x4) var(--Spacing-x5) var(--Spacing-x3)
var(--Spacing-x5);
}
.title {
margin: 0 auto;
display: flex;
max-width: var(--max-width-navigation);
align-items: center;
justify-content: space-between;
}
.link {
display: flex;
padding-bottom: var(--Spacing-x6);
@@ -50,13 +76,6 @@
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);

View File

@@ -19,7 +19,11 @@ import {
} from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { ChevronRightIcon } from "@/components/Icons"
import StaticMap from "@/components/Maps/StaticMap"
import Alert from "@/components/TempDesignSystem/Alert"
import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link"
import Preamble from "@/components/TempDesignSystem/Text/Preamble"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import TrackingSDK from "@/components/TrackingSDK"
import { getIntl } from "@/i18n"
import { setLang } from "@/i18n/serverContext"
@@ -32,6 +36,7 @@ import {
TrackingSDKHotelInfo,
TrackingSDKPageData,
} from "@/types/components/tracking"
import { AlertTypeEnum } from "@/types/enums/alert"
import { LangParams, PageArgs } from "@/types/params"
export default async function SelectHotelPage({
@@ -99,17 +104,44 @@ export default async function SelectHotelPage({
return (
<>
<header className={styles.header}>
<div>{city.name}</div>
<HotelSorter />
<div className={styles.title}>
<div className={styles.cityInformation}>
<Subtitle>{city.name}</Subtitle>
<Preamble>{hotels.length} hotels</Preamble>
</div>
<HotelSorter />
</div>
<MobileMapButtonContainer city={searchParams.city} />
</header>
<main className={styles.main}>
<div className={styles.sideBar}>
<Link
className={styles.link}
color="burgundy"
href={selectHotelMap[params.lang]}
keepSearchParams
>
{hotels.length > 0 ? ( // TODO: Temp fix until API returns hotels that are not available
<Link
className={styles.link}
color="burgundy"
href={selectHotelMap[params.lang]}
keepSearchParams
>
<div className={styles.mapContainer}>
<StaticMap
city={searchParams.city}
width={340}
height={180}
zoomLevel={11}
mapType="roadmap"
altText={`Map of ${searchParams.city} city center`}
/>
<Button wrapping size="medium" intent="text" theme="base">
{intl.formatMessage({ id: "See map" })}
<ChevronRightIcon
color="baseButtonTextOnFillNormal"
width={20}
height={20}
/>
</Button>
</div>
</Link>
) : (
<div className={styles.mapContainer}>
<StaticMap
city={searchParams.city}
@@ -119,16 +151,22 @@ export default async function SelectHotelPage({
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>
<HotelCardListing hotelData={hotels} />
<div className={styles.hotelList}>
{!hotels.length && (
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({ id: "No availability" })}
text={intl.formatMessage({
id: "There are no rooms available that match your request.",
})}
/>
)}
<HotelCardListing hotelData={hotels} />
</div>
<TrackingSDK
pageData={pageTrackingData}
hotelInfo={hotelsTrackingData}

View File

@@ -87,38 +87,3 @@ export function getFiltersFromHotels(hotels: HotelData[]): CategorizedFilters {
{ facilityFilters: [], surroundingsFilters: [] }
)
}
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,
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(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 /= hotels.length
centralCoordinates.lng /= hotels.length
return centralCoordinates
}

View File

@@ -18,7 +18,7 @@ import { setLang } from "@/i18n/serverContext"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import { LangParams, PageArgs } from "@/types/params"
import type { LangParams, PageArgs } from "@/types/params"
export default async function SelectRatePage({
params,
@@ -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-D")
: dt().utc().format("YYYY-MM-DD")
const validToDate =
searchParams.toDate && dt(searchParams.toDate).isAfter(validFromDate)
? searchParams.toDate
: dt().utc().add(1, "day").format("YYYY-MM-D")
: dt().utc().add(1, "day").format("YYYY-MM-DD")
const adults = selectRoomParamsObject.room[0].adults || 1 // TODO: Handle multiple rooms
const childrenCount = selectRoomParamsObject.room[0].child?.length
const children = selectRoomParamsObject.room[0].child
@@ -94,9 +94,16 @@ export default async function SelectRatePage({
const roomCategories = hotelData?.included
const noRoomsAvailable = roomsAvailability.roomConfigurations.reduce(
(acc, room) => {
return acc && room.status === "NotAvailable"
},
true
)
return (
<>
<HotelInfoCard hotelData={hotelData} />
<HotelInfoCard hotelData={hotelData} noAvailability={noRoomsAvailable} />
<Rooms
roomsAvailability={roomsAvailability}
roomCategories={roomCategories ?? []}

View File

@@ -1,5 +1,5 @@
import LoadingSpinner from "@/components/LoadingSpinner"
export default function Loading() {
return <LoadingSpinner />
return <LoadingSpinner fullPage />
}

View File

@@ -1,17 +1,11 @@
import { env } from "@/env/server"
import LoadingSpinner from "@/components/LoadingSpinner"
import styles from "./loading.module.css"
import { BookingWidgetSkeleton } from "@/components/BookingWidget/Client"
export default function LoadingBookingWidget() {
if (env.HIDE_FOR_NEXT_RELEASE) {
return null
}
return (
<div className={styles.container}>
<LoadingSpinner />
</div>
)
return <BookingWidgetSkeleton />
}

View File

@@ -1,11 +1,17 @@
import { env } from "@/env/server"
import CurrentLoadingSpinner from "@/components/Current/LoadingSpinner"
import LoadingSpinner from "@/components/LoadingSpinner"
import { FooterDetailsSkeleton } from "@/components/Footer/Details"
import { FooterNavigationSkeleton } from "@/components/Footer/Navigation"
export default function LoadingFooter() {
if (env.HIDE_FOR_NEXT_RELEASE) {
return <CurrentLoadingSpinner />
}
return <LoadingSpinner />
return (
<footer>
<FooterNavigationSkeleton />
<FooterDetailsSkeleton />
</footer>
)
}

View File

@@ -1,5 +1,5 @@
import LoadingSpinner from "@/components/LoadingSpinner"
export default function Loading() {
return <LoadingSpinner />
return <LoadingSpinner fullPage />
}

View File

@@ -68,7 +68,6 @@ export default async function RootLayout({
<ServerIntlProvider intl={{ defaultLocale, locale, messages }}>
<RouterTracking />
{header}
<BookingWidget />
{children}
<Footer />
<TokenRefresher />