Merged in SW-3270-move-interactive-map-to-design-system-or-booking-flow (pull request #2681)
SW-3270 move interactive map to design system or booking flow * wip * wip * merge * wip * add support for locales in design-system * add story for HotelCard * setup alias * . * remove tracking from design-system for hotelcard * pass isUserLoggedIn * export design-system-new-deprecated.css from design-system * Add HotelMarkerByType to Storybook * Add interactive map to Storybook * fix reactintl in vitest * rename env variables * . * fix background colors * add storybook stories for <Link /> * merge * fix tracking for when clicking 'See rooms' in InteractiveMap * Merge branch 'master' of bitbucket.org:scandic-swap/web into SW-3270-move-interactive-map-to-design-system-or-booking-flow * remove deprecated comment Approved-by: Anton Gunnarsson
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import "@scandic-hotels/design-system/fonts.css"
|
||||
import "@scandic-hotels/design-system/style.css"
|
||||
import "@/public/_static/css/design-system-new-deprecated.css"
|
||||
import "@scandic-hotels/design-system/design-system-new-deprecated.css"
|
||||
import "../../globals.css"
|
||||
|
||||
import { BookingFlowTrackingProvider } from "@scandic-hotels/booking-flow/BookingFlowTrackingProvider"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type NextRequest,NextResponse } from "next/server"
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
import { AuthError } from "next-auth"
|
||||
|
||||
import { logger } from "@scandic-hotels/common/logger"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "@scandic-hotels/design-system/fonts.css"
|
||||
import "@/app/globals.css"
|
||||
import "@/public/_static/css/design-system-new-deprecated.css"
|
||||
import "@scandic-hotels/design-system/design-system-new-deprecated.css"
|
||||
import "@scandic-hotels/design-system/style.css"
|
||||
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "@scandic-hotels/design-system/fonts.css"
|
||||
import "@/app/globals.css"
|
||||
import "@/public/_static/css/design-system-new-deprecated.css"
|
||||
import "@scandic-hotels/design-system/design-system-new-deprecated.css"
|
||||
import "@scandic-hotels/design-system/style.css"
|
||||
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "@scandic-hotels/design-system/fonts.css"
|
||||
import "@/app/globals.css"
|
||||
import "@/public/_static/css/design-system-new-deprecated.css"
|
||||
import "@scandic-hotels/design-system/design-system-new-deprecated.css"
|
||||
import "@scandic-hotels/design-system/style.css"
|
||||
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@scandic-hotels/design-system/fonts.css"
|
||||
import "@/app/globals.css"
|
||||
import "@/public/_static/css/design-system-new-deprecated.css"
|
||||
import "@scandic-hotels/design-system/style.css"
|
||||
import "@scandic-hotels/design-system/design-system-new-deprecated.css"
|
||||
|
||||
import Script from "next/script"
|
||||
import { NuqsAdapter } from "nuqs/adapters/next/app"
|
||||
|
||||
@@ -3,12 +3,12 @@ import { useIntl } from "react-intl"
|
||||
import { getSingleDecimal } from "@scandic-hotels/common/utils/numberFormatting"
|
||||
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||
import { FacilityToIcon } from "@scandic-hotels/design-system/FacilityToIcon"
|
||||
import HotelLogoIcon from "@scandic-hotels/design-system/Icons/HotelLogoIcon"
|
||||
import TripadvisorIcon from "@scandic-hotels/design-system/Icons/TripadvisorIcon"
|
||||
import ImageGallery from "@scandic-hotels/design-system/ImageGallery"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||
|
||||
import styles from "./hotelListingItem.module.css"
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useIntl } from "react-intl"
|
||||
import { getSingleDecimal } from "@scandic-hotels/common/utils/numberFormatting"
|
||||
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||
import { FacilityToIcon } from "@scandic-hotels/design-system/FacilityToIcon"
|
||||
import HotelLogoIcon from "@scandic-hotels/design-system/Icons/HotelLogoIcon"
|
||||
import TripadvisorIcon from "@scandic-hotels/design-system/Icons/TripadvisorIcon"
|
||||
import ImageGallery from "@scandic-hotels/design-system/ImageGallery"
|
||||
@@ -14,7 +15,6 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
|
||||
|
||||
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||
|
||||
import styles from "./hotelListItem.module.css"
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useIntl } from "react-intl"
|
||||
import { getSingleDecimal } from "@scandic-hotels/common/utils/numberFormatting"
|
||||
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||
import { FacilityToIcon } from "@scandic-hotels/design-system/FacilityToIcon"
|
||||
import HotelLogoIcon from "@scandic-hotels/design-system/Icons/HotelLogoIcon"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import TripadvisorIcon from "@scandic-hotels/design-system/Icons/TripadvisorIcon"
|
||||
@@ -16,7 +17,6 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
|
||||
|
||||
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||
|
||||
import styles from "./hotelListingItem.module.css"
|
||||
|
||||
@@ -3,13 +3,13 @@ import { useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
||||
import { FacilityToIcon } from "@scandic-hotels/design-system/FacilityToIcon"
|
||||
import { IconButton } from "@scandic-hotels/design-system/IconButton"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
|
||||
|
||||
import { FacilityToIcon } from "../../HotelPage/data"
|
||||
import { usePageType } from "../Map/PageTypeProvider"
|
||||
import DialogImage from "./DialogImage"
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ import { type PropsWithChildren, useEffect, useRef, useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { MAP_RESTRICTIONS } from "@scandic-hotels/design-system/Map/mapConstants"
|
||||
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
|
||||
|
||||
import { MAP_RESTRICTIONS } from "@/constants/map"
|
||||
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
|
||||
|
||||
import { ErrorBoundary } from "@/components/ErrorBoundary/ErrorBoundary"
|
||||
|
||||
@@ -8,9 +8,10 @@ import {
|
||||
} from "@vis.gl/react-google-maps"
|
||||
import { useMediaQuery } from "usehooks-ts"
|
||||
|
||||
import { HotelMarkerByType } from "@scandic-hotels/design-system/Map/Markers/HotelMarkerByType"
|
||||
|
||||
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
|
||||
|
||||
import HotelMarkerByType from "@/components/Maps/Markers"
|
||||
import { trackMapClick } from "@/utils/tracking/destinationPage"
|
||||
|
||||
import HotelMapCard from "../../../HotelMapCard"
|
||||
|
||||
@@ -13,8 +13,12 @@ import { useIntl } from "react-intl"
|
||||
import { debounce } from "@scandic-hotels/common/utils/debounce"
|
||||
import { Button } from "@scandic-hotels/design-system/Button"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { InteractiveMap } from "@scandic-hotels/design-system/Map/InteractiveMap"
|
||||
|
||||
import InteractiveMap from "@/components/Maps/InteractiveMap"
|
||||
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||
|
||||
import { useIsUserLoggedIn } from "@/hooks/useIsUserLoggedIn"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import Sidebar from "./Sidebar"
|
||||
|
||||
@@ -43,11 +47,15 @@ export default function HotelMapPageClient({
|
||||
mapId,
|
||||
}: HotelMapPageClientProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const router = useRouter()
|
||||
const rootDiv = useRef<HTMLDivElement | null>(null)
|
||||
const [mapHeight, setMapHeight] = useState("100dvh")
|
||||
const [activePoi, setActivePoi] = useState<string | null>(null)
|
||||
|
||||
const isUserLoggedIn = useIsUserLoggedIn()
|
||||
const hotelMapStore = useHotelsMapStore()
|
||||
|
||||
// Calculate the height of the map based on the viewport height from the start-point (below the header and booking widget)
|
||||
const handleMapHeight = useCallback(() => {
|
||||
const topPosition = rootDiv.current?.getBoundingClientRect().top ?? 0
|
||||
@@ -132,6 +140,15 @@ export default function HotelMapPageClient({
|
||||
onActivePoiChange={(poi) => setActivePoi(poi ?? null)}
|
||||
mapId={mapId}
|
||||
markerInfo={markerInfo}
|
||||
onHoverHotelPin={(args) => {
|
||||
if (!args) {
|
||||
hotelMapStore.disengage()
|
||||
}
|
||||
hotelMapStore.engage(hotelName)
|
||||
}}
|
||||
hoveredHotelPin={hotelMapStore.hoveredHotel}
|
||||
lang={lang}
|
||||
isUserLoggedIn={isUserLoggedIn}
|
||||
/>
|
||||
</div>
|
||||
</APIProvider>
|
||||
|
||||
@@ -6,10 +6,9 @@ import { useState } from "react"
|
||||
import { Button as ButtonRAC } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { PoiMarker } from "@scandic-hotels/design-system/Map/Markers/PoiMarker"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import PoiMarker from "@/components/Maps/Markers/Poi"
|
||||
|
||||
import { translatePOIGroup } from "./util"
|
||||
|
||||
import styles from "./sidebar.module.css"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
||||
import { FacilityToIcon } from "@scandic-hotels/design-system/FacilityToIcon"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import styles from "./amenitiesList.module.css"
|
||||
|
||||
@@ -5,9 +5,9 @@ import { useEffect, useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
||||
import { PoiMarker } from "@scandic-hotels/design-system/Map/Markers/PoiMarker"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import PoiMarker from "@/components/Maps/Markers/Poi"
|
||||
import { trackHotelMapClick } from "@/utils/tracking"
|
||||
|
||||
import styles from "./mapCard.module.css"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HotelMarkerByType } from "@scandic-hotels/design-system/Map/Markers/HotelMarkerByType"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import HotelMarkerByType from "@/components/Maps/Markers"
|
||||
import StaticMapComp from "@/components/Maps/StaticMap"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { calculateLatWithOffset } from "@/utils/map"
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
import { cx } from "class-variance-authority"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { BookingCodeChip } from "@scandic-hotels/design-system/BookingCodeChip"
|
||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||
import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
|
||||
|
||||
import BookingCodeChip from "@/components/BookingCodeChip"
|
||||
|
||||
import PriceDetails from "../../PriceDetails"
|
||||
|
||||
import styles from "./totalPrice.module.css"
|
||||
|
||||
@@ -9,12 +9,12 @@ import { longDateFormat } from "@scandic-hotels/common/constants/dateFormats"
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
|
||||
import Body from "@scandic-hotels/design-system/Body"
|
||||
import { BookingCodeChip } from "@scandic-hotels/design-system/BookingCodeChip"
|
||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import Subtitle from "@scandic-hotels/design-system/Subtitle"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import BookingCodeChip from "@/components/BookingCodeChip"
|
||||
import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal"
|
||||
import { isBookingCodeRate } from "@/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils"
|
||||
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer"
|
||||
|
||||
import styles from "./HotelCardSkeleton.module.css"
|
||||
|
||||
export function HotelCardSkeleton() {
|
||||
return (
|
||||
<article className={styles.card}>
|
||||
{/* image container */}
|
||||
<div className={styles.imageContainer}>
|
||||
<SkeletonShimmer width={"100%"} height="100%" />
|
||||
</div>
|
||||
|
||||
<div className={styles.content}>
|
||||
<SkeletonShimmer height={"65px"} />
|
||||
<div className={styles.text}>
|
||||
<SkeletonShimmer height={"20px"} />
|
||||
<SkeletonShimmer height={"20px"} />
|
||||
<SkeletonShimmer height={"20px"} />
|
||||
<SkeletonShimmer height={"20px"} />
|
||||
</div>
|
||||
<SkeletonShimmer height={"56px"} />
|
||||
<SkeletonShimmer height={"52px"} width={"150px"} />
|
||||
</div>
|
||||
|
||||
<div className={styles.priceVariants}>
|
||||
{/* price variants */}
|
||||
{Array.from({ length: 2 }).map((_, index) => (
|
||||
<SkeletonShimmer key={index} height={"100px"} />
|
||||
))}
|
||||
<SkeletonShimmer height={"40px"} />
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { cx } from "class-variance-authority"
|
||||
import {
|
||||
type ReadonlyURLSearchParams,
|
||||
useParams,
|
||||
useRouter,
|
||||
useSearchParams,
|
||||
} from "next/navigation"
|
||||
import { memo } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import TripAdvisorChip from "@scandic-hotels/booking-flow/components/TripAdvisorChip"
|
||||
import {
|
||||
alternativeHotelsMap,
|
||||
selectHotelMap,
|
||||
selectRate,
|
||||
} from "@scandic-hotels/common/constants/routes/hotelReservation"
|
||||
import { getSingleDecimal } from "@scandic-hotels/common/utils/numberFormatting"
|
||||
import Caption from "@scandic-hotels/design-system/Caption"
|
||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||
import HotelLogoIcon from "@scandic-hotels/design-system/Icons/HotelLogoIcon"
|
||||
import ImageGallery from "@scandic-hotels/design-system/ImageGallery"
|
||||
import Link from "@scandic-hotels/design-system/Link"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||
|
||||
import BookingCodeChip from "@/components/BookingCodeChip"
|
||||
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||
import HotelDetailsSidePeek from "@/components/SidePeeks/HotelDetailsSidePeek"
|
||||
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||
|
||||
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 type { Lang } from "@scandic-hotels/common/constants/language"
|
||||
|
||||
import { HotelCardListingTypeEnum } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import type { HotelCardProps } from "@/types/components/hotelReservation/selectHotel/hotelCardProps"
|
||||
|
||||
function HotelCard({
|
||||
hotelData: { availability, hotel, additionalData, restaurants, url },
|
||||
isUserLoggedIn,
|
||||
state = "default",
|
||||
type = HotelCardListingTypeEnum.PageListing,
|
||||
bookingCode = "",
|
||||
isAlternative,
|
||||
}: HotelCardProps) {
|
||||
const params = useParams()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const lang = params.lang as Lang
|
||||
const intl = useIntl()
|
||||
const { activate, engage, disengage, disengageAfterDelay } =
|
||||
useHotelsMapStore()
|
||||
|
||||
const amenities = hotel.detailedFacilities.slice(0, 5)
|
||||
const router = useRouter()
|
||||
const classNames = hotelCardVariants({
|
||||
type,
|
||||
state,
|
||||
})
|
||||
|
||||
const mapUrl = isAlternative
|
||||
? alternativeHotelsMap(lang)
|
||||
: selectHotelMap(lang)
|
||||
const handleAddressClick = (event: React.MouseEvent) => {
|
||||
event.preventDefault()
|
||||
disengage() // Disengage the current hotel to avoid the hover state from being active when clicking on the address
|
||||
activate(hotel.name)
|
||||
router.push(`${mapUrl}?${searchParams.toString()}`)
|
||||
}
|
||||
|
||||
const addressStr = `${hotel.address.streetAddress}, ${hotel.address.city}`
|
||||
const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || [])
|
||||
const fullPrice = !availability.bookingCode
|
||||
const price = availability.productType
|
||||
|
||||
const hasInsufficientPoints = !price?.redemptions?.some(
|
||||
(r) => r.hasEnoughPoints
|
||||
)
|
||||
const notEnoughPointsLabel = intl.formatMessage({
|
||||
defaultMessage: "Not enough points",
|
||||
})
|
||||
|
||||
const isDisabled = price?.redemptions?.length && hasInsufficientPoints
|
||||
|
||||
return (
|
||||
<article
|
||||
className={classNames}
|
||||
onMouseEnter={() => engage(hotel.name)}
|
||||
onMouseLeave={() => disengageAfterDelay()}
|
||||
>
|
||||
<div>
|
||||
<div className={styles.imageContainer}>
|
||||
<ImageGallery
|
||||
title={hotel.name}
|
||||
images={galleryImages}
|
||||
fill
|
||||
sizes="(min-width: 768px) calc(100vw - 340px), (min-width: 1367px) 33vw, 100vw"
|
||||
/>
|
||||
{hotel.ratings?.tripAdvisor && (
|
||||
<TripAdvisorChip rating={hotel.ratings.tripAdvisor.rating} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.hotelContent}>
|
||||
<div className={styles.hotelInformation}>
|
||||
<div className={styles.titleContainer}>
|
||||
<HotelLogoIcon
|
||||
hotelId={hotel.operaId}
|
||||
hotelType={hotel.hotelType}
|
||||
/>
|
||||
<Typography variant="Title/Subtitle/lg">
|
||||
<h2>{hotel.name}</h2>
|
||||
</Typography>
|
||||
<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"
|
||||
textDecoration="underline"
|
||||
onClick={handleAddressClick}
|
||||
href={mapUrl}
|
||||
keepSearchParams
|
||||
aria-label={intl.formatMessage({
|
||||
defaultMessage: "See on map",
|
||||
})}
|
||||
>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>{addressStr}</p>
|
||||
</Typography>
|
||||
</Link>
|
||||
)}
|
||||
</address>
|
||||
|
||||
<div>
|
||||
<Divider variant="vertical" />
|
||||
</div>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<span>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "{number} km to city center",
|
||||
},
|
||||
{
|
||||
number: getSingleDecimal(
|
||||
hotel.location.distanceToCentre / 1000
|
||||
),
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hotel.hotelContent.texts.descriptions ? (
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p className={styles.hotelDescription}>
|
||||
{hotel.hotelContent.texts.descriptions.short}
|
||||
</p>
|
||||
</Typography>
|
||||
) : null}
|
||||
|
||||
<div className={styles.facilities}>
|
||||
{amenities.map((facility) => (
|
||||
<div className={styles.facilitiesItem} key={facility.id}>
|
||||
<FacilityToIcon id={facility.id} color="CurrentColor" />
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<span>{facility.name}</span>
|
||||
</Typography>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<HotelDetailsSidePeek
|
||||
hotel={{ ...hotel, url: url }}
|
||||
restaurants={restaurants}
|
||||
additionalHotelData={additionalData}
|
||||
triggerLabel={intl.formatMessage({
|
||||
defaultMessage: "See hotel details",
|
||||
})}
|
||||
buttonVariant="primary"
|
||||
/>
|
||||
</div>
|
||||
<PricesWrapper
|
||||
pathname={selectRate(lang)}
|
||||
isClickable={availability.productType && !isDisabled}
|
||||
hotelId={hotel.operaId}
|
||||
removeBookingCodeFromSearchParams={!!(bookingCode && fullPrice)}
|
||||
searchParams={searchParams}
|
||||
>
|
||||
{!availability.productType ? (
|
||||
<NoPriceAvailableCard />
|
||||
) : (
|
||||
<>
|
||||
{bookingCode && (
|
||||
<BookingCodeChip
|
||||
bookingCode={bookingCode}
|
||||
isUnavailable={fullPrice}
|
||||
/>
|
||||
)}
|
||||
{(!isUserLoggedIn ||
|
||||
!price?.member ||
|
||||
(bookingCode && !fullPrice)) &&
|
||||
price?.public && (
|
||||
<HotelPriceCard
|
||||
productTypePrices={price.public}
|
||||
className={styles.priceCard}
|
||||
/>
|
||||
)}
|
||||
{availability.productType.member && (
|
||||
<HotelPriceCard
|
||||
productTypePrices={availability.productType.member}
|
||||
className={styles.priceCard}
|
||||
isMemberPrice
|
||||
/>
|
||||
)}
|
||||
{price?.voucher && (
|
||||
<HotelVoucherCard productTypeVoucher={price.voucher} />
|
||||
)}
|
||||
{price?.bonusCheque && (
|
||||
<HotelChequeCard productTypeCheque={price.bonusCheque} />
|
||||
)}
|
||||
{price?.redemptions?.length ? (
|
||||
<div className={styles.pointsCard}>
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "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}
|
||||
{isDisabled ? (
|
||||
<div className={cx(styles.fakeButton, styles.disabled)}>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<span>{notEnoughPointsLabel}</span>
|
||||
</Typography>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.fakeButton}>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "See rooms",
|
||||
})}
|
||||
</span>
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</PricesWrapper>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
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 = <div className={styles.prices}>{children}</div>
|
||||
|
||||
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 (
|
||||
<Link href={href} color="none" className={styles.link}>
|
||||
{content}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(HotelCard)
|
||||
@@ -1,43 +0,0 @@
|
||||
import Chip from "@scandic-hotels/design-system/Chip"
|
||||
import TripadvisorIcon from "@scandic-hotels/design-system/Icons/TripadvisorIcon"
|
||||
import Image from "@scandic-hotels/design-system/Image"
|
||||
|
||||
import { hotelCardDialogImageVariants } from "./variants"
|
||||
|
||||
import styles from "./hotelCardDialogImage.module.css"
|
||||
|
||||
import type { HotelCardDialogImageProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
|
||||
export default function HotelCardDialogImage({
|
||||
firstImage,
|
||||
altText,
|
||||
rating,
|
||||
imageError,
|
||||
setImageError,
|
||||
position,
|
||||
}: HotelCardDialogImageProps) {
|
||||
const classNames = hotelCardDialogImageVariants({ position })
|
||||
|
||||
return (
|
||||
<div className={classNames}>
|
||||
{!firstImage || imageError ? (
|
||||
<div className={styles.imagePlaceholder} />
|
||||
) : (
|
||||
<Image
|
||||
src={firstImage}
|
||||
alt={altText || ""}
|
||||
fill
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
)}
|
||||
{rating ? (
|
||||
<div className={styles.tripAdvisor}>
|
||||
<Chip className={styles.tripAdvisor}>
|
||||
<TripadvisorIcon color="Icon/Interactive/Default" />
|
||||
{rating}
|
||||
</Chip>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
"use client"
|
||||
|
||||
import { useSession } from "next-auth/react"
|
||||
import { useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { selectRate } from "@scandic-hotels/common/constants/routes/hotelReservation"
|
||||
import Caption from "@scandic-hotels/design-system/Caption"
|
||||
import { FacilityToIcon } from "@scandic-hotels/design-system/FacilityToIcon"
|
||||
import { HotelCardDialogImage } from "@scandic-hotels/design-system/HotelCard/HotelCardDialogImage"
|
||||
import { HotelPointsRow } from "@scandic-hotels/design-system/HotelCard/HotelPointsRow"
|
||||
import { NoPriceAvailableCard } from "@scandic-hotels/design-system/HotelCard/NoPriceAvailableCard"
|
||||
import { IconButton } from "@scandic-hotels/design-system/IconButton"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import Link from "@scandic-hotels/design-system/Link"
|
||||
@@ -12,14 +17,9 @@ import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton
|
||||
import Subtitle from "@scandic-hotels/design-system/Subtitle"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { isValidClientSession } from "@/utils/clientSession"
|
||||
|
||||
import HotelPointsRow from "../../HotelCard/HotelPointsRow"
|
||||
import NoPriceAvailableCard from "../../HotelCard/NoPriceAvailableCard"
|
||||
import HotelCardDialogImage from "../HotelCardDialogImage"
|
||||
|
||||
import styles from "./listingHotelCardDialog.module.css"
|
||||
|
||||
import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
@@ -81,7 +81,7 @@ export default function ListingHotelCardDialog({
|
||||
<HotelCardDialogImage
|
||||
firstImage={firstImage}
|
||||
altText={altText}
|
||||
rating={ratings}
|
||||
rating={{ tripAdvisor: ratings }}
|
||||
imageError={imageError}
|
||||
setImageError={setImageError}
|
||||
position="top"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { useSession } from "next-auth/react"
|
||||
import { useEffect, useMemo, useRef } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
@@ -8,20 +9,29 @@ import {
|
||||
BookingCodeFilterEnum,
|
||||
useBookingCodeFilterStore,
|
||||
} from "@scandic-hotels/booking-flow/stores/bookingCode-filter"
|
||||
import {
|
||||
alternativeHotelsMap,
|
||||
selectHotelMap,
|
||||
} from "@scandic-hotels/common/constants/routes/hotelReservation"
|
||||
import { BackToTopButton } from "@scandic-hotels/design-system/BackToTopButton"
|
||||
import { HotelCard } from "@scandic-hotels/design-system/HotelCard"
|
||||
|
||||
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
||||
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||
|
||||
import HotelDetailsSidePeek from "@/components/SidePeeks/HotelDetailsSidePeek"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { useScrollToTop } from "@/hooks/useScrollToTop"
|
||||
import { isValidClientSession } from "@/utils/clientSession"
|
||||
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||
|
||||
import HotelCard from "../HotelCard"
|
||||
import { DEFAULT_SORT } from "../SelectHotel/HotelSorter"
|
||||
import { getSortedHotels } from "./utils"
|
||||
|
||||
import styles from "./hotelCardListing.module.css"
|
||||
|
||||
import type { HotelType } from "@scandic-hotels/common/constants/hotelType"
|
||||
|
||||
import {
|
||||
type HotelCardListingProps,
|
||||
HotelCardListingTypeEnum,
|
||||
@@ -33,13 +43,15 @@ export default function HotelCardListing({
|
||||
type = HotelCardListingTypeEnum.PageListing,
|
||||
isAlternative,
|
||||
}: HotelCardListingProps) {
|
||||
const router = useRouter()
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
const { data: session } = useSession()
|
||||
const isUserLoggedIn = isValidClientSession(session)
|
||||
const searchParams = useSearchParams()
|
||||
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
||||
const setResultCount = useHotelFilterStore((state) => state.setResultCount)
|
||||
const { activeHotel } = useHotelsMapStore()
|
||||
const { activeHotel, activate, disengage, engage } = useHotelsMapStore()
|
||||
const { showBackToTop, scrollToTop } = useScrollToTop({ threshold: 490 })
|
||||
const activeCardRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
@@ -118,32 +130,79 @@ export default function HotelCardListing({
|
||||
|
||||
return (
|
||||
<section className={styles.hotelCards}>
|
||||
{hotels?.length
|
||||
? hotels.map((hotel) => (
|
||||
<div
|
||||
key={hotel.hotel.operaId}
|
||||
ref={
|
||||
isHotelActiveInMapView(hotel.hotel.name) ? activeCardRef : null
|
||||
}
|
||||
data-active={
|
||||
isHotelActiveInMapView(hotel.hotel.name) ? "true" : "false"
|
||||
}
|
||||
>
|
||||
<HotelCard
|
||||
hotelData={hotel}
|
||||
isUserLoggedIn={isUserLoggedIn}
|
||||
state={
|
||||
isHotelActiveInMapView(hotel.hotel.name)
|
||||
? "active"
|
||||
: "default"
|
||||
}
|
||||
type={type}
|
||||
bookingCode={bookingCode}
|
||||
isAlternative={isAlternative}
|
||||
{hotels.map((hotel) => (
|
||||
<div
|
||||
key={hotel.hotel.operaId}
|
||||
ref={isHotelActiveInMapView(hotel.hotel.name) ? activeCardRef : null}
|
||||
data-active={
|
||||
isHotelActiveInMapView(hotel.hotel.name) ? "true" : "false"
|
||||
}
|
||||
>
|
||||
<HotelCard
|
||||
hotel={{
|
||||
id: hotel.hotel.operaId,
|
||||
name: hotel.hotel.name,
|
||||
address: hotel.hotel.address,
|
||||
description: hotel.hotel.hotelContent.texts.descriptions?.short,
|
||||
hotelType: hotel.hotel.hotelType as HotelType,
|
||||
detailedFacilities: hotel.hotel.detailedFacilities,
|
||||
ratings: {
|
||||
tripAdvisor: hotel.hotel.ratings?.tripAdvisor.rating,
|
||||
},
|
||||
}}
|
||||
lang={lang}
|
||||
prices={{
|
||||
public: hotel.availability.productType?.public
|
||||
? {
|
||||
...hotel.availability.productType.public,
|
||||
requestedPrice:
|
||||
hotel.availability.productType?.public.requestedPrice ??
|
||||
undefined,
|
||||
}
|
||||
: undefined,
|
||||
member: hotel.availability.productType?.member
|
||||
? {
|
||||
...hotel.availability.productType.member,
|
||||
requestedPrice:
|
||||
hotel.availability.productType?.member.requestedPrice ??
|
||||
undefined,
|
||||
}
|
||||
: undefined,
|
||||
}}
|
||||
onHover={() => engage(hotel.hotel.name)}
|
||||
onHoverEnd={() => disengage()}
|
||||
onAddressClick={() => {
|
||||
const mapUrl = isAlternative
|
||||
? alternativeHotelsMap(lang)
|
||||
: selectHotelMap(lang)
|
||||
|
||||
disengage() // Disengage the current hotel to avoid the hover state from being active when clicking on the address
|
||||
activate(hotel.hotel.name)
|
||||
router.push(`${mapUrl}?${searchParams.toString()}`)
|
||||
}}
|
||||
belowInfoSlot={
|
||||
<HotelDetailsSidePeek
|
||||
hotel={{ ...hotel.hotel, url: "" }}
|
||||
restaurants={hotel.restaurants}
|
||||
additionalHotelData={hotel.additionalData}
|
||||
triggerLabel={intl.formatMessage({
|
||||
defaultMessage: "See hotel details",
|
||||
})}
|
||||
buttonVariant="primary"
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
}
|
||||
distanceToCityCenter={hotel.hotel.location.distanceToCentre}
|
||||
images={mapApiImagesToGalleryImages(hotel.hotel.galleryImages)}
|
||||
isUserLoggedIn={isUserLoggedIn}
|
||||
state={
|
||||
isHotelActiveInMapView(hotel.hotel.name) ? "active" : "default"
|
||||
}
|
||||
type={type}
|
||||
bookingCode={bookingCode}
|
||||
isAlternative={isAlternative}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{showBackToTop && (
|
||||
<BackToTopButton
|
||||
position="right"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import BookingCodeChip from "@/components/BookingCodeChip"
|
||||
import { BookingCodeChip } from "@scandic-hotels/design-system/BookingCodeChip"
|
||||
|
||||
import styles from "./row.module.css"
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import { useMap } from "@vis.gl/react-google-maps"
|
||||
import { useCallback, useMemo, useRef, useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
@@ -18,15 +19,18 @@ import { BackToTopButton } from "@scandic-hotels/design-system/BackToTopButton"
|
||||
import { Button } from "@scandic-hotels/design-system/Button"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import Link from "@scandic-hotels/design-system/Link"
|
||||
import { InteractiveMap } from "@scandic-hotels/design-system/Map/InteractiveMap"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
||||
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||
|
||||
import { RoomCardSkeleton } from "@/components/HotelReservation/RoomCardSkeleton/RoomCardSkeleton"
|
||||
import InteractiveMap from "@/components/Maps/InteractiveMap"
|
||||
import { useIsUserLoggedIn } from "@/hooks/useIsUserLoggedIn"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { useScrollToTop } from "@/hooks/useScrollToTop"
|
||||
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||
import { trackEvent } from "@/utils/tracking/base"
|
||||
|
||||
import FilterAndSortModal from "../../Filters/FilterAndSortModal"
|
||||
import HotelListing from "../HotelListing"
|
||||
@@ -39,7 +43,7 @@ import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/he
|
||||
|
||||
const SKELETON_LOAD_DELAY = 750
|
||||
|
||||
export default function SelectHotelContent({
|
||||
export function SelectHotelMapContent({
|
||||
hotelPins,
|
||||
cityCoordinates,
|
||||
mapId,
|
||||
@@ -52,6 +56,7 @@ export default function SelectHotelContent({
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
const map = useMap()
|
||||
const isUserLoggedIn = useIsUserLoggedIn()
|
||||
|
||||
const isAboveMobile = useMediaQuery("(min-width: 900px)")
|
||||
const [visibleHotels, setVisibleHotels] = useState<HotelResponse[]>([])
|
||||
@@ -59,7 +64,7 @@ export default function SelectHotelContent({
|
||||
const listingContainerRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
||||
const { activeHotel } = useHotelsMapStore()
|
||||
const hotelMapStore = useHotelsMapStore()
|
||||
|
||||
const { showBackToTop, scrollToTop } = useScrollToTop({
|
||||
threshold: 490,
|
||||
@@ -71,8 +76,10 @@ export default function SelectHotelContent({
|
||||
)
|
||||
|
||||
const coordinates = useMemo(() => {
|
||||
if (activeHotel) {
|
||||
const hotel = hotels.find((hotel) => hotel.hotel.name === activeHotel)
|
||||
if (hotelMapStore.activeHotel) {
|
||||
const hotel = hotels.find(
|
||||
(hotel) => hotel.hotel.name === hotelMapStore.activeHotel
|
||||
)
|
||||
|
||||
if (hotel && hotel.hotel.location) {
|
||||
return isAboveMobile
|
||||
@@ -89,7 +96,7 @@ export default function SelectHotelContent({
|
||||
return isAboveMobile
|
||||
? cityCoordinates
|
||||
: { ...cityCoordinates, lat: cityCoordinates.lat - 0.006 }
|
||||
}, [activeHotel, hotels, isAboveMobile, cityCoordinates])
|
||||
}, [hotelMapStore.activeHotel, hotels, isAboveMobile, cityCoordinates])
|
||||
|
||||
const showOnlyBookingCodeRates =
|
||||
bookingCode &&
|
||||
@@ -231,10 +238,62 @@ export default function SelectHotelContent({
|
||||
<InteractiveMap
|
||||
closeButton={closeButton}
|
||||
coordinates={coordinates}
|
||||
hotelPins={filteredHotelPins}
|
||||
hotelPins={filteredHotelPins.map((pin) => {
|
||||
const galleryImage = mapApiImagesToGalleryImages(pin.images).at(0)
|
||||
return {
|
||||
...pin,
|
||||
ratings: {
|
||||
tripAdvisor: pin.ratings ?? null,
|
||||
},
|
||||
image: {
|
||||
alt: galleryImage?.alt ?? "",
|
||||
url: galleryImage?.src ?? "",
|
||||
},
|
||||
}
|
||||
})}
|
||||
mapId={mapId}
|
||||
onTilesLoaded={debouncedUpdateHotelCards}
|
||||
fitBounds={isAboveMobile || !activeHotel}
|
||||
fitBounds={isAboveMobile || !hotelMapStore.activeHotel}
|
||||
onHoverHotelPin={(args) => {
|
||||
if (!args) {
|
||||
hotelMapStore.disengage()
|
||||
return
|
||||
}
|
||||
|
||||
hotelMapStore.engage(args.hotelName)
|
||||
}}
|
||||
hoveredHotelPin={hotelMapStore.hoveredHotel}
|
||||
onSetActiveHotelPin={(args) => {
|
||||
if (!args || args.hotelName === hotelMapStore.activeHotel) {
|
||||
hotelMapStore.deactivate()
|
||||
return
|
||||
}
|
||||
|
||||
trackEvent({
|
||||
event: "hotelClickMap",
|
||||
map: {
|
||||
action: "hotel click - map",
|
||||
},
|
||||
hotelInfo: {
|
||||
hotelId: args.hotelId,
|
||||
},
|
||||
})
|
||||
|
||||
hotelMapStore.activate(args.hotelName)
|
||||
}}
|
||||
onClickHotel={(hotelId) => {
|
||||
trackEvent({
|
||||
event: "hotelClickMap",
|
||||
map: {
|
||||
action: "hotel click - map",
|
||||
},
|
||||
hotelInfo: {
|
||||
hotelId,
|
||||
},
|
||||
})
|
||||
}}
|
||||
lang={lang}
|
||||
isUserLoggedIn={isUserLoggedIn}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { APIProvider } from "@vis.gl/react-google-maps"
|
||||
|
||||
import SelectHotelMapContent from "./SelectHotelMapContent"
|
||||
import { SelectHotelMapContent } from "./SelectHotelMapContent"
|
||||
|
||||
import type { SelectHotelMapProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { HotelCardSkeleton } from "@scandic-hotels/design-system/HotelCard/HotelCardSkeleton"
|
||||
import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer"
|
||||
|
||||
import { HotelCardSkeleton } from "../HotelCard/HotelCardSkeleton"
|
||||
|
||||
import styles from "./selectHotel.module.css"
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -4,9 +4,9 @@ import { useState } from "react"
|
||||
import { Button as ButtonRAC } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { FacilityToIcon } from "@scandic-hotels/design-system/FacilityToIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||
import HotelDetailsSidePeek from "@/components/SidePeeks/HotelDetailsSidePeek"
|
||||
import Alert from "@/components/TempDesignSystem/Alert"
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import TripAdvisorChip from "@scandic-hotels/booking-flow/components/TripAdvisorChip"
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
import { getSingleDecimal } from "@scandic-hotels/common/utils/numberFormatting"
|
||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||
import { FacilityToIcon } from "@scandic-hotels/design-system/FacilityToIcon"
|
||||
import ImageGallery from "@scandic-hotels/design-system/ImageGallery"
|
||||
import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer"
|
||||
import { TripAdvisorChip } from "@scandic-hotels/design-system/TripAdvisorChip"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||
import HotelDetailsSidePeek from "@/components/SidePeeks/HotelDetailsSidePeek"
|
||||
import Alert from "@/components/TempDesignSystem/Alert"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
|
||||
import { RateTypeEnum } from "@scandic-hotels/trpc/enums/rateType"
|
||||
import { RateTypeEnum } from "@scandic-hotels/common/constants/rateType"
|
||||
|
||||
import { sumPackages } from "@/components/HotelReservation/utils"
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import { ChipButton } from "@scandic-hotels/design-system/ChipButton"
|
||||
import { IconButton } from "@scandic-hotels/design-system/IconButton"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { RateTypeEnum } from "@scandic-hotels/trpc/enums/rateType"
|
||||
import { RateTypeEnum } from "@scandic-hotels/common/constants/rateType"
|
||||
|
||||
import { useSelectRateContext } from "@/contexts/SelectRate/SelectRateContext"
|
||||
import { useBreakpoint } from "@/hooks/useBreakpoint"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||
|
||||
import BookingCodeChip from "@/components/BookingCodeChip"
|
||||
import { BookingCodeChip } from "@scandic-hotels/design-system/BookingCodeChip"
|
||||
|
||||
import { useSelectRateContext } from "@/contexts/SelectRate/SelectRateContext"
|
||||
|
||||
export function RemoveBookingCodeButton() {
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import {
|
||||
AdvancedMarker,
|
||||
AdvancedMarkerAnchorPoint,
|
||||
InfoWindow,
|
||||
} from "@vis.gl/react-google-maps"
|
||||
import { useCallback } from "react"
|
||||
import { useMediaQuery } from "usehooks-ts"
|
||||
|
||||
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||
|
||||
import StandaloneHotelCardDialog from "@/components/HotelReservation/HotelCardDialog/StandaloneHotelCardDialog"
|
||||
import { trackEvent } from "@/utils/tracking/base"
|
||||
|
||||
import HotelPin from "./HotelPin"
|
||||
|
||||
import styles from "./hotelListingMapContent.module.css"
|
||||
|
||||
import type { HotelListingMapContentProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
|
||||
function HotelListingMapContent({ hotelPins }: HotelListingMapContentProps) {
|
||||
const { activeHotel, hoveredHotel, activate, deactivate, engage, disengage } =
|
||||
useHotelsMapStore()
|
||||
const isDesktop = useMediaQuery("(min-width: 768px)")
|
||||
|
||||
const toggleActiveHotelPin = useCallback(
|
||||
(pinName: string | null, hotelId: string) => {
|
||||
if (activeHotel === pinName || pinName === null) {
|
||||
deactivate()
|
||||
return
|
||||
}
|
||||
trackEvent({
|
||||
event: "hotelClickMap",
|
||||
map: {
|
||||
action: "hotel click - map",
|
||||
},
|
||||
hotelInfo: {
|
||||
hotelId,
|
||||
},
|
||||
})
|
||||
activate(pinName)
|
||||
},
|
||||
[activeHotel, activate, deactivate]
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{hotelPins.map((pin) => {
|
||||
const isActiveOrHovered =
|
||||
activeHotel === pin.name || hoveredHotel === pin.name
|
||||
const hotelPrice =
|
||||
pin.memberPrice ??
|
||||
pin.publicPrice ??
|
||||
pin.redemptionPrice ??
|
||||
pin.voucherPrice ??
|
||||
pin.chequePrice?.numberOfCheques ??
|
||||
null
|
||||
|
||||
const hotelAdditionalPrice = pin.chequePrice
|
||||
? pin.chequePrice.additionalPricePerStay
|
||||
: undefined
|
||||
const hotelAdditionalCurrency = pin.chequePrice
|
||||
? pin.chequePrice.currency?.toString()
|
||||
: undefined
|
||||
return (
|
||||
<AdvancedMarker
|
||||
key={pin.name}
|
||||
className={styles.advancedMarker}
|
||||
position={pin.coordinates}
|
||||
anchorPoint={AdvancedMarkerAnchorPoint.CENTER}
|
||||
zIndex={isActiveOrHovered ? 2 : 0}
|
||||
onMouseEnter={() => engage(pin.name)}
|
||||
onMouseLeave={() => disengage()}
|
||||
onClick={() => toggleActiveHotelPin(pin.name, pin.operaId)}
|
||||
>
|
||||
{isActiveOrHovered && isDesktop && (
|
||||
<InfoWindow
|
||||
position={pin.coordinates}
|
||||
pixelOffset={[0, -24]}
|
||||
headerDisabled={true}
|
||||
shouldFocus={false}
|
||||
>
|
||||
<StandaloneHotelCardDialog
|
||||
data={pin}
|
||||
handleClose={() => {
|
||||
deactivate()
|
||||
disengage()
|
||||
}}
|
||||
/>
|
||||
</InfoWindow>
|
||||
)}
|
||||
<HotelPin
|
||||
isActive={isActiveOrHovered}
|
||||
hotelPrice={hotelPrice}
|
||||
currency={pin.currency}
|
||||
hotelAdditionalPrice={hotelAdditionalPrice}
|
||||
hotelAdditionalCurrency={hotelAdditionalCurrency}
|
||||
/>
|
||||
</AdvancedMarker>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HotelListingMapContent
|
||||
@@ -1,114 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { Map, type MapProps, useMap } from "@vis.gl/react-google-maps"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { IconButton } from "@scandic-hotels/design-system/IconButton"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
|
||||
import {
|
||||
DEFAULT_ZOOM,
|
||||
MAP_RESTRICTIONS,
|
||||
MAX_ZOOM,
|
||||
MIN_ZOOM,
|
||||
} from "@/constants/map"
|
||||
|
||||
import { useZoomControls } from "@/hooks/maps/useZoomControls"
|
||||
|
||||
import HotelListingMapContent from "./HotelListingMapContent"
|
||||
import PoiMapMarkers from "./PoiMapMarkers"
|
||||
|
||||
import styles from "./interactiveMap.module.css"
|
||||
|
||||
import type { InteractiveMapProps } from "@/types/components/hotelPage/map/interactiveMap"
|
||||
|
||||
export default function InteractiveMap({
|
||||
coordinates,
|
||||
pointsOfInterest,
|
||||
activePoi,
|
||||
hotelPins,
|
||||
mapId,
|
||||
closeButton,
|
||||
markerInfo,
|
||||
fitBounds = true,
|
||||
onTilesLoaded,
|
||||
onActivePoiChange,
|
||||
}: InteractiveMapProps) {
|
||||
const intl = useIntl()
|
||||
const map = useMap()
|
||||
const [hasInitializedBounds, setHasInitializedBounds] = useState(false)
|
||||
const { zoomIn, zoomOut, isMaxZoom, isMinZoom } = useZoomControls()
|
||||
|
||||
const mapOptions: MapProps = {
|
||||
defaultZoom: DEFAULT_ZOOM,
|
||||
minZoom: MIN_ZOOM,
|
||||
maxZoom: MAX_ZOOM,
|
||||
defaultCenter: coordinates,
|
||||
disableDefaultUI: true,
|
||||
clickableIcons: false,
|
||||
mapId,
|
||||
gestureHandling: "greedy",
|
||||
restriction: MAP_RESTRICTIONS,
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (map && hotelPins?.length && !hasInitializedBounds) {
|
||||
if (fitBounds) {
|
||||
const bounds = new google.maps.LatLngBounds()
|
||||
hotelPins.forEach((marker) => {
|
||||
bounds.extend(marker.coordinates)
|
||||
})
|
||||
map.fitBounds(bounds, 100)
|
||||
}
|
||||
setHasInitializedBounds(true)
|
||||
}
|
||||
}, [map, fitBounds, hotelPins, hasInitializedBounds])
|
||||
|
||||
return (
|
||||
<div className={styles.mapContainer}>
|
||||
<Map {...mapOptions} onTilesLoaded={onTilesLoaded}>
|
||||
{hotelPins && <HotelListingMapContent hotelPins={hotelPins} />}
|
||||
{pointsOfInterest && markerInfo && (
|
||||
<PoiMapMarkers
|
||||
coordinates={coordinates}
|
||||
pointsOfInterest={pointsOfInterest}
|
||||
onActivePoiChange={onActivePoiChange}
|
||||
activePoi={activePoi}
|
||||
markerInfo={markerInfo}
|
||||
/>
|
||||
)}
|
||||
</Map>
|
||||
<div className={styles.ctaButtons}>
|
||||
{closeButton}
|
||||
<div className={styles.zoomButtons}>
|
||||
<IconButton
|
||||
theme="Inverted"
|
||||
style="Elevated"
|
||||
className={styles.zoomButton}
|
||||
onClick={zoomOut}
|
||||
aria-label={intl.formatMessage({
|
||||
defaultMessage: "Zoom out",
|
||||
})}
|
||||
isDisabled={isMinZoom}
|
||||
>
|
||||
<MaterialIcon icon="remove" color="CurrentColor" />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
theme="Inverted"
|
||||
style="Elevated"
|
||||
className={styles.zoomButton}
|
||||
onClick={zoomIn}
|
||||
aria-label={intl.formatMessage({
|
||||
defaultMessage: "Zoom in",
|
||||
})}
|
||||
isDisabled={isMaxZoom}
|
||||
>
|
||||
<MaterialIcon icon="add" color="CurrentColor" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { IconByIconName } from "@scandic-hotels/design-system/Icons/IconByIconName"
|
||||
|
||||
import { getIconByPoiGroupAndCategory } from "../utils"
|
||||
import { poiVariants } from "./variants"
|
||||
|
||||
import type { PoiMarkerProps } from "@/types/components/maps/poiMarker"
|
||||
|
||||
export default function PoiMarker({
|
||||
group,
|
||||
categoryName,
|
||||
skipBackground,
|
||||
size = "small",
|
||||
className = "",
|
||||
}: PoiMarkerProps) {
|
||||
const iconName = getIconByPoiGroupAndCategory(group, categoryName)
|
||||
const classNames = poiVariants({ group, skipBackground, size, className })
|
||||
|
||||
return iconName ? (
|
||||
<span className={classNames}>
|
||||
<IconByIconName
|
||||
iconName={iconName}
|
||||
color={skipBackground ? "Icon/Feedback/Neutral" : "Icon/Inverted"}
|
||||
size={size === "small" ? 16 : size === "large" ? 24 : 20}
|
||||
/>
|
||||
</span>
|
||||
) : null
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
import { PointOfInterestGroupEnum } from "@scandic-hotels/trpc/enums/pointOfInterest"
|
||||
|
||||
import styles from "./poi.module.css"
|
||||
|
||||
export const poiVariants = cva(styles.icon, {
|
||||
variants: {
|
||||
group: {
|
||||
[PointOfInterestGroupEnum.ATTRACTIONS]: styles.attractions,
|
||||
[PointOfInterestGroupEnum.BUSINESS]: styles.business,
|
||||
[PointOfInterestGroupEnum.LOCATION]: styles.location,
|
||||
[PointOfInterestGroupEnum.PARKING]: styles.parking,
|
||||
[PointOfInterestGroupEnum.PUBLIC_TRANSPORT]: styles.publicTransport,
|
||||
[PointOfInterestGroupEnum.SHOPPING_DINING]: styles.shoppingDining,
|
||||
},
|
||||
skipBackground: {
|
||||
true: styles.transparent,
|
||||
false: "",
|
||||
},
|
||||
size: {
|
||||
small: styles.small,
|
||||
medium: styles.small,
|
||||
large: styles.large,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
skipBackground: false,
|
||||
size: "small",
|
||||
},
|
||||
})
|
||||
@@ -1,89 +0,0 @@
|
||||
import { HotelTypeEnum } from "@scandic-hotels/trpc/enums/hotelType"
|
||||
|
||||
import DowntownCamperMarker from "./DowntownCamper"
|
||||
import DowntownCamperSmallMarker from "./DowntownCamperSmall"
|
||||
import GrandHotelMarker from "./GrandHotel"
|
||||
import GrandHotelSmallMarker from "./GrandHotelSmall"
|
||||
import HaymarketMarker from "./Haymarket"
|
||||
import HaymarketSmallMarker from "./HaymarketSmall"
|
||||
import HotelNorgeMarker from "./HotelNorge"
|
||||
import HotelNorgeSmallMarker from "./HotelNorgeSmall"
|
||||
import MarskiMarker from "./Marski"
|
||||
import MarskiSmallMarker from "./MarskiSmall"
|
||||
import ScandicMarker from "./Scandic"
|
||||
import ScandicGoMarker from "./ScandicGo"
|
||||
import ScandicGoSmallMarker from "./ScandicGoSmall"
|
||||
import ScandicSmallMarker from "./ScandicSmall"
|
||||
import TheDockMarker from "./TheDock"
|
||||
import TheDockSmallMarker from "./TheDockSmall"
|
||||
|
||||
import type { MarkerInfo } from "@scandic-hotels/trpc/types/marker"
|
||||
|
||||
import { SignatureHotelEnum } from "@/types/enums/signatureHotel"
|
||||
|
||||
interface HotelMarkerByTypeProps
|
||||
extends MarkerInfo,
|
||||
React.SVGAttributes<HTMLOrSVGElement> {
|
||||
size?: "large" | "small"
|
||||
}
|
||||
|
||||
export default function HotelMarkerByType({
|
||||
hotelId,
|
||||
hotelType,
|
||||
size = "large",
|
||||
...props
|
||||
}: HotelMarkerByTypeProps) {
|
||||
if (hotelType === HotelTypeEnum.ScandicGo) {
|
||||
return size === "small" ? (
|
||||
<ScandicGoSmallMarker {...props} />
|
||||
) : (
|
||||
<ScandicGoMarker {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
switch (hotelId) {
|
||||
case SignatureHotelEnum.Haymarket:
|
||||
return size === "small" ? (
|
||||
<HaymarketSmallMarker {...props} />
|
||||
) : (
|
||||
<HaymarketMarker {...props} />
|
||||
)
|
||||
case SignatureHotelEnum.HotelNorge:
|
||||
return size === "small" ? (
|
||||
<HotelNorgeSmallMarker {...props} />
|
||||
) : (
|
||||
<HotelNorgeMarker {...props} />
|
||||
)
|
||||
case SignatureHotelEnum.DowntownCamper:
|
||||
return size === "small" ? (
|
||||
<DowntownCamperSmallMarker {...props} />
|
||||
) : (
|
||||
<DowntownCamperMarker {...props} />
|
||||
)
|
||||
case SignatureHotelEnum.GrandHotelOslo:
|
||||
return size === "small" ? (
|
||||
<GrandHotelSmallMarker {...props} />
|
||||
) : (
|
||||
<GrandHotelMarker {...props} />
|
||||
)
|
||||
case SignatureHotelEnum.Marski:
|
||||
return size === "small" ? (
|
||||
<MarskiSmallMarker {...props} />
|
||||
) : (
|
||||
<MarskiMarker {...props} />
|
||||
)
|
||||
case SignatureHotelEnum.TheDock:
|
||||
return size === "small" ? (
|
||||
<TheDockSmallMarker {...props} />
|
||||
) : (
|
||||
<TheDockMarker {...props} />
|
||||
)
|
||||
|
||||
default:
|
||||
return size === "small" ? (
|
||||
<ScandicSmallMarker {...props} />
|
||||
) : (
|
||||
<ScandicMarker {...props} />
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { IconName } from "@scandic-hotels/design-system/Icons/iconName"
|
||||
import { PointOfInterestGroupEnum } from "@scandic-hotels/trpc/enums/pointOfInterest"
|
||||
|
||||
export function getIconByPoiGroupAndCategory(
|
||||
group: PointOfInterestGroupEnum,
|
||||
category?: string
|
||||
) {
|
||||
switch (group) {
|
||||
case PointOfInterestGroupEnum.PUBLIC_TRANSPORT:
|
||||
return category === "Airport" ? IconName.Airplane : IconName.Train
|
||||
case PointOfInterestGroupEnum.ATTRACTIONS:
|
||||
return category === "Museum" ? IconName.Museum : IconName.Camera
|
||||
case PointOfInterestGroupEnum.BUSINESS:
|
||||
return IconName.Business
|
||||
case PointOfInterestGroupEnum.PARKING:
|
||||
return IconName.Parking
|
||||
case PointOfInterestGroupEnum.SHOPPING_DINING:
|
||||
return category === "Restaurant" ? IconName.Restaurant : IconName.Shopping
|
||||
case PointOfInterestGroupEnum.LOCATION:
|
||||
default:
|
||||
return IconName.Location
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,13 @@ import { ArrowLeft } from "react-feather"
|
||||
import Link from "@scandic-hotels/design-system/Link"
|
||||
|
||||
import { overview } from "@/constants/routes/webviews"
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
import { webviewSearchParams } from "@/utils/webviews"
|
||||
|
||||
import styles from "./linkToOverview.module.css"
|
||||
import { env } from "@/env/server"
|
||||
|
||||
export default async function LinkToOverview() {
|
||||
if (!env.WEBVIEW_SHOW_OVERVIEW) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -3,7 +3,7 @@ import { parsePhoneNumberFromString } from "libphonenumber-js"
|
||||
|
||||
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
|
||||
import { logger } from "@scandic-hotels/common/logger"
|
||||
import { RateTypeEnum } from "@scandic-hotels/trpc/enums/rateType"
|
||||
import { RateTypeEnum } from "@scandic-hotels/common/constants/rateType"
|
||||
|
||||
import {
|
||||
sumPackages,
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { PointOfInterest } from "@scandic-hotels/trpc/types/hotel"
|
||||
import type { MarkerInfo } from "@scandic-hotels/trpc/types/marker"
|
||||
import type { ReactElement } from "react"
|
||||
|
||||
import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
import type { Coordinates } from "@/types/components/maps/coordinates"
|
||||
|
||||
export interface InteractiveMapProps {
|
||||
coordinates: Coordinates
|
||||
activePoi?: PointOfInterest["name"] | null
|
||||
hotelPins?: HotelPin[]
|
||||
pointsOfInterest?: PointOfInterest[]
|
||||
markerInfo?: MarkerInfo
|
||||
mapId: string
|
||||
closeButton: ReactElement<any>
|
||||
fitBounds?: boolean
|
||||
onTilesLoaded?: () => void
|
||||
onActivePoiChange?: (poi: PointOfInterest["name"] | null) => void
|
||||
}
|
||||
@@ -50,10 +50,6 @@ export type HotelPin = {
|
||||
hasEnoughPoints: boolean
|
||||
}
|
||||
|
||||
export interface HotelListingMapContentProps {
|
||||
hotelPins: HotelPin[]
|
||||
}
|
||||
|
||||
export interface HotelCardDialogProps {
|
||||
type?: "listing" | "standalone"
|
||||
isOpen: boolean
|
||||
@@ -61,15 +57,6 @@ export interface HotelCardDialogProps {
|
||||
handleClose: (event: { stopPropagation: () => void }) => void
|
||||
}
|
||||
|
||||
export interface HotelCardDialogImageProps {
|
||||
firstImage?: string
|
||||
altText?: string
|
||||
rating?: number | null
|
||||
imageError: boolean
|
||||
setImageError: (error: boolean) => void
|
||||
position: "top" | "left"
|
||||
}
|
||||
|
||||
export interface HotelCardDialogListingProps {
|
||||
hotels: HotelResponse[]
|
||||
unfilteredHotelCount: number
|
||||
|
||||
@@ -10,12 +10,6 @@ export type PriceCardProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export type PointsRowProps = {
|
||||
pointsPerStay: number
|
||||
additionalPricePerStay?: number
|
||||
additionalPriceCurrency?: string
|
||||
}
|
||||
|
||||
export type VoucherCardProps = {
|
||||
productTypeVoucher: ProductTypeVoucher
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { PointOfInterestGroupEnum } from "@scandic-hotels/trpc/enums/pointOfInterest"
|
||||
import type { VariantProps } from "class-variance-authority"
|
||||
|
||||
import type { poiVariants } from "@/components/Maps/Markers/Poi/variants"
|
||||
|
||||
export interface PoiMarkerProps extends VariantProps<typeof poiVariants> {
|
||||
group: PointOfInterestGroupEnum
|
||||
categoryName?: string
|
||||
className?: string
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FacilityEnum } from "@scandic-hotels/trpc/enums/facilities"
|
||||
import { FacilityEnum } from "@scandic-hotels/common/constants/facilities"
|
||||
|
||||
import type {
|
||||
Amenities,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { FacilityEnum } from "@scandic-hotels/common/constants/facilities"
|
||||
import { FacilityToIcon } from "@scandic-hotels/design-system/FacilityToIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { FacilityEnum } from "@scandic-hotels/trpc/enums/facilities"
|
||||
|
||||
import { FacilityToIcon } from "../TEMP/FacilityToIcon"
|
||||
|
||||
import styles from "./additionalAmenities.module.css"
|
||||
|
||||
|
||||
@@ -1,305 +0,0 @@
|
||||
import { IconByIconName } from "@scandic-hotels/design-system/Icons/IconByIconName"
|
||||
import { IconName } from "@scandic-hotels/design-system/Icons/iconName"
|
||||
import { FacilityEnum } from "@scandic-hotels/trpc/enums/facilities"
|
||||
|
||||
import type {
|
||||
IconProps,
|
||||
NucleoIconProps,
|
||||
} from "@scandic-hotels/design-system/Icons"
|
||||
import type { MaterialIconSetIconProps } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import type { JSX } from "react"
|
||||
|
||||
const facilityToIconMap: Record<FacilityEnum, IconName> = {
|
||||
[FacilityEnum.AccessibleBathingControls]: IconName.StarFilled,
|
||||
[FacilityEnum.AccessibleBathtubs]: IconName.StarFilled,
|
||||
[FacilityEnum.AccessibleElevators]: IconName.StarFilled,
|
||||
[FacilityEnum.AccessibleLightSwitch]: IconName.StarFilled,
|
||||
[FacilityEnum.AccessibleRoomsAtHotel1]: IconName.StarFilled,
|
||||
[FacilityEnum.AccessibleRoomsAtHotel2]: IconName.StarFilled,
|
||||
[FacilityEnum.AccessibleToilets]: IconName.StarFilled,
|
||||
[FacilityEnum.AccessibleWashBasins]: IconName.StarFilled,
|
||||
[FacilityEnum.AdaptedRoomDoors]: IconName.StarFilled,
|
||||
[FacilityEnum.AdjoiningConventionCentre]: IconName.ConventionCentre,
|
||||
[FacilityEnum.AirConAirCooling]: IconName.AirConAirCooling,
|
||||
[FacilityEnum.AirConditioningInRoom]: IconName.AirConditioningInRoom,
|
||||
[FacilityEnum.AirportMaxDistance8Km]: IconName.Airplane,
|
||||
[FacilityEnum.AlarmsContinuouslyMonitored]: IconName.StarFilled,
|
||||
[FacilityEnum.AlarmsHaveStrobeLightsForDeafHardHearingInAllGuestRooms]:
|
||||
IconName.StarFilled,
|
||||
[FacilityEnum.AlarmsHaveStrobeLightsForDeafHardHearingInAllHallways]:
|
||||
IconName.StarFilled,
|
||||
[FacilityEnum.AlarmsHaveStrobeLightsForDeafHardHearingInAllPublicAreas]:
|
||||
IconName.StarFilled,
|
||||
[FacilityEnum.AllAudibleSmokeAlarmsHardwired]: IconName.StarFilled,
|
||||
[FacilityEnum.AllExteriorDoorsRequireKeyAccessAtNightOrAutomaticallyLock]:
|
||||
IconName.StarFilled,
|
||||
[FacilityEnum.AllGuestRoomDoorsHaveViewports]: IconName.StarFilled,
|
||||
[FacilityEnum.AllGuestRoomDoorsSelfClosing]: IconName.StarFilled,
|
||||
[FacilityEnum.AllParkingAreasPatrolled]: IconName.StarFilled,
|
||||
[FacilityEnum.AllParkingAreasWellLit]: IconName.StarFilled,
|
||||
[FacilityEnum.AllStairsWellsVentilated]: IconName.StarFilled,
|
||||
[FacilityEnum.ArmchairBed]: IconName.ArmChair,
|
||||
[FacilityEnum.AudibleAlarms]: IconName.StarFilled,
|
||||
[FacilityEnum.AudibleSmokeAlarmsInAllHalls]: IconName.StarFilled,
|
||||
[FacilityEnum.AudibleSmokeAlarmsInAllPublicAreas]: IconName.StarFilled,
|
||||
[FacilityEnum.AudibleSmokeAlarmsInAllRooms]: IconName.StarFilled,
|
||||
[FacilityEnum.AudioVisualEquipmentAvailable]: IconName.StarFilled,
|
||||
[FacilityEnum.AutolinkFireDepartment]: IconName.StarFilled,
|
||||
[FacilityEnum.AutomatedExternalDefibrillatorOnSiteAED]: IconName.StarFilled,
|
||||
[FacilityEnum.AutomaticFireDoors]: IconName.StarFilled,
|
||||
[FacilityEnum.AutoRecallElevators]: IconName.StarFilled,
|
||||
[FacilityEnum.BalconiesAccessibleToAdjoiningRooms]: IconName.StarFilled,
|
||||
[FacilityEnum.Ballroom]: IconName.StarFilled,
|
||||
[FacilityEnum.Banquet]: IconName.StarFilled,
|
||||
[FacilityEnum.Bar]: IconName.LocalBar,
|
||||
[FacilityEnum.BasicMedicalEquipmentOnSite]: IconName.StarFilled,
|
||||
[FacilityEnum.BathroomsAdaptedForDisabledGuests]: IconName.StarFilled,
|
||||
[FacilityEnum.Beach]: IconName.Beach,
|
||||
[FacilityEnum.Beach0To1Km]: IconName.Beach,
|
||||
[FacilityEnum.BeautySalon]: IconName.BeautySalon,
|
||||
[FacilityEnum.BedroomsWithWheelchairAccess]: IconName.StarFilled,
|
||||
[FacilityEnum.BikesForLoan]: IconName.Bike,
|
||||
[FacilityEnum.Bowling]: IconName.Bowling,
|
||||
[FacilityEnum.BrailleLargePrintHotelLiterature]: IconName.StarFilled,
|
||||
[FacilityEnum.BrailleLargePrintMenus]: IconName.StarFilled,
|
||||
[FacilityEnum.Breakfast]: IconName.Breakfast,
|
||||
[FacilityEnum.Business1]: IconName.BusinessCentre,
|
||||
[FacilityEnum.Business2]: IconName.BusinessCentre,
|
||||
[FacilityEnum.BusinessCentre]: IconName.BusinessCentre,
|
||||
[FacilityEnum.Cafe]: IconName.Restaurant,
|
||||
[FacilityEnum.CashFree8pmTill6am]: IconName.CashFree,
|
||||
[FacilityEnum.CashFreeHotel]: IconName.CashFree,
|
||||
[FacilityEnum.ChildrenWelcome]: IconName.StarFilled,
|
||||
[FacilityEnum.City]: IconName.City,
|
||||
[FacilityEnum.CoffeeInReceptionAtCharge]: IconName.CoffeeInReceptionAtCharge,
|
||||
[FacilityEnum.CoffeeShop]: IconName.CoffeeShop,
|
||||
[FacilityEnum.CoffeeTeaFacilities]: IconName.CoffeeAlt,
|
||||
[FacilityEnum.ColourTVInRoomsAllScandicHotels]: IconName.StarFilled,
|
||||
[FacilityEnum.ComplimentaryColdRefreshments]:
|
||||
IconName.ComplimentaryColdRefreshments,
|
||||
[FacilityEnum.CongressHall]: IconName.StarFilled,
|
||||
[FacilityEnum.ConventionCentre]: IconName.ConventionCentre,
|
||||
[FacilityEnum.Couples]: IconName.StarFilled,
|
||||
[FacilityEnum.DeadboltsOnConnectingDoors]: IconName.StarFilled,
|
||||
[FacilityEnum.DeadboltsSecondaryLocksOnAllGuestRoomDoors]:
|
||||
IconName.StarFilled,
|
||||
[FacilityEnum.Defibrillator]: IconName.StarFilled,
|
||||
[FacilityEnum.Desk]: IconName.Desk,
|
||||
[FacilityEnum.DirectDialPhoneInRoomsAllScandic]: IconName.DirectDial,
|
||||
[FacilityEnum.DisabledEmergencyPlan1]: IconName.StarFilled,
|
||||
[FacilityEnum.DisabledEmergencyPlan2]: IconName.StarFilled,
|
||||
[FacilityEnum.DisabledParking]: IconName.Wheelchair,
|
||||
[FacilityEnum.DiscoNightClub]: IconName.Nightlife,
|
||||
[FacilityEnum.DJLiveMusic]: IconName.Nightlife,
|
||||
[FacilityEnum.DO_NOT_USE_Restaurant]: IconName.StarFilled,
|
||||
[FacilityEnum.Downtown]: IconName.StarFilled,
|
||||
[FacilityEnum.DrinkableTapWater]: IconName.StarFilled,
|
||||
[FacilityEnum.DVDPlayer]: IconName.StarFilled,
|
||||
[FacilityEnum.EBikesChargingStation]: IconName.ElectricBike,
|
||||
[FacilityEnum.ElectronicKeyCards]: IconName.StarFilled,
|
||||
[FacilityEnum.Elevator]: IconName.Elevator,
|
||||
[FacilityEnum.EmergencyBackUpGenerators]: IconName.StarFilled,
|
||||
[FacilityEnum.EmergencyCallButtonOnPhone]: IconName.StarFilled,
|
||||
[FacilityEnum.EmergencyCodesOrButtonsInRooms]: IconName.StarFilled,
|
||||
[FacilityEnum.EmergencyEvacuationPlan1]: IconName.StarFilled,
|
||||
[FacilityEnum.EmergencyEvacuationPlan2]: IconName.StarFilled,
|
||||
[FacilityEnum.EmergencyEvaluationDrillFrequency]: IconName.StarFilled,
|
||||
[FacilityEnum.EmergencyInfoInAllRooms]: IconName.StarFilled,
|
||||
[FacilityEnum.EmergencyLightingAllScandic]: IconName.StarFilled,
|
||||
[FacilityEnum.EmergencyLightningInAllPublicAreas]: IconName.StarFilled,
|
||||
[FacilityEnum.EmergencyServiceResponseTimeInMinutes]: IconName.StarFilled,
|
||||
[FacilityEnum.Entertainment]: IconName.Theatre,
|
||||
[FacilityEnum.EventVenue]: IconName.StarFilled,
|
||||
[FacilityEnum.ExchangeFacility]: IconName.StarFilled,
|
||||
[FacilityEnum.ExitMapsInRooms]: IconName.StarFilled,
|
||||
[FacilityEnum.ExitSignsLit]: IconName.StarFilled,
|
||||
[FacilityEnum.ExtraFamilyFriendly]: IconName.ExtraFamilyFriendly,
|
||||
[FacilityEnum.Families]: IconName.ExtraFamilyFriendly,
|
||||
[FacilityEnum.FaxFacilityInRoom]: IconName.Fax,
|
||||
[FacilityEnum.Financial]: IconName.StarFilled,
|
||||
[FacilityEnum.FireDetectorsAllScandic]: IconName.StarFilled,
|
||||
[FacilityEnum.FireDetectorsInAllHalls]: IconName.StarFilled,
|
||||
[FacilityEnum.FireDetectorsInAllPublicAreas]: IconName.StarFilled,
|
||||
[FacilityEnum.FireDetectorsInAllRooms]: IconName.StarFilled,
|
||||
[FacilityEnum.FireExtinguishersInAllPublicAreas]: IconName.StarFilled,
|
||||
[FacilityEnum.FireExtinguishersInPublicAreasAllScandic]: IconName.StarFilled,
|
||||
[FacilityEnum.FireSafetyAllScandic]: IconName.StarFilled,
|
||||
[FacilityEnum.FirstAidAvailable]: IconName.StarFilled,
|
||||
[FacilityEnum.FoodDrinks247]: IconName.FoodDrinks247,
|
||||
[FacilityEnum.FreeWiFi]: IconName.Wifi,
|
||||
[FacilityEnum.GiftShop]: IconName.Gift,
|
||||
[FacilityEnum.Golf]: IconName.Golf,
|
||||
[FacilityEnum.GolfCourse0To30Km]: IconName.Golf,
|
||||
[FacilityEnum.GuestRoomDoorsHaveASecondLock]: IconName.StarFilled,
|
||||
[FacilityEnum.Gym]: IconName.Fitness,
|
||||
[FacilityEnum.GymTrainingFacilities]: IconName.Fitness,
|
||||
[FacilityEnum.Hairdresser]: IconName.Hairdresser,
|
||||
[FacilityEnum.HairdryerInRoomAllScandic]: IconName.HairdryerInRoomAllScandic,
|
||||
[FacilityEnum.HandicapFacilities]: IconName.StarFilled,
|
||||
[FacilityEnum.HandrailsInBathrooms]: IconName.StarFilled,
|
||||
[FacilityEnum.HearingInductionLoops]: IconName.StarFilled,
|
||||
[FacilityEnum.Highway1]: IconName.StarFilled,
|
||||
[FacilityEnum.Highway2]: IconName.StarFilled,
|
||||
[FacilityEnum.Hiking0To3Km]: IconName.Hiking,
|
||||
[FacilityEnum.HotelCompliesWithAAASecurityStandards]: IconName.StarFilled,
|
||||
[FacilityEnum.HotelIsFollowingScandicsSafetySecurityPolicy]:
|
||||
IconName.StarFilled,
|
||||
[FacilityEnum.HotelWorksAccordingToScandicsAccessibilityConcepts]:
|
||||
IconName.StarFilled,
|
||||
[FacilityEnum.IceMachine]: IconName.IceMachine,
|
||||
[FacilityEnum.IceMachineReception]: IconName.IceMachine,
|
||||
[FacilityEnum.IDRequiredToReplaceAGuestRoomKey]: IconName.StarFilled,
|
||||
[FacilityEnum.IfNoWhatAreTheHoursUse24ClockEx0000To0600]: IconName.StarFilled,
|
||||
[FacilityEnum.InCountry]: IconName.StarFilled,
|
||||
[FacilityEnum.IndustrialPark]: IconName.StarFilled,
|
||||
[FacilityEnum.InternetHighSpeedInternetConnectionAllScandic]:
|
||||
IconName.StarFilled,
|
||||
[FacilityEnum.InternetHotSpotsAllScandic]: IconName.StarFilled,
|
||||
[FacilityEnum.IroningRoom]: IconName.Ironing,
|
||||
[FacilityEnum.IronIroningBoardAllScandic]: IconName.Ironing,
|
||||
[FacilityEnum.Jacuzzi]: IconName.Jacuzzi,
|
||||
[FacilityEnum.JacuzziInRoom]: IconName.Jacuzzi,
|
||||
[FacilityEnum.KayaksForLoan]: IconName.Kayaking,
|
||||
[FacilityEnum.KeyAccessOnlySecuredFloorsAvailable]: IconName.StarFilled,
|
||||
[FacilityEnum.KeyAccessOnlyToHealthClubGym]: IconName.Fitness,
|
||||
[FacilityEnum.KidsPlayRoom]: IconName.StarFilled,
|
||||
[FacilityEnum.KidsUpToAndIncluding12YearsStayForFree]: IconName.StarFilled,
|
||||
[FacilityEnum.KitchenInRoom]: IconName.Kitchen,
|
||||
[FacilityEnum.Lake0To1Km]: IconName.Houseboat,
|
||||
[FacilityEnum.LakeOrSea0To1Km]: IconName.Houseboat,
|
||||
[FacilityEnum.LaptopSafe]: IconName.LaptopSafe,
|
||||
[FacilityEnum.LateCheckOutUntil1400Guaranteed]: IconName.Business,
|
||||
[FacilityEnum.LaundryRoom]: IconName.LaundryMachine,
|
||||
[FacilityEnum.LaundryService]: IconName.LaundryMachine,
|
||||
[FacilityEnum.LaundryServiceExpress]: IconName.TshirtWash,
|
||||
[FacilityEnum.Leisure]: IconName.StarFilled,
|
||||
[FacilityEnum.LifestyleConcierge]: IconName.Concierge,
|
||||
[FacilityEnum.LuggageLockers]: IconName.LuggageLockers,
|
||||
[FacilityEnum.LuggageStorageAdditionalCost]: IconName.Luggage,
|
||||
[FacilityEnum.LuggageStorageNoCost]: IconName.Luggage,
|
||||
[FacilityEnum.Massage]: IconName.Massage,
|
||||
[FacilityEnum.MeetingArea]: IconName.Business,
|
||||
[FacilityEnum.MeetingConferenceFacilities]: IconName.Business,
|
||||
[FacilityEnum.MeetingRooms]: IconName.Business,
|
||||
[FacilityEnum.MinibarInRoom]: IconName.Minibar,
|
||||
[FacilityEnum.MobileLift]: IconName.StarFilled,
|
||||
[FacilityEnum.Mountains0To1Km]: IconName.Landscape,
|
||||
[FacilityEnum.MovieChannelsInRoomAllScandic]: IconName.TVRemote,
|
||||
[FacilityEnum.MultipleExitsOnEachFloor]: IconName.StarFilled,
|
||||
[FacilityEnum.NonSmokingRoomsAllScandic]: IconName.NonSmoking,
|
||||
[FacilityEnum.OnSiteTrainingFacilities]: IconName.Fitness,
|
||||
[FacilityEnum.OtherExplainInBriefDescription]: IconName.StarFilled,
|
||||
[FacilityEnum.OutdoorTerrace]: IconName.Deck,
|
||||
[FacilityEnum.OvernightSecurity]: IconName.Guard,
|
||||
[FacilityEnum.ParkingAdditionalCost]: IconName.Parking,
|
||||
[FacilityEnum.ParkingAttendant]: IconName.StarFilled,
|
||||
[FacilityEnum.ParkingElectricCharging]: IconName.ElectricCar,
|
||||
[FacilityEnum.ParkingFreeParking]: IconName.Parking,
|
||||
[FacilityEnum.ParkingGarage]: IconName.Garage,
|
||||
[FacilityEnum.ParkingOutdoor]: IconName.ParkingOutdoor,
|
||||
[FacilityEnum.PCHookUpInRoom]: IconName.StarFilled,
|
||||
[FacilityEnum.PetFriendlyRooms]: IconName.Pets,
|
||||
[FacilityEnum.PillowAlarmsAvailable]: IconName.StarFilled,
|
||||
[FacilityEnum.PlayStationInPlayArea]: IconName.Gaming,
|
||||
[FacilityEnum.Pool]: IconName.Swim,
|
||||
[FacilityEnum.PoolSwimmingPoolJacuzziAtHotel]: IconName.Swim,
|
||||
[FacilityEnum.PrintingService]: IconName.StarFilled,
|
||||
[FacilityEnum.PropertyMeetsRequirementsFireSafety]: IconName.StarFilled,
|
||||
[FacilityEnum.PublicAddressSystem]: IconName.StarFilled,
|
||||
[FacilityEnum.RelaxationSuite]: IconName.StarFilled,
|
||||
[FacilityEnum.Restaurant]: IconName.Restaurant,
|
||||
[FacilityEnum.RestrictedRoomAccessAllScandic]: IconName.StarFilled,
|
||||
[FacilityEnum.RooftopBar]: IconName.Deck,
|
||||
[FacilityEnum.RoomsAccessibleFromTheInterior]: IconName.StarFilled,
|
||||
[FacilityEnum.RoomService]: IconName.RoomService,
|
||||
[FacilityEnum.RoomWindowsOpen]: IconName.StarFilled,
|
||||
[FacilityEnum.RoomWindowsThatOpenHaveLockingDevice]: IconName.StarFilled,
|
||||
[FacilityEnum.Rural1]: IconName.StarFilled,
|
||||
[FacilityEnum.Rural2]: IconName.StarFilled,
|
||||
[FacilityEnum.SafeDepositBoxInRoomsAllScandic]: IconName.SafetyBox,
|
||||
[FacilityEnum.SafeDepositBoxInRoomsCanHoldA17InchLaptop]: IconName.SafetyBox,
|
||||
[FacilityEnum.SafeDepositBoxInRoomsCannotHoldALaptop]: IconName.SafetyBox,
|
||||
[FacilityEnum.SafetyChainsOnGuestRoomDoor]: IconName.StarFilled,
|
||||
[FacilityEnum.Sauna]: IconName.Sauna,
|
||||
[FacilityEnum.ScandicShop24Hrs]: IconName.ConvenienceStore24h,
|
||||
[FacilityEnum.SecondaryLocksOnSlidingGlassDoors]: IconName.StarFilled,
|
||||
[FacilityEnum.SecondaryLocksOnWindows]: IconName.StarFilled,
|
||||
[FacilityEnum.Security24Hours]: IconName.Guard,
|
||||
[FacilityEnum.SecurityEscortsAvailableOnRequest]: IconName.Guard,
|
||||
[FacilityEnum.SecurityPersonnelOnSite]: IconName.Guard,
|
||||
[FacilityEnum.SeparateFloorsForWomen]: IconName.StarFilled,
|
||||
[FacilityEnum.ServesBreakfastAlwaysIncluded]: IconName.Breakfast,
|
||||
[FacilityEnum.ServesBreakfastNotAlwaysIncluded]: IconName.Breakfast,
|
||||
[FacilityEnum.ServesOrganicBreakfastAlwaysIncluded]: IconName.Breakfast,
|
||||
[FacilityEnum.ServesOrganicBreakfastNotAlwaysIncluded]: IconName.Breakfast,
|
||||
[FacilityEnum.ServiceGuideDogsAllowed]: IconName.StarFilled,
|
||||
[FacilityEnum.ServiceSecurity24Hrs]: IconName.Guard,
|
||||
[FacilityEnum.Shopping]: IconName.Shopping,
|
||||
[FacilityEnum.SkateboardsForLoan]: IconName.Skateboarding,
|
||||
[FacilityEnum.Skiing0To1Km]: IconName.Skiing,
|
||||
[FacilityEnum.Skybar]: IconName.LocalBar,
|
||||
[FacilityEnum.SmokeDetectorsAllScandic]: IconName.StarFilled,
|
||||
[FacilityEnum.Solarium]: IconName.StarFilled,
|
||||
[FacilityEnum.SpecialNeedsMenus]: IconName.StarFilled,
|
||||
[FacilityEnum.Sports]: IconName.Sports,
|
||||
[FacilityEnum.SprinklersAllScandic]: IconName.StarFilled,
|
||||
[FacilityEnum.SprinklersInAllHalls]: IconName.StarFilled,
|
||||
[FacilityEnum.SprinklersInAllPublicAreas]: IconName.StarFilled,
|
||||
[FacilityEnum.SprinklersInAllRooms]: IconName.StarFilled,
|
||||
[FacilityEnum.StaffInDuplicateKeys]: IconName.StarFilled,
|
||||
[FacilityEnum.StaffRedCrossCertifiedInCPR]: IconName.StarFilled,
|
||||
[FacilityEnum.StaffTrainedForDisabledGuests]: IconName.StarFilled,
|
||||
[FacilityEnum.StaffTrainedInAutomatedExternalDefibrillatorUsageAED]:
|
||||
IconName.StarFilled,
|
||||
[FacilityEnum.StaffTrainedInCPR]: IconName.StarFilled,
|
||||
[FacilityEnum.StaffTrainedInFirstAid]: IconName.StarFilled,
|
||||
[FacilityEnum.StaffTrainedInFirstAidTechniques]: IconName.StarFilled,
|
||||
[FacilityEnum.StaffTrainedToCaterForDisabledGuestsAllScandic]:
|
||||
IconName.StarFilled,
|
||||
[FacilityEnum.Suburbs]: IconName.StarFilled,
|
||||
[FacilityEnum.SwingboltLock]: IconName.StarFilled,
|
||||
[FacilityEnum.TeleConferencingFacilitiesAvailable]: IconName.StarFilled,
|
||||
[FacilityEnum.TelevisionsWithSubtitlesOrClosedCaptions]: IconName.StarFilled,
|
||||
[FacilityEnum.Tennis1]: IconName.Sports,
|
||||
[FacilityEnum.Tennis2]: IconName.Sports,
|
||||
[FacilityEnum.TennisPadel]: IconName.Sports,
|
||||
[FacilityEnum.Theatre]: IconName.Theatre,
|
||||
[FacilityEnum.TrouserPress]: IconName.Ironing,
|
||||
[FacilityEnum.TVWithChromecast1]: IconName.TvCasting,
|
||||
[FacilityEnum.TVWithChromecast2]: IconName.TvCasting,
|
||||
[FacilityEnum.UniformSecurityOnPremises]: IconName.StarFilled,
|
||||
[FacilityEnum.UtilityRoomForIroning]: IconName.Ironing,
|
||||
[FacilityEnum.VendingMachineWithNecessities]: IconName.Dining,
|
||||
[FacilityEnum.VideoSurveillanceInHallways]: IconName.StarFilled,
|
||||
[FacilityEnum.VideoSurveillanceInPublicAreas]: IconName.StarFilled,
|
||||
[FacilityEnum.VideoSurveillanceMonitored24HrsADay]: IconName.StarFilled,
|
||||
[FacilityEnum.VideoSurveillanceOfAllParkingAreas]: IconName.StarFilled,
|
||||
[FacilityEnum.VideoSurveillanceOfExteriorFrontEntrance]: IconName.StarFilled,
|
||||
[FacilityEnum.VideoSurveillanceRecorded24HrsADayParkingArea]:
|
||||
IconName.StarFilled,
|
||||
[FacilityEnum.WallMountedCycleRack]: IconName.Bike,
|
||||
[FacilityEnum.WellLitWalkways]: IconName.StarFilled,
|
||||
[FacilityEnum.WellnessAndSaunaEntranceFeeAdmission16PlusYears]: IconName.Spa,
|
||||
[FacilityEnum.WellnessPoolSaunaEntranceFeeAdmission16PlusYears]: IconName.Spa,
|
||||
[FacilityEnum.WheelchairAccess]: IconName.Wheelchair,
|
||||
[FacilityEnum.WideCorridors]: IconName.StarFilled,
|
||||
[FacilityEnum.WideEntrance]: IconName.StarFilled,
|
||||
[FacilityEnum.WideRestaurantEntrance]: IconName.StarFilled,
|
||||
[FacilityEnum.WiFiWirelessInternetAccessAllScandic]: IconName.StarFilled,
|
||||
}
|
||||
|
||||
interface mapFacilityToIconProps {
|
||||
id: FacilityEnum
|
||||
}
|
||||
export function FacilityToIcon({
|
||||
id,
|
||||
...props
|
||||
}: mapFacilityToIconProps &
|
||||
(
|
||||
| MaterialIconSetIconProps
|
||||
| NucleoIconProps
|
||||
| IconProps
|
||||
)): JSX.Element | null {
|
||||
const iconName = facilityToIconMap[id]
|
||||
return <IconByIconName iconName={iconName} {...props} />
|
||||
}
|
||||
7
packages/common/constants/hotelType.ts
Normal file
7
packages/common/constants/hotelType.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const HotelTypes = {
|
||||
Signature: "signature",
|
||||
ScandicGo: "scandicgo",
|
||||
Regular: "regular",
|
||||
} as const
|
||||
|
||||
export type HotelType = (typeof HotelTypes)[keyof typeof HotelTypes]
|
||||
8
packages/common/constants/signatureHotels.ts
Normal file
8
packages/common/constants/signatureHotels.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export enum SignatureHotelEnum {
|
||||
DowntownCamper = "879",
|
||||
GrandHotelOslo = "340",
|
||||
Haymarket = "890",
|
||||
HotelNorge = "785",
|
||||
Marski = "605",
|
||||
TheDock = "796",
|
||||
}
|
||||
@@ -33,12 +33,16 @@
|
||||
"./utils/isValidJson": "./utils/isValidJson.ts",
|
||||
"./hooks/*": "./hooks/*.ts",
|
||||
"./stores/*": "./stores/*.ts",
|
||||
"./constants/currency": "./constants/currency.ts",
|
||||
"./constants/dateFormats": "./constants/dateFormats.ts",
|
||||
"./constants/facilities": "./constants/facilities.ts",
|
||||
"./constants/hotelType": "./constants/hotelType.ts",
|
||||
"./constants/language": "./constants/language.ts",
|
||||
"./constants/membershipLevels": "./constants/membershipLevels.ts",
|
||||
"./constants/paymentMethod": "./constants/paymentMethod.ts",
|
||||
"./constants/currency": "./constants/currency.ts",
|
||||
"./constants/dateFormats": "./constants/dateFormats.ts",
|
||||
"./constants/routes/*": "./constants/routes/*.ts"
|
||||
"./constants/rateType": "./constants/rateType.ts",
|
||||
"./constants/routes/*": "./constants/routes/*.ts",
|
||||
"./constants/signatureHotels": "./constants/signatureHotels.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { dirname, join } from 'path'
|
||||
import type { StorybookConfig } from '@storybook/nextjs-vite'
|
||||
import { dirname, join } from 'path'
|
||||
import { mergeConfig } from 'vite'
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../lib/**/*.mdx', '../lib/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
@@ -9,11 +10,42 @@ const config: StorybookConfig = {
|
||||
getAbsolutePath('@storybook/addon-vitest'),
|
||||
getAbsolutePath('@storybook/addon-docs'),
|
||||
getAbsolutePath('@storybook/addon-a11y'),
|
||||
getAbsolutePath('storybook-react-intl'),
|
||||
],
|
||||
framework: {
|
||||
name: getAbsolutePath('@storybook/nextjs-vite'),
|
||||
options: {},
|
||||
},
|
||||
async viteFinal(config) {
|
||||
return mergeConfig(config, {
|
||||
plugins: [
|
||||
// Add babel plugin for react-intl transformation
|
||||
{
|
||||
name: 'formatjs-transform',
|
||||
async transform(code, id) {
|
||||
if (id.includes('node_modules')) return
|
||||
if (!/\.(jsx?|tsx?)$/.test(id)) return
|
||||
|
||||
const babel = await import('@babel/core')
|
||||
const result = babel.transformSync(code, {
|
||||
plugins: [
|
||||
[
|
||||
'formatjs',
|
||||
{
|
||||
idInterpolationPattern: '[sha512:contenthash:base64:6]',
|
||||
ast: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
filename: id,
|
||||
})
|
||||
|
||||
return result?.code
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
}
|
||||
export default config
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { withThemeByClassName } from '@storybook/addon-themes'
|
||||
|
||||
import { IntlProvider } from 'react-intl'
|
||||
import type { Preview, ReactRenderer } from '@storybook/nextjs-vite'
|
||||
import { reactIntl } from './reactIntl'
|
||||
|
||||
import '../lib/fonts.css'
|
||||
import '../lib/style.css'
|
||||
import '../lib/design-system-new-deprecated.css'
|
||||
|
||||
export const themes = {
|
||||
themes: {
|
||||
@@ -19,8 +21,33 @@ export const themes = {
|
||||
}
|
||||
|
||||
const preview: Preview = {
|
||||
decorators: [withThemeByClassName<ReactRenderer>(themes)],
|
||||
decorators: [
|
||||
// Theme decorator
|
||||
withThemeByClassName<ReactRenderer>(themes),
|
||||
(Story) => (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
<IntlProvider locale="en" {...(reactIntl as any)}>
|
||||
<Story />
|
||||
</IntlProvider>
|
||||
),
|
||||
],
|
||||
initialGlobals: {
|
||||
locale: reactIntl.defaultLocale,
|
||||
locales: {
|
||||
en: { icon: '🇬🇧', title: 'English', right: 'EN' },
|
||||
sv: { icon: '🇸🇪', title: 'Svenska', right: 'SV' },
|
||||
da: { icon: '🇩🇰', title: 'Dansk', right: 'DA' },
|
||||
no: { icon: '🇳🇴', title: 'Norsk', right: 'NO' },
|
||||
fi: { icon: '🇫🇮', title: 'Suomi', right: 'FI' },
|
||||
de: { icon: '🇩🇪', title: 'Deutsch', right: 'DE' },
|
||||
},
|
||||
backgrounds: { value: 'scandicSubtle' },
|
||||
},
|
||||
parameters: {
|
||||
reactIntl,
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
docs: {
|
||||
toc: true,
|
||||
},
|
||||
@@ -31,18 +58,18 @@ const preview: Preview = {
|
||||
},
|
||||
},
|
||||
backgrounds: {
|
||||
values: [
|
||||
options: {
|
||||
// 👇 Scandic
|
||||
{ name: 'Scandic Primary', value: '#FAF6F2' },
|
||||
{ name: 'Scandic Subtle', value: '#F2ECE6' },
|
||||
{ name: 'Scandic Primary Dark', value: '#4D001B' },
|
||||
scandicPrimary: { name: 'Scandic Primary', value: '#FAF6F2' },
|
||||
scandicSubtle: { name: 'Scandic Subtle', value: '#F2ECE6' },
|
||||
scandicPrimaryDark: { name: 'Scandic Primary Dark', value: '#4D001B' },
|
||||
// 👇 Default values
|
||||
{ name: 'Storybook Dark', value: '#333' },
|
||||
{ name: 'Storybook Light', value: '#F7F9F2' },
|
||||
],
|
||||
default: 'Scandic Primary',
|
||||
storybookDark: { name: 'Storybook Dark', value: '#333' },
|
||||
storybookLight: { name: 'Storybook Light', value: '#F7F9F2' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
tags: ['autodocs'],
|
||||
}
|
||||
|
||||
|
||||
27
packages/design-system/.storybook/reactIntl.ts
Normal file
27
packages/design-system/.storybook/reactIntl.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/* eslint-disable import/no-relative-packages */
|
||||
import en from '../../../apps/scandic-web/i18n/dictionaries/en.json'
|
||||
import sv from '../../../apps/scandic-web/i18n/dictionaries/sv.json'
|
||||
import da from '../../../apps/scandic-web/i18n/dictionaries/da.json'
|
||||
import fi from '../../../apps/scandic-web/i18n/dictionaries/fi.json'
|
||||
import de from '../../../apps/scandic-web/i18n/dictionaries/de.json'
|
||||
import no from '../../../apps/scandic-web/i18n/dictionaries/no.json'
|
||||
|
||||
const locales = ['en', 'sv', 'da', 'fi', 'no', 'de']
|
||||
|
||||
const messages: Record<(typeof locales)[number], unknown> = {
|
||||
en,
|
||||
sv,
|
||||
da,
|
||||
fi,
|
||||
no,
|
||||
de,
|
||||
}
|
||||
|
||||
const formats = {} // optional, if you have any formats
|
||||
|
||||
export const reactIntl = {
|
||||
defaultLocale: 'en',
|
||||
locales,
|
||||
messages,
|
||||
formats,
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import { FlatCompat } from '@eslint/eslintrc'
|
||||
import js from '@eslint/js'
|
||||
import importPlugin from 'eslint-plugin-import'
|
||||
import formatjs from 'eslint-plugin-formatjs'
|
||||
|
||||
const compat = new FlatCompat({
|
||||
recommendedConfig: js.configs.recommended,
|
||||
@@ -28,6 +29,7 @@ export default defineConfig([
|
||||
plugins: {
|
||||
'react-refresh': reactRefresh,
|
||||
import: importPlugin,
|
||||
formatjs,
|
||||
},
|
||||
rules: {
|
||||
'import/no-relative-packages': 'error',
|
||||
@@ -37,6 +39,18 @@ export default defineConfig([
|
||||
allowConstantExport: true,
|
||||
},
|
||||
],
|
||||
|
||||
'formatjs/enforce-default-message': ['error', 'literal'],
|
||||
'formatjs/enforce-placeholders': ['error'],
|
||||
'formatjs/enforce-plural-rules': ['error'],
|
||||
'formatjs/no-literal-string-in-jsx': ['error'],
|
||||
'formatjs/no-multiple-whitespaces': ['error'],
|
||||
'formatjs/no-multiple-plurals': ['error'],
|
||||
'formatjs/no-invalid-icu': ['error'],
|
||||
'formatjs/no-id': ['error'],
|
||||
'formatjs/no-complex-selectors': ['error'],
|
||||
'formatjs/no-useless-message': ['error'],
|
||||
'formatjs/prefer-pound-in-plural': ['error'],
|
||||
},
|
||||
},
|
||||
globalIgnores(['**/dist', '**/.eslintrc.cjs']),
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import { fn } from 'storybook/test'
|
||||
import { BookingCodeChip } from './index'
|
||||
|
||||
const meta = {
|
||||
title: 'Components/BookingCodeChip',
|
||||
component: BookingCodeChip,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies Meta<typeof BookingCodeChip>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof BookingCodeChip>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
render: () => <BookingCodeChip bookingCode="ABC123" withText />,
|
||||
}
|
||||
|
||||
export const WithoutText: Story = {
|
||||
args: {},
|
||||
render: () => <BookingCodeChip bookingCode="ABC123" withText={false} />,
|
||||
}
|
||||
|
||||
export const FilledIcon: Story = {
|
||||
args: {},
|
||||
render: () => <BookingCodeChip bookingCode="ABC123" filledIcon />,
|
||||
}
|
||||
|
||||
export const Unavailable: Story = {
|
||||
args: {},
|
||||
render: () => <BookingCodeChip bookingCode="ABC123" isUnavailable />,
|
||||
}
|
||||
|
||||
export const AlignCenter: Story = {
|
||||
args: {},
|
||||
render: () => <BookingCodeChip bookingCode="ABC123" alignCenter />,
|
||||
}
|
||||
|
||||
export const WithCloseButton: Story = {
|
||||
args: {},
|
||||
render: () => (
|
||||
<BookingCodeChip bookingCode="ABC123" withCloseButton onClose={fn} />
|
||||
),
|
||||
}
|
||||
|
||||
export const CampaignBreakfastIncluded: Story = {
|
||||
args: {},
|
||||
render: () => (
|
||||
<BookingCodeChip isCampaign bookingCode="SUMMER25" isBreakfastIncluded />
|
||||
),
|
||||
}
|
||||
|
||||
export const CampaignBreakfastExcluded: Story = {
|
||||
args: {},
|
||||
render: () => (
|
||||
<BookingCodeChip
|
||||
isCampaign
|
||||
bookingCode="SUMMER25"
|
||||
isBreakfastIncluded={false}
|
||||
/>
|
||||
),
|
||||
}
|
||||
|
||||
export const CampaignFilledIcon: Story = {
|
||||
args: {},
|
||||
render: () => (
|
||||
<BookingCodeChip
|
||||
isCampaign
|
||||
bookingCode="SUMMER25"
|
||||
isBreakfastIncluded
|
||||
filledIcon
|
||||
/>
|
||||
),
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Button as ButtonRAC } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
import { Button as ButtonRAC } from 'react-aria-components'
|
||||
import { useIntl } from 'react-intl'
|
||||
|
||||
import IconChip from "@scandic-hotels/design-system/IconChip"
|
||||
import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon"
|
||||
import FilledDiscountIcon from "@scandic-hotels/design-system/Icons/FilledDiscountIcon"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import IconChip from '@scandic-hotels/design-system/IconChip'
|
||||
import DiscountIcon from '@scandic-hotels/design-system/Icons/DiscountIcon'
|
||||
import FilledDiscountIcon from '@scandic-hotels/design-system/Icons/FilledDiscountIcon'
|
||||
import { MaterialIcon } from '@scandic-hotels/design-system/Icons/MaterialIcon'
|
||||
import { Typography } from '@scandic-hotels/design-system/Typography'
|
||||
|
||||
import styles from "./bookingCodeChip.module.css"
|
||||
import styles from './bookingCodeChip.module.css'
|
||||
|
||||
type BaseBookingCodeChipProps = {
|
||||
alignCenter?: boolean
|
||||
@@ -30,7 +30,7 @@ type BookingCodeChipProps =
|
||||
| BookingCodeChipWithoutCloseButtonProps
|
||||
| BookingCodeChipWithCloseButtonProps
|
||||
|
||||
export default function BookingCodeChip({
|
||||
export function BookingCodeChip({
|
||||
alignCenter,
|
||||
bookingCode,
|
||||
isBreakfastIncluded,
|
||||
@@ -58,19 +58,19 @@ export default function BookingCodeChip({
|
||||
<p className={styles.bookingCodeChip}>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<strong>
|
||||
{intl.formatMessage({ defaultMessage: "Campaign" })}
|
||||
{intl.formatMessage({ defaultMessage: 'Campaign' })}
|
||||
</strong>
|
||||
</Typography>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<span>
|
||||
{isBreakfastIncluded
|
||||
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||
`${bookingCode ?? ""} ${intl.formatMessage({
|
||||
defaultMessage: "Breakfast included",
|
||||
`${bookingCode ?? ''} ${intl.formatMessage({
|
||||
defaultMessage: 'Breakfast included',
|
||||
})}`
|
||||
: // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||
`${bookingCode ?? ""} ${intl.formatMessage({
|
||||
defaultMessage: "Breakfast excluded",
|
||||
`${bookingCode ?? ''} ${intl.formatMessage({
|
||||
defaultMessage: 'Breakfast excluded',
|
||||
})}`}
|
||||
</span>
|
||||
</Typography>
|
||||
@@ -96,12 +96,14 @@ export default function BookingCodeChip({
|
||||
className={alignCenter ? styles.center : undefined}
|
||||
>
|
||||
<p
|
||||
className={`${styles.bookingCodeChip} ${isUnavailable ? styles.unavailable : ""}`}
|
||||
className={`${styles.bookingCodeChip} ${isUnavailable ? styles.unavailable : ''}`}
|
||||
>
|
||||
{withText && (
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<strong>
|
||||
{intl.formatMessage({ defaultMessage: "Booking code" })}
|
||||
{intl.formatMessage({
|
||||
defaultMessage: 'Booking code',
|
||||
})}
|
||||
</strong>
|
||||
</Typography>
|
||||
)}
|
||||
@@ -556,6 +556,7 @@ export const TextWithIcon: Story = {
|
||||
args: {
|
||||
onPress: fn(),
|
||||
children: (
|
||||
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||
<>
|
||||
Text with icon
|
||||
<MaterialIcon icon="chevron_right" size={24} color="CurrentColor" />
|
||||
@@ -577,6 +578,7 @@ export const TextWithIconInverted: Story = {
|
||||
args: {
|
||||
onPress: fn(),
|
||||
children: (
|
||||
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||
<>
|
||||
Text with icon
|
||||
<MaterialIcon icon="chevron_right" size={24} color="CurrentColor" />
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import Footnote from '../Footnote'
|
||||
import Footnote from '@scandic-hotels/design-system/Footnote'
|
||||
|
||||
import { chipVariants } from './variants'
|
||||
import { VariantProps } from 'class-variance-authority'
|
||||
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
|
||||
interface ChipProps
|
||||
export interface ChipProps
|
||||
extends React.HtmlHTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof chipVariants> {}
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
|
||||
import { FacilityEnum } from '@scandic-hotels/common/constants/facilities'
|
||||
|
||||
import { FacilityToIcon } from '.'
|
||||
import { iconVariantConfig } from '../Icons/variants'
|
||||
|
||||
const facilityMapping: Record<string, FacilityEnum> = Object.fromEntries(
|
||||
Object.entries(FacilityEnum).filter(([k]) => isNaN(Number(k)))
|
||||
) as Record<string, FacilityEnum>
|
||||
|
||||
const colorOptions = Object.keys(iconVariantConfig.variants.color)
|
||||
|
||||
const meta: Meta<typeof FacilityToIcon> = {
|
||||
title: 'Components/Facility To Icon',
|
||||
component: FacilityToIcon,
|
||||
argTypes: {
|
||||
id: {
|
||||
control: 'select',
|
||||
options: Object.keys(FacilityEnum)
|
||||
.map((key) => FacilityEnum[key as keyof typeof FacilityEnum])
|
||||
.filter((x) => typeof x === 'string')
|
||||
.toSorted(),
|
||||
mapping: facilityMapping,
|
||||
description: 'Facility identifier (mapped to the corresponding icon)',
|
||||
},
|
||||
color: {
|
||||
control: 'select',
|
||||
options: colorOptions,
|
||||
description: 'Icon color variant',
|
||||
},
|
||||
size: {
|
||||
control: 'number',
|
||||
description: 'Icon pixel size',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof FacilityToIcon>
|
||||
|
||||
export const Playground: Story = {
|
||||
args: {
|
||||
id: FacilityEnum.Bar,
|
||||
size: 24,
|
||||
color: 'Icon/Default',
|
||||
},
|
||||
}
|
||||
|
||||
const exampleFacilities = [
|
||||
FacilityEnum.AirConAirCooling,
|
||||
FacilityEnum.AirportMaxDistance8Km,
|
||||
FacilityEnum.Bar,
|
||||
FacilityEnum.CashFreeHotel,
|
||||
FacilityEnum.ChildrenWelcome,
|
||||
FacilityEnum.Elevator,
|
||||
FacilityEnum.Gym,
|
||||
FacilityEnum.ParkingGarage,
|
||||
FacilityEnum.ParkingOutdoor,
|
||||
FacilityEnum.Pool,
|
||||
FacilityEnum.Tennis1,
|
||||
]
|
||||
|
||||
export const Examples: Story = {
|
||||
args: {
|
||||
size: 24,
|
||||
color: 'Icon/Default',
|
||||
},
|
||||
render: (args) => (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))',
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
{exampleFacilities.map((key) => (
|
||||
<div
|
||||
key={key}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
padding: 8,
|
||||
borderRadius: 8,
|
||||
background: 'var(--ds-color-surface-subtle, #F2ECE6)',
|
||||
}}
|
||||
>
|
||||
<FacilityToIcon id={key} size={args.size} color={args.color} />
|
||||
<span style={{ fontSize: 12 }}>{FacilityEnum[key]}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
@@ -1,13 +1,26 @@
|
||||
import { IconByIconName } from "@scandic-hotels/design-system/Icons/IconByIconName"
|
||||
import { IconName } from "@scandic-hotels/design-system/Icons/iconName"
|
||||
import { FacilityEnum } from "@scandic-hotels/trpc/enums/facilities"
|
||||
import { FacilityEnum } from '@scandic-hotels/common/constants/facilities'
|
||||
|
||||
import type {
|
||||
IconProps,
|
||||
NucleoIconProps,
|
||||
} from "@scandic-hotels/design-system/Icons"
|
||||
import type { MaterialIconSetIconProps } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import type { JSX } from "react"
|
||||
import type { JSX } from 'react'
|
||||
import { IconName } from '../Icons/iconName'
|
||||
import { MaterialIconSetIconProps } from '../Icons/MaterialIcon'
|
||||
import { IconProps, NucleoIconProps } from '../Icons'
|
||||
import { IconByIconName } from '../Icons/IconByIconName'
|
||||
|
||||
interface mapFacilityToIconProps {
|
||||
id: FacilityEnum
|
||||
}
|
||||
export function FacilityToIcon({
|
||||
id,
|
||||
...props
|
||||
}: mapFacilityToIconProps &
|
||||
(
|
||||
| MaterialIconSetIconProps
|
||||
| NucleoIconProps
|
||||
| IconProps
|
||||
)): JSX.Element | null {
|
||||
const iconName = facilityToIconMap[id]
|
||||
return <IconByIconName iconName={iconName} {...props} />
|
||||
}
|
||||
|
||||
const facilityToIconMap: Record<FacilityEnum, IconName> = {
|
||||
[FacilityEnum.AccessibleBathingControls]: IconName.StarFilled,
|
||||
@@ -287,19 +300,3 @@ const facilityToIconMap: Record<FacilityEnum, IconName> = {
|
||||
[FacilityEnum.WideRestaurantEntrance]: IconName.StarFilled,
|
||||
[FacilityEnum.WiFiWirelessInternetAccessAllScandic]: IconName.StarFilled,
|
||||
}
|
||||
|
||||
interface mapFacilityToIconProps {
|
||||
id: FacilityEnum
|
||||
}
|
||||
export function FacilityToIcon({
|
||||
id,
|
||||
...props
|
||||
}: mapFacilityToIconProps &
|
||||
(
|
||||
| MaterialIconSetIconProps
|
||||
| NucleoIconProps
|
||||
| IconProps
|
||||
)): JSX.Element | null {
|
||||
const iconName = facilityToIconMap[id]
|
||||
return <IconByIconName iconName={iconName} {...props} />
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import { HotelCard } from './index'
|
||||
|
||||
import { fn } from 'storybook/test'
|
||||
import { RateTypeEnum } from '@scandic-hotels/common/constants/rateType'
|
||||
import { HotelTypeEnum } from '@scandic-hotels/trpc/enums/hotelType'
|
||||
import { Button } from '@scandic-hotels/design-system/Button'
|
||||
import { MaterialIcon } from '../Icons/MaterialIcon'
|
||||
|
||||
const meta: Meta<typeof HotelCard> = {
|
||||
title: 'Components/HotelCard',
|
||||
component: HotelCard,
|
||||
argTypes: {
|
||||
state: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: ['default', 'active'],
|
||||
},
|
||||
},
|
||||
type: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: ['mapListing', 'pageListing'],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof HotelCard>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
hotel: {
|
||||
id: '1',
|
||||
name: 'Test Hotel',
|
||||
address: { streetAddress: '123 Test Street', city: 'Test City' },
|
||||
description: 'A great place to stay.',
|
||||
hotelType: HotelTypeEnum.Signature,
|
||||
detailedFacilities: [],
|
||||
ratings: {
|
||||
tripAdvisor: 4,
|
||||
},
|
||||
},
|
||||
prices: {
|
||||
public: {
|
||||
rateType: RateTypeEnum.Regular,
|
||||
localPrice: {
|
||||
currency: 'SEK',
|
||||
pricePerNight: 1000,
|
||||
pricePerStay: 1000,
|
||||
},
|
||||
},
|
||||
member: {
|
||||
rateType: RateTypeEnum.Regular,
|
||||
localPrice: {
|
||||
currency: 'SEK',
|
||||
pricePerNight: 800,
|
||||
pricePerStay: 800,
|
||||
},
|
||||
},
|
||||
},
|
||||
state: 'default',
|
||||
isAlternative: false,
|
||||
type: 'pageListing',
|
||||
isUserLoggedIn: false,
|
||||
distanceToCityCenter: 0,
|
||||
bookingCode: 'ABC123',
|
||||
images: [
|
||||
{
|
||||
src: 'img/img2.jpg',
|
||||
alt: 'Alt text',
|
||||
smallSrc: 'img/img2.jpg',
|
||||
caption: 'Caption',
|
||||
},
|
||||
],
|
||||
|
||||
belowInfoSlot: (
|
||||
<Button
|
||||
onPress={() => fn()}
|
||||
variant="Text"
|
||||
typography="Body/Paragraph/mdBold"
|
||||
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||
>
|
||||
Read more
|
||||
<MaterialIcon icon="chevron_right" size={24} color="CurrentColor" />
|
||||
</Button>
|
||||
),
|
||||
onAddressClick: fn,
|
||||
onHover: fn,
|
||||
onHoverEnd: fn,
|
||||
},
|
||||
}
|
||||
|
||||
export const MapListing: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
type: 'mapListing',
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import TripadvisorIcon from '@scandic-hotels/design-system/Icons/TripadvisorIcon'
|
||||
import Image from '@scandic-hotels/design-system/Image'
|
||||
|
||||
import Chip from '@scandic-hotels/design-system/Chip'
|
||||
|
||||
import { hotelCardDialogImageVariants } from './variants'
|
||||
|
||||
import styles from './hotelCardDialogImage.module.css'
|
||||
|
||||
export type HotelCardDialogImageProps = {
|
||||
firstImage?: string
|
||||
altText?: string
|
||||
rating?: { tripAdvisor?: number | null }
|
||||
imageError: boolean
|
||||
setImageError: (error: boolean) => void
|
||||
position: 'top' | 'left'
|
||||
}
|
||||
|
||||
export function HotelCardDialogImage({
|
||||
firstImage,
|
||||
altText,
|
||||
rating,
|
||||
imageError,
|
||||
setImageError,
|
||||
position,
|
||||
}: HotelCardDialogImageProps) {
|
||||
const classNames = hotelCardDialogImageVariants({ position })
|
||||
|
||||
return (
|
||||
<div className={classNames}>
|
||||
{!firstImage || imageError ? (
|
||||
<div className={styles.imagePlaceholder} />
|
||||
) : (
|
||||
<Image
|
||||
src={firstImage}
|
||||
alt={altText || ''}
|
||||
fill
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
)}
|
||||
{rating?.tripAdvisor && (
|
||||
<div className={styles.tripAdvisor}>
|
||||
<Chip className={styles.tripAdvisor}>
|
||||
<TripadvisorIcon color="Icon/Interactive/Default" />
|
||||
{rating.tripAdvisor}
|
||||
</Chip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cva } from "class-variance-authority"
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
import styles from "./hotelCardDialogImage.module.css"
|
||||
import styles from './hotelCardDialogImage.module.css'
|
||||
|
||||
export const hotelCardDialogImageVariants = cva(styles.imageContainer, {
|
||||
variants: {
|
||||
@@ -10,6 +10,6 @@ export const hotelCardDialogImageVariants = cva(styles.imageContainer, {
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
position: "top",
|
||||
position: 'top',
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
import SkeletonShimmer from '@scandic-hotels/design-system/SkeletonShimmer'
|
||||
|
||||
import styles from './HotelCardSkeleton.module.css'
|
||||
|
||||
export function HotelCardSkeleton() {
|
||||
return (
|
||||
<article className={styles.card}>
|
||||
{/* image container */}
|
||||
<div className={styles.imageContainer}>
|
||||
<SkeletonShimmer width={'100%'} height="100%" />
|
||||
</div>
|
||||
|
||||
<div className={styles.content}>
|
||||
<SkeletonShimmer height={'65px'} />
|
||||
<div className={styles.text}>
|
||||
<SkeletonShimmer height={'20px'} />
|
||||
<SkeletonShimmer height={'20px'} />
|
||||
<SkeletonShimmer height={'20px'} />
|
||||
<SkeletonShimmer height={'20px'} />
|
||||
</div>
|
||||
<SkeletonShimmer height={'56px'} />
|
||||
<SkeletonShimmer height={'52px'} width={'150px'} />
|
||||
</div>
|
||||
|
||||
<div className={styles.priceVariants}>
|
||||
{/* price variants */}
|
||||
{Array.from({ length: 2 }).map((_, index) => (
|
||||
<SkeletonShimmer key={index} height={'100px'} />
|
||||
))}
|
||||
<SkeletonShimmer height={'40px'} />
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useIntl } from "react-intl"
|
||||
import { useIntl } from 'react-intl'
|
||||
|
||||
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
|
||||
import Caption from "@scandic-hotels/design-system/Caption"
|
||||
import Subtitle from "@scandic-hotels/design-system/Subtitle"
|
||||
import { CurrencyEnum } from '@scandic-hotels/common/constants/currency'
|
||||
import Caption from '@scandic-hotels/design-system/Caption'
|
||||
import Subtitle from '@scandic-hotels/design-system/Subtitle'
|
||||
|
||||
import styles from "./hotelChequeCard.module.css"
|
||||
import styles from './hotelChequeCard.module.css'
|
||||
|
||||
import type { ProductTypeCheque } from "@scandic-hotels/trpc/types/availability"
|
||||
import type { ProductTypeCheque } from '@scandic-hotels/trpc/types/availability'
|
||||
|
||||
export default function HotelChequeCard({
|
||||
productTypeCheque,
|
||||
@@ -19,7 +19,7 @@ export default function HotelChequeCard({
|
||||
<div className={styles.chequeRow}>
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "From",
|
||||
defaultMessage: 'From',
|
||||
})}
|
||||
</Caption>
|
||||
<div className={styles.cheque}>
|
||||
@@ -29,7 +29,7 @@ export default function HotelChequeCard({
|
||||
<Caption color="uiTextHighContrast">{CurrencyEnum.CC}</Caption>
|
||||
{productTypeCheque.localPrice.additionalPricePerStay > 0 ? (
|
||||
<>
|
||||
{"+"}
|
||||
{'+'}
|
||||
<Subtitle type="two" color="uiTextHighContrast">
|
||||
{productTypeCheque.localPrice.additionalPricePerStay}
|
||||
</Subtitle>
|
||||
@@ -45,15 +45,15 @@ export default function HotelChequeCard({
|
||||
<div className={styles.chequeRow}>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Approx.",
|
||||
defaultMessage: 'Approx.',
|
||||
})}
|
||||
</Caption>
|
||||
<Caption color={"uiTextMediumContrast"}>
|
||||
<Caption color={'uiTextMediumContrast'}>
|
||||
{productTypeCheque.requestedPrice.numberOfCheques} {CurrencyEnum.CC}
|
||||
{productTypeCheque.requestedPrice.additionalPricePerStay
|
||||
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||
" + "
|
||||
: ""}
|
||||
' + '
|
||||
: ''}
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
{`${productTypeCheque.requestedPrice.additionalPricePerStay} ${productTypeCheque.requestedPrice.currency}`}
|
||||
</Caption>
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import { StandaloneHotelCardDialog } from './index'
|
||||
|
||||
import { fn } from 'storybook/test'
|
||||
|
||||
const meta: Meta<typeof StandaloneHotelCardDialog> = {
|
||||
title: 'Components/StandaloneHotelCardDialog',
|
||||
component: StandaloneHotelCardDialog,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof StandaloneHotelCardDialog>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
data: {
|
||||
name: 'Hotel Name',
|
||||
image: {
|
||||
url: 'img/img2.jpg',
|
||||
alt: 'Alt text',
|
||||
},
|
||||
coordinates: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
chequePrice: null,
|
||||
publicPrice: 100,
|
||||
memberPrice: 200,
|
||||
redemptionPrice: null,
|
||||
voucherPrice: null,
|
||||
rateType: null,
|
||||
currency: 'SEK',
|
||||
amenities: [],
|
||||
ratings: { tripAdvisor: 5 },
|
||||
operaId: '123',
|
||||
facilityIds: [],
|
||||
hasEnoughPoints: false,
|
||||
},
|
||||
handleClose: fn(),
|
||||
},
|
||||
}
|
||||
@@ -1,46 +1,45 @@
|
||||
"use client"
|
||||
import { useSession } from "next-auth/react"
|
||||
import { useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
'use client'
|
||||
|
||||
import { selectRate } from "@scandic-hotels/common/constants/routes/hotelReservation"
|
||||
import Body from "@scandic-hotels/design-system/Body"
|
||||
import Caption from "@scandic-hotels/design-system/Caption"
|
||||
import Footnote from "@scandic-hotels/design-system/Footnote"
|
||||
import { IconButton } from "@scandic-hotels/design-system/IconButton"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import Link from "@scandic-hotels/design-system/Link"
|
||||
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
|
||||
import Subtitle from "@scandic-hotels/design-system/Subtitle"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { useState } from 'react'
|
||||
import { useIntl } from 'react-intl'
|
||||
|
||||
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { isValidClientSession } from "@/utils/clientSession"
|
||||
import { trackEvent } from "@/utils/tracking/base"
|
||||
import { selectRate } from '@scandic-hotels/common/constants/routes/hotelReservation'
|
||||
import Body from '@scandic-hotels/design-system/Body'
|
||||
import Caption from '@scandic-hotels/design-system/Caption'
|
||||
import Footnote from '@scandic-hotels/design-system/Footnote'
|
||||
import { IconButton } from '@scandic-hotels/design-system/IconButton'
|
||||
import { MaterialIcon } from '@scandic-hotels/design-system/Icons/MaterialIcon'
|
||||
import Link from '@scandic-hotels/design-system/Link'
|
||||
import { OldDSButton as Button } from '@scandic-hotels/design-system/OldDSButton'
|
||||
import Subtitle from '@scandic-hotels/design-system/Subtitle'
|
||||
import { Typography } from '@scandic-hotels/design-system/Typography'
|
||||
|
||||
import HotelPointsRow from "../../HotelCard/HotelPointsRow"
|
||||
import NoPriceAvailableCard from "../../HotelCard/NoPriceAvailableCard"
|
||||
import HotelCardDialogImage from "../HotelCardDialogImage"
|
||||
import { NoPriceAvailableCard } from '../../NoPriceAvailableCard'
|
||||
import { HotelCardDialogImage } from '../../HotelCardDialogImage'
|
||||
|
||||
import styles from "./standaloneHotelCardDialog.module.css"
|
||||
|
||||
import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
import styles from './standaloneHotelCardDialog.module.css'
|
||||
import { Lang } from '@scandic-hotels/common/constants/language'
|
||||
import { HotelPin } from '../../../Map/types'
|
||||
import { FacilityToIcon } from '@scandic-hotels/design-system/FacilityToIcon'
|
||||
import { HotelPointsRow } from '../../HotelPointsRow'
|
||||
|
||||
interface StandaloneHotelCardProps {
|
||||
data: HotelPin
|
||||
lang: Lang
|
||||
isUserLoggedIn: boolean
|
||||
handleClose: () => void
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
export default function StandaloneHotelCardDialog({
|
||||
export function StandaloneHotelCardDialog({
|
||||
data,
|
||||
lang,
|
||||
handleClose,
|
||||
isUserLoggedIn,
|
||||
onClick,
|
||||
}: StandaloneHotelCardProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const [imageError, setImageError] = useState(false)
|
||||
const { data: session } = useSession()
|
||||
const isUserLoggedIn = isValidClientSession(session)
|
||||
const {
|
||||
name,
|
||||
chequePrice,
|
||||
@@ -50,16 +49,14 @@ export default function StandaloneHotelCardDialog({
|
||||
voucherPrice,
|
||||
currency,
|
||||
amenities,
|
||||
images,
|
||||
image,
|
||||
ratings,
|
||||
operaId,
|
||||
hasEnoughPoints,
|
||||
} = data
|
||||
|
||||
const firstImage = images[0]?.imageSizes?.small
|
||||
const altText = images[0]?.metaData?.altText
|
||||
const notEnoughPointsLabel = intl.formatMessage({
|
||||
defaultMessage: "Not enough points",
|
||||
defaultMessage: 'Not enough points',
|
||||
})
|
||||
|
||||
const shouldShowNotEnoughPoints = redemptionPrice && !hasEnoughPoints
|
||||
@@ -72,15 +69,15 @@ export default function StandaloneHotelCardDialog({
|
||||
className={styles.closeButton}
|
||||
onPress={handleClose}
|
||||
aria-label={intl.formatMessage({
|
||||
defaultMessage: "Close",
|
||||
defaultMessage: 'Close',
|
||||
})}
|
||||
>
|
||||
<MaterialIcon icon="close" size={22} color="CurrentColor" />
|
||||
</IconButton>
|
||||
<HotelCardDialogImage
|
||||
firstImage={firstImage}
|
||||
altText={altText}
|
||||
rating={ratings}
|
||||
firstImage={image?.url}
|
||||
altText={image?.alt}
|
||||
rating={{ tripAdvisor: ratings?.tripAdvisor ?? null }}
|
||||
imageError={imageError}
|
||||
setImageError={setImageError}
|
||||
position="left"
|
||||
@@ -96,7 +93,7 @@ export default function StandaloneHotelCardDialog({
|
||||
)
|
||||
return (
|
||||
<div className={styles.facilitiesItem} key={facility.id}>
|
||||
{Icon && Icon}
|
||||
{Icon}
|
||||
<Footnote color="uiTextMediumContrast">
|
||||
{facility.name}
|
||||
</Footnote>
|
||||
@@ -115,13 +112,13 @@ export default function StandaloneHotelCardDialog({
|
||||
{redemptionPrice ? (
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Available rates",
|
||||
defaultMessage: 'Available rates',
|
||||
})}
|
||||
</Caption>
|
||||
) : (
|
||||
<Caption type="bold">
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "From",
|
||||
defaultMessage: 'From',
|
||||
})}
|
||||
</Caption>
|
||||
)}
|
||||
@@ -129,19 +126,19 @@ export default function StandaloneHotelCardDialog({
|
||||
<Subtitle type="two">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "{price} {currency}",
|
||||
defaultMessage: '{price} {currency}',
|
||||
},
|
||||
{
|
||||
price: chequePrice.numberOfCheques,
|
||||
currency: "CC",
|
||||
currency: 'CC',
|
||||
}
|
||||
)}
|
||||
{chequePrice.additionalPricePerStay > 0
|
||||
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||
" + " +
|
||||
' + ' +
|
||||
intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "{price} {currency}",
|
||||
defaultMessage: '{price} {currency}',
|
||||
},
|
||||
{
|
||||
price: chequePrice.additionalPricePerStay,
|
||||
@@ -154,7 +151,7 @@ export default function StandaloneHotelCardDialog({
|
||||
<span>
|
||||
/
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "night",
|
||||
defaultMessage: 'night',
|
||||
})}
|
||||
</span>
|
||||
</Body>
|
||||
@@ -164,7 +161,7 @@ export default function StandaloneHotelCardDialog({
|
||||
<Subtitle type="two">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "{price} {currency}",
|
||||
defaultMessage: '{price} {currency}',
|
||||
},
|
||||
{
|
||||
price: voucherPrice,
|
||||
@@ -176,7 +173,7 @@ export default function StandaloneHotelCardDialog({
|
||||
<span>
|
||||
/
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "night",
|
||||
defaultMessage: 'night',
|
||||
})}
|
||||
</span>
|
||||
</Body>
|
||||
@@ -186,7 +183,7 @@ export default function StandaloneHotelCardDialog({
|
||||
<Subtitle type="two">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "{price} {currency}",
|
||||
defaultMessage: '{price} {currency}',
|
||||
},
|
||||
{
|
||||
price: publicPrice,
|
||||
@@ -198,7 +195,7 @@ export default function StandaloneHotelCardDialog({
|
||||
<span>
|
||||
/
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "night",
|
||||
defaultMessage: 'night',
|
||||
})}
|
||||
</span>
|
||||
</Body>
|
||||
@@ -208,7 +205,7 @@ export default function StandaloneHotelCardDialog({
|
||||
<Subtitle type="two" color="red">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "{price} {currency}",
|
||||
defaultMessage: '{price} {currency}',
|
||||
},
|
||||
{
|
||||
price: memberPrice,
|
||||
@@ -220,7 +217,7 @@ export default function StandaloneHotelCardDialog({
|
||||
<span>
|
||||
/
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "night",
|
||||
defaultMessage: 'night',
|
||||
})}
|
||||
</span>
|
||||
</Body>
|
||||
@@ -242,17 +239,7 @@ export default function StandaloneHotelCardDialog({
|
||||
theme="base"
|
||||
size="small"
|
||||
className={styles.button}
|
||||
onClick={() =>
|
||||
trackEvent({
|
||||
event: "hotelClickMap",
|
||||
map: {
|
||||
action: "hotel click - map",
|
||||
},
|
||||
hotelInfo: {
|
||||
hotelId: operaId,
|
||||
},
|
||||
})
|
||||
}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Link
|
||||
href={`${selectRate(lang)}?hotel=${operaId}`}
|
||||
@@ -260,7 +247,7 @@ export default function StandaloneHotelCardDialog({
|
||||
keepSearchParams
|
||||
>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "See rooms",
|
||||
defaultMessage: 'See rooms',
|
||||
})}
|
||||
</Link>
|
||||
</Button>
|
||||
@@ -1,13 +1,16 @@
|
||||
import { useIntl } from "react-intl"
|
||||
import { useIntl } from 'react-intl'
|
||||
|
||||
import Caption from "@scandic-hotels/design-system/Caption"
|
||||
import Subtitle from "@scandic-hotels/design-system/Subtitle"
|
||||
import Caption from '@scandic-hotels/design-system/Caption'
|
||||
import Subtitle from '@scandic-hotels/design-system/Subtitle'
|
||||
|
||||
import styles from "./hotelPointsRow.module.css"
|
||||
import styles from './hotelPointsRow.module.css'
|
||||
|
||||
import type { PointsRowProps } from "@/types/components/hotelReservation/selectHotel/priceCardProps"
|
||||
|
||||
export default function HotelPointsRow({
|
||||
export type PointsRowProps = {
|
||||
pointsPerStay: number
|
||||
additionalPricePerStay?: number
|
||||
additionalPriceCurrency?: string
|
||||
}
|
||||
export function HotelPointsRow({
|
||||
pointsPerStay,
|
||||
additionalPricePerStay,
|
||||
additionalPriceCurrency,
|
||||
@@ -21,12 +24,12 @@ export default function HotelPointsRow({
|
||||
</Subtitle>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Points",
|
||||
defaultMessage: 'Points',
|
||||
})}
|
||||
</Caption>
|
||||
{additionalPricePerStay ? (
|
||||
<>
|
||||
{"+"}
|
||||
{'+'}
|
||||
<Subtitle type="two" color="uiTextHighContrast">
|
||||
{additionalPricePerStay}
|
||||
</Subtitle>
|
||||
@@ -1,17 +1,31 @@
|
||||
import { cx } from "class-variance-authority"
|
||||
import { useIntl } from "react-intl"
|
||||
import { cx } from 'class-variance-authority'
|
||||
import { useIntl } from 'react-intl'
|
||||
|
||||
import Body from "@scandic-hotels/design-system/Body"
|
||||
import Caption from "@scandic-hotels/design-system/Caption"
|
||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||
import Subtitle from "@scandic-hotels/design-system/Subtitle"
|
||||
import { RateTypeEnum } from "@scandic-hotels/trpc/enums/rateType"
|
||||
import Body from '@scandic-hotels/design-system/Body'
|
||||
import Caption from '@scandic-hotels/design-system/Caption'
|
||||
import { Divider } from '@scandic-hotels/design-system/Divider'
|
||||
import Subtitle from '@scandic-hotels/design-system/Subtitle'
|
||||
import { RateTypeEnum } from '@scandic-hotels/common/constants/rateType'
|
||||
|
||||
import styles from "./hotelPriceCard.module.css"
|
||||
import styles from './hotelPriceCard.module.css'
|
||||
|
||||
import type { PriceCardProps } from "@/types/components/hotelReservation/selectHotel/priceCardProps"
|
||||
type Price = {
|
||||
pricePerStay: number
|
||||
pricePerNight: number
|
||||
currency: string
|
||||
}
|
||||
|
||||
export default function HotelPriceCard({
|
||||
export type PriceCardProps = {
|
||||
productTypePrices: {
|
||||
rateType: RateTypeEnum
|
||||
localPrice: Price
|
||||
requestedPrice?: Price
|
||||
}
|
||||
isMemberPrice?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function HotelPriceCard({
|
||||
productTypePrices,
|
||||
isMemberPrice = false,
|
||||
className,
|
||||
@@ -29,7 +43,7 @@ export default function HotelPriceCard({
|
||||
<dt>
|
||||
<Caption color="red">
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Member price",
|
||||
defaultMessage: 'Member price',
|
||||
})}
|
||||
</Caption>
|
||||
</dt>
|
||||
@@ -39,7 +53,7 @@ export default function HotelPriceCard({
|
||||
<dt>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Standard price",
|
||||
defaultMessage: 'Standard price',
|
||||
})}
|
||||
</Caption>
|
||||
</dt>
|
||||
@@ -49,10 +63,10 @@ export default function HotelPriceCard({
|
||||
<dt>
|
||||
<Caption
|
||||
type="bold"
|
||||
color={isMemberPrice ? "red" : "uiTextHighContrast"}
|
||||
color={isMemberPrice ? 'red' : 'uiTextHighContrast'}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "From",
|
||||
defaultMessage: 'From',
|
||||
})}
|
||||
</Caption>
|
||||
</dt>
|
||||
@@ -60,12 +74,12 @@ export default function HotelPriceCard({
|
||||
<div className={styles.price}>
|
||||
<Subtitle
|
||||
type="two"
|
||||
color={isMemberPrice ? "red" : "uiTextHighContrast"}
|
||||
color={isMemberPrice ? 'red' : 'uiTextHighContrast'}
|
||||
>
|
||||
{productTypePrices.localPrice.pricePerNight}
|
||||
</Subtitle>
|
||||
<Body
|
||||
color={isMemberPrice ? "red" : "uiTextHighContrast"}
|
||||
color={isMemberPrice ? 'red' : 'uiTextHighContrast'}
|
||||
textTransform="bold"
|
||||
>
|
||||
{productTypePrices.localPrice.currency}
|
||||
@@ -73,7 +87,7 @@ export default function HotelPriceCard({
|
||||
<span className={styles.perNight}>
|
||||
/
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "night",
|
||||
defaultMessage: 'night',
|
||||
})}
|
||||
</span>
|
||||
</Body>
|
||||
@@ -85,12 +99,12 @@ export default function HotelPriceCard({
|
||||
<dt>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Approx.",
|
||||
defaultMessage: 'Approx.',
|
||||
})}
|
||||
</Caption>
|
||||
</dt>
|
||||
<dd>
|
||||
<Caption color={"uiTextMediumContrast"}>
|
||||
<Caption color={'uiTextMediumContrast'}>
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
{`${productTypePrices.requestedPrice.pricePerNight} `}
|
||||
{productTypePrices.requestedPrice.currency}
|
||||
@@ -108,12 +122,12 @@ export default function HotelPriceCard({
|
||||
<dt>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Total",
|
||||
defaultMessage: 'Total',
|
||||
})}
|
||||
</Caption>
|
||||
</dt>
|
||||
<dd>
|
||||
<Caption color={"uiTextMediumContrast"}>
|
||||
<Caption color={'uiTextMediumContrast'}>
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
{`${productTypePrices.localPrice.pricePerStay} `}
|
||||
{productTypePrices.localPrice.currency}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useIntl } from "react-intl"
|
||||
import { useIntl } from 'react-intl'
|
||||
|
||||
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
|
||||
import Caption from "@scandic-hotels/design-system/Caption"
|
||||
import Subtitle from "@scandic-hotels/design-system/Subtitle"
|
||||
import { CurrencyEnum } from '@scandic-hotels/common/constants/currency'
|
||||
import Caption from '@scandic-hotels/design-system/Caption'
|
||||
import Subtitle from '@scandic-hotels/design-system/Subtitle'
|
||||
|
||||
import styles from "./hotelVoucherCard.module.css"
|
||||
import styles from './hotelVoucherCard.module.css'
|
||||
|
||||
import type { ProductTypeVoucher } from "@scandic-hotels/trpc/types/availability"
|
||||
import type { ProductTypeVoucher } from '@scandic-hotels/trpc/types/availability'
|
||||
|
||||
export default function HotelVoucherCard({
|
||||
productTypeVoucher,
|
||||
@@ -19,7 +19,7 @@ export default function HotelVoucherCard({
|
||||
<div className={styles.voucherRow}>
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "From",
|
||||
defaultMessage: 'From',
|
||||
})}
|
||||
</Caption>
|
||||
<div className={styles.voucher}>
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useIntl } from "react-intl"
|
||||
import { useIntl } from 'react-intl'
|
||||
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { MaterialIcon } from '@scandic-hotels/design-system/Icons/MaterialIcon'
|
||||
import { Typography } from '@scandic-hotels/design-system/Typography'
|
||||
|
||||
import styles from "./noPriceAvailable.module.css"
|
||||
import styles from './noPriceAvailable.module.css'
|
||||
|
||||
export default function NoPriceAvailableCard() {
|
||||
export function NoPriceAvailableCard() {
|
||||
const intl = useIntl()
|
||||
return (
|
||||
<div className={styles.priceCard}>
|
||||
@@ -15,7 +15,7 @@ export default function NoPriceAvailableCard() {
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
defaultMessage:
|
||||
"There are no rooms available that match your request.",
|
||||
'There are no rooms available that match your request.',
|
||||
})}
|
||||
</span>
|
||||
</Typography>
|
||||
@@ -35,6 +35,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--Space-x2);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.hotelDescription {
|
||||
377
packages/design-system/lib/components/HotelCard/index.tsx
Normal file
377
packages/design-system/lib/components/HotelCard/index.tsx
Normal file
@@ -0,0 +1,377 @@
|
||||
'use client'
|
||||
|
||||
import { cx } from 'class-variance-authority'
|
||||
import { type ReadonlyURLSearchParams, useSearchParams } from 'next/navigation'
|
||||
import { memo } from 'react'
|
||||
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 '@scandic-hotels/design-system/Caption'
|
||||
import { Divider } from '@scandic-hotels/design-system/Divider'
|
||||
import { FacilityToIcon } from '@scandic-hotels/design-system/FacilityToIcon'
|
||||
import HotelLogoIcon from '@scandic-hotels/design-system/Icons/HotelLogoIcon'
|
||||
import ImageGallery, {
|
||||
GalleryImage,
|
||||
} from '@scandic-hotels/design-system/ImageGallery'
|
||||
import { HotelPointsRow } from './HotelPointsRow'
|
||||
import { NoPriceAvailableCard } from './NoPriceAvailableCard'
|
||||
import Link from '@scandic-hotels/design-system/Link'
|
||||
import { Typography } from '@scandic-hotels/design-system/Typography'
|
||||
|
||||
import HotelChequeCard from './HotelChequeCard'
|
||||
import { HotelPriceCard } from './HotelPriceCard'
|
||||
import HotelVoucherCard from './HotelVoucherCard'
|
||||
import { hotelCardVariants } from './variants'
|
||||
|
||||
import styles from './hotelCard.module.css'
|
||||
|
||||
import type { Lang } from '@scandic-hotels/common/constants/language'
|
||||
import { FacilityEnum } from '@scandic-hotels/common/constants/facilities'
|
||||
import { RateTypeEnum } from '@scandic-hotels/common/constants/rateType'
|
||||
import { CurrencyEnum } from '@scandic-hotels/common/constants/currency'
|
||||
import { BookingCodeChip } from '../BookingCodeChip'
|
||||
import { HotelType } from '@scandic-hotels/common/constants/hotelType'
|
||||
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: string
|
||||
}
|
||||
}[]
|
||||
}
|
||||
|
||||
images: GalleryImage[]
|
||||
distanceToCityCenter: number
|
||||
isUserLoggedIn: boolean
|
||||
type?: 'mapListing' | 'pageListing'
|
||||
state?: 'default' | 'active'
|
||||
bookingCode?: string | null
|
||||
isAlternative?: boolean
|
||||
|
||||
lang: Lang
|
||||
|
||||
belowInfoSlot: React.ReactNode
|
||||
|
||||
onHover: () => void
|
||||
onHoverEnd: () => void
|
||||
onAddressClick: () => void
|
||||
}
|
||||
|
||||
export const HotelCard = memo(
|
||||
({
|
||||
prices,
|
||||
hotel,
|
||||
distanceToCityCenter,
|
||||
isUserLoggedIn,
|
||||
state = 'default',
|
||||
type = 'pageListing',
|
||||
bookingCode = '',
|
||||
isAlternative,
|
||||
images,
|
||||
lang,
|
||||
belowInfoSlot,
|
||||
onAddressClick,
|
||||
onHover,
|
||||
onHoverEnd,
|
||||
}: HotelCardProps) => {
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
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 fullPrice = !bookingCode
|
||||
|
||||
const hasInsufficientPoints = !prices.redemptions?.some(
|
||||
(r) => r.hasEnoughPoints
|
||||
)
|
||||
const notEnoughPointsLabel = intl.formatMessage({
|
||||
defaultMessage: 'Not enough points',
|
||||
})
|
||||
|
||||
const isDisabled = prices.redemptions?.length && hasInsufficientPoints
|
||||
|
||||
return (
|
||||
<article
|
||||
className={classNames}
|
||||
onMouseEnter={() => onHover()}
|
||||
onMouseLeave={() => onHoverEnd()}
|
||||
>
|
||||
<div>
|
||||
<div className={styles.imageContainer}>
|
||||
<ImageGallery
|
||||
title={hotel.name}
|
||||
images={images}
|
||||
fill
|
||||
sizes="(min-width: 768px) calc(100vw - 340px), (min-width: 1367px) 33vw, 100vw"
|
||||
/>
|
||||
{hotel.ratings?.tripAdvisor && (
|
||||
<TripAdvisorChip rating={hotel.ratings.tripAdvisor} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.hotelContent}>
|
||||
<div className={styles.hotelInformation}>
|
||||
<div className={styles.titleContainer}>
|
||||
<HotelLogoIcon hotelId={hotel.id} hotelType={hotel.hotelType} />
|
||||
<Typography variant="Title/Subtitle/lg">
|
||||
<h2>{hotel.name}</h2>
|
||||
</Typography>
|
||||
<div className={styles.addressContainer}>
|
||||
<address className={styles.address}>
|
||||
{type == 'mapListing' && (
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>{addressStr}</p>
|
||||
</Typography>
|
||||
)}
|
||||
{type === 'pageListing' && (
|
||||
<Link
|
||||
size="small"
|
||||
textDecoration="underline"
|
||||
onClick={handleAddressClick}
|
||||
href={mapUrl}
|
||||
keepSearchParams
|
||||
aria-label={intl.formatMessage({
|
||||
defaultMessage: 'See on map',
|
||||
})}
|
||||
>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>{addressStr}</p>
|
||||
</Typography>
|
||||
</Link>
|
||||
)}
|
||||
</address>
|
||||
|
||||
<div>
|
||||
<Divider variant="vertical" />
|
||||
</div>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<span>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: '{number} km to city center',
|
||||
},
|
||||
{
|
||||
number: getSingleDecimal(distanceToCityCenter / 1000),
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hotel.description ? (
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p className={styles.hotelDescription}>{hotel.description}</p>
|
||||
</Typography>
|
||||
) : null}
|
||||
|
||||
<div className={styles.facilities}>
|
||||
{amenities.map((facility) => (
|
||||
<div className={styles.facilitiesItem} key={facility.id}>
|
||||
<FacilityToIcon id={facility.id} color="CurrentColor" />
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<span>{facility.name}</span>
|
||||
</Typography>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{belowInfoSlot}
|
||||
</div>
|
||||
<PricesWrapper
|
||||
pathname={selectRate(lang)}
|
||||
isClickable={prices && !isDisabled}
|
||||
hotelId={hotel.id}
|
||||
removeBookingCodeFromSearchParams={!!(bookingCode && fullPrice)}
|
||||
searchParams={searchParams}
|
||||
>
|
||||
{!prices ? (
|
||||
<NoPriceAvailableCard />
|
||||
) : (
|
||||
<>
|
||||
{bookingCode && (
|
||||
<BookingCodeChip
|
||||
bookingCode={bookingCode}
|
||||
isUnavailable={fullPrice}
|
||||
/>
|
||||
)}
|
||||
{(!isUserLoggedIn ||
|
||||
!prices?.member ||
|
||||
(bookingCode && !fullPrice)) &&
|
||||
prices?.public && (
|
||||
<HotelPriceCard
|
||||
productTypePrices={prices.public}
|
||||
className={styles.priceCard}
|
||||
/>
|
||||
)}
|
||||
{prices.member && (
|
||||
<HotelPriceCard
|
||||
productTypePrices={prices.member}
|
||||
className={styles.priceCard}
|
||||
isMemberPrice
|
||||
/>
|
||||
)}
|
||||
{prices?.voucher && (
|
||||
<HotelVoucherCard productTypeVoucher={prices.voucher} />
|
||||
)}
|
||||
{prices?.bonusCheque && (
|
||||
<HotelChequeCard productTypeCheque={prices.bonusCheque} />
|
||||
)}
|
||||
{prices?.redemptions?.length ? (
|
||||
<div className={styles.pointsCard}>
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: 'Available rates',
|
||||
})}
|
||||
</Caption>
|
||||
{prices.redemptions.map((redemption) => (
|
||||
<HotelPointsRow
|
||||
key={redemption.rateCode}
|
||||
pointsPerStay={redemption.localPrice.pointsPerStay}
|
||||
additionalPricePerStay={
|
||||
redemption.localPrice.additionalPricePerStay
|
||||
}
|
||||
additionalPriceCurrency={
|
||||
redemption.localPrice.currency ?? undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{isDisabled ? (
|
||||
<div className={cx(styles.fakeButton, styles.disabled)}>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<span>{notEnoughPointsLabel}</span>
|
||||
</Typography>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.fakeButton}>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: 'See rooms',
|
||||
})}
|
||||
</span>
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</PricesWrapper>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
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 = <div className={styles.prices}>{children}</div>
|
||||
|
||||
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 (
|
||||
<Link href={href} color="none" className={styles.link}>
|
||||
{content}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cva } from "class-variance-authority"
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
import styles from "./hotelCard.module.css"
|
||||
import styles from './hotelCard.module.css'
|
||||
|
||||
export const hotelCardVariants = cva(styles.card, {
|
||||
variants: {
|
||||
@@ -14,7 +14,7 @@ export const hotelCardVariants = cva(styles.card, {
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
type: "pageListing",
|
||||
state: "default",
|
||||
type: 'pageListing',
|
||||
state: 'default',
|
||||
},
|
||||
})
|
||||
@@ -12,7 +12,7 @@ import Lightbox from '../Lightbox'
|
||||
|
||||
import styles from './imageGallery.module.css'
|
||||
|
||||
interface GalleryImage {
|
||||
export interface GalleryImage {
|
||||
src: string
|
||||
alt: string
|
||||
caption?: string | null
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
|
||||
.heart > .li::before,
|
||||
.li:has(.heart)::before {
|
||||
content: url("/_static/icons/heart.svg");
|
||||
content: url('/_static/icons/heart.svg');
|
||||
position: relative;
|
||||
height: 8px;
|
||||
top: 3px;
|
||||
@@ -94,7 +94,7 @@
|
||||
|
||||
.check > .li::before,
|
||||
.li:has(.check)::before {
|
||||
content: url("/_static/icons/check-ring.svg");
|
||||
content: url('/_static/icons/check-ring.svg');
|
||||
position: relative;
|
||||
height: 8px;
|
||||
top: 3px;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { RTERenderMark, RTERenderOptionComponent } from "./node"
|
||||
import type { RTERenderMark, RTERenderOptionComponent } from './node'
|
||||
|
||||
export type RenderOptions = {
|
||||
[type: string]: RTERenderOptionComponent | RTERenderMark
|
||||
|
||||
71
packages/design-system/lib/components/Link/Link.stories.tsx
Normal file
71
packages/design-system/lib/components/Link/Link.stories.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
|
||||
import { expect } from 'storybook/test'
|
||||
import Link from '.'
|
||||
|
||||
const meta: Meta<typeof Link> = {
|
||||
title: 'Components/Link',
|
||||
component: Link,
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['small', 'regular', 'tiny', 'none'],
|
||||
},
|
||||
scroll: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
prefetch: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
partialMatch: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Link>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
active: false,
|
||||
href: 'https://www.scandichotels.com/en',
|
||||
},
|
||||
render: (args) => <Link {...args}>{args.href}</Link>,
|
||||
play: async ({ canvasElement }) => {
|
||||
const link = canvasElement.querySelector('a')
|
||||
if (!link) throw new Error('Link not found')
|
||||
expect(link).toBeInTheDocument()
|
||||
},
|
||||
}
|
||||
|
||||
export const Focused: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
},
|
||||
render: Default.render,
|
||||
play: async ({ canvasElement }) => {
|
||||
const link = canvasElement.querySelector('a')
|
||||
if (!link) throw new Error('Link not found')
|
||||
expect(link).toBeInTheDocument()
|
||||
|
||||
expect(link).not.toHaveFocus()
|
||||
let styles = getComputedStyle(link)
|
||||
expect(styles.outlineStyle).toBe('none')
|
||||
expect(parseFloat(styles.outlineWidth)).toBe(0)
|
||||
|
||||
link?.focus()
|
||||
|
||||
expect(link).toHaveFocus()
|
||||
styles = getComputedStyle(link)
|
||||
expect(styles.outlineStyle).not.toBe('none')
|
||||
expect(parseFloat(styles.outlineWidth)).toBeGreaterThan(0)
|
||||
},
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useIntl } from "react-intl"
|
||||
import { useIntl } from 'react-intl'
|
||||
|
||||
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { formatPrice } from '@scandic-hotels/common/utils/numberFormatting'
|
||||
import { MaterialIcon } from '@scandic-hotels/design-system/Icons/MaterialIcon'
|
||||
import { Typography } from '@scandic-hotels/design-system/Typography'
|
||||
|
||||
import HotelMarker from "@/components/Maps/Markers/HotelMarker"
|
||||
import HotelMarker from '../../../Markers/HotelMarker'
|
||||
|
||||
import styles from "./hotelPin.module.css"
|
||||
import styles from './hotelPin.module.css'
|
||||
|
||||
interface HotelPinProps {
|
||||
isActive: boolean
|
||||
@@ -16,7 +16,7 @@ interface HotelPinProps {
|
||||
hotelAdditionalCurrency?: string
|
||||
}
|
||||
|
||||
export default function HotelPin({
|
||||
export function HotelPin({
|
||||
isActive,
|
||||
hotelPrice,
|
||||
currency,
|
||||
@@ -28,7 +28,7 @@ export default function HotelPin({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.pin} ${isActive ? styles.active : ""}`}
|
||||
className={`${styles.pin} ${isActive ? styles.active : ''}`}
|
||||
data-hotelpin
|
||||
>
|
||||
<span className={styles.pinIcon}>
|
||||
@@ -36,17 +36,16 @@ export default function HotelPin({
|
||||
<MaterialIcon
|
||||
icon="calendar_clock"
|
||||
size={16}
|
||||
color={isActive ? "Icon/Interactive/Default" : "Icon/Inverted"}
|
||||
color={isActive ? 'Icon/Interactive/Default' : 'Icon/Inverted'}
|
||||
/>
|
||||
) : (
|
||||
<HotelMarker width={16} color={isActive ? "burgundy" : "white"} />
|
||||
<HotelMarker width={16} color={isActive ? 'burgundy' : 'white'} />
|
||||
)}
|
||||
</span>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>
|
||||
{isNotAvailable
|
||||
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||
"—"
|
||||
? '—'
|
||||
: formatPrice(
|
||||
intl,
|
||||
hotelPrice,
|
||||
@@ -0,0 +1,122 @@
|
||||
import {
|
||||
AdvancedMarker,
|
||||
AdvancedMarkerAnchorPoint,
|
||||
InfoWindow,
|
||||
} from '@vis.gl/react-google-maps'
|
||||
import { useMediaQuery } from 'usehooks-ts'
|
||||
|
||||
import { HotelPin } from './HotelPin'
|
||||
import type { HotelPin as HotelPinType } from '../../types'
|
||||
import styles from './hotelListingMapContent.module.css'
|
||||
import { StandaloneHotelCardDialog } from '../../../HotelCard/HotelDialogCard/StandaloneHotelCardDialog'
|
||||
import { Lang } from '@scandic-hotels/common/constants/language'
|
||||
|
||||
export type HotelListingMapContentProps = {
|
||||
hotelPins: HotelPinType[]
|
||||
activeHotel?: string | null
|
||||
hoveredHotel?: string | null
|
||||
lang: Lang
|
||||
isUserLoggedIn: boolean
|
||||
onClickHotel?: (hotelId: string) => void
|
||||
setActiveHotel?: (args: { hotelName: string; hotelId: string } | null) => void
|
||||
setHoveredHotel?: (
|
||||
args: { hotelName: string; hotelId: string } | null
|
||||
) => void
|
||||
}
|
||||
export function HotelListingMapContent({
|
||||
hotelPins,
|
||||
activeHotel,
|
||||
hoveredHotel,
|
||||
isUserLoggedIn,
|
||||
setActiveHotel,
|
||||
setHoveredHotel,
|
||||
lang,
|
||||
onClickHotel,
|
||||
}: HotelListingMapContentProps) {
|
||||
const isDesktop = useMediaQuery('(min-width: 768px)')
|
||||
|
||||
const toggleActiveHotelPin = (
|
||||
args: { hotelName: string; hotelId: string } | null
|
||||
) => {
|
||||
if (!args) {
|
||||
setActiveHotel?.(null)
|
||||
return
|
||||
}
|
||||
|
||||
setActiveHotel?.({ hotelName: args.hotelName, hotelId: args.hotelId })
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{hotelPins.map((pin) => {
|
||||
const isActiveOrHovered =
|
||||
activeHotel === pin.name || hoveredHotel === pin.name
|
||||
const hotelPrice =
|
||||
pin.memberPrice ??
|
||||
pin.publicPrice ??
|
||||
pin.redemptionPrice ??
|
||||
pin.voucherPrice ??
|
||||
pin.chequePrice?.numberOfCheques ??
|
||||
null
|
||||
|
||||
const hotelAdditionalPrice = pin.chequePrice
|
||||
? pin.chequePrice.additionalPricePerStay
|
||||
: undefined
|
||||
const hotelAdditionalCurrency = pin.chequePrice
|
||||
? pin.chequePrice.currency?.toString()
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<AdvancedMarker
|
||||
key={pin.name}
|
||||
className={styles.advancedMarker}
|
||||
position={pin.coordinates}
|
||||
anchorPoint={AdvancedMarkerAnchorPoint.CENTER}
|
||||
zIndex={isActiveOrHovered ? 2 : 0}
|
||||
onMouseEnter={() => {
|
||||
setHoveredHotel?.({ hotelName: pin.name, hotelId: pin.operaId })
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoveredHotel?.(null)
|
||||
}}
|
||||
onClick={() =>
|
||||
toggleActiveHotelPin({
|
||||
hotelName: pin.name,
|
||||
hotelId: pin.operaId,
|
||||
})
|
||||
}
|
||||
>
|
||||
{isActiveOrHovered && isDesktop && (
|
||||
<InfoWindow
|
||||
position={pin.coordinates}
|
||||
pixelOffset={[0, -24]}
|
||||
headerDisabled={true}
|
||||
shouldFocus={false}
|
||||
>
|
||||
<StandaloneHotelCardDialog
|
||||
data={pin}
|
||||
lang={lang}
|
||||
isUserLoggedIn={isUserLoggedIn}
|
||||
handleClose={() => {
|
||||
setActiveHotel?.(null)
|
||||
setHoveredHotel?.(null)
|
||||
}}
|
||||
onClick={() => {
|
||||
onClickHotel?.(pin.operaId)
|
||||
}}
|
||||
/>
|
||||
</InfoWindow>
|
||||
)}
|
||||
<HotelPin
|
||||
isActive={isActiveOrHovered}
|
||||
hotelPrice={hotelPrice}
|
||||
currency={pin.currency}
|
||||
hotelAdditionalPrice={hotelAdditionalPrice}
|
||||
hotelAdditionalCurrency={hotelAdditionalCurrency}
|
||||
/>
|
||||
</AdvancedMarker>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
|
||||
// import { expect, fn } from 'storybook/test'
|
||||
import { InteractiveMap } from '.'
|
||||
import { Lang } from '@scandic-hotels/common/constants/language'
|
||||
import { APIProvider } from '@vis.gl/react-google-maps'
|
||||
import { useState } from 'react'
|
||||
|
||||
const meta: Meta<typeof InteractiveMap> = {
|
||||
title: 'Components/Map/Interactive Map',
|
||||
component: InteractiveMap,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof InteractiveMap>
|
||||
|
||||
export const PrimaryDefault: Story = {
|
||||
args: {
|
||||
lang: Lang.en,
|
||||
hotelPins: [
|
||||
{
|
||||
coordinates: {
|
||||
lat: 59.331303,
|
||||
lng: 18.065542,
|
||||
},
|
||||
name: 'Downtown Camper by Scandic',
|
||||
chequePrice: null,
|
||||
publicPrice: 1100,
|
||||
memberPrice: 1067,
|
||||
redemptionPrice: null,
|
||||
voucherPrice: null,
|
||||
rateType: 'Regular',
|
||||
currency: 'SEK',
|
||||
|
||||
amenities: [
|
||||
{
|
||||
filter: 'Hotel facilities',
|
||||
icon: 'Pool',
|
||||
id: 1831,
|
||||
name: 'Pool',
|
||||
public: true,
|
||||
sortOrder: 7000,
|
||||
slug: 'pool',
|
||||
},
|
||||
{
|
||||
filter: 'Hotel facilities',
|
||||
icon: 'Restaurant',
|
||||
id: 1383,
|
||||
name: 'Restaurant',
|
||||
public: true,
|
||||
sortOrder: 6000,
|
||||
slug: 'restaurant',
|
||||
},
|
||||
{
|
||||
filter: 'None',
|
||||
icon: 'KayaksForLoan',
|
||||
id: 162585,
|
||||
name: 'Kayaks for loan',
|
||||
public: true,
|
||||
sortOrder: 5000,
|
||||
slug: 'kayaks-for-loan',
|
||||
},
|
||||
{
|
||||
filter: 'Hotel facilities',
|
||||
icon: 'None',
|
||||
id: 239348,
|
||||
name: 'Rooftop bar',
|
||||
public: false,
|
||||
sortOrder: 4000,
|
||||
slug: 'rooftop-bar',
|
||||
},
|
||||
{
|
||||
filter: 'None',
|
||||
icon: 'BikesForLoan',
|
||||
id: 5550,
|
||||
name: 'Bikes for loan',
|
||||
public: true,
|
||||
sortOrder: 3000,
|
||||
slug: 'bikes-for-loan',
|
||||
},
|
||||
],
|
||||
ratings: {
|
||||
tripAdvisor: 4.4,
|
||||
},
|
||||
operaId: '879',
|
||||
facilityIds: [
|
||||
1831, 1383, 162585, 239348, 5550, 162586, 5806, 1014, 1835, 1829,
|
||||
1379, 1382, 162587, 1017, 1378, 1408, 1833, 971, 1834, 162584, 1381,
|
||||
229144, 267806,
|
||||
],
|
||||
hasEnoughPoints: false,
|
||||
image: {
|
||||
alt: 'Bar of Downtown Camper by Scandic in Stockholm',
|
||||
url: 'https://images-test.scandichotels.com/publishedmedia/z68596isempb61xm2ns9/Scandic_Downtown_Camper_spa_wellness_the_nest_swim.jpg',
|
||||
},
|
||||
},
|
||||
{
|
||||
coordinates: {
|
||||
lat: 59.33469,
|
||||
lng: 18.061586,
|
||||
},
|
||||
name: 'Haymarket by Scandic',
|
||||
chequePrice: null,
|
||||
publicPrice: null,
|
||||
memberPrice: 9999,
|
||||
redemptionPrice: null,
|
||||
voucherPrice: null,
|
||||
rateType: 'Regular',
|
||||
currency: 'SEK',
|
||||
|
||||
amenities: [
|
||||
{
|
||||
filter: 'Hotel facilities',
|
||||
icon: 'Restaurant',
|
||||
id: 1383,
|
||||
name: 'Restaurant',
|
||||
public: true,
|
||||
sortOrder: 6000,
|
||||
slug: 'restaurant',
|
||||
},
|
||||
{
|
||||
filter: 'None',
|
||||
icon: 'None',
|
||||
id: 5806,
|
||||
name: 'Meeting / conference facilities',
|
||||
public: true,
|
||||
sortOrder: 1500,
|
||||
slug: 'meeting-conference-facilities',
|
||||
},
|
||||
{
|
||||
filter: 'Hotel facilities',
|
||||
icon: 'Bar',
|
||||
id: 1014,
|
||||
name: 'Bar',
|
||||
public: true,
|
||||
sortOrder: 1401,
|
||||
slug: 'bar',
|
||||
},
|
||||
{
|
||||
filter: 'Hotel facilities',
|
||||
icon: 'PetFriendlyRooms',
|
||||
id: 1835,
|
||||
name: 'Pet-friendly rooms',
|
||||
public: true,
|
||||
sortOrder: 1201,
|
||||
slug: 'pet-friendly-rooms',
|
||||
},
|
||||
{
|
||||
filter: 'Hotel facilities',
|
||||
icon: 'Gym',
|
||||
id: 1829,
|
||||
name: 'Gym',
|
||||
public: true,
|
||||
sortOrder: 1101,
|
||||
slug: 'gym',
|
||||
},
|
||||
],
|
||||
ratings: {
|
||||
tripAdvisor: 4.1,
|
||||
},
|
||||
operaId: '890',
|
||||
facilityIds: [
|
||||
1383, 5806, 1014, 1835, 1829, 1382, 162587, 1017, 1833, 971, 1834,
|
||||
1381, 1406, 1913, 345180, 375885,
|
||||
],
|
||||
hasEnoughPoints: false,
|
||||
image: {
|
||||
alt: 'Bar',
|
||||
url: 'https://images-test.scandichotels.com/publishedmedia/6wobp0j1ocvoopy1dmce/haymarket-by-scandic-bar-pauls_-3-.jpg',
|
||||
},
|
||||
},
|
||||
],
|
||||
isUserLoggedIn: false,
|
||||
coordinates: {
|
||||
lat: 59.32644916839965,
|
||||
lng: 18.067759400301135,
|
||||
},
|
||||
},
|
||||
render: (args) => {
|
||||
const mapKey = import.meta.env.VITE_GOOGLE_STATIC_MAP_KEY
|
||||
const mapId = import.meta.env.VITE_GOOGLE_DYNAMIC_MAP_ID
|
||||
if (!mapKey || !mapId) {
|
||||
throw new Error(
|
||||
'VITE_GOOGLE_STATIC_MAP_KEY or VITE_GOOGLE_DYNAMIC_MAP_ID is not defined in your .env file. Please add it to run this story.'
|
||||
)
|
||||
}
|
||||
|
||||
const [hoveredHotelPin, setHoveredHotelPin] = useState<string | null>()
|
||||
const [activeHotelPin, setActiveHotelPin] = useState<string | null>()
|
||||
|
||||
return (
|
||||
<APIProvider apiKey={mapKey}>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
'--hotel-map-height': '300px',
|
||||
height: 'max(500px, 90vh)',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<InteractiveMap
|
||||
{...args}
|
||||
mapId={mapId}
|
||||
hoveredHotelPin={hoveredHotelPin}
|
||||
onHoverHotelPin={(args) => {
|
||||
setHoveredHotelPin(args?.hotelName ?? null)
|
||||
}}
|
||||
activeHotelPin={activeHotelPin}
|
||||
onSetActiveHotelPin={(args) => {
|
||||
setActiveHotelPin(args?.hotelName ?? null)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</APIProvider>
|
||||
)
|
||||
},
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
import {
|
||||
AdvancedMarker,
|
||||
AdvancedMarkerAnchorPoint,
|
||||
} from "@vis.gl/react-google-maps"
|
||||
import { useIntl } from "react-intl"
|
||||
} from '@vis.gl/react-google-maps'
|
||||
import { useIntl } from 'react-intl'
|
||||
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { Typography } from '@scandic-hotels/design-system/Typography'
|
||||
|
||||
import HotelMarkerByType from "../../Markers"
|
||||
import PoiMarker from "../../Markers/Poi"
|
||||
import { HotelMarkerByType } from '../../Markers/HotelMarkerByType'
|
||||
import { PoiMarker } from '../../Markers/PoiMarker'
|
||||
|
||||
import styles from "./poiMapMarkers.module.css"
|
||||
import styles from './poiMapMarkers.module.css'
|
||||
|
||||
import type { PointOfInterest } from "@scandic-hotels/trpc/types/hotel"
|
||||
import type { MarkerInfo } from "@scandic-hotels/trpc/types/marker"
|
||||
import type { PointOfInterest } from '@scandic-hotels/trpc/types/hotel'
|
||||
import type { MarkerInfo } from '@scandic-hotels/trpc/types/marker'
|
||||
|
||||
export type PoiMapMarkersProps = {
|
||||
activePoi?: string | null
|
||||
@@ -52,15 +52,15 @@ export default function PoiMapMarkers({
|
||||
zIndex={activePoi === poi.name ? 2 : 0}
|
||||
onMouseEnter={() => onActivePoiChange?.(poi.name ?? null)}
|
||||
onMouseLeave={() => onActivePoiChange?.(null)}
|
||||
onClick={() => toggleActivePoi(poi.name ?? "")}
|
||||
onClick={() => toggleActivePoi(poi.name ?? '')}
|
||||
>
|
||||
<span
|
||||
className={`${styles.poi} ${activePoi === poi.name ? styles.active : ""}`}
|
||||
className={`${styles.poi} ${activePoi === poi.name ? styles.active : ''}`}
|
||||
>
|
||||
<PoiMarker
|
||||
group={poi.group}
|
||||
categoryName={poi.categoryName}
|
||||
size={activePoi === poi.name ? "large" : "small"}
|
||||
size={activePoi === poi.name ? 'large' : 'small'}
|
||||
/>
|
||||
<span className={styles.poiLabel}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
@@ -73,7 +73,7 @@ export default function PoiMapMarkers({
|
||||
<span>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "{distanceInKm} km",
|
||||
defaultMessage: '{distanceInKm} km',
|
||||
},
|
||||
{
|
||||
distanceInKm: poi.distance,
|
||||
@@ -0,0 +1,176 @@
|
||||
'use client'
|
||||
|
||||
import { Map, type MapProps, useMap } from '@vis.gl/react-google-maps'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useIntl } from 'react-intl'
|
||||
|
||||
import { IconButton } from '@scandic-hotels/design-system/IconButton'
|
||||
import { MaterialIcon } from '@scandic-hotels/design-system/Icons/MaterialIcon'
|
||||
|
||||
import {
|
||||
DEFAULT_ZOOM,
|
||||
MAP_RESTRICTIONS,
|
||||
MAX_ZOOM,
|
||||
MIN_ZOOM,
|
||||
} from '../mapConstants'
|
||||
|
||||
import { useZoomControls } from './useZoomControls'
|
||||
|
||||
import { HotelListingMapContent } from './HotelListingMapContent'
|
||||
import PoiMapMarkers from './PoiMapMarkers'
|
||||
|
||||
import styles from './interactiveMap.module.css'
|
||||
|
||||
import type { PointOfInterest } from '@scandic-hotels/trpc/types/hotel'
|
||||
import type { MarkerInfo } from '@scandic-hotels/trpc/types/marker'
|
||||
import { HotelPin } from '../types'
|
||||
import { Lang } from '@scandic-hotels/common/constants/language'
|
||||
|
||||
export type InteractiveMapProps = {
|
||||
lang: Lang
|
||||
coordinates: {
|
||||
lat: number
|
||||
lng: number
|
||||
}
|
||||
activePoi?: PointOfInterest['name'] | null
|
||||
hotelPins?: HotelPin[]
|
||||
pointsOfInterest?: PointOfInterest[]
|
||||
markerInfo?: MarkerInfo
|
||||
mapId: string
|
||||
closeButton: React.ReactNode
|
||||
fitBounds?: boolean
|
||||
hoveredHotelPin?: string | null
|
||||
activeHotelPin?: string | null
|
||||
|
||||
isUserLoggedIn: boolean
|
||||
onTilesLoaded?: () => void
|
||||
onActivePoiChange?: (poi: PointOfInterest['name'] | null) => void
|
||||
|
||||
onClickHotel?: (hotelId: string) => void
|
||||
|
||||
/**
|
||||
* Called when a hotel pin is hovered.
|
||||
* @param args when null, it means the hover has ended
|
||||
* @returns
|
||||
*/
|
||||
onHoverHotelPin?: (
|
||||
args: { hotelName: string; hotelId: string } | null
|
||||
) => void
|
||||
|
||||
/**
|
||||
* Called when a hotel pin is activated.
|
||||
* @param args when null, it means nothing is active
|
||||
* @returns
|
||||
*/
|
||||
onSetActiveHotelPin?: (
|
||||
args: { hotelName: string; hotelId: string } | null
|
||||
) => void
|
||||
}
|
||||
|
||||
export function InteractiveMap({
|
||||
lang,
|
||||
coordinates,
|
||||
pointsOfInterest,
|
||||
activePoi,
|
||||
hotelPins,
|
||||
mapId,
|
||||
closeButton,
|
||||
markerInfo,
|
||||
fitBounds = true,
|
||||
hoveredHotelPin,
|
||||
activeHotelPin,
|
||||
isUserLoggedIn,
|
||||
onClickHotel,
|
||||
onHoverHotelPin,
|
||||
onSetActiveHotelPin,
|
||||
onTilesLoaded,
|
||||
onActivePoiChange,
|
||||
}: InteractiveMapProps) {
|
||||
const intl = useIntl()
|
||||
const map = useMap()
|
||||
const [hasInitializedBounds, setHasInitializedBounds] = useState(false)
|
||||
const { zoomIn, zoomOut, isMaxZoom, isMinZoom } = useZoomControls()
|
||||
|
||||
const mapOptions: MapProps = {
|
||||
defaultZoom: DEFAULT_ZOOM,
|
||||
minZoom: MIN_ZOOM,
|
||||
maxZoom: MAX_ZOOM,
|
||||
defaultCenter: coordinates,
|
||||
disableDefaultUI: true,
|
||||
clickableIcons: false,
|
||||
mapId,
|
||||
gestureHandling: 'greedy',
|
||||
restriction: MAP_RESTRICTIONS,
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (map && hotelPins?.length && !hasInitializedBounds) {
|
||||
if (fitBounds) {
|
||||
const bounds = new google.maps.LatLngBounds()
|
||||
hotelPins.forEach((marker) => {
|
||||
bounds.extend(marker.coordinates)
|
||||
})
|
||||
map.fitBounds(bounds, 100)
|
||||
}
|
||||
setHasInitializedBounds(true)
|
||||
}
|
||||
}, [map, fitBounds, hotelPins, hasInitializedBounds])
|
||||
|
||||
return (
|
||||
<div className={styles.mapContainer}>
|
||||
<Map {...mapOptions} onTilesLoaded={onTilesLoaded}>
|
||||
{hotelPins && (
|
||||
<HotelListingMapContent
|
||||
lang={lang}
|
||||
isUserLoggedIn={isUserLoggedIn}
|
||||
hotelPins={hotelPins}
|
||||
setActiveHotel={onSetActiveHotelPin}
|
||||
setHoveredHotel={onHoverHotelPin}
|
||||
activeHotel={activeHotelPin}
|
||||
hoveredHotel={hoveredHotelPin}
|
||||
onClickHotel={onClickHotel}
|
||||
/>
|
||||
)}
|
||||
{pointsOfInterest && markerInfo && (
|
||||
<PoiMapMarkers
|
||||
coordinates={coordinates}
|
||||
pointsOfInterest={pointsOfInterest}
|
||||
onActivePoiChange={onActivePoiChange}
|
||||
activePoi={activePoi}
|
||||
markerInfo={markerInfo}
|
||||
/>
|
||||
)}
|
||||
</Map>
|
||||
<div className={styles.ctaButtons}>
|
||||
{closeButton}
|
||||
<div className={styles.zoomButtons}>
|
||||
<IconButton
|
||||
theme="Inverted"
|
||||
style="Elevated"
|
||||
className={styles.zoomButton}
|
||||
onClick={zoomOut}
|
||||
aria-label={intl.formatMessage({
|
||||
defaultMessage: 'Zoom out',
|
||||
})}
|
||||
isDisabled={isMinZoom}
|
||||
>
|
||||
<MaterialIcon icon="remove" color="CurrentColor" />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
theme="Inverted"
|
||||
style="Elevated"
|
||||
className={styles.zoomButton}
|
||||
onClick={zoomIn}
|
||||
aria-label={intl.formatMessage({
|
||||
defaultMessage: 'Zoom in',
|
||||
})}
|
||||
isDisabled={isMaxZoom}
|
||||
>
|
||||
<MaterialIcon icon="add" color="CurrentColor" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
|
||||
.mapContainer::after {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user