Merged in feat/reusable-card (pull request #190)

Feat/reusable card and button design

Approved-by: Michael Zetterberg
This commit is contained in:
Christel Westerberg
2024-05-21 15:24:00 +00:00
committed by Michael Zetterberg
42 changed files with 690 additions and 426 deletions

View File

@@ -1,7 +1,7 @@
.layout {
--header-height: 4.5rem;
background-color: var(--Brand-Coffee-Subtle);
background-color: var(--Scandic-Brand-Warm-White);
display: grid;
font-family: var(--ff-fira-sans);
grid-template-rows: var(--header-height) auto 1fr;

View File

@@ -16,7 +16,6 @@ export default function EditProfile({ params }: PageArgs<LangParams>) {
<Button
aria-label="Cancel"
asChild
bgcolor="white"
form="edit-profile"
size="small"
type="reset"
@@ -24,12 +23,10 @@ export default function EditProfile({ params }: PageArgs<LangParams>) {
<Link href={profile[params.lang]}>{_("Cancel")}</Link>
</Button>
<Button
bgcolor="quarternary"
disabled={!isValid || isPending}
form="edit-profile"
size="small"
type="submit"
weight="regular"
>
{_("Save")}
</Button>

View File

@@ -8,7 +8,7 @@ import type { LangParams, PageArgs } from "@/types/params"
export default function ProfileView({ params }: PageArgs<LangParams>) {
return (
<Button asChild bgcolor="quarternary" size="small" weight="regular">
<Button asChild size="small">
<Link href={profileEdit[params.lang]}>{_("Edit")}</Link>
</Button>
)

View File

@@ -1,5 +1,5 @@
.layout {
display: grid;
font-family: var(--ff-fira-sans);
background-color: var(--Brand-Coffee-Subtle);
background-color: var(--Scandic-Brand-Warm-White);
}

View File

@@ -1,5 +1,3 @@
import { firaMono, firaSans } from "@/app/[lang]/(live)/fonts"
import styles from "./layout.module.css"
import {
@@ -11,15 +9,8 @@ import {
export default function ContentTypeLayout({
children,
params,
}: React.PropsWithChildren<
LayoutArgs<LangParams & ContentTypeParams & UIDParams>
>) {
return (
<div
className={`${firaMono.variable} ${firaSans.variable} ${styles.layout}`}
>
{children}
</div>
)
return <div className={styles.layout}>{children}</div>
}

View File

@@ -1,4 +1,5 @@
import { Fira_Mono, Fira_Sans } from "next/font/google"
import localFont from "next/font/local"
export const firaMono = Fira_Mono({
subsets: ["latin"],
@@ -11,3 +12,14 @@ export const firaSans = Fira_Sans({
weight: ["300", "400", "600", "900"],
variable: "--ff-fira-sans",
})
export const biroScriptPlus = localFont({
src: [
{
path: "../../../public/_static/fonts/biro-script-plus/Biro-Script-Plus.ttf",
style: "normal",
weight: "500",
},
],
variable: "--ff-biro-script-plus",
})

View File

@@ -8,6 +8,8 @@ import TrpcProvider from "@/lib/trpc/Provider"
import AdobeScript from "@/components/Current/AdobeScript"
import VwoScript from "@/components/Current/VwoScript"
import { biroScriptPlus, firaMono, firaSans } from "./fonts"
import type { Metadata } from "next"
import type { LangParams, LayoutArgs } from "@/types/params"
@@ -42,7 +44,9 @@ export default async function RootLayout({
<AdobeScript />
<VwoScript />
</head>
<body>
<body
className={`${firaMono.variable} ${firaSans.variable} ${biroScriptPlus.variable} `}
>
<TrpcProvider lang={params.lang}>{children}</TrpcProvider>
<Script id="page-tracking">{`
typeof _satellite !== "undefined" && _satellite.pageBottom();

View File

@@ -17,7 +17,8 @@ import type {
import { RTEMarkType } from "@/types/rte/node"
import type { RenderOptions } from "@/types/rte/option"
function extractPossibleAttributes(attrs: Attributes) {
function extractPossibleAttributes(attrs: Attributes | undefined) {
if (!attrs) return {}
const props: Record<string, any> = {}
if (attrs.id) {
props.id = attrs.id
@@ -252,7 +253,7 @@ export const renderOptions: RenderOptions = {
const type = node.attrs.type
if (type === RTEItemTypeEnum.asset) {
const image = embeds?.[node?.attrs?.["asset-uid"]]
if (image.node.__typename === EmbedEnum.SysAsset) {
if (image?.node.__typename === EmbedEnum.SysAsset) {
const alt = image?.node?.title ?? node.attrs.alt
const alignment = node.attrs?.style?.["text-align"]
? {

View File

@@ -14,7 +14,7 @@ import type { RenderOptions } from "@/types/rte/option"
export function groupEmbedsByUid(embedsArray: Node<Embeds>[]) {
const embedsByUid = embedsArray.reduce<EmbedByUid>((acc, embed) => {
if (embed.node.system.uid) {
if (embed.node.system?.uid) {
acc[embed.node.system.uid] = embed
}
return acc

View File

@@ -1,28 +0,0 @@
.container {
display: grid;
gap: 2.4rem;
}
.subtitle {
margin: 0;
}
.titleContainer {
display: grid;
gap: 0.8rem;
}
.cardContainer {
display: grid;
gap: 1.6rem;
}
@media screen and (min-width: 950px) {
.cardContainer {
grid-template-columns: 1fr 1fr;
}
.cardWrapper:last-child {
grid-column: span 2;
}
}

View File

@@ -1,41 +0,0 @@
import { _ } from "@/lib/translation"
import Card from "@/components/TempDesignSystem/Card"
import Title from "@/components/Title"
import styles from "./cardGrid.module.css"
import { CardGridProps } from "@/types/components/loyalty/blocks"
export default function CardGrid({ card_grid }: CardGridProps) {
return (
<section className={styles.container}>
<header className={styles.titleContainer}>
<Title as="h3" level="h2" weight="semiBold" uppercase>
{card_grid.title}
</Title>
{card_grid.subtitle ? (
<Title
as="h5"
level="h3"
weight="regular"
className={styles.subtitle}
>
{card_grid.subtitle}
</Title>
) : null}
</header>
<div className={styles.cardContainer}>
{card_grid.cards.map((card, i) => (
<div className={styles.cardWrapper} key={`${card.title}+${i}`}>
<Card
subtitle={card.subtitle}
title={card.title}
link={card.link}
/>
</div>
))}
</div>
</section>
)
}

View File

@@ -0,0 +1,4 @@
.section {
display: grid;
gap: 2.4rem;
}

View File

@@ -0,0 +1,30 @@
import { _ } from "@/lib/translation"
import Header from "@/components/MyPages/Blocks/Header"
import Card from "@/components/TempDesignSystem/Card"
import CardGrid from "@/components/TempDesignSystem/CardGrid"
import styles from "./cardsGrid.module.css"
import { CardsGridProps } from "@/types/components/loyalty/blocks"
export default function CardsGrid({ cards_grid }: CardsGridProps) {
return (
<section className={styles.section}>
<Header title={cards_grid.title} subtitle={cards_grid.preamble} />
<CardGrid variant={cards_grid.layout}>
{cards_grid.cards.map((card) => (
<Card
theme={cards_grid.theme || "one"}
key={card.system.uid}
scriptedTopTitle={card.scripted_top_title}
heading={card.heading}
bodyText={card.body_text}
secondaryButton={card.secondaryButton}
primaryButton={card.primaryButton}
/>
))}
</CardGrid>
</section>
)
}

View File

@@ -5,7 +5,7 @@
width: 100%;
height: 37rem;
border-radius: 1.6rem;
background-color: var(--Base-Fill-Normal);
background-color: var(--UI-Grey-10);
text-align: center;
margin-right: 1.6rem;
}

View File

@@ -27,7 +27,7 @@
height: 37rem;
min-width: 32rem;
padding: 4rem 1rem;
background-color: var(--Base-Fill-Normal);
background-color: var(--UI-Grey-10);
border-radius: 1.6rem;
gap: 1.8rem;
}

View File

@@ -2,7 +2,7 @@ import JsonToHtml from "@/components/JsonToHtml"
import DynamicContentBlock from "@/components/Loyalty/Blocks/DynamicContent"
import Shortcuts from "@/components/MyPages/Blocks/Shortcuts"
import CardGrid from "./CardGrid"
import CardsGrid from "./CardsGrid"
import type { BlocksProps } from "@/types/components/loyalty/blocks"
import { LoyaltyBlocksTypenameEnum } from "@/types/components/loyalty/enums"
@@ -10,8 +10,6 @@ import { LoyaltyBlocksTypenameEnum } from "@/types/components/loyalty/enums"
export function Blocks({ blocks }: BlocksProps) {
return blocks.map((block) => {
switch (block.__typename) {
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardGrid:
return <CardGrid card_grid={block.card_grid} />
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksContent:
return (
<section>
@@ -31,6 +29,8 @@ export function Blocks({ blocks }: BlocksProps) {
subtitle={block.shortcuts.preamble}
/>
)
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardsGrid:
return <CardsGrid cards_grid={block.cards_grid} />
default:
return null
}

View File

@@ -8,7 +8,7 @@
flex-direction: column;
align-items: center;
justify-content: center;
border-top: 0.5px solid var(--Base-Border-Disabled);
border-top: 0.5px solid var(--UI-Grey-30);
padding: 3.4rem;
text-align: center;
gap: 6.2rem;

View File

@@ -46,7 +46,7 @@
.contactContainer {
display: block;
border-top: 0.5px solid var(--Base-Border-Disabled);
border-top: 0.5px solid var(--UI-Grey-30);
display: flex;
justify-content: center;
padding: 3.4rem;

View File

@@ -29,7 +29,7 @@
flex-direction: column;
gap: 10px;
min-height: 280px;
background-color: var(--Base-Fill-Normal);
background-color: var(--UI-Grey-10);
justify-content: center;
align-items: center;
padding: 30px;

View File

@@ -24,7 +24,7 @@ export default async function NextLevelBenefitsBlock({
<CardGrid variant="twoColumnGrid">
{perks.map((perk) => (
<article key={perk.id} className={styles.card}>
<Button type="button" intent="secondary" disabled>
<Button type="button" disabled>
<Lock height={16} />
Level up to unlock
</Button>

View File

@@ -13,7 +13,7 @@ export default function ShowMoreButton({
<Button
disabled={disabled}
intent="primary"
bgcolor="white"
theme="secondaryDark"
type="button"
onClick={loadMoreData}
>

View File

@@ -17,7 +17,7 @@ export default function EmptyUpcomingStaysBlock() {
{_("Where should you go next?")}
</span>
</Title>
<Button intent="primary" bgcolor="quarternary" asChild type="button">
<Button intent="primary" asChild type="button">
<Link className={styles.link} href={"#"} key="getInspired">
{_("Get inspired")}
</Link>

View File

@@ -17,7 +17,7 @@ export default function EmptyUpcomingStaysBlock() {
{_("Where should you go next?")}
</span>
</Title>
<Button intent="primary" bgcolor="quarternary" asChild type="button">
<Button intent="primary" asChild type="button">
<Link className={styles.link} href={"#"} key="getInspired">
{_("Get inspired")}
</Link>

View File

@@ -4,21 +4,43 @@
cursor: pointer;
margin: 0;
padding: 0;
text-align: center;
font-family: var(--ff-fira-sans);
font-weight: 600;
line-height: 150%;
letter-spacing: 1%;
}
.primary {
color: var(--font-color,);
background-color: var(--background-color,);
}
.primary:hover,
.primary:active,
.primary:focus {
background-color: var(--hover-background,);
color: var(--hover-color,);
}
.secondary {
background-color: transparent;
border: 0.1rem solid var(--background-color);
color: var(--background-color,);
}
.secondary:hover,
.secondary:active,
.secondary:focus {
border: 0.1rem solid var(--hover-color,);
color: var(--hover-color);
}
.default {
align-items: center;
background-color: var(--some-white-color, #fff);
border-radius: 4rem;
color: var(--some-grey-color, #111);
display: flex;
font-family: var(--ff-fira-sans);
font-size: 1.8rem;
font-weight: 500;
gap: 1rem;
letter-spacing: 1%;
line-height: 2.2rem;
padding: 0.75rem 1.65rem;
}
.icon {
@@ -26,125 +48,110 @@
align-items: baseline;
}
/* Primary styles */
.primary {
background-color: var(--some-grey-color, #444343);
border: 2px solid transparent;
outline: 1px solid transparent;
border-radius: 46px;
color: var(--Main-Grey-00, #fff);
}
.primary:hover {
background: var(--some-grey-color, #444343);
}
.primary:active,
.primary:focus {
border: var(--some-grey-color, #444343);
outline: var(--some-grey-color, #444343);
}
/* Secondary styles */
.secondary {
border: 1px solid var(--some-grey-color, #444343);
background-color: transparent;
color: var(--some-grey-color, #444343);
border-radius: 46px;
font-size: 12px;
display: flex;
align-items: center;
}
.secondary:hover {
border: 1px solid var(--some-grey-color, #444343);
}
.secondary:active,
.secondary:focus {
border: 1px solid var(--some-grey-color, #444343);
}
/* Disabled styles */
.btn:disabled {
border: 1px solid var(--some-grey-color, #444343);
background-color: transparent;
color: var(--some-grey-color, #444343);
background-color: var(--disabled-background-color);
color: var(--disabled-color);
cursor: not-allowed;
}
/* Sizes */
.small {
border-radius: 3rem;
padding: var(--Spacing-x1) var(--Spacing-x2);
gap: 0.2rem;
font-size: 1.4rem;
height: 2.6rem;
line-height: 1.7rem;
padding: 0.8rem 2.2rem;
}
.average {
border-radius: 4.7rem;
font-size: 1.4rem;
height: 3.2rem;
letter-spacing: 1%;
line-height: 1.6rem;
padding: 0.65rem 1.3rem;
}
.light {
font-weight: 300;
}
.regular {
font-weight: 400;
height: 4rem;
}
.medium {
font-weight: 500;
height: 3rem;
padding: 1.2rem var(--Spacing-x2,);
font-size: 1.6rem;
}
.semiBold {
font-weight: 600;
.large {
padding: var(--Spacing-x2) 2.4rem;
gap: 0.4rem;
font-size: 1.6rem;
}
.bold {
font-weight: 700;
.primaryLight {
--font-color: var(--Theme-Primary-Light-Button-Primary-On-Fill-Normal);
--background-color: var(--Theme-Primary-Light-Button-Primary-Fill-Normal,);
--hover-background: var(--Theme-Primary-Light-Button-Primary-Fill-Hover,);
--hover-color: var(--Theme-Primary-Light-Button-Primary-On-Fill-Hover,);
--disabled-background-color: var(
--Theme-Primary-Light-Button-Primary-Fill-Disabled,
);
--disabled-color: var(--Theme-Primary-Light-Button-Primary-On-Fill-Disabled,);
}
.black {
font-weight: 900;
.primaryDark {
--font-color: var(--Theme-Primary-Dark-Button-Primary-On-Fill-Normal,);
--background-color: var(--Theme-Primary-Dark-Button-Primary-Fill-Normal,);
--hover-color: var(--Theme-Primary-Dark-Button-Primary-On-Fill-Hover,);
--hover-background: var(--Theme-Primary-Dark-Button-Primary-Fill-Hover,);
--disabled-background-color: var(
--Theme-Primary-Dark-Button-Primary-Fill-Disabled,
);
--disabled-color: var(--Theme-Primary-Dark-Button-Primary-On-Fill-Disabled,);
}
.primary {
background-color: var(--scandic-blue, #02838e);
border: 0.1rem solid var(--scandic-blue, #02838e);
color: var(--some-white-color, #fff);
.secondaryLight {
--font-color: var(--Theme-Secondary-Light-Button-Primary-On-Fill-Normal,);
--background-color: var(--Theme-Secondary-Light-Button-Primary-Fill-Normal,);
--hover-color: var(--Theme-Secondary-Light-Button-Primary-On-Fill-Hover,);
--hover-background: var(--Theme-Secondary-Light-Button-Primary-Fill-Hover,);
--disabled-background-color: var(
--Theme-Secondary-Light-Button-Primary-Fill-Disabled,
);
--disabled-color: var(
--Theme-Secondary-Light-Button-Primary-On-Fill-Disabled,
);
}
.secondary {
background-color: var(--some-black-color, #000);
border: 0.1rem solid var(--some-black-color, #000);
color: var(--some-white-color, #fff);
.secondaryDark {
--font-color: var(--Theme-Secondary-Dark-Button-Primary-On-Fill-Normal,);
--background-color: var(--Theme-Secondary-Dark-Button-Primary-Fill-Normal,);
--hover-color: var(--Theme-Secondary-Dark-Button-Primary-On-Fill-Hover,);
--hover-background: var(--Theme-Secondary-Dark-Button-Primary-Fill-Hover,);
--disabled-background-color: var(
--Theme-Secondary-Dark-Button-Primary-Fill-Disabled,
);
--disabled-color: var(
--Theme-Secondary-Dark-Button-Primary-On-Fill-Disabled,
);
}
.tertiary {
background-color: var(--some-red-color, #d60728);
border: 0.1rem solid var(--some-red-color, #d60728);
color: var(--some-white-color, #fff);
.tertiaryLight {
--font-color: var(--Theme-Tertiary-Light-Button-Primary-On-Fill-Normal,);
--background-color: var(--Theme-Tertiary-Light-Button-Primary-Fill-Normal,);
--hover-color: var(--Theme-Tertiary-Light-Button-Primary-On-Fill-Hover,);
--hover-background: var(--Theme-Tertiary-Light-Button-Primary-Fill-Hover,);
--disabled-background-color: var(
--Theme-Tertiary-Light-Button-Primary-Fill-Disabled,
);
--disabled-color: var(
--Theme-Tertiary-Light-Button-Primary-On-Fill-Disabled,
);
}
.quarternary {
background-color: var(--some-grey-color, #727272);
border: 0.1rem solid var(--some-black-color, #727272);
color: var(--some-white-color, #fff);
}
.tertiaryDark {
--font-color: var(--Theme-Tertiary-Dark-Button-Primary-On-Fill-Normal);
--background-color: var(--Theme-Tertiary-Dark-Button-Primary-Fill-Normal);
--hover-color: var(--Theme-Tertiary-Dark-Button-Primary-On-Fill-Hover);
--hover-background: var(--Theme-Tertiary-Dark-Button-Primary-Fill-Hover);
.white {
background-color: var(--some-white-color, #fff);
border: 0.1rem solid var(--some-black-color, #000);
color: var(--some-black-color, #000);
}
.btn:disabled {
background-color: var(--some-grey-color, #d9d9d9);
color: var(--some-grey-color, #757575);
--disabled-background-color: var(
--Theme-Tertiary-Dark-Button-Primary-Fill-Disabled,
);
--disabled-color: var(--Theme-Tertiary-Dark-Button-Primary-On-Fill-Disabled,);
}

View File

@@ -8,23 +8,21 @@ import type { ButtonProps } from "./button"
export default function Button({
asChild = false,
bgcolor,
theme,
className,
disabled,
intent,
size,
variant,
weight,
...props
}: ButtonProps) {
const Comp = asChild ? Slot : "button"
const classNames = buttonVariants({
bgcolor,
theme,
className,
intent,
size,
variant,
weight,
})
return <Comp className={classNames} disabled={disabled} {...props} />
}

View File

@@ -4,12 +4,13 @@ import styles from "./button.module.css"
export const buttonVariants = cva(styles.btn, {
variants: {
bgcolor: {
primary: styles.primary,
secondary: styles.secondary,
tertiary: styles.tertiary,
quarternary: styles.quarternary,
white: styles.white,
theme: {
primaryLight: styles.primaryLight,
primaryDark: styles.primaryDark,
secondaryLight: styles.secondaryLight,
secondaryDark: styles.secondaryDark,
tertiaryLight: styles.tertiaryLight,
tertiaryDark: styles.tertiaryDark,
},
intent: {
primary: styles.primary,
@@ -17,23 +18,18 @@ export const buttonVariants = cva(styles.btn, {
},
size: {
small: styles.small,
regular: styles.average,
medium: styles.medium,
large: styles.large,
},
variant: {
default: styles.default,
icon: styles.icon,
},
weight: {
light: styles.light,
regular: styles.regular,
medium: styles.medium,
semiBold: styles.semiBold,
bold: styles.bold,
black: styles.black,
},
},
defaultVariants: {
variant: "default",
weight: "regular",
size: "medium",
theme: "primaryLight",
intent: "primary",
},
})

View File

@@ -1,15 +1,80 @@
.linkCard {
.container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 37rem;
height: 48rem;
width: 100%;
margin-right: 1.6rem;
border-radius: 1.6rem;
gap: 1rem;
padding: 1.6rem;
gap: var(--Spacing-x2);
padding: 0px 3.2rem;
background-color: var(--Base-Fill-Normal);
text-align: center;
}
.themeOne {
--font-color: var(--Brand-Main-Strong, #4d001b);
--script-color: var(--UI-Red-70, #ad0015);
--divider-color: var(--UI-Red-10, #f7c1c2);
background: var(--Theme-Primary-Light-Surface-Normal, #f7e1d5);
}
.themeTwo {
--font-color: var(--Brand-Forest-Strong, #093021);
--script-color: var(--UI-Green-70, #286806);
--divider-color: var(--UI-Green-10, #badda8);
background: var(--Brand-Forest-Subtle, #d2edaf);
}
.themeThree {
--font-color: var(--Brand-Sea-Strong, #0d1440);
--script-color: var(--UI-Blue-70, #1555b4);
--divider-color: var(--UI-Blue-10, #c7d9f5);
background: var(--Brand-Sea-Accent, #fff0c2);
}
.scriptContainer {
display: grid;
gap: 1rem;
}
.scriptedTitle {
color: var(--script-color);
font-family: var(--ff-biro-script-plus);
font-size: var(--typography-Script-2-fontSize);
font-weight: var(--typography-Script-2-fontWeight);
line-height: var(--typography-Script-2-lineHeight);
letter-spacing: 0.48px;
padding: 1rem;
transform: rotate(-3deg);
}
.divider {
border-bottom-color: var(--divider-color);
}
.heading {
color: var(--font-color);
}
.bodyText {
color: var(--font-color);
text-align: center;
font-size: var(--typography-Body-Regular-fontSize);
font-weight: var(--typography-Body-Regular-fontWeight);
line-height: var(--typography-Body-Regular-lineHeight);
letter-spacing: 0.096px;
}
.buttonContainer {
display: flex;
gap: 0.8rem;
justify-content: center;
}

View File

@@ -1,9 +1,24 @@
export type CardProps = {
link?: {
import { cardVariants } from "./variants"
import type { VariantProps } from "class-variance-authority"
export interface CardProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof cardVariants> {
primaryButton?: {
href: string
title: string
openInNewTab?: boolean
isExternal: boolean
}
title?: string | null
subtitle?: string | null
openInNewTab?: boolean
secondaryButton?: {
href: string
title: string
openInNewTab?: boolean
isExternal: boolean
}
scriptedTopTitle?: string | null
heading?: string | null
bodyText?: string | null
backgroundImage?: { url: string }
}

View File

@@ -1,36 +1,93 @@
import Title from "@/components/Title"
import Button from "../Button"
import { ButtonProps } from "../Button/button"
import Divider from "../Divider"
import Link from "../Link"
import { CardProps } from "./card"
import { cardVariants } from "./variants"
import styles from "./card.module.css"
export default function Card({
link,
subtitle,
title,
openInNewTab = false,
primaryButton,
secondaryButton,
scriptedTopTitle,
heading,
bodyText,
backgroundImage,
className,
theme,
}: CardProps) {
let buttonTheme: ButtonProps["theme"] = "primaryLight"
switch (theme) {
case "one":
buttonTheme = "primaryLight"
break
case "two":
buttonTheme = "secondaryLight"
break
case "three":
buttonTheme = "tertiaryLight"
break
}
return (
<article className={styles.linkCard}>
{title ? (
<Title level="h3" weight="semiBold">
{title}
<article
className={cardVariants({
className,
theme,
})}
>
{scriptedTopTitle ? (
<section className={styles.scriptContainer}>
<Title level="h3" weight="semiBold" className={styles.scriptedTitle}>
{scriptedTopTitle}
</Title>
<Divider className={styles.divider} />
</section>
) : null}
{heading ? (
<Title
level="h3"
as="h5"
weight="semiBold"
uppercase
className={styles.heading}
>
{heading}
</Title>
) : null}
{subtitle ? (
<Title level="h5" weight="light">
{subtitle}
</Title>
) : null}
{link ? (
<Button asChild intent="primary">
<Link href={link.href} target={openInNewTab ? "_blank" : undefined}>
{link.title}
</Link>
</Button>
) : null}
{bodyText ? <p className={styles.bodyText}>{bodyText}</p> : null}
<div className={styles.buttonContainer}>
{primaryButton ? (
<Button asChild theme={buttonTheme} size="small">
<Link
href={primaryButton.href}
target={primaryButton.openInNewTab ? "_blank" : undefined}
>
{primaryButton.title}
</Link>
</Button>
) : null}
{secondaryButton ? (
<Button
asChild
theme={buttonTheme}
size="small"
intent="secondary"
disabled
>
<Link
href={secondaryButton.href}
target={secondaryButton.openInNewTab ? "_blank" : undefined}
>
{secondaryButton.title}
</Link>
</Button>
) : null}
</div>
</article>
)
}

View File

@@ -0,0 +1,16 @@
import { cva } from "class-variance-authority"
import styles from "./card.module.css"
export const cardVariants = cva(styles.container, {
variants: {
theme: {
one: styles.themeOne,
two: styles.themeTwo,
three: styles.themeThree,
},
},
defaultVariants: {
theme: "one",
},
})

View File

@@ -37,14 +37,19 @@
}
@media screen and (min-width: 950px) {
.twoColumnGrid {
.twoColumnGrid,
.twoPlusOne {
grid-template-columns: repeat(2, 1fr);
}
.treeColumnGrid {
.threeColumnGrid {
grid-template-columns: repeat(3, 1fr);
}
.twoPlusOne > *:last-child {
grid-column: span 2;
}
.carousel {
grid-auto-flow: unset;
margin: 0;

View File

@@ -9,10 +9,11 @@ export const cardGridVariants = cva(styles.gridContainer, {
},
variant: {
twoColumnGrid: styles.twoColumnGrid,
treeColumnGrid: styles.treeColumnGrid,
threeColumnGrid: styles.threeColumnGrid,
twoPlusOne: styles.twoPlusOne,
},
},
defaultVariants: {
variant: "treeColumnGrid",
variant: "threeColumnGrid",
},
})

View File

@@ -2,6 +2,7 @@
/* font-family: var(--ff-brandon-text); */
margin: 0;
padding: 0;
color: var(--Scandic-Brand-Burgundy);
}
.uppercase {

View File

@@ -0,0 +1,49 @@
fragment CardBlock on Card {
heading
body_text
background_image
scripted_top_title
title
secondary_button {
is_contentstack_link
cta_text
open_in_new_tab
external_link {
title
href
}
linkConnection {
edges {
node {
__typename
...LoyaltyPageLink
...ContentPageLink
...AccountPageLink
}
}
}
}
primary_button {
is_contentstack_link
cta_text
open_in_new_tab
external_link {
title
href
}
linkConnection {
edges {
node {
__typename
...LoyaltyPageLink
...ContentPageLink
...AccountPageLink
}
}
}
}
system {
locale
uid
}
}

View File

@@ -0,0 +1,29 @@
fragment CardBlockRef on Card {
secondary_button {
linkConnection {
edges {
node {
__typename
...LoyaltyPageRef
...ContentPageRef
...AccountPageRef
}
}
}
}
primary_button {
linkConnection {
edges {
node {
__typename
...LoyaltyPageRef
...ContentPageRef
...AccountPageRef
}
}
}
}
system {
...System
}
}

View File

@@ -1,4 +1,6 @@
#import "../Fragments/Image.graphql"
#import "../Fragments/Blocks/Card.graphql"
#import "../Fragments/Blocks/Refs/Card.graphql"
#import "../Fragments/PageLink/AccountPageLink.graphql"
#import "../Fragments/PageLink/ContentPageLink.graphql"
#import "../Fragments/PageLink/LoyaltyPageLink.graphql"
@@ -11,8 +13,8 @@
query GetLoyaltyPage($locale: String!, $uid: String!) {
loyalty_page(uid: $uid, locale: $locale) {
blocks {
__typename
... on LoyaltyPageBlocksShortcuts {
__typename
shortcuts {
title
preamble
@@ -22,10 +24,9 @@ query GetLoyaltyPage($locale: String!, $uid: String!) {
linkConnection {
edges {
node {
__typename
...AccountPageLink
...LoyaltyPageLink
...ContentPageLink
...AccountPageLink
}
}
totalCount
@@ -34,6 +35,7 @@ query GetLoyaltyPage($locale: String!, $uid: String!) {
}
}
... on LoyaltyPageBlocksDynamicContent {
__typename
dynamic_content {
title
subtitle
@@ -52,30 +54,8 @@ query GetLoyaltyPage($locale: String!, $uid: String!) {
}
}
}
... on LoyaltyPageBlocksCardGrid {
card_grid {
title
subtitle
cards {
referenceConnection {
edges {
node {
__typename
...LoyaltyPageLink
...ContentPageLink
...AccountPageLink
}
}
totalCount
}
title
subtitle
open_in_new_tab
cta_text
}
}
}
... on LoyaltyPageBlocksContent {
__typename
content {
content {
json
@@ -83,7 +63,6 @@ query GetLoyaltyPage($locale: String!, $uid: String!) {
edges {
node {
__typename
...Image
...LoyaltyPageLink
...ContentPageLink
}
@@ -93,6 +72,22 @@ query GetLoyaltyPage($locale: String!, $uid: String!) {
}
}
}
... on LoyaltyPageBlocksCardsGrid {
__typename
cards_grid {
title
preamble
layout
theme
cardConnection(limit: 10) {
edges {
node {
...CardBlock
}
}
}
}
}
}
title
heading
@@ -185,23 +180,6 @@ query GetLoyaltyPageRefs($locale: String!, $uid: String!) {
}
}
}
... on LoyaltyPageBlocksCardGrid {
__typename
card_grid {
cards {
referenceConnection {
edges {
node {
__typename
...AccountPageRef
...ContentPageRef
...LoyaltyPageRef
}
}
}
}
}
}
... on LoyaltyPageBlocksContent {
__typename
content {
@@ -228,6 +206,18 @@ query GetLoyaltyPageRefs($locale: String!, $uid: String!) {
}
}
}
... on LoyaltyPageBlocksCardsGrid {
__typename
cards_grid {
cardConnection(limit: 10) {
edges {
node {
...CardBlockRef
}
}
}
}
}
}
sidebar {
... on LoyaltyPageSidebarContent {

View File

@@ -13,37 +13,28 @@ import { PageLinkEnum } from "@/types/requests/pageLinks"
import { EdgesWithTotalCount } from "@/types/requests/utils/edges"
import { RTEDocument } from "@/types/rte/node"
const loyaltyPageBlockCardGrid = z.object({
__typename: z.literal(LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardGrid),
card_grid: z.object({
title: z.string().nullable(),
subtitle: z.string().nullable(),
cards: z.array(
z.object({
title: z.string().nullable(),
subtitle: z.string().nullable(),
referenceConnection: z.object({
edges: z.array(
z.object({
node: z.object({
system: z.object({
uid: z.string(),
locale: z.nativeEnum(Lang),
}),
url: z.string(),
title: z.string(),
__typename: z.string(),
}),
})
),
totalCount: z.number(),
const pageLink = z.object({
edges: z.array(
z.object({
node: z.object({
system: z.object({
uid: z.string(),
locale: z.nativeEnum(Lang),
}),
open_in_new_tab: z.boolean(),
cta_text: z.string().nullable(),
})
),
}),
web: z
.object({
original_url: z.string().nullable(),
})
.nullable()
.optional(),
url: z.string(),
title: z.string(),
__typename: z.string().optional(),
}),
})
),
})
const loyaltyPageDynamicContent = z.object({
__typename: z.literal(
LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksDynamicContent
@@ -54,21 +45,7 @@ const loyaltyPageDynamicContent = z.object({
component: z.nativeEnum(LoyaltyComponentEnum),
link: z.object({
text: z.string().nullable(),
pageConnection: z.object({
edges: z.array(
z.object({
node: z.object({
system: z.object({
uid: z.string(),
locale: z.nativeEnum(Lang),
}),
url: z.string(),
title: z.string(),
}),
})
),
totalCount: z.number(),
}),
pageConnection: pageLink,
}),
}),
})
@@ -80,26 +57,7 @@ const loyaltyPageShortcuts = z.object({
preamble: z.string().nullable(),
shortcuts: z.array(
z.object({
linkConnection: z.object({
edges: z.array(
z.object({
node: z.object({
system: z.object({
uid: z.string(),
locale: z.nativeEnum(Lang),
}),
url: z.string(),
web: z
.object({
original_url: z.string().nullable(),
})
.nullable(),
title: z.string(),
}),
})
),
totalCount: z.number(),
}),
linkConnection: pageLink,
text: z.string().nullable(),
open_in_new_tab: z.boolean(),
})
@@ -107,6 +65,59 @@ const loyaltyPageShortcuts = z.object({
}),
})
const cardBlock = z.object({
heading: z.string().nullable(),
body_text: z.string().nullable(),
background_image: z.any(),
scripted_top_title: z.string().nullable(),
primary_button: z
.object({
is_contentstack_link: z.boolean(),
cta_text: z.string().nullable(),
open_in_new_tab: z.boolean(),
external_link: z.object({
title: z.string().nullable(),
href: z.string().nullable(),
}),
linkConnection: pageLink,
})
.nullable(),
secondary_button: z
.object({
is_contentstack_link: z.boolean(),
cta_text: z.string().nullable(),
open_in_new_tab: z.boolean().nullable(),
external_link: z.object({
title: z.string().nullable(),
href: z.string().nullable(),
}),
linkConnection: pageLink,
})
.nullable(),
system: z.object({
locale: z.nativeEnum(Lang),
uid: z.string(),
}),
})
const loyaltyPageCards = z.object({
__typename: z.literal(LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardsGrid),
cards_grid: z.object({
title: z.string().nullable(),
preamble: z.string().nullable(),
layout: z.enum(["twoColumnGrid", "threeColumnGrid", "twoPlusOne"]),
theme: z.enum(["one", "two", "three"]).nullable(),
cardConnection: z.object({
edges: z.array(
z.object({
node: cardBlock,
})
),
}),
}),
})
const loyaltyPageBlockTextContent = z.object({
__typename: z.literal(LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksContent),
content: z.object({
@@ -121,10 +132,10 @@ const loyaltyPageBlockTextContent = z.object({
})
const loyaltyPageBlockItem = z.discriminatedUnion("__typename", [
loyaltyPageBlockCardGrid,
loyaltyPageDynamicContent,
loyaltyPageBlockTextContent,
loyaltyPageShortcuts,
loyaltyPageCards,
])
const loyaltyPageSidebarTextContent = z.object({
@@ -154,7 +165,6 @@ const loyaltyPageJoinLoyaltyContact = z.object({
),
contact: z.object({
display_text: z.string().nullable(),
contact_field: z.string(),
}),
})
@@ -178,26 +188,6 @@ export const validateLoyaltyPageSchema = z.object({
})
// Block types
type CardGridRaw = z.infer<typeof loyaltyPageBlockCardGrid>
export type CardGridCard = Omit<
CardGridRaw["card_grid"]["cards"][number],
"referenceConnection"
> & {
link:
| {
href: string
title: string
}
| undefined
}
export type CardGrid = Omit<CardGridRaw, "card_grid"> & {
card_grid: Omit<CardGridRaw["card_grid"], "cards"> & {
cards: CardGridCard[]
}
}
type DynamicContentRaw = z.infer<typeof loyaltyPageDynamicContent>
export type DynamicContent = Omit<DynamicContentRaw, "dynamic_content"> & {
@@ -222,6 +212,34 @@ export interface RteBlockContent extends BlockContentRaw {
}
}
type CardsGridRaw = z.infer<typeof loyaltyPageCards>
export type CardsRaw =
CardsGridRaw["cards_grid"]["cardConnection"]["edges"][number]["node"]
export type CardsGrid = Omit<CardsGridRaw, "cards_grid"> & {
cards_grid: Omit<CardsGridRaw["cards_grid"], "cardConnection"> & {
cards: (Omit<CardsRaw, "primaryButton" | "secondaryButton"> & {
primaryButton:
| {
openInNewTab: boolean
title: string
href: string
isExternal: boolean
}
| undefined
secondaryButton:
| {
openInNewTab: boolean
title: string
href: string
isExternal: boolean
}
| undefined
})[]
}
}
type ShortcutsRaw = z.infer<typeof loyaltyPageShortcuts>
export type Shortcuts = Omit<ShortcutsRaw, "shortcuts"> & {
@@ -235,7 +253,7 @@ export type Shortcuts = Omit<ShortcutsRaw, "shortcuts"> & {
}
}
export type Block = CardGrid | RteBlockContent | DynamicContent | Shortcuts
export type Block = RteBlockContent | DynamicContent | Shortcuts | CardsGrid
// Sidebar block types
type SidebarContentRaw = z.infer<typeof loyaltyPageSidebarTextContent>
@@ -275,14 +293,34 @@ const pageConnectionRefs = z.object({
),
})
const loyaltyPageBlockCardGridRefs = z.object({
__typename: z.literal(LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardGrid),
card_grid: z.object({
cards: z.array(
z.object({
referenceConnection: pageConnectionRefs,
})
),
const cardBlockRefs = z.object({
primary_button: z
.object({
linkConnection: pageConnectionRefs,
})
.nullable(),
secondary_button: z
.object({
linkConnection: pageConnectionRefs,
})
.nullable(),
system: z.object({
content_type_uid: z.string(),
uid: z.string(),
}),
})
const loyaltyPageCardsRefs = z.object({
__typename: z.literal(LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardsGrid),
cards_grid: z.object({
cardConnection: z.object({
edges: z.array(
z.object({
node: cardBlockRefs,
})
),
}),
}),
})
@@ -318,10 +356,10 @@ const loyaltyPageBlockTextContentRefs = z.object({
})
const loyaltyPageBlocRefsItem = z.discriminatedUnion("__typename", [
loyaltyPageBlockCardGridRefs,
loyaltyPageDynamicContentRefs,
loyaltyPageBlockTextContentRefs,
loyaltyPageShortcutsRefs,
loyaltyPageCardsRefs,
])
const loyaltyPageSidebarTextContentRef = z.object({

View File

@@ -5,7 +5,7 @@ import {
import { request } from "@/lib/graphql/request"
import { _ } from "@/lib/translation"
import { internalServerError, notFound } from "@/server/errors/trpc"
import { contentstackProcedure, publicProcedure, router } from "@/server/trpc"
import { contentstackProcedure, router } from "@/server/trpc"
import {
generateRefsResponseTag,
@@ -15,6 +15,7 @@ import {
import { removeEmptyObjects } from "../../utils"
import {
CardsRaw,
type LoyaltyPage,
type LoyaltyPageDataRaw,
type LoyaltyPageRefsDataRaw,
@@ -31,6 +32,26 @@ import { Embeds } from "@/types/requests/embeds"
import { Edges } from "@/types/requests/utils/edges"
import { RTEDocument } from "@/types/rte/node"
function makeButtonObject(
button: CardsRaw["primary_button" | "secondary_button"]
) {
if (!button) return undefined
return {
openInNewTab: button.open_in_new_tab,
title:
button.cta_text ||
(button.is_contentstack_link && button.linkConnection.edges.length
? button.linkConnection.edges[0].node.title
: button.external_link.title),
href:
button.is_contentstack_link && button.linkConnection.edges.length
? button.linkConnection.edges[0].node.web?.original_url ||
`/${button.linkConnection.edges[0].node.system.locale}${button.linkConnection.edges[0].node.url}`
: button.external_link.href,
isExternal: !button.is_contentstack_link,
}
}
export const loyaltyPageQueryRouter = router({
get: contentstackProcedure.query(async ({ ctx }) => {
const { lang, uid } = ctx
@@ -60,6 +81,8 @@ export const loyaltyPageQueryRouter = router({
const validatedLoyaltyPageRefs =
validateLoyaltyPageRefsSchema.safeParse(cleanedData)
if (!validatedLoyaltyPageRefs.success) {
console.error("Bad validation for `GetLoyaltyPageRefs`")
console.error(validatedLoyaltyPageRefs.error)
throw internalServerError(validatedLoyaltyPageRefs.error)
}
@@ -114,30 +137,12 @@ export const loyaltyPageQueryRouter = router({
const blocks = validatedLoyaltyPage.data.loyalty_page.blocks
? validatedLoyaltyPage.data.loyalty_page.blocks.map((block) => {
switch (block.__typename) {
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardGrid:
return {
...block,
card_grid: {
...block.card_grid,
cards: block.card_grid.cards.map((card) => {
return {
...card,
link: card.referenceConnection.totalCount
? {
href: `/${card.referenceConnection.edges[0].node.system.locale}${card.referenceConnection.edges[0].node.url}`,
title: card.cta_text || _("Read more"),
}
: undefined,
}
}),
},
}
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksDynamicContent:
return {
...block,
dynamic_content: {
...block.dynamic_content,
link: block.dynamic_content.link.pageConnection.totalCount
link: block.dynamic_content.link.pageConnection.edges.length
? {
text: block.dynamic_content.link.text,
href: `/${block.dynamic_content.link.pageConnection.edges[0].node.system.locale}${block.dynamic_content.link.pageConnection.edges[0].node.url}`,
@@ -174,6 +179,24 @@ export const loyaltyPageQueryRouter = router({
})),
},
}
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardsGrid:
return {
...block,
cards_grid: {
...block.cards_grid,
cards: block.cards_grid.cardConnection.edges.map(
({ node: card }) => {
return {
...card,
primaryButton: makeButtonObject(card.primary_button),
secondaryButton: makeButtonObject(
card.secondary_button
),
}
}
),
},
}
default:
return block
}

View File

@@ -15,12 +15,16 @@ export function getConnections(refs: LoyaltyPageRefsDataRaw) {
}
break
}
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardGrid: {
item.card_grid.cards.forEach((card) => {
if (card.referenceConnection.edges.length) {
connections.push(card.referenceConnection)
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardsGrid: {
connections.push(item.cards_grid.cardConnection)
item.cards_grid.cardConnection.edges.forEach((card) => {
if (card.node.primary_button) {
connections.push(card.node.primary_button?.linkConnection)
} else if (card.node.secondary_button) {
connections.push(card.node.secondary_button?.linkConnection)
}
})
break
}
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksShortcuts: {

View File

@@ -1,6 +1,6 @@
import {
Block,
CardGrid,
CardsGrid,
DynamicContent,
RteBlockContent,
} from "@/server/routers/contentstack/loyaltyPage/output"
@@ -17,7 +17,7 @@ export type DynamicComponentProps = {
component: DynamicContent["dynamic_content"]["component"]
}
export type CardGridProps = Pick<CardGrid, "card_grid">
export type CardsGridProps = Pick<CardsGrid, "cards_grid">
export type Content = { content: RteBlockContent["content"]["content"] }

View File

@@ -27,7 +27,7 @@ export type LoyaltyComponent = keyof typeof LoyaltyComponentEnum
export enum LoyaltyBlocksTypenameEnum {
LoyaltyPageBlocksDynamicContent = "LoyaltyPageBlocksDynamicContent",
LoyaltyPageBlocksCardGrid = "LoyaltyPageBlocksCardGrid",
LoyaltyPageBlocksContent = "LoyaltyPageBlocksContent",
LoyaltyPageBlocksShortcuts = "LoyaltyPageBlocksShortcuts",
LoyaltyPageBlocksCardsGrid = "LoyaltyPageBlocksCardsGrid",
}