Merged in feature/SW-3327-move-hotel-info-card-to-design-system (pull request #2730)
Feature/SW-3327 move hotel info card to design system * wip * wip * wip * wip moving hotelinfocard * add controls for HotelInfoCard in storybook * merge Approved-by: Anton Gunnarsson
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { HotelInfoCardSkeleton } from "@/components/HotelReservation/SelectRate/HotelInfoCard"
|
import { HotelInfoCardSkeleton } from "@scandic-hotels/design-system/HotelInfoCard"
|
||||||
|
|
||||||
import { RoomsContainerSkeleton } from "@/components/HotelReservation/SelectRate/RoomsContainer/RoomsContainerSkeleton"
|
import { RoomsContainerSkeleton } from "@/components/HotelReservation/SelectRate/RoomsContainer/RoomsContainerSkeleton"
|
||||||
|
|
||||||
export default function LoadingSelectRate() {
|
export default function LoadingSelectRate() {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import SidePanel from "@/components/HotelReservation/SidePanel"
|
|||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import BookingConfirmationProvider from "@/providers/BookingConfirmationProvider"
|
import BookingConfirmationProvider from "@/providers/BookingConfirmationProvider"
|
||||||
|
|
||||||
import { getHotelAlertsForBookingDates } from "../utils"
|
import { filterOverlappingDates } from "../utils"
|
||||||
import Confirmation from "./Confirmation"
|
import Confirmation from "./Confirmation"
|
||||||
import Tracking from "./Tracking"
|
import Tracking from "./Tracking"
|
||||||
import { mapRoomState } from "./utils"
|
import { mapRoomState } from "./utils"
|
||||||
@@ -79,7 +79,7 @@ export default async function BookingConfirmation({
|
|||||||
<PaymentDetails />
|
<PaymentDetails />
|
||||||
<Divider color="Border/Divider/Subtle" />
|
<Divider color="Border/Divider/Subtle" />
|
||||||
<HotelDetails hotel={hotel} />
|
<HotelDetails hotel={hotel} />
|
||||||
{getHotelAlertsForBookingDates(
|
{filterOverlappingDates(
|
||||||
hotel.specialAlerts,
|
hotel.specialAlerts,
|
||||||
dt(booking.checkInDate),
|
dt(booking.checkInDate),
|
||||||
dt(booking.checkOutDate)
|
dt(booking.checkOutDate)
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import { getIntl } from "@/i18n"
|
|||||||
import MyStayProvider from "@/providers/MyStay"
|
import MyStayProvider from "@/providers/MyStay"
|
||||||
import { isLoggedInUser } from "@/utils/isLoggedInUser"
|
import { isLoggedInUser } from "@/utils/isLoggedInUser"
|
||||||
|
|
||||||
import { getHotelAlertsForBookingDates } from "../utils"
|
import { filterOverlappingDates } from "../utils"
|
||||||
|
|
||||||
import styles from "./index.module.css"
|
import styles from "./index.module.css"
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ export default async function MyStay(props: {
|
|||||||
} satisfies SafeUser)
|
} satisfies SafeUser)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
hotel.specialAlerts = getHotelAlertsForBookingDates(
|
hotel.specialAlerts = filterOverlappingDates(
|
||||||
hotel.specialAlerts,
|
hotel.specialAlerts,
|
||||||
dt(fromDate),
|
dt(fromDate),
|
||||||
dt(toDate)
|
dt(toDate)
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useState } from "react"
|
|
||||||
import { Button as ButtonRAC } from "react-aria-components"
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { FacilityToIcon } from "@scandic-hotels/design-system/FacilityToIcon"
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
||||||
|
|
||||||
import HotelDetailsSidePeek from "@/components/SidePeeks/HotelDetailsSidePeek"
|
|
||||||
|
|
||||||
import styles from "./hotelDescription.module.css"
|
|
||||||
|
|
||||||
import type {
|
|
||||||
AdditionalData,
|
|
||||||
Hotel,
|
|
||||||
Restaurant,
|
|
||||||
} from "@scandic-hotels/trpc/types/hotel"
|
|
||||||
|
|
||||||
export default function HotelDescription({
|
|
||||||
description,
|
|
||||||
hotel,
|
|
||||||
sortedFacilities,
|
|
||||||
restaurants,
|
|
||||||
additionalData,
|
|
||||||
}: {
|
|
||||||
description?: string
|
|
||||||
hotel: Hotel & { url: string | null }
|
|
||||||
sortedFacilities: Hotel["detailedFacilities"]
|
|
||||||
restaurants: Restaurant[]
|
|
||||||
additionalData: AdditionalData | undefined
|
|
||||||
}) {
|
|
||||||
const intl = useIntl()
|
|
||||||
|
|
||||||
const [expanded, setExpanded] = useState(false)
|
|
||||||
|
|
||||||
const handleToggle = () => {
|
|
||||||
setExpanded((prev) => !prev)
|
|
||||||
}
|
|
||||||
|
|
||||||
const textShowMore = intl.formatMessage({
|
|
||||||
defaultMessage: "Show more",
|
|
||||||
})
|
|
||||||
|
|
||||||
const textShowLess = intl.formatMessage({
|
|
||||||
defaultMessage: "Show less",
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.descriptionWrapper}>
|
|
||||||
<div className={styles.facilityList}>
|
|
||||||
{sortedFacilities?.map((facility) => (
|
|
||||||
<div className={styles.facilitiesItem} key={facility.id}>
|
|
||||||
<FacilityToIcon id={facility.id} color="Icon/Default" />
|
|
||||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
|
||||||
<p>{facility.name}</p>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
|
||||||
<p
|
|
||||||
className={`${styles.hotelDescription} ${
|
|
||||||
expanded ? styles.expanded : styles.collapsed
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{description}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="Link/md">
|
|
||||||
<ButtonRAC className={styles.showMoreButton} onPress={handleToggle}>
|
|
||||||
{expanded ? textShowLess : textShowMore}
|
|
||||||
</ButtonRAC>
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{expanded && (
|
|
||||||
<div className={styles.expandedContent}>
|
|
||||||
<HotelDetailsSidePeek
|
|
||||||
hotel={hotel}
|
|
||||||
restaurants={restaurants}
|
|
||||||
additionalHotelData={additionalData}
|
|
||||||
triggerLabel={intl.formatMessage({
|
|
||||||
defaultMessage: "See all amenities",
|
|
||||||
})}
|
|
||||||
buttonVariant="primary"
|
|
||||||
wrapping={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,17 @@
|
|||||||
import { cookies } from "next/headers"
|
import { cookies } from "next/headers"
|
||||||
|
|
||||||
|
import { dt } from "@scandic-hotels/common/dt"
|
||||||
|
import { HotelInfoCard } from "@scandic-hotels/design-system/HotelInfoCard"
|
||||||
|
|
||||||
import { FamilyAndFriendsCodes } from "@/constants/booking"
|
import { FamilyAndFriendsCodes } from "@/constants/booking"
|
||||||
|
|
||||||
import { HotelInfoCard } from "@/components/HotelReservation/SelectRate/HotelInfoCard"
|
|
||||||
import { RoomsContainer } from "@/components/HotelReservation/SelectRate/RoomsContainer"
|
import { RoomsContainer } from "@/components/HotelReservation/SelectRate/RoomsContainer"
|
||||||
|
import HotelDetailsSidePeek from "@/components/SidePeeks/HotelDetailsSidePeek"
|
||||||
|
import { getIntl } from "@/i18n"
|
||||||
|
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||||
|
|
||||||
import FnFNotAllowedAlert from "../FnFNotAllowedAlert/FnFNotAllowedAlert"
|
import FnFNotAllowedAlert from "../FnFNotAllowedAlert/FnFNotAllowedAlert"
|
||||||
|
import { hasOverlappingDates } from "../utils"
|
||||||
import AvailabilityError from "./AvailabilityError"
|
import AvailabilityError from "./AvailabilityError"
|
||||||
import Tracking from "./Tracking"
|
import Tracking from "./Tracking"
|
||||||
|
|
||||||
@@ -20,6 +26,7 @@ export default async function SelectRatePage({
|
|||||||
hotelData: NonNullable<RouterOutput["hotel"]["get"]>
|
hotelData: NonNullable<RouterOutput["hotel"]["get"]>
|
||||||
booking: SelectRateBooking
|
booking: SelectRateBooking
|
||||||
}) {
|
}) {
|
||||||
|
const intl = await getIntl()
|
||||||
const bookingCode = booking.bookingCode
|
const bookingCode = booking.bookingCode
|
||||||
|
|
||||||
let isInValidFNF = false
|
let isInValidFNF = false
|
||||||
@@ -27,13 +34,51 @@ export default async function SelectRatePage({
|
|||||||
const cookieStore = await cookies()
|
const cookieStore = await cookies()
|
||||||
isInValidFNF = cookieStore.get("sc")?.value !== "1"
|
isInValidFNF = cookieStore.get("sc")?.value !== "1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const validAlerts = hotelData.hotel.specialAlerts.filter((alert) =>
|
||||||
|
hasOverlappingDates(alert, dt(booking.fromDate), dt(booking.toDate))
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HotelInfoCard
|
<HotelInfoCard
|
||||||
hotel={{ ...hotelData.hotel, url: hotelData.url }}
|
hotel={{
|
||||||
restaurants={hotelData.restaurants}
|
id: hotelData.hotel.id,
|
||||||
additionalData={hotelData.additionalData}
|
name: hotelData.hotel.name,
|
||||||
booking={booking}
|
url: hotelData.url,
|
||||||
|
ratings: hotelData.hotel.ratings,
|
||||||
|
}}
|
||||||
|
address={{
|
||||||
|
streetAddress: hotelData.hotel.address.streetAddress,
|
||||||
|
city: hotelData.hotel.address.city,
|
||||||
|
kilometersToCentre: hotelData.hotel.location.distanceToCentre / 1000,
|
||||||
|
}}
|
||||||
|
galleryImages={mapApiImagesToGalleryImages(
|
||||||
|
hotelData.hotel.galleryImages
|
||||||
|
)}
|
||||||
|
description={
|
||||||
|
hotelData.hotel.hotelContent.texts.descriptions?.medium ?? ""
|
||||||
|
}
|
||||||
|
alerts={validAlerts.map((alert) => ({
|
||||||
|
...alert,
|
||||||
|
heading: alert.heading ?? "",
|
||||||
|
text: alert.text ?? "",
|
||||||
|
}))}
|
||||||
|
facilities={hotelData.hotel.detailedFacilities}
|
||||||
|
slot={
|
||||||
|
<HotelDetailsSidePeek
|
||||||
|
hotel={{
|
||||||
|
...hotelData.hotel,
|
||||||
|
url: hotelData.url,
|
||||||
|
}}
|
||||||
|
restaurants={hotelData.restaurants}
|
||||||
|
additionalHotelData={hotelData.additionalData}
|
||||||
|
triggerLabel={intl.formatMessage({
|
||||||
|
defaultMessage: "See all amenities",
|
||||||
|
})}
|
||||||
|
buttonVariant="primary"
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isInValidFNF ? (
|
{isInValidFNF ? (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest"
|
|||||||
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
|
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
|
||||||
import { dt } from "@scandic-hotels/common/dt"
|
import { dt } from "@scandic-hotels/common/dt"
|
||||||
|
|
||||||
import { getHotelAlertsForBookingDates } from "./index"
|
import { filterOverlappingDates } from "./index"
|
||||||
|
|
||||||
import type { specialAlertsSchema } from "@scandic-hotels/trpc/routers/hotels/schemas/hotel/specialAlerts"
|
import type { specialAlertsSchema } from "@scandic-hotels/trpc/routers/hotels/schemas/hotel/specialAlerts"
|
||||||
import type { z } from "zod"
|
import type { z } from "zod"
|
||||||
@@ -23,11 +23,11 @@ function makeAlert(start: string, end: string): Alert {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("getHotelAlertsForBookingDates", () => {
|
describe("filterOverlappingDates", () => {
|
||||||
const alert = makeAlert("2025-09-01", "2025-09-10")
|
const alert = makeAlert("2025-09-01", "2025-09-10")
|
||||||
|
|
||||||
it("shows alert if booking starts inside alert", () => {
|
it("shows alert if booking starts inside alert", () => {
|
||||||
const result = getHotelAlertsForBookingDates(
|
const result = filterOverlappingDates(
|
||||||
[alert],
|
[alert],
|
||||||
dt("2025-09-05"),
|
dt("2025-09-05"),
|
||||||
dt("2025-09-12")
|
dt("2025-09-12")
|
||||||
@@ -36,7 +36,7 @@ describe("getHotelAlertsForBookingDates", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("shows alert if booking ends inside alert", () => {
|
it("shows alert if booking ends inside alert", () => {
|
||||||
const result = getHotelAlertsForBookingDates(
|
const result = filterOverlappingDates(
|
||||||
[alert],
|
[alert],
|
||||||
dt("2025-08-28"),
|
dt("2025-08-28"),
|
||||||
dt("2025-09-05")
|
dt("2025-09-05")
|
||||||
@@ -45,7 +45,7 @@ describe("getHotelAlertsForBookingDates", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("shows alert if booking fully contains alert", () => {
|
it("shows alert if booking fully contains alert", () => {
|
||||||
const result = getHotelAlertsForBookingDates(
|
const result = filterOverlappingDates(
|
||||||
[alert],
|
[alert],
|
||||||
dt("2025-08-28"),
|
dt("2025-08-28"),
|
||||||
dt("2025-09-15")
|
dt("2025-09-15")
|
||||||
@@ -54,7 +54,7 @@ describe("getHotelAlertsForBookingDates", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("shows alert if alert fully contains booking", () => {
|
it("shows alert if alert fully contains booking", () => {
|
||||||
const result = getHotelAlertsForBookingDates(
|
const result = filterOverlappingDates(
|
||||||
[alert],
|
[alert],
|
||||||
dt("2025-09-03"),
|
dt("2025-09-03"),
|
||||||
dt("2025-09-05")
|
dt("2025-09-05")
|
||||||
@@ -63,7 +63,7 @@ describe("getHotelAlertsForBookingDates", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("does not show alert if no overlap", () => {
|
it("does not show alert if no overlap", () => {
|
||||||
const result = getHotelAlertsForBookingDates(
|
const result = filterOverlappingDates(
|
||||||
[alert],
|
[alert],
|
||||||
dt("2025-08-01"),
|
dt("2025-08-01"),
|
||||||
dt("2025-08-05")
|
dt("2025-08-05")
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { ChildBedMapEnum } from "@scandic-hotels/trpc/enums/childBedMapEnum"
|
|||||||
import { ChildBedTypeEnum } from "@scandic-hotels/trpc/enums/childBedTypeEnum"
|
import { ChildBedTypeEnum } from "@scandic-hotels/trpc/enums/childBedTypeEnum"
|
||||||
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
|
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
|
||||||
|
|
||||||
import type { specialAlertsSchema } from "@scandic-hotels/trpc/routers/hotels/schemas/hotel/specialAlerts"
|
|
||||||
import type { Package, Packages } from "@scandic-hotels/trpc/types/packages"
|
import type { Package, Packages } from "@scandic-hotels/trpc/types/packages"
|
||||||
import type { JSX } from "react"
|
import type { JSX } from "react"
|
||||||
|
|
||||||
@@ -88,41 +87,57 @@ export function calculateVat(priceInclVat: number, vat: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getHotelAlertsForBookingDates(
|
export function filterOverlappingDates<
|
||||||
specialAlerts: Zod.infer<typeof specialAlertsSchema>,
|
T extends {
|
||||||
|
startDate: Date | Dayjs | string | undefined | null
|
||||||
|
endDate: Date | Dayjs | string | undefined | null
|
||||||
|
},
|
||||||
|
>(dateRangeItems: T[], fromDate: Date | Dayjs, toDate: Date | Dayjs) {
|
||||||
|
const startDate = dt(fromDate)
|
||||||
|
const endDate = dt(toDate)
|
||||||
|
|
||||||
|
return dateRangeItems.filter((item) =>
|
||||||
|
hasOverlappingDates(item, startDate, endDate)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasOverlappingDates(
|
||||||
|
dateRangeItem: {
|
||||||
|
startDate: Date | Dayjs | string | undefined | null
|
||||||
|
endDate: Date | Dayjs | string | undefined | null
|
||||||
|
},
|
||||||
fromDate: Date | Dayjs,
|
fromDate: Date | Dayjs,
|
||||||
toDate: Date | Dayjs
|
toDate: Date | Dayjs
|
||||||
) {
|
) {
|
||||||
return specialAlerts.filter((alert) => {
|
const startDate = dt(fromDate)
|
||||||
if (alert.endDate && alert.startDate) {
|
const endDate = dt(toDate)
|
||||||
const alertStartDate = dt(alert.startDate)
|
|
||||||
const alertEndDate = dt(alert.endDate)
|
|
||||||
const bookingStart = dt(fromDate)
|
|
||||||
const bookingEnd = dt(toDate)
|
|
||||||
|
|
||||||
const fromDateIsBetweenAlertDates = bookingStart.isBetween(
|
if (dateRangeItem.endDate && dateRangeItem.startDate) {
|
||||||
alertStartDate,
|
const itemStartDate = dt(dateRangeItem.startDate)
|
||||||
alertEndDate,
|
const itemEndDate = dt(dateRangeItem.endDate)
|
||||||
"date",
|
|
||||||
"[]"
|
|
||||||
)
|
|
||||||
const toDateIsBetweenAlertDates = bookingEnd.isBetween(
|
|
||||||
alertStartDate,
|
|
||||||
alertEndDate,
|
|
||||||
"date",
|
|
||||||
"[]"
|
|
||||||
)
|
|
||||||
|
|
||||||
const bookingFullyContainsAlert =
|
const fromDateIsBetweenItemDates = startDate.isBetween(
|
||||||
bookingStart.isSameOrBefore(alertStartDate, "date") &&
|
itemStartDate,
|
||||||
bookingEnd.isSameOrAfter(alertEndDate, "date")
|
itemEndDate,
|
||||||
|
"date",
|
||||||
|
"[]"
|
||||||
|
)
|
||||||
|
const toDateIsBetweenItemDates = endDate.isBetween(
|
||||||
|
itemStartDate,
|
||||||
|
itemEndDate,
|
||||||
|
"date",
|
||||||
|
"[]"
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
const itemFullyContained =
|
||||||
fromDateIsBetweenAlertDates ||
|
startDate.isSameOrBefore(itemStartDate, "date") &&
|
||||||
toDateIsBetweenAlertDates ||
|
endDate.isSameOrAfter(itemEndDate, "date")
|
||||||
bookingFullyContainsAlert
|
|
||||||
)
|
return (
|
||||||
}
|
fromDateIsBetweenItemDates ||
|
||||||
return true
|
toDateIsBetweenItemDates ||
|
||||||
})
|
itemFullyContained
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Button as ButtonRAC } from 'react-aria-components'
|
||||||
|
import { useIntl } from 'react-intl'
|
||||||
|
|
||||||
|
import { FacilityToIcon } from '../..//FacilityToIcon'
|
||||||
|
import { Typography } from '../../Typography'
|
||||||
|
|
||||||
|
import styles from './hotelDescription.module.css'
|
||||||
|
import { FacilityEnum } from '@scandic-hotels/common/constants/facilities'
|
||||||
|
|
||||||
|
export default function HotelDescription({
|
||||||
|
description,
|
||||||
|
facilities,
|
||||||
|
}: {
|
||||||
|
description?: string
|
||||||
|
facilities: {
|
||||||
|
id: FacilityEnum
|
||||||
|
name: string
|
||||||
|
}[]
|
||||||
|
}) {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
setExpanded((prev) => !prev)
|
||||||
|
}
|
||||||
|
|
||||||
|
const textShowMore = intl.formatMessage({
|
||||||
|
defaultMessage: 'Show more',
|
||||||
|
})
|
||||||
|
|
||||||
|
const textShowLess = intl.formatMessage({
|
||||||
|
defaultMessage: 'Show less',
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.descriptionWrapper}>
|
||||||
|
<div className={styles.facilityList}>
|
||||||
|
{facilities?.map((facility) => (
|
||||||
|
<div className={styles.facilitiesItem} key={facility.id}>
|
||||||
|
<FacilityToIcon id={facility.id} color="Icon/Default" />
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
<p>{facility.name}</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p
|
||||||
|
className={`${styles.hotelDescription} ${
|
||||||
|
expanded ? styles.expanded : styles.collapsed
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Link/md">
|
||||||
|
<ButtonRAC className={styles.showMoreButton} onPress={handleToggle}>
|
||||||
|
{expanded ? textShowLess : textShowMore}
|
||||||
|
</ButtonRAC>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||||
|
|
||||||
|
import { HotelInfoCard } from './index'
|
||||||
|
import { FacilityEnum } from '@scandic-hotels/common/constants/facilities'
|
||||||
|
import { AlertTypeEnum } from '@scandic-hotels/common/constants/alert'
|
||||||
|
import { Button } from '../Button'
|
||||||
|
import { fn } from 'storybook/test'
|
||||||
|
import { MaterialIcon } from '../Icons/MaterialIcon'
|
||||||
|
const meta: Meta<typeof HotelInfoCard> = {
|
||||||
|
title: 'Components/HotelInfoCard',
|
||||||
|
component: HotelInfoCard,
|
||||||
|
argTypes: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof HotelInfoCard>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
argTypes: {
|
||||||
|
alerts: {
|
||||||
|
control: 'select',
|
||||||
|
options: ['none', 'info', 'warning', 'alarm', 'success'],
|
||||||
|
mapping: {
|
||||||
|
none: [],
|
||||||
|
info: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
heading: 'Hot dog alert',
|
||||||
|
text: `They are handing out free hot dogs available in the square outside the hotel.`,
|
||||||
|
type: AlertTypeEnum.Info,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
warning: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
heading: 'Construction work',
|
||||||
|
text: `There is construction work going on outside the hotel. Expect some noise during daytime.`,
|
||||||
|
type: AlertTypeEnum.Warning,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
success: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
heading: 'Free breakfast',
|
||||||
|
text: `We are now serving free breakfast in the lobby between 7-10am.`,
|
||||||
|
type: AlertTypeEnum.Success,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
alarm: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
heading: 'Fire alarm',
|
||||||
|
text: `The fire alarm is activated. Please evacuate the building immediately using the nearest exit.`,
|
||||||
|
type: AlertTypeEnum.Alarm,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
slot: {
|
||||||
|
control: 'select',
|
||||||
|
description: 'A slot where you can inject components',
|
||||||
|
options: ['none', 'button'],
|
||||||
|
table: {
|
||||||
|
defaultValue: { summary: 'button' },
|
||||||
|
},
|
||||||
|
mapping: {
|
||||||
|
none: null,
|
||||||
|
button: (
|
||||||
|
<Button
|
||||||
|
variant={'Text'}
|
||||||
|
typography={'Body/Supporting text (caption)/smBold'}
|
||||||
|
onPress={() => fn()}
|
||||||
|
>
|
||||||
|
Read more <MaterialIcon icon="chevron_right" />
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
hotel: {
|
||||||
|
id: '1',
|
||||||
|
name: 'Grand Hotel Budapest',
|
||||||
|
url: 'https://www.scandichotels.com/en/hello',
|
||||||
|
ratings: {
|
||||||
|
tripAdvisor: { rating: 4.5 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
city: 'Budapest',
|
||||||
|
kilometersToCentre: 0.5,
|
||||||
|
streetAddress: '1 Main St',
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
"Escape to the crown jewel of the Republic of Zubrowka, where timeless luxury awaits atop our breathtaking mountain sanctuary. The Grand Budapest Hotel stands as Europe's most distinguished retreat, a rose-colored palace that has welcomed discerning guests since the golden age of travel.",
|
||||||
|
facilities: [
|
||||||
|
{ id: FacilityEnum.AirConAirCooling, name: 'Air Conditioning' },
|
||||||
|
{ id: FacilityEnum.FoodDrinks247, name: 'Food & Drinks 24/7' },
|
||||||
|
{ id: FacilityEnum.KayaksForLoan, name: 'Kayaks for Loan' },
|
||||||
|
],
|
||||||
|
galleryImages: [
|
||||||
|
{
|
||||||
|
src: './img/GrandHotelBudapest.png',
|
||||||
|
alt: 'Grand Hotel Budapest',
|
||||||
|
smallSrc: './img/GrandHotelBudapest.png',
|
||||||
|
caption: 'Grand Hotel Budapest',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: './img/img1.png',
|
||||||
|
alt: 'Image 1',
|
||||||
|
smallSrc: './img/img1.png',
|
||||||
|
caption: 'Image 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: './img/img2.png',
|
||||||
|
alt: 'Image 2',
|
||||||
|
smallSrc: './img/img2.png',
|
||||||
|
caption: 'Image 2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
alerts: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithSlot: Story = {
|
||||||
|
argTypes: {},
|
||||||
|
args: {
|
||||||
|
...Default.args,
|
||||||
|
slot: Default.argTypes?.slot?.mapping?.button,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithAlert: Story = {
|
||||||
|
argTypes: {},
|
||||||
|
args: {
|
||||||
|
...Default.args,
|
||||||
|
alerts: Default.argTypes?.alerts?.mapping?.info,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -54,6 +54,16 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.slotWrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@media screen and (min-width: 1367px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.hotelAlert {
|
.hotelAlert {
|
||||||
max-width: var(--max-width-page);
|
max-width: var(--max-width-page);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@@ -1,58 +1,57 @@
|
|||||||
import { dt } from "@scandic-hotels/common/dt"
|
'use client'
|
||||||
import { getSingleDecimal } from "@scandic-hotels/common/utils/numberFormatting"
|
|
||||||
import { Alert } from "@scandic-hotels/design-system/Alert"
|
|
||||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
|
||||||
import { FacilityToIcon } from "@scandic-hotels/design-system/FacilityToIcon"
|
|
||||||
import ImageGallery from "@scandic-hotels/design-system/ImageGallery"
|
|
||||||
import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer"
|
|
||||||
import { TripAdvisorChip } from "@scandic-hotels/design-system/TripAdvisorChip"
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
||||||
|
|
||||||
import HotelDetailsSidePeek from "@/components/SidePeeks/HotelDetailsSidePeek"
|
import { getSingleDecimal } from '@scandic-hotels/common/utils/numberFormatting'
|
||||||
import { getIntl } from "@/i18n"
|
import { Alert } from '../Alert'
|
||||||
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
import { Divider } from '../Divider'
|
||||||
|
import { FacilityToIcon } from '../FacilityToIcon'
|
||||||
|
import ImageGallery, { GalleryImage } from '../ImageGallery'
|
||||||
|
import SkeletonShimmer from '../SkeletonShimmer'
|
||||||
|
import { TripAdvisorChip } from '../TripAdvisorChip'
|
||||||
|
import { Typography } from '../Typography'
|
||||||
|
|
||||||
import { getHotelAlertsForBookingDates } from "../../utils"
|
import HotelDescription from './HotelDescription'
|
||||||
import HotelDescription from "./HotelDescription"
|
|
||||||
|
|
||||||
import styles from "./hotelInfoCard.module.css"
|
import styles from './hotelInfoCard.module.css'
|
||||||
|
import { useIntl } from 'react-intl'
|
||||||
import type {
|
import { AlertTypeEnum } from '@scandic-hotels/common/constants/alert'
|
||||||
AdditionalData,
|
import { FacilityEnum } from '@scandic-hotels/common/constants/facilities'
|
||||||
Hotel,
|
|
||||||
Restaurant,
|
|
||||||
} from "@scandic-hotels/trpc/types/hotel"
|
|
||||||
|
|
||||||
import type { SelectRateBooking } from "@/types/components/hotelReservation/selectRate/selectRate"
|
|
||||||
|
|
||||||
export type HotelInfoCardProps = {
|
export type HotelInfoCardProps = {
|
||||||
booking: SelectRateBooking
|
hotel: {
|
||||||
hotel: Hotel & { url: string | null }
|
id: string
|
||||||
restaurants: Restaurant[]
|
name: string
|
||||||
additionalData: AdditionalData | undefined
|
url: string | null
|
||||||
|
ratings?: {
|
||||||
|
tripAdvisor?: { rating: number }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
description: string
|
||||||
|
address: {
|
||||||
|
streetAddress: string
|
||||||
|
city: string
|
||||||
|
kilometersToCentre: number
|
||||||
|
}
|
||||||
|
galleryImages: GalleryImage[]
|
||||||
|
alerts: SpecialAlertProps['alert'][]
|
||||||
|
facilities: {
|
||||||
|
id: FacilityEnum
|
||||||
|
name: string
|
||||||
|
}[]
|
||||||
|
slot?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function HotelInfoCard({
|
export function HotelInfoCard({
|
||||||
booking,
|
|
||||||
hotel,
|
hotel,
|
||||||
restaurants,
|
galleryImages,
|
||||||
additionalData,
|
address,
|
||||||
|
facilities,
|
||||||
|
alerts,
|
||||||
|
description,
|
||||||
|
slot,
|
||||||
}: HotelInfoCardProps) {
|
}: HotelInfoCardProps) {
|
||||||
const intl = await getIntl()
|
const intl = useIntl()
|
||||||
|
|
||||||
const sortedFacilities = hotel.detailedFacilities
|
const firstFacilities = facilities.slice(0, 5)
|
||||||
.sort((a, b) => b.sortOrder - a.sortOrder)
|
|
||||||
.slice(0, 5)
|
|
||||||
|
|
||||||
const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || [])
|
|
||||||
|
|
||||||
const bookingFromDate = dt(booking.fromDate)
|
|
||||||
const bookingToDate = dt(booking.toDate)
|
|
||||||
const specialAlerts = getHotelAlertsForBookingDates(
|
|
||||||
hotel.specialAlerts,
|
|
||||||
bookingFromDate,
|
|
||||||
bookingToDate
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className={styles.container}>
|
<article className={styles.container}>
|
||||||
@@ -74,37 +73,31 @@ export async function HotelInfoCard({
|
|||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{
|
{
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
"{address}, {city} ∙ {distanceToCityCenterInKm} km to city center",
|
'{address}, {city} ∙ {distanceToCityCenterInKm} km to city center',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
address: hotel.address.streetAddress,
|
address: address.streetAddress,
|
||||||
city: hotel.address.city,
|
city: address.city,
|
||||||
distanceToCityCenterInKm: getSingleDecimal(
|
distanceToCityCenterInKm: getSingleDecimal(
|
||||||
hotel.location.distanceToCentre / 1000
|
address.kilometersToCentre
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
<p className={styles.hotelDescription}>
|
<p className={styles.hotelDescription}>{description}</p>
|
||||||
{hotel.hotelContent.texts.descriptions?.medium}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<HotelDescription
|
<HotelDescription
|
||||||
key={hotel.operaId}
|
description={description}
|
||||||
description={hotel.hotelContent.texts.descriptions?.medium}
|
facilities={firstFacilities}
|
||||||
hotel={hotel}
|
|
||||||
restaurants={restaurants}
|
|
||||||
additionalData={additionalData}
|
|
||||||
sortedFacilities={sortedFacilities}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Divider variant="vertical" />
|
<Divider variant="vertical" />
|
||||||
<div className={styles.facilities}>
|
<div className={styles.facilities}>
|
||||||
<div className={styles.facilityList}>
|
<div className={styles.facilityList}>
|
||||||
{sortedFacilities?.map((facility) => (
|
{firstFacilities?.map((facility) => (
|
||||||
<div className={styles.facilitiesItem} key={facility.id}>
|
<div className={styles.facilitiesItem} key={facility.id}>
|
||||||
<FacilityToIcon id={facility.id} color="Icon/Default" />
|
<FacilityToIcon id={facility.id} color="Icon/Default" />
|
||||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
@@ -113,26 +106,22 @@ export async function HotelInfoCard({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<HotelDetailsSidePeek
|
{slot}
|
||||||
hotel={hotel}
|
|
||||||
restaurants={restaurants}
|
|
||||||
additionalHotelData={additionalData}
|
|
||||||
triggerLabel={intl.formatMessage({
|
|
||||||
defaultMessage: "See all amenities",
|
|
||||||
})}
|
|
||||||
buttonVariant="primary"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{specialAlerts.map((alert) => (
|
<div className={styles.slotWrapper}>{slot}</div>
|
||||||
|
{alerts.map((alert) => (
|
||||||
<SpecialAlert key={alert.id} alert={alert} />
|
<SpecialAlert key={alert.id} alert={alert} />
|
||||||
))}
|
))}
|
||||||
</article>
|
</article>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function SpecialAlert({ alert }: { alert: Hotel["specialAlerts"][number] }) {
|
type SpecialAlertProps = {
|
||||||
|
alert: { id: string; type: AlertTypeEnum; heading: string; text: string }
|
||||||
|
}
|
||||||
|
function SpecialAlert({ alert }: SpecialAlertProps) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.hotelAlert} key={`wrapper_${alert.id}`}>
|
<div className={styles.hotelAlert} key={`wrapper_${alert.id}`}>
|
||||||
<Alert
|
<Alert
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
"./Form/ErrorMessage": "./lib/components/Form/ErrorMessage/index.tsx",
|
"./Form/ErrorMessage": "./lib/components/Form/ErrorMessage/index.tsx",
|
||||||
"./Form/Phone": "./lib/components/Form/Phone/index.tsx",
|
"./Form/Phone": "./lib/components/Form/Phone/index.tsx",
|
||||||
"./Form/RadioCard": "./lib/components/Form/RadioCard/index.tsx",
|
"./Form/RadioCard": "./lib/components/Form/RadioCard/index.tsx",
|
||||||
|
"./HotelInfoCard": "./lib/components/HotelInfoCard/index.tsx",
|
||||||
"./HotelCard": "./lib/components/HotelCard/index.tsx",
|
"./HotelCard": "./lib/components/HotelCard/index.tsx",
|
||||||
"./HotelCard/HotelCardDialogImage": "./lib/components/HotelCard/HotelCardDialogImage/index.tsx",
|
"./HotelCard/HotelCardDialogImage": "./lib/components/HotelCard/HotelCardDialogImage/index.tsx",
|
||||||
"./HotelCard/HotelPointsRow": "./lib/components/HotelCard/HotelPointsRow/index.tsx",
|
"./HotelCard/HotelPointsRow": "./lib/components/HotelCard/HotelPointsRow/index.tsx",
|
||||||
|
|||||||
BIN
packages/design-system/public/img/GrandHotelBudapest.png
Normal file
BIN
packages/design-system/public/img/GrandHotelBudapest.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
Reference in New Issue
Block a user