fix: sync current header with static-pages

This commit is contained in:
Christel Westerberg
2024-05-24 13:22:15 +02:00
parent 19d8b757cd
commit dd930688fc
22 changed files with 612 additions and 148 deletions

View File

@@ -60,7 +60,7 @@ export default async function CurrentContentPage({
return (
<>
<Header lang={params.lang} uid={pageData.system.uid} />
<Header lang={params.lang} />
<ContentPage data={response.data} />
<Tracking pageData={trackingData} />
</>

View File

@@ -4,6 +4,7 @@ import Script from "next/script"
import AdobeScript from "@/components/Current/AdobeScript"
import Footer from "@/components/Current/Footer"
import Header from "@/components/Current/Header"
import LangPopup from "@/components/Current/LangPopup"
import SkipToMainContent from "@/components/SkipToMainContent"
@@ -75,6 +76,7 @@ export default function RootLayout({
<body className="theme-00Corecolours theme-X0Oldcorecolours">
<LangPopup lang={params.lang} />
<SkipToMainContent lang={params.lang} />
<Header lang={params.lang} />
{children}
<Footer lang={params.lang} />
<Script id="page-tracking">{`

View File

@@ -3,20 +3,16 @@ import { useState } from "react"
import Image from "@/components/Image"
import Mobile from "../LanguageSwitcher/Mobile"
import styles from "./mainMenu.module.css"
import type { MainMenuProps } from "@/types/components/current/header/mainMenu"
export default function MainMenu({
currentLanguage,
frontpageLinkText,
homeHref,
links,
logo,
topMenuMobileLinks,
urls,
}: MainMenuProps) {
const [isOpen, setIsOpen] = useState(false)
@@ -69,8 +65,8 @@ export default function MainMenu({
data-collapsable="main-menu"
id="main-menu"
>
{links.map((link) => (
<li className={styles.li} key={link.href}>
{links.map((link, i) => (
<li className={styles.li} key={link.href + i}>
<a className={styles.link} href={link.href}>
{link.title}
</a>
@@ -78,8 +74,8 @@ export default function MainMenu({
))}
<ul className={styles.mobileList}>
{topMenuMobileLinks.map(({ link }) => (
<li className={styles.mobileLi} key={link.href}>
{topMenuMobileLinks.map(({ link }, i) => (
<li className={styles.mobileLi} key={link.href + i}>
<a className={styles.mobileLink} href={link.href}>
{link.title}
</a>
@@ -87,11 +83,9 @@ export default function MainMenu({
))}
</ul>
{urls ? (
<li className={styles.mobileLi}>
<Mobile currentLanguage={currentLanguage} urls={urls} />
</li>
) : null}
{/* {languageSwitcher ? (
<li className={styles.mobileLi}>{languageSwitcher}</li>
) : null} */}
</ul>
</nav>
</div>

View File

@@ -1,33 +1,29 @@
import Desktop from "../LanguageSwitcher/Desktop"
import styles from "./topMenu.module.css"
import type { TopMenuProps } from "@/types/components/current/header/topMenu"
export default function TopMenu({ currentLanguage, frontpageLinkText, homeHref, links, urls }: TopMenuProps) {
export default function TopMenu({
frontpageLinkText,
homeHref,
links,
}: TopMenuProps) {
return (
<div className={styles.topMenu}>
<div className={styles.container}>
<a
className={styles.homeLink}
href={homeHref}
>
<a className={styles.homeLink} href={homeHref}>
{frontpageLinkText}
</a>
<ul className={styles.list}>
{urls ? (
{/* {languageSwitcher ? (
<li className="nav-secondary__item hidden-xxsmall hidden-xsmall hidden-small">
<Desktop currentLanguage={currentLanguage} urls={urls} />
{languageSwitcher}
</li>
) : null}
) : null} */}
{links.map(({ link }) => (
<li key={link.href}>
<a
className={styles.link}
href={link.href}
>
{links.map(({ link }, i) => (
<li key={link.href + i}>
<a className={styles.link} href={link.href}>
{link.title}
</a>
</li>

View File

@@ -1,13 +1,6 @@
import { homeHrefs } from "@/constants/homeHrefs"
import { languages } from "@/constants/languages"
import { env } from "@/env/server"
import { batchRequest } from "@/lib/graphql/batchRequest"
import { GetHeader } from "@/lib/graphql/Query/Header.graphql"
import {
GetDaDeEnUrls,
GetFiNoSvUrls,
} from "@/lib/graphql/Query/LanguageSwitcher.graphql"
import { request } from "@/lib/graphql/request"
import { serverClient } from "@/lib/trpc/server"
import MainMenu from "./MainMenu"
import OfflineBanner from "./OfflineBanner"
@@ -15,72 +8,33 @@ import TopMenu from "./TopMenu"
import styles from "./header.module.css"
import type { HeaderProps } from "@/types/components/current/header"
import { LangParams } from "@/types/params"
import type { HeaderQueryData } from "@/types/requests/header"
import type { LanguageSwitcherQueryData } from "@/types/requests/languageSwitcher"
export default async function Header({ lang, uid }: LangParams & HeaderProps) {
try {
const variables = {
locale: lang,
uid,
}
export default async function Header({ lang }: LangParams) {
const data = await serverClient().contentstack.config.header()
const { data } = await request<HeaderQueryData>(
GetHeader,
{ locale: lang },
{ next: { tags: [`header-${lang}`] } }
)
const { data: urls } = await batchRequest<LanguageSwitcherQueryData>([
{
document: GetDaDeEnUrls,
tags: [`DA-DE-EN-${uid}`],
variables,
},
{
document: GetFiNoSvUrls,
tags: [`FI-NO-SV-${uid}`],
variables,
},
])
const homeHref = homeHrefs[env.NODE_ENV][lang]
const { frontpage_link_text, logo, menu, top_menu } = data
if (!data.all_header.items.length) {
return null
}
const topMenuMobileLinks = top_menu.links
.filter((link) => link.show_on_mobile)
.sort((a, b) => (a.sort_order_mobile < b.sort_order_mobile ? 1 : -1))
const currentLanguage = languages[lang]
const homeHref = homeHrefs[env.NODE_ENV][lang]
const { frontpage_link_text, logoConnection, menu, top_menu } =
data.all_header.items[0]
const logo = logoConnection.edges?.[0]?.node
const topMenuMobileLinks = top_menu.links
.filter((link) => link.show_on_mobile)
.sort((a, b) => (a.sort_order_mobile < b.sort_order_mobile ? 1 : -1))
return (
<header className={styles.header} role="banner">
<OfflineBanner />
<TopMenu
currentLanguage={currentLanguage}
frontpageLinkText={frontpage_link_text}
homeHref={homeHref}
links={top_menu.links}
urls={urls}
/>
<MainMenu
currentLanguage={currentLanguage}
frontpageLinkText={frontpage_link_text}
homeHref={homeHref}
links={menu.links}
logo={logo}
topMenuMobileLinks={topMenuMobileLinks}
urls={urls}
/>
</header>
)
} catch (error) {
console.error(error)
return null
}
return (
<header className={styles.header} role="banner">
<OfflineBanner />
<TopMenu
frontpageLinkText={frontpage_link_text}
homeHref={homeHref}
links={top_menu.links}
/>
<MainMenu
frontpageLinkText={frontpage_link_text}
homeHref={homeHref}
links={menu.links}
logo={logo}
topMenuMobileLinks={topMenuMobileLinks}
/>
</header>
)
}

View File

@@ -8,7 +8,7 @@ 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()
const data = await serverClient().contentstack.config.contact()
const val = getValueFromContactConfig(contact.contact_field, data)

View File

@@ -0,0 +1,32 @@
#import "../Fragments/Image.graphql"
query GetCurrentHeader($locale: String!) {
all_current_header(limit: 1, locale: $locale) {
items {
frontpage_link_text
logoConnection {
edges {
node {
...Image
}
}
}
menu {
links {
href
title
}
}
top_menu {
links {
link {
href
title
}
show_on_mobile
sort_order_mobile
}
}
}
}
}

View File

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

View File

@@ -1,5 +1,7 @@
import { z } from "zod"
import { Image } from "@/types/image"
// Help me write this zod schema based on the type ContactConfig
export const validateContactConfigSchema = z.object({
all_contact_config: z.object({
@@ -58,3 +60,61 @@ export type ContactFields = {
display_text: string | null
contact_field: string
}
export const validateHeaderConfigSchema = z.object({
all_current_header: z.object({
items: z.array(
z.object({
frontpage_link_text: z.string(),
logoConnection: z.object({
edges: z.array(
z.object({
node: z.object({
description: z.string().optional(),
dimension: z.object({
height: z.number(),
width: z.number(),
}),
metadata: z.any().nullable(),
system: z.object({
uid: z.string(),
}),
title: z.string(),
url: z.string(),
}),
})
),
}),
menu: z.object({
links: z.array(
z.object({
href: z.string(),
title: z.string(),
})
),
}),
top_menu: z.object({
links: z.array(
z.object({
link: z.object({
href: z.string(),
title: z.string(),
}),
show_on_mobile: z.boolean(),
sort_order_mobile: z.number(),
})
),
}),
})
),
}),
})
export type HeaderDataRaw = z.infer<typeof validateHeaderConfigSchema>
export type HeaderData = Omit<
HeaderDataRaw["all_current_header"]["items"][0],
"logoConnection"
> & {
logo: Image
}

View File

@@ -0,0 +1,66 @@
import { GetContactConfig } from "@/lib/graphql/Query/ContactConfig.graphql"
import { GetCurrentHeader } from "@/lib/graphql/Query/CurrentHeader.graphql"
import { request } from "@/lib/graphql/request"
import { internalServerError, notFound } from "@/server/errors/trpc"
import { contentstackProcedure, publicProcedure, router } from "@/server/trpc"
import {
type ContactConfigData,
HeaderData,
HeaderDataRaw,
validateContactConfigSchema,
validateHeaderConfigSchema,
} from "./output"
export const configQueryRouter = router({
contact: contentstackProcedure.query(async ({ ctx }) => {
const { lang } = ctx
const response = await request<ContactConfigData>(GetContactConfig, {
locale: lang,
})
if (!response.data) {
throw notFound(response)
}
const validatedContactConfigConfig = validateContactConfigSchema.safeParse(
response.data
)
if (!validatedContactConfigConfig.success) {
throw internalServerError(validatedContactConfigConfig.error)
}
return validatedContactConfigConfig.data.all_contact_config.items[0]
}),
header: publicProcedure.query(async ({ ctx }) => {
console.log({ ctx })
const response = await request<HeaderDataRaw>(
GetCurrentHeader,
{ locale: ctx.lang },
{ next: { tags: [`header-${ctx.lang}`] } }
)
if (!response.data) {
throw notFound(response)
}
const validatedHeaderConfig = validateHeaderConfigSchema.safeParse(
response.data
)
if (!validatedHeaderConfig.success) {
throw internalServerError(validatedHeaderConfig.error)
}
const logo =
validatedHeaderConfig.data.all_current_header.items[0].logoConnection
.edges?.[0]?.node
return {
...validatedHeaderConfig.data.all_current_header.items[0],
logo,
} as HeaderData
}),
})

View File

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

View File

@@ -1,30 +0,0 @@
import { GetContactConfig } from "@/lib/graphql/Query/ContactConfig.graphql"
import { request } from "@/lib/graphql/request"
import { internalServerError, notFound } from "@/server/errors/trpc"
import { contentstackProcedure, router } from "@/server/trpc"
import { type ContactConfigData, validateContactConfigSchema } from "./output"
export const contactConfigQueryRouter = router({
get: contentstackProcedure.query(async ({ ctx }) => {
const { lang } = ctx
const response = await request<ContactConfigData>(GetContactConfig, {
locale: lang,
})
if (!response.data) {
throw notFound(response)
}
const validatedContactConfigConfig = validateContactConfigSchema.safeParse(
response.data
)
if (!validatedContactConfigConfig.success) {
throw internalServerError(validatedContactConfigConfig.error)
}
return validatedContactConfigConfig.data.all_contact_config.items[0]
}),
})

View File

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

View File

@@ -0,0 +1,190 @@
import { z } from "zod"
import { Lang } from "@/constants/languages"
import {
ContentEntries,
DynamicContentComponents,
} from "@/types/components/myPages/myPage/enums"
import { Embeds } from "@/types/requests/embeds"
import { PageLinkEnum } from "@/types/requests/pageLinks"
import { Edges } from "@/types/requests/utils/edges"
import { RTEDocument } from "@/types/rte/node"
const accountPageShortcuts = z.object({
__typename: z.literal(ContentEntries.AccountPageContentShortcuts),
shortcuts: z.object({
title: z.string().nullable(),
preamble: z.string().nullable(),
shortcuts: z.array(
z.object({
linkConnection: z.object({
edges: z.array(
z.object({
node: z.object({
system: z.object({
uid: z.string(),
locale: z.nativeEnum(Lang),
}),
original_url: z.string().nullable().optional(),
url: z.string(),
title: z.string(),
}),
})
),
totalCount: z.number(),
}),
text: z.string().nullable(),
open_in_new_tab: z.boolean(),
})
),
}),
})
const accountPageDynamicContent = z.object({
__typename: z.literal(ContentEntries.AccountPageContentDynamicContent),
dynamic_content: z.object({
title: z.string().nullable(),
preamble: z.string().nullable(),
component: z.nativeEnum(DynamicContentComponents),
link: z.object({
linkConnection: z.object({
edges: z.array(
z.object({
node: z.object({
system: z.object({
uid: z.string(),
locale: z.nativeEnum(Lang),
}),
url: z.string(),
original_url: z.string().nullable().optional(),
title: z.string(),
}),
})
),
totalCount: z.number(),
}),
link_text: z.string(),
}),
}),
})
const accountPageTextContent = z.object({
__typename: z.literal(ContentEntries.AccountPageContentTextContent),
text_content: z.object({
content: z.object({
json: z.any(),
embedded_itemsConnection: z.object({
edges: z.array(z.any()),
totalCount: z.number(),
}),
}),
}),
})
type TextContentRaw = z.infer<typeof accountPageTextContent>
type DynamicContentRaw = z.infer<typeof accountPageDynamicContent>
type ShortcutsRaw = z.infer<typeof accountPageShortcuts>
export type Shortcuts = Omit<ShortcutsRaw, "shortcuts"> & {
shortcuts: Omit<ShortcutsRaw["shortcuts"], "shortcuts"> & {
shortcuts: {
text?: string
openInNewTab: boolean
url: string
title: string
}[]
}
}
export type RteTextContent = Omit<TextContentRaw, "text_content"> & {
text_content: {
content: {
json: RTEDocument
embedded_itemsConnection: Edges<Embeds>
}
}
}
export type AccountPageContentItem =
| DynamicContentRaw
| Shortcuts
| RteTextContent
const accountPageContentItem = z.discriminatedUnion("__typename", [
accountPageShortcuts,
accountPageDynamicContent,
accountPageTextContent,
])
export const validateAccountPageSchema = z.object({
account_page: z.object({
url: z.string(),
title: z.string(),
content: z.array(accountPageContentItem),
}),
})
export type AccountPageDataRaw = z.infer<typeof validateAccountPageSchema>
type AccountPageRaw = AccountPageDataRaw["account_page"]
export type AccountPage = Omit<AccountPageRaw, "content"> & {
content: AccountPageContentItem[]
}
// Refs types
const pageConnectionRefs = z.object({
edges: z.array(
z.object({
node: z.object({
__typename: z.nativeEnum(PageLinkEnum),
system: z.object({
content_type_uid: z.string(),
uid: z.string(),
}),
}),
})
),
})
const accountPageShortcutsRefs = z.object({
__typename: z.literal(ContentEntries.AccountPageContentShortcuts),
shortcuts: z.object({
shortcuts: z.array(
z.object({
linkConnection: pageConnectionRefs,
})
),
}),
})
const accountPageDynamicContentRefs = z.object({
__typename: z.literal(ContentEntries.AccountPageContentDynamicContent),
dynamic_content: z.object({
link: z.object({
linkConnection: pageConnectionRefs,
}),
}),
})
const accountPageContentItemRefs = z.discriminatedUnion("__typename", [
accountPageDynamicContentRefs,
accountPageShortcutsRefs,
])
export const validateAccountPageRefsSchema = z.object({
account_page: z.object({
content: z.array(accountPageContentItemRefs),
system: z.object({
content_type_uid: z.string(),
uid: z.string(),
}),
}),
})
export type AccountPageRefsDataRaw = z.infer<
typeof validateAccountPageRefsSchema
>

View File

@@ -0,0 +1,134 @@
import {
GetAccountPage,
GetAccountPageRefs,
} from "@/lib/graphql/Query/AccountPage.graphql"
import { request } from "@/lib/graphql/request"
import { internalServerError, notFound } from "@/server/errors/trpc"
import { contentstackProcedure, router } from "@/server/trpc"
import {
generateRefsResponseTag,
generateTag,
generateTags,
} from "@/utils/generateTag"
import { removeEmptyObjects } from "../../utils"
import {
type AccountPage,
AccountPageRefsDataRaw,
validateAccountPageRefsSchema,
validateAccountPageSchema,
} from "./output"
import { getConnections } from "./utils"
import { ContentEntries } from "@/types/components/myPages/myPage/enums"
import { Embeds } from "@/types/requests/embeds"
import { Edges } from "@/types/requests/utils/edges"
import { RTEDocument } from "@/types/rte/node"
export const accountPageQueryRouter = router({
get: contentstackProcedure.query(async ({ ctx }) => {
const { lang, uid } = ctx
const refsResponse = await request<AccountPageRefsDataRaw>(
GetAccountPageRefs,
{
locale: lang,
uid,
},
{
next: {
tags: [generateRefsResponseTag(lang, uid)],
},
}
)
if (!refsResponse.data) {
throw notFound(refsResponse)
}
// Remove empty objects from a fetched content type. Needed since
// Contentstack returns empty objects for all non queried blocks in modular blocks.
// This is an ongoing support case in Contentstack, ticker number #00031579
const cleanedData = removeEmptyObjects(refsResponse.data)
const validatedAccountPageRefs =
validateAccountPageRefsSchema.safeParse(cleanedData)
if (!validatedAccountPageRefs.success) {
throw internalServerError(validatedAccountPageRefs.error)
}
const connections = getConnections(validatedAccountPageRefs.data)
const tags = [
generateTags(lang, connections),
generateTag(lang, validatedAccountPageRefs.data.account_page.system.uid),
].flat()
const response = await request<AccountPageRefsDataRaw>(
GetAccountPage,
{
locale: lang,
uid,
},
{ next: { tags } }
)
if (!response.data) {
throw notFound(response)
}
const validatedAccountPage = validateAccountPageSchema.safeParse(
response.data
)
if (!validatedAccountPage.success) {
throw internalServerError(validatedAccountPage.error)
}
// TODO: Make returned data nicer
const content = validatedAccountPage.data.account_page.content.map(
(block) => {
switch (block.__typename) {
case ContentEntries.AccountPageContentDynamicContent:
return block
case ContentEntries.AccountPageContentShortcuts:
return {
...block,
shortcuts: {
...block.shortcuts,
shortcuts: block.shortcuts.shortcuts.map((shortcut) => ({
text: shortcut.text,
openInNewTab: shortcut.open_in_new_tab,
...shortcut.linkConnection.edges[0].node,
url:
shortcut.linkConnection.edges[0].node.original_url ||
`/${shortcut.linkConnection.edges[0].node.system.locale}${shortcut.linkConnection.edges[0].node.url}`,
})),
},
}
case ContentEntries.AccountPageContentTextContent:
return {
...block,
text_content: {
content: {
json: block.text_content.content.json as RTEDocument,
embedded_itemsConnection: block.text_content.content
.embedded_itemsConnection as Edges<Embeds>,
},
},
}
default:
return null
}
}
)
const accountPage = {
...validatedAccountPage.data.account_page,
content,
} as AccountPage
return accountPage
}),
})

View File

@@ -0,0 +1,31 @@
import { AccountPageRefsDataRaw } from "./output"
import { ContentEntries } from "@/types/components/myPages/myPage/enums"
import type { Edges } from "@/types/requests/utils/edges"
import type { NodeRefs } from "@/types/requests/utils/refs"
export function getConnections(refs: AccountPageRefsDataRaw) {
const connections: Edges<NodeRefs>[] = []
if (refs.account_page.content) {
refs.account_page.content.forEach((item) => {
switch (item.__typename) {
case ContentEntries.AccountPageContentShortcuts: {
item.shortcuts.shortcuts.forEach((shortcut) => {
if (shortcut.linkConnection.edges.length) {
connections.push(shortcut.linkConnection)
}
})
break
}
case ContentEntries.AccountPageContentDynamicContent: {
if (item.dynamic_content.link.linkConnection.edges.length) {
connections.push(item.dynamic_content.link.linkConnection)
}
break
}
}
})
}
return connections
}

View File

@@ -2,14 +2,14 @@ import { router } from "@/server/trpc"
import { accountPageRouter } from "./accountPage"
import { breadcrumbsRouter } from "./breadcrumbs"
import { contactConfigRouter } from "./contactConfig"
import { configRouter } from "./config"
import { loyaltyPageRouter } from "./loyaltyPage"
import { myPagesRouter } from "./myPages"
export const contentstackRouter = router({
breadcrumbs: breadcrumbsRouter,
accountPage: accountPageRouter,
contactConfig: contactConfigRouter,
config: configRouter,
loyaltyPage: loyaltyPageRouter,
myPages: myPagesRouter,
})

View File

@@ -1,13 +1,10 @@
import type { HeaderLink, TopMenuHeaderLink } from "@/types/requests/header"
import type { Image } from "@/types/image"
import type { LanguageSwitcherQueryData } from "@/types/requests/languageSwitcher"
import type { HeaderLink, TopMenuHeaderLink } from "@/types/requests/header"
export type MainMenuProps = {
currentLanguage: string
frontpageLinkText: string
homeHref: string
links: HeaderLink[]
logo: Image
topMenuMobileLinks: TopMenuHeaderLink[]
urls: LanguageSwitcherQueryData
}

View File

@@ -1,10 +1,7 @@
import type { LanguageSwitcherQueryData } from "@/types/requests/languageSwitcher"
import type { TopMenuHeaderLink } from "@/types/requests/header"
export type TopMenuProps = {
currentLanguage: string
frontpageLinkText: string
homeHref: string
links: TopMenuHeaderLink[]
urls: LanguageSwitcherQueryData
}

View File

@@ -1,4 +1,4 @@
import { ContactFields } from "@/server/routers/contentstack/contactConfig/output"
import { ContactFields } from "@/server/routers/contentstack/config/output"
import {
JoinLoyaltyContact,
Sidebar,

View File

@@ -0,0 +1,36 @@
import type { Image } from "../image"
import type { AllRequestResponse } from "./utils/all"
import type { Edges } from "./utils/edges"
export type CurrentHeaderLink = {
href: string
title: string
}
export type TopMenuCurrentHeaderLink = {
link: {
href: string
title: string
}
show_on_mobile: boolean
sort_order_mobile: number
}
export type CurrentHeaderLinks = {
links: CurrentHeaderLink[]
}
export type TopMenuCurrentHeaderLinks = {
links: TopMenuCurrentHeaderLink[]
}
export type CurrentHeader = {
frontpage_link_text: string
logoConnection: Edges<Image>
menu: CurrentHeaderLinks
top_menu: TopMenuCurrentHeaderLinks
}
export type GetCurrentHeaderData = {
all_current_header: AllRequestResponse<CurrentHeader>
}

View File

@@ -1,7 +1,7 @@
import {
ContactConfig,
ContactFieldGroups,
} from "@/server/routers/contentstack/contactConfig/output"
} from "@/server/routers/contentstack/config/output"
export function getValueFromContactConfig(
keyString: string,