fix: add loyaltyCard

This commit is contained in:
Christel Westerberg
2024-06-28 11:21:09 +02:00
parent 5be118d9e5
commit 323df671d8
23 changed files with 467 additions and 106 deletions

View File

@@ -8,7 +8,7 @@
.blocks {
display: grid;
gap: var(--Spacing-x5);
gap: var(--Spacing-x7);
padding-left: var(--Spacing-x2);
padding-right: var(--Spacing-x2);
}
@@ -29,6 +29,7 @@
}
.blocks {
gap: var(--Spacing-x9);
padding-left: var(--Spacing-x0);
padding-right: var(--Spacing-x0);
}

View File

@@ -2,25 +2,50 @@ import SectionContainer from "@/components/Section/Container"
import Header from "@/components/Section/Header"
import Card from "@/components/TempDesignSystem/Card"
import Grids from "@/components/TempDesignSystem/Grids"
import LoyaltyCard from "@/components/TempDesignSystem/LoyaltyCard"
import { CardsGridProps } from "@/types/components/loyalty/blocks"
import { LoyaltyCardsGridEnum } from "@/types/components/loyalty/enums"
export default function CardsGrid({ cards_grid }: CardsGridProps) {
export default function CardsGrid({
cards_grid,
firstItem = false,
}: CardsGridProps) {
return (
<SectionContainer>
<Header title={cards_grid.title} subtitle={cards_grid.preamble} />
<Header
title={cards_grid.title}
subtitle={cards_grid.preamble}
topTitle={firstItem}
/>
<Grids.Stackable>
{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}
/>
))}
{cards_grid.cards.map((card) => {
switch (card.__typename) {
case LoyaltyCardsGridEnum.LoyaltyCard:
return (
<LoyaltyCard
key={card.system.uid}
image={card.image}
heading={card.heading}
bodyText={card.body_text}
link={card.link}
/>
)
case LoyaltyCardsGridEnum.Card: {
return (
<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}
/>
)
}
}
})}
</Grids.Stackable>
</SectionContainer>
)

View File

@@ -16,7 +16,7 @@ import {
TrueFriend,
} from "@/components/Levels"
import BiroScript from "@/components/TempDesignSystem/Text/BiroScript"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Title from "@/components/TempDesignSystem/Text/Title"
import levelsData from "./data"
@@ -97,7 +97,8 @@ function LevelCard({ formatMessage, lang, level }: LevelCardProps) {
</Title>
<div className={styles.textContainer}>
{level.benefits.map((benefit) => (
<Footnote
<Caption
className={styles.levelText}
key={benefit.title}
textAlign="center"
color="textMediumContrast"
@@ -107,7 +108,7 @@ function LevelCard({ formatMessage, lang, level }: LevelCardProps) {
color="primaryLightOnSurfaceAccent"
/>
{benefit.title}
</Footnote>
</Caption>
))}
</div>
</article>

View File

@@ -20,9 +20,9 @@
display: grid;
gap: var(--Spacing-x2);
min-height: 280px;
justify-content: center;
justify-items: center;
padding: var(--Spacing-x5) var(--Spacing-x1);
grid-template-rows: auto auto 1fr;
}
.textContainer {
@@ -34,6 +34,10 @@
justify-content: center;
}
.levelText {
margin: 0;
}
.checkIcon {
vertical-align: middle;
}

View File

@@ -43,6 +43,7 @@ export function Blocks({ blocks }: BlocksProps) {
<CardsGrid
cards_grid={block.cards_grid}
key={`${block.cards_grid.title}-${idx}`}
firstItem={firstItem}
/>
)
default:

View File

@@ -1,11 +1,15 @@
.aside {
align-content: flex-start;
display: grid;
gap: var(--Spacing-x4);
display: none;
}
@media screen and (max-width: 1366px) {
.content {
padding: var(--Spacing-x0) var(--Spacing-x2);
.content {
padding: var(--Spacing-x0) var(--Spacing-x2);
}
@media screen and (min-width: 1366px) {
.aside {
align-content: flex-start;
display: grid;
gap: var(--Spacing-x4);
}
}

View File

@@ -0,0 +1,65 @@
import ArrowRight from "@/components/Icons/ArrowRight"
import Image from "@/components/Image"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Title from "@/components/TempDesignSystem/Text/Title"
import { loyaltyCardVariants } from "./variants"
import styles from "./loyaltyCard.module.css"
import type { LoyaltyCardProps } from "./loyaltyCard"
export default function LoyaltyCard({
link,
image,
heading,
bodyText,
theme = "white",
className,
}: LoyaltyCardProps) {
return (
<article
className={loyaltyCardVariants({
className,
theme,
})}
>
{image ? (
<section>
<Image
src={image.url}
width={180}
height={160}
className={styles.image}
alt={image.meta.alt || image.title}
/>
</section>
) : null}
<Title as="h5" level="h3" textAlign="center">
{heading}
</Title>
{bodyText ? (
<Body textAlign="center" color="red">
{bodyText}
</Body>
) : null}
<div className={styles.buttonContainer}>
{link ? (
<span className={styles.linkWrapper}>
<ArrowRight color="burgundy" className={styles.icon} />
<Link
className={styles.link}
color="burgundy"
href={link.href}
target={link.openInNewTab ? "_blank" : undefined}
variant="myPage"
>
{link.title}
</Link>
</span>
) : null}
</div>
</article>
)
}

View File

@@ -0,0 +1,43 @@
.container {
align-items: center;
display: grid;
border-radius: var(--Corner-radius-xLarge);
gap: var(--Spacing-x2);
height: 480px;
justify-content: space-between;
margin-right: var(--Spacing-x2);
padding: var(--Spacing-x4) var(--Spacing-x3);
text-align: center;
width: 100%;
}
.image {
object-fit: contain;
height: 160px;
width: auto;
}
.white {
background-color: var(--Main-Grey-White);
}
.buttonContainer {
display: flex;
gap: var(--Spacing-x1);
justify-content: center;
}
.linkWrapper {
display: flex;
align-items: baseline;
gap: var(--Spacing-x-half);
}
.link {
display: flex;
align-items: center;
}
.icon {
align-self: center;
}

View File

@@ -0,0 +1,20 @@
import { loyaltyCardVariants } from "./variants"
import type { VariantProps } from "class-variance-authority"
import { ImageVaultAsset } from "@/types/components/imageVaultImage"
export interface LoyaltyCardProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof loyaltyCardVariants> {
link?: {
href: string
title: string
openInNewTab?: boolean
isExternal: boolean
}
image?: ImageVaultAsset
heading?: string | null
bodyText?: string | null
backgroundImage?: { url: string }
}

View File

@@ -0,0 +1,14 @@
import { cva } from "class-variance-authority"
import styles from "./loyaltyCard.module.css"
export const loyaltyCardVariants = cva(styles.container, {
variants: {
theme: {
white: styles.white,
},
},
defaultVariants: {
theme: "white",
},
})

View File

@@ -36,3 +36,15 @@
.pale {
color: var(--Scandic-Brand-Pale-Peach);
}
.textMediumContrast {
color: var(--Base-Text-UI-Medium-contrast);
}
.center {
text-align: center;
}
.left {
text-align: left;
}

View File

@@ -9,19 +9,21 @@ export default function Caption({
className = "",
color,
fontOnly = false,
textAlign,
textTransform,
...props
}: CaptionProps) {
const Comp = asChild ? Slot : "p"
const classNames = fontOnly
? fontOnlycaptionVariants({
className,
textTransform,
})
className,
textTransform,
})
: captionVariants({
className,
color,
textTransform,
})
className,
color,
textTransform,
textAlign,
})
return <Comp className={classNames} {...props} />
}

View File

@@ -8,11 +8,16 @@ const config = {
black: styles.black,
burgundy: styles.burgundy,
pale: styles.pale,
textMediumContrast: styles.textMediumContrast,
},
textTransform: {
bold: styles.bold,
regular: styles.regular,
},
textAlign: {
center: styles.center,
left: styles.left,
},
},
defaultVariants: {
color: "black",

View File

@@ -0,0 +1,28 @@
fragment LoyaltyCardBlock on LoyaltyCard {
heading
body_text
image
title
link {
cta_text
open_in_new_tab
external_link {
title
href
}
linkConnection {
edges {
node {
__typename
...LoyaltyPageLink
...ContentPageLink
...AccountPageLink
}
}
}
}
system {
locale
uid
}
}

View File

@@ -1,4 +1,5 @@
fragment CardBlockRef on Card {
__typename
secondary_button {
linkConnection {
edges {

View File

@@ -0,0 +1,18 @@
fragment LoyaltyCardBlockRef on LoyaltyCard {
__typename
link {
linkConnection {
edges {
node {
__typename
...LoyaltyPageRef
...ContentPageRef
...AccountPageRef
}
}
}
}
system {
...System
}
}

View File

@@ -1,6 +1,10 @@
#import "../Fragments/Image.graphql"
#import "../Fragments/Blocks/Card.graphql"
#import "../Fragments/Blocks/LoyaltyCard.graphql"
#import "../Fragments/Blocks/Refs/Card.graphql"
#import "../Fragments/Blocks/Refs/LoyaltyCard.graphql"
#import "../Fragments/LoyaltyPage/Breadcrumbs.graphql"
#import "../Fragments/PageLink/AccountPageLink.graphql"
#import "../Fragments/PageLink/ContentPageLink.graphql"
@@ -83,7 +87,9 @@ query GetLoyaltyPage($locale: String!, $uid: String!) {
cardConnection(limit: 10) {
edges {
node {
__typename
...CardBlock
...LoyaltyCardBlock
}
}
}
@@ -104,6 +110,7 @@ query GetLoyaltyPage($locale: String!, $uid: String!) {
contact {
display_text
contact_field
footnote
}
}
}
@@ -206,6 +213,7 @@ query GetLoyaltyPageRefs($locale: String!, $uid: String!) {
edges {
node {
...CardBlockRef
...LoyaltyCardBlockRef
}
}
}

View File

@@ -59,6 +59,7 @@ export type ContactConfig = ContactConfigData["all_contact_config"]["items"][0]
export type ContactFields = {
display_text: string | null
contact_field: string
footnote: string | null
}
export const validateHeaderConfigSchema = z.object({

View File

@@ -2,9 +2,11 @@ import { z } from "zod"
import { Lang } from "@/constants/languages"
import { ImageVaultAsset } from "@/types/components/imageVaultImage"
import {
JoinLoyaltyContactTypenameEnum,
LoyaltyBlocksTypenameEnum,
LoyaltyCardsGridEnum,
LoyaltyComponentEnum,
SidebarTypenameEnum,
} from "@/types/components/loyalty/enums"
@@ -47,6 +49,7 @@ const loyaltyPageShortcuts = z.object({
})
const cardBlock = z.object({
__typename: z.literal(LoyaltyCardsGridEnum.Card),
heading: z.string().nullable(),
body_text: z.string().nullable(),
background_image: z.any(),
@@ -73,6 +76,30 @@ const cardBlock = z.object({
}),
})
const loyaltyCardBlock = z.object({
__typename: z.literal(LoyaltyCardsGridEnum.LoyaltyCard),
heading: z.string().nullable(),
body_text: z.string().nullable(),
image: z.any(),
link: z
.object({
openInNewTab: z.boolean(),
title: z.string(),
href: z.string(),
isExternal: z.boolean(),
})
.optional(),
system: z.object({
locale: z.nativeEnum(Lang),
uid: z.string(),
}),
})
const loyaltyPageCardsItems = z.discriminatedUnion("__typename", [
loyaltyCardBlock,
cardBlock,
])
const loyaltyPageCards = z.object({
__typename: z.literal(LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardsGrid),
cards_grid: z.object({
@@ -80,7 +107,7 @@ const loyaltyPageCards = z.object({
preamble: z.string().nullable(),
layout: z.enum(["twoColumnGrid", "threeColumnGrid", "twoPlusOne"]),
theme: z.enum(["one", "two", "three"]).nullable(),
cards: z.array(cardBlock),
cards: z.array(loyaltyPageCardsItems),
}),
})
@@ -132,6 +159,7 @@ const loyaltyPageJoinLoyaltyContact = z.object({
contact: z.object({
display_text: z.string().nullable(),
contact_field: z.string(),
footnote: z.string().nullable(),
}),
})
),
@@ -151,7 +179,6 @@ export const validateLoyaltyPageSchema = z.object({
})
// Block types
export type DynamicContent = z.infer<typeof loyaltyPageDynamicContent>
type BlockContentRaw = z.infer<typeof loyaltyPageBlockTextContent>
@@ -164,11 +191,25 @@ export interface RteBlockContent extends BlockContentRaw {
}
}
type LoyaltyCardRaw = z.infer<typeof loyaltyCardBlock>
type LoyaltyCard = Omit<LoyaltyCardRaw, "image"> & {
image?: ImageVaultAsset
}
type CardRaw = z.infer<typeof cardBlock>
type Card = Omit<CardRaw, "background_image"> & {
backgroundImage?: ImageVaultAsset
}
type CardsGridRaw = z.infer<typeof loyaltyPageCards>
export type CardsRaw = CardsGridRaw["cards_grid"]["cards"][number]
export type CardsGrid = Omit<CardsGridRaw, "cards"> & {
cards: (LoyaltyCard | Card)[]
}
export type CardsGrid = z.infer<typeof loyaltyPageCards>
export type CardsRaw = CardsGrid["cards_grid"]["cards"][number]
export type Shortcuts = z.infer<typeof loyaltyPageShortcuts>
@@ -210,6 +251,7 @@ const pageConnectionRefs = z.object({
})
const cardBlockRefs = z.object({
__typename: z.literal(LoyaltyCardsGridEnum.Card),
primary_button: z
.object({
linkConnection: pageConnectionRefs,
@@ -227,13 +269,32 @@ const cardBlockRefs = z.object({
}),
})
const loyaltyCardBlockRefs = z.object({
__typename: z.literal(LoyaltyCardsGridEnum.LoyaltyCard),
link: z
.object({
linkConnection: pageConnectionRefs,
})
.nullable(),
system: z.object({
content_type_uid: z.string(),
uid: z.string(),
}),
})
const cardGridCardsRef = z.discriminatedUnion("__typename", [
loyaltyCardBlockRefs,
cardBlockRefs,
])
const loyaltyPageCardsRefs = z.object({
__typename: z.literal(LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardsGrid),
cards_grid: z.object({
cardConnection: z.object({
edges: z.array(
z.object({
node: cardBlockRefs,
node: cardGridCardsRef,
})
),
}),

View File

@@ -11,6 +11,7 @@ import {
generateTag,
generateTags,
} from "@/utils/generateTag"
import { insertResponseToImageVaultAsset } from "@/utils/imageVault"
import { removeMultipleSlashes } from "@/utils/url"
import { removeEmptyObjects } from "../../utils"
@@ -22,7 +23,17 @@ import {
} from "./output"
import { getConnections } from "./utils"
import { LoyaltyBlocksTypenameEnum } from "@/types/components/loyalty/enums"
import { InsertResponse } from "@/types/components/imageVaultImage"
import {
LoyaltyBlocksTypenameEnum,
LoyaltyCardsGridEnum,
} from "@/types/components/loyalty/enums"
function makeImageVaultImage(image: any) {
return image && !!Object.keys(image).length
? insertResponseToImageVaultAsset(image as InsertResponse)
: undefined
}
function makeButtonObject(button: any) {
return {
@@ -35,9 +46,9 @@ function makeButtonObject(button: any) {
href:
button.is_contentstack_link && button.linkConnection.edges.length
? button.linkConnection.edges[0].node.web?.original_url ||
removeMultipleSlashes(
`/${button.linkConnection.edges[0].node.system.locale}/${button.linkConnection.edges[0].node.url}`
)
removeMultipleSlashes(
`/${button.linkConnection.edges[0].node.system.locale}/${button.linkConnection.edges[0].node.url}`
)
: button.external_link.href,
isExternal: !button.is_contentstack_link,
}
@@ -96,66 +107,82 @@ export const loyaltyPageQueryRouter = router({
const blocks = response.data.loyalty_page.blocks
? response.data.loyalty_page.blocks.map((block: any) => {
switch (block.__typename) {
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksDynamicContent:
return {
...block,
dynamic_content: {
...block.dynamic_content,
link: block.dynamic_content.link.pageConnection.edges.length
? {
text: block.dynamic_content.link.text,
href: removeMultipleSlashes(
`/${block.dynamic_content.link.pageConnection.edges[0].node.system.locale}/${block.dynamic_content.link.pageConnection.edges[0].node.url}`
),
title:
block.dynamic_content.link.pageConnection.edges[0]
.node.title,
}
: undefined,
},
}
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksShortcuts:
return {
...block,
shortcuts: {
...block.shortcuts,
shortcuts: block.shortcuts.shortcuts.map((shortcut: any) => ({
text: shortcut.text,
openInNewTab: shortcut.open_in_new_tab,
...shortcut.linkConnection.edges[0].node,
url:
shortcut.linkConnection.edges[0].node.web?.original_url ||
removeMultipleSlashes(
`/${shortcut.linkConnection.edges[0].node.system.locale}/${shortcut.linkConnection.edges[0].node.url}`
),
})),
},
}
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardsGrid:
return {
...block,
cards_grid: {
...block.cards_grid,
cards: block.cards_grid.cardConnection.edges.map(
({ node: card }: { node: any }) => {
return {
...card,
primaryButton: card.has_primary_button
? makeButtonObject(card.primary_button)
: undefined,
secondaryButton: card.has_secondary_button
? makeButtonObject(card.secondary_button)
: undefined,
switch (block.__typename) {
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksDynamicContent:
return {
...block,
dynamic_content: {
...block.dynamic_content,
link: block.dynamic_content.link.pageConnection.edges.length
? {
text: block.dynamic_content.link.text,
href: removeMultipleSlashes(
`/${block.dynamic_content.link.pageConnection.edges[0].node.system.locale}/${block.dynamic_content.link.pageConnection.edges[0].node.url}`
),
title:
block.dynamic_content.link.pageConnection.edges[0]
.node.title,
}
: undefined,
},
}
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksShortcuts:
return {
...block,
shortcuts: {
...block.shortcuts,
shortcuts: block.shortcuts.shortcuts.map((shortcut: any) => ({
text: shortcut.text,
openInNewTab: shortcut.open_in_new_tab,
...shortcut.linkConnection.edges[0].node,
url:
shortcut.linkConnection.edges[0].node.web?.original_url ||
removeMultipleSlashes(
`/${shortcut.linkConnection.edges[0].node.system.locale}/${shortcut.linkConnection.edges[0].node.url}`
),
})),
},
}
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardsGrid:
return {
...block,
cards_grid: {
...block.cards_grid,
cards: block.cards_grid.cardConnection.edges.map(
({ node: card }: { node: any }) => {
switch (card.__typename) {
case LoyaltyCardsGridEnum.LoyaltyCard:
return {
...card,
image: makeImageVaultImage(card.image),
link: makeButtonObject({
...card.link,
is_contentstack_link:
!!card.link.linkConnection.edges.length,
}),
}
case LoyaltyCardsGridEnum.Card:
return {
...card,
backgroundImage: makeImageVaultImage(
card.background_image
),
primaryButton: card.has_primary_button
? makeButtonObject(card.primary_button)
: undefined,
secondaryButton: card.has_secondary_button
? makeButtonObject(card.secondary_button)
: undefined,
}
}
}
}
),
},
}
default:
return block
}
})
),
},
}
default:
return block
}
})
: null
const loyaltyPage = {

View File

@@ -1,6 +1,9 @@
import { LoyaltyPageRefsDataRaw } from "./output"
import { LoyaltyBlocksTypenameEnum } from "@/types/components/loyalty/enums"
import {
LoyaltyBlocksTypenameEnum,
LoyaltyCardsGridEnum,
} from "@/types/components/loyalty/enums"
import type { Edges } from "@/types/requests/utils/edges"
import type { NodeRefs } from "@/types/requests/utils/refs"
@@ -18,13 +21,23 @@ export function getConnections(refs: LoyaltyPageRefsDataRaw) {
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)
switch (card.node.__typename) {
case LoyaltyCardsGridEnum.LoyaltyCard: {
if (card.node.link) {
connections.push(card.node.link?.linkConnection)
}
break
}
case LoyaltyCardsGridEnum.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
}
}
})
break
}
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksShortcuts: {

View File

@@ -24,7 +24,9 @@ export type DynamicComponentProps = {
component: DynamicContent["dynamic_content"]["component"]
}
export type CardsGridProps = Pick<CardsGrid, "cards_grid">
export type CardsGridProps = Pick<CardsGrid, "cards_grid"> & {
firstItem?: boolean
}
export type Content = { content: RteBlockContent["content"]["content"] }

View File

@@ -31,3 +31,8 @@ export enum LoyaltyBlocksTypenameEnum {
LoyaltyPageBlocksShortcuts = "LoyaltyPageBlocksShortcuts",
LoyaltyPageBlocksCardsGrid = "LoyaltyPageBlocksCardsGrid",
}
export enum LoyaltyCardsGridEnum {
LoyaltyCard = "LoyaltyCard",
Card = "Card",
}