feat: adjust select rate ui to latest design
This commit is contained in:
@@ -11,10 +11,8 @@ import type {
|
||||
HotelData,
|
||||
NullableHotelData,
|
||||
} from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import type {
|
||||
CategorizedFilters,
|
||||
Filter,
|
||||
} from "@/types/components/hotelReservation/selectHotel/hotelFilters"
|
||||
import type { CategorizedFilters } from "@/types/components/hotelReservation/selectHotel/hotelFilters"
|
||||
import type { DetailedFacility } from "@/types/hotel"
|
||||
import type { HotelsAvailabilityItem } from "@/types/trpc/routers/hotel/availability"
|
||||
|
||||
const hotelSurroundingsFilterNames = [
|
||||
@@ -111,9 +109,9 @@ export function getFiltersFromHotels(hotels: HotelData[]): CategorizedFilters {
|
||||
})
|
||||
|
||||
const uniqueFilterIds = [...new Set(filters.map((filter) => filter.id))]
|
||||
const filterList: Filter[] = uniqueFilterIds
|
||||
const filterList: DetailedFacility[] = uniqueFilterIds
|
||||
.map((filterId) => filters.find((filter) => filter.id === filterId))
|
||||
.filter((filter): filter is Filter => filter !== undefined)
|
||||
.filter((filter): filter is DetailedFacility => filter !== undefined)
|
||||
.sort((a, b) => b.sortOrder - a.sortOrder)
|
||||
|
||||
return filterList.reduce<CategorizedFilters>(
|
||||
|
||||
@@ -8,7 +8,7 @@ export function getTypeSpecificInformation(
|
||||
const { images } = hotel.hotelContent
|
||||
const { descriptions, meetingDescription } = hotel.hotelContent.texts
|
||||
const hotelData = {
|
||||
description: descriptions.short,
|
||||
description: descriptions?.short,
|
||||
imageSrc: images.imageSizes.small,
|
||||
altText: images.metaData.altText,
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export default function HotelListingItem({
|
||||
</Caption>
|
||||
</div>
|
||||
</div>
|
||||
<Body>{hotel.hotelContent.texts.descriptions.short}</Body>
|
||||
<Body>{hotel.hotelContent.texts.descriptions?.short}</Body>
|
||||
<ul className={styles.amenityList}>
|
||||
{amenities.map((amenity) => {
|
||||
const IconComponent = mapFacilityToIcon(amenity.id)
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type {
|
||||
Hotel,
|
||||
HotelAddress,
|
||||
HotelContent,
|
||||
HotelLocation,
|
||||
HotelTripAdvisor,
|
||||
} from "@/types/hotel"
|
||||
|
||||
export type IntroSectionProps = {
|
||||
address: HotelAddress
|
||||
hotelDescription: HotelContent["texts"]["descriptions"]["short"]
|
||||
hotelDescription: string | undefined
|
||||
hotelName: Hotel["name"]
|
||||
location: HotelLocation
|
||||
tripAdvisor: HotelTripAdvisor
|
||||
|
||||
@@ -35,7 +35,7 @@ export default async function AboutTheHotelSidePeek({
|
||||
ecoLabels={ecoLabels}
|
||||
/>
|
||||
<Divider color="baseSurfaceSubtleHover" />
|
||||
<Preamble>{descriptions.descriptions.medium}</Preamble>
|
||||
<Preamble>{descriptions.descriptions?.medium}</Preamble>
|
||||
<Body>{descriptions.facilityInformation}</Body>
|
||||
<Body>{descriptions.surroundingInformation}</Body>
|
||||
</section>
|
||||
|
||||
@@ -98,7 +98,7 @@ export default async function HotelPage({ hotelId }: HotelPageProps) {
|
||||
} = hotelData.additionalData
|
||||
|
||||
const images = gallery?.smallerImages
|
||||
const description = hotelContent.texts.descriptions.medium
|
||||
const description = hotelContent.texts.descriptions?.medium
|
||||
|
||||
const { spaPage, activitiesCards } = content
|
||||
|
||||
@@ -107,9 +107,9 @@ export default async function HotelPage({ hotelId }: HotelPageProps) {
|
||||
const hasWellness = healthFacilities.length > 0
|
||||
|
||||
const facilities = setFacilityCards(
|
||||
restaurantImages,
|
||||
conferencesAndMeetings,
|
||||
healthAndWellness,
|
||||
restaurantImages ?? undefined,
|
||||
conferencesAndMeetings ?? undefined,
|
||||
healthAndWellness ?? undefined,
|
||||
hasRestaurants,
|
||||
hasMeetingRooms,
|
||||
hasWellness
|
||||
|
||||
@@ -42,7 +42,7 @@ export default function Header({
|
||||
busyStatus: "FREE",
|
||||
categories: ["booking", "hotel", "stay"],
|
||||
created: generateDateTime(booking.createDateTime),
|
||||
description: hotel.hotelContent.texts.descriptions.medium,
|
||||
description: hotel.hotelContent.texts.descriptions?.medium,
|
||||
end: generateDateTime(booking.checkOutDate),
|
||||
endInputType: "utc",
|
||||
geo: {
|
||||
|
||||
@@ -14,6 +14,7 @@ export default function ToggleSidePeek({
|
||||
hotelId,
|
||||
roomTypeCode,
|
||||
intent = "textInverted",
|
||||
title,
|
||||
}: ToggleSidePeekProps) {
|
||||
const intl = useIntl()
|
||||
const openSidePeek = useSidePeekStore((state) => state.openSidePeek)
|
||||
@@ -29,7 +30,7 @@ export default function ToggleSidePeek({
|
||||
intent={intent}
|
||||
wrapping
|
||||
>
|
||||
{intl.formatMessage({ id: "See room details" })}
|
||||
{title ? title : intl.formatMessage({ id: "See room details" })}
|
||||
<ChevronRight height="14" />
|
||||
</Button>
|
||||
)
|
||||
|
||||
@@ -137,7 +137,7 @@ function HotelCard({
|
||||
</div>
|
||||
</div>
|
||||
<Body className={styles.hotelDescription}>
|
||||
{hotelData.hotelContent.texts.descriptions.short}
|
||||
{hotelData.hotelContent.texts.descriptions?.short}
|
||||
</Body>
|
||||
<div className={styles.facilities}>
|
||||
{amenities.map((facility) => {
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function ActionPanel({
|
||||
busyStatus: "FREE",
|
||||
categories: ["booking", "hotel", "stay"],
|
||||
created: generateDateTime(booking.createDateTime),
|
||||
description: hotel.hotelContent.texts.descriptions.medium,
|
||||
description: hotel.hotelContent.texts.descriptions?.medium,
|
||||
end: generateDateTime(booking.checkOutDate),
|
||||
endInputType: "utc",
|
||||
geo: {
|
||||
|
||||
@@ -58,7 +58,7 @@ export default async function HotelInfoCard({ hotelData }: HotelInfoCardProps) {
|
||||
)}
|
||||
</Caption>
|
||||
<Body color="uiTextHighContrast">
|
||||
{hotel.hotelContent.texts.descriptions.medium}
|
||||
{hotel.hotelContent.texts.descriptions?.medium}
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getRates } from "@/components/HotelReservation/SelectRate/utils"
|
||||
import { EditIcon } from "@/components/Icons"
|
||||
import Image from "@/components/Image"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Chip from "@/components/TempDesignSystem/Chip"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
@@ -72,7 +73,7 @@ export default function SelectedRoomPanel() {
|
||||
|
||||
return (
|
||||
<div className={styles.selectedRoomPanel}>
|
||||
<div>
|
||||
<div className={styles.content}>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "Room {roomIndex}" },
|
||||
@@ -90,20 +91,22 @@ export default function SelectedRoomPanel() {
|
||||
{intl.formatMessage({ id: "night" })}
|
||||
</Body>
|
||||
</div>
|
||||
<div className={styles.imageAndModifyButtonContainer}>
|
||||
{images?.[0]?.imageSizes?.tiny && (
|
||||
<div className={styles.imageContainer}>
|
||||
<Image
|
||||
alt={selectedRate?.roomType ?? images[0].metaData?.altText ?? ""}
|
||||
fill
|
||||
src={images[0].imageSizes.tiny}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.imageContainer}>
|
||||
{images?.[0]?.imageSizes?.tiny ? (
|
||||
<Image
|
||||
alt={selectedRate?.roomType ?? images[0].metaData?.altText ?? ""}
|
||||
className={styles.img}
|
||||
height={300}
|
||||
src={images[0].imageSizes.tiny}
|
||||
width={600}
|
||||
/>
|
||||
) : null}
|
||||
<div className={styles.modifyButtonContainer}>
|
||||
<Button variant="icon" size="small" onClick={modifyRate}>
|
||||
<EditIcon />
|
||||
{intl.formatMessage({ id: "Modify" })}
|
||||
<Button clean onClick={modifyRate}>
|
||||
<Chip size="small" variant="uiTextHighContrast">
|
||||
<EditIcon />
|
||||
{intl.formatMessage({ id: "Modify" })}
|
||||
</Chip>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,50 +1,53 @@
|
||||
.selectedRoomPanel {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
display: grid;
|
||||
grid-template-areas: "content image";
|
||||
grid-template-columns: 1fr 190px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modifyButtonContainer {
|
||||
position: absolute;
|
||||
right: var(--Spacing-x2);
|
||||
bottom: var(--Spacing-x2);
|
||||
.content {
|
||||
grid-area: content;
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
width: 187px;
|
||||
height: 105px;
|
||||
position: relative;
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
grid-area: image;
|
||||
}
|
||||
|
||||
.titleContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x1);
|
||||
.img {
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
height: auto;
|
||||
max-height: 105px;
|
||||
object-fit: fill;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modifyButtonContainer {
|
||||
bottom: var(--Spacing-x1);
|
||||
position: absolute;
|
||||
right: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
div.selectedRoomPanel p.subtitle {
|
||||
padding-bottom: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.imageContainer {
|
||||
width: 120px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.imageAndModifyButtonContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
@media screen and (max-width: 768px) {
|
||||
.selectedRoomPanel {
|
||||
gap: var(--Spacing-x1);
|
||||
grid-template-areas: "image" "content";
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
}
|
||||
|
||||
.modifyButtonContainer {
|
||||
position: relative;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
.img {
|
||||
max-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
.img {
|
||||
max-height: 190px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { useRatesStore } from "@/stores/select-rate"
|
||||
|
||||
import { ChevronUpIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { useRoomContext } from "@/contexts/Room"
|
||||
|
||||
@@ -17,7 +19,13 @@ export default function MultiRoomWrapper({
|
||||
}: React.PropsWithChildren<{ isMultiRoom: boolean }>) {
|
||||
const intl = useIntl()
|
||||
const activeRoom = useRatesStore((state) => state.activeRoom)
|
||||
const { bookingRoom, isActiveRoom, roomNr, selectedRate } = useRoomContext()
|
||||
const {
|
||||
actions: { closeSection },
|
||||
bookingRoom,
|
||||
isActiveRoom,
|
||||
roomNr,
|
||||
selectedRate,
|
||||
} = useRoomContext()
|
||||
|
||||
const onlyAdultsMsg = intl.formatMessage(
|
||||
{ id: "{adults} adults" },
|
||||
@@ -56,19 +64,33 @@ export default function MultiRoomWrapper({
|
||||
selected: !!selectedRate && !isActiveRoom,
|
||||
})
|
||||
return (
|
||||
<div className={styles.roomContainer}>
|
||||
{selectedRate && !isActiveRoom ? null : (
|
||||
<Subtitle className={styles.subtitle} color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "Room {roomIndex}" },
|
||||
{ roomIndex: roomNr }
|
||||
)}
|
||||
,{" "}
|
||||
{bookingRoom.childrenInRoom?.length
|
||||
? adultsAndChildrenMsg
|
||||
: onlyAdultsMsg}
|
||||
</Subtitle>
|
||||
)}
|
||||
<div className={styles.roomContainer} data-multiroom="true">
|
||||
<div className={styles.header}>
|
||||
{selectedRate && !isActiveRoom ? null : (
|
||||
<Subtitle className={styles.subtitle} color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "Room {roomIndex}" },
|
||||
{ roomIndex: roomNr }
|
||||
)}
|
||||
,{" "}
|
||||
{bookingRoom.childrenInRoom?.length
|
||||
? adultsAndChildrenMsg
|
||||
: onlyAdultsMsg}
|
||||
</Subtitle>
|
||||
)}
|
||||
{selectedRate && isActiveRoom ? (
|
||||
<Button
|
||||
intent="text"
|
||||
onClick={closeSection}
|
||||
size="medium"
|
||||
theme="base"
|
||||
variant="icon"
|
||||
>
|
||||
{intl.formatMessage({ id: "Close" })}
|
||||
<ChevronUpIcon height={20} width={20} />
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className={classNames}>
|
||||
<div className={styles.roomPanel}>
|
||||
<SelectedRoomPanel />
|
||||
|
||||
@@ -4,7 +4,13 @@
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--Spacing-x2);
|
||||
padding: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.roomPanel,
|
||||
@@ -27,27 +33,21 @@
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.roomSelectionPanelContainer.active .roomSelectionPanel,
|
||||
.roomSelectionPanelContainer.selected .roomPanel {
|
||||
grid-template-rows: 1fr;
|
||||
opacity: 1;
|
||||
height: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.roomSelectionPanelContainer.active .roomPanel {
|
||||
padding-top: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.roomSelectionPanelContainer.selected .roomSelectionPanel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.roomSelectionPanelContainer.active .roomSelectionPanel {
|
||||
grid-template-rows: 1fr;
|
||||
opacity: 1;
|
||||
height: auto;
|
||||
padding-top: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
div.roomContainer p.subtitle {
|
||||
padding-bottom: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.roomContainer {
|
||||
padding: var(--Spacing-x2);
|
||||
|
||||
@@ -138,20 +138,20 @@ export default function PriceList({
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{isUserLoggedIn
|
||||
? intl.formatMessage(
|
||||
{ id: "{memberPrice} {currency}" },
|
||||
{
|
||||
memberPrice: totalMemberRequestedPricePerNight,
|
||||
currency: publicRequestedPrice.currency,
|
||||
}
|
||||
)
|
||||
{ id: "{memberPrice} {currency}" },
|
||||
{
|
||||
memberPrice: totalMemberRequestedPricePerNight,
|
||||
currency: publicRequestedPrice.currency,
|
||||
}
|
||||
)
|
||||
: intl.formatMessage(
|
||||
{ id: "{publicPrice}/{memberPrice} {currency}" },
|
||||
{
|
||||
publicPrice: totalPublicRequestedPricePerNight,
|
||||
memberPrice: totalMemberRequestedPricePerNight,
|
||||
currency: publicRequestedPrice.currency,
|
||||
}
|
||||
)}
|
||||
{ id: "{publicPrice}/{memberPrice} {currency}" },
|
||||
{
|
||||
publicPrice: totalPublicRequestedPricePerNight,
|
||||
memberPrice: totalMemberRequestedPricePerNight,
|
||||
currency: publicRequestedPrice.currency,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
cursor: pointer;
|
||||
background-color: var(--Base-Surface-Primary-light-Hover-alt);
|
||||
}
|
||||
|
||||
.checkIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
@@ -33,10 +34,18 @@
|
||||
align-items: center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type="radio"].radio {
|
||||
opacity: 0;
|
||||
position: fixed;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
input[type="radio"]:checked + .card {
|
||||
border: 1px solid var(--Primary-Dark-On-Surface-Divider);
|
||||
background-color: var(--Base-Surface-Primary-light-Hover-alt);
|
||||
}
|
||||
|
||||
input[type="radio"]:checked + .card .checkIcon {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
@@ -81,11 +90,13 @@ input[type="radio"]:checked + .card .checkIcon {
|
||||
.terms {
|
||||
padding-top: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.termsText:nth-child(n) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.termsIcon {
|
||||
padding-right: var(--Spacing-x1);
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -34,6 +34,7 @@ export default function FlexibilityOption({
|
||||
const {
|
||||
actions: { selectRate },
|
||||
isMainRoom,
|
||||
roomNr,
|
||||
} = useRoomContext()
|
||||
|
||||
function handleSelect() {
|
||||
@@ -74,7 +75,8 @@ export default function FlexibilityOption({
|
||||
<label>
|
||||
<input
|
||||
checked={isSelected}
|
||||
name={`rateCode-${rate.rateCode}`}
|
||||
className={styles.radio}
|
||||
name={`rateCode-${roomNr}-${rate.rateCode}`}
|
||||
onChange={handleSelect}
|
||||
type="radio"
|
||||
value={rate.rateCode}
|
||||
|
||||
@@ -14,23 +14,29 @@ export default function RoomSize({ roomSize }: RoomSizeProps) {
|
||||
|
||||
if (roomSize.min === roomSize.max) {
|
||||
return (
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{roomSize} m²" },
|
||||
{ roomSize: roomSize.min }
|
||||
)}
|
||||
</Caption>
|
||||
<>
|
||||
<Caption color="uiTextMediumContrast">∙</Caption>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{roomSize} m²" },
|
||||
{ roomSize: roomSize.min }
|
||||
)}
|
||||
</Caption>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{roomSizeMin} - {roomSizeMax} m²" },
|
||||
{
|
||||
roomSizeMin: roomSize.min,
|
||||
roomSizeMax: roomSize.max,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
<>
|
||||
<Caption color="uiTextMediumContrast">∙</Caption>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{roomSizeMin} - {roomSizeMax} m²" },
|
||||
{
|
||||
roomSizeMin: roomSize.min,
|
||||
roomSizeMax: roomSize.max,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -35,13 +35,17 @@ function getBreakfastMessage(
|
||||
memberBreakfastIncluded: boolean,
|
||||
hotelType: string | undefined,
|
||||
userIsLoggedIn: boolean,
|
||||
msgs: Record<"included" | "noSelection" | "scandicgo" | "notIncluded", string>
|
||||
msgs: Record<
|
||||
"included" | "noSelection" | "scandicgo" | "notIncluded",
|
||||
string
|
||||
>,
|
||||
roomNr: number
|
||||
) {
|
||||
if (hotelType === HotelTypeEnum.ScandicGo) {
|
||||
return msgs.scandicgo
|
||||
}
|
||||
|
||||
if (userIsLoggedIn && memberBreakfastIncluded) {
|
||||
if (userIsLoggedIn && memberBreakfastIncluded && roomNr === 1) {
|
||||
return msgs.included
|
||||
}
|
||||
|
||||
@@ -81,7 +85,7 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
||||
rateDefinitions: state.roomsAvailability?.rateDefinitions,
|
||||
roomCategories: state.roomCategories,
|
||||
}))
|
||||
const { isMainRoom, selectedPackage, selectedRate } = useRoomContext()
|
||||
const { isMainRoom, roomNr, selectedPackage, selectedRate } = useRoomContext()
|
||||
|
||||
const classNames = cardVariants({
|
||||
availability:
|
||||
@@ -105,7 +109,8 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
||||
roomConfiguration.breakfastIncludedInAllRatesMember,
|
||||
hotelType,
|
||||
isUserLoggedIn,
|
||||
breakfastMessages
|
||||
breakfastMessages,
|
||||
roomNr
|
||||
)
|
||||
|
||||
if (!rateDefinitions) {
|
||||
@@ -124,7 +129,7 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
||||
)
|
||||
)
|
||||
|
||||
const { name, roomSize, totalOccupancy, images } = selectedRoom || {}
|
||||
const { images, name, occupancy, roomSize } = selectedRoom || {}
|
||||
const galleryImages = mapApiImagesToGalleryImages(images || [])
|
||||
|
||||
const freeCancelation = intl.formatMessage({ id: "Free cancellation" })
|
||||
@@ -203,86 +208,87 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
||||
|
||||
return (
|
||||
<li className={classNames}>
|
||||
<div>
|
||||
<div className={styles.imageContainer}>
|
||||
<div className={styles.chipContainer}>
|
||||
{lessThanFiveRoomsLeft ? (
|
||||
<span className={styles.chip}>
|
||||
<Footnote color="burgundy" textTransform="uppercase">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount, number} left" },
|
||||
{ amount: roomConfiguration.roomsLeft }
|
||||
)}
|
||||
</Footnote>
|
||||
<div className={styles.imageContainer}>
|
||||
<div className={styles.chipContainer}>
|
||||
{lessThanFiveRoomsLeft ? (
|
||||
<span className={styles.chip}>
|
||||
<Footnote color="burgundy" textTransform="uppercase">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount, number} left" },
|
||||
{ amount: roomConfiguration.roomsLeft }
|
||||
)}
|
||||
</Footnote>
|
||||
</span>
|
||||
) : null}
|
||||
{roomConfiguration.features
|
||||
.filter((feature) => selectedPackage === feature.code)
|
||||
.map((feature) => (
|
||||
<span className={styles.chip} key={feature.code}>
|
||||
{createElement(getIconForFeatureCode(feature.code), {
|
||||
color: "burgundy",
|
||||
height: 16,
|
||||
width: 16,
|
||||
})}
|
||||
</span>
|
||||
) : null}
|
||||
{roomConfiguration.features
|
||||
.filter((feature) => selectedPackage === feature.code)
|
||||
.map((feature) => (
|
||||
<span className={styles.chip} key={feature.code}>
|
||||
{createElement(getIconForFeatureCode(feature.code), {
|
||||
color: "burgundy",
|
||||
height: 16,
|
||||
width: 16,
|
||||
})}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<ImageGallery
|
||||
images={galleryImages}
|
||||
title={roomConfiguration.roomType}
|
||||
fill
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<ImageGallery
|
||||
images={galleryImages}
|
||||
title={roomConfiguration.roomType}
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.specification}>
|
||||
{totalOccupancy && (
|
||||
<Caption color="uiTextMediumContrast" className={styles.guests}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "Max {max, plural, one {{range} guest} other {{range} guests}}",
|
||||
},
|
||||
{ max: totalOccupancy.max, range: totalOccupancy.range }
|
||||
)}
|
||||
</Caption>
|
||||
<div className={styles.specification}>
|
||||
{occupancy && (
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{occupancy.max === occupancy.min
|
||||
? intl.formatMessage(
|
||||
{ id: "guests.plural" },
|
||||
{ guests: occupancy.max }
|
||||
)
|
||||
: intl.formatMessage({ id: "guests.span" }, occupancy)}
|
||||
</Caption>
|
||||
)}
|
||||
<RoomSize roomSize={roomSize} />
|
||||
<div className={styles.toggleSidePeek}>
|
||||
{roomConfiguration.roomTypeCode && (
|
||||
<ToggleSidePeek
|
||||
hotelId={hotelId.toString()}
|
||||
roomTypeCode={roomConfiguration.roomTypeCode}
|
||||
title={intl.formatMessage({ id: "Room details" })}
|
||||
/>
|
||||
)}
|
||||
<RoomSize roomSize={roomSize} />
|
||||
<div className={styles.toggleSidePeek}>
|
||||
{roomConfiguration.roomTypeCode && (
|
||||
<ToggleSidePeek
|
||||
hotelId={hotelId.toString()}
|
||||
roomTypeCode={roomConfiguration.roomTypeCode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.roomDetails}>
|
||||
<Subtitle className={styles.name} type="two">
|
||||
{name}
|
||||
</Subtitle>
|
||||
{/* Out of scope for now
|
||||
</div>
|
||||
<div className={styles.roomDetails}>
|
||||
<Subtitle className={styles.name} type="two">
|
||||
{name}
|
||||
</Subtitle>
|
||||
{/* Out of scope for now
|
||||
<Body>{descriptions?.short}</Body>
|
||||
*/}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.container}>
|
||||
{roomConfiguration.status === AvailabilityEnum.NotAvailable ? (
|
||||
<div className={styles.noRoomsContainer}>
|
||||
<div className={styles.noRooms}>
|
||||
<ErrorCircleIcon color="red" width={16} />
|
||||
<Caption color="uiTextHighContrast" type="bold">
|
||||
{intl.formatMessage({
|
||||
id: "This room is not available",
|
||||
})}
|
||||
</Caption>
|
||||
<>
|
||||
{/** The empty div is used to allow for subgrid to align rows */}
|
||||
<div></div>
|
||||
<div className={styles.noRoomsContainer}>
|
||||
<div className={styles.noRooms}>
|
||||
<ErrorCircleIcon color="red" width={16} />
|
||||
<Caption color="uiTextHighContrast" type="bold">
|
||||
{intl.formatMessage({
|
||||
id: "This room is not available",
|
||||
})}
|
||||
</Caption>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Caption color="uiTextHighContrast" type="bold">
|
||||
{breakfastMessage}
|
||||
</Caption>
|
||||
<Caption color="uiTextHighContrast">{breakfastMessage}</Caption>
|
||||
{roomConfiguration.products.map((product) => {
|
||||
const rate = getRateInfo(product)
|
||||
const isSelectedRateCode =
|
||||
|
||||
@@ -1,112 +1,100 @@
|
||||
.card {
|
||||
font-size: 14px;
|
||||
background-color: #fff;
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
display: grid;
|
||||
font-size: 14px;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
grid-row: span 7;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: subgrid;
|
||||
background-color: #fff;
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
position: relative;
|
||||
justify-content: space-between;
|
||||
grid-row: span 5;
|
||||
padding: 0 var(--Spacing-x2) var(--Spacing-x2);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div[data-multiroom="true"] .card {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card.noAvailability {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.specification {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--Spacing-x1);
|
||||
padding: 0 var(--Spacing-x1) 0 var(--Spacing-x-one-and-half);
|
||||
.imageContainer {
|
||||
margin: 0 calc(-1 * var(--Spacing-x2));
|
||||
min-height: 190px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.specification .guests {
|
||||
border-right: 1px solid var(--Base-Border-Subtle);
|
||||
padding-right: var(--Spacing-x1);
|
||||
div[data-multiroom="true"] .imageContainer {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chipContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--Spacing-x1);
|
||||
left: 12px;
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.chip {
|
||||
background-color: var(--Main-Grey-White);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
padding: var(--Spacing-x-half) var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.card .imageContainer img {
|
||||
aspect-ratio: 16/9;
|
||||
border-radius: var(--Corner-radius-Medium) var(--Corner-radius-Medium) 0 0;
|
||||
max-width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.specification {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--Spacing-x1);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.toggleSidePeek {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.toggleSidePeek button {
|
||||
.specification .toggleSidePeek button {
|
||||
padding: 0;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: var(--Spacing-x1) var(--Spacing-x2) var(--Spacing-x2);
|
||||
display: grid;
|
||||
grid-template-rows: subgrid;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
grid-row: span 4;
|
||||
}
|
||||
|
||||
/* Make sure rows with only unavailable rooms still has a min-height */
|
||||
.container:has(.noRoomsContainer) {
|
||||
min-height: 400px;
|
||||
grid-template-rows: auto repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.roomDetails {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x1);
|
||||
padding: var(--Spacing-x1) var(--Spacing-x2) 0;
|
||||
padding-bottom: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.name {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.card img {
|
||||
max-width: 100%;
|
||||
aspect-ratio: 16/9;
|
||||
object-fit: cover;
|
||||
border-radius: var(--Corner-radius-Medium) var(--Corner-radius-Medium) 0 0;
|
||||
}
|
||||
|
||||
.flexibilityOptions {
|
||||
.container {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.chipContainer {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.chip {
|
||||
background-color: var(--Main-Grey-White);
|
||||
padding: var(--Spacing-x-half) var(--Spacing-x1);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
min-height: 190px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.noRoomsContainer {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
grid-row: span 4;
|
||||
grid-template-rows: subgrid;
|
||||
}
|
||||
|
||||
.noRooms {
|
||||
padding: var(--Spacing-x2);
|
||||
background-color: var(--Base-Surface-Secondary-light-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
margin: 0;
|
||||
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
margin: 0;
|
||||
padding: var(--Spacing-x2);
|
||||
}
|
||||
@@ -41,16 +41,14 @@ export default function RoomSelectionPanel() {
|
||||
</div>
|
||||
) : null}
|
||||
<RoomTypeFilter />
|
||||
<div className={styles.wrapper}>
|
||||
<ul className={styles.roomList}>
|
||||
{rooms.map((roomConfiguration) => (
|
||||
<RoomCard
|
||||
key={roomConfiguration.roomTypeCode}
|
||||
roomConfiguration={roomConfiguration}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<ul className={styles.roomList}>
|
||||
{rooms.map((roomConfiguration) => (
|
||||
<RoomCard
|
||||
key={roomConfiguration.roomTypeCode}
|
||||
roomConfiguration={roomConfiguration}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,12 +9,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.roomList input[type="radio"] {
|
||||
opacity: 0;
|
||||
position: fixed;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.hotelAlert {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
@@ -47,22 +47,26 @@ export default async function ParkingInformation({
|
||||
{intl.formatMessage({ id: "Weekday prices" })}
|
||||
</Caption>
|
||||
<Divider color="baseSurfaceSubtleHover" />
|
||||
<ParkingPrices
|
||||
pricing={parking.pricing.localCurrency?.ordinary}
|
||||
currency={parking.pricing.localCurrency?.currency}
|
||||
freeParking={parking.pricing.freeParking}
|
||||
/>
|
||||
{parking.pricing.localCurrency ? (
|
||||
<ParkingPrices
|
||||
currency={parking.pricing.localCurrency.currency}
|
||||
freeParking={parking.pricing.freeParking}
|
||||
pricing={parking.pricing.localCurrency.ordinary}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className={styles.priceWrapper}>
|
||||
<Caption color="uiTextMediumContrast" textTransform="uppercase">
|
||||
{intl.formatMessage({ id: "Weekend prices" })}
|
||||
</Caption>
|
||||
<Divider color="baseSurfaceSubtleHover" />
|
||||
<ParkingPrices
|
||||
pricing={parking.pricing.localCurrency?.weekend}
|
||||
currency={parking.pricing.localCurrency?.currency}
|
||||
freeParking={parking.pricing.freeParking}
|
||||
/>
|
||||
{parking.pricing.localCurrency ? (
|
||||
<ParkingPrices
|
||||
currency={parking.pricing.localCurrency.currency}
|
||||
freeParking={parking.pricing.freeParking}
|
||||
pricing={parking.pricing.localCurrency.weekend}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{parking.externalParkingUrl && showExternalParkingButton && (
|
||||
|
||||
@@ -55,10 +55,12 @@ a.text {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* TODO: The variants for combinations of size/text/wrapping should be looked at and iterated on */
|
||||
.text:not(.wrapping) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* VARIANTS */
|
||||
.default,
|
||||
a.default {
|
||||
@@ -827,3 +829,15 @@ a.default {
|
||||
.icon.tertiaryLightSecondary:disabled svg * {
|
||||
fill: var(--Tertiary-Light-Button-Secondary-On-Fill-Disabled);
|
||||
}
|
||||
|
||||
button.btn.clean {
|
||||
background: none;
|
||||
background-color: unset;
|
||||
border: none;
|
||||
border-color: unset;
|
||||
border-radius: unset;
|
||||
color: unset;
|
||||
gap: unset;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { ButtonProps } from "./button"
|
||||
export default function Button(props: ButtonProps) {
|
||||
const {
|
||||
className,
|
||||
clean,
|
||||
intent,
|
||||
size,
|
||||
theme,
|
||||
@@ -21,6 +22,7 @@ export default function Button(props: ButtonProps) {
|
||||
|
||||
const classNames = buttonVariants({
|
||||
className,
|
||||
clean,
|
||||
intent,
|
||||
size,
|
||||
theme,
|
||||
|
||||
@@ -28,12 +28,16 @@ export const buttonVariants = cva(styles.btn, {
|
||||
tertiaryDark: "",
|
||||
},
|
||||
variant: {
|
||||
clean: styles.clean,
|
||||
default: styles.default,
|
||||
icon: styles.icon,
|
||||
},
|
||||
wrapping: {
|
||||
true: styles.wrapping,
|
||||
},
|
||||
clean: {
|
||||
true: styles.clean,
|
||||
},
|
||||
fullWidth: {
|
||||
true: styles.fullWidth,
|
||||
},
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
div.chip {
|
||||
--chip-text-color: var(--Base-Text-High-contrast);
|
||||
--chip-background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: var(--Spacing-x-half) var(--Spacing-x1);
|
||||
gap: var(--Spacing-x-half);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
color: var(--chip-text-color);
|
||||
background-color: var(--chip-background-color);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chip.small {
|
||||
padding: var(--Spacing-x-quarter) var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.chip.medium {
|
||||
padding: var(--Spacing-x-half) var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.chip *,
|
||||
@@ -29,3 +36,8 @@ div.chip {
|
||||
.chip.tag {
|
||||
--chip-background-color: var(--Base-Surface-Subtle-Hover);
|
||||
}
|
||||
|
||||
.chip.uiTextHighContrast {
|
||||
--chip-background-color: var(--UI-Text-High-contrast);
|
||||
--chip-text-color: var(--UI-Input-Controls-On-Fill-Normal);
|
||||
}
|
||||
|
||||
@@ -4,9 +4,15 @@ import { chipVariants } from "./variants"
|
||||
|
||||
import type { ChipProps } from "./chip"
|
||||
|
||||
export default function Chip({ children, className, variant }: ChipProps) {
|
||||
export default function Chip({
|
||||
children,
|
||||
className,
|
||||
size,
|
||||
variant,
|
||||
}: ChipProps) {
|
||||
const classNames = chipVariants({
|
||||
className,
|
||||
size,
|
||||
variant,
|
||||
})
|
||||
return (
|
||||
|
||||
@@ -4,14 +4,20 @@ import styles from "./chip.module.css"
|
||||
|
||||
export const chipVariants = cva(styles.chip, {
|
||||
variants: {
|
||||
size: {
|
||||
small: styles.small,
|
||||
medium: styles.medium,
|
||||
},
|
||||
variant: {
|
||||
default: styles.default,
|
||||
burgundy: styles.burgundy,
|
||||
transparent: styles.transparent,
|
||||
tag: styles.tag,
|
||||
uiTextHighContrast: styles.uiTextHighContrast,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "medium",
|
||||
variant: "default",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -716,6 +716,8 @@
|
||||
"booking.thisRoomIsEquippedWith": "Dette værelse er udstyret med",
|
||||
"friday": "fredag",
|
||||
"from": "fra",
|
||||
"guests.plural": "{guests, plural, one {# gæst} other {# gæster}}",
|
||||
"guests.span": "{min}-{max} gæster",
|
||||
"max {seatings} pers": "max {seatings} pers",
|
||||
"monday": "mandag",
|
||||
"next level: {nextLevel}": "next level: {nextLevel}",
|
||||
|
||||
@@ -717,6 +717,8 @@
|
||||
"booking.thisRoomIsEquippedWith": "Dieses Zimmer ist ausgestattet mit",
|
||||
"friday": "freitag",
|
||||
"from": "von",
|
||||
"guests.plural": "{guests, plural, one {# gast} other {# gäste}}",
|
||||
"guests.span": "{min}-{max} gäste",
|
||||
"max {seatings} pers": "max {seatings} pers",
|
||||
"monday": "montag",
|
||||
"next level: {nextLevel}": "next level: {nextLevel}",
|
||||
|
||||
@@ -521,6 +521,7 @@
|
||||
"Room": "Room",
|
||||
"Room & Terms": "Room & Terms",
|
||||
"Room charge": "Room charge",
|
||||
"Room details": "Room details",
|
||||
"Room facilities": "Room facilities",
|
||||
"Room total": "Room total",
|
||||
"Room {roomIndex}": "Room {roomIndex}",
|
||||
@@ -720,6 +721,8 @@
|
||||
"cm": "cm",
|
||||
"friday": "friday",
|
||||
"from": "from",
|
||||
"guests.plural": "{guests, plural, one {# guest} other {# guests}}",
|
||||
"guests.span": "{min}-{max} guests",
|
||||
"max {seatings} pers": "max {seatings} pers",
|
||||
"monday": "monday",
|
||||
"next level: {nextLevel}": "next level: {nextLevel}",
|
||||
|
||||
@@ -717,6 +717,8 @@
|
||||
"booking.thisRoomIsEquippedWith": "Tämä huone on varustettu",
|
||||
"friday": "perjantai",
|
||||
"from": "alkaa",
|
||||
"guests.plural": "{guests, plural, one {# vieras} other {# vieraita}}",
|
||||
"guests.span": "{min}-{max} vieraita",
|
||||
"max {seatings} pers": "max {seatings} pers",
|
||||
"monday": "maanantai",
|
||||
"next level: {nextLevel}": "next level: {nextLevel}",
|
||||
|
||||
@@ -715,6 +715,8 @@
|
||||
"booking.thisRoomIsEquippedWith": "Dette rommet er utstyrt med",
|
||||
"friday": "fredag",
|
||||
"from": "fra",
|
||||
"guests.plural": "{guests, plural, one {# gjest} other {# gjester}}",
|
||||
"guests.span": "{min}-{max} gjester",
|
||||
"max {seatings} pers": "max {seatings} pers",
|
||||
"monday": "mandag",
|
||||
"next level: {nextLevel}": "next level: {nextLevel}",
|
||||
|
||||
@@ -715,6 +715,8 @@
|
||||
"booking.thisRoomIsEquippedWith": "Detta rum är utrustat med",
|
||||
"friday": "fredag",
|
||||
"from": "från",
|
||||
"guests.plural": "{guests, plural, one {# gäst} other {# gäster}}",
|
||||
"guests.span": "{min}-{max} gäster",
|
||||
"max {seatings} pers": "max {seatings} pers",
|
||||
"monday": "måndag",
|
||||
"next level: {nextLevel}": "next level: {nextLevel}",
|
||||
|
||||
@@ -12,6 +12,7 @@ export default function RoomProvider({
|
||||
room,
|
||||
}: RoomProviderProps) {
|
||||
const activeRoom = useRatesStore((state) => state.activeRoom)
|
||||
const closeSection = useRatesStore((state) => state.actions.closeSection(idx))
|
||||
const modifyRate = useRatesStore((state) => state.actions.modifyRate(idx))
|
||||
const selectFilter = useRatesStore((state) => state.actions.selectFilter(idx))
|
||||
const selectRate = useRatesStore((state) => state.actions.selectRate(idx))
|
||||
@@ -21,6 +22,7 @@ export default function RoomProvider({
|
||||
value={{
|
||||
...room,
|
||||
actions: {
|
||||
closeSection,
|
||||
modifyRate,
|
||||
selectFilter,
|
||||
selectRate,
|
||||
|
||||
@@ -93,7 +93,7 @@ export function getDescription(data: RawMetadataSchema) {
|
||||
return metadata.description
|
||||
}
|
||||
if (data.hotelData) {
|
||||
return data.hotelData.hotelContent.texts.descriptions.short
|
||||
return data.hotelData.hotelContent.texts.descriptions?.short
|
||||
}
|
||||
if (data.preamble) {
|
||||
return truncateTextAfterLastPeriod(data.preamble)
|
||||
|
||||
@@ -18,7 +18,7 @@ import { cache } from "@/utils/cache"
|
||||
|
||||
import { getHotelPageUrl } from "../contentstack/hotelPage/utils"
|
||||
import { getVerifiedUser, parsedUser } from "../user/query"
|
||||
import { additionalDataSchema } from "./schemas/additionalData"
|
||||
import { additionalDataSchema } from "./schemas/hotel/include/additionalData"
|
||||
import { meetingRoomsSchema } from "./schemas/meetingRoom"
|
||||
import {
|
||||
ancillaryPackageInputSchema,
|
||||
@@ -510,8 +510,8 @@ export const hotelQueryRouter = router({
|
||||
adults: adultCount,
|
||||
...(childArray &&
|
||||
childArray.length > 0 && {
|
||||
children: childArray.join(","),
|
||||
}),
|
||||
children: childArray.join(","),
|
||||
}),
|
||||
...(bookingCode && { bookingCode }),
|
||||
language: apiLang,
|
||||
}
|
||||
@@ -754,9 +754,9 @@ export const hotelQueryRouter = router({
|
||||
type: matchingRoom.mainBed.type,
|
||||
extraBed: matchingRoom.fixedExtraBed
|
||||
? {
|
||||
type: matchingRoom.fixedExtraBed.type,
|
||||
description: matchingRoom.fixedExtraBed.description,
|
||||
}
|
||||
type: matchingRoom.fixedExtraBed.type,
|
||||
description: matchingRoom.fixedExtraBed.description,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
@@ -855,7 +855,7 @@ export const hotelQueryRouter = router({
|
||||
}),
|
||||
}),
|
||||
rates: router({
|
||||
get: publicProcedure.input(ratesInputSchema).query(async ({}) => {
|
||||
get: publicProcedure.input(ratesInputSchema).query(async () => {
|
||||
// TODO: Do a real API call when the endpoint is ready
|
||||
// const { hotelId } = input
|
||||
|
||||
@@ -1112,9 +1112,9 @@ export const hotelQueryRouter = router({
|
||||
|
||||
return hotelData
|
||||
? {
|
||||
...hotelData,
|
||||
url,
|
||||
}
|
||||
...hotelData,
|
||||
url,
|
||||
}
|
||||
: null
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { imageSchema } from "./image"
|
||||
|
||||
const specialNeedSchema = z.object({
|
||||
name: z.string(),
|
||||
details: z.string(),
|
||||
})
|
||||
|
||||
const specialNeedGroupSchema = z.object({
|
||||
name: z.string(),
|
||||
specialNeeds: z.array(specialNeedSchema),
|
||||
})
|
||||
|
||||
export const gallerySchema = z.object({
|
||||
heroImages: z.array(imageSchema),
|
||||
smallerImages: z.array(imageSchema),
|
||||
})
|
||||
|
||||
export const facilitySchema = z.object({
|
||||
headingText: z.string().default(""),
|
||||
heroImages: z.array(imageSchema),
|
||||
})
|
||||
|
||||
export const restaurantsOverviewPageSchema = z.object({
|
||||
restaurantsOverviewPageLinkText: z.string().optional(),
|
||||
restaurantsOverviewPageLink: z.string().optional(),
|
||||
restaurantsContentDescriptionShort: z.string().optional(),
|
||||
restaurantsContentDescriptionMedium: z.string().optional(),
|
||||
})
|
||||
|
||||
export const extraPageSchema = z.object({
|
||||
elevatorPitch: z.string().default(""),
|
||||
mainBody: z.string().optional(),
|
||||
nameInUrl: z.string().optional(),
|
||||
})
|
||||
|
||||
export const accessibilitySchema = z.object({
|
||||
headingText: z.string().default(""),
|
||||
heroImages: z.array(imageSchema),
|
||||
})
|
||||
|
||||
export const additionalDataSchema = z.object({
|
||||
attributes: z.object({
|
||||
name: z.string(),
|
||||
id: z.string(),
|
||||
displayWebPage: z.object({
|
||||
healthGym: z.boolean(),
|
||||
meetingRoom: z.boolean(),
|
||||
parking: z.boolean(),
|
||||
specialNeeds: z.boolean(),
|
||||
}),
|
||||
specialNeedGroups: z.array(specialNeedGroupSchema),
|
||||
gallery: gallerySchema.optional(),
|
||||
conferencesAndMeetings: facilitySchema.optional(),
|
||||
healthAndWellness: facilitySchema.optional(),
|
||||
restaurantImages: facilitySchema.optional(),
|
||||
parkingImages: facilitySchema.optional(),
|
||||
restaurantsOverviewPage: restaurantsOverviewPageSchema,
|
||||
meetingRooms: extraPageSchema,
|
||||
healthAndFitness: extraPageSchema,
|
||||
hotelParking: extraPageSchema,
|
||||
hotelSpecialNeeds: extraPageSchema,
|
||||
hotelRoomElevatorPitchText: z.string().optional(),
|
||||
accessibility: accessibilitySchema.optional(),
|
||||
}),
|
||||
type: z.literal("additionalData"),
|
||||
})
|
||||
|
||||
export function transformAdditionalData(
|
||||
data: z.output<typeof additionalDataSchema>
|
||||
) {
|
||||
return {
|
||||
...data.attributes,
|
||||
id: data.attributes.id,
|
||||
type: data.type,
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,21 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import {
|
||||
nullableArrayObjectValidator,
|
||||
nullableArrayStringValidator,
|
||||
} from "@/utils/zod/arrayValidator"
|
||||
import { nullableNumberValidator } from "@/utils/zod/numberValidator"
|
||||
import { nullableStringValidator } from "@/utils/zod/stringValidator"
|
||||
|
||||
import { addressSchema } from "./hotel/address"
|
||||
import { contactInformationSchema } from "./hotel/contactInformation"
|
||||
import { hotelContentSchema } from "./hotel/content"
|
||||
import { detailedFacilitiesSchema } from "./hotel/detailedFacility"
|
||||
import { hotelFactsSchema } from "./hotel/facts"
|
||||
import { gallerySchema } from "./hotel/gallery"
|
||||
import { healthFacilitySchema } from "./hotel/healthFacilities"
|
||||
import { healthFacilitiesSchema } from "./hotel/healthFacilities"
|
||||
import { displayWebPageSchema } from "./hotel/include/additionalData/displayWebPage"
|
||||
import { facilitySchema } from "./hotel/include/additionalData/facility"
|
||||
import { gallerySchema } from "./hotel/include/additionalData/gallery"
|
||||
import { includeSchema } from "./hotel/include/include"
|
||||
import { locationSchema } from "./hotel/location"
|
||||
import { merchantInformationSchema } from "./hotel/merchantInformation"
|
||||
@@ -18,41 +25,41 @@ import { ratingsSchema } from "./hotel/rating"
|
||||
import { rewardNightSchema } from "./hotel/rewardNight"
|
||||
import { socialMediaSchema } from "./hotel/socialMedia"
|
||||
import { specialAlertsSchema } from "./hotel/specialAlerts"
|
||||
import { specialNeedGroupSchema } from "./hotel/specialNeedGroups"
|
||||
import { facilitySchema } from "./additionalData"
|
||||
import { imageSchema } from "./image"
|
||||
|
||||
export const attributesSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
address: addressSchema,
|
||||
cityId: z.string(),
|
||||
cityName: z.string(),
|
||||
conferencesAndMeetings: facilitySchema.optional(),
|
||||
cityId: nullableStringValidator,
|
||||
cityName: nullableStringValidator,
|
||||
conferencesAndMeetings: facilitySchema.nullish(),
|
||||
contactInformation: contactInformationSchema,
|
||||
countryCode: nullableStringValidator,
|
||||
detailedFacilities: detailedFacilitiesSchema,
|
||||
gallery: gallerySchema.optional(),
|
||||
galleryImages: z.array(imageSchema).optional(),
|
||||
healthAndWellness: facilitySchema.optional(),
|
||||
healthFacilities: z.array(healthFacilitySchema),
|
||||
displayWebPage: displayWebPageSchema,
|
||||
gallery: gallerySchema.nullish(),
|
||||
galleryImages: z
|
||||
.array(imageSchema)
|
||||
.nullish()
|
||||
.transform((arr) => (arr ? arr.filter(Boolean) : [])),
|
||||
healthAndWellness: facilitySchema.nullish(),
|
||||
healthFacilities: healthFacilitiesSchema,
|
||||
hotelContent: hotelContentSchema,
|
||||
hotelFacts: hotelFactsSchema,
|
||||
hotelRoomElevatorPitchText: z.string().optional(),
|
||||
hotelType: z.string().optional(),
|
||||
hotelType: nullableStringValidator,
|
||||
isActive: z.boolean(),
|
||||
isPublished: z.boolean(),
|
||||
keywords: z.array(z.string()),
|
||||
keywords: nullableArrayStringValidator,
|
||||
location: locationSchema,
|
||||
merchantInformationData: merchantInformationSchema,
|
||||
name: z.string(),
|
||||
operaId: z.string(),
|
||||
parking: z.array(parkingSchema),
|
||||
name: nullableStringValidator,
|
||||
operaId: nullableStringValidator,
|
||||
parking: nullableArrayObjectValidator(parkingSchema),
|
||||
pointsOfInterest: pointOfInterestsSchema,
|
||||
ratings: ratingsSchema,
|
||||
restaurantImages: facilitySchema.nullish(),
|
||||
rewardNight: rewardNightSchema,
|
||||
restaurantImages: facilitySchema.optional(),
|
||||
socialMedia: socialMediaSchema,
|
||||
specialAlerts: specialAlertsSchema,
|
||||
specialNeedGroups: z.array(specialNeedGroupSchema),
|
||||
vat: nullableNumberValidator,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { nullableStringValidator } from "@/utils/zod/stringValidator"
|
||||
|
||||
export const addressSchema = z.object({
|
||||
city: z.string(),
|
||||
country: z.string(),
|
||||
streetAddress: z.string(),
|
||||
zipCode: z.string(),
|
||||
city: nullableStringValidator,
|
||||
country: nullableStringValidator,
|
||||
streetAddress: nullableStringValidator,
|
||||
zipCode: nullableStringValidator,
|
||||
})
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import {
|
||||
nullableStringEmailValidator,
|
||||
nullableStringValidator,
|
||||
} from "@/utils/zod/stringValidator"
|
||||
|
||||
export const contactInformationSchema = z.object({
|
||||
email: z.string(),
|
||||
faxNumber: z.string().optional(),
|
||||
phoneNumber: z.string(),
|
||||
websiteUrl: z.string(),
|
||||
email: nullableStringEmailValidator,
|
||||
faxNumber: nullableStringValidator,
|
||||
phoneNumber: nullableStringValidator,
|
||||
websiteUrl: nullableStringValidator,
|
||||
})
|
||||
|
||||
@@ -1,40 +1,26 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { nullableStringValidator } from "@/utils/zod/stringValidator"
|
||||
|
||||
import { imageSchema } from "../image"
|
||||
import { restaurantsOverviewPageSchema } from "./include/additionalData/restaurantsOverviewPage"
|
||||
|
||||
const descriptionSchema = z
|
||||
.object({
|
||||
medium: nullableStringValidator,
|
||||
short: nullableStringValidator,
|
||||
})
|
||||
.nullish()
|
||||
|
||||
const textsSchema = z.object({
|
||||
descriptions: descriptionSchema,
|
||||
facilityInformation: nullableStringValidator,
|
||||
meetingDescription: descriptionSchema,
|
||||
surroundingInformation: nullableStringValidator,
|
||||
})
|
||||
|
||||
export const hotelContentSchema = z.object({
|
||||
images: imageSchema.default({
|
||||
metaData: {
|
||||
altText: "default image",
|
||||
altText_En: "default image",
|
||||
copyRight: "default image",
|
||||
title: "default image",
|
||||
},
|
||||
imageSizes: {
|
||||
large: "https://placehold.co/1280x720",
|
||||
medium: "https://placehold.co/1280x720",
|
||||
small: "https://placehold.co/1280x720",
|
||||
tiny: "https://placehold.co/1280x720",
|
||||
},
|
||||
}),
|
||||
restaurantsOverviewPage: z.object({
|
||||
restaurantsContentDescriptionMedium: z.string().optional(),
|
||||
restaurantsContentDescriptionShort: z.string().optional(),
|
||||
restaurantsOverviewPageLink: z.string().optional(),
|
||||
restaurantsOverviewPageLinkText: z.string().optional(),
|
||||
}),
|
||||
texts: z.object({
|
||||
descriptions: z.object({
|
||||
medium: z.string(),
|
||||
short: z.string(),
|
||||
}),
|
||||
facilityInformation: z.string().optional(),
|
||||
meetingDescription: z
|
||||
.object({
|
||||
medium: z.string().optional(),
|
||||
short: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
surroundingInformation: z.string(),
|
||||
}),
|
||||
images: imageSchema,
|
||||
restaurantsOverviewPage: restaurantsOverviewPageSchema,
|
||||
texts: textsSchema,
|
||||
})
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { z } from "zod"
|
||||
|
||||
const detailedFacilitySchema = z.object({
|
||||
filter: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
import { nullableArrayObjectValidator } from "@/utils/zod/arrayValidator"
|
||||
import { nullableStringValidator } from "@/utils/zod/stringValidator"
|
||||
|
||||
import { FacilityEnum } from "@/types/enums/facilities"
|
||||
|
||||
export const detailedFacilitySchema = z.object({
|
||||
filter: nullableStringValidator,
|
||||
icon: nullableStringValidator,
|
||||
id: z.nativeEnum(FacilityEnum),
|
||||
name: nullableStringValidator,
|
||||
public: z.boolean(),
|
||||
sortOrder: z.number(),
|
||||
})
|
||||
|
||||
export const detailedFacilitiesSchema = z
|
||||
.array(detailedFacilitySchema)
|
||||
.transform((facilities) =>
|
||||
facilities.sort((a, b) => b.sortOrder - a.sortOrder)
|
||||
)
|
||||
export const detailedFacilitiesSchema = nullableArrayObjectValidator(
|
||||
detailedFacilitySchema
|
||||
).transform((facilities) =>
|
||||
facilities.sort((a, b) => b.sortOrder - a.sortOrder)
|
||||
)
|
||||
|
||||
@@ -1,50 +1,19 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { nullableStringValidator } from "@/utils/zod/stringValidator"
|
||||
|
||||
export const checkinSchema = z.object({
|
||||
checkInTime: nullableStringValidator,
|
||||
checkOutTime: nullableStringValidator,
|
||||
onlineCheckout: z.boolean(),
|
||||
onlineCheckOutAvailableFrom: nullableStringValidator,
|
||||
})
|
||||
|
||||
const ecoLabelsSchema = z.object({
|
||||
euEcoLabel: z.boolean(),
|
||||
greenGlobeLabel: z.boolean(),
|
||||
nordicEcoLabel: z.boolean(),
|
||||
svanenEcoLabelCertificateNumber: z.string().optional(),
|
||||
})
|
||||
|
||||
export const checkinSchema = z.object({
|
||||
checkInTime: z.string(),
|
||||
checkOutTime: z.string(),
|
||||
onlineCheckout: z.boolean(),
|
||||
onlineCheckOutAvailableFrom: z.string().nullable().optional(),
|
||||
})
|
||||
|
||||
const hotelFacilityDetailSchema = z
|
||||
.object({
|
||||
description: z.string(),
|
||||
heading: z.string(),
|
||||
})
|
||||
.optional()
|
||||
|
||||
/** Possibly more values */
|
||||
const hotelFacilityDetailsSchema = z.object({
|
||||
breakfast: hotelFacilityDetailSchema,
|
||||
checkout: hotelFacilityDetailSchema,
|
||||
gym: hotelFacilityDetailSchema,
|
||||
internet: hotelFacilityDetailSchema,
|
||||
laundry: hotelFacilityDetailSchema,
|
||||
luggage: hotelFacilityDetailSchema,
|
||||
shop: hotelFacilityDetailSchema,
|
||||
telephone: hotelFacilityDetailSchema,
|
||||
})
|
||||
|
||||
const hotelInformationSchema = z
|
||||
.object({
|
||||
description: z.string(),
|
||||
heading: z.string(),
|
||||
link: z.string().optional(),
|
||||
})
|
||||
.optional()
|
||||
|
||||
const hotelInformationsSchema = z.object({
|
||||
accessibility: hotelInformationSchema,
|
||||
safety: hotelInformationSchema,
|
||||
sustainability: hotelInformationSchema,
|
||||
svanenEcoLabelCertificateNumber: nullableStringValidator,
|
||||
})
|
||||
|
||||
const interiorSchema = z.object({
|
||||
@@ -53,7 +22,7 @@ const interiorSchema = z.object({
|
||||
numberOfFloors: z.number(),
|
||||
numberOfRooms: z.object({
|
||||
connected: z.number(),
|
||||
forAllergics: z.number().optional(),
|
||||
forAllergics: z.number(),
|
||||
forDisabled: z.number(),
|
||||
nonSmoking: z.number(),
|
||||
pet: z.number(),
|
||||
@@ -64,17 +33,15 @@ const interiorSchema = z.object({
|
||||
|
||||
const receptionHoursSchema = z.object({
|
||||
alwaysOpen: z.boolean(),
|
||||
closingTime: nullableStringValidator,
|
||||
isClosed: z.boolean(),
|
||||
openingTime: z.string().optional(),
|
||||
closingTime: z.string().optional(),
|
||||
openingTime: nullableStringValidator,
|
||||
})
|
||||
|
||||
export const hotelFactsSchema = z.object({
|
||||
checkin: checkinSchema,
|
||||
ecoLabels: ecoLabelsSchema,
|
||||
hotelFacilityDetail: hotelFacilityDetailsSchema.default({}),
|
||||
hotelInformation: hotelInformationsSchema.default({}),
|
||||
interior: interiorSchema,
|
||||
receptionHours: receptionHoursSchema,
|
||||
yearBuilt: z.string(),
|
||||
yearBuilt: nullableStringValidator,
|
||||
})
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { imageSchema } from "../image"
|
||||
|
||||
export const gallerySchema = z.object({
|
||||
heroImages: z.array(imageSchema),
|
||||
smallerImages: z.array(imageSchema),
|
||||
})
|
||||
@@ -1,41 +1,61 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { nullableArrayObjectValidator } from "@/utils/zod/arrayValidator"
|
||||
import { nullableNumberValidator } from "@/utils/zod/numberValidator"
|
||||
import { nullableStringValidator } from "@/utils/zod/stringValidator"
|
||||
|
||||
import { imageSchema } from "../image"
|
||||
|
||||
const healthFacilitiesOpenHoursSchema = z.object({
|
||||
alwaysOpen: z.boolean(),
|
||||
closingTime: z.string().optional(),
|
||||
closingTime: nullableStringValidator,
|
||||
isClosed: z.boolean(),
|
||||
openingTime: z.string().optional(),
|
||||
sortOrder: z.number().optional(),
|
||||
openingTime: nullableStringValidator,
|
||||
sortOrder: nullableNumberValidator,
|
||||
})
|
||||
|
||||
const descriptionSchema = z
|
||||
.object({
|
||||
medium: nullableStringValidator,
|
||||
short: nullableStringValidator,
|
||||
})
|
||||
.nullish()
|
||||
|
||||
const detailsSchema = z.object({
|
||||
name: nullableStringValidator,
|
||||
type: nullableStringValidator,
|
||||
value: nullableStringValidator,
|
||||
})
|
||||
|
||||
const textsSchema = z.object({
|
||||
descriptions: descriptionSchema,
|
||||
facilityInformation: nullableStringValidator,
|
||||
meetingDescription: descriptionSchema,
|
||||
surroundingInformation: nullableStringValidator,
|
||||
})
|
||||
|
||||
export const healthFacilitySchema = z.object({
|
||||
content: z.object({
|
||||
images: z.array(imageSchema),
|
||||
texts: z.object({
|
||||
descriptions: z.object({
|
||||
short: z.string(),
|
||||
medium: z.string(),
|
||||
}),
|
||||
facilityInformation: z.string().optional(),
|
||||
surroundingInformation: z.string().optional(),
|
||||
}),
|
||||
images: z
|
||||
.array(imageSchema)
|
||||
.nullish()
|
||||
.transform((arr) => (arr ? arr.filter(Boolean) : [])),
|
||||
texts: textsSchema,
|
||||
}),
|
||||
details: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
value: z.string().optional(),
|
||||
})
|
||||
),
|
||||
details: nullableArrayObjectValidator(detailsSchema),
|
||||
openingDetails: z.object({
|
||||
manualOpeningHours: z.string().optional(),
|
||||
manualOpeningHours: nullableStringValidator,
|
||||
openingHours: z.object({
|
||||
ordinary: healthFacilitiesOpenHoursSchema,
|
||||
weekends: healthFacilitiesOpenHoursSchema,
|
||||
}),
|
||||
useManualOpeningHours: z.boolean(),
|
||||
useManualOpeningHours: z
|
||||
.boolean()
|
||||
.nullish()
|
||||
.transform((b) => !!b),
|
||||
}),
|
||||
type: z.string(),
|
||||
type: nullableStringValidator,
|
||||
})
|
||||
|
||||
export const healthFacilitiesSchema =
|
||||
nullableArrayObjectValidator(healthFacilitySchema)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { nullableArrayObjectValidator } from "@/utils/zod/arrayValidator"
|
||||
import { nullableStringValidator } from "@/utils/zod/stringValidator"
|
||||
|
||||
import { displayWebPageSchema } from "./additionalData/displayWebPage"
|
||||
import { facilitySchema } from "./additionalData/facility"
|
||||
import { gallerySchema } from "./additionalData/gallery"
|
||||
import { restaurantsOverviewPageSchema } from "./additionalData/restaurantsOverviewPage"
|
||||
import { specialNeedGroupSchema } from "./additionalData/specialNeedGroups"
|
||||
|
||||
export const extraPageSchema = z.object({
|
||||
elevatorPitch: nullableStringValidator,
|
||||
mainBody: nullableStringValidator,
|
||||
nameInUrl: nullableStringValidator,
|
||||
})
|
||||
|
||||
export const additionalDataSchema = z.object({
|
||||
attributes: z.object({
|
||||
accessibility: facilitySchema.nullish(),
|
||||
conferencesAndMeetings: facilitySchema.nullish(),
|
||||
displayWebPage: displayWebPageSchema,
|
||||
gallery: gallerySchema.nullish(),
|
||||
healthAndFitness: extraPageSchema,
|
||||
healthAndWellness: facilitySchema.nullish(),
|
||||
hotelParking: extraPageSchema,
|
||||
hotelRoomElevatorPitchText: nullableStringValidator,
|
||||
hotelSpecialNeeds: extraPageSchema,
|
||||
id: nullableStringValidator,
|
||||
meetingRooms: extraPageSchema,
|
||||
name: nullableStringValidator,
|
||||
parkingImages: facilitySchema.nullish(),
|
||||
restaurantImages: facilitySchema.nullish(),
|
||||
restaurantsOverviewPage: restaurantsOverviewPageSchema,
|
||||
specialNeedGroups: nullableArrayObjectValidator(specialNeedGroupSchema),
|
||||
}),
|
||||
type: z.literal("additionalData"),
|
||||
})
|
||||
|
||||
export function transformAdditionalData(
|
||||
data: z.output<typeof additionalDataSchema>
|
||||
) {
|
||||
return {
|
||||
...data.attributes,
|
||||
id: data.attributes.id,
|
||||
type: data.type,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const displayWebPageSchema = z.object({
|
||||
healthGym: z.boolean(),
|
||||
meetingRoom: z.boolean(),
|
||||
parking: z.boolean(),
|
||||
specialNeeds: z.boolean(),
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { imageSchema } from "@/server/routers/hotels/schemas/image"
|
||||
|
||||
import { nullableStringValidator } from "@/utils/zod/stringValidator"
|
||||
|
||||
export const facilitySchema = z.object({
|
||||
headingText: nullableStringValidator,
|
||||
heroImages: z
|
||||
.array(imageSchema)
|
||||
.nullish()
|
||||
.transform((arr) => (arr ? arr.filter(Boolean) : [])),
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { imageSchema } from "@/server/routers/hotels/schemas/image"
|
||||
|
||||
const imagesSchema = z
|
||||
.array(imageSchema)
|
||||
.nullish()
|
||||
.transform((arr) => (arr ? arr.filter(Boolean) : []))
|
||||
|
||||
export const gallerySchema = z.object({
|
||||
heroImages: imagesSchema,
|
||||
smallerImages: imagesSchema,
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { nullableStringValidator } from "@/utils/zod/stringValidator"
|
||||
|
||||
export const restaurantsOverviewPageSchema = z.object({
|
||||
restaurantsContentDescriptionMedium: nullableStringValidator,
|
||||
restaurantsContentDescriptionShort: nullableStringValidator,
|
||||
restaurantsOverviewPageLink: nullableStringValidator,
|
||||
restaurantsOverviewPageLinkText: nullableStringValidator,
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { nullableArrayObjectValidator } from "@/utils/zod/arrayValidator"
|
||||
import { nullableStringValidator } from "@/utils/zod/stringValidator"
|
||||
|
||||
const specialNeedSchema = z.object({
|
||||
details: nullableStringValidator,
|
||||
name: nullableStringValidator,
|
||||
})
|
||||
|
||||
export const specialNeedGroupSchema = z.object({
|
||||
name: nullableStringValidator,
|
||||
specialNeeds: nullableArrayObjectValidator(specialNeedSchema),
|
||||
})
|
||||
@@ -8,10 +8,7 @@ import {
|
||||
transformRoomCategories,
|
||||
} from "@/server/routers/hotels/schemas/hotel/include/roomCategories"
|
||||
|
||||
import {
|
||||
additionalDataSchema,
|
||||
transformAdditionalData,
|
||||
} from "../../additionalData"
|
||||
import { additionalDataSchema, transformAdditionalData } from "./additionalData"
|
||||
|
||||
export const includeSchema = z
|
||||
.discriminatedUnion("type", [
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { imageMetaDataSchema, imageSizesSchema } from "../../image"
|
||||
import { imageSchema } from "@/server/routers/hotels/schemas/image"
|
||||
|
||||
import { nullableArrayObjectValidator } from "@/utils/zod/arrayValidator"
|
||||
import { nullableStringValidator } from "@/utils/zod/stringValidator"
|
||||
|
||||
const minMaxSchema = z.object({
|
||||
max: z.number(),
|
||||
@@ -8,8 +11,8 @@ const minMaxSchema = z.object({
|
||||
})
|
||||
|
||||
const bedTypeSchema = z.object({
|
||||
description: z.string().default(""),
|
||||
type: z.string(),
|
||||
description: nullableStringValidator,
|
||||
type: nullableStringValidator,
|
||||
widthRange: minMaxSchema,
|
||||
})
|
||||
|
||||
@@ -20,28 +23,26 @@ const occupancySchema = z.object({
|
||||
})
|
||||
|
||||
const roomContentSchema = z.object({
|
||||
images: z.array(
|
||||
z.object({
|
||||
imageSizes: imageSizesSchema,
|
||||
metaData: imageMetaDataSchema,
|
||||
})
|
||||
),
|
||||
images: z
|
||||
.array(imageSchema)
|
||||
.nullish()
|
||||
.transform((arr) => (arr ? arr.filter(Boolean) : [])),
|
||||
texts: z.object({
|
||||
descriptions: z.object({
|
||||
medium: z.string().optional(),
|
||||
short: z.string().optional(),
|
||||
medium: nullableStringValidator,
|
||||
short: nullableStringValidator,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const roomTypesSchema = z.object({
|
||||
code: z.string(),
|
||||
description: z.string(),
|
||||
code: nullableStringValidator,
|
||||
description: nullableStringValidator,
|
||||
fixedExtraBed: bedTypeSchema,
|
||||
isLackingCribs: z.boolean(),
|
||||
isLackingExtraBeds: z.boolean(),
|
||||
mainBed: bedTypeSchema,
|
||||
name: z.string(),
|
||||
name: nullableStringValidator,
|
||||
occupancy: occupancySchema,
|
||||
roomCount: z.number(),
|
||||
roomSize: minMaxSchema,
|
||||
@@ -58,11 +59,11 @@ const roomFacilitiesSchema = z.object({
|
||||
export const roomCategoriesSchema = z.object({
|
||||
attributes: z.object({
|
||||
content: roomContentSchema,
|
||||
name: z.string(),
|
||||
name: nullableStringValidator,
|
||||
occupancy: minMaxSchema,
|
||||
roomFacilities: z.array(roomFacilitiesSchema),
|
||||
roomFacilities: nullableArrayObjectValidator(roomFacilitiesSchema),
|
||||
roomSize: minMaxSchema,
|
||||
roomTypes: z.array(roomTypesSchema),
|
||||
roomTypes: nullableArrayObjectValidator(roomTypesSchema),
|
||||
sortOrder: z.number(),
|
||||
}),
|
||||
id: z.string(),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { nullableStringValidator } from "@/utils/zod/stringValidator"
|
||||
|
||||
import type { PaymentMethodEnum } from "@/constants/booking"
|
||||
|
||||
export const merchantInformationSchema = z.object({
|
||||
@@ -17,5 +19,5 @@ export const merchantInformationSchema = z.object({
|
||||
.map(([key]) => key)
|
||||
.filter((key): key is PaymentMethodEnum => !!key)
|
||||
}),
|
||||
webMerchantId: z.string().optional(),
|
||||
webMerchantId: nullableStringValidator,
|
||||
})
|
||||
|
||||
@@ -1,41 +1,45 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { nullableArrayObjectValidator } from "@/utils/zod/arrayValidator"
|
||||
import { nullableNumberValidator } from "@/utils/zod/numberValidator"
|
||||
import { nullableStringValidator } from "@/utils/zod/stringValidator"
|
||||
|
||||
const periodSchema = z.object({
|
||||
amount: z.number().optional(),
|
||||
endTime: z.string().optional(),
|
||||
period: z.string().optional(),
|
||||
startTime: z.string().optional(),
|
||||
amount: nullableNumberValidator,
|
||||
endTime: nullableStringValidator,
|
||||
period: nullableStringValidator,
|
||||
startTime: nullableStringValidator,
|
||||
})
|
||||
|
||||
const currencySchema = z
|
||||
.object({
|
||||
currency: z.string().optional(),
|
||||
ordinary: z.array(periodSchema).optional(),
|
||||
currency: nullableStringValidator,
|
||||
ordinary: nullableArrayObjectValidator(periodSchema),
|
||||
range: z
|
||||
.object({
|
||||
min: z.number().optional(),
|
||||
max: z.number().optional(),
|
||||
min: nullableNumberValidator,
|
||||
max: nullableNumberValidator,
|
||||
})
|
||||
.optional(),
|
||||
weekend: z.array(periodSchema).optional(),
|
||||
.nullish(),
|
||||
weekend: nullableArrayObjectValidator(periodSchema),
|
||||
})
|
||||
.optional()
|
||||
.nullish()
|
||||
|
||||
const pricingSchema = z.object({
|
||||
freeParking: z.boolean(),
|
||||
localCurrency: currencySchema,
|
||||
paymentType: z.string().optional(),
|
||||
paymentType: nullableStringValidator,
|
||||
requestedCurrency: currencySchema,
|
||||
})
|
||||
|
||||
export const parkingSchema = z.object({
|
||||
address: z.string().optional(),
|
||||
address: nullableStringValidator,
|
||||
canMakeReservation: z.boolean(),
|
||||
distanceToHotel: z.number().optional(),
|
||||
externalParkingUrl: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
numberOfChargingSpaces: z.number().optional(),
|
||||
numberOfParkingSpots: z.number().optional(),
|
||||
distanceToHotel: nullableNumberValidator,
|
||||
externalParkingUrl: nullableStringValidator,
|
||||
name: nullableStringValidator,
|
||||
numberOfChargingSpaces: nullableNumberValidator,
|
||||
numberOfParkingSpots: nullableNumberValidator,
|
||||
pricing: pricingSchema,
|
||||
type: z.string().optional(),
|
||||
type: nullableStringValidator,
|
||||
})
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { nullableNumberValidator } from "@/utils/zod/numberValidator"
|
||||
import { nullableStringValidator } from "@/utils/zod/stringValidator"
|
||||
|
||||
import { getPoiGroupByCategoryName } from "../../utils"
|
||||
import { locationSchema } from "./location"
|
||||
|
||||
export const pointOfInterestSchema = z
|
||||
.object({
|
||||
category: z.object({
|
||||
name: z.string().optional(),
|
||||
group: z.string().optional(),
|
||||
name: nullableStringValidator,
|
||||
}),
|
||||
distance: z.number().optional(),
|
||||
isHighlighted: z.boolean().optional(),
|
||||
location: locationSchema.optional(),
|
||||
name: z.string().optional(),
|
||||
distance: nullableNumberValidator,
|
||||
location: locationSchema,
|
||||
name: nullableStringValidator,
|
||||
})
|
||||
.transform((poi) => ({
|
||||
categoryName: poi.category.name,
|
||||
coordinates: {
|
||||
lat: poi.location?.latitude ?? 0,
|
||||
lng: poi.location?.longitude ?? 0,
|
||||
lat: poi.location.latitude,
|
||||
lng: poi.location.longitude,
|
||||
},
|
||||
distance: poi.distance,
|
||||
group: getPoiGroupByCategoryName(poi.category.name),
|
||||
@@ -27,6 +28,8 @@ export const pointOfInterestSchema = z
|
||||
|
||||
export const pointOfInterestsSchema = z
|
||||
.array(pointOfInterestSchema)
|
||||
.nullish()
|
||||
.transform((arr) => (arr ? arr.filter(Boolean) : []))
|
||||
.transform((pois) =>
|
||||
pois.sort((a, b) => (a.distance ?? 0) - (b.distance ?? 0))
|
||||
)
|
||||
|
||||
@@ -1,31 +1,57 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { nullableArrayObjectValidator } from "@/utils/zod/arrayValidator"
|
||||
import {
|
||||
nullableStringUrlValidator,
|
||||
nullableStringValidator,
|
||||
} from "@/utils/zod/stringValidator"
|
||||
|
||||
const awardSchema = z.object({
|
||||
displayName: z.string(),
|
||||
images: z.object({
|
||||
large: z.string(),
|
||||
medium: z.string(),
|
||||
small: z.string(),
|
||||
}),
|
||||
displayName: nullableStringValidator,
|
||||
images: z
|
||||
.object({
|
||||
large: nullableStringValidator,
|
||||
medium: nullableStringValidator,
|
||||
small: nullableStringValidator,
|
||||
})
|
||||
.nullish()
|
||||
.transform((obj) =>
|
||||
obj
|
||||
? obj
|
||||
: {
|
||||
small: "",
|
||||
medium: "",
|
||||
large: "",
|
||||
}
|
||||
),
|
||||
})
|
||||
|
||||
const reviewsSchema = z
|
||||
.object({
|
||||
widgetHtmlTagId: z.string(),
|
||||
widgetScriptEmbedUrlIframe: z.string(),
|
||||
widgetScriptEmbedUrlJavaScript: z.string(),
|
||||
widgetHtmlTagId: nullableStringValidator,
|
||||
widgetScriptEmbedUrlIframe: nullableStringValidator,
|
||||
widgetScriptEmbedUrlJavaScript: nullableStringValidator,
|
||||
})
|
||||
.optional()
|
||||
.nullish()
|
||||
.transform((obj) =>
|
||||
obj
|
||||
? obj
|
||||
: {
|
||||
widgetHtmlTagId: "",
|
||||
widgetScriptEmbedUrlIframe: "",
|
||||
widgetScriptEmbedUrlJavaScript: "",
|
||||
}
|
||||
)
|
||||
|
||||
export const ratingsSchema = z
|
||||
.object({
|
||||
tripAdvisor: z.object({
|
||||
awards: z.array(awardSchema),
|
||||
awards: nullableArrayObjectValidator(awardSchema),
|
||||
numberOfReviews: z.number(),
|
||||
rating: z.number(),
|
||||
ratingImageUrl: z.string(),
|
||||
ratingImageUrl: nullableStringUrlValidator,
|
||||
reviews: reviewsSchema,
|
||||
webUrl: z.string(),
|
||||
webUrl: nullableStringUrlValidator,
|
||||
}),
|
||||
})
|
||||
.optional()
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { nullableStringValidator } from "@/utils/zod/stringValidator"
|
||||
|
||||
export const rewardNightSchema = z.object({
|
||||
campaign: z.object({
|
||||
end: z.string(),
|
||||
end: nullableStringValidator,
|
||||
points: z.number(),
|
||||
start: z.string(),
|
||||
start: nullableStringValidator,
|
||||
}),
|
||||
points: z.number(),
|
||||
})
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { nullableStringValidator } from "@/utils/zod/stringValidator"
|
||||
|
||||
export const socialMediaSchema = z.object({
|
||||
facebook: z.string().optional(),
|
||||
instagram: z.string().optional(),
|
||||
facebook: nullableStringValidator,
|
||||
instagram: nullableStringValidator,
|
||||
})
|
||||
|
||||
@@ -17,6 +17,8 @@ const specialAlertSchema = z.object({
|
||||
|
||||
export const specialAlertsSchema = z
|
||||
.array(specialAlertSchema)
|
||||
.nullish()
|
||||
.transform((arr) => (arr ? arr.filter(Boolean) : []))
|
||||
.transform((data) => {
|
||||
const now = dt().utc().format("YYYY-MM-DD")
|
||||
const filteredAlerts = data.filter((alert) => {
|
||||
@@ -35,4 +37,3 @@ export const specialAlertsSchema = z
|
||||
displayInBookingFlow: alert.displayInBookingFlow,
|
||||
}))
|
||||
})
|
||||
.default([])
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { z } from "zod"
|
||||
|
||||
const specialNeedSchema = z.object({
|
||||
details: z.string(),
|
||||
name: z.string(),
|
||||
})
|
||||
|
||||
export const specialNeedGroupSchema = z.object({
|
||||
name: z.string(),
|
||||
specialNeeds: z.array(specialNeedSchema),
|
||||
})
|
||||
@@ -1,25 +1,27 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { nullableStringValidator } from "@/utils/zod/stringValidator"
|
||||
|
||||
export const imageSizesSchema = z.object({
|
||||
large: z.string(),
|
||||
medium: z.string(),
|
||||
small: z.string(),
|
||||
tiny: z.string(),
|
||||
large: nullableStringValidator,
|
||||
medium: nullableStringValidator,
|
||||
small: nullableStringValidator,
|
||||
tiny: nullableStringValidator,
|
||||
})
|
||||
|
||||
export const imageMetaDataSchema = z.object({
|
||||
altText: z.string(),
|
||||
altText_En: z.string(),
|
||||
copyRight: z.string(),
|
||||
title: z.string(),
|
||||
altText: nullableStringValidator,
|
||||
altText_En: nullableStringValidator,
|
||||
copyRight: nullableStringValidator,
|
||||
title: nullableStringValidator,
|
||||
})
|
||||
|
||||
const DEFAULT_IMAGE_OBJ = {
|
||||
metaData: {
|
||||
title: "Default image",
|
||||
altText: "Default image",
|
||||
altText_En: "Default image",
|
||||
copyRight: "Default image",
|
||||
title: "Default image",
|
||||
},
|
||||
imageSizes: {
|
||||
tiny: "https://placehold.co/1280x720",
|
||||
@@ -31,11 +33,10 @@ const DEFAULT_IMAGE_OBJ = {
|
||||
|
||||
export const imageSchema = z
|
||||
.object({
|
||||
metaData: imageMetaDataSchema,
|
||||
imageSizes: imageSizesSchema,
|
||||
metaData: imageMetaDataSchema,
|
||||
})
|
||||
.default(DEFAULT_IMAGE_OBJ)
|
||||
.nullable()
|
||||
.nullish()
|
||||
.transform((val) => {
|
||||
if (!val) {
|
||||
return DEFAULT_IMAGE_OBJ
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
} from "./output"
|
||||
import { getHotel } from "./query"
|
||||
|
||||
import type { Country } from "@/types/enums/country"
|
||||
import { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest"
|
||||
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
||||
import type { HotelDataWithUrl } from "@/types/hotel"
|
||||
|
||||
@@ -120,6 +120,19 @@ export function createRatesStore({
|
||||
|
||||
return create<RatesState>()((set) => ({
|
||||
actions: {
|
||||
closeSection(idx) {
|
||||
return function () {
|
||||
return set(
|
||||
produce((state: RatesState) => {
|
||||
if (state.rateSummary.length === state.booking.rooms.length) {
|
||||
state.activeRoom = -1
|
||||
} else {
|
||||
state.activeRoom = idx + 1
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
modifyRate(idx) {
|
||||
return function () {
|
||||
return set(
|
||||
@@ -201,10 +214,11 @@ export function createRatesStore({
|
||||
selectedRate.roomTypeCode
|
||||
)
|
||||
|
||||
state.activeRoom =
|
||||
idx + 1 < state.booking.rooms.length
|
||||
? idx + 1
|
||||
: state.booking.rooms.length
|
||||
if (state.rateSummary.length === state.booking.rooms.length) {
|
||||
state.activeRoom = -1
|
||||
} else {
|
||||
state.activeRoom = idx + 1
|
||||
}
|
||||
|
||||
state.searchParams = new ReadonlyURLSearchParams(searchParams)
|
||||
window.history.pushState(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Hotel } from "@/types/hotel"
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
|
||||
export type CategorizedFilters = {
|
||||
facilityFilters: Hotel["detailedFacilities"]
|
||||
@@ -9,15 +9,6 @@ export type HotelFiltersProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export type Filter = {
|
||||
name: string
|
||||
id: number
|
||||
public: boolean
|
||||
sortOrder: number
|
||||
filter?: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
export type HotelFilterModalProps = {
|
||||
filters: CategorizedFilters
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { z } from "zod"
|
||||
|
||||
import type { Coordinates } from "@/types/components/maps/coordinates"
|
||||
import type { Location } from "@/types/trpc/routers/hotel/locations"
|
||||
import type { Amenities } from "@/types/hotel"
|
||||
import type { imageSchema } from "@/server/routers/hotels/schemas/image"
|
||||
import type { Child } from "../selectRate/selectRate"
|
||||
import type { HotelData } from "./hotelCardListingProps"
|
||||
import type { CategorizedFilters, Filter } from "./hotelFilters"
|
||||
import type { CategorizedFilters } from "./hotelFilters"
|
||||
import type {
|
||||
AlternativeHotelsSearchParams,
|
||||
SelectHotelSearchParams,
|
||||
@@ -39,7 +38,7 @@ export type HotelPin = {
|
||||
imageSizes: ImageSizes
|
||||
metaData: ImageMetaData
|
||||
}[]
|
||||
amenities: Filter[]
|
||||
amenities: Amenities
|
||||
ratings: number | null
|
||||
operaId: string
|
||||
facilityIds: number[]
|
||||
|
||||
@@ -2,4 +2,5 @@ export type ToggleSidePeekProps = {
|
||||
hotelId: string
|
||||
roomTypeCode?: string
|
||||
intent?: "text" | "textInverted"
|
||||
title?: string
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { ImageProps as NextImageProps } from "next/image"
|
||||
|
||||
import type { ImageVaultAsset } from "./imageVault"
|
||||
|
||||
export interface FocalPoint {
|
||||
x: number
|
||||
y: number
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { SelectedRate, SelectedRoom } from "@/types/stores/rates"
|
||||
|
||||
export interface RoomContextValue extends SelectedRoom {
|
||||
actions: {
|
||||
closeSection: () => void
|
||||
modifyRate: () => void
|
||||
selectFilter: (code: RoomPackageCodeEnum | undefined) => void
|
||||
selectRate: (rate: SelectedRate) => void
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import type { z } from "zod"
|
||||
|
||||
import type { hotelSchema } from "@/server/routers/hotels/output"
|
||||
import type {
|
||||
extraPageSchema,
|
||||
facilitySchema,
|
||||
transformAdditionalData,
|
||||
} from "@/server/routers/hotels/schemas/additionalData"
|
||||
import type { citySchema } from "@/server/routers/hotels/schemas/city"
|
||||
import type { attributesSchema } from "@/server/routers/hotels/schemas/hotel"
|
||||
import type { addressSchema } from "@/server/routers/hotels/schemas/hotel/address"
|
||||
import type { hotelContentSchema } from "@/server/routers/hotels/schemas/hotel/content"
|
||||
import type { detailedFacilitiesSchema } from "@/server/routers/hotels/schemas/hotel/detailedFacility"
|
||||
import type {
|
||||
detailedFacilitiesSchema,
|
||||
detailedFacilitySchema,
|
||||
} from "@/server/routers/hotels/schemas/hotel/detailedFacility"
|
||||
import type { checkinSchema } from "@/server/routers/hotels/schemas/hotel/facts"
|
||||
import type { healthFacilitySchema } from "@/server/routers/hotels/schemas/hotel/healthFacilities"
|
||||
import type {
|
||||
extraPageSchema,
|
||||
transformAdditionalData,
|
||||
} from "@/server/routers/hotels/schemas/hotel/include/additionalData"
|
||||
import type { facilitySchema } from "@/server/routers/hotels/schemas/hotel/include/additionalData/facility"
|
||||
import type { nearbyHotelsSchema } from "@/server/routers/hotels/schemas/hotel/include/nearbyHotels"
|
||||
import type {
|
||||
openingHoursDetailsSchema,
|
||||
@@ -35,6 +38,7 @@ export type City = Pick<CitySchema, "id" | "type"> & CitySchema["attributes"]
|
||||
export type FacilityData = z.output<typeof facilitySchema>
|
||||
export type Facility = FacilityData & { id: string }
|
||||
export type ApiImage = z.output<typeof imageSchema>
|
||||
export type DetailedFacility = z.output<typeof detailedFacilitySchema>
|
||||
export type HealthFacility = z.output<typeof healthFacilitySchema>
|
||||
export type HealthFacilities = HealthFacility[]
|
||||
export type Hotel = z.output<typeof attributesSchema>
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
} from "@/types/trpc/routers/hotel/roomAvailability"
|
||||
|
||||
interface Actions {
|
||||
closeSection: (idx: number) => () => void
|
||||
modifyRate: (idx: number) => () => void
|
||||
selectFilter: (idx: number) => (code: RoomPackageCodeEnum | undefined) => void
|
||||
selectRate: (idx: number) => (rate: SelectedRate) => void
|
||||
|
||||
17
utils/zod/arrayValidator.ts
Normal file
17
utils/zod/arrayValidator.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { nullableStringValidator } from "./stringValidator"
|
||||
|
||||
import type { ZodObject, ZodRawShape } from "zod"
|
||||
|
||||
export function nullableArrayObjectValidator<T extends ZodRawShape>(
|
||||
schema: ZodObject<T>
|
||||
) {
|
||||
return schema
|
||||
.array()
|
||||
.nullish()
|
||||
.transform((arr) => (arr ? arr.filter(Boolean) : []))
|
||||
}
|
||||
|
||||
export const nullableArrayStringValidator = nullableStringValidator
|
||||
.array()
|
||||
.nullish()
|
||||
.transform((arr) => (arr ? arr.filter(Boolean) : []))
|
||||
@@ -5,6 +5,12 @@ export const nullableStringValidator = z
|
||||
.nullish()
|
||||
.transform((str) => (str ? str : ""))
|
||||
|
||||
export const nullableStringEmailValidator = z
|
||||
.string()
|
||||
.email()
|
||||
.nullish()
|
||||
.transform((str) => (str ? str : ""))
|
||||
|
||||
export const nullableStringUrlValidator = z
|
||||
.string()
|
||||
.url()
|
||||
|
||||
Reference in New Issue
Block a user