"use client" import NextLink from "next/link" 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 { 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 { FakeButton } from "../FakeButton" import { TripAdvisorChip } from "../TripAdvisorChip" import { PointType } from "@scandic-hotels/common/constants/pointType" 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 pointsType?: PointType | null } }[] } | undefined images: GalleryImage[] distanceToCityCenter: number isUserLoggedIn: boolean type?: "mapListing" | "pageListing" state?: "default" | "active" bookingCode?: string | null isAlternative?: boolean isPartnerBrand: boolean 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, images, lang, belowInfoSlot, fullPrice, isCampaignWithBookingCode, onAddressClick, onHover, onHoverEnd, onFocusIn, onFocusOut, }: HotelCardProps) => { const searchParams = useSearchParams() const [isFocusWithin, setIsFocusWithin] = useState(false) const [isPricesHovered, setIsPricesHovered] = 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}
setIsPricesHovered(true)} onHoverEnd={() => setIsPricesHovered(false)} > {!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 onHoverStart: () => void onHoverEnd: () => void } function PricesWrapper({ children, hotelId, isClickable, pathname, removeBookingCodeFromSearchParams, searchParams, onHoverStart, onHoverEnd, }: 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} ) }