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.name} +

+ {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}} + Scandic Friends + {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 + } +}