'use client' import { cx } from 'class-variance-authority' import { type ReadonlyURLSearchParams, useSearchParams } from 'next/navigation' import { memo, useState } from 'react' import { useFocusWithin } from 'react-aria' import { useIntl } from 'react-intl' import { alternativeHotelsMap, selectHotelMap, selectRate, } from '@scandic-hotels/common/constants/routes/hotelReservation' import { getSingleDecimal } from '@scandic-hotels/common/utils/numberFormatting' import Caption from '../Caption' import { Divider } from '../Divider' import { FacilityToIcon } from '../FacilityToIcon' import HotelLogoIcon from '../Icons/Logos' import ImageGallery, { GalleryImage } from '../ImageGallery' import Link from '../OldDSLink' import { Typography } from '../Typography' import { HotelPointsRow } from './HotelPointsRow' import { NoPriceAvailableCard } from './NoPriceAvailableCard' import HotelChequeCard from './HotelChequeCard' import { HotelPriceCard } from './HotelPriceCard' import HotelVoucherCard from './HotelVoucherCard' import { hotelCardVariants } from './variants' import styles from './hotelCard.module.css' import { CurrencyEnum } from '@scandic-hotels/common/constants/currency' import { FacilityEnum } from '@scandic-hotels/common/constants/facilities' import { HotelType } from '@scandic-hotels/common/constants/hotelType' import type { Lang } from '@scandic-hotels/common/constants/language' import { RateTypeEnum } from '@scandic-hotels/common/constants/rateType' import { BookingCodeChip } from '../BookingCodeChip' import { TripAdvisorChip } from '../TripAdvisorChip' type Price = { pricePerStay: number pricePerNight: number currency: string } export type HotelCardProps = { hotel: { id: string hotelType: HotelType name: string description?: string detailedFacilities: { name: string; id: FacilityEnum }[] address: { city: string streetAddress: string } ratings?: { tripAdvisor?: number } } prices: | { public?: { rateType: RateTypeEnum localPrice: Price requestedPrice?: Price } member?: { rateType: RateTypeEnum localPrice: Price requestedPrice?: Price } voucher?: { numberOfVouchers: number rateCode: string rateType: RateTypeEnum } bonusCheque?: { rateCode: string rateType: RateTypeEnum localPrice: { additionalPricePerStay: number currency: CurrencyEnum | null | undefined numberOfCheques: number } requestedPrice?: { additionalPricePerStay: number currency: CurrencyEnum | null | undefined numberOfCheques: number } } redemptions?: { rateCode: string hasEnoughPoints: boolean localPrice: { additionalPricePerStay: number pointsPerStay: number currency: CurrencyEnum | null | undefined } }[] } | undefined images: GalleryImage[] distanceToCityCenter: number isUserLoggedIn: boolean type?: 'mapListing' | 'pageListing' state?: 'default' | 'active' bookingCode?: string | null isAlternative?: boolean isPartnerBrand: boolean pointsCurrency?: CurrencyEnum fullPrice: boolean isCampaignWithBookingCode: boolean lang: Lang belowInfoSlot: React.ReactNode onHover: () => void onHoverEnd: () => void onFocusIn: () => void onFocusOut: () => void onAddressClick: () => void } export const HotelCardComponent = memo( ({ prices, hotel, distanceToCityCenter, isUserLoggedIn, state = 'default', type = 'pageListing', bookingCode = '', isAlternative, isPartnerBrand, pointsCurrency, images, lang, belowInfoSlot, fullPrice, isCampaignWithBookingCode, onAddressClick, onHover, onHoverEnd, onFocusIn, onFocusOut, }: HotelCardProps) => { const searchParams = useSearchParams() const [isFocusWithin, setIsFocusWithin] = useState(false) const { focusWithinProps } = useFocusWithin({ onFocusWithin: onFocusIn, onBlurWithin: onFocusOut, onFocusWithinChange: (isFocusWithin) => { setIsFocusWithin(isFocusWithin) }, }) const intl = useIntl() const amenities = hotel.detailedFacilities.slice(0, 5) const classNames = hotelCardVariants({ type, state, }) const mapUrl = isAlternative ? alternativeHotelsMap(lang) : selectHotelMap(lang) const handleAddressClick = (event: React.MouseEvent) => { event.preventDefault() onAddressClick() } const addressStr = `${hotel.address.streetAddress}, ${hotel.address.city}` const hasInsufficientPoints = !prices?.redemptions?.some( (r) => r.hasEnoughPoints ) const notEnoughPointsLabel = intl.formatMessage({ id: 'booking.notEnoughPoints', defaultMessage: 'Not enough points', }) const isDisabled = prices?.redemptions?.length && hasInsufficientPoints const isCampaign = prices?.public?.rateType === RateTypeEnum.PublicPromotion || prices?.member?.rateType === RateTypeEnum.PublicPromotion const showBookingCodeChip = bookingCode || isCampaign function onMouseEnter() { if (!isFocusWithin) { onHover() } } function onMouseLeave() { if (!isFocusWithin) { onHoverEnd() } } return (
{hotel.ratings?.tripAdvisor && ( )}

{hotel.name}

{type == 'mapListing' && (

{addressStr}

)} {type === 'pageListing' && (

{addressStr}

)}
{intl.formatMessage( { id: 'common.kmToCityCenter', defaultMessage: '{number} km to city center', }, { number: getSingleDecimal(distanceToCityCenter / 1000), } )}
{hotel.description ? (

{hotel.description}

) : null}
{amenities.map((facility) => (
{facility.name}
))}
{belowInfoSlot}
{!prices ? ( ) : ( <> {showBookingCodeChip && ( )} {(!isUserLoggedIn || !prices?.member || (bookingCode && !fullPrice)) && prices?.public && ( )} {prices.member && ( )} {prices?.voucher && ( )} {prices?.bonusCheque && ( )} {prices?.redemptions?.length ? (
{intl.formatMessage({ id: 'hotelCard.availableRates', defaultMessage: 'Available rates', })} {prices.redemptions.map((redemption) => ( ))}
) : null} {isDisabled ? (
{notEnoughPointsLabel}
) : (
{intl.formatMessage({ id: 'common.seeRooms', defaultMessage: 'See rooms', })}
)} )}
) } ) export const HotelCard = HotelCardComponent as React.MemoExoticComponent< (props: HotelCardProps) => React.ReactElement > 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 =
{children}
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 ( {content} ) }