feat: add mobile summary

This commit is contained in:
Christel Westerberg
2024-11-07 11:26:49 +01:00
parent c8ce61c855
commit 66b2dc0c78
19 changed files with 345 additions and 116 deletions

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

@@ -4,11 +4,14 @@ import {
} 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"
@@ -62,16 +65,41 @@ export default async function SummaryPage({
}
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,
}}
/>
</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,
}}
/>
</div>
<div className={styles.shadow} />
</div>
</>
)
}

View File

@@ -11,8 +11,6 @@
.enter-details-layout__content {
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(
@@ -22,80 +20,23 @@
}
.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);
position: fixed;
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;
}
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__content {
grid-template-columns: 1fr 340px;
grid-template-rows: auto 1fr;
}
.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

@@ -30,11 +30,7 @@ export default async function StepLayout({
{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" />
</aside>
<aside className={"enter-details-layout__summaryContainer"}>{summary}</aside>
</div>
</main>
</EnterDetailsProvider>

View File

@@ -45,7 +45,7 @@
.iconWrapper {
position: relative;
top: var(--Spacing-x1);
z-index: 2;
z-index: 1;
}
.circle {

View File

@@ -38,7 +38,7 @@
.iconWrapper {
position: relative;
top: var(--Spacing-x1);
z-index: 2;
z-index: 1;
}
.circle {

View File

@@ -0,0 +1,49 @@
.wrapper {
display: grid;
grid-template-rows: 0fr 7.5em;
transition: 0.5s ease-in-out;
border-top: 1px solid var(--Base-Border-Subtle);
background: var(--Base-Surface-Primary-light-Normal);
align-content: end;
}
.bottomSheet {
display: grid;
grid-template-columns: 1fr auto;
padding: var(--Spacing-x2) var(--Spacing-x3) var(--Spacing-x5)
var(--Spacing-x3);
justify-content: space-between;
width: auto;
align-items: flex-start;
transition: 0.5s ease-in-out;
}
.priceDetails {
display: block;
border: none;
background: none;
text-align: start;
opacity: 1;
transition:
opacity 0.5s ease-in-out,
padding 0.5s ease-in-out;
}
.wrapper[data-open="true"] {
grid-template-rows: 1fr 7.5em;
}
.wrapper[data-open="true"] .bottomSheet {
grid-template-columns: 0fr 1fr;
}
.wrapper[data-open="true"] .priceDetails {
opacity: 0;
padding: 0;
}
.content,
.priceDetails {
overflow: hidden;
}

View File

@@ -0,0 +1,55 @@
"use client"
import { PropsWithChildren, useState } from "react"
import { useIntl } from "react-intl"
import { useEnterDetailsStore } from "@/stores/enter-details"
import Button from "@/components/TempDesignSystem/Button"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { formatNumber } from "@/utils/format"
import styles from "./bottomSheet.module.css"
export function SummaryBottomSheet({ children }: PropsWithChildren) {
const intl = useIntl()
const { isSummaryOpen, toggleSummaryOpen, totalPrice } = useEnterDetailsStore(
(state) => ({
isSummaryOpen: state.isSummaryOpen,
toggleSummaryOpen: state.toggleSummaryOpen,
totalPrice: state.totalPrice,
})
)
return (
<div className={styles.wrapper} data-open={isSummaryOpen}>
<div className={styles.content}>{children}</div>
<div className={styles.bottomSheet}>
<button
data-open={isSummaryOpen}
onClick={toggleSummaryOpen}
className={styles.priceDetails}
>
<Caption>{intl.formatMessage({ id: "Total price" })}:</Caption>
<Subtitle>
{intl.formatMessage(
{ id: "{amount} {currency}" },
{
amount: formatNumber(totalPrice.local.price),
currency: totalPrice.local.currency,
}
)}
</Subtitle>
<Caption color="baseTextHighContrast" type="underline">
{intl.formatMessage({ id: "See details" })}
</Caption>
</button>
<Button intent="primary" size="large" type="submit">
{intl.formatMessage({ id: "Complete booking" })}
</Button>
</div>
</div>
)
}

View File

@@ -6,7 +6,7 @@ import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import { useEnterDetailsStore } from "@/stores/enter-details"
import { ArrowRightIcon } from "@/components/Icons"
import { ArrowRightIcon, CloseIcon } from "@/components/Icons"
import Divider from "@/components/TempDesignSystem/Divider"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
@@ -36,20 +36,25 @@ export default function Summary({
const [chosenBreakfast, setChosenBreakfast] = useState<
BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST
>()
const [totalPrice, setTotalPrice] = useState({
local: parsePrice(room.localPrice.price),
euro: parsePrice(room.euroPrice.price),
})
const intl = useIntl()
const lang = useLang()
const { fromDate, toDate, bedType, breakfast } = useEnterDetailsStore(
(state) => ({
fromDate: state.roomData.fromDate,
toDate: state.roomData.toDate,
bedType: state.userData.bedType,
breakfast: state.userData.breakfast,
})
)
const {
fromDate,
toDate,
bedType,
breakfast,
setTotalPrice,
totalPrice,
toggleSummaryOpen,
} = useEnterDetailsStore((state) => ({
fromDate: state.roomData.fromDate,
toDate: state.roomData.toDate,
bedType: state.userData.bedType,
breakfast: state.userData.breakfast,
toggleSummaryOpen: state.toggleSummaryOpen,
setTotalPrice: state.setTotalPrice,
totalPrice: state.totalPrice,
}))
const diff = dt(toDate).diff(fromDate, "days")
@@ -70,21 +75,48 @@ export default function Summary({
setChosenBreakfast(breakfast)
if (breakfast === BreakfastPackageEnum.NO_BREAKFAST) {
setTotalPrice({
local: parsePrice(room.localPrice.price),
euro: parsePrice(room.euroPrice.price),
local: {
price: parsePrice(room.localPrice.price),
currency: room.localPrice.currency!,
},
euro: {
price: parsePrice(room.euroPrice.price),
currency: room.euroPrice.currency!,
},
})
} else {
setTotalPrice({
local:
parsePrice(room.localPrice.price) +
parsePrice(breakfast.localPrice.totalPrice),
euro:
parsePrice(room.euroPrice.price) +
parsePrice(breakfast.requestedPrice.totalPrice),
local: {
price:
parsePrice(room.localPrice.price) +
parsePrice(breakfast.localPrice.totalPrice),
currency: room.localPrice.currency!,
},
euro: {
price:
parsePrice(room.euroPrice.price) +
parsePrice(breakfast.requestedPrice.totalPrice),
currency: room.euroPrice.currency!,
},
})
}
}
}, [bedType, breakfast, room.localPrice, room.euroPrice])
}, [bedType, breakfast, room.localPrice, room.euroPrice, setTotalPrice])
useEffect(() => {
setTotalPrice({
local: {
price: parsePrice(room.localPrice.price),
currency: room.localPrice.currency!,
},
euro: {
price: parsePrice(room.euroPrice.price),
currency: room.euroPrice.currency!,
},
})
}, [room.localPrice, room.euroPrice, setTotalPrice])
const showToggleButton = true
return (
<section className={styles.summary}>
@@ -95,6 +127,7 @@ export default function Summary({
<ArrowRightIcon color="peach80" height={15} width={15} />
{dt(toDate).locale(lang).format("ddd, D MMM")} ({nights})
</Body>
{showToggleButton ? <CloseIcon onClick={toggleSummaryOpen} /> : null}
</header>
<Divider color="primaryLightSubtle" />
<div className={styles.addOns}>
@@ -203,8 +236,8 @@ export default function Summary({
{intl.formatMessage(
{ id: "{amount} {currency}" },
{
amount: intl.formatNumber(totalPrice.local),
currency: room.localPrice.currency,
amount: intl.formatNumber(totalPrice.local.price),
currency: totalPrice.local.currency,
}
)}
</Body>
@@ -213,14 +246,14 @@ export default function Summary({
{intl.formatMessage(
{ id: "{amount} {currency}" },
{
amount: intl.formatNumber(totalPrice.euro),
currency: room.euroPrice.currency,
amount: intl.formatNumber(totalPrice.euro.price),
currency: totalPrice.euro.currency,
}
)}
</Caption>
</div>
</div>
<Divider color="primaryLightSubtle" />
<Divider className={styles.bottomDivider} color="primaryLightSubtle" />
</div>
</section>
)

View File

@@ -38,3 +38,13 @@
flex-direction: column;
gap: var(--Spacing-x2);
}
.bottomDivider {
display: none;
}
@media screen and (min-width: 1367px) {
.bottomDivider {
display: block;
}
}

View File

@@ -25,6 +25,15 @@ p.caption {
text-decoration: var(--typography-Caption-Labels-textDecoration);
}
.underline {
font-family: var(--typography-Caption-Underline-fontFamily);
font-size: var(--typography-Caption-Underline-fontSize);
font-weight: var(--typography-Caption-Underline-fontWeight);
letter-spacing: var(--typography-Caption-Underline-letterSpacing);
line-height: var(--typography-Caption-Underline-lineHeight);
text-decoration: underline; /* var(--typography-Caption-Underline-textDecoration) /* Commented till figma values are fixed to underline instead of "underline" */
}
.regular {
font-family: var(--typography-Caption-Regular-fontFamily);
font-size: var(--typography-Caption-Regular-fontSize);
@@ -58,6 +67,10 @@ p.caption {
color: var(--Base-Text-Medium-contrast);
}
.baseTextHighContrast {
color: var(--Base-Text-High-contrast);
}
.red {
color: var(--Scandic-Brand-Scandic-Red);
}

View File

@@ -8,6 +8,7 @@ const config = {
regular: styles.regular,
bold: styles.bold,
label: styles.labels,
underline: styles.underline,
},
color: {
baseTextAccent: styles.baseTextAccent,
@@ -22,6 +23,7 @@ const config = {
uiTextMediumContrast: styles.uiTextMediumContrast,
uiTextPlaceholder: styles.uiTextPlaceholder,
disabled: styles.disabled,
baseTextHighContrast: styles.baseTextHighContrast,
},
textTransform: {
uppercase: styles.uppercase,
@@ -48,6 +50,7 @@ const fontOnlyConfig = {
regular: styles.regular,
bold: styles.bold,
label: styles.labels,
underline: styles.underline,
},
textTransform: {
uppercase: styles.uppercase,

View File

@@ -53,6 +53,7 @@
"Bus terminal": "Busstation",
"Business": "Forretning",
"Cancel": "Afbestille",
"Change room": "Skift værelse",
"Check in": "Check ind",
"Check out": "Check ud",
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Tjek de kreditkort, der er gemt på din profil. Betal med et gemt kort, når du er logget ind for en mere jævn weboplevelse.",
@@ -71,6 +72,7 @@
"Code / Voucher": "Bookingkoder / voucher",
"Coming up": "Er lige om hjørnet",
"Compare all levels": "Sammenlign alle niveauer",
"Complete booking": "Fuldfør bookingen",
"Complete booking & go to payment": "Udfyld booking & gå til betaling",
"Complete the booking": "Fuldfør bookingen",
"Contact information": "Kontaktoplysninger",
@@ -285,6 +287,7 @@
"Search": "Søge",
"See all FAQ": "Se alle FAQ",
"See all photos": "Se alle billeder",
"See details": "Se detaljer",
"See hotel details": "Se hoteloplysninger",
"See less FAQ": "Se mindre FAQ",
"See on map": "Se på kort",

View File

@@ -53,6 +53,7 @@
"Bus terminal": "Busbahnhof",
"Business": "Geschäft",
"Cancel": "Stornieren",
"Change room": "Zimmer ändern",
"Check in": "Einchecken",
"Check out": "Auschecken",
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Sehen Sie sich die in Ihrem Profil gespeicherten Kreditkarten an. Bezahlen Sie mit einer gespeicherten Karte, wenn Sie angemeldet sind, für ein reibungsloseres Web-Erlebnis.",
@@ -71,6 +72,7 @@
"Code / Voucher": "Buchungscodes / Gutscheine",
"Coming up": "Demnächst",
"Compare all levels": "Vergleichen Sie alle Levels",
"Complete booking": "Buchung abschließen",
"Complete booking & go to payment": "Buchung abschließen & zur Bezahlung gehen",
"Complete the booking": "Buchung abschließen",
"Contact information": "Kontaktinformationen",
@@ -284,6 +286,7 @@
"Search": "Suchen",
"See all FAQ": "Siehe alle FAQ",
"See all photos": "Alle Fotos ansehen",
"See details": "Siehe Einzelheiten",
"See hotel details": "Hotelinformationen ansehen",
"See less FAQ": "Weniger anzeigen FAQ",
"See on map": "Karte ansehen",

View File

@@ -80,6 +80,7 @@
"Code / Voucher": "Code / Voucher",
"Coming up": "Coming up",
"Compare all levels": "Compare all levels",
"Complete booking": "Complete booking",
"Complete booking & go to payment": "Complete booking & go to payment",
"Complete the booking": "Complete the booking",
"Contact information": "Contact information",
@@ -314,6 +315,7 @@
"Search": "Search",
"See all FAQ": "See all FAQ",
"See all photos": "See all photos",
"See details": "See details",
"See hotel details": "See hotel details",
"See less FAQ": "See less FAQ",
"See on map": "See on map",

View File

@@ -53,6 +53,7 @@
"Bus terminal": "Bussiasema",
"Business": "Business",
"Cancel": "Peruuttaa",
"Change room": "Vaihda huonetta",
"Check in": "Sisäänkirjautuminen",
"Check out": "Uloskirjautuminen",
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Tarkista profiiliisi tallennetut luottokortit. Maksa tallennetulla kortilla kirjautuneena, jotta verkkokokemus on sujuvampi.",
@@ -71,6 +72,7 @@
"Code / Voucher": "Varauskoodit / kupongit",
"Coming up": "Tulossa",
"Compare all levels": "Vertaa kaikkia tasoja",
"Complete booking": "Täydennä varaus",
"Complete booking & go to payment": "Täydennä varaus & siirry maksamaan",
"Complete the booking": "Täydennä varaus",
"Contact information": "Yhteystiedot",
@@ -286,6 +288,7 @@
"Search": "Haku",
"See all FAQ": "Katso kaikki UKK",
"See all photos": "Katso kaikki kuvat",
"See details": "Katso tiedot",
"See hotel details": "Katso hotellin tiedot",
"See less FAQ": "Katso vähemmän UKK",
"See on map": "Näytä kartalla",

View File

@@ -53,6 +53,7 @@
"Bus terminal": "Bussterminal",
"Business": "Forretnings",
"Cancel": "Avbryt",
"Change room": "Endre rom",
"Check in": "Sjekk inn",
"Check out": "Sjekk ut",
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Sjekk ut kredittkortene som er lagret på profilen din. Betal med et lagret kort når du er pålogget for en jevnere nettopplevelse.",
@@ -71,6 +72,7 @@
"Code / Voucher": "Bestillingskoder / kuponger",
"Coming up": "Kommer opp",
"Compare all levels": "Sammenlign alle nivåer",
"Complete booking": "Fullfør reservasjonen",
"Complete booking & go to payment": "Fullfør bestilling & gå til betaling",
"Complete the booking": "Fullfør reservasjonen",
"Contact information": "Kontaktinformasjon",
@@ -283,6 +285,7 @@
"Search": "Søk",
"See all FAQ": "Se alle FAQ",
"See all photos": "Se alle bilder",
"See details": "Se detaljer",
"See hotel details": "Se hotellinformasjon",
"See less FAQ": "Se mindre FAQ",
"See on map": "Se på kart",

View File

@@ -53,6 +53,7 @@
"Bus terminal": "Bussterminal",
"Business": "Business",
"Cancel": "Avbryt",
"Change room": "Ändra rum",
"Check in": "Checka in",
"Check out": "Checka ut",
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Kolla in kreditkorten som sparats i din profil. Betala med ett sparat kort när du är inloggad för en smidigare webbupplevelse.",
@@ -71,6 +72,7 @@
"Code / Voucher": "Bokningskoder / kuponger",
"Coming up": "Kommer härnäst",
"Compare all levels": "Jämför alla nivåer",
"Complete booking": "Slutför bokning",
"Complete booking & go to payment": "Fullför bokning & gå till betalning",
"Complete the booking": "Slutför bokningen",
"Contact information": "Kontaktinformation",
@@ -283,6 +285,7 @@
"Search": "Sök",
"See all FAQ": "Se alla FAQ",
"See all photos": "Se alla foton",
"See details": "Se detaljer",
"See hotel details": "Se hotellinformation",
"See less FAQ": "See färre FAQ",
"See on map": "Se på karta",

View File

@@ -23,6 +23,11 @@ import { BreakfastPackageEnum } from "@/types/enums/breakfast"
const SESSION_STORAGE_KEY = "enterDetails"
type TotalPrice = {
local: { price: number; currency: string }
euro: { price: number; currency: string }
}
interface EnterDetailsState {
userData: {
bedType: BedTypeSchema | undefined
@@ -32,6 +37,8 @@ interface EnterDetailsState {
steps: StepEnum[]
selectRateUrl: string
currentStep: StepEnum
totalPrice: TotalPrice
isSummaryOpen: boolean
isValid: Record<StepEnum, boolean>
completeStep: (updatedData: Partial<EnterDetailsState["userData"]>) => void
navigate: (
@@ -42,6 +49,8 @@ interface EnterDetailsState {
>
) => void
setCurrentStep: (step: StepEnum) => void
toggleSummaryOpen: () => void
setTotalPrice: (totalPrice: TotalPrice) => void
}
export function initEditDetailsState(
@@ -129,6 +138,11 @@ export function initEditDetailsState(
roomData,
selectRateUrl,
steps: Object.values(StepEnum),
totalPrice: {
local: { price: 0, currency: "" },
euro: { price: 0, currency: "" },
},
isSummaryOpen: false,
setCurrentStep: (step) => set({ currentStep: step }),
navigate: (step, updatedData) =>
set(
@@ -166,6 +180,8 @@ export function initEditDetailsState(
get().navigate(nextStep, updatedData)
})
),
toggleSummaryOpen: () => set({ isSummaryOpen: !get().isSummaryOpen }),
setTotalPrice: (totalPrice) => set({ totalPrice: totalPrice }),
}))
}