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:
@@ -16,7 +16,7 @@ import SidePanel from "@/components/HotelReservation/SidePanel"
|
||||
import { getIntl } from "@/i18n"
|
||||
import BookingConfirmationProvider from "@/providers/BookingConfirmationProvider"
|
||||
|
||||
import { getHotelAlertsForBookingDates } from "../utils"
|
||||
import { filterOverlappingDates } from "../utils"
|
||||
import Confirmation from "./Confirmation"
|
||||
import Tracking from "./Tracking"
|
||||
import { mapRoomState } from "./utils"
|
||||
@@ -79,7 +79,7 @@ export default async function BookingConfirmation({
|
||||
<PaymentDetails />
|
||||
<Divider color="Border/Divider/Subtle" />
|
||||
<HotelDetails hotel={hotel} />
|
||||
{getHotelAlertsForBookingDates(
|
||||
{filterOverlappingDates(
|
||||
hotel.specialAlerts,
|
||||
dt(booking.checkInDate),
|
||||
dt(booking.checkOutDate)
|
||||
|
||||
@@ -39,7 +39,7 @@ import { getIntl } from "@/i18n"
|
||||
import MyStayProvider from "@/providers/MyStay"
|
||||
import { isLoggedInUser } from "@/utils/isLoggedInUser"
|
||||
|
||||
import { getHotelAlertsForBookingDates } from "../utils"
|
||||
import { filterOverlappingDates } from "../utils"
|
||||
|
||||
import styles from "./index.module.css"
|
||||
|
||||
@@ -202,7 +202,7 @@ export default async function MyStay(props: {
|
||||
} satisfies SafeUser)
|
||||
: null
|
||||
|
||||
hotel.specialAlerts = getHotelAlertsForBookingDates(
|
||||
hotel.specialAlerts = filterOverlappingDates(
|
||||
hotel.specialAlerts,
|
||||
dt(fromDate),
|
||||
dt(toDate)
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
.hotelDescription {
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.descriptionWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
margin: var(--Space-x15) 0;
|
||||
}
|
||||
|
||||
.expanded {
|
||||
display: block;
|
||||
max-height: none;
|
||||
margin: var(--Space-x15) 0;
|
||||
}
|
||||
|
||||
.expandedContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-top: var(--Space-x2);
|
||||
}
|
||||
|
||||
.description {
|
||||
display: flex;
|
||||
gap: var(--Space-x025);
|
||||
}
|
||||
|
||||
.showMoreButton {
|
||||
display: flex;
|
||||
background-color: transparent;
|
||||
border-width: 0;
|
||||
padding: 0;
|
||||
color: var(--Text-Interactive-Secondary);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--Text-Interactive-Secondary-Hover);
|
||||
}
|
||||
}
|
||||
|
||||
.facilities {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x15);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.facilityList {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--Space-x15);
|
||||
}
|
||||
|
||||
.facilitiesItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Space-x1);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.descriptionWrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -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,134 +0,0 @@
|
||||
.container {
|
||||
background-color: var(--Base-Surface-Subtle-Normal);
|
||||
padding: var(--Space-x3) 0;
|
||||
}
|
||||
|
||||
.hotelName {
|
||||
color: var(--Text-Heading);
|
||||
}
|
||||
|
||||
.hotelAddress {
|
||||
color: var(--Text-Tertiary);
|
||||
}
|
||||
.wrapper {
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
max-width: var(--max-width-page);
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x2);
|
||||
}
|
||||
.hotelDescription {
|
||||
display: none;
|
||||
}
|
||||
.imageWrapper {
|
||||
position: relative;
|
||||
height: 200px;
|
||||
width: 100%;
|
||||
border-radius: var(--Corner-radius-md);
|
||||
}
|
||||
|
||||
.hotelContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hotelInformation {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x1);
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hotelAddressDescription {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x15);
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.facilities {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hotelAlert {
|
||||
max-width: var(--max-width-page);
|
||||
margin: 0 auto;
|
||||
padding-top: var(--Space-x15);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.container {
|
||||
padding: var(--Space-x4) 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.container {
|
||||
padding: var(--Space-x4) var(--Space-x5);
|
||||
}
|
||||
.hotelDescription {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.facilities {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--Space-x3) 0 var(--Space-x025);
|
||||
gap: var(--Space-x15);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.facilityList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--Space-x1);
|
||||
}
|
||||
|
||||
.facilitiesItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Space-x1);
|
||||
}
|
||||
|
||||
.imageWrapper {
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.hotelContent {
|
||||
flex-direction: row;
|
||||
gap: var(--Space-x6);
|
||||
}
|
||||
|
||||
.hotelInformation {
|
||||
padding-right: var(--Space-x3);
|
||||
width: min(607px, 100%);
|
||||
align-items: normal;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.hotelAddressDescription {
|
||||
align-items: normal;
|
||||
text-align: left;
|
||||
gap: var(--Space-x2);
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
gap: var(--Space-x3);
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.facilityTitle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.imageWrapper {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
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 { getIntl } from "@/i18n"
|
||||
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||
|
||||
import { getHotelAlertsForBookingDates } from "../../utils"
|
||||
import HotelDescription from "./HotelDescription"
|
||||
|
||||
import styles from "./hotelInfoCard.module.css"
|
||||
|
||||
import type {
|
||||
AdditionalData,
|
||||
Hotel,
|
||||
Restaurant,
|
||||
} from "@scandic-hotels/trpc/types/hotel"
|
||||
|
||||
import type { SelectRateBooking } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
|
||||
export type HotelInfoCardProps = {
|
||||
booking: SelectRateBooking
|
||||
hotel: Hotel & { url: string | null }
|
||||
restaurants: Restaurant[]
|
||||
additionalData: AdditionalData | undefined
|
||||
}
|
||||
|
||||
export async function HotelInfoCard({
|
||||
booking,
|
||||
hotel,
|
||||
restaurants,
|
||||
additionalData,
|
||||
}: HotelInfoCardProps) {
|
||||
const intl = await getIntl()
|
||||
|
||||
const sortedFacilities = hotel.detailedFacilities
|
||||
.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 (
|
||||
<article className={styles.container}>
|
||||
<section className={styles.wrapper}>
|
||||
<div className={styles.imageWrapper}>
|
||||
<ImageGallery title={hotel.name} images={galleryImages} fill />
|
||||
{hotel.ratings?.tripAdvisor && (
|
||||
<TripAdvisorChip rating={hotel.ratings.tripAdvisor.rating} />
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.hotelContent}>
|
||||
<div className={styles.hotelInformation}>
|
||||
<Typography variant="Title/md">
|
||||
<h1 className={styles.hotelName}>{hotel.name}</h1>
|
||||
</Typography>
|
||||
<div className={styles.hotelAddressDescription}>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p className={styles.hotelAddress}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage:
|
||||
"{address}, {city} ∙ {distanceToCityCenterInKm} km to city center",
|
||||
},
|
||||
{
|
||||
address: hotel.address.streetAddress,
|
||||
city: hotel.address.city,
|
||||
distanceToCityCenterInKm: getSingleDecimal(
|
||||
hotel.location.distanceToCentre / 1000
|
||||
),
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p className={styles.hotelDescription}>
|
||||
{hotel.hotelContent.texts.descriptions?.medium}
|
||||
</p>
|
||||
</Typography>
|
||||
<HotelDescription
|
||||
key={hotel.operaId}
|
||||
description={hotel.hotelContent.texts.descriptions?.medium}
|
||||
hotel={hotel}
|
||||
restaurants={restaurants}
|
||||
additionalData={additionalData}
|
||||
sortedFacilities={sortedFacilities}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Divider variant="vertical" />
|
||||
<div className={styles.facilities}>
|
||||
<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>
|
||||
<HotelDetailsSidePeek
|
||||
hotel={hotel}
|
||||
restaurants={restaurants}
|
||||
additionalHotelData={additionalData}
|
||||
triggerLabel={intl.formatMessage({
|
||||
defaultMessage: "See all amenities",
|
||||
})}
|
||||
buttonVariant="primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{specialAlerts.map((alert) => (
|
||||
<SpecialAlert key={alert.id} alert={alert} />
|
||||
))}
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
function SpecialAlert({ alert }: { alert: Hotel["specialAlerts"][number] }) {
|
||||
return (
|
||||
<div className={styles.hotelAlert} key={`wrapper_${alert.id}`}>
|
||||
<Alert
|
||||
key={alert.id}
|
||||
type={alert.type}
|
||||
heading={alert.heading}
|
||||
text={alert.text}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function HotelInfoCardSkeleton() {
|
||||
return (
|
||||
<article className={styles.container}>
|
||||
<section className={styles.wrapper}>
|
||||
<div className={styles.imageWrapper}>
|
||||
<SkeletonShimmer height="100%" width="100%" />
|
||||
</div>
|
||||
<div className={styles.hotelContent}>
|
||||
<div className={styles.hotelInformation}>
|
||||
<SkeletonShimmer width="60ch" height="40px" />
|
||||
<div className={styles.hotelAddressDescription}>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<SkeletonShimmer width="40ch" />
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>
|
||||
<SkeletonShimmer width="60ch" />
|
||||
<SkeletonShimmer width="58ch" />
|
||||
<SkeletonShimmer width="45ch" />
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<Divider variant="vertical" />
|
||||
<div className={styles.facilities}>
|
||||
<div className={styles.facilityList}>
|
||||
<Typography
|
||||
variant="Body/Paragraph/mdBold"
|
||||
className={styles.facilityTitle}
|
||||
>
|
||||
<SkeletonShimmer width="20ch" />
|
||||
</Typography>
|
||||
{[1, 2, 3, 4, 5]?.map((id) => {
|
||||
return (
|
||||
<div className={styles.facilitiesItem} key={id}>
|
||||
<SkeletonShimmer width="10ch" />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className={styles.hotelAlert}>
|
||||
<SkeletonShimmer width="18ch" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
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 { HotelInfoCard } from "@/components/HotelReservation/SelectRate/HotelInfoCard"
|
||||
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 { hasOverlappingDates } from "../utils"
|
||||
import AvailabilityError from "./AvailabilityError"
|
||||
import Tracking from "./Tracking"
|
||||
|
||||
@@ -20,6 +26,7 @@ export default async function SelectRatePage({
|
||||
hotelData: NonNullable<RouterOutput["hotel"]["get"]>
|
||||
booking: SelectRateBooking
|
||||
}) {
|
||||
const intl = await getIntl()
|
||||
const bookingCode = booking.bookingCode
|
||||
|
||||
let isInValidFNF = false
|
||||
@@ -27,13 +34,51 @@ export default async function SelectRatePage({
|
||||
const cookieStore = await cookies()
|
||||
isInValidFNF = cookieStore.get("sc")?.value !== "1"
|
||||
}
|
||||
|
||||
const validAlerts = hotelData.hotel.specialAlerts.filter((alert) =>
|
||||
hasOverlappingDates(alert, dt(booking.fromDate), dt(booking.toDate))
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<HotelInfoCard
|
||||
hotel={{ ...hotelData.hotel, url: hotelData.url }}
|
||||
restaurants={hotelData.restaurants}
|
||||
additionalData={hotelData.additionalData}
|
||||
booking={booking}
|
||||
hotel={{
|
||||
id: hotelData.hotel.id,
|
||||
name: hotelData.hotel.name,
|
||||
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 ? (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest"
|
||||
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
|
||||
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 { 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")
|
||||
|
||||
it("shows alert if booking starts inside alert", () => {
|
||||
const result = getHotelAlertsForBookingDates(
|
||||
const result = filterOverlappingDates(
|
||||
[alert],
|
||||
dt("2025-09-05"),
|
||||
dt("2025-09-12")
|
||||
@@ -36,7 +36,7 @@ describe("getHotelAlertsForBookingDates", () => {
|
||||
})
|
||||
|
||||
it("shows alert if booking ends inside alert", () => {
|
||||
const result = getHotelAlertsForBookingDates(
|
||||
const result = filterOverlappingDates(
|
||||
[alert],
|
||||
dt("2025-08-28"),
|
||||
dt("2025-09-05")
|
||||
@@ -45,7 +45,7 @@ describe("getHotelAlertsForBookingDates", () => {
|
||||
})
|
||||
|
||||
it("shows alert if booking fully contains alert", () => {
|
||||
const result = getHotelAlertsForBookingDates(
|
||||
const result = filterOverlappingDates(
|
||||
[alert],
|
||||
dt("2025-08-28"),
|
||||
dt("2025-09-15")
|
||||
@@ -54,7 +54,7 @@ describe("getHotelAlertsForBookingDates", () => {
|
||||
})
|
||||
|
||||
it("shows alert if alert fully contains booking", () => {
|
||||
const result = getHotelAlertsForBookingDates(
|
||||
const result = filterOverlappingDates(
|
||||
[alert],
|
||||
dt("2025-09-03"),
|
||||
dt("2025-09-05")
|
||||
@@ -63,7 +63,7 @@ describe("getHotelAlertsForBookingDates", () => {
|
||||
})
|
||||
|
||||
it("does not show alert if no overlap", () => {
|
||||
const result = getHotelAlertsForBookingDates(
|
||||
const result = filterOverlappingDates(
|
||||
[alert],
|
||||
dt("2025-08-01"),
|
||||
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 { 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 { JSX } from "react"
|
||||
|
||||
@@ -88,41 +87,57 @@ export function calculateVat(priceInclVat: number, vat: number) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getHotelAlertsForBookingDates(
|
||||
specialAlerts: Zod.infer<typeof specialAlertsSchema>,
|
||||
export function filterOverlappingDates<
|
||||
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,
|
||||
toDate: Date | Dayjs
|
||||
) {
|
||||
return specialAlerts.filter((alert) => {
|
||||
if (alert.endDate && alert.startDate) {
|
||||
const alertStartDate = dt(alert.startDate)
|
||||
const alertEndDate = dt(alert.endDate)
|
||||
const bookingStart = dt(fromDate)
|
||||
const bookingEnd = dt(toDate)
|
||||
const startDate = dt(fromDate)
|
||||
const endDate = dt(toDate)
|
||||
|
||||
const fromDateIsBetweenAlertDates = bookingStart.isBetween(
|
||||
alertStartDate,
|
||||
alertEndDate,
|
||||
"date",
|
||||
"[]"
|
||||
)
|
||||
const toDateIsBetweenAlertDates = bookingEnd.isBetween(
|
||||
alertStartDate,
|
||||
alertEndDate,
|
||||
"date",
|
||||
"[]"
|
||||
)
|
||||
if (dateRangeItem.endDate && dateRangeItem.startDate) {
|
||||
const itemStartDate = dt(dateRangeItem.startDate)
|
||||
const itemEndDate = dt(dateRangeItem.endDate)
|
||||
|
||||
const bookingFullyContainsAlert =
|
||||
bookingStart.isSameOrBefore(alertStartDate, "date") &&
|
||||
bookingEnd.isSameOrAfter(alertEndDate, "date")
|
||||
const fromDateIsBetweenItemDates = startDate.isBetween(
|
||||
itemStartDate,
|
||||
itemEndDate,
|
||||
"date",
|
||||
"[]"
|
||||
)
|
||||
const toDateIsBetweenItemDates = endDate.isBetween(
|
||||
itemStartDate,
|
||||
itemEndDate,
|
||||
"date",
|
||||
"[]"
|
||||
)
|
||||
|
||||
return (
|
||||
fromDateIsBetweenAlertDates ||
|
||||
toDateIsBetweenAlertDates ||
|
||||
bookingFullyContainsAlert
|
||||
)
|
||||
}
|
||||
return true
|
||||
})
|
||||
const itemFullyContained =
|
||||
startDate.isSameOrBefore(itemStartDate, "date") &&
|
||||
endDate.isSameOrAfter(itemEndDate, "date")
|
||||
|
||||
return (
|
||||
fromDateIsBetweenItemDates ||
|
||||
toDateIsBetweenItemDates ||
|
||||
itemFullyContained
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user