feat: add card grid component

This commit is contained in:
Christel Westerberg
2024-04-26 08:19:19 +02:00
parent 2ddabf1e50
commit 00f30811cf
33 changed files with 575 additions and 121 deletions

View File

@@ -1,10 +1,7 @@
.layout { .layout {
--max-width: 101.4rem; --max-width: 101.4rem;
--header-height: 4.5rem;
display: grid; display: grid;
font-family: var(--ff-fira-sans); font-family: var(--ff-fira-sans);
grid-template-rows: var(--header-height) auto 1fr;
min-height: 100dvh;
background-color: var(--Brand-Coffee-Subtle); background-color: var(--Brand-Coffee-Subtle);
} }

View File

@@ -1,20 +1,16 @@
import { firaMono, firaSans } from "@/app/[lang]/(live)/fonts" import { firaMono, firaSans } from "@/app/[lang]/(live)/fonts"
import Header from "@/components/MyPages/Header"
import styles from "./layout.module.css" import styles from "./layout.module.css"
import type { MyPagesLayoutProps } from "@/types/components/myPages/layout" import type { MyPagesLayoutProps } from "@/types/components/myPages/layout"
export default async function LoyaltyPagesLayout({ export default async function LoyaltyPagesLayout({
children, children,
params,
}: React.PropsWithChildren<MyPagesLayoutProps>) { }: React.PropsWithChildren<MyPagesLayoutProps>) {
return ( return (
<div <div
className={`${firaMono.variable} ${firaSans.variable} ${styles.layout}`} className={`${firaMono.variable} ${firaSans.variable} ${styles.layout}`}
> >
<Header lang={params.lang} />
{children} {children}
</div> </div>
) )

View File

@@ -9,8 +9,7 @@
.blocks { .blocks {
display: grid; display: grid;
gap: 4.2rem; gap: 4.2rem;
padding-left: 2rem; padding: 1.6rem;
padding-right: 2rem;
} }
@media screen and (min-width: 950px) { @media screen and (min-width: 950px) {

View File

@@ -1,14 +1,15 @@
import { notFound } from "next/navigation" import { notFound } from "next/navigation"
import { serverClient } from "@/lib/trpc/server" import { serverClient } from "@/lib/trpc/server"
import MaxWidth from "@/components/MaxWidth"
import Sidebar from "@/components/Loyalty/Sidebar"
import { Blocks } from "@/components/Loyalty/Blocks" import { Blocks } from "@/components/Loyalty/Blocks"
import Sidebar from "@/components/Loyalty/Sidebar"
import type { LangParams, PageArgs, UriParams } from "@/types/params" import MaxWidth from "@/components/MaxWidth"
import styles from "./page.module.css" import styles from "./page.module.css"
import type { LangParams, PageArgs, UriParams } from "@/types/params"
export default async function LoyaltyPage({ export default async function LoyaltyPage({
params, params,
searchParams, searchParams,
@@ -19,12 +20,12 @@ export default async function LoyaltyPage({
} }
const loyaltyPage = await serverClient().contentstack.loyaltyPage.get({ const loyaltyPage = await serverClient().contentstack.loyaltyPage.get({
uri: searchParams.uri, href: searchParams.uri,
lang: params.lang, locale: params.lang,
}) })
return ( return (
<MaxWidth className={styles.content} tag="main"> <section className={styles.content}>
<aside> <aside>
{loyaltyPage.sidebar {loyaltyPage.sidebar
? loyaltyPage.sidebar.map((block, i) => ( ? loyaltyPage.sidebar.map((block, i) => (
@@ -32,10 +33,10 @@ export default async function LoyaltyPage({
)) ))
: null} : null}
</aside> </aside>
<section className={styles.blocks}> <MaxWidth className={styles.blocks} tag="main">
<Blocks blocks={loyaltyPage.blocks} /> <Blocks blocks={loyaltyPage.blocks} />
</section> </MaxWidth>
</MaxWidth> </section>
) )
} catch (err) { } catch (err) {
return notFound() return notFound()

View File

@@ -0,0 +1,28 @@
.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

@@ -0,0 +1,50 @@
import { _ } from "@/lib/translation"
import Card from "@/components/TempDesignSystem/Card"
import Title from "@/components/Title"
import styles from "./cardGrid.module.css"
import { CardGridProps, CardProps } 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) => (
<CardWrapper key={`${card.title}+${i}`} card={card} />
))}
</div>
</section>
)
}
function CardWrapper({ card }: CardProps) {
const link = card.referenceConnection.edges.length
? {
href: card.referenceConnection.edges[0].node.url,
title: _("Read more"),
}
: undefined
return (
<div className={styles.cardWrapper}>
<Card subtitle={card.subtitle} title={card.title} link={link} />
</div>
)
}

View File

@@ -0,0 +1,11 @@
.container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 37rem;
border-radius: 1.6rem;
background-color: var(--Base-Fill-Normal);
text-align: center;
margin-right: 1.6rem;
}

View File

@@ -1,3 +1,13 @@
import Title from "@/components/Title"
import styles from "./howItWorks.module.css"
export default function HowItWorks() { export default function HowItWorks() {
return <div></div> return (
<div className={styles.container}>
<Title level="h3" uppercase>
How it works Placeholder
</Title>
</div>
)
} }

View File

@@ -1,26 +1,50 @@
import { Check } from "react-feather"
import { _ } from "@/lib/translation"
import { serverClient } from "@/lib/trpc/server" import { serverClient } from "@/lib/trpc/server"
import Image from "@/components/Image"
import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link"
import Title from "@/components/Title"
import styles from "./loyaltyLevels.module.css"
import { LevelCardProps } from "@/types/components/loyalty/blocks"
export default async function LoyaltyLevels() { export default async function LoyaltyLevels() {
const data = await serverClient().loyalty.levels.all() const data = await serverClient().loyalty.levels.all()
return ( return (
<div> <section className={styles.container}>
{data.map((level) => ( <div className={styles.cardContainer}>
<LevelCard key={level.tier} level={level} /> {data.map((level) => (
<LevelCard key={level.tier} level={level} />
))}
</div>
<div className={styles.buttonContainer}>
<Button intent="primary" asChild>
<Link href={"/compare-all-levels"}>{_("Compare all levels")}</Link>
</Button>
</div>
</section>
)
}
function LevelCard({ level }: LevelCardProps) {
return (
<div className={styles.card}>
<Title level="h4">{level.tier}</Title>
<Image src={level.logo} alt={level.name} width={140} height={54} />
<p className={styles.qualifications}>
{level.requiredPoints} {_("or")} {level.requiredNights} {_("nights")}
</p>
{level.topBenefits.map((benefit) => (
<p key={benefit} className={styles.benefits}>
<Check className={styles.icon} />
{benefit}
</p>
))} ))}
</div> </div>
) )
} }
type LevelCardProps = {
level: {
tier: number
name: string
requiredPoints: number
requiredNights: string
topBenefits: string[]
}
}
function LevelCard({ level }: LevelCardProps) {
return <div></div>
}

View File

@@ -0,0 +1,83 @@
.container {
display: grid;
gap: 2.4rem;
}
.buttonContainer {
display: flex;
justify-content: center;
}
.cardContainer {
display: flex;
gap: 0.8rem;
overflow-x: auto;
padding-right: 1.6rem;
margin-right: -1.6rem;
/* Hide scrollbar IE and Edge */
-ms-overflow-style: none;
/* Hide Scrollbar Firefox */
scrollbar-width: none;
}
.card {
display: flex;
flex-direction: column;
align-items: center;
height: 37rem;
min-width: 32rem;
padding: 4rem 1rem;
background-color: var(--Base-Fill-Normal);
border-radius: 1.6rem;
gap: 1.8rem;
}
.qualifications {
margin: 0;
font-size: var(--typography-Body-Bold-fontSize);
line-height: var(--typography-Body-Bold-lineHeight);
/* font-weight: var(--typography-Body-Bold-fontWeight); -- Tokens not parsable*/
font-weight: 600;
}
.benefits {
font-family: var(--fira-sans);
font-size: var(--typography-Body-Regular-fontSize);
line-height: var(--typography-Body-Regular-lineHeight);
margin: 0;
text-align: center;
}
.icon {
font-family: var(--fira-sans);
position: relative;
top: 0.3rem;
height: 1.4rem;
}
@media screen and (min-width: 950px) {
.container {
gap: 3.2rem;
}
.cardContainer {
display: grid;
grid-template-columns: repeat(
12,
auto
); /* Three columns in the first row */
padding-right: 0;
margin-right: 0rem;
}
.card {
min-width: auto;
}
.card:nth-child(-n + 3) {
grid-column: span 4;
}
.card:nth-last-child(-n + 4) {
grid-column: span 3;
}
}

View File

@@ -0,0 +1,28 @@
.container {
display: grid;
gap: 2.4rem;
overflow: hidden;
margin-right: -1.6rem;
padding-right: 1.6rem;
}
.titleContainer {
display: grid;
grid-template-areas: "title link";
grid-template-columns: 1fr max-content;
padding-bottom: 0.8rem;
}
.title {
grid-area: title;
}
.link {
grid-area: link;
font-size: var(--typography-Body-Underlined-fontSize);
color: var(--some-black-color, #000);
}
.subtitle {
margin: 0;
}

View File

@@ -1,18 +1,27 @@
import Link from "@/components/TempDesignSystem/Link"
import Title from "@/components/Title" import Title from "@/components/Title"
import HowItWorks from "./HowItWorks"
import LoyaltyLevels from "./LoyaltyLevels"
import OverviewTable from "./OverviewTable"
import styles from "./dynamicContent.module.css"
import { DynamicContentProps } from "@/types/components/loyalty/blocks"
import { import {
DynamicContentProps,
LoyaltyComponent, LoyaltyComponent,
LoyaltyComponentEnum, LoyaltyComponentEnum,
} from "@/types/components/loyalty/blocks" } from "@/types/requests/loyaltyPage"
function DynamicComponentBlock({ component }: { component: LoyaltyComponent }) { function DynamicComponentBlock({ component }: { component: LoyaltyComponent }) {
switch (component) { switch (component) {
case LoyaltyComponentEnum.how_it_works: case LoyaltyComponentEnum.how_it_works:
return <p>How it works</p> return <HowItWorks />
case LoyaltyComponentEnum.loyalty_levels: case LoyaltyComponentEnum.loyalty_levels:
return <p>loyalty_levels</p> return <LoyaltyLevels />
case LoyaltyComponentEnum.overview_table: case LoyaltyComponentEnum.overview_table:
return <p>overview_table</p> // TODO: IMPLEMENT OVERVIEW TABLE!
return <OverviewTable />
default: default:
return null return null
} }
@@ -21,14 +30,44 @@ function DynamicComponentBlock({ component }: { component: LoyaltyComponent }) {
export default function DynamicContent({ export default function DynamicContent({
dynamicContent, dynamicContent,
}: DynamicContentProps) { }: DynamicContentProps) {
const link = dynamicContent.link.pageConnection.edges.length
? dynamicContent.link.pageConnection.edges[0].node.url
: null
return ( return (
<section> <section className={styles.container}>
<header> <header>
<Title level="h3">{dynamicContent.title}</Title> <div className={styles.titleContainer}>
{dynamicContent.preamble ? <p>{dynamicContent.preamble}</p> : null} {dynamicContent.title && (
{dynamicContent.link ? <></> : null} <Title
as="h3"
level="h2"
className={styles.title}
weight="semiBold"
uppercase
>
{dynamicContent.title}
</Title>
)}
{link && (
<Link className={styles.link} href={link}>
{dynamicContent.link.text}
</Link>
)}
</div>
{dynamicContent.subtitle && (
<Title
as="h5"
level="h3"
weight="regular"
className={styles.subtitle}
>
{dynamicContent.subtitle}
</Title>
)}
</header> </header>
<DynamicComponentBlock component={dynamicContent.component} /> <div>
<DynamicComponentBlock component={dynamicContent.component} />
</div>
</section> </section>
) )
} }

View File

@@ -1,6 +1,8 @@
import JsonToHtml from "@/components/JsonToHtml" import JsonToHtml from "@/components/JsonToHtml"
import DynamicContentBlock from "@/components/Loyalty/Blocks/DynamicContent" import DynamicContentBlock from "@/components/Loyalty/Blocks/DynamicContent"
import CardGrid from "./CardGrid"
import { import {
Blocks as BlocksType, Blocks as BlocksType,
LoyaltyBlocksTypenameEnum, LoyaltyBlocksTypenameEnum,
@@ -10,7 +12,7 @@ export function Blocks({ blocks }: { blocks: BlocksType[] }) {
return blocks.map((block) => { return blocks.map((block) => {
switch (block.__typename) { switch (block.__typename) {
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardGrid: case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardGrid:
return <p>Cards</p> return <CardGrid card_grid={block.card_grid} />
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksContent: case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksContent:
return ( return (
<JsonToHtml <JsonToHtml

View File

@@ -1,8 +1,11 @@
import Title from "@/components/Title" import { _ } from "@/lib/translation"
import Contact from "./Contact"
import Image from "@/components/Image"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link" import Link from "@/components/TempDesignSystem/Link"
import Image from "@/components/Image" import Title from "@/components/Title"
import Contact from "./Contact"
import styles from "./joinLoyalty.module.css" import styles from "./joinLoyalty.module.css"
@@ -26,12 +29,12 @@ export default function JoinLoyaltyContact({
/> />
{block.preamble && <p className={styles.preamble}>{block.preamble}</p>} {block.preamble && <p className={styles.preamble}>{block.preamble}</p>}
<Button intent="primary"> <Button intent="primary">
<span>{block.login_button_text}</span> <span>{_("Join Scandic Friends")}</span>
</Button> </Button>
<div className={styles.linkContainer}> <div className={styles.linkContainer}>
<Link href="/login" className={styles.logoutLink}> <Link href="/login" className={styles.logoutLink}>
Already a friend? <br /> {_("Already a friend?")} <br />
Click here to log in {_("Click here to log in")}
</Link> </Link>
</div> </div>
</div> </div>

View File

@@ -2,7 +2,6 @@
display: grid; display: grid;
font-weight: 600; font-weight: 600;
background-color: var(--Base-Background-Elevated); background-color: var(--Base-Background-Elevated);
border-radius: 32px 4px 4px 32px;
} }
.wrapper { .wrapper {
@@ -10,7 +9,7 @@
align-items: center; align-items: center;
flex-direction: column; flex-direction: column;
gap: 2rem; gap: 2rem;
padding: 4rem 2rem; padding: 6rem 2rem;
} }
.preamble { .preamble {
@@ -37,6 +36,10 @@
} }
@media screen and (min-width: 950px) { @media screen and (min-width: 950px) {
.container {
border-radius: 32px 4px 4px 32px;
}
.wrapper { .wrapper {
gap: 3rem; gap: 3rem;
} }

View File

@@ -1,4 +1,5 @@
import JsonToHtml from "@/components/JsonToHtml" import JsonToHtml from "@/components/JsonToHtml"
import JoinLoyaltyContact from "./JoinLoyalty" import JoinLoyaltyContact from "./JoinLoyalty"
import { Sidebar, SidebarTypenameEnum } from "@/types/requests/loyaltyPage" import { Sidebar, SidebarTypenameEnum } from "@/types/requests/loyaltyPage"

View File

@@ -0,0 +1,15 @@
.linkCard {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 37rem;
width: 100%;
margin-right: 1.6rem;
border-radius: 1.6rem;
gap: 1rem;
padding: 1.6rem;
background-color: var(--Base-Fill-Normal);
text-align: center;
}

View File

@@ -0,0 +1,9 @@
export type CardProps = {
link?: {
href: string
title: string
}
title?: string
subtitle?: string
openInNewTab?: boolean
}

View File

@@ -0,0 +1,38 @@
import { _ } from "@/lib/translation"
import Title from "@/components/Title"
import Button from "../Button"
import Link from "../Link"
import { CardProps } from "./card"
import styles from "./card.module.css"
export default function Card({
link,
subtitle,
title,
openInNewTab = false,
}: CardProps) {
return (
<div className={styles.linkCard}>
{title ? (
<Title level="h3" weight="semiBold">
{title}
</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}
</div>
)
}

View File

@@ -0,0 +1,17 @@
query GetContentTypeUid($locale: String!, $url: String!) {
all_content_page(where: { url: $url, locale: $locale }) {
items {
__typename
}
}
all_current_blocks_page(where: { url: $url, locale: $locale }) {
items {
__typename
}
}
all_loyalty_page(where: { url: $url, locale: $locale }) {
items {
__typename
}
}
}

View File

@@ -11,7 +11,7 @@ query GetLoyaltyPage($locale: String!, $url: String!) {
__typename __typename
dynamic_content { dynamic_content {
title title
preamble subtitle
component component
link { link {
text text
@@ -29,8 +29,8 @@ query GetLoyaltyPage($locale: String!, $url: String!) {
... on LoyaltyPageBlocksCardGrid { ... on LoyaltyPageBlocksCardGrid {
__typename __typename
card_grid { card_grid {
heading title
preamble subtitle
cards { cards {
referenceConnection { referenceConnection {
edges { edges {
@@ -42,8 +42,8 @@ query GetLoyaltyPage($locale: String!, $url: String!) {
} }
} }
} }
heading title
preamble subtitle
} }
} }
} }

View File

@@ -1,19 +1,70 @@
import { DocumentNode } from "graphql"
import { NextResponse } from "next/server" import { NextResponse } from "next/server"
import { findLang } from "@/constants/languages" import { findLang } from "@/constants/languages"
import { env } from "@/env/server"
import GetContentTypeUid from "@/lib/graphql/Query/ContentTypeUid.graphql"
import type { NextMiddleware } from "next/server" import type { NextMiddleware } from "next/server"
import { MiddlewareMatcher } from "@/types/middleware" import { MiddlewareMatcher } from "@/types/middleware"
enum PageTypeEnum {
CurrentBlocksPage = "CurrentBlocksPage",
LoyaltyPage = "LoyaltyPage",
ContentPage = "contentPage",
}
type GetContentTypeUidType = {
all_content_page: {
items: {
__typename: PageTypeEnum.ContentPage
}[]
}
all_loyalty_page: {
items: {
__typename: PageTypeEnum.LoyaltyPage
}[]
}
all_current_blocks_page: {
items: {
__typename?: PageTypeEnum.CurrentBlocksPage
}[]
}
}
type PageType = keyof typeof PageTypeEnum
export const middleware: NextMiddleware = async (request) => { export const middleware: NextMiddleware = async (request) => {
const { nextUrl } = request const { nextUrl } = request
const lang = findLang(nextUrl.pathname) const lang = findLang(nextUrl.pathname)
const contentType = "loyaltyPage"
const pathNameWithoutLang = nextUrl.pathname.replace(`/${lang}`, "") const pathNameWithoutLang = nextUrl.pathname.replace(`/${lang}`, "")
const searchParams = new URLSearchParams(request.nextUrl.searchParams) const searchParams = new URLSearchParams(request.nextUrl.searchParams)
const print = (await import("graphql/language/printer")).print
const result = await fetch(env.CMS_URL, {
method: "POST",
headers: {
access_token: env.CMS_ACCESS_TOKEN,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: print(GetContentTypeUid as DocumentNode),
variables: {
locale: lang,
url: pathNameWithoutLang,
},
}),
})
const pageTypeData = await result.json()
const pageType = pageTypeData.data as GetContentTypeUidType
const contentType = Object.values(pageType)
.map((val) => val.items[0])
.find((item) => item?.__typename)?.__typename
if (request.nextUrl.pathname.includes("preview")) { if (request.nextUrl.pathname.includes("preview")) {
searchParams.set("uri", pathNameWithoutLang.replace("/preview", "")) searchParams.set("uri", pathNameWithoutLang.replace("/preview", ""))
return NextResponse.rewrite( return NextResponse.rewrite(
@@ -23,14 +74,14 @@ export const middleware: NextMiddleware = async (request) => {
searchParams.set("uri", pathNameWithoutLang) searchParams.set("uri", pathNameWithoutLang)
switch (contentType) { switch (contentType) {
// case "currentContentPage": case PageTypeEnum.CurrentBlocksPage:
// return NextResponse.rewrite( return NextResponse.rewrite(
// new URL( new URL(
// `/${lang}/current-content-page?${searchParams.toString()}`, `/${lang}/current-content-page?${searchParams.toString()}`,
// nextUrl nextUrl
// ) )
// ) )
case "loyaltyPage": case PageTypeEnum.LoyaltyPage:
return NextResponse.rewrite( return NextResponse.rewrite(
new URL(`/${lang}/loyalty-page?${searchParams.toString()}`, nextUrl) new URL(`/${lang}/loyalty-page?${searchParams.toString()}`, nextUrl)
) )

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -1,10 +1,11 @@
import { GetMyPagesBreadcrumbs } from "@/lib/graphql/Query/BreadcrumbsMyPages.graphql"
import { request } from "@/lib/graphql/request"
import { badRequestError, internalServerError } from "@/server/errors/trpc" import { badRequestError, internalServerError } from "@/server/errors/trpc"
import { validateBreadcrumbsConstenstackSchema } from "./output"
import { publicProcedure, router } from "@/server/trpc" import { publicProcedure, router } from "@/server/trpc"
import { request } from "@/lib/graphql/request"
import { GetMyPagesBreadcrumbs } from "@/lib/graphql/Query/BreadcrumbsMyPages.graphql"
import { getBreadcrumbsInput } from "./input" import { getBreadcrumbsInput } from "./input"
import { validateBreadcrumbsConstenstackSchema } from "./output"
import { GetMyPagesBreadcrumbsData } from "@/types/requests/myPages/breadcrumbs" import { GetMyPagesBreadcrumbsData } from "@/types/requests/myPages/breadcrumbs"
export const breadcrumbsQueryRouter = router({ export const breadcrumbsQueryRouter = router({

View File

@@ -0,0 +1,10 @@
import { z } from "zod"
import { Lang } from "@/constants/languages"
const langs = Object.keys(Lang) as [keyof typeof Lang]
export const getLoyaltyPageInput = z.object({
href: z.string().min(1, { message: "href is required" }),
locale: z.enum(langs),
})

View File

@@ -1,6 +1,7 @@
import { allLevels } from "./temp"
import { protectedProcedure, publicProcedure, router } from "@/server/trpc" import { protectedProcedure, publicProcedure, router } from "@/server/trpc"
import { allLevels } from "./temp"
function fakingRequest<T>(payload: T): Promise<T> { function fakingRequest<T>(payload: T): Promise<T> {
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {

View File

@@ -9,6 +9,7 @@ export const allLevels = [
"Always best price", "Always best price",
"Book reward nights with points", "Book reward nights with points",
], ],
logo: "/_static/icons/new-friend.png",
}, },
{ {
tier: 2, tier: 2,
@@ -20,6 +21,7 @@ export const allLevels = [
"Always best price", "Always best price",
"Book reward nights with points", "Book reward nights with points",
], ],
logo: "/_static/icons/new-friend.png",
}, },
{ {
tier: 3, tier: 3,
@@ -31,6 +33,7 @@ export const allLevels = [
"Always best price", "Always best price",
"Book reward nights with points", "Book reward nights with points",
], ],
logo: "/_static/icons/new-friend.png",
}, },
{ {
tier: 4, tier: 4,
@@ -42,6 +45,7 @@ export const allLevels = [
"Always best price", "Always best price",
"Book reward nights with points", "Book reward nights with points",
], ],
logo: "/_static/icons/new-friend.png",
}, },
{ {
tier: 5, tier: 5,
@@ -53,6 +57,7 @@ export const allLevels = [
"Always best price", "Always best price",
"Book reward nights with points", "Book reward nights with points",
], ],
logo: "/_static/icons/new-friend.png",
}, },
{ {
tier: 6, tier: 6,
@@ -64,5 +69,18 @@ export const allLevels = [
"Always best price", "Always best price",
"Book reward nights with points", "Book reward nights with points",
], ],
logo: "/_static/icons/new-friend.png",
},
{
tier: 7,
name: "New Friend",
requiredPoints: 50000,
requiredNights: "X",
topBenefits: [
"15% on food on weekends",
"Always best price",
"Book reward nights with points",
],
logo: "/_static/icons/new-friend.png",
}, },
] ]

View File

@@ -1,47 +1,46 @@
import { Embeds } from "@/types/requests/embeds" import { Embeds } from "@/types/requests/embeds"
import { DynamicContentBlock } from "@/types/requests/loyaltyPage"
import { PageLink } from "@/types/requests/myPages/navigation" import { PageLink } from "@/types/requests/myPages/navigation"
import { Edges } from "@/types/requests/utils/edges" import { Edges } from "@/types/requests/utils/edges"
import { RTEDocument } from "@/types/rte/node" import { RTEDocument } from "@/types/rte/node"
export enum LoyaltyComponentEnum {
loyalty_levels = "loyalty_levels",
how_it_works = "how_it_works",
overview_table = "overview_table",
}
export type LoyaltyComponent = keyof typeof LoyaltyComponentEnum
export type DynamicContentBlock = {
dynamic_content: {
title: string
preamble?: string
component: LoyaltyComponent
link: {
text?: string
page: Edges<PageLink>
}
}
}
export type DynamicContentProps = { export type DynamicContentProps = {
dynamicContent: DynamicContentBlock["dynamic_content"] dynamicContent: DynamicContentBlock["dynamic_content"]
} }
type Card = {
referenceConnection: Edges<PageLink>
title?: string
subtitle?: string
open_in_new_tab: boolean
}
export type CardProps = { card: Card }
export type CardGrid = { export type CardGrid = {
card_grid: { card_grid: {
heading: string title?: string
preamble: string subtitle?: string
cards: { cards: Card[]
referenceConnection: Edges<PageLink>
heading: string
preamble: string
}
} }
} }
export type CardGridProps = CardGrid
export type Content = { export type Content = {
content: { content: {
embedded_itemsConnection: Edges<Embeds> embedded_itemsConnection: Edges<Embeds>
json: RTEDocument json: RTEDocument
} }
} }
export type LevelCardProps = {
level: {
tier: number
name: string
requiredPoints: number
requiredNights: string
topBenefits: string[]
logo: string
}
}

View File

@@ -1,6 +1,6 @@
import { ContactFields } from "@/types/requests/contactConfig" import { ContactFields } from "@/types/requests/contactConfig"
import { Embeds } from "@/types/requests/embeds" import { Embeds } from "@/types/requests/embeds"
import { JoinLoyaltyContactEnum } from "@/types/requests/loyaltyPage" import { JoinLoyaltyContactContact } from "@/types/requests/loyaltyPage"
import { Edges } from "@/types/requests/utils/edges" import { Edges } from "@/types/requests/utils/edges"
import { RTEDocument } from "@/types/rte/node" import { RTEDocument } from "@/types/rte/node"
@@ -16,5 +16,5 @@ export type Contact = {
} }
export type ContactProps = { export type ContactProps = {
contactBlock: JoinLoyaltyContactEnum[] contactBlock: JoinLoyaltyContactContact[]
} }

View File

@@ -49,6 +49,7 @@ export type GetContactConfigData = {
// Utility types that extract the possible strings of ContactConfigField, // Utility types that extract the possible strings of ContactConfigField,
// Which is all the dot notated values of ContactConfig (for example: 'email.name') // Which is all the dot notated values of ContactConfig (for example: 'email.name')
// From: https://stackoverflow.com/questions/47057649/typescript-string-dot-notation-of-nested-object#47058976
type PathsToStringProps<T> = T extends string type PathsToStringProps<T> = T extends string
? [] ? []
: { : {
@@ -65,7 +66,7 @@ type Join<T extends string[], D extends string> = T extends []
: never : never
: string : string
type ContactConfigField = Join<PathsToStringProps<ContactConfig>, "."> export type ContactConfigField = Join<PathsToStringProps<ContactConfig>, ".">
export type ContactFields = { export type ContactFields = {
display_text?: string display_text?: string

View File

@@ -1,24 +1,16 @@
import { CardGrid, Content } from "../components/loyalty/blocks"
import { Contact, SidebarContent } from "../components/loyalty/sidebar"
import { PageLink } from "./myPages/navigation"
import { Edges } from "./utils/edges"
import type { AllRequestResponse } from "./utils/all" import type { AllRequestResponse } from "./utils/all"
import type { Typename } from "./utils/typename" import type { Typename } from "./utils/typename"
import { Contact, SidebarContent } from "../components/loyalty/sidebar"
import {
CardGrid,
Content,
DynamicContentBlock,
} from "../components/loyalty/blocks"
export enum SidebarTypenameEnum {
LoyaltyPageSidebarJoinLoyaltyContact = "LoyaltyPageSidebarJoinLoyaltyContact",
LoyaltyPageSidebarContent = "LoyaltyPageSidebarContent",
}
export type SidebarTypename = keyof typeof SidebarTypenameEnum
export enum JoinLoyaltyContactTypenameEnum { export enum JoinLoyaltyContactTypenameEnum {
LoyaltyPageSidebarJoinLoyaltyContactBlockContactContact = "LoyaltyPageSidebarJoinLoyaltyContactBlockContactContact", LoyaltyPageSidebarJoinLoyaltyContactBlockContactContact = "LoyaltyPageSidebarJoinLoyaltyContactBlockContactContact",
} }
export type JoinLoyaltyContactEnum = Typename< export type JoinLoyaltyContactContact = Typename<
Contact, Contact,
JoinLoyaltyContactTypenameEnum.LoyaltyPageSidebarJoinLoyaltyContactBlockContactContact JoinLoyaltyContactTypenameEnum.LoyaltyPageSidebarJoinLoyaltyContactBlockContactContact
> >
@@ -27,11 +19,17 @@ export type JoinLoyaltyContact = {
join_loyalty_contact: { join_loyalty_contact: {
title?: string title?: string
preamble?: string preamble?: string
contact: JoinLoyaltyContactEnum[] contact: JoinLoyaltyContactContact[]
login_button_text: string
} }
} }
export enum SidebarTypenameEnum {
LoyaltyPageSidebarJoinLoyaltyContact = "LoyaltyPageSidebarJoinLoyaltyContact",
LoyaltyPageSidebarContent = "LoyaltyPageSidebarContent",
}
export type SidebarTypename = keyof typeof SidebarTypenameEnum
export type Sidebar = export type Sidebar =
| Typename<SidebarContent, SidebarTypenameEnum.LoyaltyPageSidebarContent> | Typename<SidebarContent, SidebarTypenameEnum.LoyaltyPageSidebarContent>
| Typename< | Typename<
@@ -39,6 +37,26 @@ export type Sidebar =
SidebarTypenameEnum.LoyaltyPageSidebarJoinLoyaltyContact SidebarTypenameEnum.LoyaltyPageSidebarJoinLoyaltyContact
> >
export enum LoyaltyComponentEnum {
loyalty_levels = "loyalty_levels",
how_it_works = "how_it_works",
overview_table = "overview_table",
}
export type LoyaltyComponent = keyof typeof LoyaltyComponentEnum
export type DynamicContentBlock = {
dynamic_content: {
title?: string
subtitle?: string
component: LoyaltyComponent
link: {
text?: string
pageConnection: Edges<PageLink>
}
}
}
export enum LoyaltyBlocksTypenameEnum { export enum LoyaltyBlocksTypenameEnum {
LoyaltyPageBlocksDynamicContent = "LoyaltyPageBlocksDynamicContent", LoyaltyPageBlocksDynamicContent = "LoyaltyPageBlocksDynamicContent",
LoyaltyPageBlocksCardGrid = "LoyaltyPageBlocksCardGrid", LoyaltyPageBlocksCardGrid = "LoyaltyPageBlocksCardGrid",

View File

@@ -1,5 +1,5 @@
import type { EmbedEnum } from "./embeds"
import type { Image } from "@/types/image" import type { Image } from "@/types/image"
import type { EmbedEnum } from "./embeds"
import type { Typename } from "./typename" import type { Typename } from "./typename"
export type SysAsset = Typename<Image, EmbedEnum.SysAsset> export type SysAsset = Typename<Image, EmbedEnum.SysAsset>

View File

@@ -1,10 +1,11 @@
import { import {
ContactConfig, ContactConfig,
ContactConfigField,
ContactFieldGroups, ContactFieldGroups,
} from "@/types/requests/contactConfig" } from "@/types/requests/contactConfig"
export function getValueFromContactConfig( export function getValueFromContactConfig(
keyStrings: string, keyStrings: ContactConfigField,
data: ContactConfig data: ContactConfig
): string | undefined { ): string | undefined {
const [groupName, key] = keyStrings.split(".") as [ const [groupName, key] = keyStrings.split(".") as [