diff --git a/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx b/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx
index b08047c79..84a20d71f 100644
--- a/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx
+++ b/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx
@@ -13,16 +13,12 @@ import type { NavigationMenuItemProps } from "@/types/components/header/navigati
export default function MenuItem({ item, isMobile }: NavigationMenuItemProps) {
const { submenu, title, link, seeAllLink, card } = item
- const [isExpanded, setIsExpanded] = useState(false)
+ const [isExpanded, setIsExpanded] = useState(false) // TODO: Use store to manage this state when adding the menu itself.
function handleButtonClick() {
setIsExpanded((prev) => !prev)
}
- if (!submenu.length && !link) {
- return null
- }
-
return submenu.length ? (
- {menuItems ? (
+ {menuItems?.length ? (
) : null}
{user ? (
@@ -74,7 +74,7 @@ export default async function MainMenu({
)}
- {menuItems ? (
+ {menuItems?.length ? (
) : null}
@@ -82,26 +82,3 @@ export default async function MainMenu({
)
}
-
-const error = {
- query: { lang: "sv" },
- error: {
- issues: [
- {
- code: "invalid_type",
- expected: "string",
- received: "null",
- path: ["all_header", "items", 0, "top_link", "title"],
- message: "Expected string, received null",
- },
- {
- code: "invalid_type",
- expected: "array",
- received: "null",
- path: ["all_header", "items", 0, "menu_items"],
- message: "Expected array, received null",
- },
- ],
- name: "ZodError",
- },
-}
diff --git a/components/Header/TopMenu/topMenu.module.css b/components/Header/TopMenu/topMenu.module.css
index a41a638cd..6d6c99abe 100644
--- a/components/Header/TopMenu/topMenu.module.css
+++ b/components/Header/TopMenu/topMenu.module.css
@@ -1,16 +1,13 @@
.topMenu {
display: none;
background-color: var(--Base-Surface-Subtle-Normal);
- padding: var(--Spacing-x2) var(--Spacing-x-one-and-half);
+ padding: var(--Spacing-x2);
border-bottom: 1px solid var(--Base-Border-Subtle);
}
.content {
max-width: var(--max-width-navigation);
margin: 0 auto;
- display: grid;
- justify-content: space-between;
- gap: var(--Spacing-x3);
}
.options {
@@ -24,7 +21,10 @@
display: block;
}
.content {
+ display: grid;
grid-template-areas: "topLink options";
+ justify-content: space-between;
+ gap: var(--Spacing-x3);
}
.topLink {
diff --git a/lib/graphql/Fragments/Header/InternalOrExternalLink.graphql b/lib/graphql/Fragments/Header/InternalOrExternalLink.graphql
deleted file mode 100644
index 1c37ed202..000000000
--- a/lib/graphql/Fragments/Header/InternalOrExternalLink.graphql
+++ /dev/null
@@ -1,22 +0,0 @@
-#import "../PageLink/ContentPageLink.graphql"
-#import "../PageLink/LoyaltyPageLink.graphql"
-
-fragment InternalOrExternalLink on InternalOrExternalLink {
- is_external_link
- open_in_new_tab
- external_link {
- href
- title
- }
- page_link {
- linkConnection {
- edges {
- node {
- ...ContentPageLink
- ...LoyaltyPageLink
- }
- }
- }
- link_title
- }
-}
diff --git a/lib/graphql/Fragments/PageLink/HotelPageLink.graphql b/lib/graphql/Fragments/PageLink/HotelPageLink.graphql
index 619b374d7..eda76855c 100644
--- a/lib/graphql/Fragments/PageLink/HotelPageLink.graphql
+++ b/lib/graphql/Fragments/PageLink/HotelPageLink.graphql
@@ -5,4 +5,8 @@ fragment HotelPageLink on HotelPage {
}
title
url
+ # TODO: Might need to add this if this is needed for hotel pages.
+ # web {
+ # original_url
+ # }
}
diff --git a/lib/graphql/Fragments/Refs/HotelPage/Breadcrumbs.graphql b/lib/graphql/Fragments/Refs/HotelPage/Breadcrumbs.graphql
new file mode 100644
index 000000000..1575f4eb6
--- /dev/null
+++ b/lib/graphql/Fragments/Refs/HotelPage/Breadcrumbs.graphql
@@ -0,0 +1,48 @@
+#import "../System.graphql"
+
+fragment HotelPageBreadcrumbsRefs on HotelPage {
+ web {
+ breadcrumbs {
+ title
+ parentsConnection {
+ edges {
+ node {
+ ... on ContentPage {
+ web {
+ breadcrumbs {
+ title
+ }
+ }
+ system {
+ ...System
+ }
+ }
+ ... on HotelPage {
+ web {
+ breadcrumbs {
+ title
+ }
+ }
+ system {
+ ...System
+ }
+ }
+ ... on LoyaltyPage {
+ web {
+ breadcrumbs {
+ title
+ }
+ }
+ system {
+ ...System
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ system {
+ ...System
+ }
+}
diff --git a/lib/graphql/Fragments/Refs/HotelPage/HotelPage.graphql b/lib/graphql/Fragments/Refs/HotelPage/HotelPage.graphql
new file mode 100644
index 000000000..d0dbdf6b3
--- /dev/null
+++ b/lib/graphql/Fragments/Refs/HotelPage/HotelPage.graphql
@@ -0,0 +1,7 @@
+#import "../System.graphql"
+
+fragment HotelPageRef on HotelPage {
+ system {
+ ...System
+ }
+}
diff --git a/lib/graphql/Query/Header.graphql b/lib/graphql/Query/Header.graphql
index 32a9d686d..1e3fddd01 100644
--- a/lib/graphql/Query/Header.graphql
+++ b/lib/graphql/Query/Header.graphql
@@ -1,11 +1,17 @@
#import "../Fragments/Refs/System.graphql"
-#import "../Fragments/Header/InternalOrExternalLink.graphql"
-#import "../Fragments/PageLink/ContentPageLink.graphql"
-#import "../Fragments/PageLink/LoyaltyPageLink.graphql"
#import "../Fragments/PageLink/AccountPageLink.graphql"
+#import "../Fragments/PageLink/ContentPageLink.graphql"
+#import "../Fragments/PageLink/HotelPageLink.graphql"
+#import "../Fragments/PageLink/LoyaltyPageLink.graphql"
#import "../Fragments/Blocks/Card.graphql"
+#import "../Fragments/Blocks/Refs/Card.graphql"
+#import "../Fragments/Refs/ContentPage/ContentPage.graphql"
+#import "../Fragments/Refs/HotelPage/HotelPage.graphql"
+#import "../Fragments/Refs/LoyaltyPage/LoyaltyPage.graphql"
+#import "../Fragments/Refs/MyPages/AccountPage.graphql"
+
query GetHeader($locale: String!) {
all_header(limit: 1, locale: $locale) {
items {
@@ -15,6 +21,7 @@ query GetHeader($locale: String!) {
edges {
node {
...ContentPageLink
+ ...HotelPageLink
...LoyaltyPageLink
}
}
@@ -26,6 +33,7 @@ query GetHeader($locale: String!) {
edges {
node {
...ContentPageLink
+ ...HotelPageLink
...LoyaltyPageLink
}
}
@@ -36,6 +44,7 @@ query GetHeader($locale: String!) {
edges {
node {
...ContentPageLink
+ ...HotelPageLink
...LoyaltyPageLink
}
}
@@ -49,6 +58,7 @@ query GetHeader($locale: String!) {
edges {
node {
...ContentPageLink
+ ...HotelPageLink
...LoyaltyPageLink
}
}
@@ -70,6 +80,59 @@ query GetHeader($locale: String!) {
query GetHeaderRef($locale: String!) {
all_header(limit: 1, locale: $locale) {
items {
+ top_link {
+ linkConnection {
+ edges {
+ node {
+ ...ContentPageRef
+ ...HotelPageRef
+ ...LoyaltyPageRef
+ }
+ }
+ }
+ }
+ menu_items {
+ linkConnection {
+ edges {
+ node {
+ ...ContentPageRef
+ ...HotelPageRef
+ ...LoyaltyPageRef
+ }
+ }
+ }
+ see_all_link {
+ linkConnection {
+ edges {
+ node {
+ ...ContentPageRef
+ ...HotelPageRef
+ ...LoyaltyPageRef
+ }
+ }
+ }
+ }
+ submenu {
+ links {
+ linkConnection {
+ edges {
+ node {
+ ...ContentPageRef
+ ...HotelPageRef
+ ...LoyaltyPageRef
+ }
+ }
+ }
+ }
+ }
+ cardConnection {
+ edges {
+ node {
+ ...CardBlockRef
+ }
+ }
+ }
+ }
system {
...System
}
diff --git a/server/routers/contentstack/base/output.ts b/server/routers/contentstack/base/output.ts
index 62a68238c..983a024ed 100644
--- a/server/routers/contentstack/base/output.ts
+++ b/server/routers/contentstack/base/output.ts
@@ -268,24 +268,26 @@ export type FooterRefDataRaw = z.infer
const linkConnectionNodeSchema = z
.object({
- edges: z.array(
- z.object({
- node: z.object({
- system: z.object({
- uid: z.string(),
- locale: z.nativeEnum(Lang),
+ edges: z
+ .array(
+ z.object({
+ node: z.object({
+ system: z.object({
+ uid: z.string(),
+ locale: z.nativeEnum(Lang),
+ }),
+ url: z.string(),
+ title: z.string(),
+ web: z.object({
+ original_url: z.string(),
+ }),
}),
- url: z.string(),
- title: z.string(),
- web: z.object({
- original_url: z.string().nullable().optional(),
- }),
- }),
- })
- ),
+ })
+ )
+ .max(1),
})
- .transform((rawData) => {
- const node = rawData.edges[0]?.node
+ .transform((data) => {
+ const node = data.edges[0]?.node
if (!node) {
return null
}
@@ -301,15 +303,14 @@ const linkConnectionNodeSchema = z
const linkWithTitleSchema = z
.object({
- title: z.string().nullable(),
+ title: z.string(),
linkConnection: linkConnectionNodeSchema,
})
.transform((rawData) => {
return rawData.linkConnection && rawData.title
? {
+ ...rawData.linkConnection,
title: rawData.title,
- href: rawData.linkConnection.href,
- isExternal: rawData.linkConnection.isExternal,
}
: null
})
@@ -325,21 +326,18 @@ const cardButtonSchema = z
linkConnection: linkConnectionNodeSchema,
open_in_new_tab: z.boolean(),
})
- .transform((rawData) => {
- if (!rawData) {
- return null
- }
- const linkConnectionData = rawData.linkConnection
- const isContentstackLink = rawData.is_contentstack_link
- const externalLink = rawData.external_link
+ .transform((data) => {
+ const linkConnectionData = data.linkConnection
+ const isContentstackLink = data.is_contentstack_link
+ const externalLink = data.external_link
const href =
isContentstackLink && externalLink.href
? externalLink.href
: linkConnectionData?.href || ""
return {
- openInNewTab: rawData.open_in_new_tab,
- title: rawData.cta_text,
+ openInNewTab: data.open_in_new_tab,
+ title: data.cta_text,
href,
isExternal: !isContentstackLink || linkConnectionData?.isExternal,
}
@@ -347,23 +345,25 @@ const cardButtonSchema = z
const cardConnectionSchema = z
.object({
- edges: z.array(
- z.object({
- node: z.object({
- heading: z.string(),
- body_text: z.string(),
- background_image: imageVaultAssetTransformedSchema,
- has_primary_button: z.boolean(),
- has_secondary_button: z.boolean(),
- scripted_top_title: z.string(),
- primary_button: cardButtonSchema,
- secondary_button: cardButtonSchema,
- }),
- })
- ),
+ edges: z
+ .array(
+ z.object({
+ node: z.object({
+ heading: z.string(),
+ body_text: z.string(),
+ background_image: imageVaultAssetTransformedSchema,
+ has_primary_button: z.boolean(),
+ has_secondary_button: z.boolean(),
+ scripted_top_title: z.string(),
+ primary_button: cardButtonSchema.nullable(),
+ secondary_button: cardButtonSchema.nullable(),
+ }),
+ })
+ )
+ .max(1),
})
- .transform((rawData) => {
- const node = rawData.edges[0]?.node
+ .transform((data) => {
+ const node = data.edges[0]?.node
if (!node) {
return null
}
@@ -391,31 +391,33 @@ export const menuItemSchema = z
see_all_link: linkWithTitleSchema,
cardConnection: cardConnectionSchema,
})
- .transform(
- ({ submenu, linkConnection, cardConnection, see_all_link, title }) => {
- return {
- title,
- link: submenu.length ? null : linkConnection,
- seeAllLink: submenu.length ? see_all_link : null,
- submenu,
- card: cardConnection,
- }
+ .transform((data) => {
+ const { submenu, linkConnection, cardConnection, see_all_link, title } =
+ data
+ return {
+ title,
+ link: submenu.length ? null : linkConnection,
+ seeAllLink: submenu.length ? see_all_link : null,
+ submenu,
+ card: cardConnection,
}
- )
+ })
export const getHeaderSchema = z
.object({
all_header: z.object({
- items: z.array(
- z.object({
- top_link: linkWithTitleSchema.nullable(),
- menu_items: z.array(menuItemSchema).nullable(),
- })
- ),
+ items: z
+ .array(
+ z.object({
+ top_link: linkWithTitleSchema,
+ menu_items: z.array(menuItemSchema),
+ })
+ )
+ .length(1),
}),
})
- .transform((rawData) => {
- const { top_link, menu_items } = rawData.all_header.items[0]
+ .transform((data) => {
+ const { top_link, menu_items } = data.all_header.items[0]
return {
topLink: top_link,
@@ -423,15 +425,78 @@ export const getHeaderSchema = z
}
})
-export const getHeaderRefSchema = z.object({
- all_header: z.object({
- items: z.array(
+const linkConnectionRefs = z.object({
+ edges: z
+ .array(
z.object({
- system: z.object({
- content_type_uid: z.string(),
- uid: z.string(),
+ node: z.object({
+ system: z.object({
+ content_type_uid: z.string(),
+ uid: z.string(),
+ }),
}),
})
- ),
+ )
+ .max(1),
+})
+
+const cardConnectionRefs = z.object({
+ primary_button: z
+ .object({
+ linkConnection: linkConnectionRefs,
+ })
+ .nullable(),
+ secondary_button: z
+ .object({
+ linkConnection: linkConnectionRefs,
+ })
+ .nullable(),
+ system: z.object({
+ content_type_uid: z.string(),
+ uid: z.string(),
+ }),
+})
+
+export const getHeaderRefSchema = z.object({
+ all_header: z.object({
+ items: z
+ .array(
+ z.object({
+ top_link: z
+ .object({
+ linkConnection: linkConnectionRefs,
+ })
+ .nullable(),
+ menu_items: z.array(
+ z.object({
+ linkConnection: linkConnectionRefs,
+ see_all_link: z.object({
+ linkConnection: linkConnectionRefs,
+ }),
+ cardConnection: z.object({
+ edges: z
+ .array(
+ z.object({
+ node: cardConnectionRefs,
+ })
+ )
+ .max(1),
+ }),
+ submenu: z.array(
+ z.object({
+ links: z.array(
+ z.object({ linkConnection: linkConnectionRefs })
+ ),
+ })
+ ),
+ })
+ ),
+ system: z.object({
+ content_type_uid: z.string(),
+ uid: z.string(),
+ }),
+ })
+ )
+ .length(1),
}),
})
diff --git a/server/routers/contentstack/base/query.ts b/server/routers/contentstack/base/query.ts
index fa99e3667..51cfb09ba 100644
--- a/server/routers/contentstack/base/query.ts
+++ b/server/routers/contentstack/base/query.ts
@@ -14,7 +14,11 @@ import { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc"
import { contentstackBaseProcedure, router } from "@/server/trpc"
-import { generateRefsResponseTag, generateTag } from "@/utils/generateTag"
+import {
+ generateRefsResponseTag,
+ generateTag,
+ generateTags,
+} from "@/utils/generateTag"
import { langInput } from "./input"
import {
@@ -24,11 +28,13 @@ import {
CurrentHeaderRefDataRaw,
FooterDataRaw,
FooterRefDataRaw,
+ getHeaderRefSchema,
getHeaderSchema,
validateContactConfigSchema,
validateCurrentHeaderConfigSchema,
validateFooterConfigSchema,
} from "./output"
+import { getConnections } from "./utils"
import { HeaderRefResponse, HeaderResponse } from "@/types/header"
@@ -64,13 +70,13 @@ const getCurrentHeaderFailCounter = meter.createCounter(
)
// OpenTelemetry metrics: Header
-const getHeaderRefCounter = meter.createCounter(
+const getHeaderRefsCounter = meter.createCounter(
"trpc.contentstack.header.ref.get"
)
-const getHeaderRefSuccessCounter = meter.createCounter(
+const getHeaderRefsSuccessCounter = meter.createCounter(
"trpc.contentstack.header.ref.get-success"
)
-const getHeaderRefFailCounter = meter.createCounter(
+const getHeaderRefsFailCounter = meter.createCounter(
"trpc.contentstack.header.ref.get-fail"
)
const getHeaderCounter = meter.createCounter("trpc.contentstack.header.get")
@@ -164,31 +170,90 @@ export const baseQueryRouter = router({
}),
header: contentstackBaseProcedure.query(async ({ ctx }) => {
const { lang } = ctx
- getHeaderRefCounter.add(1, { lang })
+ getHeaderRefsCounter.add(1, { lang })
console.info(
- "contentstack.header.ref start",
+ "contentstack.header.refs start",
JSON.stringify({ query: { lang } })
)
- // TODO: Add better ref types and error handling for responseRef
- const responseRef = await request(GetHeaderRef, {
- locale: lang,
- })
+ const responseRef = await request(
+ GetHeaderRef,
+ {
+ locale: lang,
+ },
+ {
+ cache: "force-cache",
+ next: {
+ tags: [generateRefsResponseTag(lang, "header")],
+ },
+ }
+ )
- getCurrentHeaderCounter.add(1, { lang })
+ if (!responseRef.data) {
+ const notFoundError = notFound(responseRef)
+ getHeaderRefsFailCounter.add(1, {
+ lang,
+ error_type: "not_found",
+ error: JSON.stringify({ code: notFoundError.code }),
+ })
+ console.error(
+ "contentstack.header.refs not found error",
+ JSON.stringify({
+ query: {
+ lang,
+ },
+ error: { code: notFoundError.code },
+ })
+ )
+ throw notFoundError
+ }
+
+ const validatedHeaderRefs = getHeaderRefSchema.safeParse(responseRef.data)
+
+ if (!validatedHeaderRefs.success) {
+ getHeaderRefsFailCounter.add(1, {
+ lang,
+ error_type: "validation_error",
+ error: JSON.stringify(validatedHeaderRefs.error),
+ })
+ console.error(
+ "contentstack.header.refs validation error",
+ JSON.stringify({
+ query: {
+ lang,
+ },
+ error: validatedHeaderRefs.error,
+ })
+ )
+ return null
+ }
+
+ getHeaderRefsSuccessCounter.add(1, { lang })
+ console.info(
+ "contentstack.header.refs success",
+ JSON.stringify({ query: { lang } })
+ )
+
+ const connections = getConnections(validatedHeaderRefs.data)
+
+ getHeaderCounter.add(1, { lang })
console.info(
"contentstack.header start",
JSON.stringify({ query: { lang } })
)
+ const tags = [
+ generateTags(lang, connections),
+ generateTag(
+ lang,
+ validatedHeaderRefs.data.all_header.items[0].system.uid
+ ),
+ ].flat()
+
const response = await request(
GetHeader,
- { locale: lang }
- // {
- // tags: [
- // generateTag(lang, responseRef.data.all_header.items[0].system.uid),
- // ],
- // }
+ { locale: lang },
+ { cache: "force-cache", next: { tags } }
)
if (!response.data) {
@@ -422,161 +487,3 @@ export const baseQueryRouter = router({
return validatedFooterConfig.data.all_current_footer.items[0]
}),
})
-
-const json = {
- query: { lang: "en" },
- error: {
- issues: [
- {
- code: "invalid_type",
- expected: "object",
- received: "undefined",
- path: ["all_header", "items", 0, "top_link", "link"],
- message: "Required",
- },
- {
- code: "invalid_type",
- expected: "object",
- received: "undefined",
- path: ["all_header", "items", 0, "menu_items", 0, "link"],
- message: "Required",
- },
- {
- code: "invalid_type",
- expected: "object",
- received: "undefined",
- path: [
- "all_header",
- "items",
- 0,
- "menu_items",
- 0,
- "submenu",
- 0,
- "links",
- 0,
- "link",
- ],
- message: "Required",
- },
- {
- code: "invalid_type",
- expected: "object",
- received: "undefined",
- path: [
- "all_header",
- "items",
- 0,
- "menu_items",
- 0,
- "submenu",
- 0,
- "links",
- 1,
- "link",
- ],
- message: "Required",
- },
- {
- code: "invalid_type",
- expected: "object",
- received: "undefined",
- path: [
- "all_header",
- "items",
- 0,
- "menu_items",
- 0,
- "submenu",
- 1,
- "links",
- 0,
- "link",
- ],
- message: "Required",
- },
- {
- code: "invalid_type",
- expected: "object",
- received: "undefined",
- path: [
- "all_header",
- "items",
- 0,
- "menu_items",
- 0,
- "see_all_link",
- "link",
- ],
- message: "Required",
- },
- {
- code: "invalid_type",
- expected: "object",
- received: "undefined",
- path: ["all_header", "items", 0, "menu_items", 1, "link"],
- message: "Required",
- },
- {
- code: "invalid_type",
- expected: "object",
- received: "undefined",
- path: [
- "all_header",
- "items",
- 0,
- "menu_items",
- 1,
- "see_all_link",
- "link",
- ],
- message: "Required",
- },
- {
- code: "invalid_type",
- expected: "object",
- received: "undefined",
- path: ["all_header", "items", 0, "menu_items", 2, "link"],
- message: "Required",
- },
- {
- code: "invalid_type",
- expected: "object",
- received: "undefined",
- path: [
- "all_header",
- "items",
- 0,
- "menu_items",
- 2,
- "see_all_link",
- "link",
- ],
- message: "Required",
- },
- {
- code: "invalid_type",
- expected: "object",
- received: "undefined",
- path: ["all_header", "items", 0, "menu_items", 3, "link"],
- message: "Required",
- },
- {
- code: "invalid_type",
- expected: "object",
- received: "undefined",
- path: [
- "all_header",
- "items",
- 0,
- "menu_items",
- 3,
- "see_all_link",
- "link",
- ],
- message: "Required",
- },
- ],
- name: "ZodError",
- },
-}
diff --git a/server/routers/contentstack/base/utils.ts b/server/routers/contentstack/base/utils.ts
new file mode 100644
index 000000000..dfdacfb66
--- /dev/null
+++ b/server/routers/contentstack/base/utils.ts
@@ -0,0 +1,40 @@
+import { HeaderRefResponse } from "@/types/header"
+import { Edges } from "@/types/requests/utils/edges"
+import { NodeRefs } from "@/types/requests/utils/refs"
+
+export function getConnections(refs: HeaderRefResponse) {
+ const connections: Edges[] = []
+ const headerData = refs.all_header.items[0]
+ const topLink = headerData.top_link
+ if (topLink) {
+ connections.push(topLink.linkConnection)
+ }
+
+ headerData.menu_items.forEach(
+ ({ linkConnection, see_all_link, cardConnection, submenu }) => {
+ const card = cardConnection.edges[0]?.node
+ connections.push(linkConnection)
+
+ if (see_all_link) {
+ connections.push(see_all_link.linkConnection)
+ }
+
+ if (card) {
+ if (card.primary_button) {
+ connections.push(card.primary_button.linkConnection)
+ }
+ if (card.secondary_button) {
+ connections.push(card.secondary_button.linkConnection)
+ }
+ }
+
+ submenu.forEach(({ links }) => {
+ links.forEach(({ linkConnection }) => {
+ connections.push(linkConnection)
+ })
+ })
+ }
+ )
+
+ return connections
+}