Merged in feat/sw-1493-revised-comparison-block (pull request #1236)

feat(SW-1493): Revised SAS comparison block

* Base of new TierDetails for SAS tier comparison

* Add backgrounds and content to TierDetails

* Implement new cms schema for SasTierComparison

* Override gap in jsontohtml styling to 0

* Add animations to comparison details

* Redesign again

* Update content model to new design

* Add border to bottom item in tier match list

* Wrap interpolate-size in @supports to be safe

* Merge branch 'master' into feat/sw-1493-revised-comparison-block


Approved-by: Joakim Jäderberg
This commit is contained in:
Anton Gunnarsson
2025-02-03 08:42:16 +00:00
parent f9d1736195
commit fc866c0e4d
28 changed files with 450 additions and 351 deletions

View File

@@ -135,7 +135,7 @@ export default function EnterDetailsTracking(props: Props) {
roomPrice: roomPrice.perStay.local.price,
discount: roomRate.memberRate
? roomRate.publicRate.localPrice.pricePerStay -
roomRate.memberRate.localPrice.pricePerStay
roomRate.memberRate.localPrice.pricePerStay
: 0,
analyticsrateCode: getAnalyticsRateCode(roomRate.publicRate.rateCode),
ancillaries: breakfastAncillary ? [breakfastAncillary] : [],

View File

@@ -129,6 +129,10 @@
--lightbox-z-index: 150;
--default-modal-overlay-z-index: 100;
--default-modal-z-index: 101;
@supports (interpolate-size: allow-keywords) {
interpolate-size: allow-keywords;
}
}
* {

View File

@@ -93,12 +93,7 @@ export default function Blocks({ blocks }: BlocksProps) {
case BlocksEnums.block.UspGrid:
return <UspGrid usp_grid={block.usp_grid} />
case BlocksEnums.block.SasTierComparison:
return (
<SasTierComparison
content={block.sas_tier_comparison}
firstItem={firstItem}
/>
)
return <SasTierComparison content={block.sas_tier_comparison} />
case BlocksEnums.block.FullWidthCampaign:
return <FullWidthCampaign content={block.full_width_campaign} />
default:

View File

@@ -63,16 +63,16 @@ export function filterDuplicateRoomTypesByLowestPrice(
if (
!previousLowest ||
currentRequestedPrice <
Math.min(
Number(
previousLowest.products[0].productType.public.requestedPrice
?.pricePerNight
) ?? Infinity,
Number(
previousLowest.products[0].productType.member?.requestedPrice
?.pricePerNight
) ?? Infinity
) ||
Math.min(
Number(
previousLowest.products[0].productType.public.requestedPrice
?.pricePerNight
) ?? Infinity,
Number(
previousLowest.products[0].productType.member?.requestedPrice
?.pricePerNight
) ?? Infinity
) ||
(currentRequestedPrice ===
Math.min(
Number(
@@ -85,16 +85,16 @@ export function filterDuplicateRoomTypesByLowestPrice(
) ?? Infinity
) &&
currentLocalPrice <
Math.min(
Number(
previousLowest.products[0].productType.public.localPrice
?.pricePerNight
) ?? Infinity,
Number(
previousLowest.products[0].productType.member?.localPrice
?.pricePerNight
) ?? Infinity
))
Math.min(
Number(
previousLowest.products[0].productType.public.localPrice
?.pricePerNight
) ?? Infinity,
Number(
previousLowest.products[0].productType.member?.localPrice
?.pricePerNight
) ?? Infinity
))
) {
roomMap.set(roomType, room)
}

View File

@@ -1,3 +1,5 @@
import { cx } from "class-variance-authority"
import { nodesToHtml } from "./utils"
import styles from "./jsontohtml.module.css"
@@ -8,12 +10,13 @@ export default function JsonToHtml({
embeds,
nodes,
renderOptions = {},
className,
}: JsonToHtmlProps) {
if (!Array.isArray(nodes) || !nodes.length) {
return null
}
return (
<section className={styles.container}>
<section className={cx(styles.container, className)}>
{nodesToHtml(nodes, embeds, renderOptions).filter(Boolean)}
</section>
)

View File

@@ -1,163 +1,164 @@
"use client"
import { type Key, useState } from "react"
import Image from "next/image"
import JsonToHtml from "@/components/JsonToHtml"
import Link from "@/components/TempDesignSystem/Link"
import Title from "@/components/TempDesignSystem/Text/Title"
import { ArrowRightIcon, CompareArrowsIcon } from "../Icons"
import { ArrowRightIcon, ChevronDownIcon, CompareArrowsIcon } from "../Icons"
import SectionContainer from "../Section/Container"
import SectionHeader from "../Section/Header"
import Button from "../TempDesignSystem/Button"
import Select from "../TempDesignSystem/Select"
import Body from "../TempDesignSystem/Text/Body"
import Caption from "../TempDesignSystem/Text/Caption"
import Subtitle from "../TempDesignSystem/Text/Subtitle"
import Title from "../TempDesignSystem/Text/Title"
import styles from "./sas-tier-comparison.module.css"
import type { ReactNode } from "react"
import type { SasTierComparison } from "@/types/trpc/routers/contentstack/blocks"
type SasTierComparisonContent = SasTierComparison["sas_tier_comparison"]
type TierComparisonProps = {
content: SasTierComparisonContent
firstItem: boolean
}
const scandicSasTierMappings: [string[], string[]][] = [
[["L1", "L2", "L3"], ["Basic"]],
[["L4"], ["Silver"]],
[["L5"], ["Gold"]],
[
["L6", "L7"],
["Diamond", "Pandion"],
],
]
export function SasTierComparison({ content, firstItem }: TierComparisonProps) {
export function SasTierComparison({ content }: TierComparisonProps) {
const comparisonContent = content.sasTierComparison
const scandic = comparisonContent?.scandic_friends
const sas = comparisonContent?.sas_eb
const [activeScandicTierCode, setActiveScandicTierCode] = useState(
scandic?.tiers.at(0)?.tier_code ?? ""
)
const [activeSasTierCode, setActiveSasTierCode] = useState(
sas?.tiers.at(0)?.tier_code ?? ""
)
if (!scandic || !sas) return null
const onChangeScandic = (k: Key) => {
const key = k.toString()
setActiveScandicTierCode(k.toString())
const mapping = scandicSasTierMappings.find(([tierMapping]) =>
tierMapping.includes(key)
)
if (!mapping) return
const sasTierCode = mapping[1][0]
setActiveSasTierCode(sasTierCode)
}
const onChangeSas = (k: Key) => {
const key = k.toString()
setActiveSasTierCode(key)
const mapping = scandicSasTierMappings.find(([_, tierMapping]) =>
tierMapping.includes(key)
)
if (!mapping) return
const scandicTierCode = mapping[0][0]
setActiveScandicTierCode(scandicTierCode)
}
if (!comparisonContent) return null
return (
<SectionContainer className={styles.comparisonSection}>
<div className={styles.header}>
<SectionHeader title={comparisonContent.title} topTitle={firstItem} />
<Title level="h2">{comparisonContent.title}</Title>
{comparisonContent.preamble && (
<p className={styles.preamble}>{comparisonContent.preamble}</p>
)}
</div>
<div className={styles.comparisonCard}>
<div className={styles.comparisonContainer}>
<div className={styles.comparisonBrand}>
<Title level="h3" textTransform="regular">
{scandic.title}
</Title>
<Select
name="friends-level"
label={scandic.label}
aria-label={scandic.label}
items={scandic.tiers.map((tier) => ({
label: tier.tier_label,
value: tier.tier_code,
}))}
value={activeScandicTierCode}
onSelect={onChangeScandic}
/>
{scandic.read_more_link && (
<ReadMoreLink
href={scandic.read_more_link.href}
title={scandic.read_more_link.title}
/>
)}
</div>
<div className={styles.comparisonIcon}>
<CompareArrowsIcon width={24} height={24} />
</div>
<div className={styles.comparisonBrand}>
<Title level="h3" textTransform="regular">
{sas.title}
</Title>
<Select
name="eb-level"
label={sas.label}
aria-label={sas.label}
items={sas.tiers.map((tier) => ({
label: tier.tier_label,
value: tier.tier_code,
}))}
value={activeSasTierCode}
onSelect={onChangeSas}
/>
{sas.read_more_link && (
<ReadMoreLink
href={sas.read_more_link.href}
title={sas.read_more_link.title}
/>
)}
</div>
<div>
<div className={styles.columnHeaders}>
<Image
alt="Scandic logo"
height={46}
src="/_static/img/scandic-logotype.svg"
priority
width={215}
/>
<Image
alt="SAS logo"
height={46}
src="/_static/img/sas/sas-logotype.svg"
priority
width={215}
/>
<ColumnTitle>{comparisonContent.scandic_column_title}</ColumnTitle>
<ColumnTitle>{comparisonContent.sas_column_title}</ColumnTitle>
</div>
<div className={styles.tierMatchList}>
{comparisonContent.tier_matches.map((tierMatch, i) => (
<TierDetails key={i} tierMatch={tierMatch}>
<JsonToHtml
nodes={tierMatch.content.json?.children}
embeds={[]}
className={styles.htmlContent}
/>
</TierDetails>
))}
</div>
{comparisonContent.cta && (
<div className={styles.ctaContainer}>
<Button
asChild
intent="primary"
size="small"
theme="base"
className={styles.ctaLink}
>
<Link color="none" href={comparisonContent.cta.href}>
{comparisonContent.cta.title}
</Link>
</Button>
</div>
)}
</div>
{comparisonContent.cta?.href && (
<Button theme="primaryLight" asChild className={styles.ctaButton}>
<Link href={comparisonContent.cta.href} color="white">
{comparisonContent.cta.title}
</Link>
</Button>
)}
</SectionContainer>
)
}
function ReadMoreLink({ href, title }: { href: string; title: string }) {
type TierMatch = NonNullable<
SasTierComparisonContent["sasTierComparison"]
>["tier_matches"][number]
function TierDetails({
children,
tierMatch,
}: {
children: React.ReactNode
tierMatch: TierMatch
}) {
return (
<Link
href={href}
variant="underscored"
className={styles.link}
color="peach80"
>
{title}
<ArrowRightIcon color="peach80" />
<details className={styles.tierDetails} name="sas-scandic-tier-match">
<summary className={styles.tierSummary}>
<div className={styles.tierTitles}>
<Body className={styles.tierTitle}>
{tierMatch.scandic_friends_tier_name}
</Body>
<div className={styles.comparisonIcon}>
<CompareArrowsIcon width={16} height={16} />
</div>
<div
style={{
display: "flex",
alignItems: "center",
position: "relative",
}}
>
<Body className={styles.tierTitle}>
{tierMatch.sas_eb_tier_name}
</Body>
<div className={styles.iconWrapper}>
<ChevronDownIcon
className={styles.chevron}
color="burgundy"
width={20}
height={20}
/>
</div>
</div>
</div>
</summary>
<div className={styles.tierContent}>
<div className={styles.tierInfo}>
<div className={styles.tierTitle}>
<Subtitle color="burgundy">{tierMatch.title}</Subtitle>
</div>
<div>{children}</div>
</div>
{tierMatch.link?.href && (
<ReadMoreLink href={tierMatch.link.href}>
{tierMatch.link.title}
</ReadMoreLink>
)}
</div>
</details>
)
}
function ReadMoreLink({
href,
children,
}: {
href: string
children: ReactNode
}) {
return (
<Link href={href} className={styles.link} weight="bold" color="burgundy">
{children}
<ArrowRightIcon color="burgundy" />
</Link>
)
}
function ColumnTitle({ children }: { children?: ReactNode }) {
return (
<div className={styles.columnTitle}>
<Caption type="bold" asChild>
<span>{children}</span>
</Caption>
</div>
)
}

View File

@@ -1,12 +1,18 @@
.comparisonSection {
width: 100%;
gap: var(--Spacing-x4);
gap: var(--Spacing-x6);
background-color: var(--Base-Surface-Primary-light-Normal);
border-radius: var(--Corner-radius-Large);
padding: var(--Spacing-x6) var(--Spacing-x2);
}
.header {
display: flex;
flex-direction: column;
gap: var(--Spacing-x1);
margin: 0 auto;
align-items: center;
text-align: center;
}
.preamble {
@@ -14,59 +20,164 @@
white-space: pre-wrap;
}
.comparisonCard {
background-color: var(--Base-Surface-Subtle-Normal);
padding: var(--Spacing-x4) var(--Spacing-x3);
border-radius: var(--Corner-radius-Large);
}
.comparisonContainer {
display: flex;
gap: var(--Spacing-x2);
}
.comparisonBrand {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
width: 100%;
}
.comparisonIcon {
background-color: var(--Base-Surface-Secondary-light-Normal);
border-radius: 50%;
width: 48px;
height: 48px;
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
margin-top: var(--Spacing-x7);
}
@media screen and (max-width: 767px) {
.comparisonIcon {
display: none;
}
.comparisonContainer {
flex-direction: column;
gap: var(--Spacing-x4);
}
}
.link {
display: flex;
align-items: center;
gap: var(--Spacing-x-half);
}
.ctaContainer {
border-top: 1px solid var(--Base-Border-Subtle);
.tierMatchList {
display: flex;
justify-content: center;
margin-top: var(--Spacing-x4);
flex-direction: column;
}
.ctaLink {
margin-top: var(--Spacing-x4);
.tierDetails {
overflow: hidden;
background-color: var(--Base-Surface-Primary-light-Normal);
transition: background-color 200ms;
transition-delay: 50ms;
border-bottom: 1px solid var(--Base-Border-Subtle);
&:hover,
&[open] {
background-color: var(--Base-Surface-Primary-light-Hover);
.comparisonIcon {
background-color: var(--Base-Surface-Primary-light-Normal);
}
}
&[open] {
.chevron {
transform: rotate(180deg);
}
&::details-content {
block-size: auto;
}
}
&::details-content {
block-size: 0;
transition:
block-size 0.4s,
content-visibility 0.4s;
transition-behavior: allow-discrete;
}
}
.iconWrapper {
position: absolute;
right: var(--Spacing-x1);
}
.chevron {
transition: transform 200ms;
flex-shrink: 0;
}
.tierSummary {
list-style: none;
&::marker,
&::-webkit-details-marker {
display: none;
}
}
.columnHeaders {
display: grid;
grid-template-columns: 1fr 1fr;
align-items: center;
row-gap: var(--Spacing-x2);
margin-bottom: var(--Spacing-x2);
place-items: center;
}
.tierTitles {
cursor: pointer;
padding: var(--Spacing-x-half);
position: relative;
width: 100%;
height: 64px;
align-items: center;
display: grid;
grid-template-columns: 1fr auto 1fr;
}
.tierTitle {
flex-grow: 1;
text-align: center;
font-weight: 500;
z-index: 1;
}
.comparisonIcon {
background-color: var(--Base-Surface-Secondary-light-Normal);
border-radius: 50%;
width: 32px;
height: 32px;
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
z-index: 1;
transition: background-color 0.3s;
transition-delay: 50ms;
}
.tierContent {
padding: var(--Spacing-x3);
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
}
.tierInfo {
display: grid;
gap: var(--Spacing-x2);
}
.htmlContent {
gap: 0;
& ul {
padding: 0;
}
}
.columnTitle {
width: 100%;
position: relative;
text-align: center;
& > span {
position: relative;
padding: 0 var(--Spacing-x2);
background-color: white;
}
&::before {
position: absolute;
bottom: calc(50% - 1px);
content: "";
display: block;
height: 1px;
width: 100%;
background-color: var(--Base-Border-Normal);
}
}
@media (min-width: 768px) {
.tierInfo {
grid-template-columns: 1fr 1fr;
gap: 0;
}
.ctaButton {
width: fit-content;
margin: 0 auto;
}
.columnHeaders {
column-gap: var(--Spacing-x3);
}
}

View File

@@ -9,33 +9,23 @@ fragment SasTierComparison_ContentPage on ContentPageBlocksSasTierComparison {
... on SasTierComparison {
title
preamble
scandic_friends {
scandic_column_title
sas_column_title
tier_matches {
title
label
tiers {
tier_code
tier_label
scandic_friends_tier_name
sas_eb_tier_name
content {
json
}
read_more_link {
href
title
}
}
sas_eb {
title
label
tiers {
tier_code
tier_label
}
read_more_link {
link {
href
title
}
}
cta {
href
title
href
}
}
}

View File

@@ -0,0 +1,3 @@
<svg width="144" height="51" viewBox="0 0 144 51" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M94.333 0.544556H79.8253C79.0833 0.544556 78.7102 0.908943 78.7102 0.908943L38.7269 49.5465C38.5404 49.9109 38.5404 50.0911 38.9134 50.0911H44.8637C45.4212 50.0911 45.6077 49.9089 45.6077 49.7267L54.1631 39.0177C54.1631 39.0177 54.3476 38.8376 54.5341 38.8376H68.8533C68.8533 38.8376 69.0398 38.8376 69.0398 39.0198C69.0398 39.202 66.9942 49.5465 66.9942 49.5465C66.9942 49.7287 66.9942 50.0911 67.5517 50.0911H84.847C85.0335 50.0911 85.22 50.0911 85.22 49.7267L94.8905 1.08709C94.704 1.08709 94.8905 0.540507 94.3309 0.540507L94.333 0.544556ZM69.7818 33.9386H58.6233H58.4388V33.7564L73.3155 15.0654L73.502 14.8832C73.6865 14.8832 73.502 15.0654 73.502 15.0654L69.7818 33.9406C69.9683 33.7584 69.9683 33.7584 69.7818 33.9406V33.9386ZM0.0451224 43.9208C0.0451224 43.9208 1.34869 39.5663 1.53319 39.7465C1.53319 39.0218 1.90621 38.8396 2.27723 39.202C3.95181 40.8356 9.15804 45.7366 16.4119 45.7366C24.4077 45.7366 23.8502 39.3842 23.8502 38.4772C23.8502 35.9366 20.5011 23.4119 19.5725 20.1465C16.9694 9.61981 23.4792 0 38.9134 0C47.0958 0 51.187 2.17822 52.675 2.90295C53.6056 3.44953 53.4171 4.17425 53.4171 4.17425C53.4171 4.17425 52.4885 7.98615 52.4885 8.34851C52.302 8.7129 51.931 8.7129 51.56 8.34851C49.1414 6.89704 46.3517 5.26337 42.0741 5.26337C37.0543 5.26337 34.4492 8.89307 35.7528 14.1564C36.3103 15.9723 39.4709 28.4951 40.0285 31.0356C42.2606 40.1109 35.9373 51 18.8305 51C9.34455 51 3.39428 47.5525 1.34869 45.9188C0.231632 45.0099 -0.139382 44.4653 0.0451224 43.9208ZM90.9838 44.103C90.9838 44.103 91.9143 39.3862 91.9143 39.7465C92.1008 39.202 92.6564 39.0218 93.0294 39.3842C94.8905 41.0178 99.7237 45.7366 106.978 45.7366C114.973 45.7366 114.418 39.3842 114.418 38.4772C114.418 35.9366 111.069 23.4119 110.138 20.1465C107.535 9.61981 114.047 0 129.479 0C137.661 0 141.753 2.17822 143.241 2.90295C144.171 3.44953 143.985 4.17425 143.985 4.17425C143.985 4.17425 143.054 7.98615 143.054 8.34851C142.87 8.7129 142.497 8.7129 142.126 8.34851C139.707 6.89704 136.917 5.26337 132.64 5.26337C127.62 5.26337 125.017 8.89307 126.318 14.1564C126.876 15.9723 130.037 28.4951 130.596 31.0356C132.826 40.1109 126.505 51 109.394 51C99.9102 51 94.333 47.7327 92.2874 46.101C91.1703 45.1921 90.7973 44.6475 90.9838 44.103Z" fill="#000099"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -22,34 +22,19 @@ export const sasTierComparisonSchema = z.object({
node: z.object({
title: z.string(),
preamble: z.string().optional(),
scandic_friends: z.object({
title: z.string(),
label: z.string(),
tiers: z.array(
z.object({
tier_code: z.nativeEnum(MembershipLevelEnum),
tier_label: z.string(),
})
),
read_more_link: link.optional(),
}),
sas_eb: z.object({
title: z.string(),
label: z.string(),
tiers: z.array(
z.object({
tier_code: z.enum([
"Basic",
"Silver",
"Gold",
"Diamond",
"Pandion",
]),
tier_label: z.string(),
})
),
read_more_link: link.optional(),
}),
scandic_column_title: z.string(),
sas_column_title: z.string(),
tier_matches: z.array(
z.object({
scandic_friends_tier_name: z.string(),
sas_eb_tier_name: z.string(),
title: z.string(),
content: z.object({
json: z.any(), // json
}),
link: link.optional(),
})
),
cta: link.optional(),
}),
})

View File

@@ -73,7 +73,7 @@ export const getHotelsInput = z.object({
.nullable(),
hotelsToInclude: z.array(z.string()),
})
export interface GetHotelsInput extends z.infer<typeof getHotelsInput> { }
export interface GetHotelsInput extends z.infer<typeof getHotelsInput> {}
export const nearbyHotelIdsInput = z.object({
hotelId: z.string(),

View File

@@ -34,8 +34,12 @@ export const metrics = {
},
hotelsByHotelIdAvailability: {
counter: meter.createCounter("trpc.hotel.availability.hotels-by-hotel-id"),
fail: meter.createCounter("trpc.hotel.availability.hotels-by-hotel-id-fail"),
success: meter.createCounter("trpc.hotel.availability.hotels-by-hotel-id-success"),
fail: meter.createCounter(
"trpc.hotel.availability.hotels-by-hotel-id-fail"
),
success: meter.createCounter(
"trpc.hotel.availability.hotels-by-hotel-id-success"
),
},
meetingRooms: {
counter: meter.createCounter("trpc.hotels.meetingRooms"),

View File

@@ -34,32 +34,31 @@ export const extraPageSchema = z.object({
mainBody: z.string().optional(),
})
export const additionalDataSchema = z
.object({
attributes: z.object({
name: z.string(),
id: z.string(),
displayWebPage: z.object({
healthGym: z.boolean(),
meetingRoom: z.boolean(),
parking: z.boolean(),
specialNeeds: z.boolean(),
}),
specialNeedGroups: z.array(specialNeedGroupSchema),
gallery: gallerySchema.optional(),
conferencesAndMeetings: facilitySchema.optional(),
healthAndWellness: facilitySchema.optional(),
restaurantImages: facilitySchema.optional(),
restaurantsOverviewPage: restaurantsOverviewPageSchema,
meetingRooms: extraPageSchema,
healthAndFitness: extraPageSchema,
hotelParking: extraPageSchema,
hotelSpecialNeeds: extraPageSchema,
accessibilityElevatorPitchText: z.string().optional(),
hotelRoomElevatorPitchText: z.string().optional(),
export const additionalDataSchema = z.object({
attributes: z.object({
name: z.string(),
id: z.string(),
displayWebPage: z.object({
healthGym: z.boolean(),
meetingRoom: z.boolean(),
parking: z.boolean(),
specialNeeds: z.boolean(),
}),
type: z.literal("additionalData"),
})
specialNeedGroups: z.array(specialNeedGroupSchema),
gallery: gallerySchema.optional(),
conferencesAndMeetings: facilitySchema.optional(),
healthAndWellness: facilitySchema.optional(),
restaurantImages: facilitySchema.optional(),
restaurantsOverviewPage: restaurantsOverviewPageSchema,
meetingRooms: extraPageSchema,
healthAndFitness: extraPageSchema,
hotelParking: extraPageSchema,
hotelSpecialNeeds: extraPageSchema,
accessibilityElevatorPitchText: z.string().optional(),
hotelRoomElevatorPitchText: z.string().optional(),
}),
type: z.literal("additionalData"),
})
export function transformAdditionalData(
data: z.output<typeof additionalDataSchema>
@@ -67,6 +66,6 @@ export function transformAdditionalData(
return {
...data.attributes,
id: data.attributes.id,
type: data.type
type: data.type,
}
}
}

View File

@@ -7,7 +7,11 @@ import {
roomCategoriesSchema,
transformRoomCategories,
} from "@/server/routers/hotels/schemas/hotel/include/roomCategories"
import { additionalDataSchema, transformAdditionalData } from "../../additionalData"
import {
additionalDataSchema,
transformAdditionalData,
} from "../../additionalData"
export const includeSchema = z
.discriminatedUnion("type", [

View File

@@ -86,12 +86,12 @@ export function transformRoomCategories(
totalOccupancy:
data.attributes.occupancy.min === data.attributes.occupancy.max
? {
max: data.attributes.occupancy.max,
range: `${data.attributes.occupancy.max}`,
}
max: data.attributes.occupancy.max,
range: `${data.attributes.occupancy.max}`,
}
: {
max: data.attributes.occupancy.max,
range: `${data.attributes.occupancy.min}-${data.attributes.occupancy.max}`,
},
max: data.attributes.occupancy.max,
range: `${data.attributes.occupancy.min}-${data.attributes.occupancy.max}`,
},
}
}

View File

@@ -2,49 +2,48 @@ import { z } from "zod"
import { imageSchema } from "./image"
export const meetingRoomsSchema = z
.object({
data: z.array(
z.object({
attributes: z.object({
name: z.string(),
email: z.string().optional(),
phoneNumber: z.string(),
size: z.number(),
doorWidth: z.number(),
doorHeight: z.number(),
length: z.number(),
width: z.number(),
height: z.number(),
floorNumber: z.number(),
content: z.object({
images: z.array(imageSchema),
texts: z.object({
facilityInformation: z.string().optional(),
surroundingInformation: z.string().optional(),
descriptions: z.object({
export const meetingRoomsSchema = z.object({
data: z.array(
z.object({
attributes: z.object({
name: z.string(),
email: z.string().optional(),
phoneNumber: z.string(),
size: z.number(),
doorWidth: z.number(),
doorHeight: z.number(),
length: z.number(),
width: z.number(),
height: z.number(),
floorNumber: z.number(),
content: z.object({
images: z.array(imageSchema),
texts: z.object({
facilityInformation: z.string().optional(),
surroundingInformation: z.string().optional(),
descriptions: z.object({
short: z.string().optional(),
medium: z.string().optional(),
}),
meetingDescription: z
.object({
short: z.string().optional(),
medium: z.string().optional(),
}),
meetingDescription: z
.object({
short: z.string().optional(),
medium: z.string().optional(),
})
.optional(),
}),
})
.optional(),
}),
seatings: z.array(
z.object({
type: z.string(),
capacity: z.number(),
})
),
lighting: z.string(),
sortOrder: z.number().optional(),
}),
id: z.string(),
type: z.string(),
})
),
})
seatings: z.array(
z.object({
type: z.string(),
capacity: z.number(),
})
),
lighting: z.string(),
sortOrder: z.number().optional(),
}),
id: z.string(),
type: z.string(),
})
),
})

View File

@@ -3,6 +3,7 @@ import { unstable_cache } from "next/cache"
import * as api from "@/lib/api"
import { metrics } from "./metrics"
import {
citiesByCountrySchema,
citiesSchema,
@@ -11,6 +12,7 @@ import {
locationsSchema,
} from "./output"
import type { Country } from "@/types/enums/country"
import { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest"
import type { RequestOptionsWithOutBody } from "@/types/fetch"
import type {
@@ -20,8 +22,6 @@ import type {
} from "@/types/trpc/routers/hotel/locations"
import type { Lang } from "@/constants/languages"
import type { Endpoint } from "@/lib/api/endpoints"
import { Country } from "@/types/enums/country"
import { metrics } from "./metrics"
export function getPoiGroupByCategoryName(category: string | undefined) {
if (!category) return PointOfInterestGroupEnum.LOCATION

View File

@@ -1,6 +1,6 @@
import { meetingRoomsSchema } from "@/server/routers/hotels/schemas/meetingRoom"
import type { z } from "zod"
import type { meetingRoomsSchema } from "@/server/routers/hotels/schemas/meetingRoom"
export type MeetingRoomData = z.output<typeof meetingRoomsSchema>
export type MeetingRooms = MeetingRoomData["data"]

View File

@@ -1,5 +1,5 @@
import type { ParkingAmenityProps } from "./parking"
import type { Hotel, Restaurant, RestaurantOpeningHours } from "@/types/hotel"
import type { ParkingAmenityProps } from "./parking"
export type AmenitiesSidePeekProps = {
amenitiesList: Hotel["detailedFacilities"]

View File

@@ -20,10 +20,10 @@ export interface ParkingListProps
| "distanceToHotel"
| "numberOfChargingSpaces"
| "numberOfParkingSpots"
> { }
> {}
export interface ParkingPricesProps
extends Pick<Parking["pricing"], "freeParking">,
Pick<NonNullable<Parking["pricing"]["localCurrency"]>, "currency"> {
Pick<NonNullable<Parking["pricing"]["localCurrency"]>, "currency"> {
pricing: NonNullable<Parking["pricing"]["localCurrency"]>["ordinary"]
}

View File

@@ -4,4 +4,4 @@ export interface BookingConfirmationProps {
confirmationNumber: string
}
export interface ConfirmationProps extends BookingConfirmation { }
export interface ConfirmationProps extends BookingConfirmation {}

View File

@@ -5,13 +5,13 @@ import type { breakfastPackagesSchema } from "@/server/routers/hotels/output"
import type { breakfastPackageSchema } from "@/server/routers/hotels/schemas/packages"
export interface BreakfastFormSchema
extends z.output<typeof breakfastFormSchema> { }
extends z.output<typeof breakfastFormSchema> {}
export interface BreakfastPackages
extends z.output<typeof breakfastPackagesSchema> { }
extends z.output<typeof breakfastPackagesSchema> {}
export interface BreakfastPackage
extends z.output<typeof breakfastPackageSchema> { }
extends z.output<typeof breakfastPackageSchema> {}
export interface BreakfastProps {
packages: BreakfastPackages

View File

@@ -1,9 +1,9 @@
import type { CheckInData, Hotel, Parking } from "@/types/hotel"
import type { Lang } from "@/constants/languages"
import type {
AlternativeHotelsSearchParams,
SelectHotelSearchParams,
} from "./selectHotelSearchParams"
import type { CheckInData, Hotel, Parking } from "@/types/hotel"
import type { Lang } from "@/constants/languages"
export enum AvailabilityEnum {
Available = "Available",

View File

@@ -1,4 +1,4 @@
import { HotelData } from "@/types/hotel"
import type { HotelData } from "@/types/hotel"
export enum SidePeekEnum {
hotelDetails = "hotel-detail-side-peek",

View File

@@ -5,11 +5,11 @@ import type {
Price,
RoomPrice,
} from "@/types/stores/enter-details"
import type { RoomAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
import type { BedTypeSchema } from "./enterDetails/bedType"
import type { BreakfastPackage } from "./enterDetails/breakfast"
import type { DetailsSchema } from "./enterDetails/details"
import type { Child, SelectRateSearchParams } from "./selectRate/selectRate"
import type { RoomAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
export type RoomsData = Pick<DetailsState, "roomPrice"> &
Pick<RoomAvailability, "cancellationText" | "rateDetails"> &
@@ -21,7 +21,7 @@ export type RoomsData = Pick<DetailsState, "roomPrice"> &
export interface SummaryProps
extends Pick<RoomAvailability, "cancellationText" | "rateDetails">,
Pick<RoomAvailability["selectedRoom"], "roomType"> {
Pick<RoomAvailability["selectedRoom"], "roomType"> {
isMember: boolean
breakfastIncluded: boolean
}

View File

@@ -7,6 +7,7 @@ export type JsonToHtmlProps = {
embeds: Node<Embeds>[]
nodes: RTENode[]
renderOptions?: RenderOptions
className?: string
}
export type EmbedByUid = Record<string, Node<Embeds>>

View File

@@ -4,14 +4,14 @@ import type { Hotel, Room } from "@/types/hotel"
import type { bookingConfirmationSchema } from "@/server/routers/booking/output"
export interface BookingConfirmationSchema
extends z.output<typeof bookingConfirmationSchema> { }
extends z.output<typeof bookingConfirmationSchema> {}
export interface BookingConfirmation {
booking: BookingConfirmationSchema
hotel: Hotel
room:
| (Room & {
bedType: Room["roomTypes"][number]
})
| null
| (Room & {
bedType: Room["roomTypes"][number]
})
| null
}

View File

@@ -6,7 +6,7 @@ import type {
locationsSchema,
} from "@/server/routers/hotels/output"
export interface LocationSchema extends z.output<typeof locationsSchema> { }
export interface LocationSchema extends z.output<typeof locationsSchema> {}
export type Locations = LocationSchema["data"]
export type Location = Locations[number]
@@ -20,7 +20,7 @@ export function isHotelLocation(
return location?.type === "hotels"
}
export interface CitiesByCountry
extends z.output<typeof citiesByCountrySchema> { }
extends z.output<typeof citiesByCountrySchema> {}
export type CitiesGroupedByCountry = Record<string, CitiesByCountry["data"]>
export interface Countries extends z.output<typeof countriesSchema> { }
export interface Countries extends z.output<typeof countriesSchema> {}