Feat/BOOK-240 hero video

Approved-by: Chuma Mcphoy (We Ahead)
Approved-by: Christel Westerberg
This commit is contained in:
Erik Tiekstra
2025-12-11 08:35:27 +00:00
parent cd8b30f2ec
commit f06e466827
33 changed files with 727 additions and 122 deletions

View File

@@ -1,19 +1,3 @@
import { Suspense } from "react"
import Breadcrumbs from "@/components/Breadcrumbs"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import type { BreadcrumbsProps } from "@/components/TempDesignSystem/Breadcrumbs/breadcrumbs"
export default function CollectionPageBreadcrumbs() {
const variants: Pick<BreadcrumbsProps, "color" | "size"> = {
color: "Surface/Secondary/Default",
size: "contentWidth",
}
return (
<Suspense fallback={<BreadcrumbsSkeleton {...variants} />}>
<Breadcrumbs {...variants} />
</Suspense>
)
return null
}

View File

@@ -1,4 +1,4 @@
import CollectionPage from "@/components/ContentType/StaticPages/CollectionPage"
import { CollectionPage } from "@/components/ContentType/CollectionPage"
import styles from "./page.module.css"

View File

@@ -4,7 +4,7 @@ import { redirect } from "next/navigation"
import { overview } from "@scandic-hotels/common/constants/routes/myPages"
import { isSignupPage } from "@scandic-hotels/common/constants/routes/signup"
import ContentPage from "@/components/ContentType/StaticPages/ContentPage"
import { ContentPage } from "@/components/ContentType/ContentPage"
import { getLang } from "@/i18n/serverContext"
import { isLoggedInUser } from "@/utils/isLoggedInUser"

View File

@@ -0,0 +1,85 @@
.pageSection {
display: grid;
gap: var(--Space-x4);
padding-bottom: var(--Space-x9);
}
.header {
background-color: var(--Base-Surface-Subtle-Normal);
}
.videoWrapper {
width: 100%;
height: 520px;
position: relative;
padding: var(--Space-x2) 0;
.heading {
color: var(--Text-Inverted);
}
.headerContent {
height: 100%;
align-content: end;
padding-bottom: var(--Space-x15);
}
}
.heroVideo {
position: absolute;
inset: 0;
}
.headerContent {
position: relative;
display: grid;
gap: var(--Space-x3);
padding-bottom: var(--Space-x4);
width: var(--max-width-content);
margin: 0 auto;
justify-items: start;
z-index: 1;
}
.intro {
display: grid;
max-width: var(--max-width-text-block);
gap: var(--Space-x3);
}
.heading {
color: var(--Text-Heading);
text-wrap: balance;
hyphens: auto;
}
.content {
display: flex;
flex-direction: column;
gap: var(--Space-x4);
width: var(--max-width-content);
margin: 0 auto;
}
.main {
display: grid;
gap: var(--Space-x6);
}
@media (min-width: 1367px) {
.videoWrapper {
height: 480px;
.headerContent {
padding-bottom: var(--Space-x5);
}
}
.content {
gap: var(--Space-x9);
}
.main {
gap: var(--Space-x9);
}
}

View File

@@ -0,0 +1,139 @@
import { Suspense } from "react"
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
import { serverClient } from "@/lib/trpc/server"
import Blocks from "@/components/Blocks"
import Breadcrumbs from "@/components/Breadcrumbs"
import HeaderDynamicContent from "@/components/Headers/DynamicContent"
import Hero from "@/components/Hero"
import { HeroVideo } from "@/components/HeroVideo"
import MeetingPackageWidget from "@/components/MeetingPackageWidget"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import LinkChips from "@/components/TempDesignSystem/LinkChips"
import styles from "./collectionPage.module.css"
import type { BreadcrumbsProps } from "@/components/TempDesignSystem/Breadcrumbs/breadcrumbs"
export async function CollectionPage() {
const caller = await serverClient()
const collectionPageRes = await caller.contentstack.collectionPage.get()
if (!collectionPageRes) {
return null
}
const { tracking, collectionPage } = collectionPageRes
const { blocks, hero_image, hero_video, header, meeting_package } =
collectionPage
const breadCrumbsVariants: Pick<BreadcrumbsProps, "color" | "size"> = {
color: "Surface/Secondary/Default",
size: "contentWidth",
}
return (
<>
{hero_video ? null : (
<Suspense fallback={<BreadcrumbsSkeleton {...breadCrumbsVariants} />}>
<Breadcrumbs {...breadCrumbsVariants} />
</Suspense>
)}
<section className={styles.pageSection}>
<header className={styles.header}>
{hero_video ? (
<>
<div className={styles.videoWrapper}>
<HeroVideo
className={styles.heroVideo}
src={hero_video.src}
focalPoint={hero_video.focalPoint}
captions={hero_video.captions}
isFullWidth
hasOverlay
/>
<div className={styles.headerContent}>
<Typography variant="Title/lg">
<h1 className={styles.heading}>{header.heading}</h1>
</Typography>
{header.top_primary_button?.url ? (
<ButtonLink
href={header.top_primary_button.url}
size="Medium"
variant="Primary"
color="Inverted"
>
{header.top_primary_button.title}
</ButtonLink>
) : null}
</div>
</div>
<Suspense
fallback={<BreadcrumbsSkeleton {...breadCrumbsVariants} />}
>
<Breadcrumbs {...breadCrumbsVariants} />
</Suspense>
</>
) : null}
<div className={styles.headerContent}>
{hero_video ? (
<Typography variant="Body/Lead text">
<p>{header.preamble}</p>
</Typography>
) : (
<>
<Typography variant="Title/lg">
<h1 className={styles.heading}>{header.heading}</h1>
</Typography>
<Typography variant="Body/Lead text">
<p>{header.preamble}</p>
</Typography>
{header.top_primary_button?.url ? (
<ButtonLink
href={header.top_primary_button.url}
size="Medium"
variant="Tertiary"
>
{header.top_primary_button.title}
</ButtonLink>
) : null}
</>
)}
{header.navigation_links ? (
<LinkChips chips={header.navigation_links} />
) : null}
{"dynamic_content" in header && header.dynamic_content ? (
<HeaderDynamicContent {...header.dynamic_content} />
) : null}
</div>
</header>
<div className={styles.content}>
{!hero_video && hero_image ? (
<Hero
alt={hero_image.meta.alt || hero_image.meta.caption || ""}
src={hero_image.url}
focalPoint={hero_image.focalPoint}
dimensions={hero_image.dimensions}
/>
) : null}
<main className={styles.main}>
{meeting_package?.show_widget && (
<MeetingPackageWidget
destination={meeting_package.location}
className={styles.meetingPackageWidget}
/>
)}
{blocks ? <Blocks blocks={blocks} /> : null}
</main>
</div>
</section>
<TrackingSDK pageData={tracking} />
</>
)
}

View File

@@ -0,0 +1,70 @@
.pageSection {
display: grid;
gap: var(--Space-x4);
padding-bottom: var(--Space-x9);
}
.headerWrapper {
background-color: var(--Base-Surface-Subtle-Normal);
padding-bottom: var(--Space-x4);
}
.header {
display: grid;
gap: var(--Space-x3);
width: var(--max-width-content);
margin: 0 auto;
justify-items: start;
}
.intro {
display: grid;
max-width: var(--max-width-text-block);
gap: var(--Space-x3);
}
.heading {
color: var(--Text-Heading);
text-wrap: balance;
hyphens: auto;
}
.content {
display: flex;
flex-direction: column;
gap: var(--Space-x4);
width: var(--max-width-content);
margin: 0 auto;
}
.hero {
grid-area: hero;
width: 100%;
height: 400px;
display: flex;
}
.main {
grid-area: main;
display: grid;
gap: var(--Space-x6);
}
@media (min-width: 1367px) {
.content {
display: grid;
grid-template-areas:
"hero hero"
"main sidebar";
grid-template-columns: var(--max-width-text-block) 1fr;
gap: var(--Space-x9);
}
.main {
gap: var(--Space-x9);
}
.hero {
height: 480px;
}
}

View File

@@ -0,0 +1,118 @@
import { Suspense } from "react"
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
import { serverClient } from "@/lib/trpc/server"
import Blocks from "@/components/Blocks"
import Breadcrumbs from "@/components/Breadcrumbs"
import HeaderDynamicContent from "@/components/Headers/DynamicContent"
import Hero from "@/components/Hero"
import { HeroVideo } from "@/components/HeroVideo"
import Sidebar from "@/components/Sidebar"
import SidebarSkeleton from "@/components/Sidebar/SidebarSkeleton"
import StickyMeetingPackageWidget from "@/components/StickyMeetingPackageWidget"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import LinkChips from "@/components/TempDesignSystem/LinkChips"
import styles from "./contentPage.module.css"
export async function ContentPage() {
const caller = await serverClient()
const contentPageRes = await caller.contentstack.contentPage.get()
if (!contentPageRes) {
return null
}
const { tracking, contentPage } = contentPageRes
const { blocks, hero_image, hero_video, header, meeting_package, sidebar } =
contentPage
return (
<>
{meeting_package?.show_widget && (
<StickyMeetingPackageWidget destination={meeting_package.location} />
)}
<Suspense
fallback={
<BreadcrumbsSkeleton
color="Surface/Secondary/Default"
size="contentWidth"
/>
}
>
<Breadcrumbs color="Surface/Secondary/Default" size="contentWidth" />
</Suspense>
<section className={styles.pageSection}>
{header ? (
<div className={styles.headerWrapper}>
<header className={styles.header}>
<div className={styles.intro}>
<Typography variant="Title/lg">
<h1 className={styles.heading}>{header.heading}</h1>
</Typography>
<Typography variant="Body/Lead text">
<p>{header.preamble}</p>
</Typography>
</div>
{header.top_primary_button?.url ? (
<ButtonLink
href={header.top_primary_button.url}
size="Medium"
variant="Tertiary"
>
{header.top_primary_button.title}
</ButtonLink>
) : null}
{header.navigation_links ? (
<LinkChips chips={header.navigation_links} />
) : null}
{"dynamic_content" in header && header.dynamic_content ? (
<HeaderDynamicContent {...header.dynamic_content} />
) : null}
</header>
</div>
) : null}
<div className={styles.content}>
{hero_video || hero_image ? (
<>
{hero_video ? (
<HeroVideo
className={styles.hero}
src={hero_video.src}
focalPoint={hero_video.focalPoint}
captions={hero_video.captions}
/>
) : null}
{!hero_video && hero_image ? (
<Hero
className={styles.hero}
alt={hero_image.meta.alt || hero_image.meta.caption || ""}
src={hero_image.url}
focalPoint={hero_image.focalPoint}
dimensions={hero_image.dimensions}
/>
) : null}
</>
) : null}
<main className={styles.main}>
{blocks ? <Blocks blocks={blocks} /> : null}
</main>
{sidebar?.length ? (
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar blocks={sidebar} />
</Suspense>
) : null}
</div>
</section>
<TrackingSDK pageData={tracking} />
</>
)
}

View File

@@ -1,22 +0,0 @@
import { serverClient } from "@/lib/trpc/server"
import StaticPage from ".."
export default async function CollectionPage() {
const caller = await serverClient()
const collectionPageRes = await caller.contentstack.collectionPage.get()
if (!collectionPageRes) {
return null
}
const { tracking, collectionPage } = collectionPageRes
return (
<StaticPage
content={collectionPage}
tracking={tracking}
pageType="collection"
/>
)
}

View File

@@ -1,45 +0,0 @@
import { Suspense } from "react"
import { serverClient } from "@/lib/trpc/server"
import Breadcrumbs from "@/components/Breadcrumbs"
import StickyMeetingPackageWidget from "@/components/StickyMeetingPackageWidget"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import StaticPage from ".."
export default async function ContentPage() {
const caller = await serverClient()
const contentPageRes = await caller.contentstack.contentPage.get()
if (!contentPageRes) {
return null
}
const { tracking, contentPage } = contentPageRes
return (
<>
{contentPage.meeting_package?.show_widget && (
<StickyMeetingPackageWidget
destination={contentPage.meeting_package.location}
/>
)}
<Suspense
fallback={
<BreadcrumbsSkeleton
color="Surface/Secondary/Default"
size="contentWidth"
/>
}
>
<Breadcrumbs color="Surface/Secondary/Default" size="contentWidth" />
</Suspense>
<StaticPage
content={contentPage}
tracking={tracking}
pageType="content"
/>
</>
)
}

View File

@@ -7,6 +7,7 @@ import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
import Blocks from "@/components/Blocks"
import HeaderDynamicContent from "@/components/Headers/DynamicContent"
import Hero from "@/components/Hero"
import { HeroVideo } from "@/components/HeroVideo"
import MeetingPackageWidget from "@/components/MeetingPackageWidget"
import Sidebar from "@/components/Sidebar"
import SidebarSkeleton from "@/components/Sidebar/SidebarSkeleton"
@@ -23,7 +24,7 @@ export default async function StaticPage({
tracking,
pageType,
}: StaticPageProps) {
const { blocks, hero_image, header, meeting_package } = content
const { blocks, hero_image, hero_video, header, meeting_package } = content
return (
<>
@@ -63,14 +64,25 @@ export default async function StaticPage({
</div>
</header>
{hero_image ? (
<div className={styles.heroContainer}>
<Hero
alt={hero_image.meta.alt || hero_image.meta.caption || ""}
src={hero_image.url}
focalPoint={hero_image.focalPoint}
dimensions={hero_image.dimensions}
/>
{hero_video || hero_image ? (
<div className={styles.heroWrapper}>
{hero_video ? (
<HeroVideo
className={styles.heroVideo}
src={hero_video.src}
focalPoint={hero_video.focalPoint}
captions={hero_video.captions}
/>
) : null}
{!hero_video && hero_image ? (
<Hero
className={styles.heroImage}
alt={hero_image.meta.alt || hero_image.meta.caption || ""}
src={hero_image.url}
focalPoint={hero_image.focalPoint}
dimensions={hero_image.dimensions}
/>
) : null}
</div>
) : null}

View File

@@ -26,15 +26,15 @@
hyphens: auto;
}
.heroContainer {
width: 100%;
.heroWrapper {
padding: var(--Space-x4) var(--Space-x2);
}
.heroContainer img {
.heroImage,
.heroVideo {
max-width: var(--max-width-content);
margin: 0 auto;
display: block;
display: flex;
}
.contentContainer {

View File

@@ -1,25 +1,25 @@
import { cx } from "class-variance-authority"
import Image from "@scandic-hotels/design-system/Image"
import styles from "./hero.module.css"
import type { HeroProps } from "./hero"
import type { ComponentProps } from "react"
export default async function Hero({
alt,
src,
focalPoint,
dimensions,
}: HeroProps) {
type HeroProps = Pick<
ComponentProps<typeof Image>,
"className" | "alt" | "src" | "focalPoint" | "dimensions"
>
export default async function Hero({ className, alt, ...props }: HeroProps) {
return (
<Image
className={styles.hero}
className={cx(styles.hero, className)}
alt={alt}
height={480}
width={1196}
src={src}
focalPoint={focalPoint}
dimensions={dimensions}
priority
{...props}
/>
)
}

View File

@@ -0,0 +1,10 @@
.videoWrapper {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
&:not(.fullWidth) {
border-radius: var(--Corner-radius-xl);
}
}

View File

@@ -0,0 +1,29 @@
import { cx } from "class-variance-authority"
import { VideoPlayer } from "@scandic-hotels/design-system/VideoPlayer"
import styles from "./heroVideo.module.css"
import type { ComponentProps } from "react"
interface HeroVideoProps
extends Omit<ComponentProps<typeof VideoPlayer>, "className" | "variant"> {
className?: string
isFullWidth?: boolean
}
export function HeroVideo({
className,
isFullWidth,
...props
}: HeroVideoProps) {
return (
<div
className={cx(styles.videoWrapper, className, {
[styles.fullWidth]: isFullWidth,
})}
>
<VideoPlayer variant="hero" {...props} />
</div>
)
}

View File

@@ -53,6 +53,7 @@
"./utils/chunk": "./utils/chunk.ts",
"./utils/dateFormatting": "./utils/dateFormatting.ts",
"./utils/debounce": "./utils/debounce.ts",
"./utils/focalPoint": "./utils/focalPoint.ts",
"./utils/imageVault": "./utils/imageVault.ts",
"./utils/isDefined": "./utils/isDefined.ts",
"./utils/isEdge": "./utils/isEdge.ts",

View File

@@ -0,0 +1,11 @@
import { z } from "zod"
export const focalPointSchema = z
.object({
x: z.number().nullish(),
y: z.number().nullish(),
})
.transform(({ x, y }) => ({
x: x ?? 50,
y: y ?? 50,
}))

View File

@@ -1,9 +1,6 @@
import { z } from "zod"
const focalPointSchema = z.object({
x: z.number(),
y: z.number(),
})
import { focalPointSchema } from "./focalPoint"
const deprecatedMetaDataSchema = z.object({
DefinitionType: z.number().nullish(),

View File

@@ -18,8 +18,13 @@
@media (hover: hover) {
&:hover .iconWrapper {
background-color: var(--Component-Button-Inverted-Fill-Hover);
color: var(--Component-Button-Inverted-On-fill-Hover);
background:
linear-gradient(
0deg,
var(--Component-Button-Inverted-Fill-Hover) 0%,
var(--Component-Button-Inverted-Fill-Hover) 100%
),
var(--Component-Button-Inverted-Fill-Default);
}
}

View File

@@ -22,6 +22,7 @@ interface VideoPlayerProps extends VariantProps<typeof variants> {
captions?: Caption[]
focalPoint?: FocalPoint
autoPlay?: boolean
hasOverlay?: boolean
}
export function VideoPlayer({
@@ -31,6 +32,7 @@ export function VideoPlayer({
className,
variant = 'inline',
autoPlay,
hasOverlay,
}: VideoPlayerProps) {
const intl = useIntl()
const videoRef = useRef<HTMLVideoElement>(null)
@@ -84,7 +86,13 @@ export function VideoPlayer({
const showMuteButton = variant === 'inline' && isActivated
return (
<div className={cx(classNames, { [styles.isActivated]: isActivated })}>
<div
className={cx(
classNames,
{ [styles.isActivated]: isActivated },
{ [styles.hasOverlay]: hasOverlay }
)}
>
<video
ref={videoRef}
className={styles.video}

View File

@@ -5,6 +5,7 @@
display: flex;
justify-content: center;
align-items: center;
z-index: 0;
&.inline {
border-radius: var(--Corner-radius-md);
@@ -22,6 +23,18 @@
right: var(--Space-x2);
}
}
&.hasOverlay::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0) 0%,
rgba(31, 28, 27, 0.25) 48.08%,
rgba(31, 28, 27, 0.8) 100%
);
}
}
.video {
@@ -32,17 +45,30 @@
.playButton {
position: absolute;
z-index: 1;
}
.muteButton {
position: absolute;
top: var(--Space-x2);
right: var(--Space-x2);
z-index: 1;
}
@media screen and (min-width: 768px) {
.videoPlayer.hero .playButton {
bottom: var(--Space-x4);
right: var(--Space-x4);
.videoPlayer {
&.hero .playButton {
bottom: var(--Space-x4);
right: var(--Space-x4);
}
&.hasOverlay::after {
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0) 0%,
rgba(31, 28, 27, 0.25) 53.8%,
rgba(31, 28, 27, 0.74) 97.6%
);
}
}
}

View File

@@ -188,6 +188,7 @@
"./Tooltip": "./lib/components/Tooltip/index.tsx",
"./TripAdvisorChip": "./lib/components/TripAdvisorChip/index.tsx",
"./Typography": "./lib/components/Typography/index.tsx",
"./VideoPlayer": "./lib/components/VideoPlayer/index.tsx",
"./base.css": "./lib/base.css",
"./design-system-new-deprecated.css": "./lib/design-system-new-deprecated.css",
"./downtown-camper.css": "./lib/styles/downtown-camper.css",

View File

@@ -7,3 +7,10 @@ export const System = gql`
uid
}
`
export const AssetSystem = gql`
fragment AssetSystem on SysAssetSystemField {
content_type_uid
uid
}
`

View File

@@ -0,0 +1,45 @@
import { gql } from "graphql-tag"
import { AssetSystem } from "./System.graphql"
export const Video = gql`
fragment Video on Video {
sourceConnection {
edges {
node {
url
}
}
}
focal_point {
x
y
}
captions {
is_default
language
fileConnection {
edges {
node {
url
}
}
}
}
}
`
export const VideoRef = gql`
fragment VideoRef on Video {
sourceConnection {
edges {
node {
system {
...AssetSystem
}
}
}
}
}
${AssetSystem}
`

View File

@@ -25,11 +25,15 @@ import {
TopPrimaryButtonRef_CollectionPage,
} from "../../Fragments/CollectionPage/TopPrimaryButton.graphql"
import { System } from "../../Fragments/System.graphql"
import { Video, VideoRef } from "../../Fragments/Video.graphql"
export const GetCollectionPage = gql`
query GetCollectionPage($locale: String!, $uid: String!) {
collection_page(uid: $uid, locale: $locale) {
hero_image
hero_video {
...Video
}
title
header {
heading
@@ -64,11 +68,15 @@ export const GetCollectionPage = gql`
${Shortcuts_CollectionPage}
${UspGrid_CollectionPage}
${DynamicContent_CollectionPage}
${Video}
`
export const GetCollectionPageRefs = gql`
query GetCollectionPageRefs($locale: String!, $uid: String!) {
collection_page(locale: $locale, uid: $uid) {
hero_video {
...VideoRef
}
header {
...TopPrimaryButtonRef_CollectionPage
...NavigationLinksRef_CollectionPage
@@ -92,6 +100,7 @@ export const GetCollectionPageRefs = gql`
${Shortcuts_CollectionPageRefs}
${UspGrid_CollectionPageRefs}
${DynamicContent_CollectionPageRefs}
${VideoRef}
`
export const GetDaDeEnUrlsCollectionPage = gql`

View File

@@ -60,11 +60,15 @@ import {
TeaserCardSidebar_ContentPageRefs,
} from "../../Fragments/Sidebar/TeaserCard.graphql"
import { System } from "../../Fragments/System.graphql"
import { Video, VideoRef } from "../../Fragments/Video.graphql"
export const GetContentPage = gql`
query GetContentPage($locale: String!, $uid: String!) {
content_page(uid: $uid, locale: $locale) {
hero_image
hero_video {
...Video
}
title
header {
heading
@@ -110,6 +114,7 @@ export const GetContentPage = gql`
${ScriptedCardSidebar_ContentPage}
${TeaserCardSidebar_ContentPage}
${QuickLinksSidebar_ContentPage}
${Video}
`
export const GetContentPageBlocksBatch1 = gql`
@@ -153,6 +158,9 @@ export const GetContentPageBlocksBatch2 = gql`
export const GetContentPageRefs = gql`
query GetContentPageRefs($locale: String!, $uid: String!) {
content_page(locale: $locale, uid: $uid) {
hero_video {
...VideoRef
}
header {
dynamic_content {
component
@@ -181,6 +189,7 @@ export const GetContentPageRefs = gql`
${ScriptedCardSidebar_ContentPageRefs}
${TeaserCardSidebar_ContentPageRefs}
${QuickLinksSidebar_ContentPageRefs}
${VideoRef}
`
export const GetContentPageBlocksRefs = gql`

View File

@@ -23,6 +23,7 @@ import {
} from "../schemas/linkConnection"
import { internalOrExternalLinkSchema } from "../schemas/pageLinks"
import { systemSchema } from "../schemas/system"
import { transformedVideoSchema, videoRefSchema } from "../schemas/video"
// Block schemas
export const collectionPageCards = z
@@ -78,6 +79,7 @@ const navigationLinksSchema = z
export const collectionPageSchema = z.object({
collection_page: z.object({
hero_image: transformedImageVaultAssetSchema,
hero_video: transformedVideoSchema,
blocks: discriminatedUnionArray(blocksSchema.options).nullable(),
title: z.string(),
header: z.object({
@@ -145,6 +147,7 @@ const collectionPageHeaderRefs = z.object({
export const collectionPageRefsSchema = z.object({
collection_page: z.object({
hero_video: videoRefSchema.nullish(),
header: collectionPageHeaderRefs,
blocks: discriminatedUnionArray(
collectionPageBlockRefsItem.options

View File

@@ -11,6 +11,7 @@ import {
import {
generateRefsResponseTag,
generateTag,
generateTagsFromAssetSystem,
generateTagsFromSystem,
} from "../../../utils/generateTag"
import { collectionPageRefsSchema } from "./output"
@@ -84,6 +85,7 @@ export function generatePageTags(
const connections = getConnections(validatedData)
return [
generateTagsFromSystem(lang, connections),
generateTagsFromAssetSystem(connections),
generateTag(lang, validatedData.collection_page.system.uid),
].flat()
}

View File

@@ -59,6 +59,7 @@ import {
teaserCardsSchema,
} from "../schemas/sidebar/teaserCard"
import { systemSchema } from "../schemas/system"
import { transformedVideoSchema, videoRefSchema } from "../schemas/video"
// Block schemas
export const contentPageCards = z
@@ -194,6 +195,7 @@ const navigationLinksSchema = z
export const contentPageSchema = z.object({
content_page: z.object({
hero_image: transformedImageVaultAssetSchema,
hero_video: transformedVideoSchema,
blocks: discriminatedUnionArray(blocksSchema.options).nullable(),
sidebar: discriminatedUnionArray(sidebarSchema.options).nullable(),
title: z.string(),
@@ -323,6 +325,7 @@ const contentPageHeaderRefs = z.object({
export const contentPageRefsSchema = z.object({
content_page: z.object({
hero_video: videoRefSchema.nullish(),
header: contentPageHeaderRefs,
blocks: discriminatedUnionArray(
contentPageBlockRefsItem.options

View File

@@ -10,6 +10,7 @@ import { ContentPageEnum } from "../../../types/contentPage"
import {
generateRefsResponseTag,
generateTag,
generateTagsFromAssetSystem,
generateTagsFromSystem,
} from "../../../utils/generateTag"
import { contentPageRefsSchema } from "./output"
@@ -75,12 +76,14 @@ export function generatePageTags(
const connections = getConnections(validatedData)
return [
generateTagsFromSystem(lang, connections),
generateTagsFromAssetSystem(connections),
generateTag(lang, validatedData.content_page.system.uid),
].flat()
}
export function getConnections({ content_page }: ContentPageRefs) {
const connections: System["system"][] = [content_page.system]
if (content_page.blocks) {
content_page.blocks.forEach((block) => {
switch (block.__typename) {

View File

@@ -11,3 +11,12 @@ export const systemSchema = z.object({
export interface System {
system: z.output<typeof systemSchema>
}
export const assetSystemSchema = z.object({
content_type_uid: z.string(),
uid: z.string(),
})
export interface AssetSystem {
system: z.output<typeof assetSystemSchema>
}

View File

@@ -0,0 +1,75 @@
import { z } from "zod"
import { Lang } from "@scandic-hotels/common/constants/language"
import { focalPointSchema } from "@scandic-hotels/common/utils/focalPoint"
import { assetSystemSchema } from "./system"
export const videoSchema = z.object({
sourceConnection: z.object({
edges: z.array(
z.object({
node: z.object({
url: z.string().url(),
}),
})
),
}),
focal_point: focalPointSchema.nullish(),
captions: z.array(
z.object({
is_default: z.boolean(),
fileConnection: z.object({
edges: z.array(
z.object({
node: z.object({
url: z.string().url(),
}),
})
),
}),
language: z.nativeEnum(Lang),
})
),
})
export const transformedVideoSchema = videoSchema
.nullish()
.transform((video) => {
const src = video?.sourceConnection.edges[0]?.node.url || ""
if (!video || !src) {
return null
}
const captions = video.captions
.filter((caption) => caption.fileConnection.edges[0]?.node.url)
.map((caption) => ({
src: caption.fileConnection.edges[0]?.node.url || "",
srcLang: caption.language,
isDefault: caption.is_default,
}))
return {
src,
focalPoint: video.focal_point
? { x: video.focal_point.x, y: video.focal_point.y }
: { x: 50, y: 50 },
captions,
}
})
export const videoRefSchema = z.object({
sourceConnection: z.object({
edges: z.array(
z.object({
node: z.object({
system: assetSystemSchema,
}),
})
),
}),
})
export type Video = z.output<typeof transformedVideoSchema>
export type VideoCaptions = NonNullable<Video>["captions"]

View File

@@ -1,6 +1,9 @@
import type { Lang } from "@scandic-hotels/common/constants/language"
import { Lang } from "@scandic-hotels/common/constants/language"
import type { System } from "../routers/contentstack/schemas/system"
import type {
AssetSystem,
System,
} from "../routers/contentstack/schemas/system"
import type { Edges } from "../types/edges"
import type { NodeRefs } from "../types/refs"
@@ -85,6 +88,14 @@ export function generateTagsFromSystem(
})
}
export function generateTagsFromAssetSystem(
connections: AssetSystem["system"][]
) {
return connections.map((system) => {
return generateTag(Lang.en, system.content_type_uid, system.uid)
})
}
/**
* Function to generate tags for loyalty configuration models
*

Binary file not shown.