Merged in fix/SW-2165-map-navigate-hotel-card (pull request #2246)
fix(SW-2165): map navigate on enter press * fix(SW-2165): navigate on enter press and refactor * fix(SW-2165): responsive design * fix(SW-2165): replace spacing variables * fix(SW-2165): resolve pr comment * fix(SW-2165): remove isOpen, hide/show logic already handled * fix(SW-2165): remove dialog * fix(SW-2165): use buttonicon * fix(SW-2165): do not focus on close button without tab * fix(SW-2165): remove unneccessary css Approved-by: Christian Andolf
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useSession } from "next-auth/react"
|
import { useSession } from "next-auth/react"
|
||||||
|
import { useState } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { IconButton } from "@scandic-hotels/design-system/IconButton"
|
||||||
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { selectRate } from "@/constants/routes/hotelReservation"
|
import { selectRate } from "@/constants/routes/hotelReservation"
|
||||||
@@ -11,31 +14,31 @@ import Button from "@/components/TempDesignSystem/Button"
|
|||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
import { isValidClientSession } from "@/utils/clientSession"
|
import { isValidClientSession } from "@/utils/clientSession"
|
||||||
|
|
||||||
import HotelPointsRow from "../../HotelCard/HotelPointsRow"
|
import HotelPointsRow from "../../HotelCard/HotelPointsRow"
|
||||||
import NoPriceAvailableCard from "../../HotelCard/NoPriceAvailableCard"
|
import NoPriceAvailableCard from "../../HotelCard/NoPriceAvailableCard"
|
||||||
import HotelCardDialogImage from "../HotelCardDialogImage"
|
import HotelCardDialogImage from "../HotelCardDialogImage"
|
||||||
|
|
||||||
import styles from "../hotelCardDialog.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"
|
||||||
import type { Lang } from "@/constants/languages"
|
|
||||||
|
|
||||||
interface ListingHotelCardProps {
|
interface ListingHotelCardProps {
|
||||||
data: HotelPin
|
data: HotelPin
|
||||||
lang: Lang
|
handleClose: () => void
|
||||||
imageError: boolean
|
|
||||||
setImageError: (error: boolean) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ListingHotelCardDialog({
|
export default function ListingHotelCardDialog({
|
||||||
data,
|
data,
|
||||||
lang,
|
handleClose,
|
||||||
imageError,
|
|
||||||
setImageError,
|
|
||||||
}: ListingHotelCardProps) {
|
}: ListingHotelCardProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const lang = useLang()
|
||||||
|
|
||||||
|
const [imageError, setImageError] = useState(false)
|
||||||
|
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
const isUserLoggedIn = isValidClientSession(session)
|
const isUserLoggedIn = isValidClientSession(session)
|
||||||
const {
|
const {
|
||||||
@@ -57,161 +60,174 @@ export default function ListingHotelCardDialog({
|
|||||||
const altText = images[0]?.metaData?.altText
|
const altText = images[0]?.metaData?.altText
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.content}>
|
<div className={styles.container}>
|
||||||
<div className={styles.header}>
|
<IconButton
|
||||||
<HotelCardDialogImage
|
theme="Black"
|
||||||
firstImage={firstImage}
|
style="Muted"
|
||||||
altText={altText}
|
className={styles.closeButton}
|
||||||
rating={ratings}
|
onPress={handleClose}
|
||||||
imageError={imageError}
|
aria-label={intl.formatMessage({
|
||||||
setImageError={setImageError}
|
defaultMessage: "Close",
|
||||||
position="top"
|
})}
|
||||||
/>
|
>
|
||||||
<div>
|
<MaterialIcon icon="close" size={22} color="CurrentColor" />
|
||||||
<div className={styles.name}>
|
</IconButton>
|
||||||
<Subtitle type="two">{name}</Subtitle>
|
<div className={styles.content}>
|
||||||
</div>
|
<div className={styles.header}>
|
||||||
<div className={styles.facilities}>
|
<HotelCardDialogImage
|
||||||
{amenities.map((facility) => (
|
firstImage={firstImage}
|
||||||
<div className={styles.facilitiesItem} key={facility.id}>
|
altText={altText}
|
||||||
<FacilityToIcon
|
rating={ratings}
|
||||||
id={facility.id}
|
imageError={imageError}
|
||||||
size={20}
|
setImageError={setImageError}
|
||||||
color="Icon/Default"
|
position="top"
|
||||||
/>
|
/>
|
||||||
</div>
|
<div>
|
||||||
))}
|
<div className={styles.name}>
|
||||||
</div>
|
<Subtitle type="two">{name}</Subtitle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className={styles.facilities}>
|
||||||
|
{amenities.map((facility) => (
|
||||||
{publicPrice ||
|
<div key={facility.id}>
|
||||||
memberPrice ||
|
<FacilityToIcon
|
||||||
redemptionPrice ||
|
id={facility.id}
|
||||||
voucherPrice ||
|
size={20}
|
||||||
chequePrice ? (
|
color="Icon/Default"
|
||||||
<div className={styles.bottomContainer}>
|
/>
|
||||||
<div className={styles.pricesContainer}>
|
</div>
|
||||||
{redemptionPrice ? (
|
))}
|
||||||
<Caption color="uiTextHighContrast">
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Available rates",
|
|
||||||
})}
|
|
||||||
</Caption>
|
|
||||||
) : (
|
|
||||||
<Caption color="uiTextHighContrast">
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Per night from",
|
|
||||||
})}
|
|
||||||
</Caption>
|
|
||||||
)}
|
|
||||||
<div className={styles.listingPrices}>
|
|
||||||
{publicPrice && !isUserLoggedIn && memberPrice ? (
|
|
||||||
<>
|
|
||||||
<Subtitle type="two">
|
|
||||||
{publicPrice} {currency}
|
|
||||||
</Subtitle>
|
|
||||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
|
||||||
{memberPrice && <Caption>/</Caption>}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
bookingCode &&
|
|
||||||
publicPrice && (
|
|
||||||
<Subtitle type="two" color="red">
|
|
||||||
{publicPrice} {currency}
|
|
||||||
</Subtitle>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
{memberPrice && (
|
|
||||||
<Subtitle type="two" color="red">
|
|
||||||
{intl.formatMessage(
|
|
||||||
{
|
|
||||||
defaultMessage: "{price} {currency}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
price: memberPrice,
|
|
||||||
currency,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</Subtitle>
|
|
||||||
)}
|
|
||||||
{redemptionPrice && (
|
|
||||||
<HotelPointsRow pointsPerStay={redemptionPrice} />
|
|
||||||
)}
|
|
||||||
{chequePrice && (
|
|
||||||
<Subtitle type="two">
|
|
||||||
{intl.formatMessage(
|
|
||||||
{
|
|
||||||
defaultMessage: "{price} {currency}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
price: chequePrice.numberOfCheques,
|
|
||||||
currency: "CC",
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
{chequePrice.additionalPricePerStay > 0
|
|
||||||
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
|
||||||
" + " +
|
|
||||||
intl.formatMessage(
|
|
||||||
{
|
|
||||||
defaultMessage: "{price} {currency}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
price: chequePrice.additionalPricePerStay,
|
|
||||||
currency: chequePrice.currency,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
|
||||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
|
||||||
<span>
|
|
||||||
/
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "night",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</Typography>
|
|
||||||
</Subtitle>
|
|
||||||
)}
|
|
||||||
{voucherPrice && (
|
|
||||||
<Subtitle type="two">
|
|
||||||
{intl.formatMessage(
|
|
||||||
{
|
|
||||||
defaultMessage: "{price} {currency}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
price: voucherPrice,
|
|
||||||
currency,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
|
||||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
|
||||||
<span>
|
|
||||||
/
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "night",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</Typography>
|
|
||||||
</Subtitle>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button asChild theme="base" size="small" className={styles.button}>
|
|
||||||
<Link
|
|
||||||
href={`${selectRate(lang)}?hotel=${operaId}`}
|
|
||||||
color="none"
|
|
||||||
keepSearchParams
|
|
||||||
>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "See rooms",
|
|
||||||
})}
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<NoPriceAvailableCard />
|
{publicPrice ||
|
||||||
)}
|
memberPrice ||
|
||||||
|
redemptionPrice ||
|
||||||
|
voucherPrice ||
|
||||||
|
chequePrice ? (
|
||||||
|
<div className={styles.bottomContainer}>
|
||||||
|
<div className={styles.pricesContainer}>
|
||||||
|
{redemptionPrice ? (
|
||||||
|
<Caption color="uiTextHighContrast">
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Available rates",
|
||||||
|
})}
|
||||||
|
</Caption>
|
||||||
|
) : (
|
||||||
|
<Caption color="uiTextHighContrast">
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Per night from",
|
||||||
|
})}
|
||||||
|
</Caption>
|
||||||
|
)}
|
||||||
|
<div className={styles.listingPrices}>
|
||||||
|
{publicPrice && !isUserLoggedIn && memberPrice ? (
|
||||||
|
<>
|
||||||
|
<Subtitle type="two">
|
||||||
|
{publicPrice} {currency}
|
||||||
|
</Subtitle>
|
||||||
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||||
|
{memberPrice && <Caption>/</Caption>}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
bookingCode &&
|
||||||
|
publicPrice && (
|
||||||
|
<Subtitle type="two" color="red">
|
||||||
|
{publicPrice} {currency}
|
||||||
|
</Subtitle>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{memberPrice && (
|
||||||
|
<Subtitle type="two" color="red">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "{price} {currency}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
price: memberPrice,
|
||||||
|
currency,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</Subtitle>
|
||||||
|
)}
|
||||||
|
{redemptionPrice && (
|
||||||
|
<HotelPointsRow pointsPerStay={redemptionPrice} />
|
||||||
|
)}
|
||||||
|
{chequePrice && (
|
||||||
|
<Subtitle type="two">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "{price} {currency}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
price: chequePrice.numberOfCheques,
|
||||||
|
currency: "CC",
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
{chequePrice.additionalPricePerStay > 0
|
||||||
|
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||||
|
" + " +
|
||||||
|
intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "{price} {currency}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
price: chequePrice.additionalPricePerStay,
|
||||||
|
currency: chequePrice.currency,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||||
|
<span>
|
||||||
|
/
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "night",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
</Subtitle>
|
||||||
|
)}
|
||||||
|
{voucherPrice && (
|
||||||
|
<Subtitle type="two">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "{price} {currency}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
price: voucherPrice,
|
||||||
|
currency,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||||
|
<span>
|
||||||
|
/
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "night",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
</Subtitle>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button asChild theme="base" size="small" className={styles.button}>
|
||||||
|
<Link
|
||||||
|
href={`${selectRate(lang)}?hotel=${operaId}`}
|
||||||
|
color="none"
|
||||||
|
keepSearchParams
|
||||||
|
>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "See rooms",
|
||||||
|
})}
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<NoPriceAvailableCard />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
.container {
|
||||||
|
border: 1px solid var(--Base-Border-Subtle);
|
||||||
|
border-radius: var(--Corner-radius-md);
|
||||||
|
min-width: 358px;
|
||||||
|
background: var(--Base-Surface-Primary-light-Normal);
|
||||||
|
box-shadow: 0px 0px 8px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: var(--Space-x15);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Space-x15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
height: 48px;
|
||||||
|
max-width: 180px;
|
||||||
|
margin-bottom: var(--Space-x05);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.facilities {
|
||||||
|
display: flex;
|
||||||
|
gap: 0 var(--Space-x15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.priceCard {
|
||||||
|
border-radius: var(--Corner-radius-md);
|
||||||
|
padding: var(--Space-x05) var(--Space-x1);
|
||||||
|
background: var(--Base-Surface-Secondary-light-Normal);
|
||||||
|
margin-top: var(--Space-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prices {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.bottomContainer {
|
||||||
|
display: flex;
|
||||||
|
border-top: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
||||||
|
padding-top: var(--Space-x2);
|
||||||
|
padding-bottom: var(--Space-x05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricesContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listingPrices {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content .button {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useSession } from "next-auth/react"
|
import { useSession } from "next-auth/react"
|
||||||
|
import { useState } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { IconButton } from "@scandic-hotels/design-system/IconButton"
|
||||||
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
|
||||||
import { selectRate } from "@/constants/routes/hotelReservation"
|
import { selectRate } from "@/constants/routes/hotelReservation"
|
||||||
|
|
||||||
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||||
@@ -11,6 +15,7 @@ import Body from "@/components/TempDesignSystem/Text/Body"
|
|||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
import { isValidClientSession } from "@/utils/clientSession"
|
import { isValidClientSession } from "@/utils/clientSession"
|
||||||
import { trackEvent } from "@/utils/tracking/base"
|
import { trackEvent } from "@/utils/tracking/base"
|
||||||
|
|
||||||
@@ -18,25 +23,22 @@ import HotelPointsRow from "../../HotelCard/HotelPointsRow"
|
|||||||
import NoPriceAvailableCard from "../../HotelCard/NoPriceAvailableCard"
|
import NoPriceAvailableCard from "../../HotelCard/NoPriceAvailableCard"
|
||||||
import HotelCardDialogImage from "../HotelCardDialogImage"
|
import HotelCardDialogImage from "../HotelCardDialogImage"
|
||||||
|
|
||||||
import styles from "../hotelCardDialog.module.css"
|
import styles from "./standaloneHotelCardDialog.module.css"
|
||||||
|
|
||||||
import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map"
|
import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map"
|
||||||
import type { Lang } from "@/constants/languages"
|
|
||||||
|
|
||||||
interface StandaloneHotelCardProps {
|
interface StandaloneHotelCardProps {
|
||||||
data: HotelPin
|
data: HotelPin
|
||||||
lang: Lang
|
handleClose: () => void
|
||||||
imageError: boolean
|
|
||||||
setImageError: (error: boolean) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function StandaloneHotelCardDialog({
|
export default function StandaloneHotelCardDialog({
|
||||||
data,
|
data,
|
||||||
lang,
|
handleClose,
|
||||||
imageError,
|
|
||||||
setImageError,
|
|
||||||
}: StandaloneHotelCardProps) {
|
}: StandaloneHotelCardProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const lang = useLang()
|
||||||
|
const [imageError, setImageError] = useState(false)
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
const isUserLoggedIn = isValidClientSession(session)
|
const isUserLoggedIn = isValidClientSession(session)
|
||||||
const {
|
const {
|
||||||
@@ -57,7 +59,18 @@ export default function StandaloneHotelCardDialog({
|
|||||||
const altText = images[0]?.metaData?.altText
|
const altText = images[0]?.metaData?.altText
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={styles.container}>
|
||||||
|
<IconButton
|
||||||
|
theme="Black"
|
||||||
|
style="Muted"
|
||||||
|
className={styles.closeButton}
|
||||||
|
onPress={handleClose}
|
||||||
|
aria-label={intl.formatMessage({
|
||||||
|
defaultMessage: "Close",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<MaterialIcon icon="close" size={22} color="CurrentColor" />
|
||||||
|
</IconButton>
|
||||||
<HotelCardDialogImage
|
<HotelCardDialogImage
|
||||||
firstImage={firstImage}
|
firstImage={firstImage}
|
||||||
altText={altText}
|
altText={altText}
|
||||||
@@ -67,10 +80,8 @@ export default function StandaloneHotelCardDialog({
|
|||||||
position="left"
|
position="left"
|
||||||
/>
|
/>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<div className={styles.header}>
|
<div className={styles.name}>
|
||||||
<div className={styles.name}>
|
<Body textTransform="bold">{name}</Body>
|
||||||
<Body textTransform="bold">{name}</Body>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.facilities}>
|
<div className={styles.facilities}>
|
||||||
{amenities.slice(0, 3).map((facility) => {
|
{amenities.slice(0, 3).map((facility) => {
|
||||||
@@ -246,6 +257,6 @@ export default function StandaloneHotelCardDialog({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
.container {
|
||||||
|
flex-direction: row;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
background: var(--Base-Surface-Primary-light-Normal);
|
||||||
|
box-shadow: 0px 0px 8px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 220px;
|
||||||
|
padding: var(--Space-x15);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
height: 48px;
|
||||||
|
max-width: 180px;
|
||||||
|
margin-bottom: var(--Space-x05);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-right: var(--Space-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.facilities {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0 var(--Space-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.facilitiesItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--Space-x05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prices {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.priceCard {
|
||||||
|
border-radius: var(--Corner-radius-md);
|
||||||
|
padding: var(--Space-x05) var(--Space-x1);
|
||||||
|
background: var(--Base-Surface-Secondary-light-Normal);
|
||||||
|
margin-top: var(--Space-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricesContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content .button {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
.dialog {
|
|
||||||
padding-bottom: var(--Spacing-x1);
|
|
||||||
bottom: 0;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialogContainer {
|
|
||||||
border: 1px solid var(--Base-Border-Subtle);
|
|
||||||
border-radius: var(--Corner-radius-md);
|
|
||||||
min-width: 402px;
|
|
||||||
background: var(--Base-Surface-Primary-light-Normal);
|
|
||||||
box-shadow: 0px 0px 8px 3px rgba(0, 0, 0, 0.1);
|
|
||||||
flex-direction: row;
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialogContainer[data-type="listing"] {
|
|
||||||
min-width: 358px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialogContainer[data-type="listing"] .header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: var(--Spacing-x-one-and-half);
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
height: 48px;
|
|
||||||
max-width: 180px;
|
|
||||||
margin-bottom: var(--Spacing-x-half);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.closeIcon {
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
right: 8px;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 220px;
|
|
||||||
padding: var(--Spacing-x-one-and-half);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialogContainer[data-type="listing"] .content {
|
|
||||||
gap: var(--Spacing-x-one-and-half);
|
|
||||||
}
|
|
||||||
|
|
||||||
.facilities {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0 var(--Spacing-x1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialogContainer[data-type="listing"] .facilities {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
position: relative;
|
|
||||||
padding-right: 20px;
|
|
||||||
gap: 0 var(--Spacing-x-one-and-half);
|
|
||||||
max-width: 242px;
|
|
||||||
}
|
|
||||||
.dialogContainer[data-type="listing"] .facilities::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 20px;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(
|
|
||||||
to left,
|
|
||||||
rgba(255, 255, 255, 1),
|
|
||||||
rgba(255, 255, 255, 0)
|
|
||||||
);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.facilitiesItem {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--Spacing-x-half);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialogContainer[data-type="listing"] .facilitiesItem {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: var(--Spacing-x-half);
|
|
||||||
}
|
|
||||||
|
|
||||||
.priceCard {
|
|
||||||
border-radius: var(--Corner-radius-md);
|
|
||||||
padding: var(--Spacing-x-half) var(--Spacing-x1);
|
|
||||||
background: var(--Base-Surface-Secondary-light-Normal);
|
|
||||||
margin-top: var(--Spacing-x1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.prices {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--Spacing-x1);
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottomContainer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
border-top: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
|
||||||
padding-top: var(--Spacing-x2);
|
|
||||||
padding-bottom: var(--Spacing-x-half);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pricesContainer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--Spacing-x1);
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialogContainer[data-type="listing"] .pricesContainer {
|
|
||||||
flex: 1;
|
|
||||||
height: 44px;
|
|
||||||
gap: 0;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.listingPrices {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: var(--Spacing-x1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.perNight {
|
|
||||||
color: var(--Base-Text-Subtle-light-Normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content .button {
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.dialog {
|
|
||||||
bottom: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useParams } from "next/navigation"
|
|
||||||
import { useState } from "react"
|
|
||||||
|
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
|
||||||
|
|
||||||
import ListingHotelCardDialog from "./ListingHotelCardDialog"
|
|
||||||
import StandaloneHotelCardDialog from "./StandaloneHotelCardDialog"
|
|
||||||
|
|
||||||
import styles from "./hotelCardDialog.module.css"
|
|
||||||
|
|
||||||
import type { HotelCardDialogProps } from "@/types/components/hotelReservation/selectHotel/map"
|
|
||||||
import type { Lang } from "@/constants/languages"
|
|
||||||
|
|
||||||
export default function HotelCardDialog({
|
|
||||||
data,
|
|
||||||
isOpen,
|
|
||||||
type = "standalone",
|
|
||||||
handleClose,
|
|
||||||
}: HotelCardDialogProps) {
|
|
||||||
const params = useParams()
|
|
||||||
const lang = params.lang as Lang
|
|
||||||
const [imageError, setImageError] = useState(false)
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<dialog open={isOpen} className={styles.dialog}>
|
|
||||||
<div className={styles.dialogContainer} data-type={type}>
|
|
||||||
<div onClick={handleClose}>
|
|
||||||
<MaterialIcon icon="close" className={styles.closeIcon} size={22} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{type === "standalone" ? (
|
|
||||||
<StandaloneHotelCardDialog
|
|
||||||
data={data}
|
|
||||||
lang={lang}
|
|
||||||
imageError={imageError}
|
|
||||||
setImageError={setImageError}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ListingHotelCardDialog
|
|
||||||
data={data}
|
|
||||||
lang={lang}
|
|
||||||
imageError={imageError}
|
|
||||||
setImageError={setImageError}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
.hotelCardDialogListing {
|
.hotelCardDialogListing {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
|
||||||
gap: var(--Spacing-x1);
|
gap: var(--Spacing-x1);
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
@@ -17,13 +16,7 @@
|
|||||||
transform: translateZ(0);
|
transform: translateZ(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hotelCardDialogListing > div {
|
.hotelCard {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
scroll-snap-align: center;
|
scroll-snap-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hotelCardDialogListing dialog {
|
|
||||||
position: relative;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useIntl } from "react-intl"
|
|||||||
|
|
||||||
import { useHotelsMapStore } from "@/stores/hotels-map"
|
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||||
|
|
||||||
import HotelCardDialog from "../HotelCardDialog"
|
import ListingHotelCardDialog from "../HotelCardDialog/ListingHotelCardDialog"
|
||||||
import { getHotelPins } from "./utils"
|
import { getHotelPins } from "./utils"
|
||||||
|
|
||||||
import styles from "./hotelCardDialogListing.module.css"
|
import styles from "./hotelCardDialogListing.module.css"
|
||||||
@@ -127,13 +127,9 @@ export default function HotelCardDialogListing({
|
|||||||
key={data.name}
|
key={data.name}
|
||||||
ref={isActive ? activeCardRef : null}
|
ref={isActive ? activeCardRef : null}
|
||||||
data-name={data.name}
|
data-name={data.name}
|
||||||
|
className={styles.hotelCard}
|
||||||
>
|
>
|
||||||
<HotelCardDialog
|
<ListingHotelCardDialog data={data} handleClose={deactivate} />
|
||||||
data={data}
|
|
||||||
isOpen={!!activeHotel}
|
|
||||||
handleClose={deactivate}
|
|
||||||
type="listing"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,28 +1,3 @@
|
|||||||
.advancedMarker {
|
.advancedMarker {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialogContainer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 32px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 402px;
|
|
||||||
height: 181px;
|
|
||||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card.active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.dialogContainer {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
AdvancedMarker,
|
AdvancedMarker,
|
||||||
AdvancedMarkerAnchorPoint,
|
AdvancedMarkerAnchorPoint,
|
||||||
|
InfoWindow,
|
||||||
} from "@vis.gl/react-google-maps"
|
} from "@vis.gl/react-google-maps"
|
||||||
import { useCallback } from "react"
|
import { useCallback } from "react"
|
||||||
|
import { useMediaQuery } from "usehooks-ts"
|
||||||
|
|
||||||
import { useHotelsMapStore } from "@/stores/hotels-map"
|
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||||
|
|
||||||
import HotelCardDialog from "@/components/HotelReservation/HotelCardDialog"
|
import StandaloneHotelCardDialog from "@/components/HotelReservation/HotelCardDialog/StandaloneHotelCardDialog"
|
||||||
import { trackEvent } from "@/utils/tracking/base"
|
import { trackEvent } from "@/utils/tracking/base"
|
||||||
|
|
||||||
import HotelPin from "./HotelPin"
|
import HotelPin from "./HotelPin"
|
||||||
@@ -18,6 +20,7 @@ import type { HotelListingMapContentProps } from "@/types/components/hotelReserv
|
|||||||
function HotelListingMapContent({ hotelPins }: HotelListingMapContentProps) {
|
function HotelListingMapContent({ hotelPins }: HotelListingMapContentProps) {
|
||||||
const { activeHotel, hoveredHotel, activate, deactivate, engage, disengage } =
|
const { activeHotel, hoveredHotel, activate, deactivate, engage, disengage } =
|
||||||
useHotelsMapStore()
|
useHotelsMapStore()
|
||||||
|
const isDesktop = useMediaQuery("(min-width: 768px)")
|
||||||
|
|
||||||
const toggleActiveHotelPin = useCallback(
|
const toggleActiveHotelPin = useCallback(
|
||||||
(pinName: string | null, hotelId: string) => {
|
(pinName: string | null, hotelId: string) => {
|
||||||
@@ -69,17 +72,22 @@ function HotelListingMapContent({ hotelPins }: HotelListingMapContentProps) {
|
|||||||
onMouseLeave={() => disengage()}
|
onMouseLeave={() => disengage()}
|
||||||
onClick={() => toggleActiveHotelPin(pin.name, pin.operaId)}
|
onClick={() => toggleActiveHotelPin(pin.name, pin.operaId)}
|
||||||
>
|
>
|
||||||
<div className={styles.dialogContainer}>
|
{isActiveOrHovered && isDesktop && (
|
||||||
<HotelCardDialog
|
<InfoWindow
|
||||||
isOpen={isActiveOrHovered}
|
position={pin.coordinates}
|
||||||
handleClose={(event: { stopPropagation: () => void }) => {
|
pixelOffset={[0, -24]}
|
||||||
event.stopPropagation()
|
headerDisabled={true}
|
||||||
deactivate()
|
shouldFocus={false}
|
||||||
disengage()
|
>
|
||||||
}}
|
<StandaloneHotelCardDialog
|
||||||
data={pin}
|
data={pin}
|
||||||
/>
|
handleClose={() => {
|
||||||
</div>
|
deactivate()
|
||||||
|
disengage()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</InfoWindow>
|
||||||
|
)}
|
||||||
<HotelPin
|
<HotelPin
|
||||||
isActive={isActiveOrHovered}
|
isActive={isActiveOrHovered}
|
||||||
hotelPrice={hotelPrice}
|
hotelPrice={hotelPrice}
|
||||||
|
|||||||
@@ -6,6 +6,20 @@
|
|||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mapContainer :global(.gm-style .gm-style-iw-d) {
|
||||||
|
padding: 0 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
max-height: none !important;
|
||||||
|
max-width: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapContainer :global(.gm-style .gm-style-iw-c) {
|
||||||
|
padding: 0 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
max-height: none !important;
|
||||||
|
max-width: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.mapContainer::after {
|
.mapContainer::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
Reference in New Issue
Block a user