Merged in feat/loyalty-page (pull request #151)

Feat(WEB-21): loyalty page

Approved-by: Simon.Emanuelsson
This commit is contained in:
Christel Westerberg
2024-04-29 12:37:51 +00:00
committed by Simon.Emanuelsson
81 changed files with 1750 additions and 68 deletions

View File

@@ -1,7 +1,7 @@
import CurrentBenefitsBlock from "@/components/MyPages/Blocks/Benefits/CurrentLevel"
import NextLevelBenefitsBlock from "@/components/MyPages/Blocks/Benefits/NextLevel"
import Shortcuts from "@/components/MyPages/Blocks/Shortcuts"
import Title from "@/components/MyPages/Title"
import Title from "@/components/Title"
import { shortcuts } from "./_constants"

View File

@@ -1,5 +1,4 @@
.layout {
--max-width: 101.4rem;
--header-height: 4.5rem;
display: grid;
@@ -25,4 +24,4 @@
padding-right: 2.4rem;
padding-top: 5.8rem;
}
}
}

View File

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

View File

@@ -0,0 +1,15 @@
import { firaMono, firaSans } from "@/app/[lang]/(live)/fonts"
import styles from "./layout.module.css"
export default function LoyaltyPagesLayout({
children,
}: React.PropsWithChildren) {
return (
<div
className={`${firaMono.variable} ${firaSans.variable} ${styles.layout}`}
>
{children}
</div>
)
}

View File

@@ -0,0 +1,31 @@
.content {
display: grid;
padding-bottom: 7.7rem;
padding-left: 0;
padding-right: 0;
position: relative;
}
.blocks {
display: grid;
gap: 4.2rem;
padding: 1.6rem;
}
@media screen and (min-width: 950px) {
.content {
gap: 2.7rem;
grid-template-columns: 30rem 1fr;
padding-bottom: 17.5rem;
padding-left: 2.4rem;
padding-right: 2.4rem;
padding-top: 5.8rem;
}
.blocks {
gap: 6.4rem;
padding-left: 0;
padding-right: 0;
grid-column: 2 / -1;
}
}

View File

@@ -0,0 +1,39 @@
import { notFound } from "next/navigation"
import { serverClient } from "@/lib/trpc/server"
import { Blocks } from "@/components/Loyalty/Blocks"
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,
}: PageArgs<LangParams, UriParams>) {
try {
if (!searchParams.uri) {
throw new Error("Bad URI")
}
const loyaltyPage = await serverClient().contentstack.loyaltyPage.get({
href: searchParams.uri,
locale: params.lang,
})
return (
<section className={styles.content}>
{loyaltyPage.sidebar ? <Sidebar blocks={loyaltyPage.sidebar} /> : null}
<MaxWidth className={styles.blocks} tag="main">
<Blocks blocks={loyaltyPage.blocks} />
</MaxWidth>
</section>
)
} catch (err) {
return notFound()
}
}

View File

@@ -1,5 +1,6 @@
:root {
font-size: 62.5%;
--max-width: 113.5rem;
}
* {

View File

@@ -29,6 +29,10 @@ function extractPossibleAttributes(attrs: Attributes) {
props.className = attrs["class-name"]
} else if (attrs.classname) {
props.className = attrs.classname
} else if (attrs?.style?.["text-align"]) {
props.style = {
textAlign: attrs?.style?.["text-align"],
}
}
return props
@@ -250,6 +254,11 @@ export const renderOptions: RenderOptions = {
const image = embeds?.[node?.attrs?.["asset-uid"]]
if (image.node.__typename === EmbedEnum.SysAsset) {
const alt = image?.node?.title ?? node.attrs.alt
const alignment = node.attrs?.style?.["text-align"]
? {
alignSelf: node.attrs?.style?.["text-align"],
}
: {}
return (
<Image
key={node.uid}
@@ -258,6 +267,7 @@ export const renderOptions: RenderOptions = {
height={image.node.dimension.height}
src={image?.node?.url}
width={image.node.dimension.width}
style={alignment}
/>
)
}

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,41 @@
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,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

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

View File

@@ -0,0 +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 (
<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 (
<article 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>
))}
</article>
)
}

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,3 @@
export default function OverviewTable() {
return <div></div>
}

View File

@@ -0,0 +1,29 @@
.container {
display: grid;
gap: 2.4rem;
overflow: hidden;
margin-right: -1.6rem;
padding-right: 1.6rem;
}
.titleContainer {
display: grid;
grid-template-areas: "title link" "subtitle subtitle";
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;
grid-area: subtitle;
}

View File

@@ -0,0 +1,68 @@
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 type {
DynamicComponentProps,
DynamicContentProps,
} from "@/types/components/loyalty/blocks"
import { LoyaltyComponentEnum } from "@/types/requests/loyaltyPage"
function DynamicComponentBlock({ component }: DynamicComponentProps) {
switch (component) {
case LoyaltyComponentEnum.how_it_works:
return <HowItWorks />
case LoyaltyComponentEnum.loyalty_levels:
return <LoyaltyLevels />
case LoyaltyComponentEnum.overview_table:
// TODO: IMPLEMENT OVERVIEW TABLE!
return <OverviewTable />
default:
return null
}
}
export default function DynamicContent({
dynamicContent,
}: DynamicContentProps) {
return (
<section className={styles.container}>
<header className={styles.titleContainer}>
{dynamicContent.title && (
<Title
as="h3"
level="h2"
className={styles.title}
weight="semiBold"
uppercase
>
{dynamicContent.title}
</Title>
)}
{dynamicContent.link ? (
<Link className={styles.link} href={dynamicContent.link.href}>
{dynamicContent.link.text}
</Link>
) : null}
{dynamicContent.subtitle && (
<Title
as="h5"
level="h3"
weight="regular"
className={styles.subtitle}
>
{dynamicContent.subtitle}
</Title>
)}
</header>
<div>
<DynamicComponentBlock component={dynamicContent.component} />
</div>
</section>
)
}

View File

@@ -0,0 +1,29 @@
import JsonToHtml from "@/components/JsonToHtml"
import DynamicContentBlock from "@/components/Loyalty/Blocks/DynamicContent"
import CardGrid from "./CardGrid"
import type { BlocksProps } from "@/types/components/loyalty/blocks"
import { LoyaltyBlocksTypenameEnum } from "@/types/requests/loyaltyPage"
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>
<JsonToHtml
nodes={block.content.content.json.children}
embeds={block.content.content.embedded_itemsConnection.edges}
/>
</section>
)
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksDynamicContent:
return <DynamicContentBlock dynamicContent={block.dynamic_content} />
default:
return null
}
})
}

View File

@@ -0,0 +1,21 @@
.container {
display: grid;
text-align: center;
gap: 0.4rem;
padding: 1rem;
}
.title {
font-family: var(--fira-sans);
font-size: 1.6rem;
font-weight: 700;
margin: 0;
}
.value {
font-family: var(--fira-sans);
font-size: 1.6rem;
font-weight: 400;
margin: 0;
}

View File

@@ -0,0 +1,27 @@
import { Lang } from "@/constants/languages"
import { serverClient } from "@/lib/trpc/server"
import { getValueFromContactConfig } from "@/utils/contactConfig"
import styles from "./contactRow.module.css"
import type { ContactRowProps } from "@/types/components/loyalty/sidebar"
export default async function ContactRow({ contact }: ContactRowProps) {
const data = await serverClient().contentstack.contactConfig.get({
lang: Lang.en,
})
const val = getValueFromContactConfig(contact.contact_field, data)
if (!val) {
return null
}
return (
<div className={styles.container}>
<h4 className={styles.title}>{contact.display_text}</h4>
<p className={styles.value}>{val}</p>
</div>
)
}

View File

@@ -0,0 +1,16 @@
.contactContainer {
display: none;
}
@media screen and (min-width: 950px) {
.contactContainer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-top: 0.5px solid var(--Base-Border-Disabled);
padding: 3.4rem;
text-align: center;
gap: 6.2rem;
}
}

View File

@@ -0,0 +1,33 @@
import { _ } from "@/lib/translation"
import Title from "@/components/Title"
import ContactRow from "./ContactRow"
import styles from "./contact.module.css"
import type { ContactProps } from "@/types/components/loyalty/sidebar"
import { JoinLoyaltyContactTypenameEnum } from "@/types/requests/loyaltyPage"
export default async function Contact({ contactBlock }: ContactProps) {
return (
<div className={styles.contactContainer}>
<Title level="h5">{_("Contact us")}</Title>
<section>
{contactBlock.map(({ contact, __typename }, i) => {
switch (__typename) {
case JoinLoyaltyContactTypenameEnum.LoyaltyPageSidebarJoinLoyaltyContactBlockContactContact:
return (
<ContactRow
key={`${contact.display_text}-${i}`}
contact={contact}
/>
)
default:
return null
}
})}
</section>
</div>
)
}

View File

@@ -0,0 +1,40 @@
import { _ } from "@/lib/translation"
import Image from "@/components/Image"
import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link"
import Title from "@/components/Title"
import Contact from "./Contact"
import styles from "./joinLoyalty.module.css"
import type { JoinLoyaltyContactProps } from "@/types/components/loyalty/sidebar"
export default function JoinLoyaltyContact({ block }: JoinLoyaltyContactProps) {
return (
<div className={styles.container}>
<div className={styles.wrapper}>
{block.title && <Title level="h3">{block.title}</Title>}
<Image
alt="Scandic Friends"
className={styles.image}
height={65}
src="/_static/icons/scandic-friends.png"
width={203}
/>
{block.preamble && <p className={styles.preamble}>{block.preamble}</p>}
<Button intent="primary">
<span>{_("Join Scandic Friends")}</span>
</Button>
<div className={styles.linkContainer}>
<Link href="/login" className={styles.loginLink}>
{_("Already a friend?")} <br />
{_("Click here to log in")}
</Link>
</div>
</div>
{block.contact && <Contact contactBlock={block.contact} />}
</div>
)
}

View File

@@ -0,0 +1,54 @@
.container {
display: grid;
font-weight: 600;
background-color: var(--Base-Background-Elevated);
}
.wrapper {
display: flex;
align-items: center;
flex-direction: column;
gap: 2rem;
padding: 6rem 2rem;
}
.preamble {
font-family: var(--fira-sans);
font-size: 1.6rem;
font-weight: 400;
line-height: 2.4rem;
text-align: center;
margin: 0;
}
.loginLink {
text-decoration: none;
color: var(--some-black-color, #2e2e2e);
font-size: 1.2rem;
}
.linkContainer {
text-align: center;
}
.contactContainer {
display: none;
}
@media screen and (min-width: 950px) {
.container {
border-radius: 32px 4px 4px 32px;
}
.wrapper {
gap: 3rem;
}
.contactContainer {
display: block;
border-top: 0.5px solid var(--Base-Border-Disabled);
display: flex;
justify-content: center;
padding: 3.4rem;
}
}

View File

@@ -0,0 +1,32 @@
import JsonToHtml from "@/components/JsonToHtml"
import JoinLoyaltyContact from "./JoinLoyalty"
import styles from "./sidebar.module.css"
import { SidebarProps } from "@/types/components/loyalty/sidebar"
import { SidebarTypenameEnum } from "@/types/requests/loyaltyPage"
export default function SidebarLoyalty({ blocks }: SidebarProps) {
return (
<aside>
{blocks.map((block) => {
switch (block.__typename) {
case SidebarTypenameEnum.LoyaltyPageSidebarContent:
return (
<section className={styles.content}>
<JsonToHtml
embeds={block.content.content.embedded_itemsConnection.edges}
nodes={block.content.content.json.children}
/>
</section>
)
case SidebarTypenameEnum.LoyaltyPageSidebarJoinLoyaltyContact:
return <JoinLoyaltyContact block={block.join_loyalty_contact} />
default:
return null
}
})}
</aside>
)
}

View File

@@ -0,0 +1,5 @@
@media screen and (max-width: 950px) {
.content {
padding: 0 1.6rem;
}
}

View File

@@ -2,7 +2,7 @@ import Link from "next/link"
import { serverClient } from "@/lib/trpc/server"
import Title from "@/components/MyPages/Title"
import Title from "@/components/Title"
import styles from "./current.module.css"

View File

@@ -3,8 +3,8 @@ import { Lock } from "react-feather"
import { serverClient } from "@/lib/trpc/server"
import Title from "@/components/MyPages/Title"
import Button from "@/components/TempDesignSystem/Button"
import Title from "@/components/Title"
import styles from "./next.module.css"

View File

@@ -1,5 +1,5 @@
import Image from "@/components/Image"
import Title from "@/components/MyPages/Title"
import Title from "@/components/Title"
import styles from "./challenges.module.css"

View File

@@ -3,8 +3,8 @@ import { serverClient } from "@/lib/trpc/server"
import StayCard from "@/components/MyPages/Blocks/Stays/StayCard"
import EmptyUpcomingStaysBlock from "@/components/MyPages/Blocks/Stays/Upcoming/EmptyUpcomingStays"
import Title from "@/components/MyPages/Title"
import Link from "@/components/TempDesignSystem/Link"
import Title from "@/components/Title"
import styles from "./upcoming.module.css"

View File

@@ -1,4 +1,4 @@
import Title from "@/components/MyPages/Title"
import Title from "@/components/Title"
import Friend from "./Friend"
import Stats from "./Stats"

View File

@@ -1,7 +1,7 @@
import Link from "next/link"
import Image from "@/components/Image"
import Title from "@/components/MyPages/Title"
import Title from "@/components/Title"
import styles from "./shortcuts.module.css"

View File

@@ -1,4 +1,4 @@
import Title from "@/components/MyPages/Title"
import Title from "@/components/Title"
import styles from "./header.module.css"

View File

@@ -1,6 +1,6 @@
import { _ } from "@/lib/translation"
import Title from "@/components/MyPages/Title"
import Title from "@/components/Title"
import styles from "./emptyPreviousStays.module.css"

View File

@@ -1,7 +1,7 @@
import { dt } from "@/lib/dt"
import Image from "@/components/Image"
import Title from "@/components/MyPages/Title"
import Title from "@/components/Title"
import styles from "./stay.module.css"

View File

@@ -2,8 +2,8 @@ import Link from "next/link"
import { _ } from "@/lib/translation"
import Title from "@/components/MyPages/Title"
import Button from "@/components/TempDesignSystem/Button"
import Title from "@/components/Title"
import styles from "./emptyUpcomingStays.module.css"

View File

@@ -5,8 +5,8 @@ import { GetNavigationMyPages } from "@/lib/graphql/Query/NavigationMyPages.grap
import { request } from "@/lib/graphql/request"
import Link from "@/components/TempDesignSystem/Link"
import Title from "@/components/Title"
import Title from "../Title"
import { mapMenuItems } from "./helpers"
import styles from "./sidebar.module.css"

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 (
<article 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}
</article>
)
}

View File

@@ -82,4 +82,4 @@
font-size: var(--typography-Title5-Desktop-fontSize);
line-height: var(--typography-Title5-Desktop-lineHeight);
}
}
}

View File

@@ -0,0 +1,8 @@
fragment AccountPageLink on AccountPage {
system {
locale
uid
}
title
url
}

View File

@@ -0,0 +1,8 @@
fragment ContentPageLink on ContentPage {
system {
locale
uid
}
url
title
}

View File

@@ -0,0 +1,8 @@
fragment CurrentBlocksPageLink on CurrentBlocksPage {
system {
locale
uid
}
title
url
}

View File

@@ -0,0 +1,8 @@
fragment LoyaltyPageLink on LoyaltyPage {
system {
locale
uid
}
title
url
}

View File

@@ -1,35 +0,0 @@
fragment CurrentBlocksPageLink on CurrentBlocksPage {
system {
locale
uid
}
title
url
}
fragment AccountPageLink on AccountPage {
system {
locale
uid
}
title
url
}
fragment LoyaltyPageLink on LoyaltyPage {
system {
locale
uid
}
title
url
}
fragment ContentPageLink on ContentPage {
system {
locale
uid
}
url
title
}

View File

@@ -0,0 +1,37 @@
query GetContactConfig($locale: String!) {
all_contact_config(locale: $locale) {
items {
email {
address
name
}
email_loyalty {
address
name
}
mailing_address {
name
street
zip
country
city
}
phone {
number
name
}
phone_loyalty {
name
number
}
title
visiting_address {
country
city
street
zip
}
}
total
}
}

View File

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

View File

@@ -0,0 +1,120 @@
#import "../Fragments/Image.graphql"
#import "../Fragments/PageLink/AccountPageLink.graphql"
#import "../Fragments/PageLink/ContentPageLink.graphql"
#import "../Fragments/PageLink/LoyaltyPageLink.graphql"
query GetLoyaltyPage($locale: String!, $url: String!) {
all_loyalty_page(where: { url: $url, locale: $locale }) {
items {
blocks {
__typename
... on LoyaltyPageBlocksDynamicContent {
dynamic_content {
title
subtitle
component
link {
text
pageConnection {
edges {
node {
...ContentPageLink
...LoyaltyPageLink
}
}
totalCount
}
}
}
}
... on LoyaltyPageBlocksCardGrid {
card_grid {
title
subtitle
cards {
referenceConnection {
edges {
node {
__typename
...LoyaltyPageLink
...ContentPageLink
...AccountPageLink
}
}
totalCount
}
title
subtitle
open_in_new_tab
}
}
}
... on LoyaltyPageBlocksContent {
content {
content {
json
embedded_itemsConnection {
edges {
node {
__typename
...Image
}
}
totalCount
}
}
}
}
}
title
sidebar {
__typename
... on LoyaltyPageSidebarJoinLoyaltyContact {
join_loyalty_contact {
title
preamble
contact {
... on LoyaltyPageSidebarJoinLoyaltyContactBlockContactContact {
__typename
contact {
display_text
contact_field
}
}
}
}
}
... on LoyaltyPageSidebarContent {
content {
content {
json
embedded_itemsConnection {
edges {
node {
__typename
...Image
}
}
totalCount
}
}
}
}
}
web {
breadcrumbs {
title
parents {
href
title
}
}
}
system {
uid
created_at
updated_at
}
}
}
}

View File

@@ -1,4 +1,6 @@
#import "../Fragments/PageLinks.graphql"
#import "../Fragments/PageLink/AccountPageLink.graphql"
#import "../Fragments/PageLink/ContentPageLink.graphql"
#import "../Fragments/PageLink/LoyaltyPageLink.graphql"
query GetNavigationMyPages($locale: String!) {
all_navigation_my_pages(locale: $locale) {

View File

@@ -2,6 +2,8 @@ import { NextResponse } from "next/server"
import { findLang } from "@/constants/languages"
import { getContentTypeByPathName, PageTypeEnum } from "@/utils/contentType"
import type { NextMiddleware } from "next/server"
import { MiddlewareMatcher } from "@/types/middleware"
@@ -10,10 +12,11 @@ export const middleware: NextMiddleware = async (request) => {
const { nextUrl } = request
const lang = findLang(nextUrl.pathname)
const contentType = "currentContentPage"
const pathNameWithoutLang = nextUrl.pathname.replace(`/${lang}`, "")
const searchParams = new URLSearchParams(request.nextUrl.searchParams)
const contentType = await getContentTypeByPathName(pathNameWithoutLang, lang)
if (request.nextUrl.pathname.includes("preview")) {
searchParams.set("uri", pathNameWithoutLang.replace("/preview", ""))
return NextResponse.rewrite(
@@ -23,13 +26,21 @@ export const middleware: NextMiddleware = async (request) => {
searchParams.set("uri", pathNameWithoutLang)
switch (contentType) {
case "currentContentPage":
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)
)
// case PageTypeEnum.ContentPage:
// return NextResponse.rewrite(
// new URL(`/${lang}/content-page?${searchParams.toString()}`, nextUrl)
// )
default:
return NextResponse.next()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -3,10 +3,12 @@ import { router } from "./trpc"
/** Routers */
import { contentstackRouter } from "./routers/contentstack"
import { userRouter } from "./routers/user"
import { loyaltyRouter } from "./routers/loyalty"
export const appRouter = router({
contentstack: contentstackRouter,
user: userRouter,
loyalty: loyaltyRouter,
})
export type AppRouter = typeof appRouter

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 { 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({

View File

@@ -0,0 +1,5 @@
import { mergeRouters } from "@/server/trpc"
import { contactConfigQueryRouter } from "./query"
export const contactConfigRouter = mergeRouters(contactConfigQueryRouter)

View File

@@ -0,0 +1,5 @@
import { z } from "zod"
import { Lang } from "@/constants/languages"
export const getConfigInput = z.object({ lang: z.nativeEnum(Lang) })

View File

@@ -0,0 +1,60 @@
import { z } from "zod"
// Help me write this zod schema based on the type ContactConfig
export const validateContactConfigSchema = z.object({
all_contact_config: z.object({
items: z.array(
z.object({
email: z.object({
name: z.string().nullable(),
address: z.string().nullable(),
}),
email_loyalty: z.object({
name: z.string().nullable(),
address: z.string().nullable(),
}),
mailing_address: z.object({
zip: z.string().nullable(),
street: z.string().nullable(),
name: z.string().nullable(),
city: z.string().nullable(),
country: z.string().nullable(),
}),
phone: z.object({
number: z.string().nullable(),
name: z.string().nullable(),
}),
phone_loyalty: z.object({
number: z.string().nullable(),
name: z.string().nullable(),
}),
visiting_address: z.object({
zip: z.string().nullable(),
country: z.string().nullable(),
city: z.string().nullable(),
street: z.string().nullable(),
}),
})
),
}),
})
export enum ContactFieldGroupsEnum {
email = "email",
email_loyalty = "email_loyalty",
mailing_address = "mailing_address",
phone = "phone",
phone_loyalty = "phone_loyalty",
visiting_address = "visiting_address",
}
export type ContactFieldGroups = keyof typeof ContactFieldGroupsEnum
export type ContactConfigData = z.infer<typeof validateContactConfigSchema>
export type ContactConfig = ContactConfigData["all_contact_config"]["items"][0]
export type ContactFields = {
display_text?: string
contact_field: string
}

View File

@@ -0,0 +1,34 @@
import { GetContactConfig } from "@/lib/graphql/Query/ContactConfig.graphql"
import { request } from "@/lib/graphql/request"
import { badRequestError } from "@/server/errors/trpc"
import { publicProcedure, router } from "@/server/trpc"
import { getConfigInput } from "./input"
import { type ContactConfigData, validateContactConfigSchema } from "./output"
export const contactConfigQueryRouter = router({
get: publicProcedure.input(getConfigInput).query(async ({ input }) => {
try {
const contactConfig = await request<ContactConfigData>(GetContactConfig, {
locale: input.lang,
})
if (!contactConfig.data) {
throw badRequestError()
}
const validatedContactConfigConfig =
validateContactConfigSchema.safeParse(contactConfig.data)
if (!validatedContactConfigConfig.success) {
console.error(validatedContactConfigConfig.error)
throw badRequestError()
}
return validatedContactConfigConfig.data.all_contact_config.items[0]
} catch (e) {
console.log(e)
throw badRequestError()
}
}),
})

View File

@@ -1,7 +1,11 @@
import { router } from "@/server/trpc"
import { breadcrumbsRouter } from "./breadcrumbs"
import { contactConfigRouter } from "./contactConfig"
import { loyaltyPageRouter } from "./loyaltyPage"
export const contentstackRouter = router({
breadcrumbs: breadcrumbsRouter,
loyaltyPage: loyaltyPageRouter,
contactConfig: contactConfigRouter,
})

View File

@@ -0,0 +1,5 @@
import { mergeRouters } from "@/server/trpc"
import { loyaltyPageQueryRouter } from "./query"
export const loyaltyPageRouter = mergeRouters(loyaltyPageQueryRouter)

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

@@ -0,0 +1,208 @@
import { z } from "zod"
import { Embeds } from "@/types/requests/embeds"
import {
JoinLoyaltyContactTypenameEnum,
LoyaltyBlocksTypenameEnum,
LoyaltyComponentEnum,
SidebarTypenameEnum,
} from "@/types/requests/loyaltyPage"
import { Edges } 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().optional(),
subtitle: z.string().optional(),
cards: z.array(
z.object({
title: z.string().optional(),
subtitle: z.string().optional(),
referenceConnection: z.object({
edges: z.array(
z.object({
node: z.object({
system: z.object({
uid: z.string(),
}),
url: z.string(),
title: z.string(),
__typename: z.string(),
}),
})
),
totalCount: z.number(),
}),
open_in_new_tab: z.boolean(),
})
),
}),
})
const loyaltyPageDynamicContent = z.object({
__typename: z.literal(
LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksDynamicContent
),
dynamic_content: z.object({
title: z.string().optional(),
subtitle: z.string().optional(),
component: z.nativeEnum(LoyaltyComponentEnum),
link: z.object({
text: z.string().optional(),
pageConnection: z.object({
edges: z.array(
z.object({
node: z.object({
system: z.object({
uid: z.string(),
}),
url: z.string(),
title: z.string(),
}),
})
),
totalCount: z.number(),
}),
}),
}),
})
const loyaltyPageBlockTextContent = z.object({
__typename: z.literal(LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksContent),
content: z.object({
content: z.object({
embedded_itemsConnection: z.object({
edges: z.array(z.any()),
totalCount: z.number(),
}),
json: z.any(),
}),
}),
})
const loyaltyPageBlockItem = z.discriminatedUnion("__typename", [
loyaltyPageBlockCardGrid,
loyaltyPageDynamicContent,
loyaltyPageBlockTextContent,
])
const loyaltyPageSidebarTextContent = z.object({
__typename: z.literal(SidebarTypenameEnum.LoyaltyPageSidebarContent),
content: z.object({
content: z.object({
embedded_itemsConnection: z.object({
edges: z.array(z.any()),
totalCount: z.number(),
}),
json: z.any(),
}),
}),
})
const loyaltyPageJoinLoyaltyContact = z.object({
__typename: z.literal(
SidebarTypenameEnum.LoyaltyPageSidebarJoinLoyaltyContact
),
join_loyalty_contact: z.object({
title: z.string().optional(),
preamble: z.string().optional(),
contact: z.array(
z.object({
__typename: z.literal(
JoinLoyaltyContactTypenameEnum.LoyaltyPageSidebarJoinLoyaltyContactBlockContactContact
),
contact: z.object({
display_text: z.string().optional(),
contact_field: z.string(),
}),
})
),
}),
})
const loyaltyPageSidebarItem = z.discriminatedUnion("__typename", [
loyaltyPageSidebarTextContent,
loyaltyPageJoinLoyaltyContact,
])
export const validateLoyaltyPageSchema = z.object({
all_loyalty_page: z.object({
items: z.array(
z.object({
title: z.string(),
blocks: z.array(loyaltyPageBlockItem),
sidebar: z.array(loyaltyPageSidebarItem),
})
),
}),
})
// 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"> & {
dynamic_content: Omit<DynamicContentRaw["dynamic_content"], "link"> & {
link:
| {
href: string
title: string
text?: string
}
| undefined
}
}
type BlockContentRaw = z.infer<typeof loyaltyPageBlockTextContent>
export interface RteBlockContent extends BlockContentRaw {
content: {
content: {
json: RTEDocument
embedded_itemsConnection: Edges<Embeds>
}
}
}
export type Block = CardGrid | RteBlockContent | DynamicContent
// Sidebar block types
type SidebarContentRaw = z.infer<typeof loyaltyPageSidebarTextContent>
export type RteSidebarContent = Omit<SidebarContentRaw, "content"> & {
content: {
content: {
json: RTEDocument
embedded_itemsConnection: Edges<Embeds>
}
}
}
export type JoinLoyaltyContact = z.infer<typeof loyaltyPageJoinLoyaltyContact>
export type Sidebar = JoinLoyaltyContact | RteSidebarContent
type LoyaltyPageDataRaw = z.infer<typeof validateLoyaltyPageSchema>
type LoyaltyPageRaw = LoyaltyPageDataRaw["all_loyalty_page"]["items"][0]
export type LoyaltyPage = Omit<LoyaltyPageRaw, "blocks" | "sidebar"> & {
blocks: Block[]
sidebar: Sidebar[]
}

View File

@@ -0,0 +1,130 @@
import GetLoyaltyPage from "@/lib/graphql/Query/LoyaltyPage.graphql"
import { request } from "@/lib/graphql/request"
import { badRequestError } from "@/server/errors/trpc"
import { publicProcedure, router } from "@/server/trpc"
import { getLoyaltyPageInput } from "./input"
import { type LoyaltyPage, validateLoyaltyPageSchema } from "./output"
import { Embeds } from "@/types/requests/embeds"
import {
LoyaltyBlocksTypenameEnum,
SidebarTypenameEnum,
} from "@/types/requests/loyaltyPage"
import { Edges } from "@/types/requests/utils/edges"
import { RTEDocument } from "@/types/rte/node"
export const loyaltyPageQueryRouter = router({
get: publicProcedure.input(getLoyaltyPageInput).query(async ({ input }) => {
try {
const loyaltyPageRes = await request<LoyaltyPage>(GetLoyaltyPage, {
locale: input.locale,
url: input.href,
})
if (!loyaltyPageRes.data) {
throw badRequestError()
}
const validatedLoyaltyPage = validateLoyaltyPageSchema.safeParse(
loyaltyPageRes.data
)
if (!validatedLoyaltyPage.success) {
console.error(validatedLoyaltyPage.error)
throw badRequestError()
}
const sidebar =
validatedLoyaltyPage.data.all_loyalty_page.items[0].sidebar.map(
(block) => {
if (
block.__typename == SidebarTypenameEnum.LoyaltyPageSidebarContent
) {
return {
...block,
content: {
content: {
json: block.content.content.json as RTEDocument,
embedded_itemsConnection: block.content.content
.embedded_itemsConnection as Edges<Embeds>,
},
},
}
} else {
return block
}
}
)
const blocks =
validatedLoyaltyPage.data.all_loyalty_page.items[0].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.url,
title:
card.referenceConnection.edges[0].node.title,
}
: undefined,
}
}),
},
}
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksDynamicContent:
return {
...block,
dynamic_content: {
...block.dynamic_content,
link: block.dynamic_content.link.pageConnection.totalCount
? {
text: block.dynamic_content.link.text,
href: block.dynamic_content.link.pageConnection
.edges[0].node.url,
title:
block.dynamic_content.link.pageConnection.edges[0]
.node.title,
}
: undefined,
},
}
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksContent:
return {
...block,
content: {
content: {
json: block.content.content.json as RTEDocument,
embedded_itemsConnection: block.content.content
.embedded_itemsConnection as Edges<Embeds>,
},
},
}
default:
return block
}
}
)
const loyaltyPage = {
...validatedLoyaltyPage.data.all_loyalty_page.items[0],
blocks,
sidebar,
} as LoyaltyPage
return loyaltyPage
} catch (error) {
console.info(`Get Loyalty Page Error`)
console.error(error)
throw badRequestError()
}
}),
})

View File

@@ -0,0 +1,5 @@
import { mergeRouters } from "@/server/trpc"
import { lotaltyQueryRouter } from "./query"
export const loyaltyRouter = mergeRouters(lotaltyQueryRouter)

View File

@@ -0,0 +1,28 @@
import { protectedProcedure, publicProcedure, router } from "@/server/trpc"
import { allLevels } from "./temp"
function fakingRequest<T>(payload: T): Promise<T> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(payload)
}, 1500)
})
}
export const lotaltyQueryRouter = router({
levels: router({
all: publicProcedure.query(async function ({ ctx }) {
// TODO: Make request to get user data from Scandic API
return await fakingRequest<typeof allLevels>(allLevels)
}),
current: protectedProcedure.query(async function (opts) {
// TODO: Make request to get user data from Scandic API
return await fakingRequest<(typeof allLevels)[number]>(allLevels[1])
}),
next: protectedProcedure.query(async function (opts) {
// TODO: Make request to get user data from Scandic API
return await fakingRequest<(typeof allLevels)[number]>(allLevels[2])
}),
}),
})

View File

@@ -0,0 +1,86 @@
export const allLevels = [
{
tier: 1,
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",
},
{
tier: 2,
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",
},
{
tier: 3,
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",
},
{
tier: 4,
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",
},
{
tier: 5,
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",
},
{
tier: 6,
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",
},
{
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,11 +1,12 @@
import { initTRPC } from "@trpc/server"
import { env } from "@/env/server"
import { transformer } from "./transformer"
import { unauthorizedError } from "./errors/trpc"
import type { Context } from "./context"
import { unauthorizedError } from "./errors/trpc"
import { transformer } from "./transformer"
import type { Meta } from "@/types/trpc/meta"
import type { Context } from "./context"
const t = initTRPC.context<Context>().meta<Meta>().create({ transformer })

View File

@@ -1,8 +1,7 @@
import type { RTENode } from "../rte/node"
import type { Node } from "@/types/requests/utils/edges"
import type { RenderOptions } from "../rte/option"
import type { Embeds } from "@/types/requests/embeds"
import type { Node } from "@/types/requests/utils/edges"
import type { RTENode } from "../rte/node"
import type { RenderOptions } from "../rte/option"
export type JsonToHtmlProps = {
embeds: Node<Embeds>[]

View File

@@ -0,0 +1,33 @@
import {
Block,
CardGrid,
DynamicContent,
RteBlockContent,
} from "@/server/routers/contentstack/loyaltyPage/output"
export type BlocksProps = {
blocks: Block[]
}
export type DynamicContentProps = {
dynamicContent: DynamicContent["dynamic_content"]
}
export type DynamicComponentProps = {
component: DynamicContent["dynamic_content"]["component"]
}
export type CardGridProps = Pick<CardGrid, "card_grid">
export type Content = { content: RteBlockContent["content"]["content"] }
export type LevelCardProps = {
level: {
tier: number
name: string
requiredPoints: number
requiredNights: string
topBenefits: string[]
logo: string
}
}

View File

@@ -0,0 +1,21 @@
import { ContactFields } from "@/server/routers/contentstack/contactConfig/output"
import {
JoinLoyaltyContact,
Sidebar,
} from "@/server/routers/contentstack/loyaltyPage/output"
export type SidebarProps = {
blocks: Sidebar[]
}
export type JoinLoyaltyContactProps = {
block: JoinLoyaltyContact["join_loyalty_contact"]
}
export type ContactProps = {
contactBlock: JoinLoyaltyContact["join_loyalty_contact"]["contact"]
}
export type ContactRowProps = {
contact: ContactFields
}

View File

@@ -1,10 +1,12 @@
import { headingVariants } from "@/components/MyPages/Title/variants"
import { headingVariants } from "@/components/Title/variants"
import type { VariantProps } from "class-variance-authority"
type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"
export interface HeadingProps extends React.HTMLAttributes<HTMLHeadingElement>, VariantProps<typeof headingVariants> {
export interface HeadingProps
extends React.HTMLAttributes<HTMLHeadingElement>,
VariantProps<typeof headingVariants> {
as?: HeadingLevel
level?: HeadingLevel
uppercase?: boolean

View File

@@ -4,7 +4,7 @@ export type Image = {
height: number
width: number
}
metadata: JSON
metadata: JSON | null
system: {
uid: string
}

View File

@@ -0,0 +1,13 @@
import z from "zod"
export const validateContentTypeUid = z.object({
all_content_page: z.object({
total: z.number(),
}),
all_loyalty_page: z.object({
total: z.number(),
}),
all_current_blocks_page: z.object({
total: z.number(),
}),
})

View File

@@ -0,0 +1,32 @@
import type { JoinLoyaltyContact } from "@/server/routers/contentstack/loyaltyPage/output"
import type { Typename } from "./utils/typename"
export enum JoinLoyaltyContactTypenameEnum {
LoyaltyPageSidebarJoinLoyaltyContactBlockContactContact = "LoyaltyPageSidebarJoinLoyaltyContactBlockContactContact",
}
export type JoinLoyaltyContactContact = Typename<
JoinLoyaltyContact["join_loyalty_contact"],
JoinLoyaltyContactTypenameEnum.LoyaltyPageSidebarJoinLoyaltyContactBlockContactContact
>
export enum SidebarTypenameEnum {
LoyaltyPageSidebarJoinLoyaltyContact = "LoyaltyPageSidebarJoinLoyaltyContact",
LoyaltyPageSidebarContent = "LoyaltyPageSidebarContent",
}
export type SidebarTypename = keyof typeof SidebarTypenameEnum
export enum LoyaltyComponentEnum {
loyalty_levels = "loyalty_levels",
how_it_works = "how_it_works",
overview_table = "overview_table",
}
export type LoyaltyComponent = keyof typeof LoyaltyComponentEnum
export enum LoyaltyBlocksTypenameEnum {
LoyaltyPageBlocksDynamicContent = "LoyaltyPageBlocksDynamicContent",
LoyaltyPageBlocksCardGrid = "LoyaltyPageBlocksCardGrid",
LoyaltyPageBlocksContent = "LoyaltyPageBlocksContent",
}

View File

@@ -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>

View File

@@ -1,11 +1,12 @@
import { RTETypeEnum } from "./enums"
import type { EmbedByUid } from "../components/jsontohtml"
import type {
Attributes,
RTEAnchorAttrs,
RTEAssetAttrs,
RTELinkAttrs,
} from "./attrs"
import type { EmbedByUid } from "../components/jsontohtml"
import type { RenderOptions } from "./option"
export interface RTEDefaultNode {

20
utils/contactConfig.ts Normal file
View File

@@ -0,0 +1,20 @@
import {
ContactConfig,
ContactFieldGroups,
} from "@/server/routers/contentstack/contactConfig/output"
export function getValueFromContactConfig(
keyString: string,
data: ContactConfig
): string | undefined {
const [groupName, key] = keyString.split(".") as [
ContactFieldGroups,
keyof ContactConfig[ContactFieldGroups],
]
if (data[groupName]) {
const fieldGroup = data[groupName]
return fieldGroup[key]
}
return undefined
}

54
utils/contentType.ts Normal file
View File

@@ -0,0 +1,54 @@
import { DocumentNode, print } from "graphql"
import { Lang } from "@/constants/languages"
import { env } from "@/env/server"
import { GetContentTypeUid } from "@/lib/graphql/Query/ContentTypeUid.graphql"
import { validateContentTypeUid } from "@/types/requests/contentTypeUid"
export enum PageTypeEnum {
CurrentBlocksPage = "CurrentBlocksPage",
LoyaltyPage = "LoyaltyPage",
ContentPage = "ContentPage",
}
export async function getContentTypeByPathName(
pathNameWithoutLang: string,
lang = Lang.en
) {
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 validatedContentTypeUid = validateContentTypeUid.safeParse(
pageTypeData.data
)
if (!validatedContentTypeUid.success) {
console.error(validatedContentTypeUid.error)
return null
}
const pageType = validatedContentTypeUid.data
if (pageType.all_content_page.total) {
return PageTypeEnum.ContentPage
} else if (pageType.all_loyalty_page.total) {
return PageTypeEnum.LoyaltyPage
} else if (pageType.all_current_blocks_page.total) {
return PageTypeEnum.CurrentBlocksPage
}
}