fix(SW-1491-SW-1500): address on hotel card should go to map, remove link on maplisting view * fix(SW-1491-SW-1500): address on hotel card should go to map, remove link on maplisting view * fix(SW-1491-SW-1500): fix comment * fix(SW-1491-SW-1500): add underscore Approved-by: Niclas Edenvin
272 lines
9.7 KiB
TypeScript
272 lines
9.7 KiB
TypeScript
"use client"
|
|
|
|
import { useParams } from "next/dist/client/components/navigation"
|
|
import { useRouter, useSearchParams } from "next/navigation"
|
|
import { memo, useCallback } from "react"
|
|
import { useIntl } from "react-intl"
|
|
|
|
import { HotelLogo, MaterialIcon } from "@scandic-hotels/design-system/Icons"
|
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
|
|
import { selectHotelMap, selectRate } from "@/constants/routes/hotelReservation"
|
|
import { useHotelsMapStore } from "@/stores/hotels-map"
|
|
|
|
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
|
import ImageGallery from "@/components/ImageGallery"
|
|
import Button from "@/components/TempDesignSystem/Button"
|
|
import Divider from "@/components/TempDesignSystem/Divider"
|
|
import IconChip from "@/components/TempDesignSystem/IconChip"
|
|
import Link from "@/components/TempDesignSystem/Link"
|
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
|
import { Tooltip } from "@/components/TempDesignSystem/Tooltip"
|
|
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
|
import { getSingleDecimal } from "@/utils/numberFormatting"
|
|
|
|
import ReadMore from "../ReadMore"
|
|
import TripAdvisorChip from "../TripAdvisorChip"
|
|
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 { HotelCardListingTypeEnum } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
|
import type { HotelCardProps } from "@/types/components/hotelReservation/selectHotel/hotelCardProps"
|
|
import { RateTypeEnum } from "@/types/enums/rateType"
|
|
import type { Lang } from "@/constants/languages"
|
|
|
|
function HotelCard({
|
|
hotelData: { availability, hotel },
|
|
isUserLoggedIn,
|
|
state = "default",
|
|
type = HotelCardListingTypeEnum.PageListing,
|
|
bookingCode = "",
|
|
}: HotelCardProps) {
|
|
const params = useParams()
|
|
const searchParams = useSearchParams()
|
|
|
|
const lang = params.lang as Lang
|
|
const intl = useIntl()
|
|
const { setActiveHotelPin, setActiveHotelCard } = useHotelsMapStore()
|
|
|
|
const handleMouseEnter = useCallback(() => {
|
|
setActiveHotelPin(hotel.name)
|
|
}, [setActiveHotelPin, hotel])
|
|
|
|
const handleMouseLeave = useCallback(() => {
|
|
setActiveHotelPin(null)
|
|
setActiveHotelCard(null)
|
|
}, [setActiveHotelPin, setActiveHotelCard])
|
|
|
|
const amenities = hotel.detailedFacilities.slice(0, 5)
|
|
const router = useRouter()
|
|
const classNames = hotelCardVariants({
|
|
type,
|
|
state,
|
|
})
|
|
|
|
const handleAddressClick = (event: React.MouseEvent) => {
|
|
event.preventDefault()
|
|
setActiveHotelPin(hotel.name)
|
|
setActiveHotelCard(hotel.name)
|
|
router.push(`${selectHotelMap(lang)}?${searchParams.toString()}`)
|
|
}
|
|
|
|
const addressStr = `${hotel.address.streetAddress}, ${hotel.address.city}`
|
|
const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || [])
|
|
const fullPrice =
|
|
availability.productType?.public?.rateType === RateTypeEnum.Regular ||
|
|
availability.productType?.member?.rateType === RateTypeEnum.Regular
|
|
const price = availability.productType
|
|
|
|
const hasInsufficientPoints = !price?.redemptions?.some(
|
|
(r) => r.hasEnoughPoints
|
|
)
|
|
const notEnoughPointsLabel = intl.formatMessage({ id: "Not enough points" })
|
|
|
|
return (
|
|
<article
|
|
className={classNames}
|
|
onMouseEnter={handleMouseEnter}
|
|
onMouseLeave={handleMouseLeave}
|
|
>
|
|
<div>
|
|
<div className={styles.imageContainer}>
|
|
<ImageGallery title={hotel.name} images={galleryImages} fill />
|
|
{hotel.ratings?.tripAdvisor && (
|
|
<TripAdvisorChip rating={hotel.ratings.tripAdvisor.rating} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className={styles.hotelContent}>
|
|
<section className={styles.hotelInformation}>
|
|
<div className={styles.titleContainer}>
|
|
<HotelLogo hotelId={hotel.operaId} hotelType={hotel.hotelType} />
|
|
<Subtitle textTransform="capitalize" color="uiTextHighContrast">
|
|
{hotel.name}
|
|
</Subtitle>
|
|
<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"
|
|
color="burgundy"
|
|
variant="underscored"
|
|
onClick={handleAddressClick}
|
|
href={selectHotelMap(lang)}
|
|
keepSearchParams
|
|
aria-label={intl.formatMessage({
|
|
id: "See on map",
|
|
})}
|
|
>
|
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
|
<p>{addressStr}</p>
|
|
</Typography>
|
|
</Link>
|
|
)}
|
|
</address>
|
|
|
|
<div>
|
|
<Divider variant="vertical" color="subtle" />
|
|
</div>
|
|
<Caption color="uiTextPlaceholder">
|
|
{intl.formatMessage(
|
|
{ id: "{number} km to city center" },
|
|
{
|
|
number: getSingleDecimal(
|
|
hotel.location.distanceToCentre / 1000
|
|
),
|
|
}
|
|
)}
|
|
</Caption>
|
|
</div>
|
|
</div>
|
|
<Body className={styles.hotelDescription}>
|
|
{hotel.hotelContent.texts.descriptions?.short}
|
|
</Body>
|
|
<div className={styles.facilities}>
|
|
{amenities.map((facility) => {
|
|
const Icon = (
|
|
<FacilityToIcon id={facility.id} color="Icon/Default" />
|
|
)
|
|
return (
|
|
<div className={styles.facilitiesItem} key={facility.id}>
|
|
{Icon && Icon}
|
|
<Caption color="uiTextMediumContrast">
|
|
{facility.name}
|
|
</Caption>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
<ReadMore
|
|
label={intl.formatMessage({ id: "See hotel details" })}
|
|
hotelId={hotel.operaId}
|
|
hotel={hotel}
|
|
showCTA={true}
|
|
/>
|
|
</section>
|
|
<div className={styles.prices}>
|
|
{!availability.productType ? (
|
|
<NoPriceAvailableCard />
|
|
) : (
|
|
<>
|
|
{bookingCode && (
|
|
<span className={`${fullPrice ? styles.strikedText : ""}`}>
|
|
<IconChip
|
|
color="blue"
|
|
icon={<MaterialIcon icon="sell" size={20} />}
|
|
>
|
|
{bookingCode}
|
|
</IconChip>
|
|
</span>
|
|
)}
|
|
{(!isUserLoggedIn ||
|
|
!price?.member ||
|
|
(bookingCode && !fullPrice)) &&
|
|
price?.public && (
|
|
<HotelPriceCard productTypePrices={price.public} />
|
|
)}
|
|
{availability.productType.member && (
|
|
<HotelPriceCard
|
|
productTypePrices={availability.productType.member}
|
|
isMemberPrice
|
|
/>
|
|
)}
|
|
{price?.voucher && (
|
|
<HotelVoucherCard productTypeVoucher={price.voucher} />
|
|
)}
|
|
{price?.bonusCheque && (
|
|
<HotelChequeCard productTypeCheque={price.bonusCheque} />
|
|
)}
|
|
{price?.redemptions?.length ? (
|
|
<div className={styles.pointsCard}>
|
|
<Caption>
|
|
{intl.formatMessage({ id: "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}
|
|
{price?.redemptions?.length && hasInsufficientPoints ? (
|
|
<Tooltip
|
|
arrow="left"
|
|
position="bottom"
|
|
text={notEnoughPointsLabel}
|
|
>
|
|
<Button
|
|
theme="base"
|
|
intent="primary"
|
|
size="small"
|
|
className={styles.button}
|
|
disabled
|
|
>
|
|
{notEnoughPointsLabel}
|
|
</Button>
|
|
</Tooltip>
|
|
) : (
|
|
<Button
|
|
asChild
|
|
theme="base"
|
|
intent="primary"
|
|
size="small"
|
|
className={styles.button}
|
|
>
|
|
<Link
|
|
href={`${selectRate(lang)}?hotel=${hotel.operaId}`}
|
|
color="none"
|
|
keepSearchParams
|
|
>
|
|
{intl.formatMessage({ id: "See rooms" })}
|
|
</Link>
|
|
</Button>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</article>
|
|
)
|
|
}
|
|
|
|
export default memo(HotelCard)
|