feat/SW-3108 external links

* feat(SW-3108): Added external link options to shortcuts
* feat(SW-3108): Added external link options to header

Approved-by: Matilda Landström
This commit is contained in:
Erik Tiekstra
2025-08-28 07:25:17 +00:00
parent 1a10afdbad
commit fd48f86c90
8 changed files with 213 additions and 98 deletions

View File

@@ -24,7 +24,7 @@ export default function ShortcutsListItems({
className={styles.link} className={styles.link}
> >
<Typography variant="Body/Paragraph/mdBold"> <Typography variant="Body/Paragraph/mdBold">
<span>{shortcut.text || shortcut.title}</span> <span>{shortcut.text}</span>
</Typography> </Typography>
<MaterialIcon color="CurrentColor" icon="arrow_forward" /> <MaterialIcon color="CurrentColor" icon="arrow_forward" />
</Link> </Link>

View File

@@ -56,9 +56,9 @@ export default function MegaMenu({
) : null} ) : null}
<div className={styles.megaMenuContent}> <div className={styles.megaMenuContent}>
<div className={styles.seeAllLink}> <div className={styles.seeAllLink}>
{seeAllLink?.link ? ( {seeAllLink?.url ? (
<Link <Link
href={seeAllLink.link.url} href={seeAllLink.url}
variant="icon" variant="icon"
weight="bold" weight="bold"
onClick={handleNavigate} onClick={handleNavigate}
@@ -75,11 +75,11 @@ export default function MegaMenu({
<span className={styles.submenuTitle}>{item.title}</span> <span className={styles.submenuTitle}>{item.title}</span>
</Typography> </Typography>
<ul className={styles.submenu}> <ul className={styles.submenu}>
{item.links.map(({ title, link }) => {item.links.map(({ title, url }) =>
link ? ( url ? (
<li key={title} className={styles.submenuItem}> <li key={title} className={styles.submenuItem}>
<Link <Link
href={link.url} href={url}
variant="menu" variant="menu"
className={styles.link} className={styles.link}
onClick={handleNavigate} onClick={handleNavigate}

View File

@@ -42,52 +42,58 @@ export default function MenuItem({ item, isMobile }: NavigationMenuItemProps) {
} }
} }
return submenu.length ? ( if (submenu.length) {
<> return (
<MainMenuButton <>
onClick={() => toggleMegaMenu(megaMenuTitle)} <MainMenuButton
onClick={() => toggleMegaMenu(megaMenuTitle)}
className={`${styles.navigationMenuItem} ${isMobile ? styles.mobile : ""}`}
>
{title}
{isMobile ? (
<MaterialIcon
icon="arrow_forward_ios"
size={20}
className={`${styles.chevron}`}
color="Icon/Interactive/Accent"
/>
) : (
<MaterialIcon
icon="keyboard_arrow_down"
size={20}
className={`${styles.chevron} ${isMegaMenuOpen ? styles.isExpanded : ""}`}
color="Icon/Interactive/Accent"
/>
)}
</MainMenuButton>
<div
ref={megaMenuRef}
className={`${styles.dropdown} ${isMegaMenuOpen ? styles.isExpanded : ""}`}
>
<MegaMenu
isMobile={isMobile}
title={title}
seeAllLink={seeAllLink}
submenu={submenu}
card={card}
isOpen={isMegaMenuOpen}
/>
</div>
</>
)
} else if (link?.url) {
return (
<Link
className={`${styles.navigationMenuItem} ${isMobile ? styles.mobile : ""}`} className={`${styles.navigationMenuItem} ${isMobile ? styles.mobile : ""}`}
variant="navigation"
weight="bold"
onClick={handleNavigate}
href={link.url}
> >
{title} {title}
{isMobile ? ( </Link>
<MaterialIcon )
icon="arrow_forward_ios" }
size={20}
className={`${styles.chevron}`} return null
color="Icon/Interactive/Accent"
/>
) : (
<MaterialIcon
icon="keyboard_arrow_down"
size={20}
className={`${styles.chevron} ${isMegaMenuOpen ? styles.isExpanded : ""}`}
color="Icon/Interactive/Accent"
/>
)}
</MainMenuButton>
<div
ref={megaMenuRef}
className={`${styles.dropdown} ${isMegaMenuOpen ? styles.isExpanded : ""}`}
>
<MegaMenu
isMobile={isMobile}
title={title}
seeAllLink={seeAllLink}
submenu={submenu}
card={card}
isOpen={isMegaMenuOpen}
/>
</div>
</>
) : (
<Link
className={`${styles.navigationMenuItem} ${isMobile ? styles.mobile : ""}`}
variant="navigation"
weight="bold"
onClick={handleNavigate}
href={link!.url}
>
{title}
</Link>
)
} }

View File

@@ -11,13 +11,13 @@ export default function TopLink({
}: TopLinkProps) { }: TopLinkProps) {
const linkData = isLoggedIn ? topLink.logged_in : topLink.logged_out const linkData = isLoggedIn ? topLink.logged_in : topLink.logged_out
if (!linkData?.link?.url || !linkData?.title) { if (!linkData?.url || !linkData?.title) {
return null return null
} }
return ( return (
<HeaderLink <HeaderLink
href={linkData.link.url} href={linkData.url}
iconName={linkData.icon || IconName.Gift} iconName={linkData.icon || IconName.Gift}
iconSize={iconSize} iconSize={iconSize}
> >

View File

@@ -27,6 +27,11 @@ fragment Shortcuts on Shortcuts {
title title
two_column_list two_column_list
shortcuts { shortcuts {
is_contentstack_link
external_link {
href
title
}
open_in_new_tab open_in_new_tab
text text
linkConnection { linkConnection {

View File

@@ -33,6 +33,7 @@ query GetHeader($locale: String!) {
top_link { top_link {
logged_in { logged_in {
icon icon
is_contentstack_link
title title
linkConnection { linkConnection {
edges { edges {
@@ -52,9 +53,14 @@ query GetHeader($locale: String!) {
} }
} }
} }
external_link {
href
title
}
} }
logged_out { logged_out {
icon icon
is_contentstack_link
title title
linkConnection { linkConnection {
edges { edges {
@@ -74,9 +80,14 @@ query GetHeader($locale: String!) {
} }
} }
} }
external_link {
href
title
}
} }
} }
menu_items { menu_items {
is_contentstack_link
title title
linkConnection { linkConnection {
edges { edges {
@@ -96,7 +107,12 @@ query GetHeader($locale: String!) {
} }
} }
} }
external_link {
href
title
}
see_all_link { see_all_link {
is_contentstack_link
title title
linkConnection { linkConnection {
edges { edges {
@@ -116,10 +132,15 @@ query GetHeader($locale: String!) {
} }
} }
} }
external_link {
href
title
}
} }
submenu { submenu {
title title
links { links {
is_contentstack_link
title title
linkConnection { linkConnection {
edges { edges {
@@ -139,6 +160,10 @@ query GetHeader($locale: String!) {
} }
} }
} }
external_link {
href
title
}
} }
} }
cardConnection { cardConnection {

View File

@@ -7,6 +7,10 @@ import {
import { Lang } from "@scandic-hotels/common/constants/language" import { Lang } from "@scandic-hotels/common/constants/language"
import { logger } from "@scandic-hotels/common/logger" import { logger } from "@scandic-hotels/common/logger"
import { removeMultipleSlashes } from "@scandic-hotels/common/utils/url" import { removeMultipleSlashes } from "@scandic-hotels/common/utils/url"
import {
nullableStringUrlValidator,
nullableStringValidator,
} from "@scandic-hotels/common/utils/zod/stringValidator"
import { discriminatedUnion } from "../../../utils/discriminatedUnion" import { discriminatedUnion } from "../../../utils/discriminatedUnion"
import { import {
@@ -97,7 +101,7 @@ export const validateCurrentHeaderConfigSchema = z
edges: z.array( edges: z.array(
z.object({ z.object({
node: z.object({ node: z.object({
description: z.string().optional().nullable(), description: z.string().nullish(),
dimension: z.object({ dimension: z.object({
height: z.number(), height: z.number(),
width: z.number(), width: z.number(),
@@ -106,8 +110,8 @@ export const validateCurrentHeaderConfigSchema = z
system: z.object({ system: z.object({
uid: z.string(), uid: z.string(),
}), }),
title: z.string().optional().default(""), title: nullableStringValidator,
url: z.string().optional().default(""), url: nullableStringUrlValidator,
}), }),
}) })
), ),
@@ -178,7 +182,7 @@ const validateAppDownload = z.object({
edges: z.array( edges: z.array(
z.object({ z.object({
node: z.object({ node: z.object({
description: z.string().optional().nullable(), description: z.string().nullish(),
dimension: z.object({ dimension: z.object({
height: z.number(), height: z.number(),
width: z.number(), width: z.number(),
@@ -220,7 +224,7 @@ export const validateCurrentFooterConfigSchema = z.object({
edges: z.array( edges: z.array(
z.object({ z.object({
node: z.object({ node: z.object({
description: z.string().optional().nullable(), description: z.string().nullish(),
dimension: z.object({ dimension: z.object({
height: z.number(), height: z.number(),
width: z.number(), width: z.number(),
@@ -248,7 +252,7 @@ export const validateCurrentFooterConfigSchema = z.object({
edges: z.array( edges: z.array(
z.object({ z.object({
node: z.object({ node: z.object({
description: z.string().optional().nullable(), description: z.string().nullish(),
dimension: z.object({ dimension: z.object({
height: z.number(), height: z.number(),
width: z.number(), width: z.number(),
@@ -561,6 +565,51 @@ export const headerRefsSchema = z
} }
}) })
const internalOrExternalLinkSchema = z
.object({
is_contentstack_link: z.boolean().nullish(),
external_link: z
.object({
href: nullableStringUrlValidator,
title: z.string().nullish(),
})
.nullish(),
title: nullableStringValidator,
linkConnection: z.object({
edges: z.array(
z.object({
node: linkUnionSchema.transform((data) => {
const link = transformPageLink(data)
if (link) {
return link
}
return data
}),
})
),
}),
})
.transform(
({ is_contentstack_link, external_link, linkConnection, title }) => {
if (is_contentstack_link !== false && linkConnection.edges.length) {
const linkRef = linkConnection.edges[0].node
return {
title: title || linkRef.title,
url: linkRef.url,
}
} else if (is_contentstack_link === false && external_link?.href) {
return {
title: title || external_link.title || "",
url: external_link.href,
}
} else {
return {
title,
}
}
}
)
const linkSchema = z const linkSchema = z
.object({ .object({
linkConnection: z.object({ linkConnection: z.object({
@@ -590,7 +639,7 @@ const linkSchema = z
}) })
const titleSchema = z.object({ const titleSchema = z.object({
title: z.string().optional().default(""), title: nullableStringValidator,
}) })
/** /**
@@ -605,7 +654,7 @@ const linkAndTitleSchema = z.intersection(linkSchema, titleSchema)
*/ */
export const menuItemSchema = z export const menuItemSchema = z
.intersection( .intersection(
linkAndTitleSchema, internalOrExternalLinkSchema,
z z
.object({ .object({
cardConnection: z.object({ cardConnection: z.object({
@@ -615,11 +664,11 @@ export const menuItemSchema = z
}) })
), ),
}), }),
see_all_link: linkAndTitleSchema, see_all_link: internalOrExternalLinkSchema,
submenu: z.array( submenu: z.array(
z.object({ z.object({
links: z.array(linkAndTitleSchema), links: z.array(internalOrExternalLinkSchema),
title: z.string().optional().default(""), title: nullableStringValidator,
}) })
), ),
}) })
@@ -636,11 +685,13 @@ export const menuItemSchema = z
} }
}) })
) )
.transform((data) => { .transform(({ title, url, card, seeAllLink, submenu }) => {
return { return {
...data, title,
link: data.submenu.length ? null : data.link, link: submenu.length ? null : { url },
seeAllLink: data.submenu.length ? data.seeAllLink : null, seeAllLink: submenu.length ? seeAllLink : null,
card,
submenu,
} }
}) })
@@ -652,7 +703,7 @@ enum IconName {
} }
const topLinkItemSchema = z.intersection( const topLinkItemSchema = z.intersection(
linkAndTitleSchema, internalOrExternalLinkSchema,
z.object({ z.object({
icon: z icon: z
.enum(["loyalty", "info", "offer"]) .enum(["loyalty", "info", "offer"])

View File

@@ -15,37 +15,65 @@ export const shortcutsBlockSchema = z.object({
two_column_list: z.boolean().nullable().default(false), two_column_list: z.boolean().nullable().default(false),
shortcuts: z shortcuts: z
.array( .array(
z.object({ z
open_in_new_tab: z.boolean(), .object({
text: z.string().optional().default(""), is_contentstack_link: z.boolean().nullish(),
linkConnection: z.object({ external_link: z
edges: z.array( .object({
z.object({ href: z.string().nullish().default(""),
node: linkUnionSchema.transform((data) => { title: z.string().nullish(),
const link = transformPageLink(data)
if (link) {
return link
}
return data
}),
}) })
), .nullish(),
}), open_in_new_tab: z.boolean(),
}) text: z.string().optional().default(""),
) linkConnection: z.object({
.transform((data) => { edges: z.array(
return data z.object({
.filter((node) => node.linkConnection.edges.length) node: linkUnionSchema.transform((data) => {
.map((node) => { const link = transformPageLink(data)
const link = node.linkConnection.edges[0].node if (link) {
return { return link
openInNewTab: node.open_in_new_tab, }
text: node.text, return data
title: link.title, }),
url: link.url, })
} ),
}),
}) })
}), .transform(
({
is_contentstack_link,
external_link,
linkConnection,
open_in_new_tab,
text,
}) => {
if (
is_contentstack_link !== false &&
linkConnection.edges.length
) {
const linkRef = linkConnection.edges[0].node
return {
openInNewTab: open_in_new_tab,
text: text || linkRef.title,
url: linkRef.url,
}
} else if (
is_contentstack_link === false &&
external_link?.href
) {
return {
openInNewTab: open_in_new_tab,
text: text || external_link.title || "",
url: external_link.href,
}
} else {
return null
}
}
)
)
.transform((data) => data.filter((item) => !!item)),
}) })
.transform(({ two_column_list, ...rest }) => { .transform(({ two_column_list, ...rest }) => {
return { return {