feat(SW-2873): Move HotelReservationSidePeek to booking-flow * Move sidepeek store to booking-flow * Begin move of HotelReservationSidePeek to booking-flow * Copy Link * Update AccessibilityAccordionItem * Split AccessibilityAccordionItem into two components * Fix tracking for Accordion * Duplicate ButtonLink to booking-flow TEMP * AdditionalAmeneties * wip * Move sidepeek accordion items * Remove temp ButtonLink * Merge branch 'master' into feat/sw-3218-move-hotelreservationsidepeek-to-booking-flow * Fix accordion tracking * Merge branch 'master' into feat/sw-3218-move-hotelreservationsidepeek-to-booking-flow * Update exports * Fix self-referencing import * Merge branch 'master' into feat/sw-3218-move-hotelreservationsidepeek-to-booking-flow * Add 'use client' to tracking function * Merge branch 'master' into feat/sw-3218-move-hotelreservationsidepeek-to-booking-flow * Fix TEMP folder * Refactor sidepeek tracking * Merge branch 'master' into feat/sw-3218-move-hotelreservationsidepeek-to-booking-flow Approved-by: Joakim Jäderberg
321 lines
11 KiB
TypeScript
321 lines
11 KiB
TypeScript
"use client"
|
|
|
|
import { cx } from "class-variance-authority"
|
|
import {
|
|
type ReadonlyURLSearchParams,
|
|
useParams,
|
|
useRouter,
|
|
useSearchParams,
|
|
} from "next/navigation"
|
|
import { memo } from "react"
|
|
import { useIntl } from "react-intl"
|
|
|
|
import TripAdvisorChip from "@scandic-hotels/booking-flow/components/TripAdvisorChip"
|
|
import { SidePeekEnum } from "@scandic-hotels/booking-flow/stores/sidepeek"
|
|
import {
|
|
alternativeHotelsMap,
|
|
selectHotelMap,
|
|
selectRate,
|
|
} from "@scandic-hotels/common/constants/routes/hotelReservation"
|
|
import { getSingleDecimal } from "@scandic-hotels/common/utils/numberFormatting"
|
|
import Caption from "@scandic-hotels/design-system/Caption"
|
|
import { Divider } from "@scandic-hotels/design-system/Divider"
|
|
import HotelLogoIcon from "@scandic-hotels/design-system/Icons/HotelLogoIcon"
|
|
import ImageGallery from "@scandic-hotels/design-system/ImageGallery"
|
|
import Link from "@scandic-hotels/design-system/Link"
|
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
|
|
import { useHotelsMapStore } from "@/stores/hotels-map"
|
|
|
|
import BookingCodeChip from "@/components/BookingCodeChip"
|
|
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
|
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
|
|
|
import ReadMore from "../ReadMore"
|
|
import HotelChequeCard from "./HotelChequeCard"
|
|
import HotelPointsRow from "./HotelPointsRow"
|
|
import HotelPriceCard from "./HotelPriceCard"
|
|
import HotelVoucherCard from "./HotelVoucherCard"
|
|
import NoPriceAvailableCard from "./NoPriceAvailableCard"
|
|
import { hotelCardVariants } from "./variants"
|
|
|
|
import styles from "./hotelCard.module.css"
|
|
|
|
import type { Lang } from "@scandic-hotels/common/constants/language"
|
|
|
|
import { HotelCardListingTypeEnum } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
|
import type { HotelCardProps } from "@/types/components/hotelReservation/selectHotel/hotelCardProps"
|
|
|
|
function HotelCard({
|
|
hotelData: { availability, hotel },
|
|
isUserLoggedIn,
|
|
state = "default",
|
|
type = HotelCardListingTypeEnum.PageListing,
|
|
bookingCode = "",
|
|
isAlternative,
|
|
}: HotelCardProps) {
|
|
const params = useParams()
|
|
const searchParams = useSearchParams()
|
|
|
|
const lang = params.lang as Lang
|
|
const intl = useIntl()
|
|
const { activate, engage, disengage, disengageAfterDelay } =
|
|
useHotelsMapStore()
|
|
|
|
const amenities = hotel.detailedFacilities.slice(0, 5)
|
|
const router = useRouter()
|
|
const classNames = hotelCardVariants({
|
|
type,
|
|
state,
|
|
})
|
|
|
|
const mapUrl = isAlternative
|
|
? alternativeHotelsMap(lang)
|
|
: selectHotelMap(lang)
|
|
const handleAddressClick = (event: React.MouseEvent) => {
|
|
event.preventDefault()
|
|
disengage() // Disengage the current hotel to avoid the hover state from being active when clicking on the address
|
|
activate(hotel.name)
|
|
router.push(`${mapUrl}?${searchParams.toString()}`)
|
|
}
|
|
|
|
const addressStr = `${hotel.address.streetAddress}, ${hotel.address.city}`
|
|
const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || [])
|
|
const fullPrice = !availability.bookingCode
|
|
const price = availability.productType
|
|
|
|
const hasInsufficientPoints = !price?.redemptions?.some(
|
|
(r) => r.hasEnoughPoints
|
|
)
|
|
const notEnoughPointsLabel = intl.formatMessage({
|
|
defaultMessage: "Not enough points",
|
|
})
|
|
|
|
const isDisabled = price?.redemptions?.length && hasInsufficientPoints
|
|
|
|
return (
|
|
<article
|
|
className={classNames}
|
|
onMouseEnter={() => engage(hotel.name)}
|
|
onMouseLeave={() => disengageAfterDelay()}
|
|
>
|
|
<div>
|
|
<div className={styles.imageContainer}>
|
|
<ImageGallery
|
|
title={hotel.name}
|
|
images={galleryImages}
|
|
fill
|
|
sizes="(min-width: 768px) calc(100vw - 340px), (min-width: 1367px) 33vw, 100vw"
|
|
/>
|
|
{hotel.ratings?.tripAdvisor && (
|
|
<TripAdvisorChip rating={hotel.ratings.tripAdvisor.rating} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className={styles.hotelContent}>
|
|
<div className={styles.hotelInformation}>
|
|
<div className={styles.titleContainer}>
|
|
<HotelLogoIcon
|
|
hotelId={hotel.operaId}
|
|
hotelType={hotel.hotelType}
|
|
/>
|
|
<Typography variant="Title/Subtitle/lg">
|
|
<h2>{hotel.name}</h2>
|
|
</Typography>
|
|
<div className={styles.addressContainer}>
|
|
<address className={styles.address}>
|
|
{type == HotelCardListingTypeEnum.MapListing ? (
|
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
|
<p>{addressStr}</p>
|
|
</Typography>
|
|
) : (
|
|
<Link
|
|
size="small"
|
|
textDecoration="underline"
|
|
onClick={handleAddressClick}
|
|
href={mapUrl}
|
|
keepSearchParams
|
|
aria-label={intl.formatMessage({
|
|
defaultMessage: "See on map",
|
|
})}
|
|
>
|
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
|
<p>{addressStr}</p>
|
|
</Typography>
|
|
</Link>
|
|
)}
|
|
</address>
|
|
|
|
<div>
|
|
<Divider variant="vertical" />
|
|
</div>
|
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
|
<span>
|
|
{intl.formatMessage(
|
|
{
|
|
defaultMessage: "{number} km to city center",
|
|
},
|
|
{
|
|
number: getSingleDecimal(
|
|
hotel.location.distanceToCentre / 1000
|
|
),
|
|
}
|
|
)}
|
|
</span>
|
|
</Typography>
|
|
</div>
|
|
</div>
|
|
|
|
{hotel.hotelContent.texts.descriptions ? (
|
|
<Typography variant="Body/Paragraph/mdRegular">
|
|
<p className={styles.hotelDescription}>
|
|
{hotel.hotelContent.texts.descriptions.short}
|
|
</p>
|
|
</Typography>
|
|
) : null}
|
|
|
|
<div className={styles.facilities}>
|
|
{amenities.map((facility) => (
|
|
<div className={styles.facilitiesItem} key={facility.id}>
|
|
<FacilityToIcon id={facility.id} color="CurrentColor" />
|
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
|
<span>{facility.name}</span>
|
|
</Typography>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<ReadMore
|
|
label={intl.formatMessage({
|
|
defaultMessage: "See hotel details",
|
|
})}
|
|
hotelId={hotel.operaId}
|
|
showCTA={true}
|
|
sidePeekKey={SidePeekEnum.hotelDetails}
|
|
/>
|
|
</div>
|
|
<PricesWrapper
|
|
pathname={selectRate(lang)}
|
|
isClickable={availability.productType && !isDisabled}
|
|
hotelId={hotel.operaId}
|
|
removeBookingCodeFromSearchParams={!!(bookingCode && fullPrice)}
|
|
searchParams={searchParams}
|
|
>
|
|
{!availability.productType ? (
|
|
<NoPriceAvailableCard />
|
|
) : (
|
|
<>
|
|
{bookingCode && (
|
|
<BookingCodeChip
|
|
bookingCode={bookingCode}
|
|
isUnavailable={fullPrice}
|
|
/>
|
|
)}
|
|
{(!isUserLoggedIn ||
|
|
!price?.member ||
|
|
(bookingCode && !fullPrice)) &&
|
|
price?.public && (
|
|
<HotelPriceCard
|
|
productTypePrices={price.public}
|
|
className={styles.priceCard}
|
|
/>
|
|
)}
|
|
{availability.productType.member && (
|
|
<HotelPriceCard
|
|
productTypePrices={availability.productType.member}
|
|
className={styles.priceCard}
|
|
isMemberPrice
|
|
/>
|
|
)}
|
|
{price?.voucher && (
|
|
<HotelVoucherCard productTypeVoucher={price.voucher} />
|
|
)}
|
|
{price?.bonusCheque && (
|
|
<HotelChequeCard productTypeCheque={price.bonusCheque} />
|
|
)}
|
|
{price?.redemptions?.length ? (
|
|
<div className={styles.pointsCard}>
|
|
<Caption>
|
|
{intl.formatMessage({
|
|
defaultMessage: "Available rates",
|
|
})}
|
|
</Caption>
|
|
{price.redemptions.map((redemption) => (
|
|
<HotelPointsRow
|
|
key={redemption.rateCode}
|
|
pointsPerStay={redemption.localPrice.pointsPerStay}
|
|
additionalPricePerStay={
|
|
redemption.localPrice.additionalPricePerStay
|
|
}
|
|
additionalPriceCurrency={
|
|
redemption.localPrice.currency ?? undefined
|
|
}
|
|
/>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
{isDisabled ? (
|
|
<div className={cx(styles.fakeButton, styles.disabled)}>
|
|
<Typography variant="Body/Paragraph/mdBold">
|
|
<span>{notEnoughPointsLabel}</span>
|
|
</Typography>
|
|
</div>
|
|
) : (
|
|
<div className={styles.fakeButton}>
|
|
<Typography variant="Body/Paragraph/mdBold">
|
|
<span>
|
|
{intl.formatMessage({
|
|
defaultMessage: "See rooms",
|
|
})}
|
|
</span>
|
|
</Typography>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</PricesWrapper>
|
|
</div>
|
|
</article>
|
|
)
|
|
}
|
|
|
|
interface PricesWrapperProps {
|
|
children: React.ReactNode
|
|
isClickable?: boolean
|
|
hotelId: string
|
|
pathname: string
|
|
removeBookingCodeFromSearchParams: boolean
|
|
searchParams: ReadonlyURLSearchParams
|
|
}
|
|
function PricesWrapper({
|
|
children,
|
|
hotelId,
|
|
isClickable,
|
|
pathname,
|
|
removeBookingCodeFromSearchParams,
|
|
searchParams,
|
|
}: PricesWrapperProps) {
|
|
const content = <div className={styles.prices}>{children}</div>
|
|
|
|
if (!isClickable) {
|
|
return content
|
|
}
|
|
|
|
const params = new URLSearchParams(searchParams)
|
|
params.delete("city")
|
|
params.set("hotel", hotelId)
|
|
|
|
if (removeBookingCodeFromSearchParams) {
|
|
params.delete("bookingCode")
|
|
}
|
|
|
|
const href = `${pathname}?${params.toString()}`
|
|
|
|
return (
|
|
<Link href={href} color="none" className={styles.link}>
|
|
{content}
|
|
</Link>
|
|
)
|
|
}
|
|
|
|
export default memo(HotelCard)
|