Merge branch 'develop' into feature/tracking

This commit is contained in:
Linus Flood
2024-10-09 10:23:01 +02:00
50 changed files with 891 additions and 137 deletions

View File

@@ -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>

View 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>
)
}

View File

@@ -0,0 +1,6 @@
.tableWrapper {
display: grid;
border: 1px solid var(--Base-Border-Subtle);
border-radius: var(--Corner-radius-Small);
overflow: hidden;
}

View File

@@ -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:

View File

@@ -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);
} }

View File

@@ -40,6 +40,8 @@
} }
.mainContent { .mainContent {
display: grid;
gap: var(--Spacing-x4);
width: 100%; width: 100%;
} }

View File

@@ -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> }
) )

View File

@@ -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",

View File

@@ -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}",

View File

@@ -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>

View File

@@ -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>
) )

View File

@@ -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>
) )
} }

View File

@@ -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);
} }

View File

@@ -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" />

View File

@@ -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" />

View File

@@ -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

View File

@@ -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}

View 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>
)
}

View File

@@ -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;
}

View File

@@ -0,0 +1,2 @@
export interface ScrollWrapperProps
extends React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>> {}

View File

@@ -1,4 +0,0 @@
.container {
display: flex;
justify-content: center;
}

View File

@@ -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>
) )

View File

@@ -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);
}

View 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
}

View 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,
},
},
})

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View 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
}

View 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
View 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 }
}

View File

@@ -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`,

View File

@@ -0,0 +1,9 @@
fragment Table_ContentPage on ContentPageBlocksTable {
__typename
table {
heading
preamble
column_widths
table
}
}

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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,
]) ])

View 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,
}
}),
})

View File

@@ -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(),
}) })

View File

@@ -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(),

View File

@@ -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

View File

@@ -111,6 +111,7 @@ export const getStaysSchema = z.object({
limit: z.number(), limit: z.number(),
totalCount: z.number(), totalCount: z.number(),
}) })
.optional()
.nullable(), .nullable(),
}) })

View File

@@ -0,0 +1,5 @@
import type { TableData } from "@/types/trpc/routers/contentstack/blocks"
export interface TableBlockProps {
data: TableData
}

View File

@@ -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
} }

View File

@@ -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
} }

View File

@@ -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
} }

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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> {}