Merge branch 'develop'

This commit is contained in:
Linus Flood
2024-11-07 10:20:12 +01:00
261 changed files with 5132 additions and 2106 deletions

View File

@@ -2,9 +2,10 @@ import { notFound } from "next/navigation"
import { env } from "@/env/server"
import ContentPage from "@/components/ContentType/ContentPage"
import HotelPage from "@/components/ContentType/HotelPage"
import LoyaltyPage from "@/components/ContentType/LoyaltyPage"
import CollectionPage from "@/components/ContentType/StaticPages/CollectionPage"
import ContentPage from "@/components/ContentType/StaticPages/ContentPage"
import { setLang } from "@/i18n/serverContext"
import {
@@ -22,6 +23,11 @@ export default function ContentTypePage({
setLang(params.lang)
switch (params.contentType) {
case "collection-page":
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()
}
return <CollectionPage />
case "content-page":
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()

View File

@@ -45,8 +45,8 @@ export default async function BookingConfirmationPage({
}
)
const fromDate = dt(booking.temp.fromDate).locale(params.lang)
const toDate = dt(booking.temp.toDate).locale(params.lang)
const fromDate = dt(booking.checkInDate).locale(params.lang)
const toDate = dt(booking.checkOutDate).locale(params.lang)
const nights = intl.formatMessage(
{ id: "booking.nights" },
{
@@ -77,7 +77,7 @@ export default async function BookingConfirmationPage({
textTransform="regular"
type="h1"
>
{booking.hotel.name}
{booking.hotel?.data.attributes.name}
</Title>
</hgroup>
<Body className={styles.body} textAlign="center">
@@ -91,7 +91,7 @@ export default async function BookingConfirmationPage({
<Subtitle color="burgundy" type="two">
{intl.formatMessage(
{ id: "Reference #{bookingNr}" },
{ bookingNr: "A92320VV" }
{ bookingNr: booking.confirmationNumber }
)}
</Subtitle>
</header>
@@ -183,11 +183,13 @@ export default async function BookingConfirmationPage({
</Caption>
<div>
<Body color="burgundy" textTransform="bold">
{booking.hotel.name}
{booking.hotel?.data.attributes.name}
</Body>
<Body color="uiTextHighContrast">{booking.hotel.email}</Body>
<Body color="uiTextHighContrast">
{booking.hotel.phoneNumber}
{booking.hotel?.data.attributes.contactInformation.email}
</Body>
<Body color="uiTextHighContrast">
{booking.hotel?.data.attributes.contactInformation.phoneNumber}
</Body>
</div>
</div>
@@ -219,7 +221,16 @@ export default async function BookingConfirmationPage({
<Body color="burgundy" textTransform="bold">
{intl.formatMessage({ id: "Total cost" })}
</Body>
<Body color="uiTextHighContrast">{booking.temp.total}</Body>
<Body color="uiTextHighContrast">
{" "}
{intl.formatMessage(
{ id: "{amount} {currency}" },
{
amount: intl.formatNumber(booking.totalPrice),
currency: booking.currencyCode,
}
)}
</Body>
<Caption color="uiTextPlaceholder">
{`${intl.formatMessage({ id: "Approx." })} ${booking.temp.totalInEuro}`}
</Caption>

View File

@@ -0,0 +1,25 @@
import { getHotelData } from "@/lib/trpc/memoizedRequests"
import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import SidePeek from "@/components/HotelReservation/SidePeek"
import type { LangParams, PageArgs } from "@/types/params"
export default async function HotelSidePeek({
params,
searchParams,
}: PageArgs<LangParams, { hotel: string }>) {
const search = new URLSearchParams(searchParams)
const { hotel: hotelId } = getQueryParamsForEnterDetails(search)
if (!hotelId) {
return <SidePeek hotel={null} />
}
const hotel = await getHotelData({
hotelId: hotelId,
language: params.lang,
})
return <SidePeek hotel={hotel} />
}

View File

@@ -14,7 +14,10 @@ export default async function HotelHeader({
if (!searchParams.hotel) {
redirect(home)
}
const hotel = await getHotelData(searchParams.hotel, params.lang)
const hotel = await getHotelData({
hotelId: searchParams.hotel,
language: params.lang,
})
if (!hotel?.data) {
redirect(home)
}

View File

@@ -1,21 +0,0 @@
import { redirect } from "next/navigation"
import { getHotelData } from "@/lib/trpc/memoizedRequests"
import SidePeek from "@/components/HotelReservation/EnterDetails/SidePeek"
import type { LangParams, PageArgs } from "@/types/params"
export default async function HotelSidePeek({
params,
searchParams,
}: PageArgs<LangParams, { hotel: string }>) {
if (!searchParams.hotel) {
redirect(`/${params.lang}`)
}
const hotel = await getHotelData(searchParams.hotel, params.lang)
if (!hotel?.data) {
redirect(`/${params.lang}`)
}
return <SidePeek hotel={hotel.data.attributes} />
}

View File

@@ -0,0 +1,75 @@
import {
getProfileSafely,
getSelectedRoomAvailability,
} from "@/lib/trpc/memoizedRequests"
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"
import { LangParams, PageArgs, SearchParams } from "@/types/params"
export default async function SummaryPage({
searchParams,
}: PageArgs<LangParams, SearchParams<SelectRateSearchParams>>) {
const selectRoomParams = new URLSearchParams(searchParams)
const { hotel, adults, children, roomTypeCode, rateCode, fromDate, toDate } =
getQueryParamsForEnterDetails(selectRoomParams)
const availability = await getSelectedRoomAvailability({
hotelId: hotel,
adults,
children: children ? generateChildrenString(children) : undefined,
roomStayStartDate: fromDate,
roomStayEndDate: toDate,
rateCode,
roomTypeCode,
})
const user = await getProfileSafely()
if (!availability) {
console.error("No hotel or availability data", availability)
// TODO: handle this case
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,
},
}
return (
<Summary
isMember={!!user}
room={{
roomType: availability.selectedRoom.roomType,
localPrice: prices.local,
euroPrice: prices.euro,
adults,
children,
cancellationText: availability.cancellationText,
}}
/>
)
}

View File

@@ -0,0 +1,9 @@
import {
getCreditCardsSafely,
getProfileSafely,
} from "@/lib/trpc/memoizedRequests"
export function preload() {
void getProfileSafely()
void getCreditCardsSafely()
}

View File

@@ -1,5 +1,4 @@
.layout {
min-height: 100dvh;
background-color: var(--Scandic-Brand-Warm-White);
}
@@ -9,7 +8,6 @@
grid-template-columns: 1fr 340px;
grid-template-rows: auto 1fr;
margin: var(--Spacing-x5) auto 0;
padding-top: var(--Spacing-x6);
/* simulates padding on viewport smaller than --max-width-navigation */
width: min(
calc(100dvw - (var(--Spacing-x2) * 2)),
@@ -17,8 +15,81 @@
);
}
.summary {
align-self: flex-start;
.summaryContainer {
grid-column: 2 / 3;
grid-row: 1/-1;
}
.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;
}
.hider {
display: none;
}
.shadow {
display: none;
}
@media screen and (min-width: 950px) {
.summaryContainer {
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(--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;
}
.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;
}
.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;
}
}
@media screen and (min-width: 1367px) {
.summary {
top: calc(
var(--booking-widget-desktop-height) + var(--Spacing-x2) +
var(--Spacing-x-half)
);
}
.hider {
top: calc(var(--booking-widget-desktop-height) - 6px);
}
}

View File

@@ -1,40 +1,46 @@
import {
getCreditCardsSafely,
getProfileSafely,
} from "@/lib/trpc/memoizedRequests"
import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider"
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
import Summary from "@/components/HotelReservation/EnterDetails/Summary"
import { setLang } from "@/i18n/serverContext"
import { preload } from "./page"
import { preload } from "./_preload"
import styles from "./layout.module.css"
import { StepEnum } from "@/types/components/enterDetails/step"
import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
import type { LangParams, LayoutArgs } from "@/types/params"
export default async function StepLayout({
summary,
children,
hotelHeader,
params,
sidePeek,
}: React.PropsWithChildren<
LayoutArgs<LangParams & { step: StepEnum }> & {
hotelHeader: React.ReactNode
sidePeek: React.ReactNode
summary: React.ReactNode
}
>) {
setLang(params.lang)
preload()
const user = await getProfileSafely()
return (
<EnterDetailsProvider step={params.step}>
<EnterDetailsProvider step={params.step} isMember={!!user}>
<main className={styles.layout}>
{hotelHeader}
<div className={styles.content}>
<SelectedRoom />
{children}
<aside className={styles.summary}>
<Summary />
<aside className={styles.summaryContainer}>
<div className={styles.hider} />
<div className={styles.summary}>{summary}</div>
<div className={styles.shadow} />
</aside>
</div>
{sidePeek}
</main>
</EnterDetailsProvider>
)

View File

@@ -1,11 +1,11 @@
import { notFound, redirect } from "next/navigation"
import { notFound } from "next/navigation"
import {
getBreakfastPackages,
getCreditCardsSafely,
getHotelData,
getProfileSafely,
getRoomAvailability,
getSelectedRoomAvailability,
} from "@/lib/trpc/memoizedRequests"
import BedType from "@/components/HotelReservation/EnterDetails/BedType"
@@ -14,16 +14,17 @@ import Details from "@/components/HotelReservation/EnterDetails/Details"
import HistoryStateManager from "@/components/HotelReservation/EnterDetails/HistoryStateManager"
import Payment from "@/components/HotelReservation/EnterDetails/Payment"
import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion"
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
import {
generateChildrenString,
getQueryParamsForEnterDetails,
} from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { getIntl } from "@/i18n"
import { StepEnum } from "@/types/components/enterDetails/step"
import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { LangParams, PageArgs } from "@/types/params"
export function preload() {
void getProfileSafely()
void getCreditCardsSafely()
}
function isValidStep(step: string): step is StepEnum {
return Object.values(StepEnum).includes(step as StepEnum)
}
@@ -31,34 +32,53 @@ function isValidStep(step: string): step is StepEnum {
export default async function StepPage({
params,
searchParams,
}: PageArgs<LangParams & { step: StepEnum }, { hotel: string }>) {
if (!searchParams.hotel) {
redirect(`/${params.lang}`)
}
void getBreakfastPackages(searchParams.hotel)
void getRoomAvailability({
hotelId: searchParams.hotel,
adults: Number(searchParams.adults),
roomStayStartDate: searchParams.checkIn,
roomStayEndDate: searchParams.checkOut,
})
}: PageArgs<LangParams & { step: StepEnum }, SelectRateSearchParams>) {
const { lang } = params
const intl = await getIntl()
const selectRoomParams = new URLSearchParams(searchParams)
const {
hotel: hotelId,
adults,
children,
roomTypeCode,
rateCode,
fromDate,
toDate,
} = getQueryParamsForEnterDetails(selectRoomParams)
const hotel = await getHotelData(searchParams.hotel, params.lang)
const user = await getProfileSafely()
const savedCreditCards = await getCreditCardsSafely()
const breakfastPackages = await getBreakfastPackages(searchParams.hotel)
const childrenAsString = children && generateChildrenString(children)
const roomAvailability = await getRoomAvailability({
hotelId: searchParams.hotel,
adults: Number(searchParams.adults),
roomStayStartDate: searchParams.checkIn,
roomStayEndDate: searchParams.checkOut,
rateCode: searchParams.rateCode,
const breakfastInput = { adults, fromDate, hotelId, toDate }
void getBreakfastPackages(breakfastInput)
void getSelectedRoomAvailability({
hotelId,
adults,
children: childrenAsString,
roomStayStartDate: fromDate,
roomStayEndDate: toDate,
rateCode,
roomTypeCode,
})
if (!isValidStep(params.step) || !hotel || !roomAvailability) {
const hotelData = await getHotelData({
hotelId,
language: lang,
})
const roomAvailability = await getSelectedRoomAvailability({
hotelId,
adults,
children: childrenAsString,
roomStayStartDate: fromDate,
roomStayEndDate: toDate,
rateCode,
roomTypeCode,
})
const breakfastPackages = await getBreakfastPackages(breakfastInput)
const user = await getProfileSafely()
const savedCreditCards = await getCreditCardsSafely()
if (!isValidStep(params.step) || !hotelData || !roomAvailability) {
return notFound()
}
@@ -80,13 +100,20 @@ export default async function StepPage({
return (
<section>
<HistoryStateManager />
<SectionAccordion
header={intl.formatMessage({ id: "Select bed" })}
step={StepEnum.selectBed}
label={intl.formatMessage({ id: "Request bedtype" })}
>
<BedType />
</SectionAccordion>
<SelectedRoom hotelId={hotelId} room={roomAvailability.selectedRoom} />
{/* TODO: How to handle no beds found? */}
{roomAvailability.bedTypes ? (
<SectionAccordion
header="Select bed"
step={StepEnum.selectBed}
label={intl.formatMessage({ id: "Request bedtype" })}
>
<BedType bedTypes={roomAvailability.bedTypes} />
</SectionAccordion>
) : null}
<SectionAccordion
header={intl.formatMessage({ id: "Food options" })}
step={StepEnum.breakfast}
@@ -109,7 +136,7 @@ export default async function StepPage({
<Payment
hotelId={searchParams.hotel}
otherPaymentOptions={
hotel.data.attributes.merchantInformationData
hotelData.data.attributes.merchantInformationData
.alternatePaymentOptions
}
savedCreditCards={savedCreditCards}

View File

@@ -1,4 +1,3 @@
.layout {
min-height: 100dvh;
background-color: var(--Base-Background-Primary-Normal);
}

View File

@@ -8,9 +8,17 @@ import { LangParams, LayoutArgs } from "@/types/params"
export default function HotelReservationLayout({
children,
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
sidePeek,
}: React.PropsWithChildren<LayoutArgs<LangParams>> & {
sidePeek: React.ReactNode
}) {
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()
}
return <div className={styles.layout}>{children}</div>
return (
<div className={styles.layout}>
{children}
{sidePeek}
</div>
)
}

View File

@@ -0,0 +1,75 @@
import { notFound } from "next/navigation"
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 { MapModal } from "@/components/MapModal"
import { setLang } from "@/i18n/serverContext"
import {
fetchAvailableHotels,
generateChildrenString,
getCentralCoordinates,
getPointOfInterests,
} from "../../utils"
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
import type { LangParams, PageArgs } from "@/types/params"
export default async function SelectHotelMapPage({
params,
searchParams,
}: PageArgs<LangParams, SelectHotelSearchParams>) {
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()
}
setLang(params.lang)
const locations = await getLocations()
if (!locations || "error" in locations) {
return null
}
const city = locations.data.find(
(location) =>
location.name.toLowerCase() === searchParams.city.toLowerCase()
)
if (!city) return notFound()
const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
const selectHotelParams = new URLSearchParams(searchParams)
const selectHotelParamsObject =
getHotelReservationQueryParams(selectHotelParams)
const adults = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms
const children = selectHotelParamsObject.room[0].child
? generateChildrenString(selectHotelParamsObject.room[0].child)
: undefined // TODO: Handle multiple rooms
const hotels = await fetchAvailableHotels({
cityId: city.id,
roomStayStartDate: searchParams.fromDate,
roomStayEndDate: searchParams.toDate,
adults,
children,
})
const pointOfInterests = getPointOfInterests(hotels)
const centralCoordinates = getCentralCoordinates(pointOfInterests)
return (
<MapModal>
<SelectHotelMap
apiKey={googleMapsApiKey}
coordinates={centralCoordinates}
pointsOfInterest={pointOfInterests}
mapId={googleMapId}
isModal={true}
/>
</MapModal>
)
}

View File

@@ -0,0 +1,3 @@
export default function Default() {
return null
}

View File

@@ -0,0 +1,5 @@
.layout {
min-height: 100dvh;
background-color: var(--Base-Background-Primary-Normal);
position: relative;
}

View File

@@ -0,0 +1,24 @@
import { notFound } from "next/navigation"
import { env } from "@/env/server"
import styles from "./layout.module.css"
import { LangParams, LayoutArgs } from "@/types/params"
export default function HotelReservationLayout({
children,
modal,
}: React.PropsWithChildren<
LayoutArgs<LangParams> & { modal: React.ReactNode }
>) {
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()
}
return (
<div className={styles.layout}>
{children}
{modal}
</div>
)
}

View File

@@ -2,5 +2,5 @@
display: grid;
background-color: var(--Scandic-Brand-Warm-White);
min-height: 100dvh;
grid-template-columns: 420px 1fr;
position: relative;
}

View File

@@ -1,58 +1 @@
import { env } from "@/env/server"
import {
fetchAvailableHotels,
getFiltersFromHotels,
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap"
import { setLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
import {
PointOfInterest,
PointOfInterestCategoryNameEnum,
PointOfInterestGroupEnum,
} from "@/types/hotel"
import { LangParams, PageArgs } from "@/types/params"
export default async function SelectHotelMapPage({
params,
}: PageArgs<LangParams, {}>) {
const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
setLang(params.lang)
const hotels = await fetchAvailableHotels({
cityId: "8ec4bba3-1c38-4606-82d1-bbe3f6738e54",
roomStayStartDate: "2024-11-02",
roomStayEndDate: "2024-11-03",
adults: 1,
})
const filters = getFiltersFromHotels(hotels)
// TODO: this is just a quick transformation to get something there. May need rework
const pointOfInterests: PointOfInterest[] = 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,
}))
return (
<main className={styles.main}>
<SelectHotelMap
apiKey={googleMapsApiKey}
// TODO: use correct coordinates. The city center?
coordinates={{ lat: 59.32, lng: 18.01 }}
pointsOfInterest={pointOfInterests}
mapId={googleMapId}
/>
</main>
)
}
export { default } from "../@modal/(.)map/page"

View File

@@ -1,6 +1,6 @@
.main {
display: flex;
gap: var(--Spacing-x4);
gap: var(--Spacing-x3);
padding: var(--Spacing-x4) var(--Spacing-x4) 0 var(--Spacing-x4);
background-color: var(--Scandic-Brand-Warm-White);
min-height: 100dvh;
@@ -19,8 +19,28 @@
padding: var(--Spacing-x2) var(--Spacing-x0);
}
.mapContainer {
display: none;
}
.buttonContainer {
display: flex;
gap: var(--Spacing-x2);
margin-bottom: var(--Spacing-x3);
}
.button {
flex: 1;
}
@media (min-width: 768px) {
.mapContainer {
display: block;
}
.main {
flex-direction: row;
}
.buttonContainer {
display: none;
}
}

View File

@@ -7,12 +7,15 @@ import { getLocations } from "@/lib/trpc/memoizedRequests"
import {
fetchAvailableHotels,
generateChildrenString,
getFiltersFromHotels,
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import MobileMapButtonContainer from "@/components/HotelReservation/SelectHotel/MobileMapButtonContainer"
import {
generateChildrenString,
getHotelReservationQueryParams,
} from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { ChevronRightIcon } from "@/components/Icons"
import StaticMap from "@/components/Maps/StaticMap"
import Link from "@/components/TempDesignSystem/Link"
@@ -95,25 +98,28 @@ export default async function SelectHotelPage({
return (
<main className={styles.main}>
<section className={styles.section}>
<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>
<Link
className={styles.link}
color="burgundy"
href={selectHotelMap[params.lang]}
keepSearchParams
>
{intl.formatMessage({ id: "Show map" })}
<ChevronRightIcon color="burgundy" />
</Link>
<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>
<Link
className={styles.link}
color="burgundy"
href={selectHotelMap[params.lang]}
keepSearchParams
>
{intl.formatMessage({ id: "Show map" })}
<ChevronRightIcon color="burgundy" />
</Link>
</div>
<MobileMapButtonContainer city={searchParams.city} />
<HotelFilter filters={filterList} />
</section>
<HotelCardListing hotelData={hotels} />

View File

@@ -1,12 +1,18 @@
import { getHotelData } from "@/lib/trpc/memoizedRequests"
import { serverClient } from "@/lib/trpc/server"
import { getLang } from "@/i18n/serverContext"
import { BedTypeEnum } from "@/types/components/bookingWidget/enums"
import { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput"
import { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
import { Filter } from "@/types/components/hotelReservation/selectHotel/hotelFilters"
import { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
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"
export async function fetchAvailableHotels(
input: AvailabilityInput
@@ -17,7 +23,7 @@ export async function fetchAvailableHotels(
const language = getLang()
const hotels = availableHotels.availability.map(async (hotel) => {
const hotelData = await serverClient().hotel.hotelData.get({
const hotelData = await getHotelData({
hotelId: hotel.hotelId.toString(),
language,
})
@@ -59,3 +65,33 @@ export function generateChildrenString(children: Child[]): string {
})
.join(",")}]`
}
export function getPointOfInterests(hotels: HotelData[]): PointOfInterest[] {
// TODO: this is just a quick transformation to get something there. May need rework
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,
}))
}
export function getCentralCoordinates(pointOfInterests: PointOfInterest[]) {
const centralCoordinates = pointOfInterests.reduce(
(acc, poi) => {
acc.lat += poi.coordinates.lat
acc.lng += poi.coordinates.lng
return acc
},
{ lat: 0, lng: 0 }
)
centralCoordinates.lat /= pointOfInterests.length
centralCoordinates.lng /= pointOfInterests.length
return centralCoordinates
}

View File

@@ -1,15 +1,21 @@
import { notFound } from "next/navigation"
import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
import { dt } from "@/lib/dt"
import {
getHotelData,
getLocations,
getProfileSafely,
} from "@/lib/trpc/memoizedRequests"
import { serverClient } from "@/lib/trpc/server"
import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard"
import Rooms from "@/components/HotelReservation/SelectRate/Rooms"
import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import {
generateChildrenString,
getHotelReservationQueryParams,
} from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { setLang } from "@/i18n/serverContext"
import { generateChildrenString } from "../select-hotel/utils"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import { LangParams, PageArgs } from "@/types/params"
@@ -20,6 +26,17 @@ export default async function SelectRatePage({
}: PageArgs<LangParams & { section: string }, SelectRateSearchParams>) {
setLang(params.lang)
const locations = await getLocations()
if (!locations || "error" in locations) {
return null
}
const hotel = locations.data.find(
(location) =>
"operaId" in location && location.operaId == searchParams.hotel
)
if (!hotel) {
return notFound()
}
const selectRoomParams = new URLSearchParams(searchParams)
const selectRoomParamsObject =
getHotelReservationQueryParams(selectRoomParams)
@@ -28,22 +45,27 @@ export default async function SelectRatePage({
return notFound()
}
const adults = selectRoomParamsObject.room[0].adults // TODO: Handle multiple rooms
const validFromDate =
searchParams.fromDate &&
dt(searchParams.fromDate).isAfter(dt().subtract(1, "day"))
? searchParams.fromDate
: 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-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
? generateChildrenString(selectRoomParamsObject.room[0].child)
: undefined // TODO: Handle multiple rooms
const [hotelData, roomsAvailability, packages, user] = await Promise.all([
serverClient().hotel.hotelData.get({
hotelId: searchParams.hotel,
language: params.lang,
include: ["RoomCategories"],
}),
getHotelData({ hotelId: searchParams.hotel, language: params.lang }),
serverClient().hotel.availability.rooms({
hotelId: parseInt(searchParams.hotel, 10),
roomStayStartDate: searchParams.fromDate,
roomStayEndDate: searchParams.toDate,
roomStayStartDate: validFromDate,
roomStayEndDate: validToDate,
adults,
children,
}),

View File

@@ -1,26 +1,29 @@
import InitLivePreview from "@/components/Current/LivePreview"
import { setLang } from "@/i18n/serverContext"
import "@/app/globals.css"
import "@scandic-hotels/design-system/style.css"
import type { Metadata } from "next"
import TrpcProvider from "@/lib/trpc/Provider"
import InitLivePreview from "@/components/LivePreview"
import { getIntl } from "@/i18n"
import ServerIntlProvider from "@/i18n/Provider"
import { setLang } from "@/i18n/serverContext"
import type { LangParams, LayoutArgs } from "@/types/params"
export const metadata: Metadata = {
description: "New web",
title: "Scandic Hotels",
}
export default function RootLayout({
export default async function RootLayout({
children,
params,
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
setLang(params.lang)
const { defaultLocale, locale, messages } = await getIntl()
return (
<html lang={params.lang}>
<body>
<InitLivePreview />
{children}
<ServerIntlProvider intl={{ defaultLocale, locale, messages }}>
<TrpcProvider>{children}</TrpcProvider>
</ServerIntlProvider>
</body>
</html>
)

View File

@@ -1,6 +1,14 @@
import { ContentstackLivePreview } from "@contentstack/live-preview-utils"
import { notFound } from "next/navigation"
import HotelPage from "@/components/ContentType/HotelPage"
import LoyaltyPage from "@/components/ContentType/LoyaltyPage"
import CollectionPage from "@/components/ContentType/StaticPages/CollectionPage"
import ContentPage from "@/components/ContentType/StaticPages/ContentPage"
import LoadingSpinner from "@/components/LoadingSpinner"
import { setLang } from "@/i18n/serverContext"
import {
import type {
ContentTypeParams,
LangParams,
PageArgs,
@@ -13,12 +21,32 @@ export default async function PreviewPage({
}: PageArgs<LangParams & ContentTypeParams & UIDParams, {}>) {
setLang(params.lang)
return (
<div>
<p>
Preview for {params.contentType}:{params.uid} in {params.lang} with
params <pre>{JSON.stringify(searchParams, null, 2)}</pre> goes here
</p>
</div>
)
try {
ContentstackLivePreview.setConfigFromParams(searchParams)
if (!searchParams.live_preview) {
return <LoadingSpinner />
}
switch (params.contentType) {
case "content-page":
return <ContentPage />
case "loyalty-page":
return <LoyaltyPage />
case "collection-page":
return <CollectionPage />
case "hotel-page":
return <HotelPage />
default:
console.log({ PREVIEW: params })
const type = params.contentType
console.error(`Unsupported content type given: ${type}`)
notFound()
}
} catch (error) {
// TODO: throw 500
console.error("Error in preview page")
console.error(error)
throw new Error("Something went wrong")
}
}

View File

@@ -1,6 +1,6 @@
import Footer from "@/components/Current/Footer"
import LangPopup from "@/components/Current/LangPopup"
import InitLivePreview from "@/components/Current/LivePreview"
import InitLivePreview from "@/components/LivePreview"
import SkipToMainContent from "@/components/SkipToMainContent"
import { setLang } from "@/i18n/serverContext"

View File

@@ -1,4 +1,4 @@
import ContentstackLivePreview from "@contentstack/live-preview-utils"
import { ContentstackLivePreview } from "@contentstack/live-preview-utils"
import { previewRequest } from "@/lib/graphql/previewRequest"
import { GetCurrentBlockPage } from "@/lib/graphql/Query/Current/CurrentBlockPage.graphql"

View File

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