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:
@@ -0,0 +1,12 @@
|
||||
.bookingCodeChip {
|
||||
display: flex;
|
||||
gap: var(--Space-x05);
|
||||
}
|
||||
|
||||
.bookingCodeChip .unavailable {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.center {
|
||||
justify-content: center;
|
||||
}
|
||||
90
apps/scandic-web/components/BookingCodeChip/index.tsx
Normal file
90
apps/scandic-web/components/BookingCodeChip/index.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import IconChip from "../TempDesignSystem/IconChip"
|
||||
|
||||
import styles from "./bookingCodeChip.module.css"
|
||||
|
||||
type BookingCodeChipProps = {
|
||||
alignCenter?: boolean
|
||||
bookingCode?: string | null
|
||||
isBreakfastIncluded?: boolean
|
||||
isCampaign?: boolean
|
||||
isUnavailable?: boolean
|
||||
}
|
||||
|
||||
export default function BookingCodeChip({
|
||||
alignCenter,
|
||||
bookingCode,
|
||||
isBreakfastIncluded,
|
||||
isCampaign,
|
||||
isUnavailable,
|
||||
}: BookingCodeChipProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
if (isCampaign) {
|
||||
return (
|
||||
<IconChip
|
||||
color="green"
|
||||
icon={<DiscountIcon color="Icon/Feedback/Success" />}
|
||||
className={alignCenter ? styles.center : undefined}
|
||||
>
|
||||
<p className={styles.bookingCodeChip}>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<strong>
|
||||
{intl.formatMessage({ defaultMessage: "Campaign" })}
|
||||
</strong>
|
||||
</Typography>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<span>
|
||||
{isBreakfastIncluded
|
||||
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||
`${bookingCode ?? ""} ${intl.formatMessage({
|
||||
defaultMessage: "Breakfast included",
|
||||
})}`
|
||||
: // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||
`${bookingCode ?? ""} ${intl.formatMessage({
|
||||
defaultMessage: "Breakfast excluded",
|
||||
})}`}
|
||||
</span>
|
||||
</Typography>
|
||||
</p>
|
||||
</IconChip>
|
||||
)
|
||||
}
|
||||
|
||||
if (!bookingCode) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<IconChip
|
||||
color="blue"
|
||||
icon={<DiscountIcon color="Icon/Feedback/Information" />}
|
||||
className={alignCenter ? styles.center : undefined}
|
||||
>
|
||||
<p className={styles.bookingCodeChip}>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<strong>
|
||||
{intl.formatMessage({ defaultMessage: "Booking code" })}
|
||||
</strong>
|
||||
</Typography>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<span className={`${isUnavailable ? styles.unavailable : ""}`}>
|
||||
{isUnavailable
|
||||
? intl.formatMessage(
|
||||
{ defaultMessage: "{code} unavailable" },
|
||||
{ code: bookingCode }
|
||||
)
|
||||
: intl.formatMessage(
|
||||
{ defaultMessage: "{code} applied" },
|
||||
{ code: bookingCode }
|
||||
)}
|
||||
</span>
|
||||
</Typography>
|
||||
</p>
|
||||
</IconChip>
|
||||
)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
|
||||
|
||||
import BookingCodeChip from "@/components/BookingCodeChip"
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
|
||||
@@ -23,6 +24,7 @@ export default function TotalPrice() {
|
||||
)
|
||||
|
||||
const hasAllRoomsLoaded = rooms.every((room) => room)
|
||||
const bookingCode = rooms.find((room) => room?.bookingCode)?.bookingCode
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -52,6 +54,7 @@ export default function TotalPrice() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{bookingCode && <BookingCodeChip bookingCode={bookingCode} alignCenter />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,17 +5,15 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { Button } from "@scandic-hotels/design-system/Button"
|
||||
import { IconButton } from "@scandic-hotels/design-system/IconButton"
|
||||
import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import BookingCodeChip from "@/components/BookingCodeChip"
|
||||
import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal"
|
||||
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
|
||||
import Modal from "@/components/Modal"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import IconChip from "@/components/TempDesignSystem/IconChip"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
@@ -97,6 +95,12 @@ export default function SummaryUI({
|
||||
: false
|
||||
|
||||
const priceDetailsRooms = mapToPrice(rooms, isMember, nights)
|
||||
const isAllCampaignRate = rooms.every(
|
||||
(room) => room.room.roomRate.rateDefinition.isCampaignRate
|
||||
)
|
||||
const isAllBreakfastIncluded = rooms.every(
|
||||
(room) => room.room.roomRate.rateDefinition.breakfastIncluded
|
||||
)
|
||||
|
||||
return (
|
||||
<section className={styles.summary}>
|
||||
@@ -460,6 +464,7 @@ export default function SummaryUI({
|
||||
<PriceDetailsModal
|
||||
bookingCode={booking.bookingCode}
|
||||
fromDate={booking.fromDate}
|
||||
isCampaignRate={isAllCampaignRate}
|
||||
rooms={priceDetailsRooms}
|
||||
toDate={booking.toDate}
|
||||
totalPrice={totalPrice}
|
||||
@@ -503,28 +508,12 @@ export default function SummaryUI({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{booking.bookingCode && (
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<IconChip
|
||||
color="blue"
|
||||
icon={<DiscountIcon color="Icon/Feedback/Information" />}
|
||||
>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "<strong>Booking code</strong>: {value}",
|
||||
},
|
||||
{
|
||||
value: booking.bookingCode,
|
||||
strong: (text) => (
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<strong>{text}</strong>
|
||||
</Typography>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</IconChip>
|
||||
</Typography>
|
||||
)}
|
||||
<BookingCodeChip
|
||||
isCampaign={isAllCampaignRate}
|
||||
bookingCode={booking.bookingCode}
|
||||
isBreakfastIncluded={isAllBreakfastIncluded}
|
||||
alignCenter
|
||||
/>
|
||||
<Divider className={styles.bottomDivider} color="primaryLightSubtle" />
|
||||
</div>
|
||||
{showSignupPromo && roomOneMemberPrice && !isMember ? (
|
||||
|
||||
@@ -5,18 +5,17 @@ import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { memo } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon"
|
||||
import HotelLogoIcon from "@scandic-hotels/design-system/Icons/HotelLogoIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { selectHotelMap, selectRate } from "@/constants/routes/hotelReservation"
|
||||
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||
|
||||
import BookingCodeChip from "@/components/BookingCodeChip"
|
||||
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||
import ImageGallery from "@/components/ImageGallery"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import IconChip from "@/components/TempDesignSystem/IconChip"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
@@ -39,7 +38,6 @@ import styles from "./hotelCard.module.css"
|
||||
import { HotelCardListingTypeEnum } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import type { HotelCardProps } from "@/types/components/hotelReservation/selectHotel/hotelCardProps"
|
||||
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
|
||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
function HotelCard({
|
||||
@@ -72,8 +70,8 @@ function HotelCard({
|
||||
const addressStr = `${hotel.address.streetAddress}, ${hotel.address.city}`
|
||||
const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || [])
|
||||
const fullPrice =
|
||||
availability.productType?.public?.rateType === RateTypeEnum.Regular ||
|
||||
availability.productType?.member?.rateType === RateTypeEnum.Regular
|
||||
!availability.productType?.public?.bookingCode &&
|
||||
!availability.productType?.member?.bookingCode
|
||||
const price = availability.productType
|
||||
|
||||
const hasInsufficientPoints = !price?.redemptions?.some(
|
||||
@@ -183,29 +181,10 @@ function HotelCard({
|
||||
) : (
|
||||
<>
|
||||
{bookingCode && (
|
||||
<div className={`${fullPrice ? styles.strikedText : ""}`}>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<IconChip
|
||||
color="blue"
|
||||
icon={<DiscountIcon color="Icon/Feedback/Information" />}
|
||||
>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage:
|
||||
"<strong>Booking code</strong>: {value}",
|
||||
},
|
||||
{
|
||||
value: bookingCode,
|
||||
strong: (text) => (
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<strong>{text}</strong>
|
||||
</Typography>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</IconChip>
|
||||
</Typography>
|
||||
</div>
|
||||
<BookingCodeChip
|
||||
bookingCode={bookingCode}
|
||||
isUnavailable={fullPrice}
|
||||
/>
|
||||
)}
|
||||
{(!isUserLoggedIn ||
|
||||
!price?.member ||
|
||||
|
||||
@@ -15,6 +15,8 @@ export function getHotelPins(
|
||||
(r) => r?.localPrice.pointsPerStay
|
||||
)
|
||||
return {
|
||||
bookingCode:
|
||||
productType?.public?.bookingCode ?? productType?.member?.bookingCode,
|
||||
coordinates: {
|
||||
lat: hotel.location.latitude,
|
||||
lng: hotel.location.longitude,
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
} from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||
|
||||
export default function HotelCardListing({
|
||||
hotelData,
|
||||
@@ -44,16 +43,28 @@ export default function HotelCardListing({
|
||||
const sortBy = searchParams.get("sort") ?? DEFAULT_SORT
|
||||
|
||||
const bookingCode = searchParams.get("bookingCode")
|
||||
// Special rates (corporate cheque, voucher and reward nights) will not have regular rate hotels availability
|
||||
const isSpecialRate = hotelData.find(
|
||||
(hotel) =>
|
||||
hotel.availability.productType?.bonusCheque ||
|
||||
hotel.availability.productType?.voucher ||
|
||||
hotel.availability.productType?.redemptions
|
||||
)
|
||||
// Special rates (corporate cheque, voucher) will not show regular rate hotels availability
|
||||
const isSpecialRate = bookingCode
|
||||
? hotelData.find(
|
||||
(hotel) =>
|
||||
hotel.availability.productType?.bonusCheque ||
|
||||
hotel.availability.productType?.voucher
|
||||
)
|
||||
: false
|
||||
const activeCodeFilter = useBookingCodeFilterStore(
|
||||
(state) => state.activeCodeFilter
|
||||
)
|
||||
const isBookingCodeRateAvailable =
|
||||
bookingCode && !isSpecialRate
|
||||
? hotelData.some(
|
||||
(hotel) =>
|
||||
hotel.availability.productType?.public?.bookingCode ||
|
||||
hotel.availability.productType?.member?.bookingCode
|
||||
)
|
||||
: false
|
||||
const showOnlyBookingCodeRates =
|
||||
isBookingCodeRateAvailable &&
|
||||
activeCodeFilter !== BookingCodeFilterEnum.Discounted
|
||||
|
||||
const hotels = useMemo(() => {
|
||||
const sortedHotels = getSortedHotels({
|
||||
@@ -61,24 +72,14 @@ export default function HotelCardListing({
|
||||
sortBy,
|
||||
bookingCode: isSpecialRate ? null : bookingCode,
|
||||
})
|
||||
const updatedHotelsList =
|
||||
bookingCode && !isSpecialRate
|
||||
? sortedHotels.filter(
|
||||
(hotel) =>
|
||||
!hotel.availability.productType ||
|
||||
activeCodeFilter === BookingCodeFilterEnum.All ||
|
||||
(activeCodeFilter === BookingCodeFilterEnum.Discounted &&
|
||||
hotel.availability.productType.public?.rateType !==
|
||||
RateTypeEnum.Regular &&
|
||||
hotel.availability.productType.member?.rateType !==
|
||||
RateTypeEnum.Regular) ||
|
||||
(activeCodeFilter === BookingCodeFilterEnum.Regular &&
|
||||
(hotel.availability.productType.public?.rateType ===
|
||||
RateTypeEnum.Regular ||
|
||||
hotel.availability.productType.member?.rateType ===
|
||||
RateTypeEnum.Regular))
|
||||
)
|
||||
: sortedHotels
|
||||
|
||||
const updatedHotelsList = showOnlyBookingCodeRates
|
||||
? sortedHotels.filter(
|
||||
(hotel) =>
|
||||
hotel.availability.productType?.public?.bookingCode ||
|
||||
hotel.availability.productType?.member?.bookingCode
|
||||
)
|
||||
: sortedHotels
|
||||
|
||||
if (!activeFilters.length) {
|
||||
return updatedHotelsList
|
||||
@@ -92,11 +93,11 @@ export default function HotelCardListing({
|
||||
)
|
||||
)
|
||||
}, [
|
||||
activeCodeFilter,
|
||||
activeFilters,
|
||||
bookingCode,
|
||||
hotelData,
|
||||
sortBy,
|
||||
showOnlyBookingCodeRates,
|
||||
isSpecialRate,
|
||||
])
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { SortOrder } from "@/types/components/hotelReservation/selectHotel/hotelSorter"
|
||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
|
||||
|
||||
function getPricePerNight(hotel: HotelResponse): number {
|
||||
@@ -50,18 +49,13 @@ export function getSortedHotels({
|
||||
if (bookingCode) {
|
||||
const bookingCodeRateHotels = availableHotels.filter(
|
||||
(hotel) =>
|
||||
hotel.availability.productType?.public?.rateType !==
|
||||
RateTypeEnum.Regular &&
|
||||
hotel.availability.productType?.member?.rateType !==
|
||||
RateTypeEnum.Regular &&
|
||||
!!hotel.availability.productType
|
||||
hotel.availability.productType?.public?.bookingCode ||
|
||||
hotel.availability.productType?.member?.bookingCode
|
||||
)
|
||||
const regularRateHotels = availableHotels.filter(
|
||||
(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
|
||||
)
|
||||
|
||||
return bookingCodeRateHotels
|
||||
|
||||
@@ -1,47 +1,32 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import IconChip from "@/components/TempDesignSystem/IconChip"
|
||||
import BookingCodeChip from "@/components/BookingCodeChip"
|
||||
|
||||
import styles from "./row.module.css"
|
||||
|
||||
interface BookingCodeRowProps {
|
||||
bookingCode?: string
|
||||
isBreakfastIncluded?: boolean
|
||||
isCampaignRate?: boolean
|
||||
}
|
||||
|
||||
export default function BookingCodeRow({ bookingCode }: BookingCodeRowProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
export default function BookingCodeRow({
|
||||
bookingCode,
|
||||
isBreakfastIncluded,
|
||||
isCampaignRate,
|
||||
}: BookingCodeRowProps) {
|
||||
if (!bookingCode) {
|
||||
return null
|
||||
}
|
||||
|
||||
const text = intl.formatMessage(
|
||||
{ defaultMessage: "<strong>Booking code</strong>: {value}" },
|
||||
{
|
||||
value: bookingCode,
|
||||
strong: (text) => (
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<strong>{text}</strong>
|
||||
</Typography>
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<tr className={styles.row}>
|
||||
<td colSpan={2} align="left">
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<IconChip
|
||||
color="blue"
|
||||
icon={<DiscountIcon color="Icon/Feedback/Information" />}
|
||||
>
|
||||
{text}
|
||||
</IconChip>
|
||||
</Typography>
|
||||
<BookingCodeChip
|
||||
bookingCode={bookingCode}
|
||||
isBreakfastIncluded={isBreakfastIncluded}
|
||||
isCampaign={isCampaignRate}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
|
||||
@@ -53,6 +53,7 @@ export interface Room {
|
||||
export interface PriceDetailsTableProps {
|
||||
bookingCode?: string
|
||||
fromDate: string
|
||||
isCampaignRate?: boolean
|
||||
rooms: Room[]
|
||||
toDate: string
|
||||
totalPrice: Price
|
||||
@@ -62,6 +63,7 @@ export interface PriceDetailsTableProps {
|
||||
export default function PriceDetailsTable({
|
||||
bookingCode,
|
||||
fromDate,
|
||||
isCampaignRate,
|
||||
rooms,
|
||||
toDate,
|
||||
totalPrice,
|
||||
@@ -83,6 +85,8 @@ export default function PriceDetailsTable({
|
||||
const allRoomsPackages: Package[] = rooms
|
||||
.flatMap((r) => r.packages)
|
||||
.filter((r): r is Package => !!r)
|
||||
|
||||
const isAllBreakfastIncluded = rooms.every((room) => room.breakfastIncluded)
|
||||
return (
|
||||
<table className={styles.priceDetailsTable}>
|
||||
{rooms.map((room, idx) => {
|
||||
@@ -207,7 +211,11 @@ export default function PriceDetailsTable({
|
||||
regularPrice={totalPrice.local.regularPrice}
|
||||
/>
|
||||
|
||||
<BookingCodeRow bookingCode={bookingCode} />
|
||||
<BookingCodeRow
|
||||
isCampaignRate={isCampaignRate}
|
||||
isBreakfastIncluded={isAllBreakfastIncluded}
|
||||
bookingCode={bookingCode}
|
||||
/>
|
||||
</Tbody>
|
||||
</table>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -4,11 +4,18 @@ interface IconChipProps {
|
||||
color: "blue" | "green" | "red" | null | undefined
|
||||
icon: React.ReactNode
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function IconChip({ color, icon, children }: IconChipProps) {
|
||||
export default function IconChip({
|
||||
color,
|
||||
icon,
|
||||
className,
|
||||
children,
|
||||
}: IconChipProps) {
|
||||
const classNames = iconChipVariants({
|
||||
color: color,
|
||||
className: className,
|
||||
})
|
||||
return (
|
||||
<div className={classNames}>
|
||||
|
||||
@@ -95,14 +95,25 @@ export const hotelSchema = z
|
||||
export const hotelsAvailabilitySchema = z.object({
|
||||
data: z.array(
|
||||
z.object({
|
||||
attributes: z.object({
|
||||
checkInDate: z.string(),
|
||||
checkOutDate: z.string(),
|
||||
hotelId: z.number(),
|
||||
occupancy: occupancySchema,
|
||||
productType: productTypeSchema,
|
||||
status: z.string(),
|
||||
}),
|
||||
attributes: z
|
||||
.object({
|
||||
bookingCode: z.string().nullish(),
|
||||
checkInDate: z.string(),
|
||||
checkOutDate: z.string(),
|
||||
hotelId: z.number(),
|
||||
occupancy: occupancySchema,
|
||||
productType: productTypeSchema,
|
||||
status: z.string(),
|
||||
})
|
||||
.transform((data) => {
|
||||
if (data.bookingCode && data.productType?.public) {
|
||||
data.productType.public.bookingCode = data.bookingCode
|
||||
}
|
||||
if (data.bookingCode && data.productType?.member) {
|
||||
data.productType.member.bookingCode = data.bookingCode
|
||||
}
|
||||
return data
|
||||
}),
|
||||
relationships: relationshipsSchema.optional(),
|
||||
type: z.string().optional(),
|
||||
})
|
||||
|
||||
@@ -46,6 +46,7 @@ const partialPriceSchema = z.object({
|
||||
}
|
||||
return RateTypeEnum.Regular
|
||||
}),
|
||||
bookingCode: z.string().nullish(),
|
||||
})
|
||||
|
||||
export const productTypeCorporateChequeSchema = z
|
||||
|
||||
@@ -152,17 +152,13 @@ export function createRatesStore({
|
||||
room.counterRateCode
|
||||
)
|
||||
}
|
||||
let selectedFilter
|
||||
const bookingCode = room.rateCode
|
||||
? room.bookingCode
|
||||
: booking.bookingCode
|
||||
if (isRedemptionBooking) {
|
||||
selectedFilter = BookingCodeFilterEnum.All
|
||||
} else if (bookingCode) {
|
||||
selectedFilter = BookingCodeFilterEnum.Discounted
|
||||
} else {
|
||||
selectedFilter = BookingCodeFilterEnum.Regular
|
||||
}
|
||||
const selectedFilter =
|
||||
bookingCode && !isRedemptionBooking
|
||||
? BookingCodeFilterEnum.Discounted
|
||||
: BookingCodeFilterEnum.All
|
||||
|
||||
return {
|
||||
actions: {
|
||||
@@ -301,7 +297,8 @@ export function createRatesStore({
|
||||
return set(
|
||||
produce((state: RatesState) => {
|
||||
state.rooms[idx].selectedFilter = filter
|
||||
state.rooms[idx].isFetchingAdditionalRate = true
|
||||
state.rooms[idx].isFetchingAdditionalRate =
|
||||
filter === BookingCodeFilterEnum.All
|
||||
})
|
||||
)
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ type ImageSizes = z.infer<typeof imageSchema>["imageSizes"]
|
||||
type ImageMetaData = z.infer<typeof imageSchema>["metaData"]
|
||||
|
||||
export type HotelPin = {
|
||||
bookingCode?: string | null
|
||||
name: string
|
||||
coordinates: Coordinates
|
||||
publicPrice: number | null
|
||||
|
||||
@@ -2,7 +2,9 @@ import type { Hotel } from "@/types/hotel"
|
||||
|
||||
export type NoAvailabilityAlertProp = {
|
||||
hotelsLength: number
|
||||
bookingCode?: string
|
||||
isAllUnavailable: boolean
|
||||
isAlternative?: boolean
|
||||
isBookingCodeRateNotAvailable?: boolean
|
||||
operaId: Hotel["operaId"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export enum BookingCodeFilterEnum {
|
||||
Discounted = "Discounted",
|
||||
Regular = "Regular",
|
||||
All = "All",
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function CampaignRateCard({
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<div className={classNames}>
|
||||
<Typography variant="Tag/sm">
|
||||
<Typography variant="Label/xsBold">
|
||||
<p className={styles.banner}>{bannerText}</p>
|
||||
</Typography>
|
||||
<div className={styles.container}>
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function CodeRateCard({
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<div className={classNames}>
|
||||
<Typography variant="Tag/sm">
|
||||
<Typography variant="Label/xsBold">
|
||||
<p className={styles.banner}>{bannerText}</p>
|
||||
</Typography>
|
||||
<div className={styles.container}>
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function PointsRateCard({
|
||||
|
||||
return (
|
||||
<div className={classNames}>
|
||||
<Typography variant="Tag/sm">
|
||||
<Typography variant="Label/xsBold">
|
||||
<p className={styles.banner}>{bannerText}</p>
|
||||
</Typography>
|
||||
<div className={styles.container}>
|
||||
|
||||
@@ -152,35 +152,15 @@ label:not(:has(.radio:checked)) .checkIcon {
|
||||
align-items: center;
|
||||
gap: var(--Space-x1);
|
||||
}
|
||||
.variant-campaign {
|
||||
background-color: var(--Surface-Brand-Primary-1-Default);
|
||||
}
|
||||
|
||||
.variant-campaign:hover {
|
||||
background-color: var(--Scandic-Peach-20);
|
||||
}
|
||||
|
||||
.variant-campaign .banner {
|
||||
background-color: var(--Surface-Brand-Primary-1-OnSurface-Accent);
|
||||
}
|
||||
|
||||
.variant-code {
|
||||
background-color: var(--Surface-Feedback-Information);
|
||||
}
|
||||
|
||||
.variant-code:hover {
|
||||
background-color: var(--Scandic-Blue-10);
|
||||
background-color: var(--Surface-Accent-3);
|
||||
}
|
||||
|
||||
.variant-code .banner {
|
||||
background-color: var(--Surface-Feedback-Information-Accent);
|
||||
}
|
||||
|
||||
.variant-points:hover {
|
||||
background-color: var(--Scandic-Grey-00);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.variant-points .banner {
|
||||
background-color: var(--Surface-Brand-Primary-1-OnSurface-Accent-Secondary);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user