feat(SW-187): Footer data from contentstack

This commit is contained in:
Pontus Dreij
2024-08-28 08:30:45 +02:00
parent 9a90fd891d
commit 6f6a0a2e7c
14 changed files with 370 additions and 22 deletions

View File

@@ -8,7 +8,7 @@ import Navigation from "./Navigation"
import styles from "./footer.module.css" import styles from "./footer.module.css"
export default async function Footer() { export default async function Footer() {
const footerData = await serverClient().contentstack.base.footer({ const footerData = await serverClient().contentstack.base.currentFooter({
lang: getLang(), lang: getLang(),
}) })
if (!footerData) { if (!footerData) {

View File

@@ -11,11 +11,11 @@ export default function FooterMainNav({ mainLinks }: FooterMainNavProps) {
<nav className={styles.mainNavigation}> <nav className={styles.mainNavigation}>
<ul className={styles.mainNavigationList}> <ul className={styles.mainNavigationList}>
{mainLinks.map((link) => ( {mainLinks.map((link) => (
<li key={link.id} className={styles.mainNavigationItem}> <li key={link.title} className={styles.mainNavigationItem}>
<Subtitle type="two" asChild> <Subtitle type="two" asChild>
<Link <Link
color="burgundy" color="burgundy"
href={link.href} href={link.url}
className={styles.mainNavigationLink} className={styles.mainNavigationLink}
> >
{link.title} {link.title}

View File

@@ -4,8 +4,9 @@ import FooterSecondaryNav from "./SecondaryNav"
import styles from "./navigation.module.css" import styles from "./navigation.module.css"
export default function FooterNavigation() { export default function FooterNavigation({ ...props }) {
const { mainLinks, secondaryLinks, appDownloads } = footer const { mainLinks } = props
const { secondaryLinks, appDownloads } = footer
return ( return (
<section className={styles.section}> <section className={styles.section}>
<div className={styles.maxWidth}> <div className={styles.maxWidth}>

View File

@@ -1,10 +1,21 @@
import { serverClient } from "@/lib/trpc/server"
import { getLang } from "@/i18n/serverContext"
import FooterDetails from "./Details" import FooterDetails from "./Details"
import FooterNavigation from "./Navigation" import FooterNavigation from "./Navigation"
export default function Footer() { export default async function Footer() {
const footerData = await serverClient().contentstack.base.footer({
lang: getLang(),
})
if (!footerData) {
return null
}
console.log("footerData:", footerData)
return ( return (
<footer> <footer>
<FooterNavigation /> <FooterNavigation {...footerData} />
<FooterDetails /> <FooterDetails />
</footer> </footer>
) )

View File

@@ -0,0 +1,12 @@
fragment AppDownloads on Footer {
app_downloads {
title
links {
type
href {
href
title
}
}
}
}

View File

@@ -0,0 +1,29 @@
fragment MainLinks on Footer {
main_links {
title
open_in_new_tab
link {
href
title
}
pageConnection {
edges {
node {
__typename
... on AccountPage {
title
url
}
... on LoyaltyPage {
title
url
}
... on ContentPage {
title
url
}
}
}
}
}
}

View File

@@ -0,0 +1,18 @@
fragment MainLinksRef on Footer {
__typename
main_links {
pageConnection {
edges {
node {
__typename
...LoyaltyPageRef
...ContentPageRef
...AccountPageRef
}
}
}
}
system {
...System
}
}

View File

@@ -0,0 +1,20 @@
fragment SecondaryLinksRef on Footer {
__typename
secondary_links {
links {
pageConnection {
edges {
node {
__typename
...LoyaltyPageRef
...ContentPageRef
...AccountPageRef
}
}
}
}
}
system {
...System
}
}

View File

@@ -0,0 +1,24 @@
#import "../Refs/MyPages/AccountPage.graphql"
#import "../Refs/ContentPage/ContentPage.graphql"
#import "../Refs/LoyaltyPage/LoyaltyPage.graphql"
fragment SecondaryLinks on Footer {
secondary_links {
title
links {
title
open_in_new_tab
pageConnection {
edges {
node {
__typename
}
}
}
link {
href
title
}
}
}
}

View File

@@ -0,0 +1,28 @@
#import "../Fragments/Footer/AppDownloads.graphql"
#import "../Fragments/Footer/MainLinks.graphql"
#import "../Fragments/Footer/SecondaryLinks.graphql"
#import "../Fragments/Footer/Refs/MainLinks.graphql"
#import "../Fragments/Footer/Refs/SecondaryLinks.graphql"
#import "../Fragments/Refs/System.graphql"
query GetFooter($locale: String!) {
all_footer(limit: 1, locale: $locale) {
items {
...MainLinks
...SecondaryLinks
...AppDownloads
}
}
}
query GetFooterRef($locale: String!) {
all_footer(limit: 1, locale: $locale) {
items {
...MainLinksRef
...SecondaryLinksRef
system {
...System
}
}
}
}

View File

@@ -7,6 +7,7 @@ import { removeMultipleSlashes } from "@/utils/url"
import { imageVaultAssetTransformedSchema } from "../schemas/imageVault" import { imageVaultAssetTransformedSchema } from "../schemas/imageVault"
import { Image } from "@/types/image" import { Image } from "@/types/image"
import { PageLinkEnum } from "@/types/requests/pageLinks"
// Help me write this zod schema based on the type ContactConfig // Help me write this zod schema based on the type ContactConfig
export const validateContactConfigSchema = z.object({ export const validateContactConfigSchema = z.object({
@@ -175,7 +176,7 @@ const validateNavigationItem = z.object({
export type NavigationItem = z.infer<typeof validateNavigationItem> export type NavigationItem = z.infer<typeof validateNavigationItem>
export const validateFooterConfigSchema = z.object({ export const validateCurrentFooterConfigSchema = z.object({
all_current_footer: z.object({ all_current_footer: z.object({
items: z.array( items: z.array(
z.object({ z.object({
@@ -242,16 +243,18 @@ export const validateFooterConfigSchema = z.object({
}), }),
}) })
export type FooterDataRaw = z.infer<typeof validateFooterConfigSchema> export type CurrentFooterDataRaw = z.infer<
typeof validateCurrentFooterConfigSchema
>
export type FooterData = Omit< export type CurrentFooterData = Omit<
FooterDataRaw["all_current_footer"]["items"][0], CurrentFooterDataRaw["all_current_footer"]["items"][0],
"logoConnection" "logoConnection"
> & { > & {
logo: Image logo: Image
} }
const validateFooterRefConfigSchema = z.object({ const validateCurrentFooterRefConfigSchema = z.object({
all_current_footer: z.object({ all_current_footer: z.object({
items: z.array( items: z.array(
z.object({ z.object({
@@ -264,6 +267,108 @@ const validateFooterRefConfigSchema = z.object({
}), }),
}) })
export type CurrentFooterRefDataRaw = z.infer<
typeof validateCurrentFooterRefConfigSchema
>
const validateExternalLink = z
.object({
href: z.string(),
title: z.string(),
})
.optional()
const validateInternalLink = z
.object({
edges: z.array(
z.object({
node: z.object({
title: z.string(),
url: z.string(),
}),
})
),
})
.optional()
const validateLinkItem = z.object({
title: z.string(),
open_in_new_tab: z.boolean(),
link: validateExternalLink,
pageConnection: validateInternalLink,
})
export type FooterLinkItem = z.infer<typeof validateLinkItem>
export const validateFooterConfigSchema = z.object({
all_footer: z.object({
items: z.array(
z.object({
main_links: z.array(validateLinkItem),
app_downloads: z.object({
title: z.string(),
links: z.array(
z.object({
type: z.string(),
href: validateExternalLink,
})
),
}),
secondary_links: z.array(
z.object({
title: z.string(),
links: z.array(validateLinkItem),
})
),
})
),
}),
})
export type FooterDataRaw = z.infer<typeof validateFooterConfigSchema>
export type FooterData = FooterDataRaw["all_footer"]["items"][0]
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 validateFooterRefConfigSchema = z.object({
all_footer: z.object({
items: z.array(
z.object({
main_links: z.array(
z.object({
pageConnection: pageConnectionRefs,
})
),
secondary_links: z.array(
z.object({
links: z.array(
z.object({
pageConnection: pageConnectionRefs,
})
),
})
),
system: z.object({
content_type_uid: z.string(),
uid: z.string(),
}),
})
),
}),
})
export type FooterRefDataRaw = z.infer<typeof validateFooterRefConfigSchema> export type FooterRefDataRaw = z.infer<typeof validateFooterRefConfigSchema>
const linkConnectionNodeSchema = z const linkConnectionNodeSchema = z

View File

@@ -9,6 +9,7 @@ import {
GetCurrentHeader, GetCurrentHeader,
GetCurrentHeaderRef, GetCurrentHeaderRef,
} from "@/lib/graphql/Query/CurrentHeader.graphql" } from "@/lib/graphql/Query/CurrentHeader.graphql"
import { GetFooter, GetFooterRef } from "@/lib/graphql/Query/Footer.graphql"
import { GetHeader, GetHeaderRef } from "@/lib/graphql/Query/Header.graphql" import { GetHeader, GetHeaderRef } from "@/lib/graphql/Query/Header.graphql"
import { request } from "@/lib/graphql/request" import { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc" import { notFound } from "@/server/errors/trpc"
@@ -23,6 +24,8 @@ import {
import { langInput } from "./input" import { langInput } from "./input"
import { import {
type ContactConfigData, type ContactConfigData,
CurrentFooterDataRaw,
CurrentFooterRefDataRaw,
CurrentHeaderData, CurrentHeaderData,
CurrentHeaderDataRaw, CurrentHeaderDataRaw,
CurrentHeaderRefDataRaw, CurrentHeaderRefDataRaw,
@@ -31,10 +34,11 @@ import {
getHeaderRefSchema, getHeaderRefSchema,
getHeaderSchema, getHeaderSchema,
validateContactConfigSchema, validateContactConfigSchema,
validateCurrentFooterConfigSchema,
validateCurrentHeaderConfigSchema, validateCurrentHeaderConfigSchema,
validateFooterConfigSchema, validateFooterConfigSchema,
} from "./output" } from "./output"
import { getConnections } from "./utils" import { getConnections, transformPageConnectionLinks } from "./utils"
import type { HeaderRefResponse, HeaderResponse } from "@/types/header" import type { HeaderRefResponse, HeaderResponse } from "@/types/header"
@@ -396,6 +400,87 @@ export const baseQueryRouter = router({
logo, logo,
} as CurrentHeaderData } as CurrentHeaderData
}), }),
currentFooter: contentstackBaseProcedure
.input(langInput)
.query(async ({ input }) => {
getFooterRefCounter.add(1, { lang: input.lang })
console.info(
"contentstack.footer.ref start",
JSON.stringify({ query: { lang: input.lang } })
)
const responseRef = await request<CurrentFooterRefDataRaw>(
GetCurrentFooterRef,
{
locale: input.lang,
}
)
// There's currently no error handling/validation for the responseRef, should it be added?
getFooterCounter.add(1, { lang: input.lang })
console.info(
"contentstack.footer start",
JSON.stringify({
query: {
lang: input.lang,
},
})
)
const response = await request<CurrentFooterDataRaw>(
GetCurrentFooter,
{
locale: input.lang,
},
{
next: {
tags: [generateRefsResponseTag(input.lang, "current_footer")],
},
}
)
if (!response.data) {
const notFoundError = notFound(response)
getFooterFailCounter.add(1, {
lang: input.lang,
error_type: "not_found",
error: JSON.stringify({ code: notFoundError.code }),
})
console.error(
"contentstack.footer not found error",
JSON.stringify({
query: {
lang: input.lang,
},
error: { code: notFoundError.code },
})
)
throw notFoundError
}
const validatedFooterConfig = validateCurrentFooterConfigSchema.safeParse(
response.data
)
if (!validatedFooterConfig.success) {
getFooterFailCounter.add(1, {
lang: input.lang,
error_type: "validation_error",
error: JSON.stringify(validatedFooterConfig.error),
})
console.error(
"contentstack.footer validation error",
JSON.stringify({
query: { lang: input.lang },
error: validatedFooterConfig.error,
})
)
return null
}
getFooterSuccessCounter.add(1, { lang: input.lang })
console.info(
"contentstack.footer success",
JSON.stringify({ query: { lang: input.lang } })
)
return validatedFooterConfig.data.all_current_footer.items[0]
}),
footer: contentstackBaseProcedure footer: contentstackBaseProcedure
.input(langInput) .input(langInput)
.query(async ({ input }) => { .query(async ({ input }) => {
@@ -412,7 +497,7 @@ export const baseQueryRouter = router({
{ {
cache: "force-cache", cache: "force-cache",
next: { next: {
tags: [generateRefsResponseTag(input.lang, "current_footer")], tags: [generateRefsResponseTag(input.lang, "footer")],
}, },
} }
) )
@@ -426,10 +511,9 @@ export const baseQueryRouter = router({
}, },
}) })
) )
const currentFooterUID = const currentFooterUID = responseRef.data.all_footer.items[0].system.uid
responseRef.data.all_current_footer.items[0].system.uid
const response = await request<FooterDataRaw>( const response = await request<FooterDataRaw>(
GetCurrentFooter, GetFooter,
{ {
locale: input.lang, locale: input.lang,
}, },
@@ -484,6 +568,15 @@ export const baseQueryRouter = router({
"contentstack.footer success", "contentstack.footer success",
JSON.stringify({ query: { lang: input.lang } }) JSON.stringify({ query: { lang: input.lang } })
) )
return validatedFooterConfig.data.all_current_footer.items[0] const validatedFooterData = validatedFooterConfig.data.all_footer.items[0]
const mainLinks = transformPageConnectionLinks(
validatedFooterData.main_links
)
return {
mainLinks: mainLinks,
appDownloads: validatedFooterData.app_downloads,
secondaryLinks: validatedFooterData.secondary_links,
}
}), }),
}) })

View File

@@ -1,6 +1,7 @@
import { HeaderRefResponse } from "@/types/header" import { HeaderRefResponse } from "@/types/header"
import { Edges } from "@/types/requests/utils/edges" import { Edges } from "@/types/requests/utils/edges"
import { NodeRefs } from "@/types/requests/utils/refs" import { NodeRefs } from "@/types/requests/utils/refs"
import type { FooterLinkItem } from "./output"
export function getConnections(refs: HeaderRefResponse) { export function getConnections(refs: HeaderRefResponse) {
const connections: Edges<NodeRefs>[] = [] const connections: Edges<NodeRefs>[] = []
@@ -38,3 +39,12 @@ export function getConnections(refs: HeaderRefResponse) {
return connections return connections
} }
export function transformPageConnectionLinks(links: FooterLinkItem[]) {
return links.flatMap((link) =>
link.pageConnection?.edges.map((edge) => ({
title: edge.node.title,
url: edge.node.url,
}))
)
}

View File

@@ -1,9 +1,6 @@
export type FooterMainNav = { export type FooterMainNav = {
id: string url: string
href: string
title: string title: string
openInNewTab: boolean
isExternal: boolean
} }
export type FooterMainNavProps = { export type FooterMainNavProps = {
mainLinks: FooterMainNav[] mainLinks: FooterMainNav[]