fix: make summary sticky

This commit is contained in:
Christel Westerberg
2024-10-25 11:09:03 +02:00
parent 7954c704d9
commit 7710d3f8f9
16 changed files with 170 additions and 107 deletions

View File

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

View File

@@ -13,7 +13,10 @@ export default async function HotelSidePeek({
if (!searchParams.hotel) { if (!searchParams.hotel) {
redirect(`/${params.lang}`) redirect(`/${params.lang}`)
} }
const hotel = await getHotelData(searchParams.hotel, params.lang) const hotel = await getHotelData({
hotelId: searchParams.hotel,
language: params.lang,
})
if (!hotel?.data) { if (!hotel?.data) {
redirect(`/${params.lang}`) redirect(`/${params.lang}`)
} }

View File

@@ -20,23 +20,31 @@ export default async function SummaryPage({
const { hotel, adults, children, roomTypeCode, rateCode, fromDate, toDate } = const { hotel, adults, children, roomTypeCode, rateCode, fromDate, toDate } =
getQueryParamsForEnterDetails(selectRoomParams) getQueryParamsForEnterDetails(selectRoomParams)
const user = await getProfileSafely() const [user, hotelData, availability] = await Promise.all([
const hotelData = await getHotelData(hotel, params.lang, undefined, [HotelIncludeEnum.RoomCategories]) getProfileSafely(),
const availability = await getRoomAvailability({ getHotelData({
hotelId: hotel,
language: params.lang,
include: [HotelIncludeEnum.RoomCategories],
}),
getRoomAvailability({
hotelId: parseInt(hotel), hotelId: parseInt(hotel),
adults, adults,
children, children,
roomStayStartDate: fromDate, roomStayStartDate: fromDate,
roomStayEndDate: toDate, roomStayEndDate: toDate,
}) }),
])
if (!hotelData?.data || !hotelData?.included || !availability) { if (!hotelData?.data || !hotelData?.included || !availability) {
console.error("No hotel or availability data", hotelData, availability) console.error("No hotel or availability data", hotelData, availability)
// TODO: handle this case // TODO: handle this case
return null return null
} }
const cancellationText =
availability?.rateDefinitions.find((rate) => rate.rateCode === rateCode)
?.cancellationText ?? ""
const chosenRoom = availability.roomConfigurations.find( const chosenRoom = availability.roomConfigurations.find(
(availRoom) => availRoom.roomTypeCode === roomTypeCode (availRoom) => availRoom.roomTypeCode === roomTypeCode
) )
@@ -47,28 +55,31 @@ export default async function SummaryPage({
return null return null
} }
const cancellationText = const memberRate = chosenRoom.products.find(
availability?.rateDefinitions.find((rate) => rate.rateCode === rateCode)
?.cancellationText ?? ""
const memberPrice =
chosenRoom.products.find(
(rate) => rate.productType.member?.rateCode === rateCode (rate) => rate.productType.member?.rateCode === rateCode
)?.productType.member?.localPrice.pricePerStay ?? "0" )?.productType.member
const publicPrice = const publicRate = chosenRoom.products.find(
chosenRoom.products.find(
(rate) => rate.productType.public?.rateCode === rateCode (rate) => rate.productType.public?.rateCode === rateCode
)?.productType.public?.localPrice.pricePerStay ?? "0" )?.productType.public
const price = user ? memberPrice : publicPrice const prices = user
? {
local: memberRate?.localPrice.pricePerStay,
euro: memberRate?.requestedPrice?.pricePerStay,
}
: {
local: publicRate?.localPrice.pricePerStay,
euro: publicRate?.requestedPrice?.pricePerStay,
}
return ( return (
<Summary <Summary
isMember={!!user} isMember={!!user}
room={{ room={{
roomType: chosenRoom.roomType, roomType: chosenRoom.roomType,
price: formatNumber(parseInt(price)), localPrice: formatNumber(parseInt(prices.local ?? "0")),
euroPrice: formatNumber(parseInt(prices.euro ?? "0")),
adults, adults,
cancellationText, cancellationText,
}} }}

View File

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

@@ -30,7 +30,11 @@ export default async function StepLayout({
<div className={styles.content}> <div className={styles.content}>
<SelectedRoom /> <SelectedRoom />
{children} {children}
<aside className={styles.summary}>{summary}</aside> <aside className={styles.summaryContainer}>
<div className={styles.hider} />
<div className={styles.summary}>{summary}</div>
<div className={styles.shadow} />
</aside>
</div> </div>
{sidePeek} {sidePeek}
</main> </main>

View File

@@ -20,7 +20,7 @@ import { getIntl } from "@/i18n"
import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step" import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { LangParams, PageArgs, } from "@/types/params" import type { LangParams, PageArgs } from "@/types/params"
export function preload() { export function preload() {
void getProfileSafely() void getProfileSafely()
@@ -34,12 +34,10 @@ function isValidStep(step: string): step is StepEnum {
export default async function StepPage({ export default async function StepPage({
params, params,
searchParams, searchParams,
}: PageArgs< }: PageArgs<LangParams & { step: StepEnum }, SelectRateSearchParams>) {
LangParams & { step: StepEnum }, const { lang } = params
SelectRateSearchParams
>) {
if (!searchParams.hotel) { if (!searchParams.hotel) {
redirect(`/${params.lang}`) redirect(`/${lang}`)
} }
void getBreakfastPackages(searchParams.hotel) void getBreakfastPackages(searchParams.hotel)
@@ -64,7 +62,11 @@ export default async function StepPage({
rateCode rateCode
}) })
const hotelData = await getHotelData(hotelId, params.lang, undefined, [HotelIncludeEnum.RoomCategories]) const hotelData = await getHotelData({
hotelId,
language: lang,
include: [HotelIncludeEnum.RoomCategories],
})
const user = await getProfileSafely() const user = await getProfileSafely()
const savedCreditCards = await getCreditCardsSafely() const savedCreditCards = await getCreditCardsSafely()

View File

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

View File

@@ -43,10 +43,6 @@ export default function BedType({
reValidateMode: "onChange", reValidateMode: "onChange",
}) })
const text = intl.formatMessage<React.ReactNode>(
{ id: "<b>Included</b> (based on availability)" },
{ b: (str) => <b>{str}</b> }
)
const completeStep = useEnterDetailsStore((state) => state.completeStep) const completeStep = useEnterDetailsStore((state) => state.completeStep)
const onSubmit = useCallback( const onSubmit = useCallback(
@@ -71,7 +67,7 @@ export default function BedType({
{roomTypes.map((roomType) => { {roomTypes.map((roomType) => {
const width = const width =
roomType.size.max === roomType.size.min roomType.size.max === roomType.size.min
? roomType.size.max ? `${roomType.size.min} cm`
: `${roomType.size.min} cm - ${roomType.size.max} cm` : `${roomType.size.min} cm - ${roomType.size.max} cm`
return ( return (
<RadioCard <RadioCard
@@ -81,7 +77,6 @@ export default function BedType({
id={roomType.value} id={roomType.value}
name="bedType" name="bedType"
subtitle={width} subtitle={width}
text={text}
title={roomType.description} title={roomType.description}
value={roomType.description} value={roomType.description}
/> />

View File

@@ -17,6 +17,7 @@ import useLang from "@/hooks/useLang"
import styles from "./summary.module.css" import styles from "./summary.module.css"
import { RoomsData } from "@/types/components/hotelReservation/enterDetails/bookingData" import { RoomsData } from "@/types/components/hotelReservation/enterDetails/bookingData"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
export default function Summary({ export default function Summary({
isMember, isMember,
@@ -25,8 +26,8 @@ export default function Summary({
isMember: boolean isMember: boolean
room: RoomsData room: RoomsData
}) { }) {
const [chosenBed, setChosenBed] = useState<string | undefined>() const [chosenBed, setChosenBed] = useState<string>()
const [chosenBreakfast, setCosenBreakfast] = useState<string | undefined>() const [chosenBreakfast, setCosenBreakfast] = useState<string>()
const intl = useIntl() const intl = useIntl()
const lang = useLang() const lang = useLang()
const { fromDate, toDate, bedType, breakfast } = useEnterDetailsStore( const { fromDate, toDate, bedType, breakfast } = useEnterDetailsStore(
@@ -54,7 +55,11 @@ export default function Summary({
useEffect(() => { useEffect(() => {
setChosenBed(bedType) setChosenBed(bedType)
setCosenBreakfast(breakfast) if (breakfast === BreakfastPackageEnum.NO_BREAKFAST) {
setCosenBreakfast("No breakfast")
} else if (breakfast) {
setCosenBreakfast("Breakfast buffet")
}
}, [bedType, breakfast]) }, [bedType, breakfast])
return ( return (
@@ -75,7 +80,7 @@ export default function Summary({
<Caption color={color}> <Caption color={color}>
{intl.formatMessage( {intl.formatMessage(
{ id: "{amount} {currency}" }, { id: "{amount} {currency}" },
{ amount: room.price, currency: "SEK" } { amount: room.localPrice, currency: "SEK" }
)} )}
</Caption> </Caption>
</div> </div>
@@ -121,7 +126,9 @@ export default function Summary({
{chosenBreakfast ? ( {chosenBreakfast ? (
<div className={styles.entry}> <div className={styles.entry}>
<Body color="textHighContrast">{chosenBreakfast}</Body> <Body color="textHighContrast">
{intl.formatMessage({ id: chosenBreakfast })}
</Body>
<Caption color="uiTextMediumContrast"> <Caption color="uiTextMediumContrast">
{intl.formatMessage( {intl.formatMessage(
{ id: "{amount} {currency}" }, { id: "{amount} {currency}" },
@@ -149,14 +156,14 @@ export default function Summary({
<Body textTransform="bold"> <Body textTransform="bold">
{intl.formatMessage( {intl.formatMessage(
{ id: "{amount} {currency}" }, { id: "{amount} {currency}" },
{ amount: room.price, currency: "SEK" } // TODO: calculate total price { amount: room.localPrice, currency: "SEK" } // TODO: calculate total price
)} )}
</Body> </Body>
<Caption color="uiTextMediumContrast"> <Caption color="uiTextMediumContrast">
{intl.formatMessage({ id: "Approx." })}{" "} {intl.formatMessage({ id: "Approx." })}{" "}
{intl.formatMessage( {intl.formatMessage(
{ id: "{amount} {currency}" }, { id: "{amount} {currency}" },
{ amount: "455", currency: "EUR" } { amount: room.euroPrice, currency: "EUR" }
)} )}
</Caption> </Caption>
</div> </div>

View File

@@ -16,8 +16,8 @@ export function getQueryParamsForEnterDetails(searchParams: URLSearchParams) {
return { return {
...selectRoomParamsObject, ...selectRoomParamsObject,
adults: room[0].adults, // TODO: Handle multiple rooms adults: room[0].adults, // TODO: Handle multiple rooms
children: room[0].child?.length.toString(), // TODO: Handle multiple rooms children: room[0].child?.length.toString(), // TODO: Handle multiple rooms and children
roomTypeCode: room[0].roomtypecode, roomTypeCode: room[0].roomtype,
rateCode: room[0].ratecode, rateCode: room[0].ratecode,
} }
} }

View File

@@ -23,7 +23,7 @@ interface ListCardProps extends BaseCardProps {
interface TextCardProps extends BaseCardProps { interface TextCardProps extends BaseCardProps {
list?: never list?: never
text: React.ReactNode text?: React.ReactNode
} }
export type CardProps = ListCardProps | TextCardProps export type CardProps = ListCardProps | TextCardProps

View File

@@ -54,12 +54,17 @@ export const getUserTracking = cache(async function getMemoizedUserTracking() {
return serverClient().user.tracking() return serverClient().user.tracking()
}) })
export const getHotelData = cache(async function getMemoizedHotelData( export const getHotelData = cache(async function getMemoizedHotelData({
hotelId: string, hotelId,
language: string, language,
isCardOnlyPayment?: boolean, isCardOnlyPayment,
include,
}: {
hotelId: string
language: string
isCardOnlyPayment?: boolean
include?: HotelIncludeEnum[] include?: HotelIncludeEnum[]
) { }) {
return serverClient().hotel.hotelData.get({ return serverClient().hotel.hotelData.get({
hotelId, hotelId,
language, language,

View File

@@ -87,12 +87,12 @@ const nextConfig = {
// value: undefined, // value: undefined,
// }, // },
// { // {
// key: "fromdate", // key: "fromDate",
// type: "query", // type: "query",
// value: undefined, // value: undefined,
// }, // },
// { // {
// key: "todate", // key: "toDate",
// type: "query", // type: "query",
// value: undefined, // value: undefined,
// }, // },

View File

@@ -6,7 +6,7 @@ import { create, useStore } from "zustand"
import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema" import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema"
import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema"
import { detailsSchema } from "@/components/HotelReservation/EnterDetails/Details/schema" import { detailsSchema } from "@/components/HotelReservation/EnterDetails/Details/schema"
import { getHotelReservationQueryParams } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import type { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData" import type { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData"
import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast" import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast"
@@ -37,22 +37,6 @@ interface EnterDetailsState {
closeSidePeek: () => void closeSidePeek: () => void
} }
function getUpdatedValue<T>(
searchParams: URLSearchParams,
key: string,
defaultValue: T
): T {
const value = searchParams.get(key)
if (value === null) return defaultValue
if (typeof defaultValue === "number")
return parseInt(value, 10) as unknown as T
if (typeof defaultValue === "boolean")
return (value === "true") as unknown as T
if (defaultValue instanceof Date) return new Date(value) as unknown as T
return value as unknown as T
}
export function initEditDetailsState( export function initEditDetailsState(
currentStep: StepEnum, currentStep: StepEnum,
searchParams: ReadonlyURLSearchParams searchParams: ReadonlyURLSearchParams
@@ -62,32 +46,9 @@ export function initEditDetailsState(
? sessionStorage.getItem(SESSION_STORAGE_KEY) ? sessionStorage.getItem(SESSION_STORAGE_KEY)
: null : null
const today = new Date()
const tomorrow = new Date()
tomorrow.setDate(today.getDate() + 1)
let roomData: BookingData let roomData: BookingData
if (searchParams?.size) { if (searchParams?.size) {
roomData = getHotelReservationQueryParams(searchParams) roomData = getQueryParamsForEnterDetails(searchParams)
roomData.room = roomData.room.map((room, index) => ({
...room,
adults: getUpdatedValue(
searchParams,
`room[${index}].adults`,
room.adults
),
roomtypecode: getUpdatedValue(
searchParams,
`room[${index}].roomtypecode`,
room.roomtypecode
),
ratecode: getUpdatedValue(
searchParams,
`room[${index}].ratecode`,
room.ratecode
),
}))
} }
const defaultUserData: EnterDetailsState["userData"] = { const defaultUserData: EnterDetailsState["userData"] = {

View File

@@ -5,7 +5,7 @@ interface Child {
interface Room { interface Room {
adults: number adults: number
roomtypecode?: string roomtype?: string
ratecode?: string ratecode?: string
child?: Child[] child?: Child[]
} }
@@ -18,7 +18,8 @@ export interface BookingData {
export type RoomsData = { export type RoomsData = {
roomType: string roomType: string
price: string localPrice: string
euroPrice: string
adults: number adults: number
children?: Child[] children?: Child[]
cancellationText: string cancellationText: string

View File

@@ -7,8 +7,9 @@ export interface Child {
interface Room { interface Room {
adults: number adults: number
roomtypecode?: string roomtype?: string
ratecode?: string ratecode?: string
counterratecode?: string
child?: Child[] child?: Child[]
} }