Merged in LOY-188-employee-benefit-call-to-actions (pull request #1954)

feat(LOY-188): dynamic content support in content pages headers & use in DTMC employee benefits page

* feat(LOY-188): add dynamic content handling for DTMC employee benefits page header

* fix(LOY-188): change section to div in EmployeeBenefitsCallToActions component

* refactor(LOY-188): switch to ButtonLink

* refactor(LOY-188): replace enum with as const objects in DynamicContentEnum

* chore(LOY-188): change ComponentValue type exports to internal scope in DynamicContentEnum

* fix(EmployeeBenefitsCallToActions): replace div with fragment

* chore(LOY-188): update translations


Approved-by: Christian Andolf
This commit is contained in:
Chuma Mcphoy (We Ahead)
2025-05-08 15:24:22 +00:00
parent 494f44f216
commit 7af4d3be1e
12 changed files with 217 additions and 51 deletions

View File

@@ -2,6 +2,7 @@ import Link from "next/link"
import { Suspense } from "react" import { Suspense } from "react"
import Blocks from "@/components/Blocks" import Blocks from "@/components/Blocks"
import HeaderDynamicContent from "@/components/Headers/DynamicContent"
import Hero from "@/components/Hero" import Hero from "@/components/Hero"
import MeetingPackageWidget from "@/components/MeetingPackageWidget" import MeetingPackageWidget from "@/components/MeetingPackageWidget"
import Sidebar from "@/components/Sidebar" import Sidebar from "@/components/Sidebar"
@@ -52,6 +53,9 @@ export default async function StaticPage({
{header.navigation_links ? ( {header.navigation_links ? (
<LinkChips chips={header.navigation_links} /> <LinkChips chips={header.navigation_links} />
) : null} ) : null}
{"dynamic_content" in header && header.dynamic_content ? (
<HeaderDynamicContent {...header.dynamic_content} />
) : null}
</> </>
) : null} ) : null}
</div> </div>

View File

@@ -0,0 +1,7 @@
.container {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--Spacing-x-one-and-half);
padding-bottom: var(--Spacing-x3);
}

View File

@@ -0,0 +1,71 @@
import React from "react"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { login } from "@/constants/routes/handleAuth"
import { signup } from "@/constants/routes/signup"
import { auth } from "@/auth"
import ButtonLink from "@/components/ButtonLink"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import { isValidSession } from "@/utils/session"
import styles from "./callToActions.module.css"
export default async function EmployeeBenefitsCallToActions() {
const session = await auth()
const intl = await getIntl()
const lang = getLang()
if (!isValidSession(session)) {
return (
<>
<div className={styles.container}>
<Typography variant="Body/Paragraph/mdBold">
<p>
{intl.formatMessage({
defaultMessage: "Already a Scandic Friends account?",
})}
</p>
</Typography>
<ButtonLink href={login[lang]} size="Medium" variant="Tertiary">
{intl.formatMessage({
defaultMessage: "Log in and link employment",
})}
</ButtonLink>
</div>
<div className={styles.container}>
<Typography variant="Body/Paragraph/mdBold">
<p>
{intl.formatMessage({
defaultMessage: "Don't have a Scandic Friends account yet?",
})}
</p>
</Typography>
<ButtonLink href={signup[lang]} size="Medium" variant="Secondary">
{intl.formatMessage({
defaultMessage: "Sign up and link employment",
})}
</ButtonLink>
</div>
</>
)
}
// -- TODO [LOY-196] --
// Handle case of authed user and already connected work account.
// Show member card modal.
return (
<div className={styles.container}>
{/*/ TODO [LOY-229]: Update href once we have new auth url. */}
<ButtonLink href="#" size="Medium" variant="Tertiary" color="Primary">
{intl.formatMessage({
defaultMessage: "Link Employment",
})}
</ButtonLink>
</div>
)
}

View File

@@ -0,0 +1,27 @@
import { Suspense } from "react"
import EmployeeBenefitsCallToActions from "@/components/DigitalTeamMemberCard/EmployeeBenefits/CallToActions"
import LoadingSpinner from "@/components/LoadingSpinner"
import type { HeaderDynamicContentProps } from "@/types/components/headers/dynamicContent"
import { DynamicContentEnum } from "@/types/enums/dynamicContent"
export default function HeaderDynamicContent(props: HeaderDynamicContentProps) {
return (
<Suspense fallback={<LoadingSpinner />}>
<HeaderDynamicContentComponent {...props} />
</Suspense>
)
}
function HeaderDynamicContentComponent(props: HeaderDynamicContentProps) {
const { component } = props
switch (component) {
case DynamicContentEnum.Headers.components
.dtmc_employee_benefits_call_to_actions:
return <EmployeeBenefitsCallToActions />
default:
return null
}
}

View File

@@ -1,9 +1,9 @@
import EmployeeBenefitsAuthCard from "@/components/DigitalTeamMemberCard/EmployeeBenefits/AuthCard"
import JsonToHtml from "@/components/JsonToHtml" import JsonToHtml from "@/components/JsonToHtml"
import ShortcutsList from "../Blocks/ShortcutsList" import ShortcutsList from "../Blocks/ShortcutsList"
import Card from "../TempDesignSystem/Card" import Card from "../TempDesignSystem/Card"
import TeaserCard from "../TempDesignSystem/TeaserCard" import TeaserCard from "../TempDesignSystem/TeaserCard"
import EmployeeBenefitsAuthCard from "./EmployeeBenefits/AuthCard"
import JoinLoyaltyContact from "./JoinLoyalty" import JoinLoyaltyContact from "./JoinLoyalty"
import MyPagesNavigation from "./MyPagesNavigation" import MyPagesNavigation from "./MyPagesNavigation"

View File

@@ -26,6 +26,9 @@ query GetContentPage($locale: String!, $uid: String!) {
header { header {
heading heading
preamble preamble
dynamic_content {
component
}
...TopPrimaryButton_ContentPage ...TopPrimaryButton_ContentPage
...NavigationLinks_ContentPage ...NavigationLinks_ContentPage
} }
@@ -84,6 +87,9 @@ query GetContentPageBlocksBatch2($locale: String!, $uid: String!) {
query GetContentPageRefs($locale: String!, $uid: String!) { query GetContentPageRefs($locale: String!, $uid: String!) {
content_page(locale: $locale, uid: $uid) { content_page(locale: $locale, uid: $uid) {
header { header {
dynamic_content {
component
}
...NavigationLinksRef_ContentPage ...NavigationLinksRef_ContentPage
...TopPrimaryButtonRef_ContentPage ...TopPrimaryButtonRef_ContentPage
} }

View File

@@ -26,6 +26,10 @@ import {
import { tableSchema } from "../schemas/blocks/table" import { tableSchema } from "../schemas/blocks/table"
import { textColsRefsSchema, textColsSchema } from "../schemas/blocks/textCols" import { textColsRefsSchema, textColsSchema } from "../schemas/blocks/textCols"
import { uspGridRefsSchema, uspGridSchema } from "../schemas/blocks/uspGrid" import { uspGridRefsSchema, uspGridSchema } from "../schemas/blocks/uspGrid"
import {
dynamicContentRefsSchema as headerDynamicContentRefsSchema,
dynamicContentSchema as headerDynamicContentSchema,
} from "../schemas/headers/dynamicContent"
import { tempImageVaultAssetSchema } from "../schemas/imageVault" import { tempImageVaultAssetSchema } from "../schemas/imageVault"
import { import {
linkAndTitleSchema, linkAndTitleSchema,
@@ -210,6 +214,7 @@ export const contentPageSchema = z.object({
preamble: z.string(), preamble: z.string(),
top_primary_button: topPrimaryButtonSchema, top_primary_button: topPrimaryButtonSchema,
navigation_links: navigationLinksSchema, navigation_links: navigationLinksSchema,
dynamic_content: headerDynamicContentSchema.optional(),
}), }),
meeting_package: z meeting_package: z
.object({ .object({
@@ -325,6 +330,7 @@ const contentPageSidebarRefsItem = z.discriminatedUnion("__typename", [
const contentPageHeaderRefs = z.object({ const contentPageHeaderRefs = z.object({
navigation_links: z.array(linkConnectionRefs), navigation_links: z.array(linkConnectionRefs),
top_primary_button: linkConnectionRefs.nullable(), top_primary_button: linkConnectionRefs.nullable(),
dynamic_content: headerDynamicContentRefsSchema.optional(),
}) })
export const contentPageRefsSchema = z.object({ export const contentPageRefsSchema = z.object({

View File

@@ -0,0 +1,11 @@
import { z } from "zod"
import { DynamicContentEnum } from "@/types/enums/dynamicContent"
export const dynamicContentSchema = z.object({
component: z.enum(DynamicContentEnum.Headers.enums),
})
export const dynamicContentRefsSchema = z.object({
component: z.enum(DynamicContentEnum.Headers.enums),
})

View File

@@ -0,0 +1,5 @@
import type { z } from "zod"
import type { dynamicContentSchema } from "@/server/routers/contentstack/schemas/headers/dynamicContent"
export type HeaderDynamicContentProps = z.infer<typeof dynamicContentSchema>

View File

@@ -1,61 +1,90 @@
// Helper type to get a union of an object's string literal values
type ValueOf<T> = T[keyof T]
export namespace DynamicContentEnum { export namespace DynamicContentEnum {
export namespace Blocks { export namespace Blocks {
export const enum components { export const components = {
current_benefits = "current_benefits", current_benefits: "current_benefits",
earn_and_burn = "earn_and_burn", earn_and_burn: "earn_and_burn",
expiring_points = "expiring_points", expiring_points: "expiring_points",
how_it_works = "how_it_works", how_it_works: "how_it_works",
jobylon_feed = "jobylon_feed", jobylon_feed: "jobylon_feed",
loyalty_levels = "loyalty_levels", loyalty_levels: "loyalty_levels",
membership_overview = "membership_overview", membership_overview: "membership_overview",
my_points = "my_points", my_points: "my_points",
next_benefits = "next_benefits", next_benefits: "next_benefits",
overview_table = "overview_table", overview_table: "overview_table",
points_overview = "points_overview", points_overview: "points_overview",
previous_stays = "previous_stays", previous_stays: "previous_stays",
sign_up_form = "sign_up_form", sign_up_form: "sign_up_form",
sign_up_verification = "sign_up_verification", sign_up_verification: "sign_up_verification",
soonest_stays = "soonest_stays", soonest_stays: "soonest_stays",
upcoming_stays = "upcoming_stays", upcoming_stays: "upcoming_stays",
sas_linked_account = "sas_linked_account", sas_linked_account: "sas_linked_account",
sas_transfer_points = "sas_transfer_points", sas_transfer_points: "sas_transfer_points",
sas_tier_comparison = "sas_tier_comparison", sas_tier_comparison: "sas_tier_comparison",
} } as const
/** Type needed to satisfy zod enum type */ /**
export const enums: [components, ...components[]] = [ * Represents one of the possible component string literal values from Blocks.components.
components.current_benefits, * e.g., "current_benefits" | "earn_and_burn" | ...
components.earn_and_burn, */
components.expiring_points, type ComponentValue = ValueOf<typeof components>
components.how_it_works,
components.jobylon_feed, /**
components.loyalty_levels, * Array of all component string literal values from Blocks.components.
components.membership_overview, * Typed as a non-empty tuple `[ComponentValue, ...ComponentValue[]]`,
components.my_points, * to ensure it meets Zod's z.enum() requirements.
components.next_benefits, */
components.overview_table, export const enums = Object.values(components) as [
components.points_overview, ComponentValue,
components.previous_stays, ...ComponentValue[],
components.sign_up_form, ]
components.sign_up_verification, }
components.soonest_stays,
components.upcoming_stays, export namespace Headers {
components.sas_linked_account, export const components = {
components.sas_transfer_points, dtmc_employee_benefits_call_to_actions:
components.sas_tier_comparison, "dtmc_employee_benefits_call_to_actions",
} as const
/**
* Represents one of the possible component string literal values from Headers.components.
* e.g., "dtmc_employee_benefits_call_to_actions"
*/
type ComponentValue = ValueOf<typeof components>
/**
* Array of all component string literal values from Headers.components.
* Typed as a non-empty tuple `[ComponentValue, ...ComponentValue[]]`,
* to ensure it meets Zod's z.enum() requirements.
*/
export const enums = Object.values(components) as [
ComponentValue,
...ComponentValue[],
] ]
} }
export namespace Sidebar { export namespace Sidebar {
export const enum components { export const components = {
my_pages_navigation = "my_pages_navigation", my_pages_navigation: "my_pages_navigation",
employee_benefits_auth_card = "employee_benefits_auth_card", employee_benefits_auth_card: "employee_benefits_auth_card",
} } as const
/** Type needed to satisfy zod enum type */ /**
export const enums: [string, ...string[]] = [ * Represents one of the possible component string literal values from Sidebar.components.
components.my_pages_navigation, * e.g., "my_pages_navigation" | "employee_benefits_auth_card"
components.employee_benefits_auth_card, */
type ComponentValue = ValueOf<typeof components>
/**
* Array of all component string literal values from Sidebar.components.
* Typed as a non-empty tuple `[ComponentValue, ...ComponentValue[]]`,
* to ensure it meets Zod's z.enum() requirements.
*/
export const enums = Object.values(components) as [
ComponentValue,
...ComponentValue[],
] ]
} }
} }