Merged in feat/SW-2398-ui-update-for-booking-codes (pull request #1862)
feat: SW-2398 UI updates booking codes * feat: SW-2398 UI updates booking codes * feat: SW-2398 Rate cards UI changes * feat: SW-2398 Optimized css with vars and chip code * feat: SW-2398 Optimized code as review comments * feat: SW-2398 Optimized code * feat: SW-2398 Optimized code and mobile UX * feat: SW-2398 Optimized code * feat: SW-2398 Fixed UI * feat: SW-2398 Updated animation Approved-by: Erik Tiekstra
This commit is contained in:
@@ -3,6 +3,122 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.bookingCodeFilterSelect {
|
||||
min-width: 200px;
|
||||
.dialog {
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
background-color: var(--Surface-Primary-Default);
|
||||
box-shadow: var(--popup-box-shadow);
|
||||
max-width: 340px;
|
||||
}
|
||||
|
||||
.radioGroup {
|
||||
display: grid;
|
||||
gap: var(--Space-x1);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.radio {
|
||||
padding: var(--Space-x1);
|
||||
}
|
||||
|
||||
.radio[data-hovered] {
|
||||
cursor: pointer;
|
||||
}
|
||||
.radio[data-focus-visible]::before {
|
||||
outline: 1px auto var(--Border-Interactive-Focus);
|
||||
}
|
||||
|
||||
.radio {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.radio::before {
|
||||
flex-shrink: 0;
|
||||
content: "";
|
||||
margin-right: var(--Space-x15);
|
||||
background-color: var(--Surface-UI-Fill-Default);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
box-shadow: inset 0 0 0 2px var(--Base-Border-Normal);
|
||||
}
|
||||
|
||||
.radio[data-selected]::before {
|
||||
box-shadow: inset 0 0 0 8px var(--Surface-UI-Fill-Active);
|
||||
}
|
||||
|
||||
.modalOverlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: var(--Overlay-40);
|
||||
|
||||
&[data-entering] {
|
||||
animation: overlay-fade 200ms;
|
||||
}
|
||||
|
||||
&[data-exiting] {
|
||||
animation: overlay-fade 150ms reverse ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: var(--Space-x2) var(--Space-x05);
|
||||
border-radius: var(--Corner-radius-md) var(--Corner-radius-md) 0 0;
|
||||
background-color: var(--Surface-Primary-Default);
|
||||
box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&[data-entering] {
|
||||
animation: modal-anim 200ms;
|
||||
}
|
||||
|
||||
&[data-exiting] {
|
||||
animation: modal-anim 150ms reverse ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
.modalDialog {
|
||||
display: grid;
|
||||
gap: var(--Space-x2);
|
||||
padding: 0 var(--Space-x1);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 var(--Space-x1);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.radioGroup {
|
||||
padding: var(--Space-x1);
|
||||
}
|
||||
|
||||
.modalOverlay {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes overlay-fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes modal-anim {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,39 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
Popover,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
} from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
import { useMediaQuery } from "usehooks-ts"
|
||||
|
||||
import { ChipButton } from "@scandic-hotels/design-system/ChipButton"
|
||||
import { IconButton } from "@scandic-hotels/design-system/IconButton"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
import { useRatesStore } from "@/stores/select-rate"
|
||||
|
||||
import Select from "@/components/TempDesignSystem/Select"
|
||||
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "./bookingCodeFilter.module.css"
|
||||
|
||||
import type { Key } from "react"
|
||||
|
||||
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||
|
||||
export default function BookingCodeFilter() {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const displayAsPopover = useMediaQuery("(min-width: 768px)")
|
||||
const utils = trpc.useUtils()
|
||||
const {
|
||||
actions: { appendRegularRates, selectFilter },
|
||||
@@ -36,12 +51,6 @@ export default function BookingCodeFilter() {
|
||||
}),
|
||||
value: BookingCodeFilterEnum.Discounted,
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "Full-priced rooms",
|
||||
}),
|
||||
value: BookingCodeFilterEnum.Regular,
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "All rates",
|
||||
@@ -50,23 +59,22 @@ export default function BookingCodeFilter() {
|
||||
},
|
||||
]
|
||||
|
||||
async function handleChangeFilter(selectedFilter: Key) {
|
||||
async function updateFilterValue(selectedFilter: string) {
|
||||
selectFilter(selectedFilter as BookingCodeFilterEnum)
|
||||
const room = await utils.hotel.availability.selectRate.room.fetch({
|
||||
booking: {
|
||||
...booking,
|
||||
room: {
|
||||
...bookingRoom,
|
||||
bookingCode:
|
||||
selectedFilter === BookingCodeFilterEnum.Discounted
|
||||
? booking.bookingCode
|
||||
: undefined,
|
||||
packages: selectedPackages.map((pkg) => pkg.code),
|
||||
if (selectedFilter === BookingCodeFilterEnum.All) {
|
||||
const room = await utils.hotel.availability.selectRate.room.fetch({
|
||||
booking: {
|
||||
...booking,
|
||||
room: {
|
||||
...bookingRoom,
|
||||
bookingCode: undefined,
|
||||
packages: selectedPackages.map((pkg) => pkg.code),
|
||||
},
|
||||
},
|
||||
},
|
||||
lang,
|
||||
})
|
||||
appendRegularRates(room?.roomConfigurations)
|
||||
lang,
|
||||
})
|
||||
appendRegularRates(room?.roomConfigurations)
|
||||
}
|
||||
}
|
||||
|
||||
const hideFilter = rooms.some((room) =>
|
||||
@@ -92,18 +100,121 @@ export default function BookingCodeFilter() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.bookingCodeFilter}>
|
||||
<Select
|
||||
aria-label={intl.formatMessage({
|
||||
defaultMessage: "Booking Code filter",
|
||||
})}
|
||||
className={styles.bookingCodeFilterSelect}
|
||||
name="bookingCodeFilter"
|
||||
onSelect={handleChangeFilter}
|
||||
label=""
|
||||
items={bookingCodeFilterItems}
|
||||
defaultSelectedKey={selectedFilter}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<div className={styles.bookingCodeFilter}>
|
||||
<DialogTrigger isOpen={isOpen} onOpenChange={setIsOpen}>
|
||||
<ChipButton variant="Outlined">
|
||||
{
|
||||
bookingCodeFilterItems.find(
|
||||
(item) => item.value === selectedFilter
|
||||
)?.label
|
||||
}
|
||||
<MaterialIcon
|
||||
icon="keyboard_arrow_down"
|
||||
size={20}
|
||||
color="CurrentColor"
|
||||
/>
|
||||
</ChipButton>
|
||||
{displayAsPopover ? (
|
||||
<Popover placement="bottom end">
|
||||
<Dialog className={styles.dialog}>
|
||||
{({ close }) => {
|
||||
function handleChangeFilterValue(value: string) {
|
||||
updateFilterValue(value)
|
||||
close()
|
||||
}
|
||||
return (
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<RadioGroup
|
||||
aria-label={intl.formatMessage({
|
||||
defaultMessage: "Booking Code Filter",
|
||||
})}
|
||||
onChange={handleChangeFilterValue}
|
||||
name="bookingCodeFilter"
|
||||
value={selectedFilter}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
{bookingCodeFilterItems.map((item) => (
|
||||
<Radio
|
||||
aria-label={item.label}
|
||||
key={item.value}
|
||||
value={item.value}
|
||||
className={styles.radio}
|
||||
autoFocus={selectedFilter === item.value}
|
||||
>
|
||||
{item.label}
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</Typography>
|
||||
)
|
||||
}}
|
||||
</Dialog>
|
||||
</Popover>
|
||||
) : (
|
||||
<ModalOverlay className={styles.modalOverlay} isDismissable>
|
||||
<Modal className={styles.modal}>
|
||||
<Dialog className={styles.modalDialog}>
|
||||
{({ close }) => {
|
||||
function handleChangeFilterValue(value: string) {
|
||||
updateFilterValue(value)
|
||||
close()
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className={styles.header}>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<h3>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Room rates",
|
||||
})}
|
||||
</h3>
|
||||
</Typography>
|
||||
<IconButton
|
||||
theme="Black"
|
||||
style="Muted"
|
||||
onPress={() => {
|
||||
close()
|
||||
}}
|
||||
>
|
||||
<MaterialIcon
|
||||
icon="close"
|
||||
size={24}
|
||||
color="CurrentColor"
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<RadioGroup
|
||||
aria-label={intl.formatMessage({
|
||||
defaultMessage: "Booking Code Filter",
|
||||
})}
|
||||
onChange={handleChangeFilterValue}
|
||||
name="bookingCodeFilter"
|
||||
value={selectedFilter}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
{bookingCodeFilterItems.map((item) => (
|
||||
<Radio
|
||||
aria-label={item.label}
|
||||
key={item.value}
|
||||
value={item.value}
|
||||
className={styles.radio}
|
||||
>
|
||||
{item.label}
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</Typography>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: var(--Space-x1);
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
|
||||
@@ -52,10 +52,6 @@ export default function Campaign({
|
||||
campaign = campaign.filter((product) => product.bookingCode)
|
||||
}
|
||||
|
||||
if (selectedFilter === BookingCodeFilterEnum.Regular) {
|
||||
campaign = campaign.filter((product) => !product.bookingCode)
|
||||
}
|
||||
|
||||
const pkgsSum = sumPackages(selectedPackages)
|
||||
const pkgsSumRequested = sumPackagesRequestedPrice(selectedPackages)
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
import { calculatePricePerNightPriceProduct } from "./totalPricePerNight"
|
||||
|
||||
import type { SharedRateCardProps } from "@/types/components/hotelReservation/selectRate/rates"
|
||||
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
||||
import type { CodeProduct } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||
|
||||
interface CodeProps extends SharedRateCardProps {
|
||||
@@ -35,8 +34,7 @@ export default function Code({
|
||||
roomTypeCode,
|
||||
}: CodeProps) {
|
||||
const intl = useIntl()
|
||||
const { roomNr, selectedFilter, selectedPackages, selectedRate } =
|
||||
useRoomContext()
|
||||
const { roomNr, selectedPackages, selectedRate } = useRoomContext()
|
||||
const bookingCode = useRatesStore((state) => state.booking.bookingCode)
|
||||
const rateTitles = useRateTitles()
|
||||
const night = intl
|
||||
@@ -45,10 +43,6 @@ export default function Code({
|
||||
})
|
||||
.toUpperCase()
|
||||
|
||||
if (selectedFilter === BookingCodeFilterEnum.Regular) {
|
||||
return null
|
||||
}
|
||||
|
||||
return code.map((product) => {
|
||||
let bannerText = ""
|
||||
if (product.rateDefinition.breakfastIncluded) {
|
||||
@@ -155,15 +149,16 @@ export default function Code({
|
||||
pkgsSumRequested.price
|
||||
)
|
||||
|
||||
const approximateRate = pricePerNight.totalRequestedPrice
|
||||
? {
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "Approx.",
|
||||
}),
|
||||
price: pricePerNight.totalRequestedPrice,
|
||||
unit: localPrice.currency,
|
||||
}
|
||||
: undefined
|
||||
const approximateRate =
|
||||
pricePerNight.totalRequestedPrice && requestedPrice?.currency
|
||||
? {
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "Approx.",
|
||||
}),
|
||||
price: pricePerNight.totalRequestedPrice,
|
||||
unit: requestedPrice.currency,
|
||||
}
|
||||
: undefined
|
||||
|
||||
const regularPricePerNight = calculatePricePerNightPriceProduct(
|
||||
localPrice.regularPricePerNight,
|
||||
|
||||
@@ -26,7 +26,6 @@ export default function Redemptions({
|
||||
|
||||
if (
|
||||
selectedFilter === BookingCodeFilterEnum.Discounted ||
|
||||
selectedFilter === BookingCodeFilterEnum.Regular ||
|
||||
!redemptions.length
|
||||
) {
|
||||
return null
|
||||
|
||||
@@ -53,21 +53,14 @@ export default function Rates({
|
||||
roomTypeCode,
|
||||
}
|
||||
const showAllRates = selectedFilter === BookingCodeFilterEnum.All
|
||||
const showRegularRates = selectedFilter === BookingCodeFilterEnum.Regular
|
||||
const hasBookingCodeRates = !!(campaign.length || code.length)
|
||||
const hasRegularRates = !!regular.length
|
||||
const notDiscountedPrice = showAllRates || showRegularRates
|
||||
const notDiscountedAndHasRates =
|
||||
notDiscountedPrice && hasBookingCodeRates && hasRegularRates
|
||||
const isFetchingAndOnlyShowingRegularRates =
|
||||
isFetchingAdditionalRate && !showRegularRates
|
||||
const showDivider =
|
||||
notDiscountedAndHasRates || isFetchingAndOnlyShowingRegularRates
|
||||
const showDivider = showAllRates && hasBookingCodeRates && hasRegularRates
|
||||
|
||||
return (
|
||||
<>
|
||||
<Campaign {...sharedProps} campaign={campaign} />
|
||||
<Code {...sharedProps} code={code} />
|
||||
<Campaign {...sharedProps} campaign={campaign} />
|
||||
<Redemptions {...sharedProps} redemptions={redemptions} />
|
||||
{showDivider ? <Divider color="borderDividerSubtle" /> : null}
|
||||
{isFetchingAdditionalRate ? (
|
||||
|
||||
Reference in New Issue
Block a user