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:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user