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}
>
<Typography variant="Body/Paragraph/mdBold">
<span>{shortcut.text || shortcut.title}</span>
<span>{shortcut.text}</span>
</Typography>
<MaterialIcon color="CurrentColor" icon="arrow_forward" />
</Link>

View File

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

View File

@@ -42,52 +42,58 @@ export default function MenuItem({ item, isMobile }: NavigationMenuItemProps) {
}
}
return submenu.length ? (
<>
<MainMenuButton
onClick={() => toggleMegaMenu(megaMenuTitle)}
if (submenu.length) {
return (
<>
<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 : ""}`}
variant="navigation"
weight="bold"
onClick={handleNavigate}
href={link.url}
>
{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>
</>
) : (
<Link
className={`${styles.navigationMenuItem} ${isMobile ? styles.mobile : ""}`}
variant="navigation"
weight="bold"
onClick={handleNavigate}
href={link!.url}
>
{title}
</Link>
)
</Link>
)
}
return null
}

View File

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

View File

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

View File

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

View File

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

View File

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