feat: add card grid component
This commit is contained in:
@@ -1,10 +1,7 @@
|
||||
.layout {
|
||||
--max-width: 101.4rem;
|
||||
--header-height: 4.5rem;
|
||||
|
||||
display: grid;
|
||||
font-family: var(--ff-fira-sans);
|
||||
grid-template-rows: var(--header-height) auto 1fr;
|
||||
min-height: 100dvh;
|
||||
background-color: var(--Brand-Coffee-Subtle);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import { firaMono, firaSans } from "@/app/[lang]/(live)/fonts"
|
||||
|
||||
import Header from "@/components/MyPages/Header"
|
||||
|
||||
import styles from "./layout.module.css"
|
||||
|
||||
import type { MyPagesLayoutProps } from "@/types/components/myPages/layout"
|
||||
|
||||
export default async function LoyaltyPagesLayout({
|
||||
children,
|
||||
params,
|
||||
}: React.PropsWithChildren<MyPagesLayoutProps>) {
|
||||
return (
|
||||
<div
|
||||
className={`${firaMono.variable} ${firaSans.variable} ${styles.layout}`}
|
||||
>
|
||||
<Header lang={params.lang} />
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
.blocks {
|
||||
display: grid;
|
||||
gap: 4.2rem;
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
padding: 1.6rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 950px) {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import MaxWidth from "@/components/MaxWidth"
|
||||
import Sidebar from "@/components/Loyalty/Sidebar"
|
||||
import { Blocks } from "@/components/Loyalty/Blocks"
|
||||
|
||||
import type { LangParams, PageArgs, UriParams } from "@/types/params"
|
||||
import Sidebar from "@/components/Loyalty/Sidebar"
|
||||
import MaxWidth from "@/components/MaxWidth"
|
||||
|
||||
import styles from "./page.module.css"
|
||||
|
||||
import type { LangParams, PageArgs, UriParams } from "@/types/params"
|
||||
|
||||
export default async function LoyaltyPage({
|
||||
params,
|
||||
searchParams,
|
||||
@@ -19,12 +20,12 @@ export default async function LoyaltyPage({
|
||||
}
|
||||
|
||||
const loyaltyPage = await serverClient().contentstack.loyaltyPage.get({
|
||||
uri: searchParams.uri,
|
||||
lang: params.lang,
|
||||
href: searchParams.uri,
|
||||
locale: params.lang,
|
||||
})
|
||||
|
||||
return (
|
||||
<MaxWidth className={styles.content} tag="main">
|
||||
<section className={styles.content}>
|
||||
<aside>
|
||||
{loyaltyPage.sidebar
|
||||
? loyaltyPage.sidebar.map((block, i) => (
|
||||
@@ -32,10 +33,10 @@ export default async function LoyaltyPage({
|
||||
))
|
||||
: null}
|
||||
</aside>
|
||||
<section className={styles.blocks}>
|
||||
<MaxWidth className={styles.blocks} tag="main">
|
||||
<Blocks blocks={loyaltyPage.blocks} />
|
||||
</section>
|
||||
</MaxWidth>
|
||||
</MaxWidth>
|
||||
</section>
|
||||
)
|
||||
} catch (err) {
|
||||
return notFound()
|
||||
|
||||
28
components/Loyalty/Blocks/CardGrid/cardGrid.module.css
Normal file
28
components/Loyalty/Blocks/CardGrid/cardGrid.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
50
components/Loyalty/Blocks/CardGrid/index.tsx
Normal file
50
components/Loyalty/Blocks/CardGrid/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,3 +1,13 @@
|
||||
import Title from "@/components/Title"
|
||||
|
||||
import styles from "./howItWorks.module.css"
|
||||
|
||||
export default function HowItWorks() {
|
||||
return <div></div>
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Title level="h3" uppercase>
|
||||
How it works Placeholder
|
||||
</Title>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,26 +1,50 @@
|
||||
import { Check } from "react-feather"
|
||||
|
||||
import { _ } from "@/lib/translation"
|
||||
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() {
|
||||
const data = await serverClient().loyalty.levels.all()
|
||||
|
||||
return (
|
||||
<div>
|
||||
{data.map((level) => (
|
||||
<LevelCard key={level.tier} level={level} />
|
||||
<section className={styles.container}>
|
||||
<div className={styles.cardContainer}>
|
||||
{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>
|
||||
)
|
||||
}
|
||||
|
||||
type LevelCardProps = {
|
||||
level: {
|
||||
tier: number
|
||||
name: string
|
||||
requiredPoints: number
|
||||
requiredNights: string
|
||||
topBenefits: string[]
|
||||
}
|
||||
}
|
||||
function LevelCard({ level }: LevelCardProps) {
|
||||
return <div></div>
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
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 {
|
||||
DynamicContentProps,
|
||||
LoyaltyComponent,
|
||||
LoyaltyComponentEnum,
|
||||
} from "@/types/components/loyalty/blocks"
|
||||
} from "@/types/requests/loyaltyPage"
|
||||
|
||||
function DynamicComponentBlock({ component }: { component: LoyaltyComponent }) {
|
||||
switch (component) {
|
||||
case LoyaltyComponentEnum.how_it_works:
|
||||
return <p>How it works</p>
|
||||
return <HowItWorks />
|
||||
case LoyaltyComponentEnum.loyalty_levels:
|
||||
return <p>loyalty_levels</p>
|
||||
return <LoyaltyLevels />
|
||||
case LoyaltyComponentEnum.overview_table:
|
||||
return <p>overview_table</p>
|
||||
// TODO: IMPLEMENT OVERVIEW TABLE!
|
||||
return <OverviewTable />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
@@ -21,14 +30,44 @@ function DynamicComponentBlock({ component }: { component: LoyaltyComponent }) {
|
||||
export default function DynamicContent({
|
||||
dynamicContent,
|
||||
}: DynamicContentProps) {
|
||||
const link = dynamicContent.link.pageConnection.edges.length
|
||||
? dynamicContent.link.pageConnection.edges[0].node.url
|
||||
: null
|
||||
return (
|
||||
<section>
|
||||
<section className={styles.container}>
|
||||
<header>
|
||||
<Title level="h3">{dynamicContent.title}</Title>
|
||||
{dynamicContent.preamble ? <p>{dynamicContent.preamble}</p> : null}
|
||||
{dynamicContent.link ? <></> : null}
|
||||
<div className={styles.titleContainer}>
|
||||
{dynamicContent.title && (
|
||||
<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>
|
||||
<DynamicComponentBlock component={dynamicContent.component} />
|
||||
<div>
|
||||
<DynamicComponentBlock component={dynamicContent.component} />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import JsonToHtml from "@/components/JsonToHtml"
|
||||
import DynamicContentBlock from "@/components/Loyalty/Blocks/DynamicContent"
|
||||
|
||||
import CardGrid from "./CardGrid"
|
||||
|
||||
import {
|
||||
Blocks as BlocksType,
|
||||
LoyaltyBlocksTypenameEnum,
|
||||
@@ -10,7 +12,7 @@ export function Blocks({ blocks }: { blocks: BlocksType[] }) {
|
||||
return blocks.map((block) => {
|
||||
switch (block.__typename) {
|
||||
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardGrid:
|
||||
return <p>Cards</p>
|
||||
return <CardGrid card_grid={block.card_grid} />
|
||||
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksContent:
|
||||
return (
|
||||
<JsonToHtml
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import Title from "@/components/Title"
|
||||
import Contact from "./Contact"
|
||||
import { _ } from "@/lib/translation"
|
||||
|
||||
import Image from "@/components/Image"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
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"
|
||||
|
||||
@@ -26,12 +29,12 @@ export default function JoinLoyaltyContact({
|
||||
/>
|
||||
{block.preamble && <p className={styles.preamble}>{block.preamble}</p>}
|
||||
<Button intent="primary">
|
||||
<span>{block.login_button_text}</span>
|
||||
<span>{_("Join Scandic Friends")}</span>
|
||||
</Button>
|
||||
<div className={styles.linkContainer}>
|
||||
<Link href="/login" className={styles.logoutLink}>
|
||||
Already a friend? <br />
|
||||
Click here to log in
|
||||
{_("Already a friend?")} <br />
|
||||
{_("Click here to log in")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
display: grid;
|
||||
font-weight: 600;
|
||||
background-color: var(--Base-Background-Elevated);
|
||||
border-radius: 32px 4px 4px 32px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
@@ -10,7 +9,7 @@
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
padding: 4rem 2rem;
|
||||
padding: 6rem 2rem;
|
||||
}
|
||||
|
||||
.preamble {
|
||||
@@ -37,6 +36,10 @@
|
||||
}
|
||||
|
||||
@media screen and (min-width: 950px) {
|
||||
.container {
|
||||
border-radius: 32px 4px 4px 32px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import JsonToHtml from "@/components/JsonToHtml"
|
||||
|
||||
import JoinLoyaltyContact from "./JoinLoyalty"
|
||||
|
||||
import { Sidebar, SidebarTypenameEnum } from "@/types/requests/loyaltyPage"
|
||||
|
||||
15
components/TempDesignSystem/Card/card.module.css
Normal file
15
components/TempDesignSystem/Card/card.module.css
Normal 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;
|
||||
}
|
||||
9
components/TempDesignSystem/Card/card.ts
Normal file
9
components/TempDesignSystem/Card/card.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export type CardProps = {
|
||||
link?: {
|
||||
href: string
|
||||
title: string
|
||||
}
|
||||
title?: string
|
||||
subtitle?: string
|
||||
openInNewTab?: boolean
|
||||
}
|
||||
38
components/TempDesignSystem/Card/index.tsx
Normal file
38
components/TempDesignSystem/Card/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
17
lib/graphql/Query/ContentTypeUid.graphql
Normal file
17
lib/graphql/Query/ContentTypeUid.graphql
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ query GetLoyaltyPage($locale: String!, $url: String!) {
|
||||
__typename
|
||||
dynamic_content {
|
||||
title
|
||||
preamble
|
||||
subtitle
|
||||
component
|
||||
link {
|
||||
text
|
||||
@@ -29,8 +29,8 @@ query GetLoyaltyPage($locale: String!, $url: String!) {
|
||||
... on LoyaltyPageBlocksCardGrid {
|
||||
__typename
|
||||
card_grid {
|
||||
heading
|
||||
preamble
|
||||
title
|
||||
subtitle
|
||||
cards {
|
||||
referenceConnection {
|
||||
edges {
|
||||
@@ -42,8 +42,8 @@ query GetLoyaltyPage($locale: String!, $url: String!) {
|
||||
}
|
||||
}
|
||||
}
|
||||
heading
|
||||
preamble
|
||||
title
|
||||
subtitle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,70 @@
|
||||
import { DocumentNode } from "graphql"
|
||||
import { NextResponse } from "next/server"
|
||||
|
||||
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 { 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) => {
|
||||
const { nextUrl } = request
|
||||
const lang = findLang(nextUrl.pathname)
|
||||
|
||||
const contentType = "loyaltyPage"
|
||||
const pathNameWithoutLang = nextUrl.pathname.replace(`/${lang}`, "")
|
||||
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")) {
|
||||
searchParams.set("uri", pathNameWithoutLang.replace("/preview", ""))
|
||||
return NextResponse.rewrite(
|
||||
@@ -23,14 +74,14 @@ export const middleware: NextMiddleware = async (request) => {
|
||||
|
||||
searchParams.set("uri", pathNameWithoutLang)
|
||||
switch (contentType) {
|
||||
// case "currentContentPage":
|
||||
// return NextResponse.rewrite(
|
||||
// new URL(
|
||||
// `/${lang}/current-content-page?${searchParams.toString()}`,
|
||||
// nextUrl
|
||||
// )
|
||||
// )
|
||||
case "loyaltyPage":
|
||||
case PageTypeEnum.CurrentBlocksPage:
|
||||
return NextResponse.rewrite(
|
||||
new URL(
|
||||
`/${lang}/current-content-page?${searchParams.toString()}`,
|
||||
nextUrl
|
||||
)
|
||||
)
|
||||
case PageTypeEnum.LoyaltyPage:
|
||||
return NextResponse.rewrite(
|
||||
new URL(`/${lang}/loyalty-page?${searchParams.toString()}`, nextUrl)
|
||||
)
|
||||
|
||||
BIN
public/_static/icons/new-friend.png
Normal file
BIN
public/_static/icons/new-friend.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
@@ -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 { validateBreadcrumbsConstenstackSchema } from "./output"
|
||||
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 { validateBreadcrumbsConstenstackSchema } from "./output"
|
||||
|
||||
import { GetMyPagesBreadcrumbsData } from "@/types/requests/myPages/breadcrumbs"
|
||||
|
||||
export const breadcrumbsQueryRouter = router({
|
||||
|
||||
10
server/routers/contentstack/loyaltyPage/input.ts
Normal file
10
server/routers/contentstack/loyaltyPage/input.ts
Normal 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),
|
||||
})
|
||||
@@ -1,6 +1,7 @@
|
||||
import { allLevels } from "./temp"
|
||||
import { protectedProcedure, publicProcedure, router } from "@/server/trpc"
|
||||
|
||||
import { allLevels } from "./temp"
|
||||
|
||||
function fakingRequest<T>(payload: T): Promise<T> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -9,6 +9,7 @@ export const allLevels = [
|
||||
"Always best price",
|
||||
"Book reward nights with points",
|
||||
],
|
||||
logo: "/_static/icons/new-friend.png",
|
||||
},
|
||||
{
|
||||
tier: 2,
|
||||
@@ -20,6 +21,7 @@ export const allLevels = [
|
||||
"Always best price",
|
||||
"Book reward nights with points",
|
||||
],
|
||||
logo: "/_static/icons/new-friend.png",
|
||||
},
|
||||
{
|
||||
tier: 3,
|
||||
@@ -31,6 +33,7 @@ export const allLevels = [
|
||||
"Always best price",
|
||||
"Book reward nights with points",
|
||||
],
|
||||
logo: "/_static/icons/new-friend.png",
|
||||
},
|
||||
{
|
||||
tier: 4,
|
||||
@@ -42,6 +45,7 @@ export const allLevels = [
|
||||
"Always best price",
|
||||
"Book reward nights with points",
|
||||
],
|
||||
logo: "/_static/icons/new-friend.png",
|
||||
},
|
||||
{
|
||||
tier: 5,
|
||||
@@ -53,6 +57,7 @@ export const allLevels = [
|
||||
"Always best price",
|
||||
"Book reward nights with points",
|
||||
],
|
||||
logo: "/_static/icons/new-friend.png",
|
||||
},
|
||||
{
|
||||
tier: 6,
|
||||
@@ -64,5 +69,18 @@ export const allLevels = [
|
||||
"Always best price",
|
||||
"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",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,47 +1,46 @@
|
||||
import { Embeds } from "@/types/requests/embeds"
|
||||
import { DynamicContentBlock } from "@/types/requests/loyaltyPage"
|
||||
import { PageLink } from "@/types/requests/myPages/navigation"
|
||||
import { Edges } from "@/types/requests/utils/edges"
|
||||
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 = {
|
||||
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 = {
|
||||
card_grid: {
|
||||
heading: string
|
||||
preamble: string
|
||||
cards: {
|
||||
referenceConnection: Edges<PageLink>
|
||||
heading: string
|
||||
preamble: string
|
||||
}
|
||||
title?: string
|
||||
subtitle?: string
|
||||
cards: Card[]
|
||||
}
|
||||
}
|
||||
|
||||
export type CardGridProps = CardGrid
|
||||
|
||||
export type Content = {
|
||||
content: {
|
||||
embedded_itemsConnection: Edges<Embeds>
|
||||
json: RTEDocument
|
||||
}
|
||||
}
|
||||
|
||||
export type LevelCardProps = {
|
||||
level: {
|
||||
tier: number
|
||||
name: string
|
||||
requiredPoints: number
|
||||
requiredNights: string
|
||||
topBenefits: string[]
|
||||
logo: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ContactFields } from "@/types/requests/contactConfig"
|
||||
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 { RTEDocument } from "@/types/rte/node"
|
||||
|
||||
@@ -16,5 +16,5 @@ export type Contact = {
|
||||
}
|
||||
|
||||
export type ContactProps = {
|
||||
contactBlock: JoinLoyaltyContactEnum[]
|
||||
contactBlock: JoinLoyaltyContactContact[]
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ export type GetContactConfigData = {
|
||||
|
||||
// Utility types that extract the possible strings of ContactConfigField,
|
||||
// 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
|
||||
? []
|
||||
: {
|
||||
@@ -65,7 +66,7 @@ type Join<T extends string[], D extends string> = T extends []
|
||||
: never
|
||||
: string
|
||||
|
||||
type ContactConfigField = Join<PathsToStringProps<ContactConfig>, ".">
|
||||
export type ContactConfigField = Join<PathsToStringProps<ContactConfig>, ".">
|
||||
|
||||
export type ContactFields = {
|
||||
display_text?: string
|
||||
|
||||
@@ -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 { 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 {
|
||||
LoyaltyPageSidebarJoinLoyaltyContactBlockContactContact = "LoyaltyPageSidebarJoinLoyaltyContactBlockContactContact",
|
||||
}
|
||||
|
||||
export type JoinLoyaltyContactEnum = Typename<
|
||||
export type JoinLoyaltyContactContact = Typename<
|
||||
Contact,
|
||||
JoinLoyaltyContactTypenameEnum.LoyaltyPageSidebarJoinLoyaltyContactBlockContactContact
|
||||
>
|
||||
@@ -27,11 +19,17 @@ export type JoinLoyaltyContact = {
|
||||
join_loyalty_contact: {
|
||||
title?: string
|
||||
preamble?: string
|
||||
contact: JoinLoyaltyContactEnum[]
|
||||
login_button_text: string
|
||||
contact: JoinLoyaltyContactContact[]
|
||||
}
|
||||
}
|
||||
|
||||
export enum SidebarTypenameEnum {
|
||||
LoyaltyPageSidebarJoinLoyaltyContact = "LoyaltyPageSidebarJoinLoyaltyContact",
|
||||
LoyaltyPageSidebarContent = "LoyaltyPageSidebarContent",
|
||||
}
|
||||
|
||||
export type SidebarTypename = keyof typeof SidebarTypenameEnum
|
||||
|
||||
export type Sidebar =
|
||||
| Typename<SidebarContent, SidebarTypenameEnum.LoyaltyPageSidebarContent>
|
||||
| Typename<
|
||||
@@ -39,6 +37,26 @@ export type Sidebar =
|
||||
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 {
|
||||
LoyaltyPageBlocksDynamicContent = "LoyaltyPageBlocksDynamicContent",
|
||||
LoyaltyPageBlocksCardGrid = "LoyaltyPageBlocksCardGrid",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { EmbedEnum } from "./embeds"
|
||||
import type { Image } from "@/types/image"
|
||||
import type { EmbedEnum } from "./embeds"
|
||||
import type { Typename } from "./typename"
|
||||
|
||||
export type SysAsset = Typename<Image, EmbedEnum.SysAsset>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import {
|
||||
ContactConfig,
|
||||
ContactConfigField,
|
||||
ContactFieldGroups,
|
||||
} from "@/types/requests/contactConfig"
|
||||
|
||||
export function getValueFromContactConfig(
|
||||
keyStrings: string,
|
||||
keyStrings: ContactConfigField,
|
||||
data: ContactConfig
|
||||
): string | undefined {
|
||||
const [groupName, key] = keyStrings.split(".") as [
|
||||
|
||||
Reference in New Issue
Block a user