Merged in feat/loyalty-page (pull request #151)
Feat(WEB-21): loyalty page Approved-by: Simon.Emanuelsson
This commit is contained in:
@@ -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"
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.layout {
|
||||
display: grid;
|
||||
font-family: var(--ff-fira-sans);
|
||||
background-color: var(--Brand-Coffee-Subtle);
|
||||
}
|
||||
15
app/[lang]/(live)/(public)/loyalty-page/layout.tsx
Normal file
15
app/[lang]/(live)/(public)/loyalty-page/layout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
31
app/[lang]/(live)/(public)/loyalty-page/page.module.css
Normal file
31
app/[lang]/(live)/(public)/loyalty-page/page.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
39
app/[lang]/(live)/(public)/loyalty-page/page.tsx
Normal file
39
app/[lang]/(live)/(public)/loyalty-page/page.tsx
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
:root {
|
||||
font-size: 62.5%;
|
||||
--max-width: 113.5rem;
|
||||
}
|
||||
|
||||
* {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
28
components/Loyalty/Blocks/CardGrid/cardGrid.module.css
Normal file
28
components/Loyalty/Blocks/CardGrid/cardGrid.module.css
Normal file
@@ -0,0 +1,28 @@
|
||||
.container {
|
||||
display: grid;
|
||||
gap: 2.4rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.titleContainer {
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.cardContainer {
|
||||
display: grid;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 950px) {
|
||||
.cardContainer {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.cardWrapper:last-child {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
41
components/Loyalty/Blocks/CardGrid/index.tsx
Normal file
41
components/Loyalty/Blocks/CardGrid/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
.container {
|
||||
display: grid;
|
||||
gap: 2.4rem;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cardContainer {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
overflow-x: auto;
|
||||
padding-right: 1.6rem;
|
||||
margin-right: -1.6rem;
|
||||
/* Hide scrollbar IE and Edge */
|
||||
-ms-overflow-style: none;
|
||||
/* Hide Scrollbar Firefox */
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 37rem;
|
||||
min-width: 32rem;
|
||||
padding: 4rem 1rem;
|
||||
background-color: var(--Base-Fill-Normal);
|
||||
border-radius: 1.6rem;
|
||||
gap: 1.8rem;
|
||||
}
|
||||
|
||||
.qualifications {
|
||||
margin: 0;
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
line-height: var(--typography-Body-Bold-lineHeight);
|
||||
/* font-weight: var(--typography-Body-Bold-fontWeight); -- Tokens not parsable*/
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.benefits {
|
||||
font-family: var(--fira-sans);
|
||||
font-size: var(--typography-Body-Regular-fontSize);
|
||||
line-height: var(--typography-Body-Regular-lineHeight);
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-family: var(--fira-sans);
|
||||
position: relative;
|
||||
top: 0.3rem;
|
||||
height: 1.4rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 950px) {
|
||||
.container {
|
||||
gap: 3.2rem;
|
||||
}
|
||||
.cardContainer {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(
|
||||
12,
|
||||
auto
|
||||
); /* Three columns in the first row */
|
||||
padding-right: 0;
|
||||
margin-right: 0rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.card:nth-child(-n + 3) {
|
||||
grid-column: span 4;
|
||||
}
|
||||
|
||||
.card:nth-last-child(-n + 4) {
|
||||
grid-column: span 3;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function OverviewTable() {
|
||||
return <div></div>
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
68
components/Loyalty/Blocks/DynamicContent/index.tsx
Normal file
68
components/Loyalty/Blocks/DynamicContent/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
29
components/Loyalty/Blocks/index.tsx
Normal file
29
components/Loyalty/Blocks/index.tsx
Normal 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
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
33
components/Loyalty/Sidebar/JoinLoyalty/Contact/index.tsx
Normal file
33
components/Loyalty/Sidebar/JoinLoyalty/Contact/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
40
components/Loyalty/Sidebar/JoinLoyalty/index.tsx
Normal file
40
components/Loyalty/Sidebar/JoinLoyalty/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
32
components/Loyalty/Sidebar/index.tsx
Normal file
32
components/Loyalty/Sidebar/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
5
components/Loyalty/Sidebar/sidebar.module.css
Normal file
5
components/Loyalty/Sidebar/sidebar.module.css
Normal file
@@ -0,0 +1,5 @@
|
||||
@media screen and (max-width: 950px) {
|
||||
.content {
|
||||
padding: 0 1.6rem;
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Title from "@/components/MyPages/Title"
|
||||
import Title from "@/components/Title"
|
||||
|
||||
import Friend from "./Friend"
|
||||
import Stats from "./Stats"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Title from "@/components/MyPages/Title"
|
||||
import Title from "@/components/Title"
|
||||
|
||||
import styles from "./header.module.css"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
15
components/TempDesignSystem/Card/card.module.css
Normal file
15
components/TempDesignSystem/Card/card.module.css
Normal file
@@ -0,0 +1,15 @@
|
||||
.linkCard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 37rem;
|
||||
width: 100%;
|
||||
margin-right: 1.6rem;
|
||||
border-radius: 1.6rem;
|
||||
gap: 1rem;
|
||||
padding: 1.6rem;
|
||||
|
||||
background-color: var(--Base-Fill-Normal);
|
||||
text-align: center;
|
||||
}
|
||||
9
components/TempDesignSystem/Card/card.ts
Normal file
9
components/TempDesignSystem/Card/card.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export type CardProps = {
|
||||
link?: {
|
||||
href: string
|
||||
title: string
|
||||
}
|
||||
title?: string
|
||||
subtitle?: string
|
||||
openInNewTab?: boolean
|
||||
}
|
||||
38
components/TempDesignSystem/Card/index.tsx
Normal file
38
components/TempDesignSystem/Card/index.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { _ } from "@/lib/translation"
|
||||
|
||||
import Title from "@/components/Title"
|
||||
|
||||
import Button from "../Button"
|
||||
import Link from "../Link"
|
||||
import { CardProps } from "./card"
|
||||
|
||||
import styles from "./card.module.css"
|
||||
|
||||
export default function Card({
|
||||
link,
|
||||
subtitle,
|
||||
title,
|
||||
openInNewTab = false,
|
||||
}: CardProps) {
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -82,4 +82,4 @@
|
||||
font-size: var(--typography-Title5-Desktop-fontSize);
|
||||
line-height: var(--typography-Title5-Desktop-lineHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
8
lib/graphql/Fragments/PageLink/AccountPageLink.graphql
Normal file
8
lib/graphql/Fragments/PageLink/AccountPageLink.graphql
Normal file
@@ -0,0 +1,8 @@
|
||||
fragment AccountPageLink on AccountPage {
|
||||
system {
|
||||
locale
|
||||
uid
|
||||
}
|
||||
title
|
||||
url
|
||||
}
|
||||
8
lib/graphql/Fragments/PageLink/ContentPageLink.graphql
Normal file
8
lib/graphql/Fragments/PageLink/ContentPageLink.graphql
Normal file
@@ -0,0 +1,8 @@
|
||||
fragment ContentPageLink on ContentPage {
|
||||
system {
|
||||
locale
|
||||
uid
|
||||
}
|
||||
url
|
||||
title
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
fragment CurrentBlocksPageLink on CurrentBlocksPage {
|
||||
system {
|
||||
locale
|
||||
uid
|
||||
}
|
||||
title
|
||||
url
|
||||
}
|
||||
8
lib/graphql/Fragments/PageLink/LoyaltyPageLink.graphql
Normal file
8
lib/graphql/Fragments/PageLink/LoyaltyPageLink.graphql
Normal file
@@ -0,0 +1,8 @@
|
||||
fragment LoyaltyPageLink on LoyaltyPage {
|
||||
system {
|
||||
locale
|
||||
uid
|
||||
}
|
||||
title
|
||||
url
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
37
lib/graphql/Query/ContactConfig.graphql
Normal file
37
lib/graphql/Query/ContactConfig.graphql
Normal 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
|
||||
}
|
||||
}
|
||||
11
lib/graphql/Query/ContentTypeUid.graphql
Normal file
11
lib/graphql/Query/ContentTypeUid.graphql
Normal 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
|
||||
}
|
||||
}
|
||||
120
lib/graphql/Query/LoyaltyPage.graphql
Normal file
120
lib/graphql/Query/LoyaltyPage.graphql
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
BIN
public/_static/icons/new-friend.png
Normal file
BIN
public/_static/icons/new-friend.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
BIN
public/_static/icons/scandic-friends.png
Normal file
BIN
public/_static/icons/scandic-friends.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
5
server/routers/contentstack/contactConfig/index.ts
Normal file
5
server/routers/contentstack/contactConfig/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { mergeRouters } from "@/server/trpc"
|
||||
|
||||
import { contactConfigQueryRouter } from "./query"
|
||||
|
||||
export const contactConfigRouter = mergeRouters(contactConfigQueryRouter)
|
||||
5
server/routers/contentstack/contactConfig/input.ts
Normal file
5
server/routers/contentstack/contactConfig/input.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
|
||||
export const getConfigInput = z.object({ lang: z.nativeEnum(Lang) })
|
||||
60
server/routers/contentstack/contactConfig/output.ts
Normal file
60
server/routers/contentstack/contactConfig/output.ts
Normal 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
|
||||
}
|
||||
34
server/routers/contentstack/contactConfig/query.ts
Normal file
34
server/routers/contentstack/contactConfig/query.ts
Normal 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()
|
||||
}
|
||||
}),
|
||||
})
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
5
server/routers/contentstack/loyaltyPage/index.ts
Normal file
5
server/routers/contentstack/loyaltyPage/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { mergeRouters } from "@/server/trpc"
|
||||
|
||||
import { loyaltyPageQueryRouter } from "./query"
|
||||
|
||||
export const loyaltyPageRouter = mergeRouters(loyaltyPageQueryRouter)
|
||||
10
server/routers/contentstack/loyaltyPage/input.ts
Normal file
10
server/routers/contentstack/loyaltyPage/input.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
|
||||
const langs = Object.keys(Lang) as [keyof typeof Lang]
|
||||
|
||||
export const getLoyaltyPageInput = z.object({
|
||||
href: z.string().min(1, { message: "href is required" }),
|
||||
locale: z.enum(langs),
|
||||
})
|
||||
208
server/routers/contentstack/loyaltyPage/output.ts
Normal file
208
server/routers/contentstack/loyaltyPage/output.ts
Normal 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[]
|
||||
}
|
||||
130
server/routers/contentstack/loyaltyPage/query.ts
Normal file
130
server/routers/contentstack/loyaltyPage/query.ts
Normal 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()
|
||||
}
|
||||
}),
|
||||
})
|
||||
5
server/routers/loyalty/index.ts
Normal file
5
server/routers/loyalty/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { mergeRouters } from "@/server/trpc"
|
||||
|
||||
import { lotaltyQueryRouter } from "./query"
|
||||
|
||||
export const loyaltyRouter = mergeRouters(lotaltyQueryRouter)
|
||||
28
server/routers/loyalty/query.ts
Normal file
28
server/routers/loyalty/query.ts
Normal 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])
|
||||
}),
|
||||
}),
|
||||
})
|
||||
86
server/routers/loyalty/temp.ts
Normal file
86
server/routers/loyalty/temp.ts
Normal 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",
|
||||
},
|
||||
]
|
||||
@@ -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 })
|
||||
|
||||
|
||||
@@ -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>[]
|
||||
|
||||
33
types/components/loyalty/blocks.ts
Normal file
33
types/components/loyalty/blocks.ts
Normal 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
|
||||
}
|
||||
}
|
||||
21
types/components/loyalty/sidebar.ts
Normal file
21
types/components/loyalty/sidebar.ts
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,7 @@ export type Image = {
|
||||
height: number
|
||||
width: number
|
||||
}
|
||||
metadata: JSON
|
||||
metadata: JSON | null
|
||||
system: {
|
||||
uid: string
|
||||
}
|
||||
|
||||
13
types/requests/contentTypeUid.ts
Normal file
13
types/requests/contentTypeUid.ts
Normal 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(),
|
||||
}),
|
||||
})
|
||||
32
types/requests/loyaltyPage.ts
Normal file
32
types/requests/loyaltyPage.ts
Normal 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",
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { EmbedEnum } from "./embeds"
|
||||
import type { Image } from "@/types/image"
|
||||
import type { EmbedEnum } from "./embeds"
|
||||
import type { Typename } from "./typename"
|
||||
|
||||
export type SysAsset = Typename<Image, EmbedEnum.SysAsset>
|
||||
|
||||
@@ -1,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
20
utils/contactConfig.ts
Normal 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
54
utils/contentType.ts
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user