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"
export default async function Footer() {
const footerData = await serverClient().contentstack.base.footer({
const footerData = await serverClient().contentstack.base.currentFooter({
lang: getLang(),
})
if (!footerData) {

View File

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

View File

@@ -4,8 +4,9 @@ import FooterSecondaryNav from "./SecondaryNav"
import styles from "./navigation.module.css"
export default function FooterNavigation() {
const { mainLinks, secondaryLinks, appDownloads } = footer
export default function FooterNavigation({ ...props }) {
const { mainLinks } = props
const { secondaryLinks, appDownloads } = footer
return (
<section className={styles.section}>
<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 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 (
<footer>
<FooterNavigation />
<FooterNavigation {...footerData} />
<FooterDetails />
</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 { Image } from "@/types/image"
import { PageLinkEnum } from "@/types/requests/pageLinks"
// Help me write this zod schema based on the type ContactConfig
export const validateContactConfigSchema = z.object({
@@ -175,7 +176,7 @@ const validateNavigationItem = z.object({
export type NavigationItem = z.infer<typeof validateNavigationItem>
export const validateFooterConfigSchema = z.object({
export const validateCurrentFooterConfigSchema = z.object({
all_current_footer: z.object({
items: z.array(
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<
FooterDataRaw["all_current_footer"]["items"][0],
export type CurrentFooterData = Omit<
CurrentFooterDataRaw["all_current_footer"]["items"][0],
"logoConnection"
> & {
logo: Image
}
const validateFooterRefConfigSchema = z.object({
const validateCurrentFooterRefConfigSchema = z.object({
all_current_footer: z.object({
items: z.array(
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>
const linkConnectionNodeSchema = z

View File

@@ -9,6 +9,7 @@ import {
GetCurrentHeader,
GetCurrentHeaderRef,
} 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 { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc"
@@ -23,6 +24,8 @@ import {
import { langInput } from "./input"
import {
type ContactConfigData,
CurrentFooterDataRaw,
CurrentFooterRefDataRaw,
CurrentHeaderData,
CurrentHeaderDataRaw,
CurrentHeaderRefDataRaw,
@@ -31,10 +34,11 @@ import {
getHeaderRefSchema,
getHeaderSchema,
validateContactConfigSchema,
validateCurrentFooterConfigSchema,
validateCurrentHeaderConfigSchema,
validateFooterConfigSchema,
} from "./output"
import { getConnections } from "./utils"
import { getConnections, transformPageConnectionLinks } from "./utils"
import type { HeaderRefResponse, HeaderResponse } from "@/types/header"
@@ -396,6 +400,87 @@ export const baseQueryRouter = router({
logo,
} 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
.input(langInput)
.query(async ({ input }) => {
@@ -412,7 +497,7 @@ export const baseQueryRouter = router({
{
cache: "force-cache",
next: {
tags: [generateRefsResponseTag(input.lang, "current_footer")],
tags: [generateRefsResponseTag(input.lang, "footer")],
},
}
)
@@ -426,10 +511,9 @@ export const baseQueryRouter = router({
},
})
)
const currentFooterUID =
responseRef.data.all_current_footer.items[0].system.uid
const currentFooterUID = responseRef.data.all_footer.items[0].system.uid
const response = await request<FooterDataRaw>(
GetCurrentFooter,
GetFooter,
{
locale: input.lang,
},
@@ -484,6 +568,15 @@ export const baseQueryRouter = router({
"contentstack.footer success",
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 { Edges } from "@/types/requests/utils/edges"
import { NodeRefs } from "@/types/requests/utils/refs"
import type { FooterLinkItem } from "./output"
export function getConnections(refs: HeaderRefResponse) {
const connections: Edges<NodeRefs>[] = []
@@ -38,3 +39,12 @@ export function getConnections(refs: HeaderRefResponse) {
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 = {
id: string
href: string
url: string
title: string
openInNewTab: boolean
isExternal: boolean
}
export type FooterMainNavProps = {
mainLinks: FooterMainNav[]