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:
Hrishikesh Vaipurkar
2025-05-02 12:36:22 +00:00
parent d8a48735a4
commit e6a3e5dbd8
34 changed files with 795 additions and 291 deletions

View File

@@ -4,12 +4,128 @@
width: 100%;
}
.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);
}
@media screen and (max-width: 767px) {
.bookingCodeFilter {
margin-bottom: var(--Spacing-x3);
margin-bottom: var(--Space-x3);
}
}
.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);
}
}

View File

@@ -1,25 +1,37 @@
"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 { useBookingCodeFilterStore } from "@/stores/bookingCode-filter"
import Select from "@/components/TempDesignSystem/Select"
import styles from "./bookingCodeFilter.module.css"
import type { Key } from "react"
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
export default function BookingCodeFilter() {
const intl = useIntl()
const [isOpen, setIsOpen] = useState(false)
const activeCodeFilter = useBookingCodeFilterStore(
(state) => state.activeCodeFilter
)
const setFilter = useBookingCodeFilterStore((state) => state.setFilter)
const displayAsPopover = useMediaQuery("(min-width: 768px)")
const bookingCodeFilterItems = [
{
@@ -30,38 +42,130 @@ export default function BookingCodeFilter() {
},
{
label: intl.formatMessage({
defaultMessage: "Full price rooms",
}),
value: BookingCodeFilterEnum.Regular,
},
{
label: intl.formatMessage({
defaultMessage: "See all",
defaultMessage: "All rates",
}),
value: BookingCodeFilterEnum.All,
},
]
function updateFilter(selectedFilter: Key) {
function updateFilter(selectedFilter: string) {
setFilter(selectedFilter as BookingCodeFilterEnum)
}
return (
<>
<div className={styles.bookingCodeFilter}>
<Select
aria-label={intl.formatMessage({
defaultMessage: "Booking Code Filter",
})}
className={styles.bookingCodeFilterSelect}
name="bookingCodeFilter"
onSelect={updateFilter}
label=""
items={bookingCodeFilterItems}
defaultSelectedKey={activeCodeFilter}
optionsIcon={<MaterialIcon icon="sell" />}
/>
</div>
</>
<div className={styles.bookingCodeFilter}>
<DialogTrigger isOpen={isOpen} onOpenChange={setIsOpen}>
<ChipButton variant="Outlined">
{
bookingCodeFilterItems.find(
(item) => item.value === activeCodeFilter
)?.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) {
updateFilter(value)
close()
}
return (
<Typography variant="Body/Paragraph/mdRegular">
<RadioGroup
aria-label={intl.formatMessage({
defaultMessage: "Booking Code Filter",
})}
onChange={handleChangeFilterValue}
name="bookingCodeFilter"
value={activeCodeFilter}
className={styles.radioGroup}
>
{bookingCodeFilterItems.map((item) => (
<Radio
aria-label={item.label}
key={item.value}
value={item.value}
className={styles.radio}
autoFocus={activeCodeFilter === 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) {
updateFilter(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={activeCodeFilter}
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>
)
}

View File

@@ -9,13 +9,34 @@ import { AlertTypeEnum } from "@/types/enums/alert"
export default async function NoAvailabilityAlert({
hotelsLength,
bookingCode,
isAllUnavailable,
isAlternative,
isBookingCodeRateNotAvailable,
operaId,
}: NoAvailabilityAlertProp) {
const intl = await getIntl()
const lang = getLang()
if (isBookingCodeRateNotAvailable) {
const bookingCodeText = intl.formatMessage(
{
defaultMessage:
"We found no available rooms using this booking code ({bookingCode}). See available rates below.",
},
{ bookingCode }
)
return (
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({
defaultMessage: "No availability",
})}
text={bookingCodeText}
/>
)
}
if (!isAllUnavailable) {
return null
}

View File

@@ -15,7 +15,6 @@ import SelectHotelMap from "."
import type { SelectHotelMapContainerProps } from "@/types/components/hotelReservation/selectHotel/map"
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
import { RateTypeEnum } from "@/types/enums/rateType"
export async function SelectHotelMapContainer({
searchParams,
@@ -82,8 +81,8 @@ export async function SelectHotelMapContainer({
const isBookingCodeRateAvailable = bookingCode
? hotels?.some(
(hotel) =>
hotel.availability.productType?.public?.rateType !==
RateTypeEnum.Regular
hotel.availability.productType?.public?.bookingCode ||
hotel.availability.productType?.member?.bookingCode
)
: false

View File

@@ -29,7 +29,6 @@ import styles from "./selectHotelMapContent.module.css"
import type { SelectHotelMapProps } from "@/types/components/hotelReservation/selectHotel/map"
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
import { RateTypeEnum } from "@/types/enums/rateType"
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
const SKELETON_LOAD_DELAY = 750
@@ -81,24 +80,21 @@ export default function SelectHotelContent({
: { ...cityCoordinates, lat: cityCoordinates.lat - 0.006 }
}, [activeHotel, hotels, isAboveMobile, cityCoordinates])
const showOnlyBookingCodeRates =
bookingCode &&
isBookingCodeRateAvailable &&
activeCodeFilter === BookingCodeFilterEnum.Discounted
const filteredHotelPins = useMemo(() => {
const updatedHotelsList = bookingCode
? hotelPins.filter(
(hotel) =>
!hotel.publicPrice ||
activeCodeFilter === BookingCodeFilterEnum.All ||
(activeCodeFilter === BookingCodeFilterEnum.Discounted &&
hotel.rateType !== RateTypeEnum.Regular) ||
(activeCodeFilter === BookingCodeFilterEnum.Regular &&
hotel.rateType === RateTypeEnum.Regular)
)
const updatedHotelsList = showOnlyBookingCodeRates
? hotelPins.filter((hotel) => hotel.bookingCode)
: hotelPins
return updatedHotelsList.filter((hotel) =>
activeFilters.every((filterId) =>
hotel.facilityIds.includes(Number(filterId))
)
)
}, [activeFilters, hotelPins, bookingCode, activeCodeFilter])
}, [activeFilters, hotelPins, showOnlyBookingCodeRates])
const getHotelCards = useCallback(() => {
const visibleHotels = getVisibleHotels(hotels, filteredHotelPins, map)
@@ -146,10 +142,10 @@ export default function SelectHotelContent({
const isRegularRateAvailable = bookingCode
? hotels.some(
(hotel) =>
hotel.availability.productType?.public?.rateType ===
RateTypeEnum.Regular ||
hotel.availability.productType?.member?.rateType ===
RateTypeEnum.Regular
!(
hotel.availability.productType?.public?.bookingCode ||
hotel.availability.productType?.member?.bookingCode
)
)
: false

View File

@@ -35,7 +35,6 @@ import styles from "./selectHotel.module.css"
import type { SelectHotelProps } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
import { RateTypeEnum } from "@/types/enums/rateType"
export default async function SelectHotel({
params,
@@ -137,20 +136,16 @@ export default async function SelectHotel({
const isFullPriceHotelAvailable = bookingCode
? hotels?.some(
(hotel) =>
hotel.availability.productType?.public?.rateType ===
RateTypeEnum.Regular ||
hotel.availability.productType?.member?.rateType ===
RateTypeEnum.Regular
!hotel.availability.productType?.public?.bookingCode &&
!hotel.availability.productType?.member?.bookingCode
)
: false
const isBookingCodeRateAvailable = bookingCode
? hotels?.some(
(hotel) =>
hotel.availability.productType?.public?.rateType !==
RateTypeEnum.Regular ||
hotel.availability.productType?.member?.rateType !==
RateTypeEnum.Regular
hotel.availability.productType?.public?.bookingCode ||
hotel.availability.productType?.member?.bookingCode
)
: false
@@ -268,6 +263,8 @@ export default async function SelectHotel({
isAlternative={!!isAlternativeFor}
isAllUnavailable={isAllUnavailable}
operaId={hotels?.[0]?.hotel.operaId}
bookingCode={bookingCode}
isBookingCodeRateNotAvailable={!isBookingCodeRateAvailable}
/>
<HotelCardListing hotelData={hotels} />
</div>