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:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user