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 { useBookingConfirmationStore } from "@/stores/booking-confirmation"
|
||||||
|
|
||||||
|
import BookingCodeChip from "@/components/BookingCodeChip"
|
||||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ export default function TotalPrice() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const hasAllRoomsLoaded = rooms.every((room) => room)
|
const hasAllRoomsLoaded = rooms.every((room) => room)
|
||||||
|
const bookingCode = rooms.find((room) => room?.bookingCode)?.bookingCode
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -52,6 +54,7 @@ export default function TotalPrice() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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 { Button } from "@scandic-hotels/design-system/Button"
|
||||||
import { IconButton } from "@scandic-hotels/design-system/IconButton"
|
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 { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
||||||
|
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
|
||||||
|
import BookingCodeChip from "@/components/BookingCodeChip"
|
||||||
import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal"
|
import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal"
|
||||||
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
|
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
|
||||||
import Modal from "@/components/Modal"
|
import Modal from "@/components/Modal"
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
import IconChip from "@/components/TempDesignSystem/IconChip"
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
@@ -97,6 +95,12 @@ export default function SummaryUI({
|
|||||||
: false
|
: false
|
||||||
|
|
||||||
const priceDetailsRooms = mapToPrice(rooms, isMember, nights)
|
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 (
|
return (
|
||||||
<section className={styles.summary}>
|
<section className={styles.summary}>
|
||||||
@@ -460,6 +464,7 @@ export default function SummaryUI({
|
|||||||
<PriceDetailsModal
|
<PriceDetailsModal
|
||||||
bookingCode={booking.bookingCode}
|
bookingCode={booking.bookingCode}
|
||||||
fromDate={booking.fromDate}
|
fromDate={booking.fromDate}
|
||||||
|
isCampaignRate={isAllCampaignRate}
|
||||||
rooms={priceDetailsRooms}
|
rooms={priceDetailsRooms}
|
||||||
toDate={booking.toDate}
|
toDate={booking.toDate}
|
||||||
totalPrice={totalPrice}
|
totalPrice={totalPrice}
|
||||||
@@ -503,28 +508,12 @@ export default function SummaryUI({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{booking.bookingCode && (
|
<BookingCodeChip
|
||||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
isCampaign={isAllCampaignRate}
|
||||||
<IconChip
|
bookingCode={booking.bookingCode}
|
||||||
color="blue"
|
isBreakfastIncluded={isAllBreakfastIncluded}
|
||||||
icon={<DiscountIcon color="Icon/Feedback/Information" />}
|
alignCenter
|
||||||
>
|
/>
|
||||||
{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>
|
|
||||||
)}
|
|
||||||
<Divider className={styles.bottomDivider} color="primaryLightSubtle" />
|
<Divider className={styles.bottomDivider} color="primaryLightSubtle" />
|
||||||
</div>
|
</div>
|
||||||
{showSignupPromo && roomOneMemberPrice && !isMember ? (
|
{showSignupPromo && roomOneMemberPrice && !isMember ? (
|
||||||
|
|||||||
@@ -5,18 +5,17 @@ import { useRouter, useSearchParams } from "next/navigation"
|
|||||||
import { memo } from "react"
|
import { memo } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon"
|
|
||||||
import HotelLogoIcon from "@scandic-hotels/design-system/Icons/HotelLogoIcon"
|
import HotelLogoIcon from "@scandic-hotels/design-system/Icons/HotelLogoIcon"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { selectHotelMap, selectRate } from "@/constants/routes/hotelReservation"
|
import { selectHotelMap, selectRate } from "@/constants/routes/hotelReservation"
|
||||||
import { useHotelsMapStore } from "@/stores/hotels-map"
|
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||||
|
|
||||||
|
import BookingCodeChip from "@/components/BookingCodeChip"
|
||||||
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||||
import ImageGallery from "@/components/ImageGallery"
|
import ImageGallery from "@/components/ImageGallery"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
import IconChip from "@/components/TempDesignSystem/IconChip"
|
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
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 { HotelCardListingTypeEnum } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||||
import type { HotelCardProps } from "@/types/components/hotelReservation/selectHotel/hotelCardProps"
|
import type { HotelCardProps } from "@/types/components/hotelReservation/selectHotel/hotelCardProps"
|
||||||
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
|
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
|
||||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
|
||||||
import type { Lang } from "@/constants/languages"
|
import type { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
function HotelCard({
|
function HotelCard({
|
||||||
@@ -72,8 +70,8 @@ function HotelCard({
|
|||||||
const addressStr = `${hotel.address.streetAddress}, ${hotel.address.city}`
|
const addressStr = `${hotel.address.streetAddress}, ${hotel.address.city}`
|
||||||
const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || [])
|
const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || [])
|
||||||
const fullPrice =
|
const fullPrice =
|
||||||
availability.productType?.public?.rateType === RateTypeEnum.Regular ||
|
!availability.productType?.public?.bookingCode &&
|
||||||
availability.productType?.member?.rateType === RateTypeEnum.Regular
|
!availability.productType?.member?.bookingCode
|
||||||
const price = availability.productType
|
const price = availability.productType
|
||||||
|
|
||||||
const hasInsufficientPoints = !price?.redemptions?.some(
|
const hasInsufficientPoints = !price?.redemptions?.some(
|
||||||
@@ -183,29 +181,10 @@ function HotelCard({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{bookingCode && (
|
{bookingCode && (
|
||||||
<div className={`${fullPrice ? styles.strikedText : ""}`}>
|
<BookingCodeChip
|
||||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
bookingCode={bookingCode}
|
||||||
<IconChip
|
isUnavailable={fullPrice}
|
||||||
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>
|
|
||||||
)}
|
)}
|
||||||
{(!isUserLoggedIn ||
|
{(!isUserLoggedIn ||
|
||||||
!price?.member ||
|
!price?.member ||
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export function getHotelPins(
|
|||||||
(r) => r?.localPrice.pointsPerStay
|
(r) => r?.localPrice.pointsPerStay
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
|
bookingCode:
|
||||||
|
productType?.public?.bookingCode ?? productType?.member?.bookingCode,
|
||||||
coordinates: {
|
coordinates: {
|
||||||
lat: hotel.location.latitude,
|
lat: hotel.location.latitude,
|
||||||
lng: hotel.location.longitude,
|
lng: hotel.location.longitude,
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import {
|
|||||||
} from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
} from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||||
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
||||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
|
||||||
|
|
||||||
export default function HotelCardListing({
|
export default function HotelCardListing({
|
||||||
hotelData,
|
hotelData,
|
||||||
@@ -44,16 +43,28 @@ export default function HotelCardListing({
|
|||||||
const sortBy = searchParams.get("sort") ?? DEFAULT_SORT
|
const sortBy = searchParams.get("sort") ?? DEFAULT_SORT
|
||||||
|
|
||||||
const bookingCode = searchParams.get("bookingCode")
|
const bookingCode = searchParams.get("bookingCode")
|
||||||
// Special rates (corporate cheque, voucher and reward nights) will not have regular rate hotels availability
|
// Special rates (corporate cheque, voucher) will not show regular rate hotels availability
|
||||||
const isSpecialRate = hotelData.find(
|
const isSpecialRate = bookingCode
|
||||||
(hotel) =>
|
? hotelData.find(
|
||||||
hotel.availability.productType?.bonusCheque ||
|
(hotel) =>
|
||||||
hotel.availability.productType?.voucher ||
|
hotel.availability.productType?.bonusCheque ||
|
||||||
hotel.availability.productType?.redemptions
|
hotel.availability.productType?.voucher
|
||||||
)
|
)
|
||||||
|
: false
|
||||||
const activeCodeFilter = useBookingCodeFilterStore(
|
const activeCodeFilter = useBookingCodeFilterStore(
|
||||||
(state) => state.activeCodeFilter
|
(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 hotels = useMemo(() => {
|
||||||
const sortedHotels = getSortedHotels({
|
const sortedHotels = getSortedHotels({
|
||||||
@@ -61,24 +72,14 @@ export default function HotelCardListing({
|
|||||||
sortBy,
|
sortBy,
|
||||||
bookingCode: isSpecialRate ? null : bookingCode,
|
bookingCode: isSpecialRate ? null : bookingCode,
|
||||||
})
|
})
|
||||||
const updatedHotelsList =
|
|
||||||
bookingCode && !isSpecialRate
|
const updatedHotelsList = showOnlyBookingCodeRates
|
||||||
? sortedHotels.filter(
|
? sortedHotels.filter(
|
||||||
(hotel) =>
|
(hotel) =>
|
||||||
!hotel.availability.productType ||
|
hotel.availability.productType?.public?.bookingCode ||
|
||||||
activeCodeFilter === BookingCodeFilterEnum.All ||
|
hotel.availability.productType?.member?.bookingCode
|
||||||
(activeCodeFilter === BookingCodeFilterEnum.Discounted &&
|
)
|
||||||
hotel.availability.productType.public?.rateType !==
|
: sortedHotels
|
||||||
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
|
|
||||||
|
|
||||||
if (!activeFilters.length) {
|
if (!activeFilters.length) {
|
||||||
return updatedHotelsList
|
return updatedHotelsList
|
||||||
@@ -92,11 +93,11 @@ export default function HotelCardListing({
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}, [
|
}, [
|
||||||
activeCodeFilter,
|
|
||||||
activeFilters,
|
activeFilters,
|
||||||
bookingCode,
|
bookingCode,
|
||||||
hotelData,
|
hotelData,
|
||||||
sortBy,
|
sortBy,
|
||||||
|
showOnlyBookingCodeRates,
|
||||||
isSpecialRate,
|
isSpecialRate,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { SortOrder } from "@/types/components/hotelReservation/selectHotel/hotelSorter"
|
import { SortOrder } from "@/types/components/hotelReservation/selectHotel/hotelSorter"
|
||||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
|
||||||
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
|
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
|
||||||
|
|
||||||
function getPricePerNight(hotel: HotelResponse): number {
|
function getPricePerNight(hotel: HotelResponse): number {
|
||||||
@@ -50,18 +49,13 @@ export function getSortedHotels({
|
|||||||
if (bookingCode) {
|
if (bookingCode) {
|
||||||
const bookingCodeRateHotels = availableHotels.filter(
|
const bookingCodeRateHotels = availableHotels.filter(
|
||||||
(hotel) =>
|
(hotel) =>
|
||||||
hotel.availability.productType?.public?.rateType !==
|
hotel.availability.productType?.public?.bookingCode ||
|
||||||
RateTypeEnum.Regular &&
|
hotel.availability.productType?.member?.bookingCode
|
||||||
hotel.availability.productType?.member?.rateType !==
|
|
||||||
RateTypeEnum.Regular &&
|
|
||||||
!!hotel.availability.productType
|
|
||||||
)
|
)
|
||||||
const regularRateHotels = availableHotels.filter(
|
const regularRateHotels = availableHotels.filter(
|
||||||
(hotel) =>
|
(hotel) =>
|
||||||
hotel.availability.productType?.public?.rateType ===
|
!hotel.availability.productType?.public?.bookingCode &&
|
||||||
RateTypeEnum.Regular ||
|
!hotel?.availability.productType?.member?.bookingCode
|
||||||
hotel?.availability.productType?.member?.rateType ===
|
|
||||||
RateTypeEnum.Regular
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return bookingCodeRateHotels
|
return bookingCodeRateHotels
|
||||||
|
|||||||
@@ -1,47 +1,32 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon"
|
import BookingCodeChip from "@/components/BookingCodeChip"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
||||||
|
|
||||||
import IconChip from "@/components/TempDesignSystem/IconChip"
|
|
||||||
|
|
||||||
import styles from "./row.module.css"
|
import styles from "./row.module.css"
|
||||||
|
|
||||||
interface BookingCodeRowProps {
|
interface BookingCodeRowProps {
|
||||||
bookingCode?: string
|
bookingCode?: string
|
||||||
|
isBreakfastIncluded?: boolean
|
||||||
|
isCampaignRate?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BookingCodeRow({ bookingCode }: BookingCodeRowProps) {
|
export default function BookingCodeRow({
|
||||||
const intl = useIntl()
|
bookingCode,
|
||||||
|
isBreakfastIncluded,
|
||||||
|
isCampaignRate,
|
||||||
|
}: BookingCodeRowProps) {
|
||||||
if (!bookingCode) {
|
if (!bookingCode) {
|
||||||
return null
|
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 (
|
return (
|
||||||
<tr className={styles.row}>
|
<tr className={styles.row}>
|
||||||
<td colSpan={2} align="left">
|
<td colSpan={2} align="left">
|
||||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
<BookingCodeChip
|
||||||
<IconChip
|
bookingCode={bookingCode}
|
||||||
color="blue"
|
isBreakfastIncluded={isBreakfastIncluded}
|
||||||
icon={<DiscountIcon color="Icon/Feedback/Information" />}
|
isCampaign={isCampaignRate}
|
||||||
>
|
/>
|
||||||
{text}
|
|
||||||
</IconChip>
|
|
||||||
</Typography>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export interface Room {
|
|||||||
export interface PriceDetailsTableProps {
|
export interface PriceDetailsTableProps {
|
||||||
bookingCode?: string
|
bookingCode?: string
|
||||||
fromDate: string
|
fromDate: string
|
||||||
|
isCampaignRate?: boolean
|
||||||
rooms: Room[]
|
rooms: Room[]
|
||||||
toDate: string
|
toDate: string
|
||||||
totalPrice: Price
|
totalPrice: Price
|
||||||
@@ -62,6 +63,7 @@ export interface PriceDetailsTableProps {
|
|||||||
export default function PriceDetailsTable({
|
export default function PriceDetailsTable({
|
||||||
bookingCode,
|
bookingCode,
|
||||||
fromDate,
|
fromDate,
|
||||||
|
isCampaignRate,
|
||||||
rooms,
|
rooms,
|
||||||
toDate,
|
toDate,
|
||||||
totalPrice,
|
totalPrice,
|
||||||
@@ -83,6 +85,8 @@ export default function PriceDetailsTable({
|
|||||||
const allRoomsPackages: Package[] = rooms
|
const allRoomsPackages: Package[] = rooms
|
||||||
.flatMap((r) => r.packages)
|
.flatMap((r) => r.packages)
|
||||||
.filter((r): r is Package => !!r)
|
.filter((r): r is Package => !!r)
|
||||||
|
|
||||||
|
const isAllBreakfastIncluded = rooms.every((room) => room.breakfastIncluded)
|
||||||
return (
|
return (
|
||||||
<table className={styles.priceDetailsTable}>
|
<table className={styles.priceDetailsTable}>
|
||||||
{rooms.map((room, idx) => {
|
{rooms.map((room, idx) => {
|
||||||
@@ -207,7 +211,11 @@ export default function PriceDetailsTable({
|
|||||||
regularPrice={totalPrice.local.regularPrice}
|
regularPrice={totalPrice.local.regularPrice}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BookingCodeRow bookingCode={bookingCode} />
|
<BookingCodeRow
|
||||||
|
isCampaignRate={isCampaignRate}
|
||||||
|
isBreakfastIncluded={isAllBreakfastIncluded}
|
||||||
|
bookingCode={bookingCode}
|
||||||
|
/>
|
||||||
</Tbody>
|
</Tbody>
|
||||||
</table>
|
</table>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,12 +4,128 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bookingCodeFilterSelect {
|
.dialog {
|
||||||
min-width: 200px;
|
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) {
|
@media screen and (max-width: 767px) {
|
||||||
.bookingCodeFilter {
|
.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"
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from "react"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
Modal,
|
||||||
|
ModalOverlay,
|
||||||
|
Popover,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
} from "react-aria-components"
|
||||||
import { useIntl } from "react-intl"
|
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 { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { useBookingCodeFilterStore } from "@/stores/bookingCode-filter"
|
import { useBookingCodeFilterStore } from "@/stores/bookingCode-filter"
|
||||||
|
|
||||||
import Select from "@/components/TempDesignSystem/Select"
|
|
||||||
|
|
||||||
import styles from "./bookingCodeFilter.module.css"
|
import styles from "./bookingCodeFilter.module.css"
|
||||||
|
|
||||||
import type { Key } from "react"
|
|
||||||
|
|
||||||
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
||||||
|
|
||||||
export default function BookingCodeFilter() {
|
export default function BookingCodeFilter() {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const activeCodeFilter = useBookingCodeFilterStore(
|
const activeCodeFilter = useBookingCodeFilterStore(
|
||||||
(state) => state.activeCodeFilter
|
(state) => state.activeCodeFilter
|
||||||
)
|
)
|
||||||
const setFilter = useBookingCodeFilterStore((state) => state.setFilter)
|
const setFilter = useBookingCodeFilterStore((state) => state.setFilter)
|
||||||
|
const displayAsPopover = useMediaQuery("(min-width: 768px)")
|
||||||
|
|
||||||
const bookingCodeFilterItems = [
|
const bookingCodeFilterItems = [
|
||||||
{
|
{
|
||||||
@@ -30,38 +42,130 @@ export default function BookingCodeFilter() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: intl.formatMessage({
|
label: intl.formatMessage({
|
||||||
defaultMessage: "Full price rooms",
|
defaultMessage: "All rates",
|
||||||
}),
|
|
||||||
value: BookingCodeFilterEnum.Regular,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: intl.formatMessage({
|
|
||||||
defaultMessage: "See all",
|
|
||||||
}),
|
}),
|
||||||
value: BookingCodeFilterEnum.All,
|
value: BookingCodeFilterEnum.All,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
function updateFilter(selectedFilter: Key) {
|
function updateFilter(selectedFilter: string) {
|
||||||
setFilter(selectedFilter as BookingCodeFilterEnum)
|
setFilter(selectedFilter as BookingCodeFilterEnum)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={styles.bookingCodeFilter}>
|
||||||
<div className={styles.bookingCodeFilter}>
|
<DialogTrigger isOpen={isOpen} onOpenChange={setIsOpen}>
|
||||||
<Select
|
<ChipButton variant="Outlined">
|
||||||
aria-label={intl.formatMessage({
|
{
|
||||||
defaultMessage: "Booking Code Filter",
|
bookingCodeFilterItems.find(
|
||||||
})}
|
(item) => item.value === activeCodeFilter
|
||||||
className={styles.bookingCodeFilterSelect}
|
)?.label
|
||||||
name="bookingCodeFilter"
|
}
|
||||||
onSelect={updateFilter}
|
<MaterialIcon
|
||||||
label=""
|
icon="keyboard_arrow_down"
|
||||||
items={bookingCodeFilterItems}
|
size={20}
|
||||||
defaultSelectedKey={activeCodeFilter}
|
color="CurrentColor"
|
||||||
optionsIcon={<MaterialIcon icon="sell" />}
|
/>
|
||||||
/>
|
</ChipButton>
|
||||||
</div>
|
{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({
|
export default async function NoAvailabilityAlert({
|
||||||
hotelsLength,
|
hotelsLength,
|
||||||
|
bookingCode,
|
||||||
isAllUnavailable,
|
isAllUnavailable,
|
||||||
isAlternative,
|
isAlternative,
|
||||||
|
isBookingCodeRateNotAvailable,
|
||||||
operaId,
|
operaId,
|
||||||
}: NoAvailabilityAlertProp) {
|
}: NoAvailabilityAlertProp) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
const lang = getLang()
|
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) {
|
if (!isAllUnavailable) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import SelectHotelMap from "."
|
|||||||
|
|
||||||
import type { SelectHotelMapContainerProps } from "@/types/components/hotelReservation/selectHotel/map"
|
import type { SelectHotelMapContainerProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||||
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
||||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
|
||||||
|
|
||||||
export async function SelectHotelMapContainer({
|
export async function SelectHotelMapContainer({
|
||||||
searchParams,
|
searchParams,
|
||||||
@@ -82,8 +81,8 @@ export async function SelectHotelMapContainer({
|
|||||||
const isBookingCodeRateAvailable = bookingCode
|
const isBookingCodeRateAvailable = bookingCode
|
||||||
? hotels?.some(
|
? hotels?.some(
|
||||||
(hotel) =>
|
(hotel) =>
|
||||||
hotel.availability.productType?.public?.rateType !==
|
hotel.availability.productType?.public?.bookingCode ||
|
||||||
RateTypeEnum.Regular
|
hotel.availability.productType?.member?.bookingCode
|
||||||
)
|
)
|
||||||
: false
|
: false
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import styles from "./selectHotelMapContent.module.css"
|
|||||||
|
|
||||||
import type { SelectHotelMapProps } from "@/types/components/hotelReservation/selectHotel/map"
|
import type { SelectHotelMapProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||||
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
||||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
|
||||||
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
|
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
|
||||||
|
|
||||||
const SKELETON_LOAD_DELAY = 750
|
const SKELETON_LOAD_DELAY = 750
|
||||||
@@ -81,24 +80,21 @@ export default function SelectHotelContent({
|
|||||||
: { ...cityCoordinates, lat: cityCoordinates.lat - 0.006 }
|
: { ...cityCoordinates, lat: cityCoordinates.lat - 0.006 }
|
||||||
}, [activeHotel, hotels, isAboveMobile, cityCoordinates])
|
}, [activeHotel, hotels, isAboveMobile, cityCoordinates])
|
||||||
|
|
||||||
|
const showOnlyBookingCodeRates =
|
||||||
|
bookingCode &&
|
||||||
|
isBookingCodeRateAvailable &&
|
||||||
|
activeCodeFilter === BookingCodeFilterEnum.Discounted
|
||||||
|
|
||||||
const filteredHotelPins = useMemo(() => {
|
const filteredHotelPins = useMemo(() => {
|
||||||
const updatedHotelsList = bookingCode
|
const updatedHotelsList = showOnlyBookingCodeRates
|
||||||
? hotelPins.filter(
|
? hotelPins.filter((hotel) => hotel.bookingCode)
|
||||||
(hotel) =>
|
|
||||||
!hotel.publicPrice ||
|
|
||||||
activeCodeFilter === BookingCodeFilterEnum.All ||
|
|
||||||
(activeCodeFilter === BookingCodeFilterEnum.Discounted &&
|
|
||||||
hotel.rateType !== RateTypeEnum.Regular) ||
|
|
||||||
(activeCodeFilter === BookingCodeFilterEnum.Regular &&
|
|
||||||
hotel.rateType === RateTypeEnum.Regular)
|
|
||||||
)
|
|
||||||
: hotelPins
|
: hotelPins
|
||||||
return updatedHotelsList.filter((hotel) =>
|
return updatedHotelsList.filter((hotel) =>
|
||||||
activeFilters.every((filterId) =>
|
activeFilters.every((filterId) =>
|
||||||
hotel.facilityIds.includes(Number(filterId))
|
hotel.facilityIds.includes(Number(filterId))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}, [activeFilters, hotelPins, bookingCode, activeCodeFilter])
|
}, [activeFilters, hotelPins, showOnlyBookingCodeRates])
|
||||||
|
|
||||||
const getHotelCards = useCallback(() => {
|
const getHotelCards = useCallback(() => {
|
||||||
const visibleHotels = getVisibleHotels(hotels, filteredHotelPins, map)
|
const visibleHotels = getVisibleHotels(hotels, filteredHotelPins, map)
|
||||||
@@ -146,10 +142,10 @@ export default function SelectHotelContent({
|
|||||||
const isRegularRateAvailable = bookingCode
|
const isRegularRateAvailable = bookingCode
|
||||||
? hotels.some(
|
? hotels.some(
|
||||||
(hotel) =>
|
(hotel) =>
|
||||||
hotel.availability.productType?.public?.rateType ===
|
!(
|
||||||
RateTypeEnum.Regular ||
|
hotel.availability.productType?.public?.bookingCode ||
|
||||||
hotel.availability.productType?.member?.rateType ===
|
hotel.availability.productType?.member?.bookingCode
|
||||||
RateTypeEnum.Regular
|
)
|
||||||
)
|
)
|
||||||
: false
|
: false
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import styles from "./selectHotel.module.css"
|
|||||||
|
|
||||||
import type { SelectHotelProps } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
import type { SelectHotelProps } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||||
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
||||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
|
||||||
|
|
||||||
export default async function SelectHotel({
|
export default async function SelectHotel({
|
||||||
params,
|
params,
|
||||||
@@ -137,20 +136,16 @@ export default async function SelectHotel({
|
|||||||
const isFullPriceHotelAvailable = bookingCode
|
const isFullPriceHotelAvailable = bookingCode
|
||||||
? hotels?.some(
|
? hotels?.some(
|
||||||
(hotel) =>
|
(hotel) =>
|
||||||
hotel.availability.productType?.public?.rateType ===
|
!hotel.availability.productType?.public?.bookingCode &&
|
||||||
RateTypeEnum.Regular ||
|
!hotel.availability.productType?.member?.bookingCode
|
||||||
hotel.availability.productType?.member?.rateType ===
|
|
||||||
RateTypeEnum.Regular
|
|
||||||
)
|
)
|
||||||
: false
|
: false
|
||||||
|
|
||||||
const isBookingCodeRateAvailable = bookingCode
|
const isBookingCodeRateAvailable = bookingCode
|
||||||
? hotels?.some(
|
? hotels?.some(
|
||||||
(hotel) =>
|
(hotel) =>
|
||||||
hotel.availability.productType?.public?.rateType !==
|
hotel.availability.productType?.public?.bookingCode ||
|
||||||
RateTypeEnum.Regular ||
|
hotel.availability.productType?.member?.bookingCode
|
||||||
hotel.availability.productType?.member?.rateType !==
|
|
||||||
RateTypeEnum.Regular
|
|
||||||
)
|
)
|
||||||
: false
|
: false
|
||||||
|
|
||||||
@@ -268,6 +263,8 @@ export default async function SelectHotel({
|
|||||||
isAlternative={!!isAlternativeFor}
|
isAlternative={!!isAlternativeFor}
|
||||||
isAllUnavailable={isAllUnavailable}
|
isAllUnavailable={isAllUnavailable}
|
||||||
operaId={hotels?.[0]?.hotel.operaId}
|
operaId={hotels?.[0]?.hotel.operaId}
|
||||||
|
bookingCode={bookingCode}
|
||||||
|
isBookingCodeRateNotAvailable={!isBookingCodeRateAvailable}
|
||||||
/>
|
/>
|
||||||
<HotelCardListing hotelData={hotels} />
|
<HotelCardListing hotelData={hotels} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,122 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bookingCodeFilterSelect {
|
.dialog {
|
||||||
min-width: 200px;
|
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"
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from "react"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
Modal,
|
||||||
|
ModalOverlay,
|
||||||
|
Popover,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
} from "react-aria-components"
|
||||||
import { useIntl } from "react-intl"
|
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 { trpc } from "@/lib/trpc/client"
|
||||||
import { useRatesStore } from "@/stores/select-rate"
|
import { useRatesStore } from "@/stores/select-rate"
|
||||||
|
|
||||||
import Select from "@/components/TempDesignSystem/Select"
|
|
||||||
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
import styles from "./bookingCodeFilter.module.css"
|
import styles from "./bookingCodeFilter.module.css"
|
||||||
|
|
||||||
import type { Key } from "react"
|
|
||||||
|
|
||||||
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
||||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||||
|
|
||||||
export default function BookingCodeFilter() {
|
export default function BookingCodeFilter() {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const displayAsPopover = useMediaQuery("(min-width: 768px)")
|
||||||
const utils = trpc.useUtils()
|
const utils = trpc.useUtils()
|
||||||
const {
|
const {
|
||||||
actions: { appendRegularRates, selectFilter },
|
actions: { appendRegularRates, selectFilter },
|
||||||
@@ -36,12 +51,6 @@ export default function BookingCodeFilter() {
|
|||||||
}),
|
}),
|
||||||
value: BookingCodeFilterEnum.Discounted,
|
value: BookingCodeFilterEnum.Discounted,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: intl.formatMessage({
|
|
||||||
defaultMessage: "Full-priced rooms",
|
|
||||||
}),
|
|
||||||
value: BookingCodeFilterEnum.Regular,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: intl.formatMessage({
|
label: intl.formatMessage({
|
||||||
defaultMessage: "All rates",
|
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)
|
selectFilter(selectedFilter as BookingCodeFilterEnum)
|
||||||
const room = await utils.hotel.availability.selectRate.room.fetch({
|
if (selectedFilter === BookingCodeFilterEnum.All) {
|
||||||
booking: {
|
const room = await utils.hotel.availability.selectRate.room.fetch({
|
||||||
...booking,
|
booking: {
|
||||||
room: {
|
...booking,
|
||||||
...bookingRoom,
|
room: {
|
||||||
bookingCode:
|
...bookingRoom,
|
||||||
selectedFilter === BookingCodeFilterEnum.Discounted
|
bookingCode: undefined,
|
||||||
? booking.bookingCode
|
packages: selectedPackages.map((pkg) => pkg.code),
|
||||||
: undefined,
|
},
|
||||||
packages: selectedPackages.map((pkg) => pkg.code),
|
|
||||||
},
|
},
|
||||||
},
|
lang,
|
||||||
lang,
|
})
|
||||||
})
|
appendRegularRates(room?.roomConfigurations)
|
||||||
appendRegularRates(room?.roomConfigurations)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideFilter = rooms.some((room) =>
|
const hideFilter = rooms.some((room) =>
|
||||||
@@ -92,18 +100,121 @@ export default function BookingCodeFilter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.bookingCodeFilter}>
|
<>
|
||||||
<Select
|
<div className={styles.bookingCodeFilter}>
|
||||||
aria-label={intl.formatMessage({
|
<DialogTrigger isOpen={isOpen} onOpenChange={setIsOpen}>
|
||||||
defaultMessage: "Booking Code filter",
|
<ChipButton variant="Outlined">
|
||||||
})}
|
{
|
||||||
className={styles.bookingCodeFilterSelect}
|
bookingCodeFilterItems.find(
|
||||||
name="bookingCodeFilter"
|
(item) => item.value === selectedFilter
|
||||||
onSelect={handleChangeFilter}
|
)?.label
|
||||||
label=""
|
}
|
||||||
items={bookingCodeFilterItems}
|
<MaterialIcon
|
||||||
defaultSelectedKey={selectedFilter}
|
icon="keyboard_arrow_down"
|
||||||
/>
|
size={20}
|
||||||
</div>
|
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 {
|
.filters {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--Space-x1);
|
gap: var(--Space-x1);
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
|
|||||||
@@ -52,10 +52,6 @@ export default function Campaign({
|
|||||||
campaign = campaign.filter((product) => product.bookingCode)
|
campaign = campaign.filter((product) => product.bookingCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedFilter === BookingCodeFilterEnum.Regular) {
|
|
||||||
campaign = campaign.filter((product) => !product.bookingCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
const pkgsSum = sumPackages(selectedPackages)
|
const pkgsSum = sumPackages(selectedPackages)
|
||||||
const pkgsSumRequested = sumPackagesRequestedPrice(selectedPackages)
|
const pkgsSumRequested = sumPackagesRequestedPrice(selectedPackages)
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
import { calculatePricePerNightPriceProduct } from "./totalPricePerNight"
|
import { calculatePricePerNightPriceProduct } from "./totalPricePerNight"
|
||||||
|
|
||||||
import type { SharedRateCardProps } from "@/types/components/hotelReservation/selectRate/rates"
|
import type { SharedRateCardProps } from "@/types/components/hotelReservation/selectRate/rates"
|
||||||
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
|
||||||
import type { CodeProduct } from "@/types/trpc/routers/hotel/roomAvailability"
|
import type { CodeProduct } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
|
|
||||||
interface CodeProps extends SharedRateCardProps {
|
interface CodeProps extends SharedRateCardProps {
|
||||||
@@ -35,8 +34,7 @@ export default function Code({
|
|||||||
roomTypeCode,
|
roomTypeCode,
|
||||||
}: CodeProps) {
|
}: CodeProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const { roomNr, selectedFilter, selectedPackages, selectedRate } =
|
const { roomNr, selectedPackages, selectedRate } = useRoomContext()
|
||||||
useRoomContext()
|
|
||||||
const bookingCode = useRatesStore((state) => state.booking.bookingCode)
|
const bookingCode = useRatesStore((state) => state.booking.bookingCode)
|
||||||
const rateTitles = useRateTitles()
|
const rateTitles = useRateTitles()
|
||||||
const night = intl
|
const night = intl
|
||||||
@@ -45,10 +43,6 @@ export default function Code({
|
|||||||
})
|
})
|
||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
|
|
||||||
if (selectedFilter === BookingCodeFilterEnum.Regular) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return code.map((product) => {
|
return code.map((product) => {
|
||||||
let bannerText = ""
|
let bannerText = ""
|
||||||
if (product.rateDefinition.breakfastIncluded) {
|
if (product.rateDefinition.breakfastIncluded) {
|
||||||
@@ -155,15 +149,16 @@ export default function Code({
|
|||||||
pkgsSumRequested.price
|
pkgsSumRequested.price
|
||||||
)
|
)
|
||||||
|
|
||||||
const approximateRate = pricePerNight.totalRequestedPrice
|
const approximateRate =
|
||||||
? {
|
pricePerNight.totalRequestedPrice && requestedPrice?.currency
|
||||||
label: intl.formatMessage({
|
? {
|
||||||
defaultMessage: "Approx.",
|
label: intl.formatMessage({
|
||||||
}),
|
defaultMessage: "Approx.",
|
||||||
price: pricePerNight.totalRequestedPrice,
|
}),
|
||||||
unit: localPrice.currency,
|
price: pricePerNight.totalRequestedPrice,
|
||||||
}
|
unit: requestedPrice.currency,
|
||||||
: undefined
|
}
|
||||||
|
: undefined
|
||||||
|
|
||||||
const regularPricePerNight = calculatePricePerNightPriceProduct(
|
const regularPricePerNight = calculatePricePerNightPriceProduct(
|
||||||
localPrice.regularPricePerNight,
|
localPrice.regularPricePerNight,
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export default function Redemptions({
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
selectedFilter === BookingCodeFilterEnum.Discounted ||
|
selectedFilter === BookingCodeFilterEnum.Discounted ||
|
||||||
selectedFilter === BookingCodeFilterEnum.Regular ||
|
|
||||||
!redemptions.length
|
!redemptions.length
|
||||||
) {
|
) {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -53,21 +53,14 @@ export default function Rates({
|
|||||||
roomTypeCode,
|
roomTypeCode,
|
||||||
}
|
}
|
||||||
const showAllRates = selectedFilter === BookingCodeFilterEnum.All
|
const showAllRates = selectedFilter === BookingCodeFilterEnum.All
|
||||||
const showRegularRates = selectedFilter === BookingCodeFilterEnum.Regular
|
|
||||||
const hasBookingCodeRates = !!(campaign.length || code.length)
|
const hasBookingCodeRates = !!(campaign.length || code.length)
|
||||||
const hasRegularRates = !!regular.length
|
const hasRegularRates = !!regular.length
|
||||||
const notDiscountedPrice = showAllRates || showRegularRates
|
const showDivider = showAllRates && hasBookingCodeRates && hasRegularRates
|
||||||
const notDiscountedAndHasRates =
|
|
||||||
notDiscountedPrice && hasBookingCodeRates && hasRegularRates
|
|
||||||
const isFetchingAndOnlyShowingRegularRates =
|
|
||||||
isFetchingAdditionalRate && !showRegularRates
|
|
||||||
const showDivider =
|
|
||||||
notDiscountedAndHasRates || isFetchingAndOnlyShowingRegularRates
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Campaign {...sharedProps} campaign={campaign} />
|
|
||||||
<Code {...sharedProps} code={code} />
|
<Code {...sharedProps} code={code} />
|
||||||
|
<Campaign {...sharedProps} campaign={campaign} />
|
||||||
<Redemptions {...sharedProps} redemptions={redemptions} />
|
<Redemptions {...sharedProps} redemptions={redemptions} />
|
||||||
{showDivider ? <Divider color="borderDividerSubtle" /> : null}
|
{showDivider ? <Divider color="borderDividerSubtle" /> : null}
|
||||||
{isFetchingAdditionalRate ? (
|
{isFetchingAdditionalRate ? (
|
||||||
|
|||||||
@@ -4,11 +4,18 @@ interface IconChipProps {
|
|||||||
color: "blue" | "green" | "red" | null | undefined
|
color: "blue" | "green" | "red" | null | undefined
|
||||||
icon: React.ReactNode
|
icon: React.ReactNode
|
||||||
children: 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({
|
const classNames = iconChipVariants({
|
||||||
color: color,
|
color: color,
|
||||||
|
className: className,
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<div className={classNames}>
|
<div className={classNames}>
|
||||||
|
|||||||
@@ -95,14 +95,25 @@ export const hotelSchema = z
|
|||||||
export const hotelsAvailabilitySchema = z.object({
|
export const hotelsAvailabilitySchema = z.object({
|
||||||
data: z.array(
|
data: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
attributes: z.object({
|
attributes: z
|
||||||
checkInDate: z.string(),
|
.object({
|
||||||
checkOutDate: z.string(),
|
bookingCode: z.string().nullish(),
|
||||||
hotelId: z.number(),
|
checkInDate: z.string(),
|
||||||
occupancy: occupancySchema,
|
checkOutDate: z.string(),
|
||||||
productType: productTypeSchema,
|
hotelId: z.number(),
|
||||||
status: z.string(),
|
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(),
|
relationships: relationshipsSchema.optional(),
|
||||||
type: z.string().optional(),
|
type: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const partialPriceSchema = z.object({
|
|||||||
}
|
}
|
||||||
return RateTypeEnum.Regular
|
return RateTypeEnum.Regular
|
||||||
}),
|
}),
|
||||||
|
bookingCode: z.string().nullish(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const productTypeCorporateChequeSchema = z
|
export const productTypeCorporateChequeSchema = z
|
||||||
|
|||||||
@@ -152,17 +152,13 @@ export function createRatesStore({
|
|||||||
room.counterRateCode
|
room.counterRateCode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
let selectedFilter
|
|
||||||
const bookingCode = room.rateCode
|
const bookingCode = room.rateCode
|
||||||
? room.bookingCode
|
? room.bookingCode
|
||||||
: booking.bookingCode
|
: booking.bookingCode
|
||||||
if (isRedemptionBooking) {
|
const selectedFilter =
|
||||||
selectedFilter = BookingCodeFilterEnum.All
|
bookingCode && !isRedemptionBooking
|
||||||
} else if (bookingCode) {
|
? BookingCodeFilterEnum.Discounted
|
||||||
selectedFilter = BookingCodeFilterEnum.Discounted
|
: BookingCodeFilterEnum.All
|
||||||
} else {
|
|
||||||
selectedFilter = BookingCodeFilterEnum.Regular
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
actions: {
|
actions: {
|
||||||
@@ -301,7 +297,8 @@ export function createRatesStore({
|
|||||||
return set(
|
return set(
|
||||||
produce((state: RatesState) => {
|
produce((state: RatesState) => {
|
||||||
state.rooms[idx].selectedFilter = filter
|
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"]
|
type ImageMetaData = z.infer<typeof imageSchema>["metaData"]
|
||||||
|
|
||||||
export type HotelPin = {
|
export type HotelPin = {
|
||||||
|
bookingCode?: string | null
|
||||||
name: string
|
name: string
|
||||||
coordinates: Coordinates
|
coordinates: Coordinates
|
||||||
publicPrice: number | null
|
publicPrice: number | null
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import type { Hotel } from "@/types/hotel"
|
|||||||
|
|
||||||
export type NoAvailabilityAlertProp = {
|
export type NoAvailabilityAlertProp = {
|
||||||
hotelsLength: number
|
hotelsLength: number
|
||||||
|
bookingCode?: string
|
||||||
isAllUnavailable: boolean
|
isAllUnavailable: boolean
|
||||||
isAlternative?: boolean
|
isAlternative?: boolean
|
||||||
|
isBookingCodeRateNotAvailable?: boolean
|
||||||
operaId: Hotel["operaId"]
|
operaId: Hotel["operaId"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
export enum BookingCodeFilterEnum {
|
export enum BookingCodeFilterEnum {
|
||||||
Discounted = "Discounted",
|
Discounted = "Discounted",
|
||||||
Regular = "Regular",
|
|
||||||
All = "All",
|
All = "All",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default function CampaignRateCard({
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<div className={classNames}>
|
<div className={classNames}>
|
||||||
<Typography variant="Tag/sm">
|
<Typography variant="Label/xsBold">
|
||||||
<p className={styles.banner}>{bannerText}</p>
|
<p className={styles.banner}>{bannerText}</p>
|
||||||
</Typography>
|
</Typography>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export default function CodeRateCard({
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<div className={classNames}>
|
<div className={classNames}>
|
||||||
<Typography variant="Tag/sm">
|
<Typography variant="Label/xsBold">
|
||||||
<p className={styles.banner}>{bannerText}</p>
|
<p className={styles.banner}>{bannerText}</p>
|
||||||
</Typography>
|
</Typography>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default function PointsRateCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames}>
|
<div className={classNames}>
|
||||||
<Typography variant="Tag/sm">
|
<Typography variant="Label/xsBold">
|
||||||
<p className={styles.banner}>{bannerText}</p>
|
<p className={styles.banner}>{bannerText}</p>
|
||||||
</Typography>
|
</Typography>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
|
|||||||
@@ -152,35 +152,15 @@ label:not(:has(.radio:checked)) .checkIcon {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--Space-x1);
|
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 {
|
.variant-campaign .banner {
|
||||||
background-color: var(--Surface-Brand-Primary-1-OnSurface-Accent);
|
background-color: var(--Surface-Accent-3);
|
||||||
}
|
|
||||||
|
|
||||||
.variant-code {
|
|
||||||
background-color: var(--Surface-Feedback-Information);
|
|
||||||
}
|
|
||||||
|
|
||||||
.variant-code:hover {
|
|
||||||
background-color: var(--Scandic-Blue-10);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.variant-code .banner {
|
.variant-code .banner {
|
||||||
background-color: var(--Surface-Feedback-Information-Accent);
|
background-color: var(--Surface-Feedback-Information-Accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.variant-points:hover {
|
|
||||||
background-color: var(--Scandic-Grey-00);
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.variant-points .banner {
|
.variant-points .banner {
|
||||||
background-color: var(--Surface-Brand-Primary-1-OnSurface-Accent-Secondary);
|
background-color: var(--Surface-Brand-Primary-1-OnSurface-Accent-Secondary);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user