diff --git a/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.module.css new file mode 100644 index 000000000..f40fca31d --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.module.css @@ -0,0 +1,20 @@ +.layout { + min-height: 100dvh; + background-color: var(--Scandic-Brand-Warm-White); +} + +.content { + display: grid; + gap: var(--Spacing-x3) var(--Spacing-x9); + grid-template-columns: 1fr 340px; + grid-template-rows: auto 1fr; + margin: var(--Spacing-x5) auto 0; + max-width: var(--max-width-navigation); + padding: var(--Spacing-x6) var(--Spacing-x2) 0; +} + +.summary { + align-self: flex-start; + grid-column: 2 / 3; + grid-row: 1/-1; +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.tsx new file mode 100644 index 000000000..c234123a4 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.tsx @@ -0,0 +1,38 @@ +import { redirect } from "next/navigation" + +import { serverClient } from "@/lib/trpc/server" + +import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom" +import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader" +import Summary from "@/components/HotelReservation/SelectRate/Summary" + +import styles from "./layout.module.css" + +import type { LangParams, LayoutArgs } from "@/types/params" + +export default async function StepLayout({ + children, + params, +}: React.PropsWithChildren>) { + const hotel = await serverClient().hotel.hotelData.get({ + hotelId: "811", + language: params.lang, + }) + + if (!hotel?.data) { + redirect(`/${params.lang}`) + } + + return ( +
+ +
+ + {children} + +
+
+ ) +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/[step]/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/[step]/page.module.css deleted file mode 100644 index 8962fd5ee..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/[step]/page.module.css +++ /dev/null @@ -1,30 +0,0 @@ -.page { - min-height: 100dvh; - padding-top: var(--Spacing-x6); - padding-left: var(--Spacing-x2); - padding-right: var(--Spacing-x2); - background-color: var(--Scandic-Brand-Warm-White); -} - -.content { - max-width: 1134px; - margin-top: var(--Spacing-x5); - margin-left: auto; - margin-right: auto; - display: flex; - justify-content: space-between; - gap: var(--Spacing-x7); -} - -.section { - flex-grow: 1; -} - -.summary { - max-width: 340px; -} - -.form { - display: grid; - gap: var(--Spacing-x2); -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/[step]/page.tsx index 050aa0280..b853fc9bc 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/[step]/page.tsx @@ -9,15 +9,11 @@ import { trpc } from "@/lib/trpc/client" import BedType from "@/components/HotelReservation/EnterDetails/BedType" import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast" import Details from "@/components/HotelReservation/EnterDetails/Details" -import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader" import Payment from "@/components/HotelReservation/SelectRate/Payment" import SectionAccordion from "@/components/HotelReservation/SelectRate/SectionAccordion" -import Summary from "@/components/HotelReservation/SelectRate/Summary" import LoadingSpinner from "@/components/LoadingSpinner" -import styles from "./page.module.css" - -import { LangParams, PageArgs } from "@/types/params" +import type { LangParams, PageArgs } from "@/types/params" enum StepEnum { selectBed = "select-bed", @@ -75,51 +71,43 @@ export default function StepPage({ } return ( -
- -
-
- - - - - - - -
- - - - -
- -
-
+
+ + + + + + + +
+ + + + +
) } diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx index 235eeda52..796c0fedb 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx @@ -2,7 +2,6 @@ import { serverClient } from "@/lib/trpc/server" import tempHotelData from "@/server/routers/hotels/tempHotelData.json" import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection" -import { getIntl } from "@/i18n" import { setLang } from "@/i18n/serverContext" import styles from "./page.module.css" @@ -19,18 +18,15 @@ export default async function SelectRatePage({ // TODO: Use real endpoint. const hotel = tempHotelData.data.attributes - const rates = await serverClient().hotel.rates.get({ - // TODO: pass the correct hotel ID and all other parameters that should be included in the search - hotelId: searchParams.hotel, + const roomConfigurations = await serverClient().hotel.availability.rooms({ + hotelId: parseInt(searchParams.hotel, 10), + roomStayStartDate: "2024-11-02", + roomStayEndDate: "2024-11-03", + adults: 1, }) - - // const rates = await serverClient().hotel.availability.getForHotel({ - // hotelId: 811, - // roomStayStartDate: "2024-11-02", - // roomStayEndDate: "2024-11-03", - // adults: 1, - // }) - const intl = await getIntl() + if (!roomConfigurations) { + return "No rooms found" + } return (
@@ -39,13 +35,7 @@ export default async function SelectRatePage({
- +
diff --git a/components/Blocks/Table/index.tsx b/components/Blocks/Table/index.tsx new file mode 100644 index 000000000..7d7d0c457 --- /dev/null +++ b/components/Blocks/Table/index.tsx @@ -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 ( + + {heading ? ( + + ) : null} +
+ + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + ))} + +
+
+ {showMoreVisible ? ( + + ) : null} +
+
+ ) +} diff --git a/components/Blocks/Table/table.module.css b/components/Blocks/Table/table.module.css new file mode 100644 index 000000000..805cb9935 --- /dev/null +++ b/components/Blocks/Table/table.module.css @@ -0,0 +1,6 @@ +.tableWrapper { + display: grid; + border: 1px solid var(--Base-Border-Subtle); + border-radius: var(--Corner-radius-Small); + overflow: hidden; +} diff --git a/components/Blocks/index.tsx b/components/Blocks/index.tsx index 1c6d0bcbc..2423508d1 100644 --- a/components/Blocks/index.tsx +++ b/components/Blocks/index.tsx @@ -5,6 +5,8 @@ import TextCols from "@/components/Blocks/TextCols" import UspGrid from "@/components/Blocks/UspGrid" import JsonToHtml from "@/components/JsonToHtml" +import Table from "./Table" + import type { BlocksProps } from "@/types/components/blocks" import { BlocksEnums } from "@/types/enums/blocks" @@ -47,6 +49,8 @@ export default function Blocks({ blocks }: BlocksProps) { title={block.shortcuts.title} /> ) + case BlocksEnums.block.Table: + return case BlocksEnums.block.TextCols: return case BlocksEnums.block.TextContent: diff --git a/components/BookingWidget/bookingWidget.module.css b/components/BookingWidget/bookingWidget.module.css index 2827ba72d..461057bf1 100644 --- a/components/BookingWidget/bookingWidget.module.css +++ b/components/BookingWidget/bookingWidget.module.css @@ -42,7 +42,7 @@ box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.05); position: sticky; top: 0; - z-index: 10000; + z-index: 9; background-color: var(--Base-Surface-Primary-light-Normal); } diff --git a/components/ContentType/ContentPage/contentPage.module.css b/components/ContentType/ContentPage/contentPage.module.css index 026aa6065..aa0036f83 100644 --- a/components/ContentType/ContentPage/contentPage.module.css +++ b/components/ContentType/ContentPage/contentPage.module.css @@ -40,6 +40,8 @@ } .mainContent { + display: grid; + gap: var(--Spacing-x4); width: 100%; } diff --git a/components/HotelReservation/EnterDetails/BedType/index.tsx b/components/HotelReservation/EnterDetails/BedType/index.tsx index 61633cdcf..955ce740b 100644 --- a/components/HotelReservation/EnterDetails/BedType/index.tsx +++ b/components/HotelReservation/EnterDetails/BedType/index.tsx @@ -5,7 +5,7 @@ import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" 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" @@ -24,9 +24,7 @@ export default function BedType() { reValidateMode: "onChange", }) - // @ts-expect-error - Types mismatch docs as this is - // a pattern that is allowed https://formatjs.io/docs/react-intl/api#usage - const text = intl.formatMessage( + const text = intl.formatMessage( { id: "Included (based on availability)" }, { b: (str) => {str} } ) diff --git a/components/HotelReservation/EnterDetails/Breakfast/index.tsx b/components/HotelReservation/EnterDetails/Breakfast/index.tsx index b8f00ec83..8551e12dc 100644 --- a/components/HotelReservation/EnterDetails/Breakfast/index.tsx +++ b/components/HotelReservation/EnterDetails/Breakfast/index.tsx @@ -5,7 +5,7 @@ import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" 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" @@ -31,9 +31,7 @@ export default function Breakfast() { Icon={BreakfastIcon} id={breakfastEnum.BREAKFAST} name="breakfast" - // @ts-expect-error - Types mismatch docs as this is - // a pattern that is allowed https://formatjs.io/docs/react-intl/api#usage - subtitle={intl.formatMessage( + subtitle={intl.formatMessage( { id: "{amount} {currency}/night per adult" }, { amount: "150", diff --git a/components/HotelReservation/EnterDetails/Details/index.tsx b/components/HotelReservation/EnterDetails/Details/index.tsx index 2dd1b1cdb..15792e384 100644 --- a/components/HotelReservation/EnterDetails/Details/index.tsx +++ b/components/HotelReservation/EnterDetails/Details/index.tsx @@ -4,7 +4,7 @@ import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" 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 Input from "@/components/TempDesignSystem/Form/Input" import Phone from "@/components/TempDesignSystem/Form/Phone" @@ -88,8 +88,9 @@ export default function Details({ user }: DetailsProps) {
{user ? null : ( +
+ +
+
+
+ + {intl.formatMessage({ id: "Your room" })} + +
+ {/** + * [TEMP] + * No translation on Subtitles as they will be derived + * from Room selection. + */} + + Cozy cabin + + + Free rebooking + + + Pay now + +
+
+ +
+ + ) +} diff --git a/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css b/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css new file mode 100644 index 000000000..5e64327cf --- /dev/null +++ b/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css @@ -0,0 +1,51 @@ +.container { + background-color: var(--Base-Surface-Primary-light-Normal); + border-radius: var(--Corner-radius-Large); + display: grid; + grid-template-columns: 144px 1fr; + gap: var(--Spacing-x3); + padding: var(--Spacing-x2) var(--Spacing-x4) var(--Spacing-x2) + var(--Spacing-x2); +} + +.tempImage { + align-items: center; + background-color: lightgray; + border-radius: var(--Corner-radius-Medium); + display: flex; + height: auto; + justify-content: center; + min-height: 80px; +} + +.content { + align-items: center; + display: grid; + gap: var(--Spacing-x3); + grid-template-columns: 1fr auto; +} + +.textContainer { + display: grid; +} + +.label { + grid-column: 1 / -1; +} + +.text { + display: flex; + flex-wrap: wrap; + gap: var(--Spacing-x1); +} + +p.invertFontWeight { + font-weight: 400; +} + +.invertFontWeight:not(:last-of-type)::after, +.room::after { + color: var(--UI-Text-Medium-contrast); + content: "∙"; + padding-left: var(--Spacing-x1); +} diff --git a/components/HotelReservation/HotelSelectionHeader/hotelSelectionHeader.module.css b/components/HotelReservation/HotelSelectionHeader/hotelSelectionHeader.module.css index 9646ec746..268bfe4fe 100644 --- a/components/HotelReservation/HotelSelectionHeader/hotelSelectionHeader.module.css +++ b/components/HotelReservation/HotelSelectionHeader/hotelSelectionHeader.module.css @@ -1,10 +1,13 @@ .hotelSelectionHeader { - display: flex; - flex-direction: column; background-color: var(--Base-Surface-Subtle-Normal); padding: var(--Spacing-x3) var(--Spacing-x2); - justify-content: center; +} + +.hotelSelectionHeaderWrapper { + display: flex; + flex-direction: column; gap: var(--Spacing-x3); + justify-content: center; } .titleContainer { @@ -33,9 +36,15 @@ @media (min-width: 768px) { .hotelSelectionHeader { - flex-direction: row; padding: var(--Spacing-x4) var(--Spacing-x5); + } + + .hotelSelectionHeaderWrapper { + flex-direction: row; gap: var(--Spacing-x6); + max-width: var(--max-width-navigation); + margin: 0 auto; + width: 100%; } .titleContainer > h1 { diff --git a/components/HotelReservation/HotelSelectionHeader/index.tsx b/components/HotelReservation/HotelSelectionHeader/index.tsx index e34f05f37..222dceda1 100644 --- a/components/HotelReservation/HotelSelectionHeader/index.tsx +++ b/components/HotelReservation/HotelSelectionHeader/index.tsx @@ -19,35 +19,35 @@ export default function HotelSelectionHeader({ return (
-
- - {hotel.name} - -
-
-
- -
- - - -
- -
-
- - {hotel.hotelContent.texts.descriptions.short} - - +
+
+ + {hotel.name} + +
+
+
+ +
+ + + +
+ +
+
+ + {hotel.hotelContent.texts.descriptions.short} + + +
) diff --git a/components/HotelReservation/SelectRate/Payment/index.tsx b/components/HotelReservation/SelectRate/Payment/index.tsx index 3ddf73c01..8216e8631 100644 --- a/components/HotelReservation/SelectRate/Payment/index.tsx +++ b/components/HotelReservation/SelectRate/Payment/index.tsx @@ -129,7 +129,7 @@ export default function Payment({ hotel }: PaymentProps) { name="payment-method" id="card" value="card" - checked={selectedPaymentMethod === "card"} + defaultChecked={selectedPaymentMethod === "card"} /> @@ -145,7 +145,7 @@ export default function Payment({ hotel }: PaymentProps) { name="payment-method" id={paymentOption} value={paymentOption} - checked={selectedPaymentMethod === paymentOption} + defaultChecked={selectedPaymentMethod === paymentOption} /> diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx index 5760c219f..cda09ded4 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx @@ -9,17 +9,31 @@ import styles from "./flexibilityOption.module.css" import { FlexibilityOptionProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption" export default function FlexibilityOption({ - currency, - standardPrice, - memberPrice, + product, name, - value, paymentTerm, }: FlexibilityOptionProps) { const intl = useIntl() + + if (!product) { + // TODO: Implement empty state when this rate can't be booked + return
TBI: Rate not available
+ } + + const { productType } = product + const { public: publicPrice, member: memberPrice } = productType + const { localPrice: publicLocalPrice, requestedPrice: publicRequestedPrice } = + publicPrice + const { localPrice: memberLocalPrice, requestedPrice: memberRequestedPrice } = + memberPrice + return ( + @@ -32,20 +66,15 @@ export default function RoomCard({ {/*TODO: Handle pluralisation*/} {intl.formatMessage( { - id: "Nr night, nr adult", - defaultMessage: - "{nights, number} night, {adults, number} adult", + id: "Max {nrOfGuests} guests", + defaultMessage: "Max {nrOfGuests} guests", }, - { nights: nrOfNights, adults: nrOfAdults } + // TODO: Correct number + { nrOfGuests: 2 } )} - {" | "} - {breakfastIncluded - ? intl.formatMessage({ - id: "Breakfast included", - }) - : intl.formatMessage({ - id: "Breakfast excluded", - })} + {intl.formatMessage({ + id: "Breakfast included", + })} @@ -53,25 +82,19 @@ export default function RoomCard({ name={intl.formatMessage({ id: "Non-refundable" })} value="non-refundable" paymentTerm={intl.formatMessage({ id: "Pay now" })} - standardPrice={room.prices.nonRefundable.standard} - memberPrice={room.prices.nonRefundable.member} - currency={room.prices.currency} + product={saveProduct} /> + + ) } diff --git a/components/HotelReservation/SelectRate/RoomSelection/roomSelection.module.css b/components/HotelReservation/SelectRate/RoomSelection/roomSelection.module.css index 80ed0e5be..2f4fb9402 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/roomSelection.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/roomSelection.module.css @@ -27,4 +27,6 @@ bottom: 0; left: 0; right: 0; + background-color: white; + padding: var(--Spacing-x3) var(--Spacing-x7) var(--Spacing-x5); } diff --git a/components/HotelReservation/SelectRate/SectionAccordion/sectionAccordion.module.css b/components/HotelReservation/SelectRate/SectionAccordion/sectionAccordion.module.css index 8c1a05ba4..653c98fad 100644 --- a/components/HotelReservation/SelectRate/SectionAccordion/sectionAccordion.module.css +++ b/components/HotelReservation/SelectRate/SectionAccordion/sectionAccordion.module.css @@ -40,7 +40,7 @@ .iconWrapper { position: relative; top: var(--Spacing-x1); - z-index: 10; + z-index: 2; } .circle { diff --git a/components/Icons/icon.module.css b/components/Icons/icon.module.css index 3fa235d32..bb86becdb 100644 --- a/components/Icons/icon.module.css +++ b/components/Icons/icon.module.css @@ -66,3 +66,8 @@ .blue * { fill: var(--UI-Input-Controls-Fill-Selected); } + +.baseButtonTertiaryOnFillNormal, +.baseButtonTertiaryOnFillNormal * { + fill: var(--Base-Button-Tertiary-On-Fill-Normal); +} diff --git a/components/Icons/variants.ts b/components/Icons/variants.ts index 12b9cb574..911d183da 100644 --- a/components/Icons/variants.ts +++ b/components/Icons/variants.ts @@ -5,19 +5,20 @@ import styles from "./icon.module.css" const config = { variants: { color: { + baseButtonTertiaryOnFillNormal: styles.baseButtonTertiaryOnFillNormal, baseIconLowContrast: styles.baseIconLowContrast, black: styles.black, + blue: styles.blue, burgundy: styles.burgundy, + green: styles.green, grey80: styles.grey80, pale: styles.pale, peach80: styles.peach80, primaryLightOnSurfaceAccent: styles.plosa, red: styles.red, - green: styles.green, white: styles.white, uiTextHighContrast: styles.uiTextHighContrast, uiTextMediumContrast: styles.uiTextMediumContrast, - blue: styles.blue, }, }, defaultVariants: { diff --git a/components/Lightbox/FullView.tsx b/components/Lightbox/FullView.tsx index 73f6dde98..5541d365b 100644 --- a/components/Lightbox/FullView.tsx +++ b/components/Lightbox/FullView.tsx @@ -49,10 +49,10 @@ export default function FullView({ className={styles.fullViewImage} >
diff --git a/components/TempDesignSystem/Button/button.module.css b/components/TempDesignSystem/Button/button.module.css index f6499c5c1..0afa102a1 100644 --- a/components/TempDesignSystem/Button/button.module.css +++ b/components/TempDesignSystem/Button/button.module.css @@ -64,10 +64,10 @@ a.default { justify-content: center; } -.icon { +.btn.icon:is(.small, .medium, .large) { align-items: center; display: flex; - gap: var(--Spacing-x-half); + gap: var(--Spacing-x1); justify-content: center; } @@ -76,7 +76,8 @@ a.default { font-size: var(--typography-Caption-Bold-fontSize); line-height: var(--typography-Caption-Bold-lineHeight); gap: var(--Spacing-x-quarter); - padding: calc(var(--Spacing-x1) + 2px) var(--Spacing-x2); /* Special case padding to adjust the missing border */ + padding: calc(var(--Spacing-x1) + 2px) var(--Spacing-x2); + /* Special case padding to adjust the missing border */ } .btn.small.secondary { @@ -85,7 +86,8 @@ a.default { .btn.medium { gap: var(--Spacing-x-half); - padding: calc(var(--Spacing-x-one-and-half) + 2px) var(--Spacing-x2); /* Special case padding to adjust the missing border */ + padding: calc(var(--Spacing-x-one-and-half) + 2px) var(--Spacing-x2); + /* Special case padding to adjust the missing border */ } .medium.secondary { @@ -94,7 +96,8 @@ a.default { .btn.large { gap: var(--Spacing-x-half); - padding: calc(var(--Spacing-x2) + 2px) var(--Spacing-x3); /* Special case padding to adjust the missing border */ + padding: calc(var(--Spacing-x2) + 2px) var(--Spacing-x3); + /* Special case padding to adjust the missing border */ } .large.secondary { diff --git a/components/TempDesignSystem/Form/Card/Checkbox.tsx b/components/TempDesignSystem/Form/ChoiceCard/Checkbox.tsx similarity index 58% rename from components/TempDesignSystem/Form/Card/Checkbox.tsx rename to components/TempDesignSystem/Form/ChoiceCard/Checkbox.tsx index 7f0ea428a..ca32951cb 100644 --- a/components/TempDesignSystem/Form/Card/Checkbox.tsx +++ b/components/TempDesignSystem/Form/ChoiceCard/Checkbox.tsx @@ -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) { return diff --git a/components/TempDesignSystem/Form/Card/Radio.tsx b/components/TempDesignSystem/Form/ChoiceCard/Radio.tsx similarity index 57% rename from components/TempDesignSystem/Form/Card/Radio.tsx rename to components/TempDesignSystem/Form/ChoiceCard/Radio.tsx index c1de94782..f35ab6a5e 100644 --- a/components/TempDesignSystem/Form/Card/Radio.tsx +++ b/components/TempDesignSystem/Form/ChoiceCard/Radio.tsx @@ -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) { return diff --git a/components/TempDesignSystem/Form/Card/card.module.css b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css similarity index 100% rename from components/TempDesignSystem/Form/Card/card.module.css rename to components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css diff --git a/components/TempDesignSystem/Form/Card/card.ts b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.ts similarity index 73% rename from components/TempDesignSystem/Form/Card/card.ts rename to components/TempDesignSystem/Form/ChoiceCard/_Card/card.ts index 167595164..d1a961150 100644 --- a/components/TempDesignSystem/Form/Card/card.ts +++ b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.ts @@ -1,14 +1,15 @@ import type { IconProps } from "@/types/components/icon" -interface BaseCardProps extends React.LabelHTMLAttributes { +interface BaseCardProps + extends Omit, "title"> { Icon?: (props: IconProps) => JSX.Element declined?: boolean + highlightSubtitle?: boolean iconHeight?: number iconWidth?: number - name?: string - saving?: boolean - subtitle?: string - title: string + name: string + subtitle?: React.ReactNode + title: React.ReactNode type: "checkbox" | "radio" value?: string } @@ -22,7 +23,7 @@ interface ListCardProps extends BaseCardProps { interface TextCardProps extends BaseCardProps { list?: never - text: string + text: React.ReactNode } export type CardProps = ListCardProps | TextCardProps diff --git a/components/TempDesignSystem/Form/Card/index.tsx b/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx similarity index 94% rename from components/TempDesignSystem/Form/Card/index.tsx rename to components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx index 82f99e80d..8d020ea4d 100644 --- a/components/TempDesignSystem/Form/Card/index.tsx +++ b/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx @@ -13,10 +13,10 @@ export default function Card({ iconHeight = 32, iconWidth = 32, declined = false, + highlightSubtitle = false, id, list, - name = "join", - saving = false, + name, subtitle, text, title, @@ -31,7 +31,7 @@ export default function Card({ {subtitle ? (
+import type { THeadProps } from "./table" + +function TH({ children, width = "auto", ...props }: THeadProps) { + return ( + + ) } export default TH diff --git a/components/TempDesignSystem/Table/index.tsx b/components/TempDesignSystem/Table/index.tsx index 5ba745df5..87f50326a 100644 --- a/components/TempDesignSystem/Table/index.tsx +++ b/components/TempDesignSystem/Table/index.tsx @@ -3,11 +3,33 @@ import TD from "./TD" import TH from "./TH" import THead from "./THead" import TR from "./TR" +import { tableVariants } from "./variants" -import styles from "./table.module.css" +import type { TableProps } from "./table" -function Table({ children }: React.PropsWithChildren) { - return
- {hotel.address.streetAddress}, {hotel.address.city} - - {intl.formatMessage( - { - id: "Distance to city centre", - }, - { number: hotel.location.distanceToCentre } - )} - + {hotel.address.streetAddress}, {hotel.address.city} + + {intl.formatMessage( + { id: "Distance to city centre" }, + { number: hotel.location.distanceToCentre } + )} + {room.size}Room size TBI {subtitle} diff --git a/components/TempDesignSystem/ScrollWrapper/index.tsx b/components/TempDesignSystem/ScrollWrapper/index.tsx new file mode 100644 index 000000000..529836f20 --- /dev/null +++ b/components/TempDesignSystem/ScrollWrapper/index.tsx @@ -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() + + 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 ( +
+
+ {children} +
+
+ ) +} diff --git a/components/TempDesignSystem/ScrollWrapper/scrollWrapper.module.css b/components/TempDesignSystem/ScrollWrapper/scrollWrapper.module.css new file mode 100644 index 000000000..b604a3c24 --- /dev/null +++ b/components/TempDesignSystem/ScrollWrapper/scrollWrapper.module.css @@ -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; +} diff --git a/components/TempDesignSystem/ScrollWrapper/scrollWrapper.ts b/components/TempDesignSystem/ScrollWrapper/scrollWrapper.ts new file mode 100644 index 000000000..8440abfa5 --- /dev/null +++ b/components/TempDesignSystem/ScrollWrapper/scrollWrapper.ts @@ -0,0 +1,2 @@ +export interface ScrollWrapperProps + extends React.PropsWithChildren> {} diff --git a/components/TempDesignSystem/ShowMoreButton/button.module.css b/components/TempDesignSystem/ShowMoreButton/button.module.css deleted file mode 100644 index f55557f33..000000000 --- a/components/TempDesignSystem/ShowMoreButton/button.module.css +++ /dev/null @@ -1,4 +0,0 @@ -.container { - display: flex; - justify-content: center; -} diff --git a/components/TempDesignSystem/ShowMoreButton/index.tsx b/components/TempDesignSystem/ShowMoreButton/index.tsx index 6e9cae7bf..ca8a24dcb 100644 --- a/components/TempDesignSystem/ShowMoreButton/index.tsx +++ b/components/TempDesignSystem/ShowMoreButton/index.tsx @@ -5,18 +5,29 @@ import { useIntl } from "react-intl" import { ChevronDownIcon } from "@/components/Icons" 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({ + className, + intent, disabled, + showLess, loadMoreData, -}: ShowMoreButtonParams) { - const { formatMessage } = useIntl() +}: ShowMoreButtonProps) { + const intl = useIntl() + const classNames = showMoreButtonVariants({ + className, + intent, + }) + return ( -
+
) diff --git a/components/TempDesignSystem/ShowMoreButton/showMoreButton.module.css b/components/TempDesignSystem/ShowMoreButton/showMoreButton.module.css new file mode 100644 index 000000000..32299ec80 --- /dev/null +++ b/components/TempDesignSystem/ShowMoreButton/showMoreButton.module.css @@ -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); +} diff --git a/components/TempDesignSystem/ShowMoreButton/showMoreButton.ts b/components/TempDesignSystem/ShowMoreButton/showMoreButton.ts new file mode 100644 index 000000000..b6ad929f5 --- /dev/null +++ b/components/TempDesignSystem/ShowMoreButton/showMoreButton.ts @@ -0,0 +1,11 @@ +import { showMoreButtonVariants } from "./variants" + +import type { VariantProps } from "class-variance-authority" + +export interface ShowMoreButtonProps + extends React.PropsWithChildren>, + VariantProps { + disabled?: boolean + showLess?: boolean + loadMoreData: () => void +} diff --git a/components/TempDesignSystem/ShowMoreButton/variants.ts b/components/TempDesignSystem/ShowMoreButton/variants.ts new file mode 100644 index 000000000..17f8620f1 --- /dev/null +++ b/components/TempDesignSystem/ShowMoreButton/variants.ts @@ -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, + }, + }, +}) diff --git a/components/TempDesignSystem/Table/TH.tsx b/components/TempDesignSystem/Table/TH.tsx index ca2377193..657dc2b1d 100644 --- a/components/TempDesignSystem/Table/TH.tsx +++ b/components/TempDesignSystem/Table/TH.tsx @@ -1,7 +1,13 @@ import styles from "./table.module.css" -function TH({ children }: React.PropsWithChildren) { - return
{children} + {children} +
{children}
+function Table({ + className, + intent, + borderRadius, + variant, + layout, + width = "100%", + children, + ...props +}: TableProps) { + const classNames = tableVariants({ + className, + borderRadius, + intent, + layout, + variant, + }) + + return ( + + {children} +
+ ) } Table.THead = THead diff --git a/components/TempDesignSystem/Table/table.module.css b/components/TempDesignSystem/Table/table.module.css index fcbb3fec2..2296b2ecd 100644 --- a/components/TempDesignSystem/Table/table.module.css +++ b/components/TempDesignSystem/Table/table.module.css @@ -1,20 +1,20 @@ .table { - border-radius: var(--Corner-radius-Medium); border-collapse: collapse; overflow: hidden; - width: 100%; + min-width: 100%; } .thead { - background-color: var(--Base-Background-Secondary-Normal, #f7e1d5); + color: var(--Base-Text-High-contrast); + background-color: var(--Base-Surface-Primary-dark-Normal); } .tbody { - background-color: var(--Base-Surface-Primary-light-Normal, #fff); + background-color: var(--Base-Surface-Primary-light-Normal); } .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 { @@ -28,6 +28,35 @@ 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) { .th { padding: var(--Spacing-x2) var(--Spacing-x3); diff --git a/components/TempDesignSystem/Table/table.ts b/components/TempDesignSystem/Table/table.ts new file mode 100644 index 000000000..f2759484d --- /dev/null +++ b/components/TempDesignSystem/Table/table.ts @@ -0,0 +1,14 @@ +import { tableVariants } from "./variants" + +import type { VariantProps } from "class-variance-authority" + +export interface TableProps + extends React.PropsWithChildren>, + VariantProps { + width?: string +} + +export interface THeadProps + extends React.PropsWithChildren> { + width?: string +} diff --git a/components/TempDesignSystem/Table/variants.ts b/components/TempDesignSystem/Table/variants.ts new file mode 100644 index 000000000..c70202dcf --- /dev/null +++ b/components/TempDesignSystem/Table/variants.ts @@ -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", + }, +}) diff --git a/components/TempDesignSystem/Text/Subtitle/subtitle.module.css b/components/TempDesignSystem/Text/Subtitle/subtitle.module.css index 8fdac30c4..22d09e16e 100644 --- a/components/TempDesignSystem/Text/Subtitle/subtitle.module.css +++ b/components/TempDesignSystem/Text/Subtitle/subtitle.module.css @@ -10,7 +10,7 @@ 0.3vw + 15px, var(--typography-Subtitle-1-Desktop-fontSize) ); - font-weight: 600; + font-weight: 500; letter-spacing: var(--typography-Subtitle-1-letterSpacing); line-height: var(--typography-Subtitle-1-lineHeight); } @@ -22,7 +22,7 @@ 0.3vw + 15px, var(--typography-Subtitle-2-Desktop-fontSize) ); - font-weight: 600; + font-weight: 500; letter-spacing: var(--typography-Subtitle-2-letterSpacing); line-height: var(--typography-Subtitle-2-lineHeight); } @@ -62,3 +62,7 @@ .uiTextHighContrast { color: var(--UI-Text-High-contrast); } + +.uiTextMediumContrast { + color: var(--UI-Text-Medium-contrast); +} diff --git a/components/TempDesignSystem/Text/Subtitle/variants.ts b/components/TempDesignSystem/Text/Subtitle/variants.ts index 7a8faa54c..3e36d39e0 100644 --- a/components/TempDesignSystem/Text/Subtitle/variants.ts +++ b/components/TempDesignSystem/Text/Subtitle/variants.ts @@ -9,6 +9,7 @@ const config = { burgundy: styles.burgundy, pale: styles.pale, uiTextHighContrast: styles.uiTextHighContrast, + uiTextMediumContrast: styles.uiTextMediumContrast, }, textAlign: { center: styles.center, diff --git a/hooks/useScrollShadows.ts b/hooks/useScrollShadows.ts new file mode 100644 index 000000000..3fde382f1 --- /dev/null +++ b/hooks/useScrollShadows.ts @@ -0,0 +1,34 @@ +import { useEffect, useRef, useState } from "react" + +export default function useScrollShadows() { + const containerRef = useRef(null) + const [showLeftShadow, setShowLeftShadow] = useState(false) + const [showRightShadow, setShowRightShadow] = useState(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 } +} diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index a260dd068..76237d5a5 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -13,13 +13,12 @@ "Already a friend?": "Allerede en ven?", "Amenities": "Faciliteter", "Amusement park": "Forlystelsespark", - "An error occurred when adding a credit card, please try again later.": "Der opstod en fejl under tilføjelse af et kreditkort. Prøv venligst igen senere.", "An error occurred trying to manage your preferences, please try again later.": "Der opstod en fejl under forsøget på at administrere dine præferencer. Prøv venligst igen senere.", + "An error occurred when adding a credit card, please try again later.": "Der opstod en fejl under tilføjelse af et kreditkort. Prøv venligst igen senere.", "An error occurred when trying to update profile.": "Der opstod en fejl under forsøg på at opdatere profilen.", "Any changes you've made will be lost.": "Alle ændringer, du har foretaget, går tabt.", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Er du sikker på, at du vil fjerne kortet, der slutter me {lastFourDigits} fra din medlemsprofil?", "Arrival date": "Ankomstdato", - "as of today": "pr. dags dato", "As our": "Som vores {level}", "As our Close Friend": "Som vores nære ven", "At latest": "Senest", @@ -35,9 +34,9 @@ "Breakfast buffet": "Morgenbuffet", "Breakfast excluded": "Morgenmad ikke inkluderet", "Breakfast included": "Morgenmad inkluderet", + "Breakfast restaurant": "Breakfast restaurant", "Bus terminal": "Busstation", "Business": "Forretning", - "Breakfast restaurant": "Breakfast restaurant", "Cancel": "Afbestille", "Check in": "Check ind", "Check out": "Check ud", @@ -83,9 +82,9 @@ "Edit profile": "Rediger profil", "Email": "E-mail", "Email address": "E-mailadresse", + "Enjoy relaxed restaurant experiences": "Enjoy relaxed restaurant experiences", "Enter destination or hotel": "Indtast destination eller hotel", "Enter your details": "Indtast dine oplysninger", - "Enjoy relaxed restaurant experiences": "Enjoy relaxed restaurant experiences", "Events that make an impression": "Events that make an impression", "Explore all levels and benefits": "Udforsk alle niveauer og fordele", "Explore nearby": "Udforsk i nærheden", @@ -121,7 +120,6 @@ "Join Scandic Friends": "Tilmeld dig Scandic Friends", "Join at no cost": "Tilmeld dig uden omkostninger", "King bed": "Kingsize-seng", - "km to city center": "km til byens centrum", "Language": "Sprog", "Lastname": "Efternavn", "Latest searches": "Seneste søgninger", @@ -209,11 +207,11 @@ "Read more about the hotel": "Læs mere om hotellet", "Read more about wellness & exercise": "Read more about wellness & exercise", "Remove card from member profile": "Fjern kortet fra medlemsprofilen", + "Request bedtype": "Anmod om sengetype", "Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}", "Restaurant & Bar": "Restaurant & Bar", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Gentag den nye adgangskode", - "Request bedtype": "Anmod om sengetype", "Room & Terms": "Værelse & Vilkår", "Room facilities": "Værelsesfaciliteter", "Rooms": "Værelser", @@ -299,11 +297,12 @@ "Your details": "Dine oplysninger", "Your level": "Dit niveau", "Your points to spend": "Dine brugbare point", + "Your room": "Dit værelse", "Zip code": "Postnummer", "Zoo": "Zoo", "Zoom in": "Zoom ind", "Zoom out": "Zoom ud", - "as of today": "fra idag", + "as of today": "pr. dags dato", "booking.adults": "{totalAdults, plural, one {# voksen} other {# voksne}}", "booking.nights": "{totalNights, plural, one {# nat} other {# nætter}}", "booking.rooms": "{totalRooms, plural, one {# værelse} other {# værelser}}", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index ad755efdc..742890d7c 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -207,11 +207,11 @@ "Read more about the hotel": "Lesen Sie mehr über das Hotel", "Read more about wellness & exercise": "Read more about wellness & exercise", "Remove card from member profile": "Karte aus dem Mitgliedsprofil entfernen", + "Request bedtype": "Bettentyp anfragen", "Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}", "Restaurant & Bar": "Restaurant & Bar", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Neues Passwort erneut eingeben", - "Request bedtype": "Bettentyp anfragen", "Room & Terms": "Zimmer & Bedingungen", "Room facilities": "Zimmerausstattung", "Rooms": "Räume", @@ -297,6 +297,7 @@ "Your details": "Ihre Angaben", "Your level": "Dein level", "Your points to spend": "Meine Punkte", + "Your room": "Ihr Zimmer", "Zip code": "PLZ", "Zoo": "Zoo", "Zoom in": "Vergrößern", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 43903c4c2..fa28c7399 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -120,7 +120,6 @@ "Join Scandic Friends": "Join Scandic Friends", "Join at no cost": "Join at no cost", "King bed": "King bed", - "km to city center": "km to city center", "Language": "Language", "Lastname": "Lastname", "Latest searches": "Latest searches", @@ -208,6 +207,7 @@ "Read more about the hotel": "Read more about the hotel", "Read more about wellness & exercise": "Read more about wellness & exercise", "Remove card from member profile": "Remove card from member profile", + "Request bedtype": "Request bedtype", "Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}", "Restaurant & Bar": "Restaurant & Bar", "Restaurants & Bars": "Restaurants & Bars", @@ -226,10 +226,12 @@ "See room details": "See room details", "See rooms": "See rooms", "Select a country": "Select a country", + "Select breakfast options": "Select breakfast options", "Select country of residence": "Select country of residence", "Select date of birth": "Select date of birth", "Select dates": "Select dates", "Select language": "Select language", + "Select payment method": "Select payment method", "Select your language": "Select your language", "Shopping": "Shopping", "Shopping & Dining": "Shopping & Dining", @@ -295,6 +297,7 @@ "Your details": "Your details", "Your level": "Your level", "Your points to spend": "Your points to spend", + "Your room": "Your room", "Zip code": "Zip code", "Zoo": "Zoo", "Zoom in": "Zoom in", @@ -315,9 +318,6 @@ "number": "number", "or": "or", "points": "Points", - "Request bedtype": "Request bedtype", - "Select breakfast options": "Select breakfast options", - "Select payment method": "Select payment method", "special character": "special character", "spendable points expiring by": "{points} spendable points expiring by {date}", "to": "to", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 558c99dcb..ec0b1ee0c 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -13,8 +13,8 @@ "Already a friend?": "Oletko jo ystävä?", "Amenities": "Mukavuudet", "Amusement park": "Huvipuisto", - "An error occurred when adding a credit card, please try again later.": "Luottokorttia lisättäessä tapahtui virhe. Yritä myöhemmin uudelleen.", "An error occurred trying to manage your preferences, please try again later.": "Asetusten hallinnassa tapahtui virhe. Yritä myöhemmin uudelleen.", + "An error occurred when adding a credit card, please try again later.": "Luottokorttia lisättäessä tapahtui virhe. Yritä myöhemmin uudelleen.", "An error occurred when trying to update profile.": "Profiilia päivitettäessä tapahtui virhe.", "Any changes you've made will be lost.": "Kaikki tekemäsi muutokset menetetään.", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Haluatko varmasti poistaa kortin, joka päättyy numeroon {lastFourDigits} jäsenprofiilistasi?", @@ -34,9 +34,9 @@ "Breakfast buffet": "Aamiaisbuffet", "Breakfast excluded": "Aamiainen ei sisälly", "Breakfast included": "Aamiainen sisältyy", + "Breakfast restaurant": "Breakfast restaurant", "Bus terminal": "Bussiasema", "Business": "Business", - "Breakfast restaurant": "Breakfast restaurant", "Cancel": "Peruuttaa", "Check in": "Sisäänkirjautuminen", "Check out": "Uloskirjautuminen", @@ -82,9 +82,9 @@ "Edit profile": "Muokkaa profiilia", "Email": "Sähköposti", "Email address": "Sähköpostiosoite", + "Enjoy relaxed restaurant experiences": "Enjoy relaxed restaurant experiences", "Enter destination or hotel": "Anna kohde tai hotelli", "Enter your details": "Anna tietosi", - "Enjoy relaxed restaurant experiences": "Enjoy relaxed restaurant experiences", "Events that make an impression": "Events that make an impression", "Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin", "Explore nearby": "Tutustu lähialueeseen", @@ -120,7 +120,6 @@ "Join Scandic Friends": "Liity jäseneksi", "Join at no cost": "Liity maksutta", "King bed": "King-vuode", - "km to city center": "km keskustaan", "Language": "Kieli", "Lastname": "Sukunimi", "Latest searches": "Viimeisimmät haut", @@ -208,6 +207,7 @@ "Read more about the hotel": "Lue lisää hotellista", "Read more about wellness & exercise": "Read more about wellness & exercise", "Remove card from member profile": "Poista kortti jäsenprofiilista", + "Request bedtype": "Pyydä sänkytyyppiä", "Restaurant": "{count, plural, one {#Ravintola} other {#Restaurants}}", "Restaurant & Bar": "Ravintola & Baari", "Restaurants & Bars": "Restaurants & Bars", @@ -217,7 +217,6 @@ "Rooms": "Huoneet", "Rooms & Guests": "Huoneet & Vieraat", "Rooms & Guestss": "Huoneet & Vieraat", - "Request bedtype": "Pyydä sänkytyyppiä", "Sauna and gym": "Sauna and gym", "Save": "Tallenna", "Scandic Friends Mastercard": "Scandic Friends Mastercard", @@ -299,6 +298,7 @@ "Your details": "Tietosi", "Your level": "Tasosi", "Your points to spend": "Käytettävissä olevat pisteesi", + "Your room": "Sinun huoneesi", "Zip code": "Postinumero", "Zoo": "Eläintarha", "Zoom in": "Lähennä", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 5db6252f3..4a7ab2908 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -13,13 +13,12 @@ "Already a friend?": "Allerede Friend?", "Amenities": "Fasiliteter", "Amusement park": "Tivoli", - "An error occurred when adding a credit card, please try again later.": "Det oppstod en feil ved å legge til et kredittkort. Prøv igjen senere.", "An error occurred trying to manage your preferences, please try again later.": "Det oppstod en feil under forsøket på å administrere innstillingene dine. Prøv igjen senere.", + "An error occurred when adding a credit card, please try again later.": "Det oppstod en feil ved å legge til et kredittkort. Prøv igjen senere.", "An error occurred when trying to update profile.": "Det oppstod en feil under forsøk på å oppdatere profilen.", "Any changes you've made will be lost.": "Eventuelle endringer du har gjort, går tapt.", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Er du sikker på at du vil fjerne kortet som slutter på {lastFourDigits} fra medlemsprofilen din?", "Arrival date": "Ankomstdato", - "as of today": "per i dag", "As our": "Som vår {level}", "As our Close Friend": "Som vår nære venn", "At latest": "Senest", @@ -82,16 +81,16 @@ "Edit profile": "Rediger profil", "Email": "E-post", "Email address": "E-postadresse", + "Enjoy relaxed restaurant experiences": "Enjoy relaxed restaurant experiences", "Enter destination or hotel": "Skriv inn destinasjon eller hotell", "Enter your details": "Skriv inn detaljene dine", - "Enjoy relaxed restaurant experiences": "Enjoy relaxed restaurant experiences", "Events that make an impression": "Events that make an impression", "Explore all levels and benefits": "Utforsk alle nivåer og fordeler", "Explore nearby": "Utforsk i nærheten", "Extras to your booking": "Tilvalg til bestillingen din", + "FAQ": "FAQ", "Failed to delete credit card, please try again later.": "Kunne ikke slette kredittkortet, prøv igjen senere.", "Fair": "Messe", - "FAQ": "FAQ", "Find booking": "Finn booking", "Find hotels": "Finn hotell", "Firstname": "Fornavn", @@ -120,7 +119,6 @@ "Join Scandic Friends": "Bli med i Scandic Friends", "Join at no cost": "Bli med uten kostnad", "King bed": "King-size-seng", - "km to city center": "km til sentrum", "Language": "Språk", "Lastname": "Etternavn", "Latest searches": "Siste søk", @@ -208,11 +206,11 @@ "Read more about the hotel": "Les mer om hotellet", "Read more about wellness & exercise": "Read more about wellness & exercise", "Remove card from member profile": "Fjern kortet fra medlemsprofilen", + "Request bedtype": "Be om sengetype", "Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}", "Restaurant & Bar": "Restaurant & Bar", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Skriv inn nytt passord på nytt", - "Request bedtype": "Be om sengetype", "Room & Terms": "Rom & Vilkår", "Room facilities": "Romfasiliteter", "Rooms": "Rom", @@ -298,11 +296,12 @@ "Your details": "Dine detaljer", "Your level": "Ditt nivå", "Your points to spend": "Dine brukbare poeng", + "Your room": "Rommet ditt", "Zip code": "Post kode", "Zoo": "Dyrehage", "Zoom in": "Zoom inn", "Zoom out": "Zoom ut", - "as of today": "per idag", + "as of today": "per i dag", "booking.adults": "{totalAdults, plural, one {# voksen} other {# voksne}}", "booking.nights": "{totalNights, plural, one {# natt} other {# netter}}", "booking.rooms": "{totalRooms, plural, one {# rom} other {# rom}}", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index e7b55e6f6..28c7a9889 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -13,13 +13,12 @@ "Already a friend?": "Är du redan en vän?", "Amenities": "Bekvämligheter", "Amusement park": "Nöjespark", - "An error occurred when adding a credit card, please try again later.": "Ett fel uppstod när ett kreditkort lades till, försök igen senare.", "An error occurred trying to manage your preferences, please try again later.": "Ett fel uppstod när du försökte hantera dina inställningar, försök igen senare.", + "An error occurred when adding a credit card, please try again later.": "Ett fel uppstod när ett kreditkort lades till, försök igen senare.", "An error occurred when trying to update profile.": "Ett fel uppstod när du försökte uppdatera profilen.", "Any changes you've made will be lost.": "Alla ändringar du har gjort kommer att gå förlorade.", "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Är du säker på att du vill ta bort kortet som slutar med {lastFourDigits} från din medlemsprofil?", "Arrival date": "Ankomstdatum", - "as of today": "per idag", "As our": "Som vår {level}", "As our Close Friend": "Som vår nära vän", "At latest": "Senast", @@ -35,9 +34,9 @@ "Breakfast buffet": "Frukostbuffé", "Breakfast excluded": "Frukost ingår ej", "Breakfast included": "Frukost ingår", + "Breakfast restaurant": "Breakfast restaurant", "Bus terminal": "Bussterminal", "Business": "Business", - "Breakfast restaurant": "Breakfast restaurant", "Cancel": "Avbryt", "Check in": "Checka in", "Check out": "Checka ut", @@ -83,9 +82,9 @@ "Edit profile": "Redigera profil", "Email": "E-post", "Email address": "E-postadress", + "Enjoy relaxed restaurant experiences": "Enjoy relaxed restaurant experiences", "Enter destination or hotel": "Ange destination eller hotell", "Enter your details": "Ange dina uppgifter", - "Enjoy relaxed restaurant experiences": "Enjoy relaxed restaurant experiences", "Events that make an impression": "Events that make an impression", "Explore all levels and benefits": "Utforska alla nivåer och fördelar", "Explore nearby": "Utforska i närheten", @@ -118,11 +117,9 @@ "How it works": "Hur det fungerar", "Image gallery": "Bildgalleri", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det gick inte att hantera dina kommunikationsinställningar just nu, försök igen senare eller kontakta supporten om problemet kvarstår.", - "Join Scandic Friends": "Gå med i Scandic Friends", "Join at no cost": "Gå med utan kostnad", "King bed": "King size-säng", - "km to city center": "km till stadens centrum", "Language": "Språk", "Lastname": "Efternamn", "Latest searches": "Senaste sökningarna", @@ -210,11 +207,11 @@ "Read more about the hotel": "Läs mer om hotellet", "Read more about wellness & exercise": "Read more about wellness & exercise", "Remove card from member profile": "Ta bort kortet från medlemsprofilen", + "Request bedtype": "Request bedtype", "Restaurant": "{count, plural, one {#Restaurang} other {#Restauranger}}", "Restaurant & Bar": "Restaurang & Bar", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Upprepa nytt lösenord", - "Request bedtype": "Request bedtype", "Room & Terms": "Rum & Villkor", "Room facilities": "Rumfaciliteter", "Rooms": "Rum", @@ -266,7 +263,6 @@ "Tripadvisor reviews": "{rating} ({count} recensioner på Tripadvisor)", "Type of bed": "Sängtyp", "Type of room": "Rumstyp", - "uppercase letter": "stor bokstav", "Use bonus cheque": "Använd bonuscheck", "Use code/voucher": "Använd kod/voucher", "User information": "Användarinformation", @@ -301,6 +297,7 @@ "Your details": "Dina uppgifter", "Your level": "Din nivå", "Your points to spend": "Dina spenderbara poäng", + "Your room": "Ditt rum", "Zip code": "Postnummer", "Zoo": "Djurpark", "Zoom in": "Zooma in", diff --git a/lib/api/endpoints.ts b/lib/api/endpoints.ts index 9ead56554..66bc36ec3 100644 --- a/lib/api/endpoints.ts +++ b/lib/api/endpoints.ts @@ -7,6 +7,7 @@ export namespace endpoints { } export const enum v1 { hotelsAvailability = "availability/v1/availabilities/city", + roomsAvailability = "availability/v1/availabilities/hotel", profile = "profile/v1/Profile", booking = "booking/v1/Bookings", creditCards = `${profile}/creditCards`, diff --git a/lib/graphql/Fragments/Blocks/Table.graphql b/lib/graphql/Fragments/Blocks/Table.graphql new file mode 100644 index 000000000..66736296b --- /dev/null +++ b/lib/graphql/Fragments/Blocks/Table.graphql @@ -0,0 +1,9 @@ +fragment Table_ContentPage on ContentPageBlocksTable { + __typename + table { + heading + preamble + column_widths + table + } +} diff --git a/lib/graphql/Query/ContentPage/ContentPage.graphql b/lib/graphql/Query/ContentPage/ContentPage.graphql index c1e4decc5..2bed1d02d 100644 --- a/lib/graphql/Query/ContentPage/ContentPage.graphql +++ b/lib/graphql/Query/ContentPage/ContentPage.graphql @@ -4,6 +4,7 @@ #import "../../Fragments/Blocks/Content.graphql" #import "../../Fragments/Blocks/DynamicContent.graphql" #import "../../Fragments/Blocks/Shortcuts.graphql" +#import "../../Fragments/Blocks/Table.graphql" #import "../../Fragments/Blocks/TextCols.graphql" #import "../../Fragments/Blocks/UspGrid.graphql" @@ -28,6 +29,7 @@ query GetContentPage($locale: String!, $uid: String!) { ...Content_ContentPage ...DynamicContent_ContentPage ...Shortcuts_ContentPage + ...Table_ContentPage ...TextCols_ContentPage ...UspGrid_ContentPage } diff --git a/package-lock.json b/package-lock.json index 35805d69e..c12152ff6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", "@t3-oss/env-nextjs": "^0.9.2", "@tanstack/react-query": "^5.28.6", + "@tanstack/react-table": "^8.20.5", "@trpc/client": "^11.0.0-rc.467", "@trpc/react-query": "^11.0.0-rc.467", "@trpc/server": "^11.0.0-rc.467", @@ -5971,6 +5972,37 @@ "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": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.1.0.tgz", diff --git a/package.json b/package.json index 2be8bd832..ab79709d3 100644 --- a/package.json +++ b/package.json @@ -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", "@t3-oss/env-nextjs": "^0.9.2", "@tanstack/react-query": "^5.28.6", + "@tanstack/react-table": "^8.20.5", "@trpc/client": "^11.0.0-rc.467", "@trpc/react-query": "^11.0.0-rc.467", "@trpc/server": "^11.0.0-rc.467", diff --git a/server/routers/contentstack/contentPage/output.ts b/server/routers/contentstack/contentPage/output.ts index d4b02d33e..48ebb0f40 100644 --- a/server/routers/contentstack/contentPage/output.ts +++ b/server/routers/contentstack/contentPage/output.ts @@ -18,6 +18,7 @@ import { shortcutsRefsSchema, shortcutsSchema, } from "../schemas/blocks/shortcuts" +import { tableSchema } from "../schemas/blocks/table" import { textColsRefsSchema, textColsSchema } from "../schemas/blocks/textCols" import { uspGridRefsSchema, uspGridSchema } from "../schemas/blocks/uspGrid" import { tempImageVaultAssetSchema } from "../schemas/imageVault" @@ -75,11 +76,18 @@ export const contentPageUspGrid = z }) .merge(uspGridSchema) +export const contentPageTable = z + .object({ + __typename: z.literal(ContentPageEnum.ContentStack.blocks.Table), + }) + .merge(tableSchema) + export const blocksSchema = z.discriminatedUnion("__typename", [ contentPageCards, contentPageContent, contentPageDynamicContent, contentPageShortcuts, + contentPageTable, contentPageTextCols, contentPageUspGrid, ]) diff --git a/server/routers/contentstack/schemas/blocks/table.ts b/server/routers/contentstack/schemas/blocks/table.ts new file mode 100644 index 000000000..905c20603 --- /dev/null +++ b/server/routers/contentstack/schemas/blocks/table.ts @@ -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>((transformedRow, column) => { + transformedRow[column.id] = rowData[column.id] || "" + return transformedRow + }, {}) + ) + + return { + heading: data.heading, + preamble: data.preamble, + columns, + rows, + totalWidth, + } + }), +}) diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index b0ff5e679..6deff63a6 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -17,6 +17,17 @@ export const getHotelsAvailabilityInputSchema = z.object({ 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({ hotelId: z.string(), }) diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index d233f1a82..75d16b67b 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -572,6 +572,86 @@ export type HotelsAvailability = z.infer export type HotelsAvailabilityPrices = 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 +export type RoomConfiguration = z.infer +export type Product = z.infer +export type RateDefinition = z.infer + const flexibilityPrice = z.object({ standard: z.number(), member: z.number(), diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 0e319bf6e..859e5662c 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -24,11 +24,13 @@ import { getHotelsAvailabilityInputSchema, getlHotelDataInputSchema, getRatesInputSchema, + getRoomsAvailabilityInputSchema, } from "./input" import { getHotelDataSchema, getHotelsAvailabilitySchema, getRatesSchema, + getRoomsAvailabilitySchema, roomSchema, } from "./output" import tempRatesData from "./tempRatesData.json" @@ -61,6 +63,16 @@ const hotelsAvailabilityFailCounter = meter.createCounter( "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( locale: string, uid: string | null | undefined @@ -376,6 +388,123 @@ export const hotelQueryRouter = router({ .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 = { + 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({ get: publicProcedure diff --git a/server/routers/user/output.ts b/server/routers/user/output.ts index 9c5e955ab..d4a6f0dd4 100644 --- a/server/routers/user/output.ts +++ b/server/routers/user/output.ts @@ -111,6 +111,7 @@ export const getStaysSchema = z.object({ limit: z.number(), totalCount: z.number(), }) + .optional() .nullable(), }) diff --git a/types/components/blocks/table.ts b/types/components/blocks/table.ts new file mode 100644 index 000000000..9438f7d43 --- /dev/null +++ b/types/components/blocks/table.ts @@ -0,0 +1,5 @@ +import type { TableData } from "@/types/trpc/routers/contentstack/blocks" + +export interface TableBlockProps { + data: TableData +} diff --git a/types/components/hotelReservation/selectRate/flexibilityOption.ts b/types/components/hotelReservation/selectRate/flexibilityOption.ts index c61c09e94..d76f5a520 100644 --- a/types/components/hotelReservation/selectRate/flexibilityOption.ts +++ b/types/components/hotelReservation/selectRate/flexibilityOption.ts @@ -1,8 +1,8 @@ +import { Product, RateDefinition } from "@/server/routers/hotels/output" + export type FlexibilityOptionProps = { + product: Product | undefined name: string value: string paymentTerm: string - standardPrice: number - memberPrice: number - currency: string } diff --git a/types/components/hotelReservation/selectRate/roomCard.ts b/types/components/hotelReservation/selectRate/roomCard.ts index ae9528b10..b51b3d8ab 100644 --- a/types/components/hotelReservation/selectRate/roomCard.ts +++ b/types/components/hotelReservation/selectRate/roomCard.ts @@ -1,8 +1,9 @@ -import { Rate } from "@/server/routers/hotels/output" +import { + RateDefinition, + RoomConfiguration, +} from "@/server/routers/hotels/output" export type RoomCardProps = { - room: Rate - nrOfNights: number - nrOfAdults: number - breakfastIncluded: boolean + roomConfiguration: RoomConfiguration + rateDefinitions: RateDefinition[] } diff --git a/types/components/hotelReservation/selectRate/roomSelection.ts b/types/components/hotelReservation/selectRate/roomSelection.ts index d5203f461..33dae631b 100644 --- a/types/components/hotelReservation/selectRate/roomSelection.ts +++ b/types/components/hotelReservation/selectRate/roomSelection.ts @@ -1,7 +1,5 @@ -import { Rate } from "@/server/routers/hotels/output" +import { RoomsAvailability } from "@/server/routers/hotels/output" export interface RoomSelectionProps { - rates: Rate[] - nrOfAdults: number - nrOfNights: number + roomConfigurations: RoomsAvailability } diff --git a/types/enums/blocks.ts b/types/enums/blocks.ts index b91989b2e..81a088bd1 100644 --- a/types/enums/blocks.ts +++ b/types/enums/blocks.ts @@ -4,6 +4,7 @@ export namespace BlocksEnums { Content = "Content", DynamicContent = "DynamicContent", Shortcuts = "Shortcuts", + Table = "Table", TextCols = "TextCols", TextContent = "TextContent", UspGrid = "UspGrid", diff --git a/types/enums/contentPage.ts b/types/enums/contentPage.ts index 99dd48911..df5dab28e 100644 --- a/types/enums/contentPage.ts +++ b/types/enums/contentPage.ts @@ -7,6 +7,7 @@ export namespace ContentPageEnum { Shortcuts = "ContentPageBlocksShortcuts", TextCols = "ContentPageBlocksTextCols", UspGrid = "ContentPageBlocksUspGrid", + Table = "ContentPageBlocksTable", } export const enum sidebar { diff --git a/types/trpc/routers/contentstack/blocks.ts b/types/trpc/routers/contentstack/blocks.ts index 6bffdfc90..1c46dda93 100644 --- a/types/trpc/routers/contentstack/blocks.ts +++ b/types/trpc/routers/contentstack/blocks.ts @@ -4,6 +4,7 @@ import { cardsGridSchema } from "@/server/routers/contentstack/schemas/blocks/ca import { contentSchema } from "@/server/routers/contentstack/schemas/blocks/content" import { dynamicContentSchema } from "@/server/routers/contentstack/schemas/blocks/dynamicContent" 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 { uspGridSchema } from "@/server/routers/contentstack/schemas/blocks/uspGrid" @@ -12,5 +13,7 @@ export interface Content extends z.output {} export interface DynamicContent extends z.output {} export interface Shortcuts extends z.output {} export type Shortcut = Shortcuts["shortcuts"] +export interface TableBlock extends z.output {} +export type TableData = TableBlock["table"] export interface TextCols extends z.output {} export interface UspGrid extends z.output {}