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/fonts.css"
|
||||||
import "@scandic-hotels/design-system/style.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 "../../globals.css"
|
||||||
|
|
||||||
import { BookingFlowTrackingProvider } from "@scandic-hotels/booking-flow/BookingFlowTrackingProvider"
|
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 { AuthError } from "next-auth"
|
||||||
|
|
||||||
import { logger } from "@scandic-hotels/common/logger"
|
import { logger } from "@scandic-hotels/common/logger"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import "@scandic-hotels/design-system/fonts.css"
|
import "@scandic-hotels/design-system/fonts.css"
|
||||||
import "@/app/globals.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 "@scandic-hotels/design-system/style.css"
|
||||||
|
|
||||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import "@scandic-hotels/design-system/fonts.css"
|
import "@scandic-hotels/design-system/fonts.css"
|
||||||
import "@/app/globals.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 "@scandic-hotels/design-system/style.css"
|
||||||
|
|
||||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import "@scandic-hotels/design-system/fonts.css"
|
import "@scandic-hotels/design-system/fonts.css"
|
||||||
import "@/app/globals.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 "@scandic-hotels/design-system/style.css"
|
||||||
|
|
||||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import "@scandic-hotels/design-system/fonts.css"
|
import "@scandic-hotels/design-system/fonts.css"
|
||||||
import "@/app/globals.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/style.css"
|
||||||
|
import "@scandic-hotels/design-system/design-system-new-deprecated.css"
|
||||||
|
|
||||||
import Script from "next/script"
|
import Script from "next/script"
|
||||||
import { NuqsAdapter } from "nuqs/adapters/next/app"
|
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 { getSingleDecimal } from "@scandic-hotels/common/utils/numberFormatting"
|
||||||
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
||||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
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 HotelLogoIcon from "@scandic-hotels/design-system/Icons/HotelLogoIcon"
|
||||||
import TripadvisorIcon from "@scandic-hotels/design-system/Icons/TripadvisorIcon"
|
import TripadvisorIcon from "@scandic-hotels/design-system/Icons/TripadvisorIcon"
|
||||||
import ImageGallery from "@scandic-hotels/design-system/ImageGallery"
|
import ImageGallery from "@scandic-hotels/design-system/ImageGallery"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
|
||||||
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||||
|
|
||||||
import styles from "./hotelListingItem.module.css"
|
import styles from "./hotelListingItem.module.css"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useIntl } from "react-intl"
|
|||||||
import { getSingleDecimal } from "@scandic-hotels/common/utils/numberFormatting"
|
import { getSingleDecimal } from "@scandic-hotels/common/utils/numberFormatting"
|
||||||
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
||||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
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 HotelLogoIcon from "@scandic-hotels/design-system/Icons/HotelLogoIcon"
|
||||||
import TripadvisorIcon from "@scandic-hotels/design-system/Icons/TripadvisorIcon"
|
import TripadvisorIcon from "@scandic-hotels/design-system/Icons/TripadvisorIcon"
|
||||||
import ImageGallery from "@scandic-hotels/design-system/ImageGallery"
|
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 { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
|
||||||
|
|
||||||
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
|
||||||
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||||
|
|
||||||
import styles from "./hotelListItem.module.css"
|
import styles from "./hotelListItem.module.css"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useIntl } from "react-intl"
|
|||||||
import { getSingleDecimal } from "@scandic-hotels/common/utils/numberFormatting"
|
import { getSingleDecimal } from "@scandic-hotels/common/utils/numberFormatting"
|
||||||
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
||||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
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 HotelLogoIcon from "@scandic-hotels/design-system/Icons/HotelLogoIcon"
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
import TripadvisorIcon from "@scandic-hotels/design-system/Icons/TripadvisorIcon"
|
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 { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
|
||||||
|
|
||||||
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
|
||||||
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||||
|
|
||||||
import styles from "./hotelListingItem.module.css"
|
import styles from "./hotelListingItem.module.css"
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import { useState } from "react"
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
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 { IconButton } from "@scandic-hotels/design-system/IconButton"
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
|
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
|
||||||
|
|
||||||
import { FacilityToIcon } from "../../HotelPage/data"
|
|
||||||
import { usePageType } from "../Map/PageTypeProvider"
|
import { usePageType } from "../Map/PageTypeProvider"
|
||||||
import DialogImage from "./DialogImage"
|
import DialogImage from "./DialogImage"
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import { type PropsWithChildren, useEffect, useRef, useState } from "react"
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
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 { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
|
||||||
|
|
||||||
import { MAP_RESTRICTIONS } from "@/constants/map"
|
|
||||||
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
|
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
|
||||||
|
|
||||||
import { ErrorBoundary } from "@/components/ErrorBoundary/ErrorBoundary"
|
import { ErrorBoundary } from "@/components/ErrorBoundary/ErrorBoundary"
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import {
|
|||||||
} from "@vis.gl/react-google-maps"
|
} from "@vis.gl/react-google-maps"
|
||||||
import { useMediaQuery } from "usehooks-ts"
|
import { useMediaQuery } from "usehooks-ts"
|
||||||
|
|
||||||
|
import { HotelMarkerByType } from "@scandic-hotels/design-system/Map/Markers/HotelMarkerByType"
|
||||||
|
|
||||||
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
|
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
|
||||||
|
|
||||||
import HotelMarkerByType from "@/components/Maps/Markers"
|
|
||||||
import { trackMapClick } from "@/utils/tracking/destinationPage"
|
import { trackMapClick } from "@/utils/tracking/destinationPage"
|
||||||
|
|
||||||
import HotelMapCard from "../../../HotelMapCard"
|
import HotelMapCard from "../../../HotelMapCard"
|
||||||
|
|||||||
@@ -13,8 +13,12 @@ import { useIntl } from "react-intl"
|
|||||||
import { debounce } from "@scandic-hotels/common/utils/debounce"
|
import { debounce } from "@scandic-hotels/common/utils/debounce"
|
||||||
import { Button } from "@scandic-hotels/design-system/Button"
|
import { Button } from "@scandic-hotels/design-system/Button"
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
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"
|
import Sidebar from "./Sidebar"
|
||||||
|
|
||||||
@@ -43,11 +47,15 @@ export default function HotelMapPageClient({
|
|||||||
mapId,
|
mapId,
|
||||||
}: HotelMapPageClientProps) {
|
}: HotelMapPageClientProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const lang = useLang()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const rootDiv = useRef<HTMLDivElement | null>(null)
|
const rootDiv = useRef<HTMLDivElement | null>(null)
|
||||||
const [mapHeight, setMapHeight] = useState("100dvh")
|
const [mapHeight, setMapHeight] = useState("100dvh")
|
||||||
const [activePoi, setActivePoi] = useState<string | null>(null)
|
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)
|
// 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 handleMapHeight = useCallback(() => {
|
||||||
const topPosition = rootDiv.current?.getBoundingClientRect().top ?? 0
|
const topPosition = rootDiv.current?.getBoundingClientRect().top ?? 0
|
||||||
@@ -132,6 +140,15 @@ export default function HotelMapPageClient({
|
|||||||
onActivePoiChange={(poi) => setActivePoi(poi ?? null)}
|
onActivePoiChange={(poi) => setActivePoi(poi ?? null)}
|
||||||
mapId={mapId}
|
mapId={mapId}
|
||||||
markerInfo={markerInfo}
|
markerInfo={markerInfo}
|
||||||
|
onHoverHotelPin={(args) => {
|
||||||
|
if (!args) {
|
||||||
|
hotelMapStore.disengage()
|
||||||
|
}
|
||||||
|
hotelMapStore.engage(hotelName)
|
||||||
|
}}
|
||||||
|
hoveredHotelPin={hotelMapStore.hoveredHotel}
|
||||||
|
lang={lang}
|
||||||
|
isUserLoggedIn={isUserLoggedIn}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</APIProvider>
|
</APIProvider>
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ import { useState } from "react"
|
|||||||
import { Button as ButtonRAC } from "react-aria-components"
|
import { Button as ButtonRAC } from "react-aria-components"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { PoiMarker } from "@scandic-hotels/design-system/Map/Markers/PoiMarker"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import PoiMarker from "@/components/Maps/Markers/Poi"
|
|
||||||
|
|
||||||
import { translatePOIGroup } from "./util"
|
import { translatePOIGroup } from "./util"
|
||||||
|
|
||||||
import styles from "./sidebar.module.css"
|
import styles from "./sidebar.module.css"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
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 { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
|
|
||||||
import styles from "./amenitiesList.module.css"
|
import styles from "./amenitiesList.module.css"
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { useEffect, useState } from "react"
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
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 { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import PoiMarker from "@/components/Maps/Markers/Poi"
|
|
||||||
import { trackHotelMapClick } from "@/utils/tracking"
|
import { trackHotelMapClick } from "@/utils/tracking"
|
||||||
|
|
||||||
import styles from "./mapCard.module.css"
|
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 { env } from "@/env/server"
|
||||||
|
|
||||||
import HotelMarkerByType from "@/components/Maps/Markers"
|
|
||||||
import StaticMapComp from "@/components/Maps/StaticMap"
|
import StaticMapComp from "@/components/Maps/StaticMap"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import { calculateLatWithOffset } from "@/utils/map"
|
import { calculateLatWithOffset } from "@/utils/map"
|
||||||
|
|||||||
@@ -3,14 +3,13 @@
|
|||||||
import { cx } from "class-variance-authority"
|
import { cx } from "class-variance-authority"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { BookingCodeChip } from "@scandic-hotels/design-system/BookingCodeChip"
|
||||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||||
import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer"
|
import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
|
import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
|
||||||
|
|
||||||
import BookingCodeChip from "@/components/BookingCodeChip"
|
|
||||||
|
|
||||||
import PriceDetails from "../../PriceDetails"
|
import PriceDetails from "../../PriceDetails"
|
||||||
|
|
||||||
import styles from "./totalPrice.module.css"
|
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 { dt } from "@scandic-hotels/common/dt"
|
||||||
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
|
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
|
||||||
import Body from "@scandic-hotels/design-system/Body"
|
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 { Divider } from "@scandic-hotels/design-system/Divider"
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
import Subtitle from "@scandic-hotels/design-system/Subtitle"
|
import Subtitle from "@scandic-hotels/design-system/Subtitle"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import BookingCodeChip from "@/components/BookingCodeChip"
|
|
||||||
import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal"
|
import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal"
|
||||||
import { isBookingCodeRate } from "@/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils"
|
import { isBookingCodeRate } from "@/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils"
|
||||||
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
|
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"
|
"use client"
|
||||||
|
|
||||||
import { useSession } from "next-auth/react"
|
import { useSession } from "next-auth/react"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { selectRate } from "@scandic-hotels/common/constants/routes/hotelReservation"
|
import { selectRate } from "@scandic-hotels/common/constants/routes/hotelReservation"
|
||||||
import Caption from "@scandic-hotels/design-system/Caption"
|
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 { IconButton } from "@scandic-hotels/design-system/IconButton"
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
import Link from "@scandic-hotels/design-system/Link"
|
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 Subtitle from "@scandic-hotels/design-system/Subtitle"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
import { isValidClientSession } from "@/utils/clientSession"
|
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 styles from "./listingHotelCardDialog.module.css"
|
||||||
|
|
||||||
import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map"
|
import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map"
|
||||||
@@ -81,7 +81,7 @@ export default function ListingHotelCardDialog({
|
|||||||
<HotelCardDialogImage
|
<HotelCardDialogImage
|
||||||
firstImage={firstImage}
|
firstImage={firstImage}
|
||||||
altText={altText}
|
altText={altText}
|
||||||
rating={ratings}
|
rating={{ tripAdvisor: ratings }}
|
||||||
imageError={imageError}
|
imageError={imageError}
|
||||||
setImageError={setImageError}
|
setImageError={setImageError}
|
||||||
position="top"
|
position="top"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useSearchParams } from "next/navigation"
|
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation"
|
||||||
import { useSession } from "next-auth/react"
|
import { useSession } from "next-auth/react"
|
||||||
import { useEffect, useMemo, useRef } from "react"
|
import { useEffect, useMemo, useRef } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
@@ -8,20 +9,29 @@ import {
|
|||||||
BookingCodeFilterEnum,
|
BookingCodeFilterEnum,
|
||||||
useBookingCodeFilterStore,
|
useBookingCodeFilterStore,
|
||||||
} from "@scandic-hotels/booking-flow/stores/bookingCode-filter"
|
} 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 { BackToTopButton } from "@scandic-hotels/design-system/BackToTopButton"
|
||||||
|
import { HotelCard } from "@scandic-hotels/design-system/HotelCard"
|
||||||
|
|
||||||
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
||||||
import { useHotelsMapStore } from "@/stores/hotels-map"
|
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||||
|
|
||||||
|
import HotelDetailsSidePeek from "@/components/SidePeeks/HotelDetailsSidePeek"
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
import { useScrollToTop } from "@/hooks/useScrollToTop"
|
import { useScrollToTop } from "@/hooks/useScrollToTop"
|
||||||
import { isValidClientSession } from "@/utils/clientSession"
|
import { isValidClientSession } from "@/utils/clientSession"
|
||||||
|
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||||
|
|
||||||
import HotelCard from "../HotelCard"
|
|
||||||
import { DEFAULT_SORT } from "../SelectHotel/HotelSorter"
|
import { DEFAULT_SORT } from "../SelectHotel/HotelSorter"
|
||||||
import { getSortedHotels } from "./utils"
|
import { getSortedHotels } from "./utils"
|
||||||
|
|
||||||
import styles from "./hotelCardListing.module.css"
|
import styles from "./hotelCardListing.module.css"
|
||||||
|
|
||||||
|
import type { HotelType } from "@scandic-hotels/common/constants/hotelType"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type HotelCardListingProps,
|
type HotelCardListingProps,
|
||||||
HotelCardListingTypeEnum,
|
HotelCardListingTypeEnum,
|
||||||
@@ -33,13 +43,15 @@ export default function HotelCardListing({
|
|||||||
type = HotelCardListingTypeEnum.PageListing,
|
type = HotelCardListingTypeEnum.PageListing,
|
||||||
isAlternative,
|
isAlternative,
|
||||||
}: HotelCardListingProps) {
|
}: HotelCardListingProps) {
|
||||||
|
const router = useRouter()
|
||||||
|
const lang = useLang()
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
const isUserLoggedIn = isValidClientSession(session)
|
const isUserLoggedIn = isValidClientSession(session)
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
||||||
const setResultCount = useHotelFilterStore((state) => state.setResultCount)
|
const setResultCount = useHotelFilterStore((state) => state.setResultCount)
|
||||||
const { activeHotel } = useHotelsMapStore()
|
const { activeHotel, activate, disengage, engage } = useHotelsMapStore()
|
||||||
const { showBackToTop, scrollToTop } = useScrollToTop({ threshold: 490 })
|
const { showBackToTop, scrollToTop } = useScrollToTop({ threshold: 490 })
|
||||||
const activeCardRef = useRef<HTMLDivElement | null>(null)
|
const activeCardRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
@@ -118,32 +130,79 @@ export default function HotelCardListing({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.hotelCards}>
|
<section className={styles.hotelCards}>
|
||||||
{hotels?.length
|
{hotels.map((hotel) => (
|
||||||
? hotels.map((hotel) => (
|
|
||||||
<div
|
<div
|
||||||
key={hotel.hotel.operaId}
|
key={hotel.hotel.operaId}
|
||||||
ref={
|
ref={isHotelActiveInMapView(hotel.hotel.name) ? activeCardRef : null}
|
||||||
isHotelActiveInMapView(hotel.hotel.name) ? activeCardRef : null
|
|
||||||
}
|
|
||||||
data-active={
|
data-active={
|
||||||
isHotelActiveInMapView(hotel.hotel.name) ? "true" : "false"
|
isHotelActiveInMapView(hotel.hotel.name) ? "true" : "false"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<HotelCard
|
<HotelCard
|
||||||
hotelData={hotel}
|
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"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
distanceToCityCenter={hotel.hotel.location.distanceToCentre}
|
||||||
|
images={mapApiImagesToGalleryImages(hotel.hotel.galleryImages)}
|
||||||
isUserLoggedIn={isUserLoggedIn}
|
isUserLoggedIn={isUserLoggedIn}
|
||||||
state={
|
state={
|
||||||
isHotelActiveInMapView(hotel.hotel.name)
|
isHotelActiveInMapView(hotel.hotel.name) ? "active" : "default"
|
||||||
? "active"
|
|
||||||
: "default"
|
|
||||||
}
|
}
|
||||||
type={type}
|
type={type}
|
||||||
bookingCode={bookingCode}
|
bookingCode={bookingCode}
|
||||||
isAlternative={isAlternative}
|
isAlternative={isAlternative}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))
|
))}
|
||||||
: null}
|
|
||||||
{showBackToTop && (
|
{showBackToTop && (
|
||||||
<BackToTopButton
|
<BackToTopButton
|
||||||
position="right"
|
position="right"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import BookingCodeChip from "@/components/BookingCodeChip"
|
import { BookingCodeChip } from "@scandic-hotels/design-system/BookingCodeChip"
|
||||||
|
|
||||||
import styles from "./row.module.css"
|
import styles from "./row.module.css"
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useMap } from "@vis.gl/react-google-maps"
|
import { useMap } from "@vis.gl/react-google-maps"
|
||||||
import { useCallback, useMemo, useRef, useState } from "react"
|
import { useCallback, useMemo, useRef, useState } from "react"
|
||||||
import { useIntl } from "react-intl"
|
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 { Button } from "@scandic-hotels/design-system/Button"
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
import Link from "@scandic-hotels/design-system/Link"
|
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 { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
||||||
import { useHotelsMapStore } from "@/stores/hotels-map"
|
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||||
|
|
||||||
import { RoomCardSkeleton } from "@/components/HotelReservation/RoomCardSkeleton/RoomCardSkeleton"
|
import { RoomCardSkeleton } from "@/components/HotelReservation/RoomCardSkeleton/RoomCardSkeleton"
|
||||||
import InteractiveMap from "@/components/Maps/InteractiveMap"
|
import { useIsUserLoggedIn } from "@/hooks/useIsUserLoggedIn"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
import { useScrollToTop } from "@/hooks/useScrollToTop"
|
import { useScrollToTop } from "@/hooks/useScrollToTop"
|
||||||
|
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||||
|
import { trackEvent } from "@/utils/tracking/base"
|
||||||
|
|
||||||
import FilterAndSortModal from "../../Filters/FilterAndSortModal"
|
import FilterAndSortModal from "../../Filters/FilterAndSortModal"
|
||||||
import HotelListing from "../HotelListing"
|
import HotelListing from "../HotelListing"
|
||||||
@@ -39,7 +43,7 @@ import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/he
|
|||||||
|
|
||||||
const SKELETON_LOAD_DELAY = 750
|
const SKELETON_LOAD_DELAY = 750
|
||||||
|
|
||||||
export default function SelectHotelContent({
|
export function SelectHotelMapContent({
|
||||||
hotelPins,
|
hotelPins,
|
||||||
cityCoordinates,
|
cityCoordinates,
|
||||||
mapId,
|
mapId,
|
||||||
@@ -52,6 +56,7 @@ export default function SelectHotelContent({
|
|||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const map = useMap()
|
const map = useMap()
|
||||||
|
const isUserLoggedIn = useIsUserLoggedIn()
|
||||||
|
|
||||||
const isAboveMobile = useMediaQuery("(min-width: 900px)")
|
const isAboveMobile = useMediaQuery("(min-width: 900px)")
|
||||||
const [visibleHotels, setVisibleHotels] = useState<HotelResponse[]>([])
|
const [visibleHotels, setVisibleHotels] = useState<HotelResponse[]>([])
|
||||||
@@ -59,7 +64,7 @@ export default function SelectHotelContent({
|
|||||||
const listingContainerRef = useRef<HTMLDivElement | null>(null)
|
const listingContainerRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
||||||
const { activeHotel } = useHotelsMapStore()
|
const hotelMapStore = useHotelsMapStore()
|
||||||
|
|
||||||
const { showBackToTop, scrollToTop } = useScrollToTop({
|
const { showBackToTop, scrollToTop } = useScrollToTop({
|
||||||
threshold: 490,
|
threshold: 490,
|
||||||
@@ -71,8 +76,10 @@ export default function SelectHotelContent({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const coordinates = useMemo(() => {
|
const coordinates = useMemo(() => {
|
||||||
if (activeHotel) {
|
if (hotelMapStore.activeHotel) {
|
||||||
const hotel = hotels.find((hotel) => hotel.hotel.name === activeHotel)
|
const hotel = hotels.find(
|
||||||
|
(hotel) => hotel.hotel.name === hotelMapStore.activeHotel
|
||||||
|
)
|
||||||
|
|
||||||
if (hotel && hotel.hotel.location) {
|
if (hotel && hotel.hotel.location) {
|
||||||
return isAboveMobile
|
return isAboveMobile
|
||||||
@@ -89,7 +96,7 @@ export default function SelectHotelContent({
|
|||||||
return isAboveMobile
|
return isAboveMobile
|
||||||
? cityCoordinates
|
? cityCoordinates
|
||||||
: { ...cityCoordinates, lat: cityCoordinates.lat - 0.006 }
|
: { ...cityCoordinates, lat: cityCoordinates.lat - 0.006 }
|
||||||
}, [activeHotel, hotels, isAboveMobile, cityCoordinates])
|
}, [hotelMapStore.activeHotel, hotels, isAboveMobile, cityCoordinates])
|
||||||
|
|
||||||
const showOnlyBookingCodeRates =
|
const showOnlyBookingCodeRates =
|
||||||
bookingCode &&
|
bookingCode &&
|
||||||
@@ -231,10 +238,62 @@ export default function SelectHotelContent({
|
|||||||
<InteractiveMap
|
<InteractiveMap
|
||||||
closeButton={closeButton}
|
closeButton={closeButton}
|
||||||
coordinates={coordinates}
|
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}
|
mapId={mapId}
|
||||||
onTilesLoaded={debouncedUpdateHotelCards}
|
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>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { APIProvider } from "@vis.gl/react-google-maps"
|
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"
|
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 SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer"
|
||||||
|
|
||||||
import { HotelCardSkeleton } from "../HotelCard/HotelCardSkeleton"
|
|
||||||
|
|
||||||
import styles from "./selectHotel.module.css"
|
import styles from "./selectHotel.module.css"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { useState } from "react"
|
|||||||
import { Button as ButtonRAC } from "react-aria-components"
|
import { Button as ButtonRAC } from "react-aria-components"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { FacilityToIcon } from "@scandic-hotels/design-system/FacilityToIcon"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
|
||||||
import HotelDetailsSidePeek from "@/components/SidePeeks/HotelDetailsSidePeek"
|
import HotelDetailsSidePeek from "@/components/SidePeeks/HotelDetailsSidePeek"
|
||||||
import Alert from "@/components/TempDesignSystem/Alert"
|
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 { dt } from "@scandic-hotels/common/dt"
|
||||||
import { getSingleDecimal } from "@scandic-hotels/common/utils/numberFormatting"
|
import { getSingleDecimal } from "@scandic-hotels/common/utils/numberFormatting"
|
||||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
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 ImageGallery from "@scandic-hotels/design-system/ImageGallery"
|
||||||
import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer"
|
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 { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
|
||||||
import HotelDetailsSidePeek from "@/components/SidePeeks/HotelDetailsSidePeek"
|
import HotelDetailsSidePeek from "@/components/SidePeeks/HotelDetailsSidePeek"
|
||||||
import Alert from "@/components/TempDesignSystem/Alert"
|
import Alert from "@/components/TempDesignSystem/Alert"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
|
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"
|
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 { IconButton } from "@scandic-hotels/design-system/IconButton"
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
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 { useSelectRateContext } from "@/contexts/SelectRate/SelectRateContext"
|
||||||
import { useBreakpoint } from "@/hooks/useBreakpoint"
|
import { useBreakpoint } from "@/hooks/useBreakpoint"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
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"
|
import { useSelectRateContext } from "@/contexts/SelectRate/SelectRateContext"
|
||||||
|
|
||||||
export function RemoveBookingCodeButton() {
|
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 Link from "@scandic-hotels/design-system/Link"
|
||||||
|
|
||||||
import { overview } from "@/constants/routes/webviews"
|
import { overview } from "@/constants/routes/webviews"
|
||||||
|
import { env } from "@/env/server"
|
||||||
|
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
import { getLang } from "@/i18n/serverContext"
|
||||||
import { webviewSearchParams } from "@/utils/webviews"
|
import { webviewSearchParams } from "@/utils/webviews"
|
||||||
|
|
||||||
import styles from "./linkToOverview.module.css"
|
import styles from "./linkToOverview.module.css"
|
||||||
import { env } from "@/env/server"
|
|
||||||
|
|
||||||
export default async function LinkToOverview() {
|
export default async function LinkToOverview() {
|
||||||
if (!env.WEBVIEW_SHOW_OVERVIEW) {
|
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 { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
|
||||||
import { logger } from "@scandic-hotels/common/logger"
|
import { logger } from "@scandic-hotels/common/logger"
|
||||||
import { RateTypeEnum } from "@scandic-hotels/trpc/enums/rateType"
|
import { RateTypeEnum } from "@scandic-hotels/common/constants/rateType"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
sumPackages,
|
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
|
hasEnoughPoints: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HotelListingMapContentProps {
|
|
||||||
hotelPins: HotelPin[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HotelCardDialogProps {
|
export interface HotelCardDialogProps {
|
||||||
type?: "listing" | "standalone"
|
type?: "listing" | "standalone"
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
@@ -61,15 +57,6 @@ export interface HotelCardDialogProps {
|
|||||||
handleClose: (event: { stopPropagation: () => void }) => void
|
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 {
|
export interface HotelCardDialogListingProps {
|
||||||
hotels: HotelResponse[]
|
hotels: HotelResponse[]
|
||||||
unfilteredHotelCount: number
|
unfilteredHotelCount: number
|
||||||
|
|||||||
@@ -10,12 +10,6 @@ export type PriceCardProps = {
|
|||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PointsRowProps = {
|
|
||||||
pointsPerStay: number
|
|
||||||
additionalPricePerStay?: number
|
|
||||||
additionalPriceCurrency?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type VoucherCardProps = {
|
export type VoucherCardProps = {
|
||||||
productTypeVoucher: ProductTypeVoucher
|
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 {
|
import type {
|
||||||
Amenities,
|
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 { 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"
|
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",
|
"./utils/isValidJson": "./utils/isValidJson.ts",
|
||||||
"./hooks/*": "./hooks/*.ts",
|
"./hooks/*": "./hooks/*.ts",
|
||||||
"./stores/*": "./stores/*.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/language": "./constants/language.ts",
|
||||||
"./constants/membershipLevels": "./constants/membershipLevels.ts",
|
"./constants/membershipLevels": "./constants/membershipLevels.ts",
|
||||||
"./constants/paymentMethod": "./constants/paymentMethod.ts",
|
"./constants/paymentMethod": "./constants/paymentMethod.ts",
|
||||||
"./constants/currency": "./constants/currency.ts",
|
"./constants/rateType": "./constants/rateType.ts",
|
||||||
"./constants/dateFormats": "./constants/dateFormats.ts",
|
"./constants/routes/*": "./constants/routes/*.ts",
|
||||||
"./constants/routes/*": "./constants/routes/*.ts"
|
"./constants/signatureHotels": "./constants/signatureHotels.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { dirname, join } from 'path'
|
|
||||||
import type { StorybookConfig } from '@storybook/nextjs-vite'
|
import type { StorybookConfig } from '@storybook/nextjs-vite'
|
||||||
|
import { dirname, join } from 'path'
|
||||||
|
import { mergeConfig } from 'vite'
|
||||||
|
|
||||||
const config: StorybookConfig = {
|
const config: StorybookConfig = {
|
||||||
stories: ['../lib/**/*.mdx', '../lib/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
stories: ['../lib/**/*.mdx', '../lib/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||||
@@ -9,11 +10,42 @@ const config: StorybookConfig = {
|
|||||||
getAbsolutePath('@storybook/addon-vitest'),
|
getAbsolutePath('@storybook/addon-vitest'),
|
||||||
getAbsolutePath('@storybook/addon-docs'),
|
getAbsolutePath('@storybook/addon-docs'),
|
||||||
getAbsolutePath('@storybook/addon-a11y'),
|
getAbsolutePath('@storybook/addon-a11y'),
|
||||||
|
getAbsolutePath('storybook-react-intl'),
|
||||||
],
|
],
|
||||||
framework: {
|
framework: {
|
||||||
name: getAbsolutePath('@storybook/nextjs-vite'),
|
name: getAbsolutePath('@storybook/nextjs-vite'),
|
||||||
options: {},
|
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
|
export default config
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { withThemeByClassName } from '@storybook/addon-themes'
|
import { withThemeByClassName } from '@storybook/addon-themes'
|
||||||
|
import { IntlProvider } from 'react-intl'
|
||||||
import type { Preview, ReactRenderer } from '@storybook/nextjs-vite'
|
import type { Preview, ReactRenderer } from '@storybook/nextjs-vite'
|
||||||
|
import { reactIntl } from './reactIntl'
|
||||||
|
|
||||||
import '../lib/fonts.css'
|
import '../lib/fonts.css'
|
||||||
import '../lib/style.css'
|
import '../lib/style.css'
|
||||||
|
import '../lib/design-system-new-deprecated.css'
|
||||||
|
|
||||||
export const themes = {
|
export const themes = {
|
||||||
themes: {
|
themes: {
|
||||||
@@ -19,8 +21,33 @@ export const themes = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const preview: Preview = {
|
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: {
|
parameters: {
|
||||||
|
reactIntl,
|
||||||
|
nextjs: {
|
||||||
|
appDirectory: true,
|
||||||
|
},
|
||||||
docs: {
|
docs: {
|
||||||
toc: true,
|
toc: true,
|
||||||
},
|
},
|
||||||
@@ -31,18 +58,18 @@ const preview: Preview = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
backgrounds: {
|
backgrounds: {
|
||||||
values: [
|
options: {
|
||||||
// 👇 Scandic
|
// 👇 Scandic
|
||||||
{ name: 'Scandic Primary', value: '#FAF6F2' },
|
scandicPrimary: { name: 'Scandic Primary', value: '#FAF6F2' },
|
||||||
{ name: 'Scandic Subtle', value: '#F2ECE6' },
|
scandicSubtle: { name: 'Scandic Subtle', value: '#F2ECE6' },
|
||||||
{ name: 'Scandic Primary Dark', value: '#4D001B' },
|
scandicPrimaryDark: { name: 'Scandic Primary Dark', value: '#4D001B' },
|
||||||
// 👇 Default values
|
// 👇 Default values
|
||||||
{ name: 'Storybook Dark', value: '#333' },
|
storybookDark: { name: 'Storybook Dark', value: '#333' },
|
||||||
{ name: 'Storybook Light', value: '#F7F9F2' },
|
storybookLight: { name: 'Storybook Light', value: '#F7F9F2' },
|
||||||
],
|
|
||||||
default: 'Scandic Primary',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
|
||||||
tags: ['autodocs'],
|
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 { FlatCompat } from '@eslint/eslintrc'
|
||||||
import js from '@eslint/js'
|
import js from '@eslint/js'
|
||||||
import importPlugin from 'eslint-plugin-import'
|
import importPlugin from 'eslint-plugin-import'
|
||||||
|
import formatjs from 'eslint-plugin-formatjs'
|
||||||
|
|
||||||
const compat = new FlatCompat({
|
const compat = new FlatCompat({
|
||||||
recommendedConfig: js.configs.recommended,
|
recommendedConfig: js.configs.recommended,
|
||||||
@@ -28,6 +29,7 @@ export default defineConfig([
|
|||||||
plugins: {
|
plugins: {
|
||||||
'react-refresh': reactRefresh,
|
'react-refresh': reactRefresh,
|
||||||
import: importPlugin,
|
import: importPlugin,
|
||||||
|
formatjs,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'import/no-relative-packages': 'error',
|
'import/no-relative-packages': 'error',
|
||||||
@@ -37,6 +39,18 @@ export default defineConfig([
|
|||||||
allowConstantExport: true,
|
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']),
|
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 { Button as ButtonRAC } from 'react-aria-components'
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from 'react-intl'
|
||||||
|
|
||||||
import IconChip from "@scandic-hotels/design-system/IconChip"
|
import IconChip from '@scandic-hotels/design-system/IconChip'
|
||||||
import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon"
|
import DiscountIcon from '@scandic-hotels/design-system/Icons/DiscountIcon'
|
||||||
import FilledDiscountIcon from "@scandic-hotels/design-system/Icons/FilledDiscountIcon"
|
import FilledDiscountIcon from '@scandic-hotels/design-system/Icons/FilledDiscountIcon'
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { MaterialIcon } from '@scandic-hotels/design-system/Icons/MaterialIcon'
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from '@scandic-hotels/design-system/Typography'
|
||||||
|
|
||||||
import styles from "./bookingCodeChip.module.css"
|
import styles from './bookingCodeChip.module.css'
|
||||||
|
|
||||||
type BaseBookingCodeChipProps = {
|
type BaseBookingCodeChipProps = {
|
||||||
alignCenter?: boolean
|
alignCenter?: boolean
|
||||||
@@ -30,7 +30,7 @@ type BookingCodeChipProps =
|
|||||||
| BookingCodeChipWithoutCloseButtonProps
|
| BookingCodeChipWithoutCloseButtonProps
|
||||||
| BookingCodeChipWithCloseButtonProps
|
| BookingCodeChipWithCloseButtonProps
|
||||||
|
|
||||||
export default function BookingCodeChip({
|
export function BookingCodeChip({
|
||||||
alignCenter,
|
alignCenter,
|
||||||
bookingCode,
|
bookingCode,
|
||||||
isBreakfastIncluded,
|
isBreakfastIncluded,
|
||||||
@@ -58,19 +58,19 @@ export default function BookingCodeChip({
|
|||||||
<p className={styles.bookingCodeChip}>
|
<p className={styles.bookingCodeChip}>
|
||||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
<strong>
|
<strong>
|
||||||
{intl.formatMessage({ defaultMessage: "Campaign" })}
|
{intl.formatMessage({ defaultMessage: 'Campaign' })}
|
||||||
</strong>
|
</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
<span>
|
<span>
|
||||||
{isBreakfastIncluded
|
{isBreakfastIncluded
|
||||||
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||||
`${bookingCode ?? ""} ${intl.formatMessage({
|
`${bookingCode ?? ''} ${intl.formatMessage({
|
||||||
defaultMessage: "Breakfast included",
|
defaultMessage: 'Breakfast included',
|
||||||
})}`
|
})}`
|
||||||
: // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
: // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||||
`${bookingCode ?? ""} ${intl.formatMessage({
|
`${bookingCode ?? ''} ${intl.formatMessage({
|
||||||
defaultMessage: "Breakfast excluded",
|
defaultMessage: 'Breakfast excluded',
|
||||||
})}`}
|
})}`}
|
||||||
</span>
|
</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -96,12 +96,14 @@ export default function BookingCodeChip({
|
|||||||
className={alignCenter ? styles.center : undefined}
|
className={alignCenter ? styles.center : undefined}
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
className={`${styles.bookingCodeChip} ${isUnavailable ? styles.unavailable : ""}`}
|
className={`${styles.bookingCodeChip} ${isUnavailable ? styles.unavailable : ''}`}
|
||||||
>
|
>
|
||||||
{withText && (
|
{withText && (
|
||||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
<strong>
|
<strong>
|
||||||
{intl.formatMessage({ defaultMessage: "Booking code" })}
|
{intl.formatMessage({
|
||||||
|
defaultMessage: 'Booking code',
|
||||||
|
})}
|
||||||
</strong>
|
</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
@@ -556,6 +556,7 @@ export const TextWithIcon: Story = {
|
|||||||
args: {
|
args: {
|
||||||
onPress: fn(),
|
onPress: fn(),
|
||||||
children: (
|
children: (
|
||||||
|
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||||
<>
|
<>
|
||||||
Text with icon
|
Text with icon
|
||||||
<MaterialIcon icon="chevron_right" size={24} color="CurrentColor" />
|
<MaterialIcon icon="chevron_right" size={24} color="CurrentColor" />
|
||||||
@@ -577,6 +578,7 @@ export const TextWithIconInverted: Story = {
|
|||||||
args: {
|
args: {
|
||||||
onPress: fn(),
|
onPress: fn(),
|
||||||
children: (
|
children: (
|
||||||
|
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||||
<>
|
<>
|
||||||
Text with icon
|
Text with icon
|
||||||
<MaterialIcon icon="chevron_right" size={24} color="CurrentColor" />
|
<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 { chipVariants } from './variants'
|
||||||
|
import { VariantProps } from 'class-variance-authority'
|
||||||
|
|
||||||
import type { VariantProps } from 'class-variance-authority'
|
export interface ChipProps
|
||||||
|
|
||||||
interface ChipProps
|
|
||||||
extends React.HtmlHTMLAttributes<HTMLDivElement>,
|
extends React.HtmlHTMLAttributes<HTMLDivElement>,
|
||||||
VariantProps<typeof chipVariants> {}
|
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 { FacilityEnum } from '@scandic-hotels/common/constants/facilities'
|
||||||
import { IconName } from "@scandic-hotels/design-system/Icons/iconName"
|
|
||||||
import { FacilityEnum } from "@scandic-hotels/trpc/enums/facilities"
|
|
||||||
|
|
||||||
import type {
|
import type { JSX } from 'react'
|
||||||
IconProps,
|
import { IconName } from '../Icons/iconName'
|
||||||
NucleoIconProps,
|
import { MaterialIconSetIconProps } from '../Icons/MaterialIcon'
|
||||||
} from "@scandic-hotels/design-system/Icons"
|
import { IconProps, NucleoIconProps } from '../Icons'
|
||||||
import type { MaterialIconSetIconProps } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { IconByIconName } from '../Icons/IconByIconName'
|
||||||
import type { JSX } from "react"
|
|
||||||
|
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> = {
|
const facilityToIconMap: Record<FacilityEnum, IconName> = {
|
||||||
[FacilityEnum.AccessibleBathingControls]: IconName.StarFilled,
|
[FacilityEnum.AccessibleBathingControls]: IconName.StarFilled,
|
||||||
@@ -287,19 +300,3 @@ const facilityToIconMap: Record<FacilityEnum, IconName> = {
|
|||||||
[FacilityEnum.WideRestaurantEntrance]: IconName.StarFilled,
|
[FacilityEnum.WideRestaurantEntrance]: IconName.StarFilled,
|
||||||
[FacilityEnum.WiFiWirelessInternetAccessAllScandic]: 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, {
|
export const hotelCardDialogImageVariants = cva(styles.imageContainer, {
|
||||||
variants: {
|
variants: {
|
||||||
@@ -10,6 +10,6 @@ export const hotelCardDialogImageVariants = cva(styles.imageContainer, {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
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 { CurrencyEnum } from '@scandic-hotels/common/constants/currency'
|
||||||
import Caption from "@scandic-hotels/design-system/Caption"
|
import Caption from '@scandic-hotels/design-system/Caption'
|
||||||
import Subtitle from "@scandic-hotels/design-system/Subtitle"
|
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({
|
export default function HotelChequeCard({
|
||||||
productTypeCheque,
|
productTypeCheque,
|
||||||
@@ -19,7 +19,7 @@ export default function HotelChequeCard({
|
|||||||
<div className={styles.chequeRow}>
|
<div className={styles.chequeRow}>
|
||||||
<Caption>
|
<Caption>
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
defaultMessage: "From",
|
defaultMessage: 'From',
|
||||||
})}
|
})}
|
||||||
</Caption>
|
</Caption>
|
||||||
<div className={styles.cheque}>
|
<div className={styles.cheque}>
|
||||||
@@ -29,7 +29,7 @@ export default function HotelChequeCard({
|
|||||||
<Caption color="uiTextHighContrast">{CurrencyEnum.CC}</Caption>
|
<Caption color="uiTextHighContrast">{CurrencyEnum.CC}</Caption>
|
||||||
{productTypeCheque.localPrice.additionalPricePerStay > 0 ? (
|
{productTypeCheque.localPrice.additionalPricePerStay > 0 ? (
|
||||||
<>
|
<>
|
||||||
{"+"}
|
{'+'}
|
||||||
<Subtitle type="two" color="uiTextHighContrast">
|
<Subtitle type="two" color="uiTextHighContrast">
|
||||||
{productTypeCheque.localPrice.additionalPricePerStay}
|
{productTypeCheque.localPrice.additionalPricePerStay}
|
||||||
</Subtitle>
|
</Subtitle>
|
||||||
@@ -45,15 +45,15 @@ export default function HotelChequeCard({
|
|||||||
<div className={styles.chequeRow}>
|
<div className={styles.chequeRow}>
|
||||||
<Caption color="uiTextMediumContrast">
|
<Caption color="uiTextMediumContrast">
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
defaultMessage: "Approx.",
|
defaultMessage: 'Approx.',
|
||||||
})}
|
})}
|
||||||
</Caption>
|
</Caption>
|
||||||
<Caption color={"uiTextMediumContrast"}>
|
<Caption color={'uiTextMediumContrast'}>
|
||||||
{productTypeCheque.requestedPrice.numberOfCheques} {CurrencyEnum.CC}
|
{productTypeCheque.requestedPrice.numberOfCheques} {CurrencyEnum.CC}
|
||||||
{productTypeCheque.requestedPrice.additionalPricePerStay
|
{productTypeCheque.requestedPrice.additionalPricePerStay
|
||||||
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||||
" + "
|
' + '
|
||||||
: ""}
|
: ''}
|
||||||
{/* 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}`}
|
{`${productTypeCheque.requestedPrice.additionalPricePerStay} ${productTypeCheque.requestedPrice.currency}`}
|
||||||
</Caption>
|
</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"
|
'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 { useState } from 'react'
|
||||||
import Body from "@scandic-hotels/design-system/Body"
|
import { useIntl } from 'react-intl'
|
||||||
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 { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
import { selectRate } from '@scandic-hotels/common/constants/routes/hotelReservation'
|
||||||
import useLang from "@/hooks/useLang"
|
import Body from '@scandic-hotels/design-system/Body'
|
||||||
import { isValidClientSession } from "@/utils/clientSession"
|
import Caption from '@scandic-hotels/design-system/Caption'
|
||||||
import { trackEvent } from "@/utils/tracking/base"
|
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 '../../NoPriceAvailableCard'
|
||||||
import NoPriceAvailableCard from "../../HotelCard/NoPriceAvailableCard"
|
import { HotelCardDialogImage } from '../../HotelCardDialogImage'
|
||||||
import HotelCardDialogImage from "../HotelCardDialogImage"
|
|
||||||
|
|
||||||
import styles from "./standaloneHotelCardDialog.module.css"
|
import styles from './standaloneHotelCardDialog.module.css'
|
||||||
|
import { Lang } from '@scandic-hotels/common/constants/language'
|
||||||
import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map"
|
import { HotelPin } from '../../../Map/types'
|
||||||
|
import { FacilityToIcon } from '@scandic-hotels/design-system/FacilityToIcon'
|
||||||
|
import { HotelPointsRow } from '../../HotelPointsRow'
|
||||||
|
|
||||||
interface StandaloneHotelCardProps {
|
interface StandaloneHotelCardProps {
|
||||||
data: HotelPin
|
data: HotelPin
|
||||||
|
lang: Lang
|
||||||
|
isUserLoggedIn: boolean
|
||||||
handleClose: () => void
|
handleClose: () => void
|
||||||
|
onClick?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function StandaloneHotelCardDialog({
|
export function StandaloneHotelCardDialog({
|
||||||
data,
|
data,
|
||||||
|
lang,
|
||||||
handleClose,
|
handleClose,
|
||||||
|
isUserLoggedIn,
|
||||||
|
onClick,
|
||||||
}: StandaloneHotelCardProps) {
|
}: StandaloneHotelCardProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const lang = useLang()
|
|
||||||
const [imageError, setImageError] = useState(false)
|
const [imageError, setImageError] = useState(false)
|
||||||
const { data: session } = useSession()
|
|
||||||
const isUserLoggedIn = isValidClientSession(session)
|
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
chequePrice,
|
chequePrice,
|
||||||
@@ -50,16 +49,14 @@ export default function StandaloneHotelCardDialog({
|
|||||||
voucherPrice,
|
voucherPrice,
|
||||||
currency,
|
currency,
|
||||||
amenities,
|
amenities,
|
||||||
images,
|
image,
|
||||||
ratings,
|
ratings,
|
||||||
operaId,
|
operaId,
|
||||||
hasEnoughPoints,
|
hasEnoughPoints,
|
||||||
} = data
|
} = data
|
||||||
|
|
||||||
const firstImage = images[0]?.imageSizes?.small
|
|
||||||
const altText = images[0]?.metaData?.altText
|
|
||||||
const notEnoughPointsLabel = intl.formatMessage({
|
const notEnoughPointsLabel = intl.formatMessage({
|
||||||
defaultMessage: "Not enough points",
|
defaultMessage: 'Not enough points',
|
||||||
})
|
})
|
||||||
|
|
||||||
const shouldShowNotEnoughPoints = redemptionPrice && !hasEnoughPoints
|
const shouldShowNotEnoughPoints = redemptionPrice && !hasEnoughPoints
|
||||||
@@ -72,15 +69,15 @@ export default function StandaloneHotelCardDialog({
|
|||||||
className={styles.closeButton}
|
className={styles.closeButton}
|
||||||
onPress={handleClose}
|
onPress={handleClose}
|
||||||
aria-label={intl.formatMessage({
|
aria-label={intl.formatMessage({
|
||||||
defaultMessage: "Close",
|
defaultMessage: 'Close',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<MaterialIcon icon="close" size={22} color="CurrentColor" />
|
<MaterialIcon icon="close" size={22} color="CurrentColor" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<HotelCardDialogImage
|
<HotelCardDialogImage
|
||||||
firstImage={firstImage}
|
firstImage={image?.url}
|
||||||
altText={altText}
|
altText={image?.alt}
|
||||||
rating={ratings}
|
rating={{ tripAdvisor: ratings?.tripAdvisor ?? null }}
|
||||||
imageError={imageError}
|
imageError={imageError}
|
||||||
setImageError={setImageError}
|
setImageError={setImageError}
|
||||||
position="left"
|
position="left"
|
||||||
@@ -96,7 +93,7 @@ export default function StandaloneHotelCardDialog({
|
|||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<div className={styles.facilitiesItem} key={facility.id}>
|
<div className={styles.facilitiesItem} key={facility.id}>
|
||||||
{Icon && Icon}
|
{Icon}
|
||||||
<Footnote color="uiTextMediumContrast">
|
<Footnote color="uiTextMediumContrast">
|
||||||
{facility.name}
|
{facility.name}
|
||||||
</Footnote>
|
</Footnote>
|
||||||
@@ -115,13 +112,13 @@ export default function StandaloneHotelCardDialog({
|
|||||||
{redemptionPrice ? (
|
{redemptionPrice ? (
|
||||||
<Caption>
|
<Caption>
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
defaultMessage: "Available rates",
|
defaultMessage: 'Available rates',
|
||||||
})}
|
})}
|
||||||
</Caption>
|
</Caption>
|
||||||
) : (
|
) : (
|
||||||
<Caption type="bold">
|
<Caption type="bold">
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
defaultMessage: "From",
|
defaultMessage: 'From',
|
||||||
})}
|
})}
|
||||||
</Caption>
|
</Caption>
|
||||||
)}
|
)}
|
||||||
@@ -129,19 +126,19 @@ export default function StandaloneHotelCardDialog({
|
|||||||
<Subtitle type="two">
|
<Subtitle type="two">
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{
|
{
|
||||||
defaultMessage: "{price} {currency}",
|
defaultMessage: '{price} {currency}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
price: chequePrice.numberOfCheques,
|
price: chequePrice.numberOfCheques,
|
||||||
currency: "CC",
|
currency: 'CC',
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
{chequePrice.additionalPricePerStay > 0
|
{chequePrice.additionalPricePerStay > 0
|
||||||
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||||
" + " +
|
' + ' +
|
||||||
intl.formatMessage(
|
intl.formatMessage(
|
||||||
{
|
{
|
||||||
defaultMessage: "{price} {currency}",
|
defaultMessage: '{price} {currency}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
price: chequePrice.additionalPricePerStay,
|
price: chequePrice.additionalPricePerStay,
|
||||||
@@ -154,7 +151,7 @@ export default function StandaloneHotelCardDialog({
|
|||||||
<span>
|
<span>
|
||||||
/
|
/
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
defaultMessage: "night",
|
defaultMessage: 'night',
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</Body>
|
</Body>
|
||||||
@@ -164,7 +161,7 @@ export default function StandaloneHotelCardDialog({
|
|||||||
<Subtitle type="two">
|
<Subtitle type="two">
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{
|
{
|
||||||
defaultMessage: "{price} {currency}",
|
defaultMessage: '{price} {currency}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
price: voucherPrice,
|
price: voucherPrice,
|
||||||
@@ -176,7 +173,7 @@ export default function StandaloneHotelCardDialog({
|
|||||||
<span>
|
<span>
|
||||||
/
|
/
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
defaultMessage: "night",
|
defaultMessage: 'night',
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</Body>
|
</Body>
|
||||||
@@ -186,7 +183,7 @@ export default function StandaloneHotelCardDialog({
|
|||||||
<Subtitle type="two">
|
<Subtitle type="two">
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{
|
{
|
||||||
defaultMessage: "{price} {currency}",
|
defaultMessage: '{price} {currency}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
price: publicPrice,
|
price: publicPrice,
|
||||||
@@ -198,7 +195,7 @@ export default function StandaloneHotelCardDialog({
|
|||||||
<span>
|
<span>
|
||||||
/
|
/
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
defaultMessage: "night",
|
defaultMessage: 'night',
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</Body>
|
</Body>
|
||||||
@@ -208,7 +205,7 @@ export default function StandaloneHotelCardDialog({
|
|||||||
<Subtitle type="two" color="red">
|
<Subtitle type="two" color="red">
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{
|
{
|
||||||
defaultMessage: "{price} {currency}",
|
defaultMessage: '{price} {currency}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
price: memberPrice,
|
price: memberPrice,
|
||||||
@@ -220,7 +217,7 @@ export default function StandaloneHotelCardDialog({
|
|||||||
<span>
|
<span>
|
||||||
/
|
/
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
defaultMessage: "night",
|
defaultMessage: 'night',
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</Body>
|
</Body>
|
||||||
@@ -242,17 +239,7 @@ export default function StandaloneHotelCardDialog({
|
|||||||
theme="base"
|
theme="base"
|
||||||
size="small"
|
size="small"
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
onClick={() =>
|
onClick={onClick}
|
||||||
trackEvent({
|
|
||||||
event: "hotelClickMap",
|
|
||||||
map: {
|
|
||||||
action: "hotel click - map",
|
|
||||||
},
|
|
||||||
hotelInfo: {
|
|
||||||
hotelId: operaId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href={`${selectRate(lang)}?hotel=${operaId}`}
|
href={`${selectRate(lang)}?hotel=${operaId}`}
|
||||||
@@ -260,7 +247,7 @@ export default function StandaloneHotelCardDialog({
|
|||||||
keepSearchParams
|
keepSearchParams
|
||||||
>
|
>
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
defaultMessage: "See rooms",
|
defaultMessage: 'See rooms',
|
||||||
})}
|
})}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from 'react-intl'
|
||||||
|
|
||||||
import Caption from "@scandic-hotels/design-system/Caption"
|
import Caption from '@scandic-hotels/design-system/Caption'
|
||||||
import Subtitle from "@scandic-hotels/design-system/Subtitle"
|
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 type PointsRowProps = {
|
||||||
|
pointsPerStay: number
|
||||||
export default function HotelPointsRow({
|
additionalPricePerStay?: number
|
||||||
|
additionalPriceCurrency?: string
|
||||||
|
}
|
||||||
|
export function HotelPointsRow({
|
||||||
pointsPerStay,
|
pointsPerStay,
|
||||||
additionalPricePerStay,
|
additionalPricePerStay,
|
||||||
additionalPriceCurrency,
|
additionalPriceCurrency,
|
||||||
@@ -21,12 +24,12 @@ export default function HotelPointsRow({
|
|||||||
</Subtitle>
|
</Subtitle>
|
||||||
<Caption color="uiTextHighContrast">
|
<Caption color="uiTextHighContrast">
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
defaultMessage: "Points",
|
defaultMessage: 'Points',
|
||||||
})}
|
})}
|
||||||
</Caption>
|
</Caption>
|
||||||
{additionalPricePerStay ? (
|
{additionalPricePerStay ? (
|
||||||
<>
|
<>
|
||||||
{"+"}
|
{'+'}
|
||||||
<Subtitle type="two" color="uiTextHighContrast">
|
<Subtitle type="two" color="uiTextHighContrast">
|
||||||
{additionalPricePerStay}
|
{additionalPricePerStay}
|
||||||
</Subtitle>
|
</Subtitle>
|
||||||
@@ -1,17 +1,31 @@
|
|||||||
import { cx } from "class-variance-authority"
|
import { cx } from 'class-variance-authority'
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from 'react-intl'
|
||||||
|
|
||||||
import Body from "@scandic-hotels/design-system/Body"
|
import Body from '@scandic-hotels/design-system/Body'
|
||||||
import Caption from "@scandic-hotels/design-system/Caption"
|
import Caption from '@scandic-hotels/design-system/Caption'
|
||||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
import { Divider } from '@scandic-hotels/design-system/Divider'
|
||||||
import Subtitle from "@scandic-hotels/design-system/Subtitle"
|
import Subtitle from '@scandic-hotels/design-system/Subtitle'
|
||||||
import { RateTypeEnum } from "@scandic-hotels/trpc/enums/rateType"
|
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,
|
productTypePrices,
|
||||||
isMemberPrice = false,
|
isMemberPrice = false,
|
||||||
className,
|
className,
|
||||||
@@ -29,7 +43,7 @@ export default function HotelPriceCard({
|
|||||||
<dt>
|
<dt>
|
||||||
<Caption color="red">
|
<Caption color="red">
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
defaultMessage: "Member price",
|
defaultMessage: 'Member price',
|
||||||
})}
|
})}
|
||||||
</Caption>
|
</Caption>
|
||||||
</dt>
|
</dt>
|
||||||
@@ -39,7 +53,7 @@ export default function HotelPriceCard({
|
|||||||
<dt>
|
<dt>
|
||||||
<Caption color="uiTextHighContrast">
|
<Caption color="uiTextHighContrast">
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
defaultMessage: "Standard price",
|
defaultMessage: 'Standard price',
|
||||||
})}
|
})}
|
||||||
</Caption>
|
</Caption>
|
||||||
</dt>
|
</dt>
|
||||||
@@ -49,10 +63,10 @@ export default function HotelPriceCard({
|
|||||||
<dt>
|
<dt>
|
||||||
<Caption
|
<Caption
|
||||||
type="bold"
|
type="bold"
|
||||||
color={isMemberPrice ? "red" : "uiTextHighContrast"}
|
color={isMemberPrice ? 'red' : 'uiTextHighContrast'}
|
||||||
>
|
>
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
defaultMessage: "From",
|
defaultMessage: 'From',
|
||||||
})}
|
})}
|
||||||
</Caption>
|
</Caption>
|
||||||
</dt>
|
</dt>
|
||||||
@@ -60,12 +74,12 @@ export default function HotelPriceCard({
|
|||||||
<div className={styles.price}>
|
<div className={styles.price}>
|
||||||
<Subtitle
|
<Subtitle
|
||||||
type="two"
|
type="two"
|
||||||
color={isMemberPrice ? "red" : "uiTextHighContrast"}
|
color={isMemberPrice ? 'red' : 'uiTextHighContrast'}
|
||||||
>
|
>
|
||||||
{productTypePrices.localPrice.pricePerNight}
|
{productTypePrices.localPrice.pricePerNight}
|
||||||
</Subtitle>
|
</Subtitle>
|
||||||
<Body
|
<Body
|
||||||
color={isMemberPrice ? "red" : "uiTextHighContrast"}
|
color={isMemberPrice ? 'red' : 'uiTextHighContrast'}
|
||||||
textTransform="bold"
|
textTransform="bold"
|
||||||
>
|
>
|
||||||
{productTypePrices.localPrice.currency}
|
{productTypePrices.localPrice.currency}
|
||||||
@@ -73,7 +87,7 @@ export default function HotelPriceCard({
|
|||||||
<span className={styles.perNight}>
|
<span className={styles.perNight}>
|
||||||
/
|
/
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
defaultMessage: "night",
|
defaultMessage: 'night',
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</Body>
|
</Body>
|
||||||
@@ -85,12 +99,12 @@ export default function HotelPriceCard({
|
|||||||
<dt>
|
<dt>
|
||||||
<Caption color="uiTextMediumContrast">
|
<Caption color="uiTextMediumContrast">
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
defaultMessage: "Approx.",
|
defaultMessage: 'Approx.',
|
||||||
})}
|
})}
|
||||||
</Caption>
|
</Caption>
|
||||||
</dt>
|
</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<Caption color={"uiTextMediumContrast"}>
|
<Caption color={'uiTextMediumContrast'}>
|
||||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||||
{`${productTypePrices.requestedPrice.pricePerNight} `}
|
{`${productTypePrices.requestedPrice.pricePerNight} `}
|
||||||
{productTypePrices.requestedPrice.currency}
|
{productTypePrices.requestedPrice.currency}
|
||||||
@@ -108,12 +122,12 @@ export default function HotelPriceCard({
|
|||||||
<dt>
|
<dt>
|
||||||
<Caption color="uiTextMediumContrast">
|
<Caption color="uiTextMediumContrast">
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
defaultMessage: "Total",
|
defaultMessage: 'Total',
|
||||||
})}
|
})}
|
||||||
</Caption>
|
</Caption>
|
||||||
</dt>
|
</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<Caption color={"uiTextMediumContrast"}>
|
<Caption color={'uiTextMediumContrast'}>
|
||||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||||
{`${productTypePrices.localPrice.pricePerStay} `}
|
{`${productTypePrices.localPrice.pricePerStay} `}
|
||||||
{productTypePrices.localPrice.currency}
|
{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 { CurrencyEnum } from '@scandic-hotels/common/constants/currency'
|
||||||
import Caption from "@scandic-hotels/design-system/Caption"
|
import Caption from '@scandic-hotels/design-system/Caption'
|
||||||
import Subtitle from "@scandic-hotels/design-system/Subtitle"
|
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({
|
export default function HotelVoucherCard({
|
||||||
productTypeVoucher,
|
productTypeVoucher,
|
||||||
@@ -19,7 +19,7 @@ export default function HotelVoucherCard({
|
|||||||
<div className={styles.voucherRow}>
|
<div className={styles.voucherRow}>
|
||||||
<Caption>
|
<Caption>
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
defaultMessage: "From",
|
defaultMessage: 'From',
|
||||||
})}
|
})}
|
||||||
</Caption>
|
</Caption>
|
||||||
<div className={styles.voucher}>
|
<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 { MaterialIcon } from '@scandic-hotels/design-system/Icons/MaterialIcon'
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
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()
|
const intl = useIntl()
|
||||||
return (
|
return (
|
||||||
<div className={styles.priceCard}>
|
<div className={styles.priceCard}>
|
||||||
@@ -15,7 +15,7 @@ export default function NoPriceAvailableCard() {
|
|||||||
<span>
|
<span>
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
"There are no rooms available that match your request.",
|
'There are no rooms available that match your request.',
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: var(--Space-x2);
|
padding: var(--Space-x2);
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hotelDescription {
|
.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, {
|
export const hotelCardVariants = cva(styles.card, {
|
||||||
variants: {
|
variants: {
|
||||||
@@ -14,7 +14,7 @@ export const hotelCardVariants = cva(styles.card, {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
type: "pageListing",
|
type: 'pageListing',
|
||||||
state: "default",
|
state: 'default',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -12,7 +12,7 @@ import Lightbox from '../Lightbox'
|
|||||||
|
|
||||||
import styles from './imageGallery.module.css'
|
import styles from './imageGallery.module.css'
|
||||||
|
|
||||||
interface GalleryImage {
|
export interface GalleryImage {
|
||||||
src: string
|
src: string
|
||||||
alt: string
|
alt: string
|
||||||
caption?: string | null
|
caption?: string | null
|
||||||
|
|||||||
@@ -77,7 +77,7 @@
|
|||||||
|
|
||||||
.heart > .li::before,
|
.heart > .li::before,
|
||||||
.li:has(.heart)::before {
|
.li:has(.heart)::before {
|
||||||
content: url("/_static/icons/heart.svg");
|
content: url('/_static/icons/heart.svg');
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
top: 3px;
|
top: 3px;
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
|
|
||||||
.check > .li::before,
|
.check > .li::before,
|
||||||
.li:has(.check)::before {
|
.li:has(.check)::before {
|
||||||
content: url("/_static/icons/check-ring.svg");
|
content: url('/_static/icons/check-ring.svg');
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
top: 3px;
|
top: 3px;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { RTERenderMark, RTERenderOptionComponent } from "./node"
|
import type { RTERenderMark, RTERenderOptionComponent } from './node'
|
||||||
|
|
||||||
export type RenderOptions = {
|
export type RenderOptions = {
|
||||||
[type: string]: RTERenderOptionComponent | RTERenderMark
|
[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 { formatPrice } from '@scandic-hotels/common/utils/numberFormatting'
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { MaterialIcon } from '@scandic-hotels/design-system/Icons/MaterialIcon'
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
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 {
|
interface HotelPinProps {
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
@@ -16,7 +16,7 @@ interface HotelPinProps {
|
|||||||
hotelAdditionalCurrency?: string
|
hotelAdditionalCurrency?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function HotelPin({
|
export function HotelPin({
|
||||||
isActive,
|
isActive,
|
||||||
hotelPrice,
|
hotelPrice,
|
||||||
currency,
|
currency,
|
||||||
@@ -28,7 +28,7 @@ export default function HotelPin({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${styles.pin} ${isActive ? styles.active : ""}`}
|
className={`${styles.pin} ${isActive ? styles.active : ''}`}
|
||||||
data-hotelpin
|
data-hotelpin
|
||||||
>
|
>
|
||||||
<span className={styles.pinIcon}>
|
<span className={styles.pinIcon}>
|
||||||
@@ -36,17 +36,16 @@ export default function HotelPin({
|
|||||||
<MaterialIcon
|
<MaterialIcon
|
||||||
icon="calendar_clock"
|
icon="calendar_clock"
|
||||||
size={16}
|
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>
|
</span>
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
<p>
|
<p>
|
||||||
{isNotAvailable
|
{isNotAvailable
|
||||||
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
? '—'
|
||||||
"—"
|
|
||||||
: formatPrice(
|
: formatPrice(
|
||||||
intl,
|
intl,
|
||||||
hotelPrice,
|
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 {
|
import {
|
||||||
AdvancedMarker,
|
AdvancedMarker,
|
||||||
AdvancedMarkerAnchorPoint,
|
AdvancedMarkerAnchorPoint,
|
||||||
} from "@vis.gl/react-google-maps"
|
} from '@vis.gl/react-google-maps'
|
||||||
import { useIntl } from "react-intl"
|
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 { HotelMarkerByType } from '../../Markers/HotelMarkerByType'
|
||||||
import PoiMarker from "../../Markers/Poi"
|
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 { PointOfInterest } from '@scandic-hotels/trpc/types/hotel'
|
||||||
import type { MarkerInfo } from "@scandic-hotels/trpc/types/marker"
|
import type { MarkerInfo } from '@scandic-hotels/trpc/types/marker'
|
||||||
|
|
||||||
export type PoiMapMarkersProps = {
|
export type PoiMapMarkersProps = {
|
||||||
activePoi?: string | null
|
activePoi?: string | null
|
||||||
@@ -52,15 +52,15 @@ export default function PoiMapMarkers({
|
|||||||
zIndex={activePoi === poi.name ? 2 : 0}
|
zIndex={activePoi === poi.name ? 2 : 0}
|
||||||
onMouseEnter={() => onActivePoiChange?.(poi.name ?? null)}
|
onMouseEnter={() => onActivePoiChange?.(poi.name ?? null)}
|
||||||
onMouseLeave={() => onActivePoiChange?.(null)}
|
onMouseLeave={() => onActivePoiChange?.(null)}
|
||||||
onClick={() => toggleActivePoi(poi.name ?? "")}
|
onClick={() => toggleActivePoi(poi.name ?? '')}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`${styles.poi} ${activePoi === poi.name ? styles.active : ""}`}
|
className={`${styles.poi} ${activePoi === poi.name ? styles.active : ''}`}
|
||||||
>
|
>
|
||||||
<PoiMarker
|
<PoiMarker
|
||||||
group={poi.group}
|
group={poi.group}
|
||||||
categoryName={poi.categoryName}
|
categoryName={poi.categoryName}
|
||||||
size={activePoi === poi.name ? "large" : "small"}
|
size={activePoi === poi.name ? 'large' : 'small'}
|
||||||
/>
|
/>
|
||||||
<span className={styles.poiLabel}>
|
<span className={styles.poiLabel}>
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
@@ -73,7 +73,7 @@ export default function PoiMapMarkers({
|
|||||||
<span>
|
<span>
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{
|
{
|
||||||
defaultMessage: "{distanceInKm} km",
|
defaultMessage: '{distanceInKm} km',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
distanceInKm: poi.distance,
|
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 {
|
.mapContainer::after {
|
||||||
content: "";
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 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