Merged in feat/SW-1719-strikethrough-rates (pull request #2266)

Feat/SW-1719 strikethrough rates

* feat(SW-1719): Strikethrough rate if logged in on regular rate cards

* feat(SW-1719): Strikethrough rate if logged in on rate summary

* feat(SW-1719): Strikethrough rate if logged in on mobile rate summary

* feat(SW-1719): Strikethrough rate if logged in on enter details

* feat(SW-1719): Strikethrough rate support for multiple rooms

* feat(SW-1719): booking receipt fixes on confirmation page

* feat(SW-1719): improve initial total price calculation

* feat: harmonize enter details total price to use one and the same function


Approved-by: Michael Zetterberg
This commit is contained in:
Simon.Emanuelsson
2025-06-13 12:01:16 +00:00
committed by Michael Zetterberg
parent e1ede52014
commit 85acd3453d
52 changed files with 2403 additions and 1380 deletions

View File

@@ -1,8 +1,7 @@
.wrapper {
display: grid;
grid-template-rows: 0fr 7.5em;
transition: 0.5s ease-in-out;
grid-template-rows: 0fr auto;
transition: all 0.5s ease-in-out;
border-top: 1px solid var(--Base-Border-Subtle);
background: var(--Base-Surface-Primary-light-Normal);
align-content: end;
@@ -10,24 +9,22 @@
.bottomSheet {
display: grid;
grid-template-columns: 1fr auto;
padding: var(--Spacing-x2) 0 var(--Spacing-x5);
grid-template-columns: 1fr 1fr;
padding: var(--Space-x2) var(--Space-x3) var(--Space-x5);
align-items: flex-start;
transition: 0.5s ease-in-out;
max-width: var(--max-width-page);
width: 100%;
margin: 0 auto;
transition: all 0.5s ease-in-out;
width: 100vw;
}
.priceDetailsButton {
display: block;
border: none;
background: none;
border-width: 0;
background-color: transparent;
text-align: start;
transition: padding 0.5s ease-in-out;
cursor: pointer;
white-space: nowrap;
padding: 0;
display: grid;
overflow: hidden;
transition: all 0.3s ease-in-out;
}
.wrapper[data-open="true"] {
@@ -51,35 +48,47 @@
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;
}
.priceLabel {
color: var(--Text-Default);
}
.price {
color: var(--Text-Default);
&.discounted {
color: var(--Text-Accent-Primary);
}
}
.strikeThroughRate {
text-decoration: line-through;
color: var(--Text-Secondary);
}
.seeDetails {
margin-top: var(--Space-x15);
display: flex;
gap: var(--Space-x1);
align-items: center;
color: var(--Component-Button-Brand-Secondary-On-fill-Default);
}
@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;
padding: var(--Space-x2) 0 var(--Space-x7);
}
}

View File

@@ -1,33 +1,49 @@
"use client"
import { cx } from "class-variance-authority"
import { useSearchParams } from "next/navigation"
import { type PropsWithChildren, useEffect, useRef } from "react"
import { Button as ButtonRAC } from "react-aria-components"
import { useIntl } from "react-intl"
import { Button } from "@scandic-hotels/design-system/Button"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useEnterDetailsStore } from "@/stores/enter-details"
import { formId } from "@/components/HotelReservation/EnterDetails/Payment/PaymentClient"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { isBookingCodeRate } from "@/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/utils"
import { formatPrice } from "@/utils/numberFormatting"
import styles from "./bottomSheet.module.css"
export default function SummaryBottomSheet({ children }: PropsWithChildren) {
interface SummaryBottomSheetProps
extends PropsWithChildren<{
isMember: boolean
}> { }
export default function SummaryBottomSheet({
children,
isMember,
}: SummaryBottomSheetProps) {
const intl = useIntl()
const scrollY = useRef(0)
const searchParams = useSearchParams()
const errorCode = searchParams.get("errorCode")
const { isSummaryOpen, toggleSummaryOpen, totalPrice, isSubmitting } =
useEnterDetailsStore((state) => ({
isSummaryOpen: state.isSummaryOpen,
toggleSummaryOpen: state.actions.toggleSummaryOpen,
totalPrice: state.totalPrice,
isSubmitting: state.isSubmitting,
}))
const {
isSummaryOpen,
toggleSummaryOpen,
totalPrice,
isSubmitting,
rooms,
} = useEnterDetailsStore((state) => ({
isSummaryOpen: state.isSummaryOpen,
toggleSummaryOpen: state.actions.toggleSummaryOpen,
totalPrice: state.totalPrice,
isSubmitting: state.isSubmitting,
rooms: state.rooms,
}))
useEffect(() => {
if (isSummaryOpen) {
@@ -53,43 +69,77 @@ export default function SummaryBottomSheet({ children }: PropsWithChildren) {
}
}, [isSummaryOpen, errorCode])
const containsBookingCodeRate = rooms.find(
(r) => r && isBookingCodeRate(r.room.roomRate)
)
const showDiscounted = containsBookingCodeRate || isMember
return (
<div className={styles.wrapper} data-open={isSummaryOpen}>
<div className={styles.content}>{children}</div>
<div className={styles.bottomSheet}>
<button
<ButtonRAC
data-open={isSummaryOpen}
onClick={toggleSummaryOpen}
onPress={toggleSummaryOpen}
className={styles.priceDetailsButton}
>
<Caption>
{intl.formatMessage({
defaultMessage: "Total price",
})}
</Caption>
<Subtitle>
{formatPrice(
intl,
totalPrice.local.price,
totalPrice.local.currency,
totalPrice.local.additionalPrice,
totalPrice.local.additionalPriceCurrency
)}
</Subtitle>
<Caption color="baseTextHighContrast" type="underline">
{intl.formatMessage({
defaultMessage: "See details",
})}
</Caption>
</button>
<Typography variant="Body/Supporting text (caption)/smRegular">
<span className={styles.priceLabel}>
{intl.formatMessage({
defaultMessage: "Total price",
})}
</span>
</Typography>
<Typography variant="Title/Subtitle/lg">
<span
className={cx(styles.price, {
[styles.discounted]: showDiscounted,
})}
>
{formatPrice(
intl,
totalPrice.local.price,
totalPrice.local.currency,
totalPrice.local.additionalPrice,
totalPrice.local.additionalPriceCurrency
)}
</span>
</Typography>
{showDiscounted && totalPrice.local.regularPrice ? (
<Typography variant="Body/Paragraph/mdRegular">
<s className={styles.strikeThroughRate}>
{formatPrice(
intl,
totalPrice.local.regularPrice,
totalPrice.local.currency
)}
</s>
</Typography>
) : null}
<Typography variant="Body/Supporting text (caption)/smBold">
<span className={styles.seeDetails}>
<span>
{intl.formatMessage({
defaultMessage: "See details",
})}
</span>
<MaterialIcon
icon="chevron_right"
color="CurrentColor"
size={20}
/>
</span>
</Typography>
</ButtonRAC>
<Button
variant="Primary"
color="Primary"
size="Large"
type="submit"
typography="Body/Paragraph/mdBold"
isDisabled={isSubmitting}
isPending={isSubmitting}
typography="Body/Supporting text (caption)/smBold"
form={formId}
>
{intl.formatMessage({
defaultMessage: "Complete booking",

View File

@@ -51,7 +51,7 @@ export default function MobileSummary({ isMember }: SummaryProps) {
/>
)}
<SummaryBottomSheet>
<SummaryBottomSheet isMember={isMember}>
<div className={styles.wrapper}>
<SummaryUI
booking={booking}