Merge branch 'develop' into feature/sw-561-design-fixes
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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<LayoutArgs<LangParams>>) {
|
||||
const hotel = await serverClient().hotel.hotelData.get({
|
||||
hotelId: "811",
|
||||
language: params.lang,
|
||||
})
|
||||
|
||||
if (!hotel?.data) {
|
||||
redirect(`/${params.lang}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<main className={styles.layout}>
|
||||
<HotelSelectionHeader hotel={hotel.data.attributes} />
|
||||
<div className={styles.content}>
|
||||
<SelectedRoom />
|
||||
{children}
|
||||
<aside className={styles.summary}>
|
||||
<Summary />
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 (
|
||||
<main className={styles.page}>
|
||||
<HotelSelectionHeader hotel={hotel.data.attributes} />
|
||||
<div className={styles.content}>
|
||||
<section className={styles.section}>
|
||||
<SectionAccordion
|
||||
header="Select bed"
|
||||
isCompleted={true}
|
||||
isOpen={activeStep === StepEnum.selectBed}
|
||||
label={intl.formatMessage({ id: "Request bedtype" })}
|
||||
path="/select-bed"
|
||||
>
|
||||
<BedType />
|
||||
</SectionAccordion>
|
||||
<SectionAccordion
|
||||
header="Food options"
|
||||
isCompleted={true}
|
||||
isOpen={activeStep === StepEnum.breakfast}
|
||||
label={intl.formatMessage({ id: "Select breakfast options" })}
|
||||
path="/breakfast"
|
||||
>
|
||||
<Breakfast />
|
||||
</SectionAccordion>
|
||||
<SectionAccordion
|
||||
header="Details"
|
||||
isCompleted={false}
|
||||
isOpen={activeStep === StepEnum.details}
|
||||
label={intl.formatMessage({ id: "Enter your details" })}
|
||||
path="/details"
|
||||
>
|
||||
<Details user={null} />
|
||||
</SectionAccordion>
|
||||
<SectionAccordion
|
||||
header="Payment"
|
||||
isCompleted={false}
|
||||
isOpen={activeStep === StepEnum.payment}
|
||||
label={intl.formatMessage({ id: "Select payment method" })}
|
||||
path="/hotelreservation/select-bed"
|
||||
>
|
||||
<Payment hotel={hotel.data.attributes} />
|
||||
</SectionAccordion>
|
||||
</section>
|
||||
<aside className={styles.summary}>
|
||||
<Summary />
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
<section>
|
||||
<SectionAccordion
|
||||
header="Select bed"
|
||||
isCompleted={true}
|
||||
isOpen={activeStep === StepEnum.selectBed}
|
||||
label={intl.formatMessage({ id: "Request bedtype" })}
|
||||
path="/select-bed"
|
||||
>
|
||||
<BedType />
|
||||
</SectionAccordion>
|
||||
<SectionAccordion
|
||||
header="Food options"
|
||||
isCompleted={true}
|
||||
isOpen={activeStep === StepEnum.breakfast}
|
||||
label={intl.formatMessage({ id: "Select breakfast options" })}
|
||||
path="/breakfast"
|
||||
>
|
||||
<Breakfast />
|
||||
</SectionAccordion>
|
||||
<SectionAccordion
|
||||
header="Details"
|
||||
isCompleted={false}
|
||||
isOpen={activeStep === StepEnum.details}
|
||||
label={intl.formatMessage({ id: "Enter your details" })}
|
||||
path="/details"
|
||||
>
|
||||
<Details user={null} />
|
||||
</SectionAccordion>
|
||||
<SectionAccordion
|
||||
header="Payment"
|
||||
isCompleted={false}
|
||||
isOpen={activeStep === StepEnum.payment}
|
||||
label={intl.formatMessage({ id: "Select payment method" })}
|
||||
path="/hotelreservation/select-bed"
|
||||
>
|
||||
<Payment hotel={hotel.data.attributes} />
|
||||
</SectionAccordion>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
@@ -39,13 +35,7 @@ export default async function SelectRatePage({
|
||||
|
||||
<div className={styles.content}>
|
||||
<div className={styles.main}>
|
||||
<RoomSelection
|
||||
rates={rates}
|
||||
// TODO: Get real value
|
||||
nrOfNights={1}
|
||||
// TODO: Get real value
|
||||
nrOfAdults={1}
|
||||
/>
|
||||
<RoomSelection roomConfigurations={roomConfigurations} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
107
components/Blocks/Table/index.tsx
Normal file
107
components/Blocks/Table/index.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import { useState } from "react"
|
||||
|
||||
import SectionContainer from "@/components/Section/Container"
|
||||
import SectionHeader from "@/components/Section/Header"
|
||||
import ScrollWrapper from "@/components/TempDesignSystem/ScrollWrapper"
|
||||
import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
|
||||
import Table from "@/components/TempDesignSystem/Table"
|
||||
|
||||
import styles from "./table.module.css"
|
||||
|
||||
import type { TableBlockProps } from "@/types/components/blocks/table"
|
||||
|
||||
export default function TableBlock({ data }: TableBlockProps) {
|
||||
const { columns, rows, totalWidth, heading, preamble } = data
|
||||
const initialPageSize = 5
|
||||
const [pageSize, setPageSize] = useState(initialPageSize)
|
||||
const showMoreVisible = rows.length > initialPageSize
|
||||
const showLessVisible = pageSize >= rows.length
|
||||
const columnDefs = columns.map((col) => ({
|
||||
accessorKey: col.id,
|
||||
header: col.header,
|
||||
size: col.width,
|
||||
}))
|
||||
|
||||
const table = useReactTable({
|
||||
columns: columnDefs,
|
||||
data: rows,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
state: {
|
||||
pagination: {
|
||||
pageIndex: 0,
|
||||
pageSize,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
function handleShowMore() {
|
||||
setPageSize(showLessVisible ? initialPageSize : rows.length)
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionContainer>
|
||||
{heading ? (
|
||||
<SectionHeader preamble={data.preamble} title={heading} />
|
||||
) : null}
|
||||
<div className={styles.tableWrapper}>
|
||||
<ScrollWrapper>
|
||||
<Table
|
||||
width={`${totalWidth}%`}
|
||||
variant="content"
|
||||
intent="striped"
|
||||
layout="fixed"
|
||||
borderRadius="none"
|
||||
>
|
||||
<Table.THead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<Table.TR key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<Table.TH
|
||||
key={header.id}
|
||||
width={`${header.column.columnDef.size}%`}
|
||||
>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</Table.TH>
|
||||
))}
|
||||
</Table.TR>
|
||||
))}
|
||||
</Table.THead>
|
||||
<Table.TBody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<Table.TR key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<Table.TD key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</Table.TD>
|
||||
))}
|
||||
</Table.TR>
|
||||
))}
|
||||
</Table.TBody>
|
||||
</Table>
|
||||
</ScrollWrapper>
|
||||
{showMoreVisible ? (
|
||||
<ShowMoreButton
|
||||
loadMoreData={handleShowMore}
|
||||
showLess={showLessVisible}
|
||||
intent="table"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</SectionContainer>
|
||||
)
|
||||
}
|
||||
6
components/Blocks/Table/table.module.css
Normal file
6
components/Blocks/Table/table.module.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.tableWrapper {
|
||||
display: grid;
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import TextCols from "@/components/Blocks/TextCols"
|
||||
import UspGrid from "@/components/Blocks/UspGrid"
|
||||
import 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 <Table data={block.table} />
|
||||
case BlocksEnums.block.TextCols:
|
||||
return <TextCols text_cols={block.text_cols} />
|
||||
case BlocksEnums.block.TextContent:
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x4);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<React.ReactNode>(
|
||||
{ id: "<b>Included</b> (based on availability)" },
|
||||
{ b: (str) => <b>{str}</b> }
|
||||
)
|
||||
|
||||
@@ -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<React.ReactNode>(
|
||||
{ id: "<b>{amount} {currency}</b>/night per adult" },
|
||||
{
|
||||
amount: "150",
|
||||
|
||||
@@ -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) {
|
||||
<footer className={styles.footer}>
|
||||
{user ? null : (
|
||||
<CheckboxCard
|
||||
highlightSubtitle
|
||||
list={list}
|
||||
saving
|
||||
name="join"
|
||||
subtitle={intl.formatMessage(
|
||||
{
|
||||
id: "{difference}{amount} {currency}",
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { EditIcon, ImageIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
|
||||
import styles from "./selectedRoom.module.css"
|
||||
|
||||
export default function SelectedRoom() {
|
||||
const intl = useIntl()
|
||||
return (
|
||||
<article className={styles.container}>
|
||||
<div className={styles.tempImage}>
|
||||
<ImageIcon
|
||||
color="baseButtonTertiaryOnFillNormal"
|
||||
height={60}
|
||||
width={60}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.textContainer}>
|
||||
<Footnote
|
||||
className={styles.label}
|
||||
color="uiTextPlaceholder"
|
||||
textTransform="uppercase"
|
||||
>
|
||||
{intl.formatMessage({ id: "Your room" })}
|
||||
</Footnote>
|
||||
<div className={styles.text}>
|
||||
{/**
|
||||
* [TEMP]
|
||||
* No translation on Subtitles as they will be derived
|
||||
* from Room selection.
|
||||
*/}
|
||||
<Subtitle
|
||||
className={styles.room}
|
||||
color="uiTextHighContrast"
|
||||
type="two"
|
||||
>
|
||||
Cozy cabin
|
||||
</Subtitle>
|
||||
<Subtitle
|
||||
className={styles.invertFontWeight}
|
||||
color="uiTextMediumContrast"
|
||||
type="two"
|
||||
>
|
||||
Free rebooking
|
||||
</Subtitle>
|
||||
<Subtitle
|
||||
className={styles.invertFontWeight}
|
||||
color="uiTextMediumContrast"
|
||||
type="two"
|
||||
>
|
||||
Pay now
|
||||
</Subtitle>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
asChild
|
||||
intent="tertiary"
|
||||
size="small"
|
||||
theme="base"
|
||||
variant="icon"
|
||||
>
|
||||
<Link href="#">
|
||||
<EditIcon color="baseButtonTertiaryOnFillNormal" />
|
||||
{intl.formatMessage({ id: "Modify" })}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -19,35 +19,35 @@ export default function HotelSelectionHeader({
|
||||
|
||||
return (
|
||||
<header className={styles.hotelSelectionHeader}>
|
||||
<div className={styles.titleContainer}>
|
||||
<Title as="h3" level="h1">
|
||||
{hotel.name}
|
||||
</Title>
|
||||
<address className={styles.address}>
|
||||
<Caption color="textMediumContrast">
|
||||
{hotel.address.streetAddress}, {hotel.address.city}
|
||||
</Caption>
|
||||
<div>
|
||||
<Divider variant="vertical" color="subtle" />
|
||||
</div>
|
||||
<Caption color="textMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "Distance to city centre",
|
||||
},
|
||||
{ number: hotel.location.distanceToCentre }
|
||||
)}
|
||||
</Caption>
|
||||
</address>
|
||||
</div>
|
||||
<div className={styles.dividerContainer}>
|
||||
<Divider variant="vertical" color="subtle" />
|
||||
</div>
|
||||
<div className={styles.descriptionContainer}>
|
||||
<Body color="textHighContrast">
|
||||
{hotel.hotelContent.texts.descriptions.short}
|
||||
</Body>
|
||||
<HotelDetailSidePeek />
|
||||
<div className={styles.hotelSelectionHeaderWrapper}>
|
||||
<div className={styles.titleContainer}>
|
||||
<Title as="h3" level="h1">
|
||||
{hotel.name}
|
||||
</Title>
|
||||
<address className={styles.address}>
|
||||
<Caption color="textMediumContrast">
|
||||
{hotel.address.streetAddress}, {hotel.address.city}
|
||||
</Caption>
|
||||
<div>
|
||||
<Divider variant="vertical" color="subtle" />
|
||||
</div>
|
||||
<Caption color="textMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "Distance to city centre" },
|
||||
{ number: hotel.location.distanceToCentre }
|
||||
)}
|
||||
</Caption>
|
||||
</address>
|
||||
</div>
|
||||
<div className={styles.dividerContainer}>
|
||||
<Divider variant="vertical" color="subtle" />
|
||||
</div>
|
||||
<div className={styles.descriptionContainer}>
|
||||
<Body color="textHighContrast">
|
||||
{hotel.hotelContent.texts.descriptions.short}
|
||||
</Body>
|
||||
<HotelDetailSidePeek />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
|
||||
@@ -129,7 +129,7 @@ export default function Payment({ hotel }: PaymentProps) {
|
||||
name="payment-method"
|
||||
id="card"
|
||||
value="card"
|
||||
checked={selectedPaymentMethod === "card"}
|
||||
defaultChecked={selectedPaymentMethod === "card"}
|
||||
/>
|
||||
<label htmlFor="card">card</label>
|
||||
</button>
|
||||
@@ -145,7 +145,7 @@ export default function Payment({ hotel }: PaymentProps) {
|
||||
name="payment-method"
|
||||
id={paymentOption}
|
||||
value={paymentOption}
|
||||
checked={selectedPaymentMethod === paymentOption}
|
||||
defaultChecked={selectedPaymentMethod === paymentOption}
|
||||
/>
|
||||
<label htmlFor={paymentOption}>{paymentOption}</label>
|
||||
</button>
|
||||
|
||||
@@ -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 <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 (
|
||||
<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.header}>
|
||||
<Body>{name}</Body>
|
||||
@@ -29,15 +43,27 @@ export default function FlexibilityOption({
|
||||
<div>
|
||||
<dt>{intl.formatMessage({ id: "Standard price" })}</dt>
|
||||
<dd>
|
||||
{standardPrice} {currency}
|
||||
{publicLocalPrice.pricePerNight} {publicLocalPrice.currency}/
|
||||
{intl.formatMessage({ id: "night" })}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{intl.formatMessage({ id: "Member price" })}</dt>
|
||||
<dd>
|
||||
{memberPrice} {currency}
|
||||
{memberLocalPrice.pricePerNight} {memberLocalPrice.currency}/
|
||||
{intl.formatMessage({ id: "night" })}
|
||||
</dd>
|
||||
</div>
|
||||
{publicRequestedPrice && memberRequestedPrice && (
|
||||
<div>
|
||||
<dt>{intl.formatMessage({ id: "Approx." })}</dt>
|
||||
<dd>
|
||||
{publicRequestedPrice.pricePerNight}/
|
||||
{memberRequestedPrice.pricePerNight}{" "}
|
||||
{publicRequestedPrice.currency}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
@@ -11,20 +11,54 @@ import styles from "./roomCard.module.css"
|
||||
import { RoomCardProps } from "@/types/components/hotelReservation/selectRate/roomCard"
|
||||
|
||||
export default function RoomCard({
|
||||
room,
|
||||
nrOfAdults,
|
||||
nrOfNights,
|
||||
breakfastIncluded,
|
||||
rateDefinitions,
|
||||
roomConfiguration,
|
||||
}: RoomCardProps) {
|
||||
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 (
|
||||
<div className={styles.card}>
|
||||
<div className={styles.cardBody}>
|
||||
<div className={styles.specification}>
|
||||
<Subtitle className={styles.name} type="two">
|
||||
{room.name}
|
||||
{roomConfiguration.roomType}
|
||||
</Subtitle>
|
||||
<Caption>{room.size}</Caption>
|
||||
<Caption>Room size TBI</Caption>
|
||||
<Button intent="text" type="button" size="small" theme="base">
|
||||
{intl.formatMessage({ id: "See room details" })}
|
||||
</Button>
|
||||
@@ -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",
|
||||
})}
|
||||
</Caption>
|
||||
</div>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
<FlexibilityOption
|
||||
name={intl.formatMessage({ id: "Free rebooking" })}
|
||||
value="free-rebooking"
|
||||
paymentTerm={intl.formatMessage({ id: "Pay now" })}
|
||||
standardPrice={room.prices.freeRebooking.standard}
|
||||
memberPrice={room.prices.freeRebooking.member}
|
||||
currency={room.prices.currency}
|
||||
product={changeProduct}
|
||||
/>
|
||||
<FlexibilityOption
|
||||
name={intl.formatMessage({ id: "Free cancellation" })}
|
||||
value="free-cancellation"
|
||||
paymentTerm={intl.formatMessage({ id: "Pay later" })}
|
||||
standardPrice={room.prices.freeCancellation.standard}
|
||||
memberPrice={room.prices.freeCancellation.member}
|
||||
currency={room.prices.currency}
|
||||
product={flexProduct}
|
||||
/>
|
||||
|
||||
<Button
|
||||
@@ -87,7 +110,8 @@ export default function RoomCard({
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
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>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
"use client"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
|
||||
import RoomCard from "./RoomCard"
|
||||
|
||||
@@ -8,12 +11,11 @@ import styles from "./roomSelection.module.css"
|
||||
import { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
||||
|
||||
export default function RoomSelection({
|
||||
rates,
|
||||
nrOfNights,
|
||||
nrOfAdults,
|
||||
roomConfigurations,
|
||||
}: RoomSelectionProps) {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const intl = useIntl()
|
||||
|
||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
@@ -25,31 +27,28 @@ export default function RoomSelection({
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<ul className={styles.roomList}>
|
||||
{rates.map((room) => (
|
||||
<li key={room.id}>
|
||||
<form
|
||||
method="GET"
|
||||
action={`select-bed?${searchParams}`}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="roomClass"
|
||||
value={room.id}
|
||||
id={`room-${room.id}`}
|
||||
/>
|
||||
<form
|
||||
method="GET"
|
||||
action={`select-bed?${searchParams}`}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<ul className={styles.roomList}>
|
||||
{roomConfigurations.roomConfigurations.map((roomConfiguration) => (
|
||||
<li key={roomConfiguration.roomType}>
|
||||
<RoomCard
|
||||
room={room}
|
||||
nrOfAdults={nrOfAdults}
|
||||
nrOfNights={nrOfNights}
|
||||
breakfastIncluded={room.breakfastIncluded}
|
||||
rateDefinitions={roomConfigurations.rateDefinitions}
|
||||
roomConfiguration={roomConfiguration}
|
||||
/>
|
||||
</form>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className={styles.summary}>This is summary</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className={styles.summary}>
|
||||
This is summary
|
||||
<Button type="submit" size="small" theme="primaryDark">
|
||||
{intl.formatMessage({ id: "Choose room" })}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -27,4 +27,6 @@
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: white;
|
||||
padding: var(--Spacing-x3) var(--Spacing-x7) var(--Spacing-x5);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
.iconWrapper {
|
||||
position: relative;
|
||||
top: var(--Spacing-x1);
|
||||
z-index: 10;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.circle {
|
||||
|
||||
@@ -66,3 +66,8 @@
|
||||
.blue * {
|
||||
fill: var(--UI-Input-Controls-Fill-Selected);
|
||||
}
|
||||
|
||||
.baseButtonTertiaryOnFillNormal,
|
||||
.baseButtonTertiaryOnFillNormal * {
|
||||
fill: var(--Base-Button-Tertiary-On-Fill-Normal);
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -49,10 +49,10 @@ export default function FullView({
|
||||
className={styles.fullViewImage}
|
||||
>
|
||||
<Image
|
||||
src={image.url}
|
||||
alt={image.alt}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
fill
|
||||
src={image.url}
|
||||
style={{ objectFit: "cover" }}
|
||||
/>
|
||||
|
||||
<div className={styles.fullViewFooter}>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 <Card {...props} type="checkbox" />
|
||||
@@ -1,6 +1,6 @@
|
||||
import Card from "."
|
||||
import Card from "./_Card"
|
||||
|
||||
import type { RadioProps } from "./card"
|
||||
import type { RadioProps } from "./_Card/card"
|
||||
|
||||
export default function RadioCard(props: RadioProps) {
|
||||
return <Card {...props} type="radio" />
|
||||
@@ -1,14 +1,15 @@
|
||||
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
|
||||
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
|
||||
@@ -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 ? (
|
||||
<Caption
|
||||
className={styles.subtitle}
|
||||
color={saving ? "baseTextAccent" : "uiTextHighContrast"}
|
||||
color={highlightSubtitle ? "baseTextAccent" : "uiTextHighContrast"}
|
||||
textTransform="bold"
|
||||
>
|
||||
{subtitle}
|
||||
36
components/TempDesignSystem/ScrollWrapper/index.tsx
Normal file
36
components/TempDesignSystem/ScrollWrapper/index.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo } from "react"
|
||||
|
||||
import useScrollShadows from "@/hooks/useScrollShadows"
|
||||
|
||||
import styles from "./scrollWrapper.module.css"
|
||||
|
||||
import type { ScrollWrapperProps } from "./scrollWrapper"
|
||||
|
||||
export default function ScrollWrapper({
|
||||
className,
|
||||
children,
|
||||
}: ScrollWrapperProps) {
|
||||
const { containerRef, showLeftShadow, showRightShadow } =
|
||||
useScrollShadows<HTMLDivElement>()
|
||||
|
||||
const classNames = useMemo(() => {
|
||||
const cls = [styles.scrollWrapper, className]
|
||||
if (showLeftShadow) {
|
||||
cls.push(styles.leftShadow)
|
||||
}
|
||||
if (showRightShadow) {
|
||||
cls.push(styles.rightShadow)
|
||||
}
|
||||
return cls.join(" ")
|
||||
}, [showLeftShadow, showRightShadow, className])
|
||||
|
||||
return (
|
||||
<div className={classNames}>
|
||||
<div className={styles.content} ref={containerRef}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
.scrollWrapper {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scrollWrapper::before,
|
||||
.scrollWrapper::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
transition: opacity 0.2s ease;
|
||||
opacity: 0;
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.scrollWrapper.leftShadow::before {
|
||||
left: 0;
|
||||
background: linear-gradient(to right, rgba(0, 0, 0, 0.3), transparent);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.scrollWrapper.rightShadow::after {
|
||||
right: 0;
|
||||
background: linear-gradient(to left, rgba(0, 0, 0, 0.3), transparent);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow-x: auto;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export interface ScrollWrapperProps
|
||||
extends React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>> {}
|
||||
@@ -1,4 +0,0 @@
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -5,18 +5,29 @@ import { useIntl } from "react-intl"
|
||||
import { ChevronDownIcon } from "@/components/Icons"
|
||||
import 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 (
|
||||
<div className={styles.container}>
|
||||
<div className={`${classNames} ${showLess ? styles.showLess : ""}`}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
disabled={disabled}
|
||||
onClick={loadMoreData}
|
||||
variant="icon"
|
||||
@@ -24,8 +35,8 @@ export default function ShowMoreButton({
|
||||
theme="base"
|
||||
intent="text"
|
||||
>
|
||||
<ChevronDownIcon />
|
||||
{formatMessage({ id: "Show more" })}
|
||||
<ChevronDownIcon className={styles.icon} />
|
||||
{intl.formatMessage({ id: showLess ? "Show less" : "Show more" })}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.table {
|
||||
display: grid;
|
||||
justify-content: stretch;
|
||||
border-top: 1px solid var(--Base-Border-Subtle);
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
}
|
||||
|
||||
.table .button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.showLess .icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
11
components/TempDesignSystem/ShowMoreButton/showMoreButton.ts
Normal file
11
components/TempDesignSystem/ShowMoreButton/showMoreButton.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { showMoreButtonVariants } from "./variants"
|
||||
|
||||
import type { VariantProps } from "class-variance-authority"
|
||||
|
||||
export interface ShowMoreButtonProps
|
||||
extends React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>>,
|
||||
VariantProps<typeof showMoreButtonVariants> {
|
||||
disabled?: boolean
|
||||
showLess?: boolean
|
||||
loadMoreData: () => void
|
||||
}
|
||||
11
components/TempDesignSystem/ShowMoreButton/variants.ts
Normal file
11
components/TempDesignSystem/ShowMoreButton/variants.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
import styles from "./showMoreButton.module.css"
|
||||
|
||||
export const showMoreButtonVariants = cva(styles.container, {
|
||||
variants: {
|
||||
intent: {
|
||||
table: styles.table,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,7 +1,13 @@
|
||||
import styles from "./table.module.css"
|
||||
|
||||
function TH({ children }: React.PropsWithChildren) {
|
||||
return <th className={styles.th}>{children}</th>
|
||||
import type { THeadProps } from "./table"
|
||||
|
||||
function TH({ children, width = "auto", ...props }: THeadProps) {
|
||||
return (
|
||||
<th className={styles.th} style={{ width }} {...props}>
|
||||
{children}
|
||||
</th>
|
||||
)
|
||||
}
|
||||
|
||||
export default TH
|
||||
|
||||
@@ -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 <table className={styles.table}>{children}</table>
|
||||
function 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
|
||||
|
||||
@@ -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);
|
||||
|
||||
14
components/TempDesignSystem/Table/table.ts
Normal file
14
components/TempDesignSystem/Table/table.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { tableVariants } from "./variants"
|
||||
|
||||
import type { VariantProps } from "class-variance-authority"
|
||||
|
||||
export interface TableProps
|
||||
extends React.PropsWithChildren<React.HTMLAttributes<HTMLTableElement>>,
|
||||
VariantProps<typeof tableVariants> {
|
||||
width?: string
|
||||
}
|
||||
|
||||
export interface THeadProps
|
||||
extends React.PropsWithChildren<React.HTMLAttributes<HTMLTableCellElement>> {
|
||||
width?: string
|
||||
}
|
||||
27
components/TempDesignSystem/Table/variants.ts
Normal file
27
components/TempDesignSystem/Table/variants.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
import styles from "./table.module.css"
|
||||
|
||||
export const tableVariants = cva(styles.table, {
|
||||
variants: {
|
||||
intent: {
|
||||
light: styles.light,
|
||||
striped: styles.striped,
|
||||
},
|
||||
variant: {
|
||||
content: styles.content,
|
||||
},
|
||||
borderRadius: {
|
||||
none: "",
|
||||
small: styles.smallRadius,
|
||||
medium: styles.mediumRadius,
|
||||
large: styles.largeRadius,
|
||||
},
|
||||
layout: {
|
||||
fixed: styles.fixed,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
borderRadius: "medium",
|
||||
},
|
||||
})
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const config = {
|
||||
burgundy: styles.burgundy,
|
||||
pale: styles.pale,
|
||||
uiTextHighContrast: styles.uiTextHighContrast,
|
||||
uiTextMediumContrast: styles.uiTextMediumContrast,
|
||||
},
|
||||
textAlign: {
|
||||
center: styles.center,
|
||||
|
||||
34
hooks/useScrollShadows.ts
Normal file
34
hooks/useScrollShadows.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
|
||||
export default function useScrollShadows<T extends HTMLElement>() {
|
||||
const containerRef = useRef<T>(null)
|
||||
const [showLeftShadow, setShowLeftShadow] = useState<boolean>(false)
|
||||
const [showRightShadow, setShowRightShadow] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const container = containerRef.current
|
||||
if (!container) return
|
||||
|
||||
setShowLeftShadow(container.scrollLeft > 0)
|
||||
setShowRightShadow(
|
||||
container.scrollLeft < container.scrollWidth - container.clientWidth
|
||||
)
|
||||
}
|
||||
|
||||
const container = containerRef.current
|
||||
if (container) {
|
||||
container.addEventListener("scroll", handleScroll)
|
||||
}
|
||||
|
||||
handleScroll()
|
||||
|
||||
return () => {
|
||||
if (container) {
|
||||
container.removeEventListener("scroll", handleScroll)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { containerRef, showLeftShadow, showRightShadow }
|
||||
}
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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ä",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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`,
|
||||
|
||||
9
lib/graphql/Fragments/Blocks/Table.graphql
Normal file
9
lib/graphql/Fragments/Blocks/Table.graphql
Normal file
@@ -0,0 +1,9 @@
|
||||
fragment Table_ContentPage on ContentPageBlocksTable {
|
||||
__typename
|
||||
table {
|
||||
heading
|
||||
preamble
|
||||
column_widths
|
||||
table
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
#import "../../Fragments/Blocks/Content.graphql"
|
||||
#import "../../Fragments/Blocks/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
|
||||
}
|
||||
|
||||
32
package-lock.json
generated
32
package-lock.json
generated
@@ -23,6 +23,7 @@
|
||||
"@scandic-hotels/design-system": "git+https://x-token-auth:$DESIGN_SYSTEM_ACCESS_TOKEN@bitbucket.org/scandic-swap/design-system.git#v0.1.0-rc.9",
|
||||
"@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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
])
|
||||
|
||||
58
server/routers/contentstack/schemas/blocks/table.ts
Normal file
58
server/routers/contentstack/schemas/blocks/table.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { BlocksEnums } from "@/types/enums/blocks"
|
||||
|
||||
export const tableSchema = z.object({
|
||||
typename: z
|
||||
.literal(BlocksEnums.block.Table)
|
||||
.optional()
|
||||
.default(BlocksEnums.block.Table),
|
||||
table: z
|
||||
.object({
|
||||
heading: z.string().optional(),
|
||||
preamble: z.string().optional().default(""),
|
||||
column_widths: z.array(z.number()),
|
||||
table: z.object({
|
||||
tableState: z.object({
|
||||
columns: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
label: z.string().default(""),
|
||||
accessor: z.string(),
|
||||
dataType: z.string(),
|
||||
})
|
||||
),
|
||||
data: z.array(z.object({}).catchall(z.string())),
|
||||
skipReset: z.boolean(),
|
||||
tableActionEnabled: z.boolean(),
|
||||
headerRowAdded: z.boolean(),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.transform((data) => {
|
||||
const totalWidth = data.column_widths.reduce(
|
||||
(acc, width) => acc + width,
|
||||
0
|
||||
)
|
||||
const columns = data.table.tableState.columns.map((col, idx) => ({
|
||||
id: col.id,
|
||||
header: col.label || "",
|
||||
width: data.column_widths[idx] || 0,
|
||||
}))
|
||||
|
||||
const rows = data.table.tableState.data.map((rowData) =>
|
||||
columns.reduce<Record<string, string>>((transformedRow, column) => {
|
||||
transformedRow[column.id] = rowData[column.id] || ""
|
||||
return transformedRow
|
||||
}, {})
|
||||
)
|
||||
|
||||
return {
|
||||
heading: data.heading,
|
||||
preamble: data.preamble,
|
||||
columns,
|
||||
rows,
|
||||
totalWidth,
|
||||
}
|
||||
}),
|
||||
})
|
||||
@@ -17,6 +17,17 @@ export const getHotelsAvailabilityInputSchema = z.object({
|
||||
attachedProfileId: z.string().optional().default(""),
|
||||
})
|
||||
|
||||
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(),
|
||||
})
|
||||
|
||||
@@ -572,6 +572,86 @@ export type HotelsAvailability = z.infer<typeof hotelsAvailabilitySchema>
|
||||
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<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({
|
||||
standard: z.number(),
|
||||
member: z.number(),
|
||||
|
||||
@@ -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<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({
|
||||
get: publicProcedure
|
||||
|
||||
@@ -111,6 +111,7 @@ export const getStaysSchema = z.object({
|
||||
limit: z.number(),
|
||||
totalCount: z.number(),
|
||||
})
|
||||
.optional()
|
||||
.nullable(),
|
||||
})
|
||||
|
||||
|
||||
5
types/components/blocks/table.ts
Normal file
5
types/components/blocks/table.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { TableData } from "@/types/trpc/routers/contentstack/blocks"
|
||||
|
||||
export interface TableBlockProps {
|
||||
data: TableData
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Product, RateDefinition } from "@/server/routers/hotels/output"
|
||||
|
||||
export type FlexibilityOptionProps = {
|
||||
product: Product | undefined
|
||||
name: string
|
||||
value: string
|
||||
paymentTerm: string
|
||||
standardPrice: number
|
||||
memberPrice: number
|
||||
currency: string
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Rate } from "@/server/routers/hotels/output"
|
||||
import {
|
||||
RateDefinition,
|
||||
RoomConfiguration,
|
||||
} from "@/server/routers/hotels/output"
|
||||
|
||||
export type RoomCardProps = {
|
||||
room: Rate
|
||||
nrOfNights: number
|
||||
nrOfAdults: number
|
||||
breakfastIncluded: boolean
|
||||
roomConfiguration: RoomConfiguration
|
||||
rateDefinitions: RateDefinition[]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ export namespace BlocksEnums {
|
||||
Content = "Content",
|
||||
DynamicContent = "DynamicContent",
|
||||
Shortcuts = "Shortcuts",
|
||||
Table = "Table",
|
||||
TextCols = "TextCols",
|
||||
TextContent = "TextContent",
|
||||
UspGrid = "UspGrid",
|
||||
|
||||
@@ -7,6 +7,7 @@ export namespace ContentPageEnum {
|
||||
Shortcuts = "ContentPageBlocksShortcuts",
|
||||
TextCols = "ContentPageBlocksTextCols",
|
||||
UspGrid = "ContentPageBlocksUspGrid",
|
||||
Table = "ContentPageBlocksTable",
|
||||
}
|
||||
|
||||
export const enum sidebar {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { cardsGridSchema } from "@/server/routers/contentstack/schemas/blocks/ca
|
||||
import { contentSchema } from "@/server/routers/contentstack/schemas/blocks/content"
|
||||
import { 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<typeof contentSchema> {}
|
||||
export interface DynamicContent extends z.output<typeof dynamicContentSchema> {}
|
||||
export interface Shortcuts extends z.output<typeof shortcutsSchema> {}
|
||||
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 UspGrid extends z.output<typeof uspGridSchema> {}
|
||||
|
||||
Reference in New Issue
Block a user