{hotel.name}
{addressStr}
{addressStr}
{hotel.description}
{intl.formatMessage({ id: "hotelCard.availableRates", defaultMessage: "Available rates", })}
"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 (
{addressStr} {addressStr} {hotel.description}
{intl.formatMessage({
id: "hotelCard.availableRates",
defaultMessage: "Available rates",
})}
{hotel.name}