diff --git a/app/[lang]/(live)/(protected)/my-pages/benefits/page.tsx b/app/[lang]/(live)/(protected)/my-pages/benefits/page.tsx
index c3aba6b90..ada9f3e70 100644
--- a/app/[lang]/(live)/(protected)/my-pages/benefits/page.tsx
+++ b/app/[lang]/(live)/(protected)/my-pages/benefits/page.tsx
@@ -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"
diff --git a/app/[lang]/(live)/(protected)/my-pages/layout.module.css b/app/[lang]/(live)/(protected)/my-pages/layout.module.css
index bc197d5f7..4782b4cb4 100644
--- a/app/[lang]/(live)/(protected)/my-pages/layout.module.css
+++ b/app/[lang]/(live)/(protected)/my-pages/layout.module.css
@@ -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;
}
-}
\ No newline at end of file
+}
diff --git a/app/[lang]/(live)/(public)/loyalty-page/layout.module.css b/app/[lang]/(live)/(public)/loyalty-page/layout.module.css
new file mode 100644
index 000000000..55219a7f1
--- /dev/null
+++ b/app/[lang]/(live)/(public)/loyalty-page/layout.module.css
@@ -0,0 +1,5 @@
+.layout {
+ display: grid;
+ font-family: var(--ff-fira-sans);
+ background-color: var(--Brand-Coffee-Subtle);
+}
diff --git a/app/[lang]/(live)/(public)/loyalty-page/layout.tsx b/app/[lang]/(live)/(public)/loyalty-page/layout.tsx
new file mode 100644
index 000000000..aa011671a
--- /dev/null
+++ b/app/[lang]/(live)/(public)/loyalty-page/layout.tsx
@@ -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 (
+
+ {children}
+
+ )
+}
diff --git a/app/[lang]/(live)/(public)/loyalty-page/page.module.css b/app/[lang]/(live)/(public)/loyalty-page/page.module.css
new file mode 100644
index 000000000..d6b3c2533
--- /dev/null
+++ b/app/[lang]/(live)/(public)/loyalty-page/page.module.css
@@ -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;
+ }
+}
diff --git a/app/[lang]/(live)/(public)/loyalty-page/page.tsx b/app/[lang]/(live)/(public)/loyalty-page/page.tsx
new file mode 100644
index 000000000..d35d2947c
--- /dev/null
+++ b/app/[lang]/(live)/(public)/loyalty-page/page.tsx
@@ -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) {
+ try {
+ if (!searchParams.uri) {
+ throw new Error("Bad URI")
+ }
+
+ const loyaltyPage = await serverClient().contentstack.loyaltyPage.get({
+ href: searchParams.uri,
+ locale: params.lang,
+ })
+
+ return (
+
+ {loyaltyPage.sidebar ? : null}
+
+
+
+
+
+ )
+ } catch (err) {
+ return notFound()
+ }
+}
diff --git a/app/globals.css b/app/globals.css
index e6547e169..f6581020a 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,5 +1,6 @@
:root {
font-size: 62.5%;
+ --max-width: 113.5rem;
}
* {
diff --git a/components/JsonToHtml/renderOptions.tsx b/components/JsonToHtml/renderOptions.tsx
index 2a6eb0d86..b9423cc48 100644
--- a/components/JsonToHtml/renderOptions.tsx
+++ b/components/JsonToHtml/renderOptions.tsx
@@ -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 (
)
}
diff --git a/components/Loyalty/Blocks/CardGrid/cardGrid.module.css b/components/Loyalty/Blocks/CardGrid/cardGrid.module.css
new file mode 100644
index 000000000..915c86f35
--- /dev/null
+++ b/components/Loyalty/Blocks/CardGrid/cardGrid.module.css
@@ -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;
+ }
+}
diff --git a/components/Loyalty/Blocks/CardGrid/index.tsx b/components/Loyalty/Blocks/CardGrid/index.tsx
new file mode 100644
index 000000000..cd457f550
--- /dev/null
+++ b/components/Loyalty/Blocks/CardGrid/index.tsx
@@ -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 (
+
+
+
+ {card_grid.title}
+
+ {card_grid.subtitle ? (
+
+ {card_grid.subtitle}
+
+ ) : null}
+
+
+ {card_grid.cards.map((card, i) => (
+
+
+
+ ))}
+
+
+ )
+}
diff --git a/components/Loyalty/Blocks/DynamicContent/HowItWorks/howItWorks.module.css b/components/Loyalty/Blocks/DynamicContent/HowItWorks/howItWorks.module.css
new file mode 100644
index 000000000..27a9a7561
--- /dev/null
+++ b/components/Loyalty/Blocks/DynamicContent/HowItWorks/howItWorks.module.css
@@ -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;
+}
diff --git a/components/Loyalty/Blocks/DynamicContent/HowItWorks/index.tsx b/components/Loyalty/Blocks/DynamicContent/HowItWorks/index.tsx
new file mode 100644
index 000000000..1ede3b337
--- /dev/null
+++ b/components/Loyalty/Blocks/DynamicContent/HowItWorks/index.tsx
@@ -0,0 +1,13 @@
+import Title from "@/components/Title"
+
+import styles from "./howItWorks.module.css"
+
+export default function HowItWorks() {
+ return (
+
+
+ How it works Placeholder
+
+
+ )
+}
diff --git a/components/Loyalty/Blocks/DynamicContent/LoyaltyLevels/index.tsx b/components/Loyalty/Blocks/DynamicContent/LoyaltyLevels/index.tsx
new file mode 100644
index 000000000..5541f649d
--- /dev/null
+++ b/components/Loyalty/Blocks/DynamicContent/LoyaltyLevels/index.tsx
@@ -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 (
+
+
+ {data.map((level) => (
+
+ ))}
+
+
+
+
+
+ )
+}
+
+function LevelCard({ level }: LevelCardProps) {
+ return (
+
+ {level.tier}
+
+
+ {level.requiredPoints} {_("or")} {level.requiredNights} {_("nights")}
+
+ {level.topBenefits.map((benefit) => (
+
+
+ {benefit}
+
+ ))}
+
+ )
+}
diff --git a/components/Loyalty/Blocks/DynamicContent/LoyaltyLevels/loyaltyLevels.module.css b/components/Loyalty/Blocks/DynamicContent/LoyaltyLevels/loyaltyLevels.module.css
new file mode 100644
index 000000000..affa9a47e
--- /dev/null
+++ b/components/Loyalty/Blocks/DynamicContent/LoyaltyLevels/loyaltyLevels.module.css
@@ -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;
+ }
+}
diff --git a/components/Loyalty/Blocks/DynamicContent/OverviewTable/index.tsx b/components/Loyalty/Blocks/DynamicContent/OverviewTable/index.tsx
new file mode 100644
index 000000000..c90c221e6
--- /dev/null
+++ b/components/Loyalty/Blocks/DynamicContent/OverviewTable/index.tsx
@@ -0,0 +1,3 @@
+export default function OverviewTable() {
+ return
+}
diff --git a/components/Loyalty/Blocks/DynamicContent/dynamicContent.module.css b/components/Loyalty/Blocks/DynamicContent/dynamicContent.module.css
new file mode 100644
index 000000000..b1ebc5a58
--- /dev/null
+++ b/components/Loyalty/Blocks/DynamicContent/dynamicContent.module.css
@@ -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;
+}
diff --git a/components/Loyalty/Blocks/DynamicContent/index.tsx b/components/Loyalty/Blocks/DynamicContent/index.tsx
new file mode 100644
index 000000000..ebf241098
--- /dev/null
+++ b/components/Loyalty/Blocks/DynamicContent/index.tsx
@@ -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
+ case LoyaltyComponentEnum.loyalty_levels:
+ return
+ case LoyaltyComponentEnum.overview_table:
+ // TODO: IMPLEMENT OVERVIEW TABLE!
+ return
+ default:
+ return null
+ }
+}
+
+export default function DynamicContent({
+ dynamicContent,
+}: DynamicContentProps) {
+ return (
+
+
+ {dynamicContent.title && (
+
+ {dynamicContent.title}
+
+ )}
+ {dynamicContent.link ? (
+
+ {dynamicContent.link.text}
+
+ ) : null}
+ {dynamicContent.subtitle && (
+
+ {dynamicContent.subtitle}
+
+ )}
+
+
+
+
+
+ )
+}
diff --git a/components/Loyalty/Blocks/index.tsx b/components/Loyalty/Blocks/index.tsx
new file mode 100644
index 000000000..58eba47a0
--- /dev/null
+++ b/components/Loyalty/Blocks/index.tsx
@@ -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
+ case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksContent:
+ return (
+
+ )
+ case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksDynamicContent:
+ return
+ default:
+ return null
+ }
+ })
+}
diff --git a/components/Loyalty/Sidebar/JoinLoyalty/Contact/ContactRow/contactRow.module.css b/components/Loyalty/Sidebar/JoinLoyalty/Contact/ContactRow/contactRow.module.css
new file mode 100644
index 000000000..47bd6b899
--- /dev/null
+++ b/components/Loyalty/Sidebar/JoinLoyalty/Contact/ContactRow/contactRow.module.css
@@ -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;
+}
diff --git a/components/Loyalty/Sidebar/JoinLoyalty/Contact/ContactRow/index.tsx b/components/Loyalty/Sidebar/JoinLoyalty/Contact/ContactRow/index.tsx
new file mode 100644
index 000000000..1e395f5a8
--- /dev/null
+++ b/components/Loyalty/Sidebar/JoinLoyalty/Contact/ContactRow/index.tsx
@@ -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 (
+
+
{contact.display_text}
+
{val}
+
+ )
+}
diff --git a/components/Loyalty/Sidebar/JoinLoyalty/Contact/contact.module.css b/components/Loyalty/Sidebar/JoinLoyalty/Contact/contact.module.css
new file mode 100644
index 000000000..38dbcdf35
--- /dev/null
+++ b/components/Loyalty/Sidebar/JoinLoyalty/Contact/contact.module.css
@@ -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;
+ }
+}
diff --git a/components/Loyalty/Sidebar/JoinLoyalty/Contact/index.tsx b/components/Loyalty/Sidebar/JoinLoyalty/Contact/index.tsx
new file mode 100644
index 000000000..577b4d48f
--- /dev/null
+++ b/components/Loyalty/Sidebar/JoinLoyalty/Contact/index.tsx
@@ -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 (
+
+
{_("Contact us")}
+
+ {contactBlock.map(({ contact, __typename }, i) => {
+ switch (__typename) {
+ case JoinLoyaltyContactTypenameEnum.LoyaltyPageSidebarJoinLoyaltyContactBlockContactContact:
+ return (
+
+ )
+ default:
+ return null
+ }
+ })}
+
+
+ )
+}
diff --git a/components/Loyalty/Sidebar/JoinLoyalty/index.tsx b/components/Loyalty/Sidebar/JoinLoyalty/index.tsx
new file mode 100644
index 000000000..6fc54eb0b
--- /dev/null
+++ b/components/Loyalty/Sidebar/JoinLoyalty/index.tsx
@@ -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 (
+
+
+ {block.title &&
{block.title}}
+
+ {block.preamble &&
{block.preamble}
}
+
+
+
+ {_("Already a friend?")}
+ {_("Click here to log in")}
+
+
+
+ {block.contact &&
}
+
+ )
+}
diff --git a/components/Loyalty/Sidebar/JoinLoyalty/joinLoyalty.module.css b/components/Loyalty/Sidebar/JoinLoyalty/joinLoyalty.module.css
new file mode 100644
index 000000000..442cb4dc0
--- /dev/null
+++ b/components/Loyalty/Sidebar/JoinLoyalty/joinLoyalty.module.css
@@ -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;
+ }
+}
diff --git a/components/Loyalty/Sidebar/index.tsx b/components/Loyalty/Sidebar/index.tsx
new file mode 100644
index 000000000..75fdcf92f
--- /dev/null
+++ b/components/Loyalty/Sidebar/index.tsx
@@ -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 (
+
+ )
+}
diff --git a/components/Loyalty/Sidebar/sidebar.module.css b/components/Loyalty/Sidebar/sidebar.module.css
new file mode 100644
index 000000000..b757da7a0
--- /dev/null
+++ b/components/Loyalty/Sidebar/sidebar.module.css
@@ -0,0 +1,5 @@
+@media screen and (max-width: 950px) {
+ .content {
+ padding: 0 1.6rem;
+ }
+}
diff --git a/components/MyPages/Blocks/Benefits/CurrentLevel/index.tsx b/components/MyPages/Blocks/Benefits/CurrentLevel/index.tsx
index 9371fde25..e106697c4 100644
--- a/components/MyPages/Blocks/Benefits/CurrentLevel/index.tsx
+++ b/components/MyPages/Blocks/Benefits/CurrentLevel/index.tsx
@@ -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"
diff --git a/components/MyPages/Blocks/Benefits/NextLevel/index.tsx b/components/MyPages/Blocks/Benefits/NextLevel/index.tsx
index 1e0fe88ed..8bb4fc277 100644
--- a/components/MyPages/Blocks/Benefits/NextLevel/index.tsx
+++ b/components/MyPages/Blocks/Benefits/NextLevel/index.tsx
@@ -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"
diff --git a/components/MyPages/Blocks/Challenges/index.tsx b/components/MyPages/Blocks/Challenges/index.tsx
index faa96b342..b1c5036e7 100644
--- a/components/MyPages/Blocks/Challenges/index.tsx
+++ b/components/MyPages/Blocks/Challenges/index.tsx
@@ -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"
diff --git a/components/MyPages/Blocks/Overview/UpcomingStays/index.tsx b/components/MyPages/Blocks/Overview/UpcomingStays/index.tsx
index 592276300..98786b46e 100644
--- a/components/MyPages/Blocks/Overview/UpcomingStays/index.tsx
+++ b/components/MyPages/Blocks/Overview/UpcomingStays/index.tsx
@@ -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"
diff --git a/components/MyPages/Blocks/Overview/index.tsx b/components/MyPages/Blocks/Overview/index.tsx
index 9725c5ca2..fb63d7415 100644
--- a/components/MyPages/Blocks/Overview/index.tsx
+++ b/components/MyPages/Blocks/Overview/index.tsx
@@ -1,4 +1,4 @@
-import Title from "@/components/MyPages/Title"
+import Title from "@/components/Title"
import Friend from "./Friend"
import Stats from "./Stats"
diff --git a/components/MyPages/Blocks/Shortcuts/index.tsx b/components/MyPages/Blocks/Shortcuts/index.tsx
index 100ecda0b..cb80b61c0 100644
--- a/components/MyPages/Blocks/Shortcuts/index.tsx
+++ b/components/MyPages/Blocks/Shortcuts/index.tsx
@@ -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"
diff --git a/components/MyPages/Blocks/Stays/Header/index.tsx b/components/MyPages/Blocks/Stays/Header/index.tsx
index d4fbeee68..683817b20 100644
--- a/components/MyPages/Blocks/Stays/Header/index.tsx
+++ b/components/MyPages/Blocks/Stays/Header/index.tsx
@@ -1,4 +1,4 @@
-import Title from "@/components/MyPages/Title"
+import Title from "@/components/Title"
import styles from "./header.module.css"
diff --git a/components/MyPages/Blocks/Stays/Previous/EmptyPreviousStays/index.tsx b/components/MyPages/Blocks/Stays/Previous/EmptyPreviousStays/index.tsx
index 1d7675412..0c390e390 100644
--- a/components/MyPages/Blocks/Stays/Previous/EmptyPreviousStays/index.tsx
+++ b/components/MyPages/Blocks/Stays/Previous/EmptyPreviousStays/index.tsx
@@ -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"
diff --git a/components/MyPages/Blocks/Stays/StayCard/index.tsx b/components/MyPages/Blocks/Stays/StayCard/index.tsx
index 459f7fb6e..c7e36641a 100644
--- a/components/MyPages/Blocks/Stays/StayCard/index.tsx
+++ b/components/MyPages/Blocks/Stays/StayCard/index.tsx
@@ -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"
diff --git a/components/MyPages/Blocks/Stays/Upcoming/EmptyUpcomingStays/index.tsx b/components/MyPages/Blocks/Stays/Upcoming/EmptyUpcomingStays/index.tsx
index ec6fe206d..08ce36f32 100644
--- a/components/MyPages/Blocks/Stays/Upcoming/EmptyUpcomingStays/index.tsx
+++ b/components/MyPages/Blocks/Stays/Upcoming/EmptyUpcomingStays/index.tsx
@@ -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"
diff --git a/components/MyPages/Sidebar/index.tsx b/components/MyPages/Sidebar/index.tsx
index 9134e8cbe..311510545 100644
--- a/components/MyPages/Sidebar/index.tsx
+++ b/components/MyPages/Sidebar/index.tsx
@@ -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"
diff --git a/components/TempDesignSystem/Card/card.module.css b/components/TempDesignSystem/Card/card.module.css
new file mode 100644
index 000000000..5f8f8a3e6
--- /dev/null
+++ b/components/TempDesignSystem/Card/card.module.css
@@ -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;
+}
diff --git a/components/TempDesignSystem/Card/card.ts b/components/TempDesignSystem/Card/card.ts
new file mode 100644
index 000000000..3c346f423
--- /dev/null
+++ b/components/TempDesignSystem/Card/card.ts
@@ -0,0 +1,9 @@
+export type CardProps = {
+ link?: {
+ href: string
+ title: string
+ }
+ title?: string
+ subtitle?: string
+ openInNewTab?: boolean
+}
diff --git a/components/TempDesignSystem/Card/index.tsx b/components/TempDesignSystem/Card/index.tsx
new file mode 100644
index 000000000..afb352530
--- /dev/null
+++ b/components/TempDesignSystem/Card/index.tsx
@@ -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 (
+
+ {title ? (
+
+ {title}
+
+ ) : null}
+ {subtitle ? (
+
+ {subtitle}
+
+ ) : null}
+ {link ? (
+
+ ) : null}
+
+ )
+}
diff --git a/components/MyPages/Title/index.tsx b/components/Title/index.tsx
similarity index 100%
rename from components/MyPages/Title/index.tsx
rename to components/Title/index.tsx
diff --git a/components/MyPages/Title/title.module.css b/components/Title/title.module.css
similarity index 99%
rename from components/MyPages/Title/title.module.css
rename to components/Title/title.module.css
index b8b2490f7..a51d9629b 100644
--- a/components/MyPages/Title/title.module.css
+++ b/components/Title/title.module.css
@@ -82,4 +82,4 @@
font-size: var(--typography-Title5-Desktop-fontSize);
line-height: var(--typography-Title5-Desktop-lineHeight);
}
-}
\ No newline at end of file
+}
diff --git a/components/MyPages/Title/variants.ts b/components/Title/variants.ts
similarity index 100%
rename from components/MyPages/Title/variants.ts
rename to components/Title/variants.ts
diff --git a/lib/graphql/Fragments/PageLink/AccountPageLink.graphql b/lib/graphql/Fragments/PageLink/AccountPageLink.graphql
new file mode 100644
index 000000000..dc5ed6667
--- /dev/null
+++ b/lib/graphql/Fragments/PageLink/AccountPageLink.graphql
@@ -0,0 +1,8 @@
+fragment AccountPageLink on AccountPage {
+ system {
+ locale
+ uid
+ }
+ title
+ url
+}
diff --git a/lib/graphql/Fragments/PageLink/ContentPageLink.graphql b/lib/graphql/Fragments/PageLink/ContentPageLink.graphql
new file mode 100644
index 000000000..f6eb6474b
--- /dev/null
+++ b/lib/graphql/Fragments/PageLink/ContentPageLink.graphql
@@ -0,0 +1,8 @@
+fragment ContentPageLink on ContentPage {
+ system {
+ locale
+ uid
+ }
+ url
+ title
+}
diff --git a/lib/graphql/Fragments/PageLink/CurrentContentPageLink.graphql b/lib/graphql/Fragments/PageLink/CurrentContentPageLink.graphql
new file mode 100644
index 000000000..1b82b2a59
--- /dev/null
+++ b/lib/graphql/Fragments/PageLink/CurrentContentPageLink.graphql
@@ -0,0 +1,8 @@
+fragment CurrentBlocksPageLink on CurrentBlocksPage {
+ system {
+ locale
+ uid
+ }
+ title
+ url
+}
diff --git a/lib/graphql/Fragments/PageLink/LoyaltyPageLink.graphql b/lib/graphql/Fragments/PageLink/LoyaltyPageLink.graphql
new file mode 100644
index 000000000..d0c6ac0a9
--- /dev/null
+++ b/lib/graphql/Fragments/PageLink/LoyaltyPageLink.graphql
@@ -0,0 +1,8 @@
+fragment LoyaltyPageLink on LoyaltyPage {
+ system {
+ locale
+ uid
+ }
+ title
+ url
+}
diff --git a/lib/graphql/Fragments/PageLinks.graphql b/lib/graphql/Fragments/PageLinks.graphql
deleted file mode 100644
index 61f380efa..000000000
--- a/lib/graphql/Fragments/PageLinks.graphql
+++ /dev/null
@@ -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
-}
diff --git a/lib/graphql/Query/ContactConfig.graphql b/lib/graphql/Query/ContactConfig.graphql
new file mode 100644
index 000000000..381d00941
--- /dev/null
+++ b/lib/graphql/Query/ContactConfig.graphql
@@ -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
+ }
+}
diff --git a/lib/graphql/Query/ContentTypeUid.graphql b/lib/graphql/Query/ContentTypeUid.graphql
new file mode 100644
index 000000000..6f8decd84
--- /dev/null
+++ b/lib/graphql/Query/ContentTypeUid.graphql
@@ -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
+ }
+}
diff --git a/lib/graphql/Query/LoyaltyPage.graphql b/lib/graphql/Query/LoyaltyPage.graphql
new file mode 100644
index 000000000..b308e6680
--- /dev/null
+++ b/lib/graphql/Query/LoyaltyPage.graphql
@@ -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
+ }
+ }
+ }
+}
diff --git a/lib/graphql/Query/NavigationMyPages.graphql b/lib/graphql/Query/NavigationMyPages.graphql
index 8968c61f4..a4663d6dd 100644
--- a/lib/graphql/Query/NavigationMyPages.graphql
+++ b/lib/graphql/Query/NavigationMyPages.graphql
@@ -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) {
diff --git a/middlewares/cmsContent.ts b/middlewares/cmsContent.ts
index 097322576..2e6a2ef11 100644
--- a/middlewares/cmsContent.ts
+++ b/middlewares/cmsContent.ts
@@ -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()
}
diff --git a/public/_static/icons/new-friend.png b/public/_static/icons/new-friend.png
new file mode 100644
index 000000000..0dfa36710
Binary files /dev/null and b/public/_static/icons/new-friend.png differ
diff --git a/public/_static/icons/scandic-friends.png b/public/_static/icons/scandic-friends.png
new file mode 100644
index 000000000..322340409
Binary files /dev/null and b/public/_static/icons/scandic-friends.png differ
diff --git a/server/index.ts b/server/index.ts
index f9aa0e258..ceb06b7fa 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -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
diff --git a/server/routers/contentstack/breadcrumbs/query.ts b/server/routers/contentstack/breadcrumbs/query.ts
index 7fe6db0f7..e9e141cfa 100644
--- a/server/routers/contentstack/breadcrumbs/query.ts
+++ b/server/routers/contentstack/breadcrumbs/query.ts
@@ -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({
diff --git a/server/routers/contentstack/contactConfig/index.ts b/server/routers/contentstack/contactConfig/index.ts
new file mode 100644
index 000000000..155949945
--- /dev/null
+++ b/server/routers/contentstack/contactConfig/index.ts
@@ -0,0 +1,5 @@
+import { mergeRouters } from "@/server/trpc"
+
+import { contactConfigQueryRouter } from "./query"
+
+export const contactConfigRouter = mergeRouters(contactConfigQueryRouter)
diff --git a/server/routers/contentstack/contactConfig/input.ts b/server/routers/contentstack/contactConfig/input.ts
new file mode 100644
index 000000000..b6be31232
--- /dev/null
+++ b/server/routers/contentstack/contactConfig/input.ts
@@ -0,0 +1,5 @@
+import { z } from "zod"
+
+import { Lang } from "@/constants/languages"
+
+export const getConfigInput = z.object({ lang: z.nativeEnum(Lang) })
diff --git a/server/routers/contentstack/contactConfig/output.ts b/server/routers/contentstack/contactConfig/output.ts
new file mode 100644
index 000000000..c42a19dc9
--- /dev/null
+++ b/server/routers/contentstack/contactConfig/output.ts
@@ -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
+
+export type ContactConfig = ContactConfigData["all_contact_config"]["items"][0]
+
+export type ContactFields = {
+ display_text?: string
+ contact_field: string
+}
diff --git a/server/routers/contentstack/contactConfig/query.ts b/server/routers/contentstack/contactConfig/query.ts
new file mode 100644
index 000000000..6985aee67
--- /dev/null
+++ b/server/routers/contentstack/contactConfig/query.ts
@@ -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(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()
+ }
+ }),
+})
diff --git a/server/routers/contentstack/index.ts b/server/routers/contentstack/index.ts
index 8098575a0..2841129f4 100644
--- a/server/routers/contentstack/index.ts
+++ b/server/routers/contentstack/index.ts
@@ -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,
})
diff --git a/server/routers/contentstack/loyaltyPage/index.ts b/server/routers/contentstack/loyaltyPage/index.ts
new file mode 100644
index 000000000..5e47223a1
--- /dev/null
+++ b/server/routers/contentstack/loyaltyPage/index.ts
@@ -0,0 +1,5 @@
+import { mergeRouters } from "@/server/trpc"
+
+import { loyaltyPageQueryRouter } from "./query"
+
+export const loyaltyPageRouter = mergeRouters(loyaltyPageQueryRouter)
diff --git a/server/routers/contentstack/loyaltyPage/input.ts b/server/routers/contentstack/loyaltyPage/input.ts
new file mode 100644
index 000000000..1cddb2b6f
--- /dev/null
+++ b/server/routers/contentstack/loyaltyPage/input.ts
@@ -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),
+})
diff --git a/server/routers/contentstack/loyaltyPage/output.ts b/server/routers/contentstack/loyaltyPage/output.ts
new file mode 100644
index 000000000..73a74c072
--- /dev/null
+++ b/server/routers/contentstack/loyaltyPage/output.ts
@@ -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
+
+export type CardGridCard = Omit<
+ CardGridRaw["card_grid"]["cards"][number],
+ "referenceConnection"
+> & {
+ link:
+ | {
+ href: string
+ title: string
+ }
+ | undefined
+}
+
+export type CardGrid = Omit & {
+ card_grid: Omit & {
+ cards: CardGridCard[]
+ }
+}
+
+type DynamicContentRaw = z.infer
+
+export type DynamicContent = Omit & {
+ dynamic_content: Omit & {
+ link:
+ | {
+ href: string
+ title: string
+ text?: string
+ }
+ | undefined
+ }
+}
+type BlockContentRaw = z.infer
+
+export interface RteBlockContent extends BlockContentRaw {
+ content: {
+ content: {
+ json: RTEDocument
+ embedded_itemsConnection: Edges
+ }
+ }
+}
+export type Block = CardGrid | RteBlockContent | DynamicContent
+
+// Sidebar block types
+type SidebarContentRaw = z.infer
+
+export type RteSidebarContent = Omit & {
+ content: {
+ content: {
+ json: RTEDocument
+ embedded_itemsConnection: Edges
+ }
+ }
+}
+export type JoinLoyaltyContact = z.infer
+export type Sidebar = JoinLoyaltyContact | RteSidebarContent
+
+type LoyaltyPageDataRaw = z.infer
+
+type LoyaltyPageRaw = LoyaltyPageDataRaw["all_loyalty_page"]["items"][0]
+
+export type LoyaltyPage = Omit & {
+ blocks: Block[]
+ sidebar: Sidebar[]
+}
diff --git a/server/routers/contentstack/loyaltyPage/query.ts b/server/routers/contentstack/loyaltyPage/query.ts
new file mode 100644
index 000000000..6a23f2a66
--- /dev/null
+++ b/server/routers/contentstack/loyaltyPage/query.ts
@@ -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(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,
+ },
+ },
+ }
+ } 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,
+ },
+ },
+ }
+ 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()
+ }
+ }),
+})
diff --git a/server/routers/loyalty/index.ts b/server/routers/loyalty/index.ts
new file mode 100644
index 000000000..412aa2846
--- /dev/null
+++ b/server/routers/loyalty/index.ts
@@ -0,0 +1,5 @@
+import { mergeRouters } from "@/server/trpc"
+
+import { lotaltyQueryRouter } from "./query"
+
+export const loyaltyRouter = mergeRouters(lotaltyQueryRouter)
diff --git a/server/routers/loyalty/query.ts b/server/routers/loyalty/query.ts
new file mode 100644
index 000000000..490623a88
--- /dev/null
+++ b/server/routers/loyalty/query.ts
@@ -0,0 +1,28 @@
+import { protectedProcedure, publicProcedure, router } from "@/server/trpc"
+
+import { allLevels } from "./temp"
+
+function fakingRequest(payload: T): Promise {
+ 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(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])
+ }),
+ }),
+})
diff --git a/server/routers/loyalty/temp.ts b/server/routers/loyalty/temp.ts
new file mode 100644
index 000000000..e8fc56550
--- /dev/null
+++ b/server/routers/loyalty/temp.ts
@@ -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",
+ },
+]
diff --git a/server/trpc.ts b/server/trpc.ts
index 428e38796..95d575418 100644
--- a/server/trpc.ts
+++ b/server/trpc.ts
@@ -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().meta().create({ transformer })
diff --git a/types/components/jsontohtml.ts b/types/components/jsontohtml.ts
index 4f319e6ed..c12ef513f 100644
--- a/types/components/jsontohtml.ts
+++ b/types/components/jsontohtml.ts
@@ -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[]
diff --git a/types/components/loyalty/blocks.ts b/types/components/loyalty/blocks.ts
new file mode 100644
index 000000000..2c73c8d58
--- /dev/null
+++ b/types/components/loyalty/blocks.ts
@@ -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
+
+export type Content = { content: RteBlockContent["content"]["content"] }
+
+export type LevelCardProps = {
+ level: {
+ tier: number
+ name: string
+ requiredPoints: number
+ requiredNights: string
+ topBenefits: string[]
+ logo: string
+ }
+}
diff --git a/types/components/loyalty/sidebar.ts b/types/components/loyalty/sidebar.ts
new file mode 100644
index 000000000..60ef2b99f
--- /dev/null
+++ b/types/components/loyalty/sidebar.ts
@@ -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
+}
diff --git a/types/components/myPages/title.ts b/types/components/myPages/title.ts
index fa73698bf..9b93f2807 100644
--- a/types/components/myPages/title.ts
+++ b/types/components/myPages/title.ts
@@ -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, VariantProps {
+export interface HeadingProps
+ extends React.HTMLAttributes,
+ VariantProps {
as?: HeadingLevel
level?: HeadingLevel
uppercase?: boolean
diff --git a/types/image.ts b/types/image.ts
index 8cbed40b1..5c442c4df 100644
--- a/types/image.ts
+++ b/types/image.ts
@@ -4,7 +4,7 @@ export type Image = {
height: number
width: number
}
- metadata: JSON
+ metadata: JSON | null
system: {
uid: string
}
diff --git a/types/requests/contentTypeUid.ts b/types/requests/contentTypeUid.ts
new file mode 100644
index 000000000..5caabfa08
--- /dev/null
+++ b/types/requests/contentTypeUid.ts
@@ -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(),
+ }),
+})
diff --git a/types/requests/loyaltyPage.ts b/types/requests/loyaltyPage.ts
new file mode 100644
index 000000000..2a9016a2c
--- /dev/null
+++ b/types/requests/loyaltyPage.ts
@@ -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",
+}
diff --git a/types/requests/utils/asset.ts b/types/requests/utils/asset.ts
index b1d35d8e6..d9dae927e 100644
--- a/types/requests/utils/asset.ts
+++ b/types/requests/utils/asset.ts
@@ -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
diff --git a/types/rte/node.ts b/types/rte/node.ts
index 25e9080bd..c762f33bb 100644
--- a/types/rte/node.ts
+++ b/types/rte/node.ts
@@ -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 {
diff --git a/utils/contactConfig.ts b/utils/contactConfig.ts
new file mode 100644
index 000000000..7a888fe0a
--- /dev/null
+++ b/utils/contactConfig.ts
@@ -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
+}
diff --git a/utils/contentType.ts b/utils/contentType.ts
new file mode 100644
index 000000000..af30d5815
--- /dev/null
+++ b/utils/contentType.ts
@@ -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
+ }
+}