Merged in feat/sw-2874-move-select-rate (pull request #2750)
Approved-by: Joakim Jäderberg
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
|
||||
import { RateEnum } from "@scandic-hotels/common/constants/rate"
|
||||
import { logger } from "@scandic-hotels/common/logger"
|
||||
import Body from "@scandic-hotels/design-system/Body"
|
||||
import Caption from "@scandic-hotels/design-system/Caption"
|
||||
import Chip from "@scandic-hotels/design-system/Chip"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import Image from "@scandic-hotels/design-system/Image"
|
||||
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
|
||||
import Subtitle from "@scandic-hotels/design-system/Subtitle"
|
||||
|
||||
import { useSelectRateContext } from "../../../../../../contexts/SelectRate/SelectRateContext"
|
||||
import { useIsLoggedIn } from "../../../../../../hooks/useIsLoggedIn"
|
||||
|
||||
import styles from "./selectedRoomPanel.module.css"
|
||||
|
||||
export function SelectedRoomPanel({ roomIndex }: { roomIndex: number }) {
|
||||
const intl = useIntl()
|
||||
|
||||
const isMainRoom = roomIndex === 0
|
||||
const roomNr = roomIndex + 1
|
||||
const {
|
||||
selectedRates,
|
||||
actions: { setActiveRoom },
|
||||
} = useSelectRateContext()
|
||||
const selectedRate = selectedRates.forRoom(roomIndex)
|
||||
const images = selectedRate?.roomInfo?.roomInfo?.images
|
||||
|
||||
const rateTitle = useRateTitle(selectedRate?.rate)
|
||||
|
||||
const selectedProductTitle = useSelectedProductTitle({ roomIndex })
|
||||
|
||||
if (!selectedRate) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!selectedProductTitle) {
|
||||
logger.error("Selected product is unknown")
|
||||
return null
|
||||
}
|
||||
|
||||
const showModifyButton =
|
||||
isMainRoom ||
|
||||
(!isMainRoom && selectedRates.rates.slice(0, roomNr).every((room) => room))
|
||||
|
||||
return (
|
||||
<div className={styles.selectedRoomPanel}>
|
||||
<div className={styles.content}>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "Room {roomIndex}",
|
||||
},
|
||||
{ roomIndex: roomNr }
|
||||
)}
|
||||
</Caption>
|
||||
<Subtitle className={styles.subtitle} color="uiTextHighContrast">
|
||||
{selectedRate.roomInfo.roomType}
|
||||
</Subtitle>
|
||||
<Body color="uiTextMediumContrast">{rateTitle}</Body>
|
||||
<Body color="uiTextHighContrast">{selectedProductTitle}</Body>
|
||||
</div>
|
||||
<div className={styles.imageContainer}>
|
||||
{images?.[0]?.imageSizes?.tiny ? (
|
||||
<Image
|
||||
alt={
|
||||
selectedRate.roomInfo.roomType ??
|
||||
images[0].metaData?.altText ??
|
||||
""
|
||||
}
|
||||
className={styles.img}
|
||||
height={300}
|
||||
src={images[0].imageSizes.tiny}
|
||||
width={600}
|
||||
/>
|
||||
) : null}
|
||||
{showModifyButton && (
|
||||
<div className={styles.modifyButtonContainer}>
|
||||
<Button clean onClick={() => setActiveRoom(roomIndex)}>
|
||||
<Chip size="small" variant="uiTextHighContrast">
|
||||
<MaterialIcon
|
||||
size={16}
|
||||
color="Icon/Inverted"
|
||||
icon="edit_square"
|
||||
/>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Change",
|
||||
})}
|
||||
</Chip>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function useSelectedProductTitle({ roomIndex }: { roomIndex: number }) {
|
||||
const intl = useIntl()
|
||||
const isUserLoggedIn = useIsLoggedIn()
|
||||
const {
|
||||
selectedRates,
|
||||
input: { nights },
|
||||
} = useSelectRateContext()
|
||||
|
||||
const selectedRate = selectedRates.forRoom(roomIndex)
|
||||
|
||||
const night = intl.formatMessage({
|
||||
defaultMessage: "night",
|
||||
})
|
||||
|
||||
const isMainRoom = roomIndex === 0
|
||||
|
||||
if (!selectedRate) {
|
||||
return null
|
||||
}
|
||||
|
||||
const selectedPackagesCurrency = selectedRate.roomInfo.selectedPackages.find(
|
||||
(pkg) => pkg.localPrice.currency
|
||||
)
|
||||
const selectedPackagesPrice = selectedRate.roomInfo.selectedPackages.reduce(
|
||||
(total, pkg) => total + pkg.localPrice.totalPrice,
|
||||
0
|
||||
)
|
||||
const selectedPackagesPricePerNight = Math.ceil(
|
||||
selectedPackagesPrice / nights
|
||||
)
|
||||
|
||||
if (
|
||||
isUserLoggedIn &&
|
||||
isMainRoom &&
|
||||
"member" in selectedRate &&
|
||||
selectedRate.member
|
||||
) {
|
||||
const { localPrice } = selectedRate.member
|
||||
return `${localPrice.pricePerNight + selectedPackagesPricePerNight} ${localPrice.currency} / ${night}`
|
||||
}
|
||||
|
||||
if ("public" in selectedRate && selectedRate.public) {
|
||||
const { localPrice } = selectedRate.public
|
||||
return `${localPrice.pricePerNight + selectedPackagesPricePerNight} ${localPrice.currency} / ${night}`
|
||||
}
|
||||
|
||||
if ("corporateCheque" in selectedRate) {
|
||||
const { localPrice } = selectedRate.corporateCheque
|
||||
const mainProductTitle = `${localPrice.numberOfCheques} ${CurrencyEnum.CC}`
|
||||
if (
|
||||
(localPrice.additionalPricePerStay || selectedPackagesPrice) &&
|
||||
localPrice.currency
|
||||
) {
|
||||
const packagesText = `${localPrice.additionalPricePerStay + selectedPackagesPrice} ${localPrice.currency}`
|
||||
return `${mainProductTitle} + ${packagesText}`
|
||||
}
|
||||
}
|
||||
|
||||
if ("voucher" in selectedRate) {
|
||||
const mainProductText = `${selectedRate.voucher.numberOfVouchers} ${CurrencyEnum.Voucher}`
|
||||
if (selectedPackagesPrice && selectedPackagesCurrency) {
|
||||
const packagesText = `${selectedPackagesPrice} ${selectedPackagesCurrency}`
|
||||
return `${mainProductText} + ${packagesText}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function useRateTitle(rate: RateEnum | undefined) {
|
||||
const intl = useIntl()
|
||||
const freeCancelation = intl.formatMessage({
|
||||
defaultMessage: "Free cancellation",
|
||||
})
|
||||
const nonRefundable = intl.formatMessage({
|
||||
defaultMessage: "Non-refundable",
|
||||
})
|
||||
const freeBooking = intl.formatMessage({
|
||||
defaultMessage: "Free rebooking",
|
||||
})
|
||||
const payLater = intl.formatMessage({
|
||||
defaultMessage: "Pay later",
|
||||
})
|
||||
const payNow = intl.formatMessage({
|
||||
defaultMessage: "Pay now",
|
||||
})
|
||||
|
||||
switch (rate) {
|
||||
case RateEnum.change:
|
||||
return `${freeBooking}, ${payNow}`
|
||||
case RateEnum.flex:
|
||||
return `${freeCancelation}, ${payLater}`
|
||||
case RateEnum.save:
|
||||
default:
|
||||
return `${nonRefundable}, ${payNow}`
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
.selectedRoomPanel {
|
||||
display: grid;
|
||||
grid-template-areas: "content image";
|
||||
grid-template-columns: 1fr 190px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content {
|
||||
grid-area: content;
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
border-radius: var(--Corner-radius-sm);
|
||||
display: flex;
|
||||
grid-area: image;
|
||||
}
|
||||
|
||||
.img {
|
||||
border-radius: var(--Corner-radius-sm);
|
||||
height: auto;
|
||||
max-height: 105px;
|
||||
object-fit: fill;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modifyButtonContainer {
|
||||
bottom: var(--Spacing-x1);
|
||||
position: absolute;
|
||||
right: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
div.selectedRoomPanel p.subtitle {
|
||||
padding-bottom: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.selectedRoomPanel {
|
||||
gap: var(--Spacing-x1);
|
||||
grid-template-areas: "image" "content";
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
}
|
||||
|
||||
.img {
|
||||
max-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
.img {
|
||||
max-height: 190px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { useEffect } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import useStickyPosition from "@scandic-hotels/common/hooks/useStickyPosition"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
|
||||
import Subtitle from "@scandic-hotels/design-system/Subtitle"
|
||||
|
||||
import { useSelectRateContext } from "../../../../../contexts/SelectRate/SelectRateContext"
|
||||
import { SelectedRoomPanel } from "./SelectedRoomPanel"
|
||||
import { roomSelectionPanelVariants } from "./variants"
|
||||
|
||||
import styles from "./multiRoomWrapper.module.css"
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
isMultiRoom: boolean
|
||||
roomIndex: number
|
||||
}
|
||||
export function MultiRoomWrapper({ children, isMultiRoom, roomIndex }: Props) {
|
||||
const intl = useIntl()
|
||||
|
||||
const { getTopOffset } = useStickyPosition()
|
||||
const {
|
||||
activeRoomIndex,
|
||||
selectedRates,
|
||||
actions: { setActiveRoom },
|
||||
input: { data },
|
||||
} = useSelectRateContext()
|
||||
const roomNr = roomIndex + 1
|
||||
const adultCount = data?.booking.rooms[roomIndex]?.adults || 0
|
||||
const childCount = data?.booking.rooms[roomIndex]?.childrenInRoom?.length || 0
|
||||
const isActiveRoom = activeRoomIndex === roomIndex
|
||||
|
||||
const roomMsg = intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "Room {roomIndex}",
|
||||
},
|
||||
{ roomIndex: roomNr }
|
||||
)
|
||||
|
||||
const adultsMsg = intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "{adults, plural, one {# adult} other {# adults}}",
|
||||
},
|
||||
{ adults: adultCount }
|
||||
)
|
||||
|
||||
const childrenMsg = intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "{children, plural, one {# child} other {# children}}",
|
||||
},
|
||||
{
|
||||
children: childCount,
|
||||
}
|
||||
)
|
||||
|
||||
const onlyAdultsMsg = adultsMsg
|
||||
const adultsAndChildrenMsg = [adultsMsg, childrenMsg].join(", ")
|
||||
const guestsMsg = childCount ? adultsAndChildrenMsg : onlyAdultsMsg
|
||||
|
||||
const title = [roomMsg, guestsMsg].join(", ")
|
||||
|
||||
useEffect(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const SCROLL_OFFSET = 12 + getTopOffset()
|
||||
const roomElements = document.querySelectorAll(`.${styles.roomContainer}`)
|
||||
|
||||
// If no room is active we will show all rooms collapsed, hence we want
|
||||
// to scroll to the first room.
|
||||
const selectedRoom =
|
||||
activeRoomIndex === -1 ? roomElements[0] : roomElements[activeRoomIndex]
|
||||
|
||||
if (selectedRoom) {
|
||||
const elementPosition = selectedRoom.getBoundingClientRect().top
|
||||
const offsetPosition = elementPosition + window.scrollY - SCROLL_OFFSET
|
||||
|
||||
// Setting a tiny delay for the scrolling. Without it the browser sometimes doesn't scroll up
|
||||
// after modifying the first room.
|
||||
setTimeout(() => {
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}, 5)
|
||||
}
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeRoomIndex])
|
||||
|
||||
const selectedRate = selectedRates.rateSelectedForRoom(roomIndex)
|
||||
|
||||
if (isMultiRoom) {
|
||||
const classNames = roomSelectionPanelVariants({
|
||||
active: isActiveRoom,
|
||||
selected: !!selectedRate && !isActiveRoom,
|
||||
})
|
||||
return (
|
||||
<div className={styles.roomContainer} data-multiroom="true">
|
||||
<div className={styles.header}>
|
||||
{selectedRate && !isActiveRoom ? null : (
|
||||
<Subtitle color="uiTextHighContrast">{title}</Subtitle>
|
||||
)}
|
||||
{selectedRate && isActiveRoom ? (
|
||||
<Button
|
||||
intent="text"
|
||||
onClick={() => {
|
||||
setActiveRoom("deselect")
|
||||
}}
|
||||
size="medium"
|
||||
theme="base"
|
||||
variant="icon"
|
||||
>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Close",
|
||||
})}
|
||||
<MaterialIcon
|
||||
icon="keyboard_arrow_up"
|
||||
size={20}
|
||||
color="CurrentColor"
|
||||
/>
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className={classNames}>
|
||||
<div className={styles.roomPanel}>
|
||||
<SelectedRoomPanel roomIndex={roomIndex} />
|
||||
</div>
|
||||
<div className={styles.roomSelectionPanel}>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
.roomContainer {
|
||||
background: var(--Base-Surface-Primary-light-Normal);
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
border-radius: var(--Corner-radius-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.roomPanel,
|
||||
.roomSelectionPanel {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
transition:
|
||||
opacity 0.3s ease,
|
||||
grid-template-rows 0.3s ease;
|
||||
transform-origin: bottom;
|
||||
}
|
||||
|
||||
.roomPanel > * {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.roomSelectionPanel {
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.roomSelectionPanelContainer.active .roomSelectionPanel,
|
||||
.roomSelectionPanelContainer.selected .roomPanel {
|
||||
grid-template-rows: 1fr;
|
||||
height: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.roomSelectionPanelContainer.active .roomPanel {
|
||||
padding-top: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.roomSelectionPanelContainer.selected .roomSelectionPanel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.roomContainer {
|
||||
padding: var(--Spacing-x2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
import styles from "./multiRoomWrapper.module.css"
|
||||
|
||||
export const roomSelectionPanelVariants = cva(
|
||||
styles.roomSelectionPanelContainer,
|
||||
{
|
||||
variants: {
|
||||
active: {
|
||||
true: styles.active,
|
||||
},
|
||||
selected: {
|
||||
true: styles.selected,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
active: false,
|
||||
selected: false,
|
||||
},
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user