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

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