Merged in feat/SW-1055-Accordion-for-summary-bar-in-mobile-on-Select-Rate (pull request #1283)

Feat/SW-1055 Accordion for summary bar in mobile on Select Rate

* feat(SW-1055) created mobile summary for select rate

* feat(SW-1055) Added summary for mobile (accordion)


Approved-by: Tobias Johansson
This commit is contained in:
Pontus Dreij
2025-02-10 15:11:31 +00:00
parent 5a0edc9187
commit c863294919
20 changed files with 405 additions and 89 deletions

View File

@@ -0,0 +1,149 @@
import { useEffect, useRef } from "react"
import { useIntl } from "react-intl"
import { useRateSelectionStore } from "@/stores/select-rate/rate-selection"
import SummaryUI from "@/components/HotelReservation/EnterDetails/Summary/UI"
import Button from "@/components/TempDesignSystem/Button"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { formatPrice } from "@/utils/numberFormatting"
import styles from "./mobileSummary.module.css"
import type { MobileSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
export default function MobileSummary({
totalPriceToShow,
isAllRoomsSelected,
booking,
isUserLoggedIn,
vat,
roomsAvailability,
}: MobileSummaryProps) {
const intl = useIntl()
const scrollY = useRef(0)
const {
guestsInRooms,
isSummaryOpen,
getSelectedRateSummary,
toggleSummaryOpen,
togglePriceDetailsModalOpen,
} = useRateSelectionStore()
const selectedRateSummary = getSelectedRateSummary()
const rooms = selectedRateSummary.map((room, index) => ({
adults: guestsInRooms[index].adults,
childrenInRoom: guestsInRooms[index].children,
roomType: room.roomType,
roomPrice: {
perNight: {
local: {
price: room.public.localPrice.pricePerNight,
currency: room.public.localPrice.currency,
},
requested: undefined,
},
perStay: {
local: {
price: room.public.localPrice.pricePerStay,
currency: room.public.localPrice.currency,
},
requested: undefined,
},
currency: room.public.localPrice.currency,
},
bedType: undefined,
breakfast: undefined,
guest: undefined,
roomRate: {
...room.public,
memberRate: room.member,
publicRate: room.public,
},
rateDetails: roomsAvailability.rateDefinitions.find(
(rate) => rate.rateCode === room.public.rateCode
)?.generalTerms,
cancellationText:
roomsAvailability.rateDefinitions.find(
(rate) => rate.rateCode === room.public.rateCode
)?.cancellationText ?? "",
}))
useEffect(() => {
if (isSummaryOpen) {
scrollY.current = window.scrollY
document.body.style.position = "fixed"
document.body.style.top = `-${scrollY.current}px`
document.body.style.width = "100%"
} else {
document.body.style.position = ""
document.body.style.top = ""
document.body.style.width = ""
window.scrollTo({
top: scrollY.current,
left: 0,
behavior: "instant",
})
}
return () => {
document.body.style.position = ""
document.body.style.top = ""
}
}, [isSummaryOpen])
return (
<div className={styles.wrapper} data-open={isSummaryOpen}>
<div className={styles.content}>
<div className={styles.summaryAccordion}>
<SummaryUI
booking={booking}
rooms={rooms}
isMember={isUserLoggedIn}
packages={undefined}
totalPrice={totalPriceToShow}
vat={vat}
breakfastIncluded={false}
toggleSummaryOpen={toggleSummaryOpen}
togglePriceDetailsModalOpen={togglePriceDetailsModalOpen}
/>
</div>
</div>
<div className={styles.bottomSheet}>
<button
data-open={isSummaryOpen}
onClick={(e) => {
e.preventDefault()
toggleSummaryOpen()
}}
className={styles.priceDetailsButton}
>
<Caption>{intl.formatMessage({ id: "Total price" })}</Caption>
<Subtitle>
{formatPrice(
intl,
totalPriceToShow.local.price,
totalPriceToShow.local.currency
)}
</Subtitle>
<Caption color="baseTextHighContrast" type="underline">
{intl.formatMessage({ id: "See details" })}
</Caption>
</button>
<Button
intent="primary"
theme="base"
size="large"
type="submit"
fullWidth
disabled={!isAllRoomsSelected}
>
{intl.formatMessage({ id: "Continue" })}
</Button>
</div>
</div>
)
}

View File

@@ -0,0 +1,92 @@
.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 1fr;
padding: var(--Spacing-x2) 0 var(--Spacing-x5);
align-items: flex-start;
transition: 0.5s ease-in-out;
max-width: var(--max-width-page);
width: 100%;
margin: 0 auto;
}
.priceDetailsButton {
display: block;
border: none;
background: none;
text-align: start;
transition: padding 0.5s ease-in-out;
cursor: pointer;
white-space: nowrap;
padding: 0;
}
.wrapper[data-open="true"] {
grid-template-rows: 1fr 7.5em;
}
.wrapper[data-open="true"] .bottomSheet {
grid-template-columns: 0fr auto;
}
.wrapper[data-open="true"] .priceDetailsButton {
animation: fadeOut 0.3s ease-out;
opacity: 0;
padding: 0;
}
.wrapper[data-open="false"] .priceDetailsButton {
animation: fadeIn 0.8s ease-in;
opacity: 1;
}
.priceDetailsButton {
overflow: hidden;
}
.content {
max-height: 50dvh;
overflow-y: auto;
}
.summaryAccordion {
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;
}
@media screen and (min-width: 768px) {
.bottomSheet {
padding: var(--Spacing-x2) 0 var(--Spacing-x7);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}

View File

@@ -11,10 +11,10 @@ import SignupPromoMobile from "@/components/HotelReservation/SignupPromo/Mobile"
import Button from "@/components/TempDesignSystem/Button"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { formatPrice } from "@/utils/numberFormatting"
import MobileSummary from "./MobileSummary"
import { calculateTotalPrice } from "./utils"
import styles from "./rateSummary.module.css"
@@ -26,13 +26,16 @@ export default function RateSummary({
isUserLoggedIn,
packages,
roomsAvailability,
rooms,
booking,
vat,
}: RateSummaryProps) {
const intl = useIntl()
const [isVisible, setIsVisible] = useState(false)
const { getSelectedRateSummary } = useRateSelectionStore()
const { rooms } = booking
useEffect(() => {
const timer = setTimeout(() => setIsVisible(true), 0)
return () => clearTimeout(timer)
@@ -88,7 +91,6 @@ export default function RateSummary({
return (
<div className={styles.summary} data-visible={isVisible}>
{showMemberDiscountBanner && <SignupPromoMobile />}
<div className={styles.content}>
<div className={styles.summaryText}>
{selectedRateSummary.map((room, index) => (
@@ -161,54 +163,48 @@ export default function RateSummary({
>
{formatPrice(
intl,
totalPriceToShow.localPrice.price,
totalPriceToShow.localPrice.currency
totalPriceToShow.local.price,
totalPriceToShow.local.currency
)}
</Subtitle>
{totalPriceToShow?.requestedPrice ? (
{totalPriceToShow?.requested ? (
<Body color="uiTextMediumContrast">
{intl.formatMessage(
{ id: "Approx. {value}" },
{
value: formatPrice(
intl,
totalPriceToShow.requestedPrice.price,
totalPriceToShow.requestedPrice.currency
totalPriceToShow.requested.price,
totalPriceToShow.requested.currency
),
}
)}
</Body>
) : null}
</div>
<div className={styles.summaryPriceTextMobile}>
<Caption color="uiTextHighContrast">
{intl.formatMessage({ id: "Total price" })}
</Caption>
<Subtitle color={isUserLoggedIn ? "red" : "uiTextHighContrast"}>
{formatPrice(
intl,
totalPriceToShow.localPrice.price,
totalPriceToShow.localPrice.currency
)}
</Subtitle>
<Footnote
color="uiTextMediumContrast"
className={styles.summaryPriceTextMobile}
>
{summaryPriceText}
</Footnote>
</div>
<Button
type="submit"
theme="base"
className={styles.continueButton}
disabled={!isAllRoomsSelected}
>
{intl.formatMessage({ id: "Continue" })}
</Button>
</div>
<Button
intent="primary"
theme="base"
type="submit"
className={styles.continueButton}
disabled={!isAllRoomsSelected}
>
{intl.formatMessage({ id: "Continue" })}
</Button>
</div>
</div>
<div className={styles.mobileSummary}>
{showMemberDiscountBanner ? <SignupPromoMobile /> : null}
<MobileSummary
totalPriceToShow={totalPriceToShow}
isAllRoomsSelected={isAllRoomsSelected}
booking={booking}
isUserLoggedIn={isUserLoggedIn}
vat={vat}
roomsAvailability={roomsAvailability}
/>
</div>
</div>
)
}

View File

@@ -5,9 +5,7 @@
left: 0;
right: 0;
background-color: var(--Base-Surface-Primary-light-Normal);
padding: 0 0 var(--Spacing-x5);
align-items: center;
border-top: 1px solid var(--Base-Border-Subtle);
transition: bottom 300ms ease-in-out;
}
@@ -15,10 +13,10 @@
width: 100%;
max-width: var(--max-width-page);
margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
display: none;
}
.summary[data-visible="true"] {
@@ -70,11 +68,17 @@
white-space: nowrap;
}
@media (min-width: 768px) {
.mobileSummary {
display: block;
}
@media (min-width: 1367px) {
.summary {
padding: var(--Spacing-x3) 0 var(--Spacing-x5);
border-top: 1px solid var(--Base-Border-Subtle);
}
.content {
display: flex;
flex-direction: row;
}
.petInfo,
@@ -98,4 +102,7 @@
padding: 0;
align-items: center;
}
.mobileSummary {
display: none;
}
}

View File

@@ -3,18 +3,14 @@ import {
RoomPackageCodeEnum,
} from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate"
interface TotalPrice {
localPrice: { currency: string; price: number }
requestedPrice?: { currency: string; price: number }
}
import type { Price } from "@/types/stores/enter-details"
export const calculateTotalPrice = (
selectedRateSummary: Rate[],
isUserLoggedIn: boolean,
petRoomPackage: RoomPackage | undefined
) => {
return selectedRateSummary.reduce<TotalPrice>(
return selectedRateSummary.reduce<Price>(
(total, room) => {
const priceToUse =
isUserLoggedIn && room.member ? room.member : room.public
@@ -29,18 +25,18 @@ export const calculateTotalPrice = (
: 0
return {
localPrice: {
local: {
currency: priceToUse.localPrice.currency,
price:
total.localPrice.price +
total.local.price +
priceToUse.localPrice.pricePerStay +
petRoomPrice,
},
requestedPrice: priceToUse.requestedPrice
requested: priceToUse.requestedPrice
? {
currency: priceToUse.requestedPrice.currency,
price:
(total.requestedPrice?.price ?? 0) +
(total.requested?.price ?? 0) +
priceToUse.requestedPrice.pricePerStay +
petRoomPrice,
}
@@ -48,11 +44,11 @@ export const calculateTotalPrice = (
}
},
{
localPrice: {
local: {
currency: selectedRateSummary[0].public.localPrice.currency,
price: 0,
},
requestedPrice: undefined,
requested: undefined,
}
)
}