From a444746ccbcc9fb4c25831bcc80ff01ba5d2bc12 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Mon, 2 Sep 2024 22:43:08 +0200 Subject: [PATCH] feat(SW-285): Add support for sidebar --- .../Contact/ContactRow/contactRow.module.css | 5 + .../JoinLoyalty/Contact/ContactRow/index.tsx | 57 ++++++++ .../JoinLoyalty/Contact/contact.module.css | 19 +++ .../Sidebar/JoinLoyalty/Contact/index.tsx | 33 +++++ .../Content/Sidebar/JoinLoyalty/index.tsx | 73 +++++++++++ .../JoinLoyalty/joinLoyalty.module.css | 24 ++++ .../Sidebar/MyPagesNavigation/index.tsx | 13 ++ components/Content/Sidebar/index.tsx | 52 ++++++++ components/Content/Sidebar/sidebar.module.css | 15 +++ lib/graphql/Query/ContentPage.graphql | 124 ++++++++++++++++++ .../contentstack/contentPage/output.ts | 113 +++++++++++++++- .../routers/contentstack/contentPage/query.ts | 21 +++ types/components/content/enums.ts | 25 ++++ types/components/content/sidebar.ts | 21 +++ 14 files changed, 594 insertions(+), 1 deletion(-) create mode 100644 components/Content/Sidebar/JoinLoyalty/Contact/ContactRow/contactRow.module.css create mode 100644 components/Content/Sidebar/JoinLoyalty/Contact/ContactRow/index.tsx create mode 100644 components/Content/Sidebar/JoinLoyalty/Contact/contact.module.css create mode 100644 components/Content/Sidebar/JoinLoyalty/Contact/index.tsx create mode 100644 components/Content/Sidebar/JoinLoyalty/index.tsx create mode 100644 components/Content/Sidebar/JoinLoyalty/joinLoyalty.module.css create mode 100644 components/Content/Sidebar/MyPagesNavigation/index.tsx create mode 100644 components/Content/Sidebar/index.tsx create mode 100644 components/Content/Sidebar/sidebar.module.css create mode 100644 types/components/content/sidebar.ts diff --git a/components/Content/Sidebar/JoinLoyalty/Contact/ContactRow/contactRow.module.css b/components/Content/Sidebar/JoinLoyalty/Contact/ContactRow/contactRow.module.css new file mode 100644 index 000000000..3dc5d411b --- /dev/null +++ b/components/Content/Sidebar/JoinLoyalty/Contact/ContactRow/contactRow.module.css @@ -0,0 +1,5 @@ +.link { + display: flex; + align-items: center; + gap: var(--Spacing-x1); +} diff --git a/components/Content/Sidebar/JoinLoyalty/Contact/ContactRow/index.tsx b/components/Content/Sidebar/JoinLoyalty/Contact/ContactRow/index.tsx new file mode 100644 index 000000000..5c8557606 --- /dev/null +++ b/components/Content/Sidebar/JoinLoyalty/Contact/ContactRow/index.tsx @@ -0,0 +1,57 @@ +import { serverClient } from "@/lib/trpc/server" + +import { EmailIcon, PhoneIcon } from "@/components/Icons" +import Link from "@/components/TempDesignSystem/Link" +import Body from "@/components/TempDesignSystem/Text/Body" +import Footnote from "@/components/TempDesignSystem/Text/Footnote" +import { getValueFromContactConfig } from "@/utils/contactConfig" + +import styles from "./contactRow.module.css" + +import type { ContactRowProps } from "@/types/components/content/sidebar" + +export default async function ContactRow({ contact }: ContactRowProps) { + const data = await serverClient().contentstack.base.contact() + if (!data) { + return null + } + + const val = getValueFromContactConfig(contact.contact_field, data) + + if (!val) { + return null + } + + let Icon = null + if (contact.contact_field.includes("email")) { + Icon = EmailIcon + } else if (contact.contact_field.includes("phone")) { + Icon = PhoneIcon + } + + let openableLink = val + if (contact.contact_field.includes("email")) { + openableLink = `mailto:${val}` + } else if (contact.contact_field.includes("phone")) { + openableLink = `tel:${val}` + } + + return ( +
+ + {contact.display_text} + + + {Icon ? : null} + {val} + + {contact.footnote} +
+ ) +} diff --git a/components/Content/Sidebar/JoinLoyalty/Contact/contact.module.css b/components/Content/Sidebar/JoinLoyalty/Contact/contact.module.css new file mode 100644 index 000000000..55234e9d2 --- /dev/null +++ b/components/Content/Sidebar/JoinLoyalty/Contact/contact.module.css @@ -0,0 +1,19 @@ +.contactContainer { + display: none; +} + +@media screen and (min-width: 1367px) { + .contactContainer { + border-top: 1px solid var(--UI-Grey-30); + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); + justify-content: center; + padding-top: var(--Spacing-x2); + } + + .contact { + display: grid; + gap: var(--Spacing-x-one-and-half); + } +} diff --git a/components/Content/Sidebar/JoinLoyalty/Contact/index.tsx b/components/Content/Sidebar/JoinLoyalty/Contact/index.tsx new file mode 100644 index 000000000..a7d39dcb7 --- /dev/null +++ b/components/Content/Sidebar/JoinLoyalty/Contact/index.tsx @@ -0,0 +1,33 @@ +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import { getIntl } from "@/i18n" + +import ContactRow from "./ContactRow" + +import styles from "./contact.module.css" + +import { JoinLoyaltyContactTypenameEnum } from "@/types/components/content/enums" +import type { ContactProps } from "@/types/components/content/sidebar" + +export default async function Contact({ contactBlock }: ContactProps) { + const { formatMessage } = await getIntl() + return ( +
+ {formatMessage({ id: "Contact us" })} +
+ {contactBlock.map(({ contact, __typename }, i) => { + switch (__typename) { + case JoinLoyaltyContactTypenameEnum.ContentPageSidebarJoinLoyaltyContactBlockContactContact: + return ( + + ) + default: + return null + } + })} +
+
+ ) +} diff --git a/components/Content/Sidebar/JoinLoyalty/index.tsx b/components/Content/Sidebar/JoinLoyalty/index.tsx new file mode 100644 index 000000000..12598a686 --- /dev/null +++ b/components/Content/Sidebar/JoinLoyalty/index.tsx @@ -0,0 +1,73 @@ +import { serverClient } from "@/lib/trpc/server" + +import LoginButton from "@/components/Current/Header/LoginButton" +import ArrowRight from "@/components/Icons/ArrowRight" +import { ScandicFriends } from "@/components/Levels" +import Button from "@/components/TempDesignSystem/Button" +import Link from "@/components/TempDesignSystem/Link" +import Body from "@/components/TempDesignSystem/Text/Body" +import Title from "@/components/TempDesignSystem/Text/Title" +import { getIntl } from "@/i18n" + +import Contact from "./Contact" + +import styles from "./joinLoyalty.module.css" + +import type { JoinLoyaltyContactProps } from "@/types/components/content/sidebar" + +export default async function JoinLoyaltyContact({ + block, +}: JoinLoyaltyContactProps) { + const { formatMessage } = await getIntl() + const user = await serverClient().user.name() + + // Check if we have user, that means we are logged in. + if (user) { + return null + } + return ( +
+
+ + {block.title} + + + {block.preamble ? {block.preamble} : null} + {block.button ? ( + + ) : null} +
+ {formatMessage({ id: "Already a friend?" })} + + + {formatMessage({ id: "Log in here" })} + +
+
+ {block.contact ? : null} +
+ ) +} diff --git a/components/Content/Sidebar/JoinLoyalty/joinLoyalty.module.css b/components/Content/Sidebar/JoinLoyalty/joinLoyalty.module.css new file mode 100644 index 000000000..23d1e2049 --- /dev/null +++ b/components/Content/Sidebar/JoinLoyalty/joinLoyalty.module.css @@ -0,0 +1,24 @@ +.wrapper { + display: grid; + gap: var(--Spacing-x3); + padding-bottom: var(--Spacing-x5); + padding-top: var(--Spacing-x4); +} + +.loginContainer { + display: grid; + gap: var(--Spacing-x2); +} + +.button { + width: fit-content; +} + +.link { + display: flex; + align-items: center; +} + +.icon { + align-self: center; +} diff --git a/components/Content/Sidebar/MyPagesNavigation/index.tsx b/components/Content/Sidebar/MyPagesNavigation/index.tsx new file mode 100644 index 000000000..19ea70405 --- /dev/null +++ b/components/Content/Sidebar/MyPagesNavigation/index.tsx @@ -0,0 +1,13 @@ +import { serverClient } from "@/lib/trpc/server" + +import MyPagesSidebar from "@/components/MyPages/Sidebar" + +export async function MyPagesNavigation() { + const user = await serverClient().user.name() + + // Check if we have user, that means we are logged in andt the My Pages menu can show. + if (!user) { + return null + } + return +} diff --git a/components/Content/Sidebar/index.tsx b/components/Content/Sidebar/index.tsx new file mode 100644 index 000000000..630ef17ca --- /dev/null +++ b/components/Content/Sidebar/index.tsx @@ -0,0 +1,52 @@ +import JsonToHtml from "@/components/JsonToHtml" + +import JoinLoyaltyContact from "./JoinLoyalty" +import { MyPagesNavigation } from "./MyPagesNavigation" + +import styles from "./sidebar.module.css" + +import { + SidebarDynamicComponentEnum, + SidebarTypenameEnum, +} from "@/types/components/content/enums" +import { SidebarProps } from "@/types/components/content/sidebar" + +export default function SidebarLoyalty({ blocks }: SidebarProps) { + return ( + + ) +} diff --git a/components/Content/Sidebar/sidebar.module.css b/components/Content/Sidebar/sidebar.module.css new file mode 100644 index 000000000..c4b8627a0 --- /dev/null +++ b/components/Content/Sidebar/sidebar.module.css @@ -0,0 +1,15 @@ +.aside { + display: none; +} + +.content { + padding: var(--Spacing-x0) var(--Spacing-x2); +} + +@media screen and (min-width: 1366px) { + .aside { + align-content: flex-start; + display: grid; + gap: var(--Spacing-x4); + } +} diff --git a/lib/graphql/Query/ContentPage.graphql b/lib/graphql/Query/ContentPage.graphql index 2b054be5f..d11f99198 100644 --- a/lib/graphql/Query/ContentPage.graphql +++ b/lib/graphql/Query/ContentPage.graphql @@ -1,3 +1,4 @@ +#import "../Fragments/Image.graphql" #import "../Fragments/Blocks/Card.graphql" #import "../Fragments/Blocks/LoyaltyCard.graphql" @@ -112,6 +113,66 @@ query GetContentPage($locale: String!, $uid: String!) { preamble } hero_image + sidebar { + __typename + ... on ContentPageSidebarDynamicContent { + dynamic_content { + component + } + } + ... on ContentPageSidebarJoinLoyaltyContact { + join_loyalty_contact { + title + preamble + button { + cta_text + external_link { + title + href + } + open_in_new_tab + linkConnection { + edges { + node { + __typename + ...AccountPageLink + ...ContentPageLink + ...LoyaltyPageLink + } + } + } + } + contact { + ... on ContentPageSidebarJoinLoyaltyContactBlockContactContact { + __typename + contact { + display_text + contact_field + footnote + } + } + } + } + } + ... on ContentPageSidebarContent { + content { + content { + json + embedded_itemsConnection { + edges { + node { + __typename + ...Image + ...LoyaltyPageLink + ...ContentPageLink + } + } + totalCount + } + } + } + } + } system { uid created_at @@ -203,6 +264,51 @@ query GetContentPageRefs($locale: String!, $uid: String!) { } } } + sidebar { + ... on ContentPageSidebarContent { + __typename + content { + content { + embedded_itemsConnection { + edges { + node { + # No fragments used since we want to include __typename for each type to avoid fetching SystemAsset + ... on ContentPage { + __typename + system { + ...System + } + } + ... on LoyaltyPage { + __typename + system { + ...System + } + } + } + } + } + } + } + } + ... on ContentPageSidebarJoinLoyaltyContact { + __typename + join_loyalty_contact { + button { + linkConnection { + edges { + node { + __typename + ...AccountPageRef + ...ContentPageRef + ...LoyaltyPageRef + } + } + } + } + } + } + } system { ...System } @@ -212,16 +318,25 @@ query GetContentPageRefs($locale: String!, $uid: String!) { query GetDaDeEnUrlsContentPage($uid: String!) { de: all_content_page(where: { uid: $uid }, locale: "de") { items { + web { + original_url + } url } } en: all_content_page(where: { uid: $uid }, locale: "en") { items { + web { + original_url + } url } } da: all_content_page(where: { uid: $uid }, locale: "da") { items { + web { + original_url + } url } } @@ -230,16 +345,25 @@ query GetDaDeEnUrlsContentPage($uid: String!) { query GetFiNoSvUrlsContentPage($uid: String!) { fi: all_content_page(where: { uid: $uid }, locale: "fi") { items { + web { + original_url + } url } } no: all_content_page(where: { uid: $uid }, locale: "no") { items { + web { + original_url + } url } } sv: all_content_page(where: { uid: $uid }, locale: "sv") { items { + web { + original_url + } url } } diff --git a/server/routers/contentstack/contentPage/output.ts b/server/routers/contentstack/contentPage/output.ts index 7f7eee45d..bc5ec0c2e 100644 --- a/server/routers/contentstack/contentPage/output.ts +++ b/server/routers/contentstack/contentPage/output.ts @@ -8,6 +8,9 @@ import { CardsGridEnum, ContentBlocksTypenameEnum, DynamicContentComponentEnum, + JoinLoyaltyContactTypenameEnum, + SidebarDynamicComponentEnum, + SidebarTypenameEnum, } from "@/types/components/content/enums" import { ImageVaultAsset } from "@/types/components/imageVault" import { Embeds } from "@/types/requests/embeds" @@ -133,6 +136,62 @@ const contentPageBlockItem = z.discriminatedUnion("__typename", [ contentPageShortcuts, ]) +const contentPageSidebarTextContent = z.object({ + __typename: z.literal(SidebarTypenameEnum.ContentPageSidebarContent), + content: z.object({ + content: z.object({ + embedded_itemsConnection: z.object({ + edges: z.array(z.any()), + totalCount: z.number(), + }), + json: z.any(), + }), + }), +}) + +const contentPageJoinLoyaltyContact = z.object({ + __typename: z.literal( + SidebarTypenameEnum.ContentPageSidebarJoinLoyaltyContact + ), + join_loyalty_contact: z.object({ + title: z.string().nullable(), + preamble: z.string().nullable(), + button: z + .object({ + openInNewTab: z.boolean(), + title: z.string(), + href: z.string(), + isExternal: z.boolean(), + }) + .nullable(), + contact: z.array( + z.object({ + __typename: z.literal( + JoinLoyaltyContactTypenameEnum.ContentPageSidebarJoinLoyaltyContactBlockContactContact + ), + contact: z.object({ + display_text: z.string().nullable(), + contact_field: z.string(), + footnote: z.string().nullable(), + }), + }) + ), + }), +}) + +const contentPageSidebarDynamicContent = z.object({ + __typename: z.literal(SidebarTypenameEnum.ContentPageSidebarDynamicContent), + dynamic_content: z.object({ + component: z.nativeEnum(SidebarDynamicComponentEnum), + }), +}) + +const contentPageSidebarItem = z.discriminatedUnion("__typename", [ + contentPageSidebarTextContent, + contentPageSidebarDynamicContent, + contentPageJoinLoyaltyContact, +]) + export type DynamicContent = z.infer type BlockContentRaw = z.infer @@ -163,6 +222,25 @@ export type CardsRaw = CardsGrid["cards_grid"]["cards"][number] export type Block = RteBlockContent | Shortcuts | CardsGrid | DynamicContent +type SidebarContentRaw = z.infer + +export type RteSidebarContent = Omit & { + content: { + content: { + json: RTEDocument + embedded_itemsConnection: EdgesWithTotalCount + } + } +} + +type SideBarDynamicContent = z.infer + +export type JoinLoyaltyContact = z.infer +export type Sidebar = + | JoinLoyaltyContact + | RteSidebarContent + | SideBarDynamicContent + // Content Page Schema and types export const validateContentPageSchema = z.object({ content_page: z.object({ @@ -173,6 +251,7 @@ export const validateContentPageSchema = z.object({ }), hero_image: imageVaultAssetSchema.nullable().optional(), blocks: z.array(contentPageBlockItem).nullable(), + sidebar: z.array(contentPageSidebarItem).nullable(), system: z.object({ uid: z.string(), locale: z.nativeEnum(Lang), @@ -185,9 +264,13 @@ export const validateContentPageSchema = z.object({ export type ContentPageDataRaw = z.infer type ContentPageRaw = ContentPageDataRaw["content_page"] -export type ContentPage = Omit & { +export type ContentPage = Omit< + ContentPageRaw, + "blocks" | "hero_image" | "sidebar" +> & { heroImage?: ImageVaultAsset blocks: Block[] + sidebar: Sidebar[] } const pageConnectionRefs = z.object({ @@ -305,9 +388,37 @@ const contentPageBlockRefsItem = z.discriminatedUnion("__typename", [ contentPageDynamicContentRefs, ]) +const contentPageSidebarTextContentRef = z.object({ + __typename: z.literal(SidebarTypenameEnum.ContentPageSidebarContent), + content: z.object({ + content: z.object({ + embedded_itemsConnection: rteConnectionRefs, + }), + }), +}) + +const contentPageSidebarJoinLoyaltyContactRef = z.object({ + __typename: z.literal( + SidebarTypenameEnum.ContentPageSidebarJoinLoyaltyContact + ), + join_loyalty_contact: z.object({ + button: z + .object({ + linkConnection: pageConnectionRefs, + }) + .nullable(), + }), +}) + +const contentPageSidebarRefsItem = z.discriminatedUnion("__typename", [ + contentPageSidebarTextContentRef, + contentPageSidebarJoinLoyaltyContactRef, +]) + export const validateContentPageRefsSchema = z.object({ content_page: z.object({ blocks: z.array(contentPageBlockRefsItem).nullable(), + sidebar: z.array(contentPageSidebarRefsItem).nullable(), system: z.object({ content_type_uid: z.string(), uid: z.string(), diff --git a/server/routers/contentstack/contentPage/query.ts b/server/routers/contentstack/contentPage/query.ts index c64cef51e..12de1a965 100644 --- a/server/routers/contentstack/contentPage/query.ts +++ b/server/routers/contentstack/contentPage/query.ts @@ -12,6 +12,7 @@ import { Block, ContentPage, ContentPageDataRaw, + Sidebar, validateContentPageSchema, } from "./output" import { @@ -25,6 +26,7 @@ import { import { CardsGridEnum, ContentBlocksTypenameEnum, + SidebarTypenameEnum, } from "@/types/components/content/enums" import { TrackingChannelEnum, @@ -145,11 +147,29 @@ export const contentPageQueryRouter = router({ }) : null + const sidebar = response.data.content_page.sidebar + ? response.data.content_page.sidebar.map((item: any) => { + switch (item.__typename) { + case SidebarTypenameEnum.ContentPageSidebarJoinLoyaltyContact: + return { + ...item, + join_loyalty_contact: { + ...item.join_loyalty_contact, + button: makeButtonObject(item.join_loyalty_contact.button), + }, + } + default: + return item + } + }) + : null + const heroImage = makeImageVaultImage(content_page.hero_image) const validatedContentPage = validateContentPageSchema.safeParse({ content_page: { ...content_page, blocks: processedBlocks, + sidebar, hero_image: heroImage, }, }) @@ -169,6 +189,7 @@ export const contentPageQueryRouter = router({ ...restContentPage, heroImage, blocks: blocks as Block[], + sidebar: sidebar as Sidebar[], } const tracking: TrackingSDKPageData = { diff --git a/types/components/content/enums.ts b/types/components/content/enums.ts index 98515a381..b69a067d5 100644 --- a/types/components/content/enums.ts +++ b/types/components/content/enums.ts @@ -1,3 +1,7 @@ +import { JoinLoyaltyContact } from "@/server/routers/contentstack/contentPage/output" + +import { Typename } from "@/types/requests/utils/typename" + export enum ContentBlocksTypenameEnum { ContentPageBlocksContent = "ContentPageBlocksContent", ContentPageBlocksShortcuts = "ContentPageBlocksShortcuts", @@ -15,3 +19,24 @@ export enum DynamicContentComponentEnum { how_it_works = "how_it_works", overview_table = "overview_table", } + +export enum SidebarTypenameEnum { + ContentPageSidebarJoinLoyaltyContact = "ContentPageSidebarJoinLoyaltyContact", + ContentPageSidebarContent = "ContentPageSidebarContent", + ContentPageSidebarDynamicContent = "ContentPageSidebarDynamicContent", +} + +export type SidebarTypename = keyof typeof SidebarTypenameEnum + +export enum JoinLoyaltyContactTypenameEnum { + ContentPageSidebarJoinLoyaltyContactBlockContactContact = "ContentPageSidebarJoinLoyaltyContactBlockContactContact", +} + +export type JoinLoyaltyContactContact = Typename< + JoinLoyaltyContact["join_loyalty_contact"], + JoinLoyaltyContactTypenameEnum.ContentPageSidebarJoinLoyaltyContactBlockContactContact +> + +export enum SidebarDynamicComponentEnum { + my_pages_navigation = "my_pages_navigation", +} diff --git a/types/components/content/sidebar.ts b/types/components/content/sidebar.ts new file mode 100644 index 000000000..843a16132 --- /dev/null +++ b/types/components/content/sidebar.ts @@ -0,0 +1,21 @@ +import { ContactFields } from "@/server/routers/contentstack/base/output" +import { + JoinLoyaltyContact, + Sidebar, +} from "@/server/routers/contentstack/contentPage/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 +}