From 0d357a116b6cdb537c1cd5e6514bcb3b16303f50 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Wed, 28 Jan 2026 07:47:15 +0000 Subject: [PATCH] feat(BOOK-768): Added UspCard component with stories and implemented it in blocks Approved-by: Bianca Widstam --- .../components/Blocks/UspGrid/index.tsx | 28 ++-- .../Blocks/UspGrid/uspgrid.module.css | 9 +- .../types/components/blocks/uspGrid.ts | 4 - .../components/InfoCard/InfoCard.stories.tsx | 2 +- .../components/UspCard/UspCard.stories.tsx | 132 ++++++++++++++++++ .../lib/components/UspCard/UspCard.tsx | 36 +++++ .../lib/components/UspCard/index.tsx | 1 + .../lib/components/UspCard/uspCard.module.css | 9 ++ .../lib/components/UspCard}/utils.ts | 34 ++++- packages/design-system/package.json | 1 + 10 files changed, 222 insertions(+), 34 deletions(-) delete mode 100644 apps/scandic-web/types/components/blocks/uspGrid.ts create mode 100644 packages/design-system/lib/components/UspCard/UspCard.stories.tsx create mode 100644 packages/design-system/lib/components/UspCard/UspCard.tsx create mode 100644 packages/design-system/lib/components/UspCard/index.tsx create mode 100644 packages/design-system/lib/components/UspCard/uspCard.module.css rename {apps/scandic-web/components/Blocks/UspGrid => packages/design-system/lib/components/UspCard}/utils.ts (70%) diff --git a/apps/scandic-web/components/Blocks/UspGrid/index.tsx b/apps/scandic-web/components/Blocks/UspGrid/index.tsx index 54c7b674b..85f15d3d0 100644 --- a/apps/scandic-web/components/Blocks/UspGrid/index.tsx +++ b/apps/scandic-web/components/Blocks/UspGrid/index.tsx @@ -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 ? ( - - ) : null -} +interface UspGridProps extends Pick {} 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 && ( -
- - -
+ ) )} diff --git a/apps/scandic-web/components/Blocks/UspGrid/uspgrid.module.css b/apps/scandic-web/components/Blocks/UspGrid/uspgrid.module.css index 7eeb99e19..71682ea13 100644 --- a/apps/scandic-web/components/Blocks/UspGrid/uspgrid.module.css +++ b/apps/scandic-web/components/Blocks/UspGrid/uspgrid.module.css @@ -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); } } diff --git a/apps/scandic-web/types/components/blocks/uspGrid.ts b/apps/scandic-web/types/components/blocks/uspGrid.ts deleted file mode 100644 index 4db708283..000000000 --- a/apps/scandic-web/types/components/blocks/uspGrid.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { UspGrid } from "@scandic-hotels/trpc/types/blocks" - -export interface UspGridProps extends Pick {} -export type UspIcon = UspGrid["usp_grid"]["usp_card"][number]["icon"] diff --git a/packages/design-system/lib/components/InfoCard/InfoCard.stories.tsx b/packages/design-system/lib/components/InfoCard/InfoCard.stories.tsx index 1efe32dcd..e04d1c8f0 100644 --- a/packages/design-system/lib/components/InfoCard/InfoCard.stories.tsx +++ b/packages/design-system/lib/components/InfoCard/InfoCard.stories.tsx @@ -20,7 +20,7 @@ const DEFAULT_ARGS = { } const meta: Meta = { - title: "Product Components/InfoCard", + title: "Core Components/Cards/InfoCard", component: InfoCard, argTypes: { topTitle: { diff --git a/packages/design-system/lib/components/UspCard/UspCard.stories.tsx b/packages/design-system/lib/components/UspCard/UspCard.stories.tsx new file mode 100644 index 000000000..3c7fc8dab --- /dev/null +++ b/packages/design-system/lib/components/UspCard/UspCard.stories.tsx @@ -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 = { + 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[]" }, + }, + }, + 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 + } + + return ( +
+ +
+ ) + }, + ], +} + +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + ...meta.args, + }, +} + +export const MultipleCards: Story = { + args: { + ...meta.args, + }, + render: (args) => ( +
+ + + +
+ ), +} + +export const DifferentIcons: Story = { + args: { + ...meta.args, + }, + render: (args) => ( +
+ + + +
+ ), +} diff --git a/packages/design-system/lib/components/UspCard/UspCard.tsx b/packages/design-system/lib/components/UspCard/UspCard.tsx new file mode 100644 index 000000000..1e85fcd3f --- /dev/null +++ b/packages/design-system/lib/components/UspCard/UspCard.tsx @@ -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 { + iconName?: UspIconName | null + embeds: Node[] + nodes: RTENode[] +} + +export function UspCard({ + className, + iconName, + embeds, + nodes, + ...props +}: UspCardProps) { + const resolvedIconName = getUspIconName(iconName) + + return ( +
+ + +
+ ) +} diff --git a/packages/design-system/lib/components/UspCard/index.tsx b/packages/design-system/lib/components/UspCard/index.tsx new file mode 100644 index 000000000..7e38231e6 --- /dev/null +++ b/packages/design-system/lib/components/UspCard/index.tsx @@ -0,0 +1 @@ +export { UspCard } from "./UspCard" diff --git a/packages/design-system/lib/components/UspCard/uspCard.module.css b/packages/design-system/lib/components/UspCard/uspCard.module.css new file mode 100644 index 000000000..441414559 --- /dev/null +++ b/packages/design-system/lib/components/UspCard/uspCard.module.css @@ -0,0 +1,9 @@ +.uspCard { + display: grid; + gap: var(--Space-x3); + align-content: start; +} + +.icon { + justify-self: start; +} diff --git a/apps/scandic-web/components/Blocks/UspGrid/utils.ts b/packages/design-system/lib/components/UspCard/utils.ts similarity index 70% rename from apps/scandic-web/components/Blocks/UspGrid/utils.ts rename to packages/design-system/lib/components/UspCard/utils.ts index a880107d6..1df1ec49c 100644 --- a/apps/scandic-web/components/Blocks/UspGrid/utils.ts +++ b/packages/design-system/lib/components/UspCard/utils.ts @@ -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": diff --git a/packages/design-system/package.json b/packages/design-system/package.json index 251dd3f8e..9d69e1e7c 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -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",