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