feat(BOOK-768): Added UspCard component with stories and implemented it in blocks

Approved-by: Bianca Widstam
This commit is contained in:
Erik Tiekstra
2026-01-28 07:47:15 +00:00
parent 22b0f71c16
commit 0d357a116b
10 changed files with 222 additions and 34 deletions

View File

@@ -1,18 +1,10 @@
import { IconByIconName } from "@scandic-hotels/design-system/Icons/IconByIconName"
import { JsonToHtml } from "@scandic-hotels/design-system/JsonToHtml"
import { getUspIconName } from "./utils"
import { UspCard } from "@scandic-hotels/design-system/UspCard"
import styles from "./uspgrid.module.css"
import type { UspGridProps, UspIcon } from "@/types/components/blocks/uspGrid"
import type { UspGrid as UspGridType } from "@scandic-hotels/trpc/types/blocks"
function UspIcon({ icon }: { icon: UspIcon }) {
const iconName = getUspIconName(icon)
return iconName ? (
<IconByIconName iconName={iconName} color="Icon/Interactive/Accent" />
) : null
}
interface UspGridProps extends Pick<UspGridType, "usp_grid"> {}
export default function UspGrid({ usp_grid }: UspGridProps) {
return (
@@ -20,13 +12,13 @@ export default function UspGrid({ usp_grid }: UspGridProps) {
{usp_grid.usp_card.map(
(usp) =>
usp.text.json && (
<div key={usp.text.json.uid} className={styles.usp}>
<UspIcon icon={usp.icon} />
<JsonToHtml
embeds={usp.text.embedded_itemsConnection?.edges}
<UspCard
className={styles.uspCard}
key={usp.text.json.uid}
iconName={usp.icon}
embeds={usp.text.embedded_itemsConnection.edges}
nodes={usp.text.json.children}
/>
</div>
)
)}
</div>

View File

@@ -3,17 +3,12 @@
gap: var(--Space-x3);
}
.usp {
display: grid;
gap: var(--Space-x3);
align-content: start;
}
@media screen and (min-width: 768px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
.grid:has(.usp:nth-child(3)):not(:has(.usp:nth-child(4))) {
.grid:has(.uspCard:nth-child(3)):not(:has(.uspCard:nth-child(4))) {
grid-template-columns: repeat(3, 1fr);
}
}

View File

@@ -1,4 +0,0 @@
import type { UspGrid } from "@scandic-hotels/trpc/types/blocks"
export interface UspGridProps extends Pick<UspGrid, "usp_grid"> {}
export type UspIcon = UspGrid["usp_grid"]["usp_card"][number]["icon"]

View File

@@ -20,7 +20,7 @@ const DEFAULT_ARGS = {
}
const meta: Meta<typeof InfoCard> = {
title: "Product Components/InfoCard",
title: "Core Components/Cards/InfoCard",
component: InfoCard,
argTypes: {
topTitle: {

View File

@@ -0,0 +1,132 @@
import type { Meta, StoryObj } from "@storybook/nextjs-vite"
import { RTETypeEnum } from "../JsonToHtml/types/rte/enums"
import type { RTENode } from "../JsonToHtml/types/rte/node"
import { UspCard } from "./UspCard"
import { USP_ICON_NAMES } from "./utils"
const DEFAULT_ARGS = {
nodes: [
{
uid: "paragraph",
attrs: { type: "asset" },
type: RTETypeEnum.p,
children: [
{
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur vitae neque non ipsum efficitur hendrerit at ut nulla.",
},
],
},
] satisfies RTENode[],
embeds: [],
}
const meta: Meta<typeof UspCard> = {
title: "Core Components/Cards/UspCard",
parameters: {
docs: {
description: {
component:
"The card itself does not have a maximum width, but it will adapt to the width of its container. The card should be used together with other usp cards. It is recommended to use the UspCard inside a grid or a container with a set maximum width for best results.",
},
},
},
component: UspCard,
argTypes: {
iconName: {
control: "select",
options: USP_ICON_NAMES,
table: {
type: {
summary: USP_ICON_NAMES.join(" | "),
},
defaultValue: { summary: "Snowflake" },
},
},
embeds: {
control: "object",
description:
"The embeds used by the JsonToHtml component to render rich text content. This data comes from the RTE field in the CMS.",
table: {
type: { summary: "Node<Embeds>[]" },
},
},
nodes: {
control: "object",
description:
"The nodes used by the JsonToHtml component to render rich text content. This data comes from the RTE field in the CMS.",
table: {
type: { summary: "RTENode[]" },
},
},
},
args: { ...DEFAULT_ARGS },
decorators: [
(Story, context) => {
const showMultipleStyles = ["multiple cards", "different icons"].some(
(substring) => context.name.toLowerCase().includes(substring)
)
if (showMultipleStyles) {
return <Story />
}
return (
<div style={{ maxWidth: "400px" }}>
<Story />
</div>
)
},
],
}
export default meta
type Story = StoryObj<typeof UspCard>
export const Default: Story = {
args: {
...meta.args,
},
}
export const MultipleCards: Story = {
args: {
...meta.args,
},
render: (args) => (
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gap: "16px",
width: "min(800px, 100%)",
margin: "0 auto",
}}
>
<UspCard {...args} />
<UspCard {...args} />
<UspCard {...args} />
</div>
),
}
export const DifferentIcons: Story = {
args: {
...meta.args,
},
render: (args) => (
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gap: "16px",
width: "min(800px, 100%)",
margin: "0 auto",
}}
>
<UspCard {...args} iconName={USP_ICON_NAMES[0]} />
<UspCard {...args} iconName={USP_ICON_NAMES[1]} />
<UspCard {...args} iconName={USP_ICON_NAMES[2]} />
</div>
),
}

View File

@@ -0,0 +1,36 @@
import { cx } from "class-variance-authority"
import { IconByIconName } from "../Icons/IconByIconName"
import { type Embeds, JsonToHtml, type Node } from "../JsonToHtml/JsonToHtml"
import type { RTENode } from "../JsonToHtml/types/rte/node"
import { getUspIconName, UspIconName } from "./utils"
import styles from "./uspCard.module.css"
interface UspCardProps extends React.HTMLAttributes<HTMLDivElement> {
iconName?: UspIconName | null
embeds: Node<Embeds>[]
nodes: RTENode[]
}
export function UspCard({
className,
iconName,
embeds,
nodes,
...props
}: UspCardProps) {
const resolvedIconName = getUspIconName(iconName)
return (
<div className={cx(styles.uspCard, className)} {...props}>
<IconByIconName
className={styles.icon}
iconName={resolvedIconName}
color="Icon/Interactive/Accent"
size={48}
/>
<JsonToHtml embeds={embeds} nodes={nodes} />
</div>
)
}

View File

@@ -0,0 +1 @@
export { UspCard } from "./UspCard"

View File

@@ -0,0 +1,9 @@
.uspCard {
display: grid;
gap: var(--Space-x3);
align-content: start;
}
.icon {
justify-self: start;
}

View File

@@ -1,9 +1,35 @@
import { IconName } from "@scandic-hotels/design-system/Icons/iconName"
import { IconName } from "../Icons/iconName"
import type { UspIcon } from "@/types/components/blocks/uspGrid"
export const USP_ICON_NAMES = [
"Snowflake",
"Information",
"Heart",
"WiFi",
"Breakfast",
"Checkbox",
"Ticket",
"Hotel",
"Bed",
"Train",
"Airplane",
"Sun",
"Star",
"Sports",
"Gym",
"Hiking",
"Skiing",
"City",
"Pool",
"Spa",
"Bar",
"Restaurant",
"Child",
] as const
export function getUspIconName(icon?: UspIcon | null) {
switch (icon) {
export type UspIconName = (typeof USP_ICON_NAMES)[number]
export function getUspIconName(iconName?: UspIconName | null) {
switch (iconName) {
case "Snowflake":
return IconName.Snowflake
case "Information":

View File

@@ -188,6 +188,7 @@
"./Tooltip": "./lib/components/Tooltip/index.tsx",
"./TripAdvisorChip": "./lib/components/TripAdvisorChip/index.tsx",
"./Typography": "./lib/components/Typography/index.tsx",
"./UspCard": "./lib/components/UspCard/index.tsx",
"./VideoPlayer": "./lib/components/VideoPlayer/index.tsx",
"./VideoWithCard": "./lib/components/VideoPlayer/VideoWithCard/index.tsx",
"./design-system-new-deprecated.css": "./lib/design-system-new-deprecated.css",