Files
web/apps/scandic-web/components/HotelReservation/HotelCard/index.tsx
Matilda Landström 1239f0c662 Merged in feat/SW-1711-SW-2077-icons (pull request #1709)
Fix(SW-1711)/(SW-2077): Export icons individually

* fix(SW-1711): export icons individually


Approved-by: Michael Zetterberg
Approved-by: Erik Tiekstra
2025-04-07 07:25:25 +00:00

276 lines
9.8 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 HotelLogoIcon from "@scandic-hotels/design-system/Icons/HotelLogoIcon"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
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}>
<HotelLogoIcon
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)