Merge branch 'develop' into feature/tracking
This commit is contained in:
@@ -2,7 +2,6 @@ import { serverClient } from "@/lib/trpc/server"
|
|||||||
import tempHotelData from "@/server/routers/hotels/tempHotelData.json"
|
import tempHotelData from "@/server/routers/hotels/tempHotelData.json"
|
||||||
|
|
||||||
import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection"
|
import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection"
|
||||||
import { getIntl } from "@/i18n"
|
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import styles from "./page.module.css"
|
import styles from "./page.module.css"
|
||||||
@@ -19,18 +18,15 @@ export default async function SelectRatePage({
|
|||||||
// TODO: Use real endpoint.
|
// TODO: Use real endpoint.
|
||||||
const hotel = tempHotelData.data.attributes
|
const hotel = tempHotelData.data.attributes
|
||||||
|
|
||||||
const rates = await serverClient().hotel.rates.get({
|
const roomConfigurations = await serverClient().hotel.availability.rooms({
|
||||||
// TODO: pass the correct hotel ID and all other parameters that should be included in the search
|
hotelId: parseInt(searchParams.hotel, 10),
|
||||||
hotelId: searchParams.hotel,
|
roomStayStartDate: "2024-11-02",
|
||||||
|
roomStayEndDate: "2024-11-03",
|
||||||
|
adults: 1,
|
||||||
})
|
})
|
||||||
|
if (!roomConfigurations) {
|
||||||
// const rates = await serverClient().hotel.availability.getForHotel({
|
return "No rooms found"
|
||||||
// hotelId: 811,
|
}
|
||||||
// roomStayStartDate: "2024-11-02",
|
|
||||||
// roomStayEndDate: "2024-11-03",
|
|
||||||
// adults: 1,
|
|
||||||
// })
|
|
||||||
const intl = await getIntl()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -39,13 +35,7 @@ export default async function SelectRatePage({
|
|||||||
|
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<div className={styles.main}>
|
<div className={styles.main}>
|
||||||
<RoomSelection
|
<RoomSelection roomConfigurations={roomConfigurations} />
|
||||||
rates={rates}
|
|
||||||
// TODO: Get real value
|
|
||||||
nrOfNights={1}
|
|
||||||
// TODO: Get real value
|
|
||||||
nrOfAdults={1}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
107
components/Blocks/Table/index.tsx
Normal file
107
components/Blocks/Table/index.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import {
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
import SectionContainer from "@/components/Section/Container"
|
||||||
|
import SectionHeader from "@/components/Section/Header"
|
||||||
|
import ScrollWrapper from "@/components/TempDesignSystem/ScrollWrapper"
|
||||||
|
import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
|
||||||
|
import Table from "@/components/TempDesignSystem/Table"
|
||||||
|
|
||||||
|
import styles from "./table.module.css"
|
||||||
|
|
||||||
|
import type { TableBlockProps } from "@/types/components/blocks/table"
|
||||||
|
|
||||||
|
export default function TableBlock({ data }: TableBlockProps) {
|
||||||
|
const { columns, rows, totalWidth, heading, preamble } = data
|
||||||
|
const initialPageSize = 5
|
||||||
|
const [pageSize, setPageSize] = useState(initialPageSize)
|
||||||
|
const showMoreVisible = rows.length > initialPageSize
|
||||||
|
const showLessVisible = pageSize >= rows.length
|
||||||
|
const columnDefs = columns.map((col) => ({
|
||||||
|
accessorKey: col.id,
|
||||||
|
header: col.header,
|
||||||
|
size: col.width,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
columns: columnDefs,
|
||||||
|
data: rows,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
state: {
|
||||||
|
pagination: {
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleShowMore() {
|
||||||
|
setPageSize(showLessVisible ? initialPageSize : rows.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionContainer>
|
||||||
|
{heading ? (
|
||||||
|
<SectionHeader preamble={data.preamble} title={heading} />
|
||||||
|
) : null}
|
||||||
|
<div className={styles.tableWrapper}>
|
||||||
|
<ScrollWrapper>
|
||||||
|
<Table
|
||||||
|
width={`${totalWidth}%`}
|
||||||
|
variant="content"
|
||||||
|
intent="striped"
|
||||||
|
layout="fixed"
|
||||||
|
borderRadius="none"
|
||||||
|
>
|
||||||
|
<Table.THead>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<Table.TR key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => (
|
||||||
|
<Table.TH
|
||||||
|
key={header.id}
|
||||||
|
width={`${header.column.columnDef.size}%`}
|
||||||
|
>
|
||||||
|
{flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
</Table.TH>
|
||||||
|
))}
|
||||||
|
</Table.TR>
|
||||||
|
))}
|
||||||
|
</Table.THead>
|
||||||
|
<Table.TBody>
|
||||||
|
{table.getRowModel().rows.map((row) => (
|
||||||
|
<Table.TR key={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<Table.TD key={cell.id}>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext()
|
||||||
|
)}
|
||||||
|
</Table.TD>
|
||||||
|
))}
|
||||||
|
</Table.TR>
|
||||||
|
))}
|
||||||
|
</Table.TBody>
|
||||||
|
</Table>
|
||||||
|
</ScrollWrapper>
|
||||||
|
{showMoreVisible ? (
|
||||||
|
<ShowMoreButton
|
||||||
|
loadMoreData={handleShowMore}
|
||||||
|
showLess={showLessVisible}
|
||||||
|
intent="table"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</SectionContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
6
components/Blocks/Table/table.module.css
Normal file
6
components/Blocks/Table/table.module.css
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.tableWrapper {
|
||||||
|
display: grid;
|
||||||
|
border: 1px solid var(--Base-Border-Subtle);
|
||||||
|
border-radius: var(--Corner-radius-Small);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import TextCols from "@/components/Blocks/TextCols"
|
|||||||
import UspGrid from "@/components/Blocks/UspGrid"
|
import UspGrid from "@/components/Blocks/UspGrid"
|
||||||
import JsonToHtml from "@/components/JsonToHtml"
|
import JsonToHtml from "@/components/JsonToHtml"
|
||||||
|
|
||||||
|
import Table from "./Table"
|
||||||
|
|
||||||
import type { BlocksProps } from "@/types/components/blocks"
|
import type { BlocksProps } from "@/types/components/blocks"
|
||||||
import { BlocksEnums } from "@/types/enums/blocks"
|
import { BlocksEnums } from "@/types/enums/blocks"
|
||||||
|
|
||||||
@@ -47,6 +49,8 @@ export default function Blocks({ blocks }: BlocksProps) {
|
|||||||
title={block.shortcuts.title}
|
title={block.shortcuts.title}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
case BlocksEnums.block.Table:
|
||||||
|
return <Table data={block.table} />
|
||||||
case BlocksEnums.block.TextCols:
|
case BlocksEnums.block.TextCols:
|
||||||
return <TextCols text_cols={block.text_cols} />
|
return <TextCols text_cols={block.text_cols} />
|
||||||
case BlocksEnums.block.TextContent:
|
case BlocksEnums.block.TextContent:
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.05);
|
box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.05);
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 10000;
|
z-index: 9;
|
||||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mainContent {
|
.mainContent {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x4);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { FormProvider, useForm } from "react-hook-form"
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { KingBedIcon } from "@/components/Icons"
|
import { KingBedIcon } from "@/components/Icons"
|
||||||
import RadioCard from "@/components/TempDesignSystem/Form/Card/Radio"
|
import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio"
|
||||||
|
|
||||||
import { bedTypeSchema } from "./schema"
|
import { bedTypeSchema } from "./schema"
|
||||||
|
|
||||||
@@ -24,9 +24,7 @@ export default function BedType() {
|
|||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
})
|
})
|
||||||
|
|
||||||
// @ts-expect-error - Types mismatch docs as this is
|
const text = intl.formatMessage<React.ReactNode>(
|
||||||
// a pattern that is allowed https://formatjs.io/docs/react-intl/api#usage
|
|
||||||
const text = intl.formatMessage(
|
|
||||||
{ id: "<b>Included</b> (based on availability)" },
|
{ id: "<b>Included</b> (based on availability)" },
|
||||||
{ b: (str) => <b>{str}</b> }
|
{ b: (str) => <b>{str}</b> }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { FormProvider, useForm } from "react-hook-form"
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { BreakfastIcon, NoBreakfastIcon } from "@/components/Icons"
|
import { BreakfastIcon, NoBreakfastIcon } from "@/components/Icons"
|
||||||
import RadioCard from "@/components/TempDesignSystem/Form/Card/Radio"
|
import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio"
|
||||||
|
|
||||||
import { breakfastSchema } from "./schema"
|
import { breakfastSchema } from "./schema"
|
||||||
|
|
||||||
@@ -31,9 +31,7 @@ export default function Breakfast() {
|
|||||||
Icon={BreakfastIcon}
|
Icon={BreakfastIcon}
|
||||||
id={breakfastEnum.BREAKFAST}
|
id={breakfastEnum.BREAKFAST}
|
||||||
name="breakfast"
|
name="breakfast"
|
||||||
// @ts-expect-error - Types mismatch docs as this is
|
subtitle={intl.formatMessage<React.ReactNode>(
|
||||||
// a pattern that is allowed https://formatjs.io/docs/react-intl/api#usage
|
|
||||||
subtitle={intl.formatMessage(
|
|
||||||
{ id: "<b>{amount} {currency}</b>/night per adult" },
|
{ id: "<b>{amount} {currency}</b>/night per adult" },
|
||||||
{
|
{
|
||||||
amount: "150",
|
amount: "150",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { FormProvider, useForm } from "react-hook-form"
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import CheckboxCard from "@/components/TempDesignSystem/Form/Card/Checkbox"
|
import CheckboxCard from "@/components/TempDesignSystem/Form/ChoiceCard/Checkbox"
|
||||||
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
||||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||||
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
||||||
@@ -88,8 +88,9 @@ export default function Details({ user }: DetailsProps) {
|
|||||||
<footer className={styles.footer}>
|
<footer className={styles.footer}>
|
||||||
{user ? null : (
|
{user ? null : (
|
||||||
<CheckboxCard
|
<CheckboxCard
|
||||||
|
highlightSubtitle
|
||||||
list={list}
|
list={list}
|
||||||
saving
|
name="join"
|
||||||
subtitle={intl.formatMessage(
|
subtitle={intl.formatMessage(
|
||||||
{
|
{
|
||||||
id: "{difference}{amount} {currency}",
|
id: "{difference}{amount} {currency}",
|
||||||
|
|||||||
@@ -9,17 +9,31 @@ import styles from "./flexibilityOption.module.css"
|
|||||||
import { FlexibilityOptionProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
|
import { FlexibilityOptionProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
|
||||||
|
|
||||||
export default function FlexibilityOption({
|
export default function FlexibilityOption({
|
||||||
currency,
|
product,
|
||||||
standardPrice,
|
|
||||||
memberPrice,
|
|
||||||
name,
|
name,
|
||||||
value,
|
|
||||||
paymentTerm,
|
paymentTerm,
|
||||||
}: FlexibilityOptionProps) {
|
}: FlexibilityOptionProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
// TODO: Implement empty state when this rate can't be booked
|
||||||
|
return <div>TBI: Rate not available</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const { productType } = product
|
||||||
|
const { public: publicPrice, member: memberPrice } = productType
|
||||||
|
const { localPrice: publicLocalPrice, requestedPrice: publicRequestedPrice } =
|
||||||
|
publicPrice
|
||||||
|
const { localPrice: memberLocalPrice, requestedPrice: memberRequestedPrice } =
|
||||||
|
memberPrice
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" name="flexibility" value={value} />
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="rateCode"
|
||||||
|
value={product.productType.public.rateCode}
|
||||||
|
/>
|
||||||
<div className={styles.card}>
|
<div className={styles.card}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<Body>{name}</Body>
|
<Body>{name}</Body>
|
||||||
@@ -29,15 +43,27 @@ export default function FlexibilityOption({
|
|||||||
<div>
|
<div>
|
||||||
<dt>{intl.formatMessage({ id: "Standard price" })}</dt>
|
<dt>{intl.formatMessage({ id: "Standard price" })}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{standardPrice} {currency}
|
{publicLocalPrice.pricePerNight} {publicLocalPrice.currency}/
|
||||||
|
{intl.formatMessage({ id: "night" })}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>{intl.formatMessage({ id: "Member price" })}</dt>
|
<dt>{intl.formatMessage({ id: "Member price" })}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{memberPrice} {currency}
|
{memberLocalPrice.pricePerNight} {memberLocalPrice.currency}/
|
||||||
|
{intl.formatMessage({ id: "night" })}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
{publicRequestedPrice && memberRequestedPrice && (
|
||||||
|
<div>
|
||||||
|
<dt>{intl.formatMessage({ id: "Approx." })}</dt>
|
||||||
|
<dd>
|
||||||
|
{publicRequestedPrice.pricePerNight}/
|
||||||
|
{memberRequestedPrice.pricePerNight}{" "}
|
||||||
|
{publicRequestedPrice.currency}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -11,20 +11,54 @@ import styles from "./roomCard.module.css"
|
|||||||
import { RoomCardProps } from "@/types/components/hotelReservation/selectRate/roomCard"
|
import { RoomCardProps } from "@/types/components/hotelReservation/selectRate/roomCard"
|
||||||
|
|
||||||
export default function RoomCard({
|
export default function RoomCard({
|
||||||
room,
|
rateDefinitions,
|
||||||
nrOfAdults,
|
roomConfiguration,
|
||||||
nrOfNights,
|
|
||||||
breakfastIncluded,
|
|
||||||
}: RoomCardProps) {
|
}: RoomCardProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
|
||||||
|
const saveRate = rateDefinitions.find(
|
||||||
|
// TODO: Update string when API has decided
|
||||||
|
(rate) => rate.cancellationRule === "NonCancellable"
|
||||||
|
)
|
||||||
|
const changeRate = rateDefinitions.find(
|
||||||
|
// TODO: Update string when API has decided
|
||||||
|
(rate) => rate.cancellationRule === "Modifiable"
|
||||||
|
)
|
||||||
|
const flexRate = rateDefinitions.find(
|
||||||
|
// TODO: Update string when API has decided
|
||||||
|
(rate) => rate.cancellationRule === "CancellableBefore6PM"
|
||||||
|
)
|
||||||
|
|
||||||
|
const saveProduct = saveRate
|
||||||
|
? roomConfiguration.products.find(
|
||||||
|
(product) =>
|
||||||
|
product.productType.public.rateCode === saveRate.rateCode ||
|
||||||
|
product.productType.member.rateCode === saveRate.rateCode
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
const changeProduct = changeRate
|
||||||
|
? roomConfiguration.products.find(
|
||||||
|
(product) =>
|
||||||
|
product.productType.public.rateCode === changeRate.rateCode ||
|
||||||
|
product.productType.member.rateCode === changeRate.rateCode
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
const flexProduct = flexRate
|
||||||
|
? roomConfiguration.products.find(
|
||||||
|
(product) =>
|
||||||
|
product.productType.public.rateCode === flexRate.rateCode ||
|
||||||
|
product.productType.member.rateCode === flexRate.rateCode
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.card}>
|
<div className={styles.card}>
|
||||||
<div className={styles.cardBody}>
|
<div className={styles.cardBody}>
|
||||||
<div className={styles.specification}>
|
<div className={styles.specification}>
|
||||||
<Subtitle className={styles.name} type="two">
|
<Subtitle className={styles.name} type="two">
|
||||||
{room.name}
|
{roomConfiguration.roomType}
|
||||||
</Subtitle>
|
</Subtitle>
|
||||||
<Caption>{room.size}</Caption>
|
<Caption>Room size TBI</Caption>
|
||||||
<Button intent="text" type="button" size="small" theme="base">
|
<Button intent="text" type="button" size="small" theme="base">
|
||||||
{intl.formatMessage({ id: "See room details" })}
|
{intl.formatMessage({ id: "See room details" })}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -32,20 +66,15 @@ export default function RoomCard({
|
|||||||
{/*TODO: Handle pluralisation*/}
|
{/*TODO: Handle pluralisation*/}
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{
|
{
|
||||||
id: "Nr night, nr adult",
|
id: "Max {nrOfGuests} guests",
|
||||||
defaultMessage:
|
defaultMessage: "Max {nrOfGuests} guests",
|
||||||
"{nights, number} night, {adults, number} adult",
|
|
||||||
},
|
},
|
||||||
{ nights: nrOfNights, adults: nrOfAdults }
|
// TODO: Correct number
|
||||||
|
{ nrOfGuests: 2 }
|
||||||
)}
|
)}
|
||||||
{" | "}
|
{intl.formatMessage({
|
||||||
{breakfastIncluded
|
id: "Breakfast included",
|
||||||
? intl.formatMessage({
|
})}
|
||||||
id: "Breakfast included",
|
|
||||||
})
|
|
||||||
: intl.formatMessage({
|
|
||||||
id: "Breakfast excluded",
|
|
||||||
})}
|
|
||||||
</Caption>
|
</Caption>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -53,25 +82,19 @@ export default function RoomCard({
|
|||||||
name={intl.formatMessage({ id: "Non-refundable" })}
|
name={intl.formatMessage({ id: "Non-refundable" })}
|
||||||
value="non-refundable"
|
value="non-refundable"
|
||||||
paymentTerm={intl.formatMessage({ id: "Pay now" })}
|
paymentTerm={intl.formatMessage({ id: "Pay now" })}
|
||||||
standardPrice={room.prices.nonRefundable.standard}
|
product={saveProduct}
|
||||||
memberPrice={room.prices.nonRefundable.member}
|
|
||||||
currency={room.prices.currency}
|
|
||||||
/>
|
/>
|
||||||
<FlexibilityOption
|
<FlexibilityOption
|
||||||
name={intl.formatMessage({ id: "Free rebooking" })}
|
name={intl.formatMessage({ id: "Free rebooking" })}
|
||||||
value="free-rebooking"
|
value="free-rebooking"
|
||||||
paymentTerm={intl.formatMessage({ id: "Pay now" })}
|
paymentTerm={intl.formatMessage({ id: "Pay now" })}
|
||||||
standardPrice={room.prices.freeRebooking.standard}
|
product={changeProduct}
|
||||||
memberPrice={room.prices.freeRebooking.member}
|
|
||||||
currency={room.prices.currency}
|
|
||||||
/>
|
/>
|
||||||
<FlexibilityOption
|
<FlexibilityOption
|
||||||
name={intl.formatMessage({ id: "Free cancellation" })}
|
name={intl.formatMessage({ id: "Free cancellation" })}
|
||||||
value="free-cancellation"
|
value="free-cancellation"
|
||||||
paymentTerm={intl.formatMessage({ id: "Pay later" })}
|
paymentTerm={intl.formatMessage({ id: "Pay later" })}
|
||||||
standardPrice={room.prices.freeCancellation.standard}
|
product={flexProduct}
|
||||||
memberPrice={room.prices.freeCancellation.member}
|
|
||||||
currency={room.prices.currency}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -87,7 +110,8 @@ export default function RoomCard({
|
|||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img
|
<img
|
||||||
alt={intl.formatMessage({ id: "A photo of the room" })}
|
alt={intl.formatMessage({ id: "A photo of the room" })}
|
||||||
src={room.imageSrc}
|
// TODO: Correct image URL
|
||||||
|
src="https://www.scandichotels.se/imageVault/publishedmedia/xnmqnmz6mz0uhuat0917/scandic-helsinki-hub-room-standard-KR-7.jpg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useRouter, useSearchParams } from "next/navigation"
|
import { useRouter, useSearchParams } from "next/navigation"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
|
||||||
import RoomCard from "./RoomCard"
|
import RoomCard from "./RoomCard"
|
||||||
|
|
||||||
@@ -8,12 +11,11 @@ import styles from "./roomSelection.module.css"
|
|||||||
import { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
import { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
||||||
|
|
||||||
export default function RoomSelection({
|
export default function RoomSelection({
|
||||||
rates,
|
roomConfigurations,
|
||||||
nrOfNights,
|
|
||||||
nrOfAdults,
|
|
||||||
}: RoomSelectionProps) {
|
}: RoomSelectionProps) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -25,31 +27,28 @@ export default function RoomSelection({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<ul className={styles.roomList}>
|
<form
|
||||||
{rates.map((room) => (
|
method="GET"
|
||||||
<li key={room.id}>
|
action={`select-bed?${searchParams}`}
|
||||||
<form
|
onSubmit={handleSubmit}
|
||||||
method="GET"
|
>
|
||||||
action={`select-bed?${searchParams}`}
|
<ul className={styles.roomList}>
|
||||||
onSubmit={handleSubmit}
|
{roomConfigurations.roomConfigurations.map((roomConfiguration) => (
|
||||||
>
|
<li key={roomConfiguration.roomType}>
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="roomClass"
|
|
||||||
value={room.id}
|
|
||||||
id={`room-${room.id}`}
|
|
||||||
/>
|
|
||||||
<RoomCard
|
<RoomCard
|
||||||
room={room}
|
rateDefinitions={roomConfigurations.rateDefinitions}
|
||||||
nrOfAdults={nrOfAdults}
|
roomConfiguration={roomConfiguration}
|
||||||
nrOfNights={nrOfNights}
|
|
||||||
breakfastIncluded={room.breakfastIncluded}
|
|
||||||
/>
|
/>
|
||||||
</form>
|
</li>
|
||||||
</li>
|
))}
|
||||||
))}
|
</ul>
|
||||||
</ul>
|
<div className={styles.summary}>
|
||||||
<div className={styles.summary}>This is summary</div>
|
This is summary
|
||||||
|
<Button type="submit" size="small" theme="primaryDark">
|
||||||
|
{intl.formatMessage({ id: "Choose room" })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,4 +27,6 @@
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
background-color: white;
|
||||||
|
padding: var(--Spacing-x3) var(--Spacing-x7) var(--Spacing-x5);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Card from "."
|
import Card from "./_Card"
|
||||||
|
|
||||||
import type { CheckboxProps } from "./card"
|
import type { CheckboxProps } from "./_Card/card"
|
||||||
|
|
||||||
export default function CheckboxCard(props: CheckboxProps) {
|
export default function CheckboxCard(props: CheckboxProps) {
|
||||||
return <Card {...props} type="checkbox" />
|
return <Card {...props} type="checkbox" />
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import Card from "."
|
import Card from "./_Card"
|
||||||
|
|
||||||
import type { RadioProps } from "./card"
|
import type { RadioProps } from "./_Card/card"
|
||||||
|
|
||||||
export default function RadioCard(props: RadioProps) {
|
export default function RadioCard(props: RadioProps) {
|
||||||
return <Card {...props} type="radio" />
|
return <Card {...props} type="radio" />
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import type { IconProps } from "@/types/components/icon"
|
import type { IconProps } from "@/types/components/icon"
|
||||||
|
|
||||||
interface BaseCardProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
|
interface BaseCardProps
|
||||||
|
extends Omit<React.LabelHTMLAttributes<HTMLLabelElement>, "title"> {
|
||||||
Icon?: (props: IconProps) => JSX.Element
|
Icon?: (props: IconProps) => JSX.Element
|
||||||
declined?: boolean
|
declined?: boolean
|
||||||
|
highlightSubtitle?: boolean
|
||||||
iconHeight?: number
|
iconHeight?: number
|
||||||
iconWidth?: number
|
iconWidth?: number
|
||||||
name?: string
|
name: string
|
||||||
saving?: boolean
|
subtitle?: React.ReactNode
|
||||||
subtitle?: string
|
title: React.ReactNode
|
||||||
title: string
|
|
||||||
type: "checkbox" | "radio"
|
type: "checkbox" | "radio"
|
||||||
value?: string
|
value?: string
|
||||||
}
|
}
|
||||||
@@ -22,7 +23,7 @@ interface ListCardProps extends BaseCardProps {
|
|||||||
|
|
||||||
interface TextCardProps extends BaseCardProps {
|
interface TextCardProps extends BaseCardProps {
|
||||||
list?: never
|
list?: never
|
||||||
text: string
|
text: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CardProps = ListCardProps | TextCardProps
|
export type CardProps = ListCardProps | TextCardProps
|
||||||
@@ -13,10 +13,10 @@ export default function Card({
|
|||||||
iconHeight = 32,
|
iconHeight = 32,
|
||||||
iconWidth = 32,
|
iconWidth = 32,
|
||||||
declined = false,
|
declined = false,
|
||||||
|
highlightSubtitle = false,
|
||||||
id,
|
id,
|
||||||
list,
|
list,
|
||||||
name = "join",
|
name,
|
||||||
saving = false,
|
|
||||||
subtitle,
|
subtitle,
|
||||||
text,
|
text,
|
||||||
title,
|
title,
|
||||||
@@ -31,7 +31,7 @@ export default function Card({
|
|||||||
{subtitle ? (
|
{subtitle ? (
|
||||||
<Caption
|
<Caption
|
||||||
className={styles.subtitle}
|
className={styles.subtitle}
|
||||||
color={saving ? "baseTextAccent" : "uiTextHighContrast"}
|
color={highlightSubtitle ? "baseTextAccent" : "uiTextHighContrast"}
|
||||||
textTransform="bold"
|
textTransform="bold"
|
||||||
>
|
>
|
||||||
{subtitle}
|
{subtitle}
|
||||||
36
components/TempDesignSystem/ScrollWrapper/index.tsx
Normal file
36
components/TempDesignSystem/ScrollWrapper/index.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useMemo } from "react"
|
||||||
|
|
||||||
|
import useScrollShadows from "@/hooks/useScrollShadows"
|
||||||
|
|
||||||
|
import styles from "./scrollWrapper.module.css"
|
||||||
|
|
||||||
|
import type { ScrollWrapperProps } from "./scrollWrapper"
|
||||||
|
|
||||||
|
export default function ScrollWrapper({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}: ScrollWrapperProps) {
|
||||||
|
const { containerRef, showLeftShadow, showRightShadow } =
|
||||||
|
useScrollShadows<HTMLDivElement>()
|
||||||
|
|
||||||
|
const classNames = useMemo(() => {
|
||||||
|
const cls = [styles.scrollWrapper, className]
|
||||||
|
if (showLeftShadow) {
|
||||||
|
cls.push(styles.leftShadow)
|
||||||
|
}
|
||||||
|
if (showRightShadow) {
|
||||||
|
cls.push(styles.rightShadow)
|
||||||
|
}
|
||||||
|
return cls.join(" ")
|
||||||
|
}, [showLeftShadow, showRightShadow, className])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames}>
|
||||||
|
<div className={styles.content} ref={containerRef}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
.scrollWrapper {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollWrapper::before,
|
||||||
|
.scrollWrapper::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
opacity: 0;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollWrapper.leftShadow::before {
|
||||||
|
left: 0;
|
||||||
|
background: linear-gradient(to right, rgba(0, 0, 0, 0.3), transparent);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollWrapper.rightShadow::after {
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(to left, rgba(0, 0, 0, 0.3), transparent);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export interface ScrollWrapperProps
|
||||||
|
extends React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>> {}
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
@@ -5,18 +5,29 @@ import { useIntl } from "react-intl"
|
|||||||
import { ChevronDownIcon } from "@/components/Icons"
|
import { ChevronDownIcon } from "@/components/Icons"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
|
||||||
import styles from "./button.module.css"
|
import { showMoreButtonVariants } from "./variants"
|
||||||
|
|
||||||
import type { ShowMoreButtonParams } from "@/types/components/myPages/stays/button"
|
import styles from "./showMoreButton.module.css"
|
||||||
|
|
||||||
|
import type { ShowMoreButtonProps } from "./showMoreButton"
|
||||||
|
|
||||||
export default function ShowMoreButton({
|
export default function ShowMoreButton({
|
||||||
|
className,
|
||||||
|
intent,
|
||||||
disabled,
|
disabled,
|
||||||
|
showLess,
|
||||||
loadMoreData,
|
loadMoreData,
|
||||||
}: ShowMoreButtonParams) {
|
}: ShowMoreButtonProps) {
|
||||||
const { formatMessage } = useIntl()
|
const intl = useIntl()
|
||||||
|
const classNames = showMoreButtonVariants({
|
||||||
|
className,
|
||||||
|
intent,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={`${classNames} ${showLess ? styles.showLess : ""}`}>
|
||||||
<Button
|
<Button
|
||||||
|
className={styles.button}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={loadMoreData}
|
onClick={loadMoreData}
|
||||||
variant="icon"
|
variant="icon"
|
||||||
@@ -24,8 +35,8 @@ export default function ShowMoreButton({
|
|||||||
theme="base"
|
theme="base"
|
||||||
intent="text"
|
intent="text"
|
||||||
>
|
>
|
||||||
<ChevronDownIcon />
|
<ChevronDownIcon className={styles.icon} />
|
||||||
{formatMessage({ id: "Show more" })}
|
{intl.formatMessage({ id: showLess ? "Show less" : "Show more" })}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
display: grid;
|
||||||
|
justify-content: stretch;
|
||||||
|
border-top: 1px solid var(--Base-Border-Subtle);
|
||||||
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table .button {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showLess .icon {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
11
components/TempDesignSystem/ShowMoreButton/showMoreButton.ts
Normal file
11
components/TempDesignSystem/ShowMoreButton/showMoreButton.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { showMoreButtonVariants } from "./variants"
|
||||||
|
|
||||||
|
import type { VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
export interface ShowMoreButtonProps
|
||||||
|
extends React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>>,
|
||||||
|
VariantProps<typeof showMoreButtonVariants> {
|
||||||
|
disabled?: boolean
|
||||||
|
showLess?: boolean
|
||||||
|
loadMoreData: () => void
|
||||||
|
}
|
||||||
11
components/TempDesignSystem/ShowMoreButton/variants.ts
Normal file
11
components/TempDesignSystem/ShowMoreButton/variants.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
|
import styles from "./showMoreButton.module.css"
|
||||||
|
|
||||||
|
export const showMoreButtonVariants = cva(styles.container, {
|
||||||
|
variants: {
|
||||||
|
intent: {
|
||||||
|
table: styles.table,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
import styles from "./table.module.css"
|
import styles from "./table.module.css"
|
||||||
|
|
||||||
function TH({ children }: React.PropsWithChildren) {
|
import type { THeadProps } from "./table"
|
||||||
return <th className={styles.th}>{children}</th>
|
|
||||||
|
function TH({ children, width = "auto", ...props }: THeadProps) {
|
||||||
|
return (
|
||||||
|
<th className={styles.th} style={{ width }} {...props}>
|
||||||
|
{children}
|
||||||
|
</th>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TH
|
export default TH
|
||||||
|
|||||||
@@ -3,11 +3,33 @@ import TD from "./TD"
|
|||||||
import TH from "./TH"
|
import TH from "./TH"
|
||||||
import THead from "./THead"
|
import THead from "./THead"
|
||||||
import TR from "./TR"
|
import TR from "./TR"
|
||||||
|
import { tableVariants } from "./variants"
|
||||||
|
|
||||||
import styles from "./table.module.css"
|
import type { TableProps } from "./table"
|
||||||
|
|
||||||
function Table({ children }: React.PropsWithChildren) {
|
function Table({
|
||||||
return <table className={styles.table}>{children}</table>
|
className,
|
||||||
|
intent,
|
||||||
|
borderRadius,
|
||||||
|
variant,
|
||||||
|
layout,
|
||||||
|
width = "100%",
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: TableProps) {
|
||||||
|
const classNames = tableVariants({
|
||||||
|
className,
|
||||||
|
borderRadius,
|
||||||
|
intent,
|
||||||
|
layout,
|
||||||
|
variant,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className={classNames} style={{ width }} {...props}>
|
||||||
|
{children}
|
||||||
|
</table>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Table.THead = THead
|
Table.THead = THead
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
.table {
|
.table {
|
||||||
border-radius: var(--Corner-radius-Medium);
|
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
min-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thead {
|
.thead {
|
||||||
background-color: var(--Base-Background-Secondary-Normal, #f7e1d5);
|
color: var(--Base-Text-High-contrast);
|
||||||
|
background-color: var(--Base-Surface-Primary-dark-Normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tbody {
|
.tbody {
|
||||||
background-color: var(--Base-Surface-Primary-light-Normal, #fff);
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tr:not(:last-of-type) {
|
.tr:not(:last-of-type) {
|
||||||
border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider, #f0c1b6);
|
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.th {
|
.th {
|
||||||
@@ -28,6 +28,35 @@
|
|||||||
padding: var(--Spacing-x2);
|
padding: var(--Spacing-x2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fixed {
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smallRadius {
|
||||||
|
border-radius: var(--Corner-radius-Small);
|
||||||
|
}
|
||||||
|
.mediumRadius {
|
||||||
|
border-radius: var(--Corner-radius-Medium);
|
||||||
|
}
|
||||||
|
.largeRadius {
|
||||||
|
border-radius: var(--Corner-radius-Large);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content .thead {
|
||||||
|
background-color: var(--Base-Surface-Subtle-Hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content .tbody {
|
||||||
|
background-color: var(--Base-Background-Primary-Normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content.striped .tbody .tr:nth-child(odd) {
|
||||||
|
background-color: var(--Base-Surface-Subtle-Normal);
|
||||||
|
}
|
||||||
|
.content.striped .tbody .tr:nth-child(even) {
|
||||||
|
background-color: var(--Base-Background-Primary-Normal);
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
.th {
|
.th {
|
||||||
padding: var(--Spacing-x2) var(--Spacing-x3);
|
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||||
|
|||||||
14
components/TempDesignSystem/Table/table.ts
Normal file
14
components/TempDesignSystem/Table/table.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { tableVariants } from "./variants"
|
||||||
|
|
||||||
|
import type { VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
export interface TableProps
|
||||||
|
extends React.PropsWithChildren<React.HTMLAttributes<HTMLTableElement>>,
|
||||||
|
VariantProps<typeof tableVariants> {
|
||||||
|
width?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface THeadProps
|
||||||
|
extends React.PropsWithChildren<React.HTMLAttributes<HTMLTableCellElement>> {
|
||||||
|
width?: string
|
||||||
|
}
|
||||||
27
components/TempDesignSystem/Table/variants.ts
Normal file
27
components/TempDesignSystem/Table/variants.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
|
import styles from "./table.module.css"
|
||||||
|
|
||||||
|
export const tableVariants = cva(styles.table, {
|
||||||
|
variants: {
|
||||||
|
intent: {
|
||||||
|
light: styles.light,
|
||||||
|
striped: styles.striped,
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
content: styles.content,
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
none: "",
|
||||||
|
small: styles.smallRadius,
|
||||||
|
medium: styles.mediumRadius,
|
||||||
|
large: styles.largeRadius,
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
fixed: styles.fixed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
borderRadius: "medium",
|
||||||
|
},
|
||||||
|
})
|
||||||
34
hooks/useScrollShadows.ts
Normal file
34
hooks/useScrollShadows.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
|
||||||
|
export default function useScrollShadows<T extends HTMLElement>() {
|
||||||
|
const containerRef = useRef<T>(null)
|
||||||
|
const [showLeftShadow, setShowLeftShadow] = useState<boolean>(false)
|
||||||
|
const [showRightShadow, setShowRightShadow] = useState<boolean>(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
const container = containerRef.current
|
||||||
|
if (!container) return
|
||||||
|
|
||||||
|
setShowLeftShadow(container.scrollLeft > 0)
|
||||||
|
setShowRightShadow(
|
||||||
|
container.scrollLeft < container.scrollWidth - container.clientWidth
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = containerRef.current
|
||||||
|
if (container) {
|
||||||
|
container.addEventListener("scroll", handleScroll)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleScroll()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (container) {
|
||||||
|
container.removeEventListener("scroll", handleScroll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return { containerRef, showLeftShadow, showRightShadow }
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ export namespace endpoints {
|
|||||||
}
|
}
|
||||||
export const enum v1 {
|
export const enum v1 {
|
||||||
hotelsAvailability = "availability/v1/availabilities/city",
|
hotelsAvailability = "availability/v1/availabilities/city",
|
||||||
|
roomsAvailability = "availability/v1/availabilities/hotel",
|
||||||
profile = "profile/v1/Profile",
|
profile = "profile/v1/Profile",
|
||||||
booking = "booking/v1/Bookings",
|
booking = "booking/v1/Bookings",
|
||||||
creditCards = `${profile}/creditCards`,
|
creditCards = `${profile}/creditCards`,
|
||||||
|
|||||||
9
lib/graphql/Fragments/Blocks/Table.graphql
Normal file
9
lib/graphql/Fragments/Blocks/Table.graphql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
fragment Table_ContentPage on ContentPageBlocksTable {
|
||||||
|
__typename
|
||||||
|
table {
|
||||||
|
heading
|
||||||
|
preamble
|
||||||
|
column_widths
|
||||||
|
table
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#import "../../Fragments/Blocks/Content.graphql"
|
#import "../../Fragments/Blocks/Content.graphql"
|
||||||
#import "../../Fragments/Blocks/DynamicContent.graphql"
|
#import "../../Fragments/Blocks/DynamicContent.graphql"
|
||||||
#import "../../Fragments/Blocks/Shortcuts.graphql"
|
#import "../../Fragments/Blocks/Shortcuts.graphql"
|
||||||
|
#import "../../Fragments/Blocks/Table.graphql"
|
||||||
#import "../../Fragments/Blocks/TextCols.graphql"
|
#import "../../Fragments/Blocks/TextCols.graphql"
|
||||||
#import "../../Fragments/Blocks/UspGrid.graphql"
|
#import "../../Fragments/Blocks/UspGrid.graphql"
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ query GetContentPage($locale: String!, $uid: String!) {
|
|||||||
...Content_ContentPage
|
...Content_ContentPage
|
||||||
...DynamicContent_ContentPage
|
...DynamicContent_ContentPage
|
||||||
...Shortcuts_ContentPage
|
...Shortcuts_ContentPage
|
||||||
|
...Table_ContentPage
|
||||||
...TextCols_ContentPage
|
...TextCols_ContentPage
|
||||||
...UspGrid_ContentPage
|
...UspGrid_ContentPage
|
||||||
}
|
}
|
||||||
|
|||||||
32
package-lock.json
generated
32
package-lock.json
generated
@@ -23,6 +23,7 @@
|
|||||||
"@scandic-hotels/design-system": "git+https://x-token-auth:$DESIGN_SYSTEM_ACCESS_TOKEN@bitbucket.org/scandic-swap/design-system.git#v0.1.0-rc.9",
|
"@scandic-hotels/design-system": "git+https://x-token-auth:$DESIGN_SYSTEM_ACCESS_TOKEN@bitbucket.org/scandic-swap/design-system.git#v0.1.0-rc.9",
|
||||||
"@t3-oss/env-nextjs": "^0.9.2",
|
"@t3-oss/env-nextjs": "^0.9.2",
|
||||||
"@tanstack/react-query": "^5.28.6",
|
"@tanstack/react-query": "^5.28.6",
|
||||||
|
"@tanstack/react-table": "^8.20.5",
|
||||||
"@trpc/client": "^11.0.0-rc.467",
|
"@trpc/client": "^11.0.0-rc.467",
|
||||||
"@trpc/react-query": "^11.0.0-rc.467",
|
"@trpc/react-query": "^11.0.0-rc.467",
|
||||||
"@trpc/server": "^11.0.0-rc.467",
|
"@trpc/server": "^11.0.0-rc.467",
|
||||||
@@ -5971,6 +5972,37 @@
|
|||||||
"react": "^18.0.0"
|
"react": "^18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/react-table": {
|
||||||
|
"version": "8.20.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.5.tgz",
|
||||||
|
"integrity": "sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/table-core": "8.20.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8",
|
||||||
|
"react-dom": ">=16.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/table-core": {
|
||||||
|
"version": "8.20.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz",
|
||||||
|
"integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@testing-library/dom": {
|
"node_modules/@testing-library/dom": {
|
||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.1.0.tgz",
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
"@scandic-hotels/design-system": "git+https://x-token-auth:$DESIGN_SYSTEM_ACCESS_TOKEN@bitbucket.org/scandic-swap/design-system.git#v0.1.0-rc.9",
|
"@scandic-hotels/design-system": "git+https://x-token-auth:$DESIGN_SYSTEM_ACCESS_TOKEN@bitbucket.org/scandic-swap/design-system.git#v0.1.0-rc.9",
|
||||||
"@t3-oss/env-nextjs": "^0.9.2",
|
"@t3-oss/env-nextjs": "^0.9.2",
|
||||||
"@tanstack/react-query": "^5.28.6",
|
"@tanstack/react-query": "^5.28.6",
|
||||||
|
"@tanstack/react-table": "^8.20.5",
|
||||||
"@trpc/client": "^11.0.0-rc.467",
|
"@trpc/client": "^11.0.0-rc.467",
|
||||||
"@trpc/react-query": "^11.0.0-rc.467",
|
"@trpc/react-query": "^11.0.0-rc.467",
|
||||||
"@trpc/server": "^11.0.0-rc.467",
|
"@trpc/server": "^11.0.0-rc.467",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
shortcutsRefsSchema,
|
shortcutsRefsSchema,
|
||||||
shortcutsSchema,
|
shortcutsSchema,
|
||||||
} from "../schemas/blocks/shortcuts"
|
} from "../schemas/blocks/shortcuts"
|
||||||
|
import { tableSchema } from "../schemas/blocks/table"
|
||||||
import { textColsRefsSchema, textColsSchema } from "../schemas/blocks/textCols"
|
import { textColsRefsSchema, textColsSchema } from "../schemas/blocks/textCols"
|
||||||
import { uspGridRefsSchema, uspGridSchema } from "../schemas/blocks/uspGrid"
|
import { uspGridRefsSchema, uspGridSchema } from "../schemas/blocks/uspGrid"
|
||||||
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
|
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
|
||||||
@@ -75,11 +76,18 @@ export const contentPageUspGrid = z
|
|||||||
})
|
})
|
||||||
.merge(uspGridSchema)
|
.merge(uspGridSchema)
|
||||||
|
|
||||||
|
export const contentPageTable = z
|
||||||
|
.object({
|
||||||
|
__typename: z.literal(ContentPageEnum.ContentStack.blocks.Table),
|
||||||
|
})
|
||||||
|
.merge(tableSchema)
|
||||||
|
|
||||||
export const blocksSchema = z.discriminatedUnion("__typename", [
|
export const blocksSchema = z.discriminatedUnion("__typename", [
|
||||||
contentPageCards,
|
contentPageCards,
|
||||||
contentPageContent,
|
contentPageContent,
|
||||||
contentPageDynamicContent,
|
contentPageDynamicContent,
|
||||||
contentPageShortcuts,
|
contentPageShortcuts,
|
||||||
|
contentPageTable,
|
||||||
contentPageTextCols,
|
contentPageTextCols,
|
||||||
contentPageUspGrid,
|
contentPageUspGrid,
|
||||||
])
|
])
|
||||||
|
|||||||
58
server/routers/contentstack/schemas/blocks/table.ts
Normal file
58
server/routers/contentstack/schemas/blocks/table.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { BlocksEnums } from "@/types/enums/blocks"
|
||||||
|
|
||||||
|
export const tableSchema = z.object({
|
||||||
|
typename: z
|
||||||
|
.literal(BlocksEnums.block.Table)
|
||||||
|
.optional()
|
||||||
|
.default(BlocksEnums.block.Table),
|
||||||
|
table: z
|
||||||
|
.object({
|
||||||
|
heading: z.string().optional(),
|
||||||
|
preamble: z.string().optional().default(""),
|
||||||
|
column_widths: z.array(z.number()),
|
||||||
|
table: z.object({
|
||||||
|
tableState: z.object({
|
||||||
|
columns: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
label: z.string().default(""),
|
||||||
|
accessor: z.string(),
|
||||||
|
dataType: z.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
data: z.array(z.object({}).catchall(z.string())),
|
||||||
|
skipReset: z.boolean(),
|
||||||
|
tableActionEnabled: z.boolean(),
|
||||||
|
headerRowAdded: z.boolean(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.transform((data) => {
|
||||||
|
const totalWidth = data.column_widths.reduce(
|
||||||
|
(acc, width) => acc + width,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
const columns = data.table.tableState.columns.map((col, idx) => ({
|
||||||
|
id: col.id,
|
||||||
|
header: col.label || "",
|
||||||
|
width: data.column_widths[idx] || 0,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const rows = data.table.tableState.data.map((rowData) =>
|
||||||
|
columns.reduce<Record<string, string>>((transformedRow, column) => {
|
||||||
|
transformedRow[column.id] = rowData[column.id] || ""
|
||||||
|
return transformedRow
|
||||||
|
}, {})
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
heading: data.heading,
|
||||||
|
preamble: data.preamble,
|
||||||
|
columns,
|
||||||
|
rows,
|
||||||
|
totalWidth,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})
|
||||||
@@ -17,6 +17,17 @@ export const getHotelsAvailabilityInputSchema = z.object({
|
|||||||
attachedProfileId: z.string().optional().default(""),
|
attachedProfileId: z.string().optional().default(""),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const getRoomsAvailabilityInputSchema = z.object({
|
||||||
|
hotelId: z.number(),
|
||||||
|
roomStayStartDate: z.string(),
|
||||||
|
roomStayEndDate: z.string(),
|
||||||
|
adults: z.number(),
|
||||||
|
children: z.number().optional().default(0),
|
||||||
|
promotionCode: z.string().optional(),
|
||||||
|
reservationProfileType: z.string().optional().default(""),
|
||||||
|
attachedProfileId: z.string().optional().default(""),
|
||||||
|
})
|
||||||
|
|
||||||
export const getRatesInputSchema = z.object({
|
export const getRatesInputSchema = z.object({
|
||||||
hotelId: z.string(),
|
hotelId: z.string(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -572,6 +572,86 @@ export type HotelsAvailability = z.infer<typeof hotelsAvailabilitySchema>
|
|||||||
export type HotelsAvailabilityPrices =
|
export type HotelsAvailabilityPrices =
|
||||||
HotelsAvailability["data"][number]["attributes"]["bestPricePerNight"]
|
HotelsAvailability["data"][number]["attributes"]["bestPricePerNight"]
|
||||||
|
|
||||||
|
const productSchema = z.object({
|
||||||
|
productType: z.object({
|
||||||
|
public: z.object({
|
||||||
|
rateCode: z.string(),
|
||||||
|
rateType: z.string().optional(),
|
||||||
|
localPrice: z.object({
|
||||||
|
pricePerNight: z.string(),
|
||||||
|
pricePerStay: z.string(),
|
||||||
|
currency: z.string(),
|
||||||
|
}),
|
||||||
|
requestedPrice: z
|
||||||
|
.object({
|
||||||
|
pricePerNight: z.string(),
|
||||||
|
pricePerStay: z.string(),
|
||||||
|
currency: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
}),
|
||||||
|
member: z.object({
|
||||||
|
rateCode: z.string(),
|
||||||
|
rateType: z.string().optional(),
|
||||||
|
localPrice: z.object({
|
||||||
|
pricePerNight: z.string(),
|
||||||
|
pricePerStay: z.string(),
|
||||||
|
currency: z.string(),
|
||||||
|
}),
|
||||||
|
requestedPrice: z
|
||||||
|
.object({
|
||||||
|
pricePerNight: z.string(),
|
||||||
|
pricePerStay: z.string(),
|
||||||
|
currency: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const roomConfigurationSchema = z.object({
|
||||||
|
status: z.string(),
|
||||||
|
bedType: z.string(),
|
||||||
|
roomType: z.string(),
|
||||||
|
roomsLeft: z.number(),
|
||||||
|
features: z.array(z.object({ inventory: z.number(), code: z.string() })),
|
||||||
|
products: z.array(productSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const rateDefinitionSchema = z.object({
|
||||||
|
title: z.string(),
|
||||||
|
breakfastIncluded: z.boolean(),
|
||||||
|
rateType: z.string().optional(),
|
||||||
|
rateCode: z.string(),
|
||||||
|
generalTerms: z.array(z.string()),
|
||||||
|
cancellationRule: z.string(),
|
||||||
|
cancellationText: z.string(),
|
||||||
|
mustBeGuaranteed: z.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const roomsAvailabilitySchema = z
|
||||||
|
.object({
|
||||||
|
data: z.object({
|
||||||
|
attributes: z.object({
|
||||||
|
checkInDate: z.string(),
|
||||||
|
checkOutDate: z.string(),
|
||||||
|
occupancy: occupancySchema.optional(),
|
||||||
|
hotelId: z.number(),
|
||||||
|
roomConfigurations: z.array(roomConfigurationSchema),
|
||||||
|
rateDefinitions: z.array(rateDefinitionSchema),
|
||||||
|
}),
|
||||||
|
relationships: linksSchema.optional(),
|
||||||
|
type: z.string().optional(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.transform((o) => o.data.attributes)
|
||||||
|
|
||||||
|
export const getRoomsAvailabilitySchema = roomsAvailabilitySchema
|
||||||
|
export type RoomsAvailability = z.infer<typeof roomsAvailabilitySchema>
|
||||||
|
export type RoomConfiguration = z.infer<typeof roomConfigurationSchema>
|
||||||
|
export type Product = z.infer<typeof productSchema>
|
||||||
|
export type RateDefinition = z.infer<typeof rateDefinitionSchema>
|
||||||
|
|
||||||
const flexibilityPrice = z.object({
|
const flexibilityPrice = z.object({
|
||||||
standard: z.number(),
|
standard: z.number(),
|
||||||
member: z.number(),
|
member: z.number(),
|
||||||
|
|||||||
@@ -24,11 +24,13 @@ import {
|
|||||||
getHotelsAvailabilityInputSchema,
|
getHotelsAvailabilityInputSchema,
|
||||||
getlHotelDataInputSchema,
|
getlHotelDataInputSchema,
|
||||||
getRatesInputSchema,
|
getRatesInputSchema,
|
||||||
|
getRoomsAvailabilityInputSchema,
|
||||||
} from "./input"
|
} from "./input"
|
||||||
import {
|
import {
|
||||||
getHotelDataSchema,
|
getHotelDataSchema,
|
||||||
getHotelsAvailabilitySchema,
|
getHotelsAvailabilitySchema,
|
||||||
getRatesSchema,
|
getRatesSchema,
|
||||||
|
getRoomsAvailabilitySchema,
|
||||||
roomSchema,
|
roomSchema,
|
||||||
} from "./output"
|
} from "./output"
|
||||||
import tempRatesData from "./tempRatesData.json"
|
import tempRatesData from "./tempRatesData.json"
|
||||||
@@ -61,6 +63,16 @@ const hotelsAvailabilityFailCounter = meter.createCounter(
|
|||||||
"trpc.hotel.availability.hotels-fail"
|
"trpc.hotel.availability.hotels-fail"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const roomsAvailabilityCounter = meter.createCounter(
|
||||||
|
"trpc.hotel.availability.rooms"
|
||||||
|
)
|
||||||
|
const roomsAvailabilitySuccessCounter = meter.createCounter(
|
||||||
|
"trpc.hotel.availability.rooms-success"
|
||||||
|
)
|
||||||
|
const roomsAvailabilityFailCounter = meter.createCounter(
|
||||||
|
"trpc.hotel.availability.rooms-fail"
|
||||||
|
)
|
||||||
|
|
||||||
async function getContentstackData(
|
async function getContentstackData(
|
||||||
locale: string,
|
locale: string,
|
||||||
uid: string | null | undefined
|
uid: string | null | undefined
|
||||||
@@ -376,6 +388,123 @@ export const hotelQueryRouter = router({
|
|||||||
.flatMap((hotels) => hotels.attributes),
|
.flatMap((hotels) => hotels.attributes),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
rooms: hotelServiceProcedure
|
||||||
|
.input(getRoomsAvailabilityInputSchema)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const {
|
||||||
|
hotelId,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
promotionCode,
|
||||||
|
reservationProfileType,
|
||||||
|
attachedProfileId,
|
||||||
|
} = input
|
||||||
|
|
||||||
|
const params: Record<string, string | number | undefined> = {
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
promotionCode,
|
||||||
|
reservationProfileType,
|
||||||
|
attachedProfileId,
|
||||||
|
}
|
||||||
|
|
||||||
|
roomsAvailabilityCounter.add(1, {
|
||||||
|
hotelId,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
promotionCode,
|
||||||
|
reservationProfileType,
|
||||||
|
})
|
||||||
|
console.info(
|
||||||
|
"api.hotels.roomsAvailability start",
|
||||||
|
JSON.stringify({ query: { hotelId, params } })
|
||||||
|
)
|
||||||
|
const apiResponse = await api.get(
|
||||||
|
`${api.endpoints.v1.roomsAvailability}/${hotelId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params
|
||||||
|
)
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
const text = await apiResponse.text()
|
||||||
|
roomsAvailabilityFailCounter.add(1, {
|
||||||
|
hotelId,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
promotionCode,
|
||||||
|
reservationProfileType,
|
||||||
|
error_type: "http_error",
|
||||||
|
error: JSON.stringify({
|
||||||
|
status: apiResponse.status,
|
||||||
|
statusText: apiResponse.statusText,
|
||||||
|
text,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
console.error(
|
||||||
|
"api.hotels.roomsAvailability error",
|
||||||
|
JSON.stringify({
|
||||||
|
query: { hotelId, params },
|
||||||
|
error: {
|
||||||
|
status: apiResponse.status,
|
||||||
|
statusText: apiResponse.statusText,
|
||||||
|
text,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const apiJson = await apiResponse.json()
|
||||||
|
const validateAvailabilityData =
|
||||||
|
getRoomsAvailabilitySchema.safeParse(apiJson)
|
||||||
|
if (!validateAvailabilityData.success) {
|
||||||
|
roomsAvailabilityFailCounter.add(1, {
|
||||||
|
hotelId,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
promotionCode,
|
||||||
|
reservationProfileType,
|
||||||
|
error_type: "validation_error",
|
||||||
|
error: JSON.stringify(validateAvailabilityData.error),
|
||||||
|
})
|
||||||
|
console.error(
|
||||||
|
"api.hotels.roomsAvailability validation error",
|
||||||
|
JSON.stringify({
|
||||||
|
query: { hotelId, params },
|
||||||
|
error: validateAvailabilityData.error,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
throw badRequestError()
|
||||||
|
}
|
||||||
|
roomsAvailabilitySuccessCounter.add(1, {
|
||||||
|
hotelId,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
promotionCode,
|
||||||
|
reservationProfileType,
|
||||||
|
})
|
||||||
|
console.info(
|
||||||
|
"api.hotels.roomsAvailability success",
|
||||||
|
JSON.stringify({
|
||||||
|
query: { hotelId, params: params },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return validateAvailabilityData.data
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
rates: router({
|
rates: router({
|
||||||
get: publicProcedure
|
get: publicProcedure
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ export const getStaysSchema = z.object({
|
|||||||
limit: z.number(),
|
limit: z.number(),
|
||||||
totalCount: z.number(),
|
totalCount: z.number(),
|
||||||
})
|
})
|
||||||
|
.optional()
|
||||||
.nullable(),
|
.nullable(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
5
types/components/blocks/table.ts
Normal file
5
types/components/blocks/table.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import type { TableData } from "@/types/trpc/routers/contentstack/blocks"
|
||||||
|
|
||||||
|
export interface TableBlockProps {
|
||||||
|
data: TableData
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { Product, RateDefinition } from "@/server/routers/hotels/output"
|
||||||
|
|
||||||
export type FlexibilityOptionProps = {
|
export type FlexibilityOptionProps = {
|
||||||
|
product: Product | undefined
|
||||||
name: string
|
name: string
|
||||||
value: string
|
value: string
|
||||||
paymentTerm: string
|
paymentTerm: string
|
||||||
standardPrice: number
|
|
||||||
memberPrice: number
|
|
||||||
currency: string
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Rate } from "@/server/routers/hotels/output"
|
import {
|
||||||
|
RateDefinition,
|
||||||
|
RoomConfiguration,
|
||||||
|
} from "@/server/routers/hotels/output"
|
||||||
|
|
||||||
export type RoomCardProps = {
|
export type RoomCardProps = {
|
||||||
room: Rate
|
roomConfiguration: RoomConfiguration
|
||||||
nrOfNights: number
|
rateDefinitions: RateDefinition[]
|
||||||
nrOfAdults: number
|
|
||||||
breakfastIncluded: boolean
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { Rate } from "@/server/routers/hotels/output"
|
import { RoomsAvailability } from "@/server/routers/hotels/output"
|
||||||
|
|
||||||
export interface RoomSelectionProps {
|
export interface RoomSelectionProps {
|
||||||
rates: Rate[]
|
roomConfigurations: RoomsAvailability
|
||||||
nrOfAdults: number
|
|
||||||
nrOfNights: number
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export namespace BlocksEnums {
|
|||||||
Content = "Content",
|
Content = "Content",
|
||||||
DynamicContent = "DynamicContent",
|
DynamicContent = "DynamicContent",
|
||||||
Shortcuts = "Shortcuts",
|
Shortcuts = "Shortcuts",
|
||||||
|
Table = "Table",
|
||||||
TextCols = "TextCols",
|
TextCols = "TextCols",
|
||||||
TextContent = "TextContent",
|
TextContent = "TextContent",
|
||||||
UspGrid = "UspGrid",
|
UspGrid = "UspGrid",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export namespace ContentPageEnum {
|
|||||||
Shortcuts = "ContentPageBlocksShortcuts",
|
Shortcuts = "ContentPageBlocksShortcuts",
|
||||||
TextCols = "ContentPageBlocksTextCols",
|
TextCols = "ContentPageBlocksTextCols",
|
||||||
UspGrid = "ContentPageBlocksUspGrid",
|
UspGrid = "ContentPageBlocksUspGrid",
|
||||||
|
Table = "ContentPageBlocksTable",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum sidebar {
|
export const enum sidebar {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { cardsGridSchema } from "@/server/routers/contentstack/schemas/blocks/ca
|
|||||||
import { contentSchema } from "@/server/routers/contentstack/schemas/blocks/content"
|
import { contentSchema } from "@/server/routers/contentstack/schemas/blocks/content"
|
||||||
import { dynamicContentSchema } from "@/server/routers/contentstack/schemas/blocks/dynamicContent"
|
import { dynamicContentSchema } from "@/server/routers/contentstack/schemas/blocks/dynamicContent"
|
||||||
import { shortcutsSchema } from "@/server/routers/contentstack/schemas/blocks/shortcuts"
|
import { shortcutsSchema } from "@/server/routers/contentstack/schemas/blocks/shortcuts"
|
||||||
|
import { tableSchema } from "@/server/routers/contentstack/schemas/blocks/table"
|
||||||
import { textColsSchema } from "@/server/routers/contentstack/schemas/blocks/textCols"
|
import { textColsSchema } from "@/server/routers/contentstack/schemas/blocks/textCols"
|
||||||
import { uspGridSchema } from "@/server/routers/contentstack/schemas/blocks/uspGrid"
|
import { uspGridSchema } from "@/server/routers/contentstack/schemas/blocks/uspGrid"
|
||||||
|
|
||||||
@@ -12,5 +13,7 @@ export interface Content extends z.output<typeof contentSchema> {}
|
|||||||
export interface DynamicContent extends z.output<typeof dynamicContentSchema> {}
|
export interface DynamicContent extends z.output<typeof dynamicContentSchema> {}
|
||||||
export interface Shortcuts extends z.output<typeof shortcutsSchema> {}
|
export interface Shortcuts extends z.output<typeof shortcutsSchema> {}
|
||||||
export type Shortcut = Shortcuts["shortcuts"]
|
export type Shortcut = Shortcuts["shortcuts"]
|
||||||
|
export interface TableBlock extends z.output<typeof tableSchema> {}
|
||||||
|
export type TableData = TableBlock["table"]
|
||||||
export interface TextCols extends z.output<typeof textColsSchema> {}
|
export interface TextCols extends z.output<typeof textColsSchema> {}
|
||||||
export interface UspGrid extends z.output<typeof uspGridSchema> {}
|
export interface UspGrid extends z.output<typeof uspGridSchema> {}
|
||||||
|
|||||||
Reference in New Issue
Block a user