feat(SW-187): Footer data from contentstack
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
12
lib/graphql/Fragments/Footer/AppDownloads.graphql
Normal file
12
lib/graphql/Fragments/Footer/AppDownloads.graphql
Normal file
@@ -0,0 +1,12 @@
|
||||
fragment AppDownloads on Footer {
|
||||
app_downloads {
|
||||
title
|
||||
links {
|
||||
type
|
||||
href {
|
||||
href
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
lib/graphql/Fragments/Footer/MainLinks.graphql
Normal file
29
lib/graphql/Fragments/Footer/MainLinks.graphql
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
lib/graphql/Fragments/Footer/Refs/MainLinks.graphql
Normal file
18
lib/graphql/Fragments/Footer/Refs/MainLinks.graphql
Normal file
@@ -0,0 +1,18 @@
|
||||
fragment MainLinksRef on Footer {
|
||||
__typename
|
||||
main_links {
|
||||
pageConnection {
|
||||
edges {
|
||||
node {
|
||||
__typename
|
||||
...LoyaltyPageRef
|
||||
...ContentPageRef
|
||||
...AccountPageRef
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
system {
|
||||
...System
|
||||
}
|
||||
}
|
||||
20
lib/graphql/Fragments/Footer/Refs/SecondaryLinks.graphql
Normal file
20
lib/graphql/Fragments/Footer/Refs/SecondaryLinks.graphql
Normal file
@@ -0,0 +1,20 @@
|
||||
fragment SecondaryLinksRef on Footer {
|
||||
__typename
|
||||
secondary_links {
|
||||
links {
|
||||
pageConnection {
|
||||
edges {
|
||||
node {
|
||||
__typename
|
||||
...LoyaltyPageRef
|
||||
...ContentPageRef
|
||||
...AccountPageRef
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
system {
|
||||
...System
|
||||
}
|
||||
}
|
||||
24
lib/graphql/Fragments/Footer/SecondaryLinks.graphql
Normal file
24
lib/graphql/Fragments/Footer/SecondaryLinks.graphql
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
lib/graphql/Query/Footer.graphql
Normal file
28
lib/graphql/Query/Footer.graphql
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
export type FooterMainNav = {
|
||||
id: string
|
||||
href: string
|
||||
url: string
|
||||
title: string
|
||||
openInNewTab: boolean
|
||||
isExternal: boolean
|
||||
}
|
||||
export type FooterMainNavProps = {
|
||||
mainLinks: FooterMainNav[]
|
||||
|
||||
Reference in New Issue
Block a user