diff --git a/sanityv3/schemas/HeroTypes.ts b/sanityv3/schemas/HeroTypes.ts index eb0940a76..6df4247b8 100644 --- a/sanityv3/schemas/HeroTypes.ts +++ b/sanityv3/schemas/HeroTypes.ts @@ -3,4 +3,5 @@ export enum HeroTypes { FIFTY_FIFTY = 'fiftyFifty', FULL_WIDTH_IMAGE = 'fullWidthImage', LOOPING_VIDEO = 'loopingVideo', + BACKGROUND_IMAGE = 'backgroundImage', } diff --git a/sanityv3/schemas/documents/magazineIndexPage.ts b/sanityv3/schemas/documents/magazineIndexPage.ts index 62d008df3..11d445642 100644 --- a/sanityv3/schemas/documents/magazineIndexPage.ts +++ b/sanityv3/schemas/documents/magazineIndexPage.ts @@ -1,12 +1,17 @@ import blocksToText from '../../helpers/blocksToText' import { configureBlockContent } from '../editors/blockContentType' +import CompactBlockEditor from '../components/CompactBlockEditor' import MagazineFooterComponent from '../objects/magazineFooterComponent' -import sharedHeroFields from './header/sharedHeaderFields' import { EdsIcon } from '../../icons' import { bookmarks } from '@equinor/eds-icons' import type { PortableTextBlock, Rule } from 'sanity' import { lang } from './langField' +import { HeroTypes } from '../HeroTypes' +import { configureTitleBlockContent } from '../editors' +import { ValidationContext } from '../../types/schemaTypes' + +const titleContentType = configureTitleBlockContent() const textContentType = configureBlockContent({ h2: true, @@ -53,7 +58,60 @@ export default { description: 'You can override the hero image as the SoMe image by uploading another image here.', fieldset: 'metadata', }, - ...sharedHeroFields, + { + name: 'title', + type: 'array', + title: 'Title', + components: { + input: CompactBlockEditor, + }, + of: [titleContentType], + fieldset: 'header', + validation: (Rule: Rule) => Rule.required(), + }, + { + title: 'Type', + name: 'heroType', + type: 'string', + options: { + list: [ + { title: 'Default', value: HeroTypes.DEFAULT }, + { title: 'Full Image', value: HeroTypes.FULL_WIDTH_IMAGE }, + { title: 'Background image', value: HeroTypes.BACKGROUND_IMAGE }, + ].filter((e) => e), + }, + initialValue: 'default', + fieldset: 'header', + }, + { + title: 'Hero image', + name: 'heroFigure', + type: 'imageWithAltAndCaption', + fieldset: 'header', + }, + { + title: 'Hero image ratio', + name: 'heroRatio', + type: 'string', + options: { + list: [ + { title: 'Tall', value: 'tall' }, + { title: '2:1(deprecated)', value: '0.5' }, + { title: 'Narrow', value: 'narrow' }, + ], + }, + hidden: ({ parent }: DocumentType) => { + return parent?.heroType !== HeroTypes.FULL_WIDTH_IMAGE + }, + validation: (Rule: Rule) => + Rule.custom((value: string, context: ValidationContext) => { + const { parent } = context as unknown as DocumentType + if (parent?.heroType === HeroTypes.FULL_WIDTH_IMAGE && !value) return 'Field is required' + return true + }), + initialValue: '0.5', + fieldset: 'header', + }, { title: 'Text', name: 'ingress', diff --git a/sanityv3/schemas/textSnippets.ts b/sanityv3/schemas/textSnippets.ts index 69192df4f..9f10747b5 100644 --- a/sanityv3/schemas/textSnippets.ts +++ b/sanityv3/schemas/textSnippets.ts @@ -355,7 +355,7 @@ const snippets: textSnippet = { defaultValue: 'Please fill out your name', group: groups.pensionForm, }, - + pension_form_email: { title: 'Email', defaultValue: 'Email *', @@ -365,7 +365,7 @@ const snippets: textSnippet = { title: 'Email validation', defaultValue: 'Please fill out a valid email address', group: groups.pensionForm, - }, + }, pension_form_category: { title: 'Category', defaultValue: 'Category', @@ -386,7 +386,7 @@ const snippets: textSnippet = { defaultValue: 'Other Pension/Insurance Related', group: groups.pensionForm, }, - + pension_form_what_is_your_request: { title: 'What is your request?', defaultValue: 'What is your request?', @@ -402,7 +402,7 @@ const snippets: textSnippet = { defaultValue: 'Please let us know how we may help you', group: groups.pensionForm, }, - + pension_form_submit: { title: 'Submit Button Text', defaultValue: 'Submit Form', @@ -912,6 +912,11 @@ const snippets: textSnippet = { defaultValue: 'Switch to', group: groups.others, }, + filter: { + title: 'Filter', + defaultValue: 'Filter', + group: groups.others, + }, next: { title: 'Next', defaultValue: 'Next', diff --git a/web/common/helpers/scrollToTop.ts b/web/common/helpers/scrollToTop.ts new file mode 100644 index 000000000..d8999583a --- /dev/null +++ b/web/common/helpers/scrollToTop.ts @@ -0,0 +1,3 @@ +export const scrollToTop = () => { + window.scrollTo({ top: 0, behavior: 'smooth' }) +} diff --git a/web/components/src/Backgrounds/ImageBackgroundContainer.tsx b/web/components/src/Backgrounds/ImageBackgroundContainer.tsx index baa15977e..ddb272ac1 100644 --- a/web/components/src/Backgrounds/ImageBackgroundContainer.tsx +++ b/web/components/src/Backgrounds/ImageBackgroundContainer.tsx @@ -8,6 +8,9 @@ type ImageBackgroundContainerProps = { scrimClassName?: string /* On mobile dont split background image and content */ dontSplit?: boolean + /* Provide gradient in scrimClassname and disable default */ + overrideGradient?: boolean + aspectRatio?: number } & ImageBackground & HTMLAttributes const DEFAULT_MAX_WIDTH = 1920 @@ -22,12 +25,14 @@ export const ImageBackgroundContainer = forwardRef diff --git a/web/core/SimplePagination/SimplePagination.tsx b/web/core/SimplePagination/SimplePagination.tsx index b9347e54a..6d8fe65ea 100644 --- a/web/core/SimplePagination/SimplePagination.tsx +++ b/web/core/SimplePagination/SimplePagination.tsx @@ -2,8 +2,8 @@ import envisTwMerge from '../../twMerge' import { useIntl } from 'react-intl' import { MediaButton } from '@core/MediaButton/MediaButton' import { useContext } from 'react' -import { PaginationContext } from '../../common/contexts/PaginationContext' import { usePrefersReducedMotion } from '../../common/hooks/usePrefersReducedMotion' +import { PaginationContext } from '../../common/contexts/PaginationContext' export type SimplePaginationProps = { className?: string diff --git a/web/lib/fetchData.ts b/web/lib/fetchData.ts index 90cd33f9a..5d714fbff 100644 --- a/web/lib/fetchData.ts +++ b/web/lib/fetchData.ts @@ -21,6 +21,30 @@ export const getComponentsData = async (page: { query: string; queryParams: Quer return { menuData, pageData, footerData } } +export type MagazineQueryParams = { + lang?: string + tag?: string | undefined + lastId?: string + lastPublishedAt?: string +} + +export const getData = async (fetchQuery: { query: string; queryParams: MagazineQueryParams }, preview = false) => { + const client = getClient(preview) + try { + const results = await client.fetch(fetchQuery.query, fetchQuery.queryParams) + return { + isSuccess: true, + data: results, + } + } catch (error) { + console.log('Error when fetching from Sanity', error) + return { + isError: true, + data: [], + } + } +} + type NewsroomQueryParams = { lang?: string tags?: string[] diff --git a/web/lib/queries/magazine.ts b/web/lib/queries/magazine.ts index 83fceaa51..3ae634c01 100644 --- a/web/lib/queries/magazine.ts +++ b/web/lib/queries/magazine.ts @@ -7,6 +7,8 @@ import downloadableImageFields from './common/actions/downloadableImageFields' import markDefs from './common/blockEditorMarks' import { seoAndSomeFields } from './common/seoAndSomeFields' import { sameLang, fixPreviewForDrafts, noDrafts } from './common/langAndDrafts' +import { publishDateTimeQuery } from './common/publishDateTime' +import background from './common/background' const footerComponentFields = /* groq */ ` title, @@ -15,7 +17,7 @@ const footerComponentFields = /* groq */ ` ${markDefs}, }, "designOptions": { - "background": coalesce(background.title, 'White'), + ${background}, "imagePosition": coalesce(imagePosition, 'left'), }, "image": image{ @@ -29,7 +31,11 @@ const footerComponentFields = /* groq */ ` }, ` -const promotedmagazineTags = /* groq */ `"": *[_type == "magazineIndex" && ${sameLang} && ${noDrafts}][0] {"magazineTags":promotedMagazineTags[]->title[$lang]}` +const promotedmagazineTags = /* groq */ `"": *[_type == "magazineIndex" && ${sameLang} && ${noDrafts}][0] {"magazineTags":promotedMagazineTags[]->{ + "id": _id, + "key": key.current, + "title":title[$lang], +}}` export const magazineQuery = /* groq */ ` *[_type == "magazine" && slug.current == $slug && ${fixPreviewForDrafts}] { @@ -40,6 +46,7 @@ export const magazineQuery = /* groq */ ` "hero": ${heroFields}, "template": _type, ${promotedmagazineTags}, + "tags": magazineTags[]->title[$lang], "content": content[] { ${pageContentFields} }, @@ -65,8 +72,62 @@ export const magazineIndexQuery = /* groq */ ` }, "background": coalesce(ingressBackground.title, 'White'), }, - "magazineTags": promotedMagazineTags[]->title[$lang], + "magazineTags": promotedMagazineTags[]->{ + "id": _id, + "key": key.current, + "title":title[$lang], + }, "footerComponent": footerComponent{ ${footerComponentFields} } - }` +}` + +export const allMagazineDocuments = /* groq */ ` +*[_type == "magazine" && ${sameLang} && ${noDrafts} ] | order(${publishDateTimeQuery} desc){ + "id": _id, + "slug": slug.current, + title[]{ + ..., + ${markDefs}, + }, + "hero": ${heroFields} +}` + +//&& (${publishDateTimeQuery} < $lastPublishedAt || (${publishDateTimeQuery} == $lastPublishedAt && _id < $lastId)) +const prevDirectionFilter = /* groq */ ` +&& (${publishDateTimeQuery} < $lastPublishedAt || (${publishDateTimeQuery} == $lastPublishedAt && _id < $lastId)) +` +//&& (${publishDateTimeQuery} > $lastPublishedAt || (${publishDateTimeQuery} == $lastPublishedAt && _id > $lastId)) +const nextDirectionFilter = /* groq */ ` +&& (${publishDateTimeQuery} > $lastPublishedAt || (${publishDateTimeQuery} == $lastPublishedAt && _id > $lastId)) +` + +export const getMagazineArticlesByTag = (hasFirstId = false, hasLastId = false) => /* groq */ ` +{ + "tagsParam": *[_type == 'magazineTag' + && !(_id in path('drafts.**')) + && key.current == $tag] + { _id, "key": key.current }, +}{ + "articles": *[_type == 'magazine' && ${sameLang} && ${noDrafts} + && references(^.tagsParam[]._id) + ${hasLastId ? nextDirectionFilter : ''} + ${hasFirstId ? prevDirectionFilter : ''} + ] | order(${publishDateTimeQuery} desc){ + "id": _id, + "slug": slug.current, + title[]{ + ..., + ${markDefs}, + }, + "hero": ${heroFields} + } +}.articles +` + +export const allMagazineTags = /* groq */ ` +*[_type == "magazineTag" && ${noDrafts}]{ +"id": _id, +"key": key.current, +"title":title[$lang], +}` diff --git a/web/pageComponents/cards/MagazineCard.tsx b/web/pageComponents/cards/MagazineCard.tsx deleted file mode 100644 index fde7eb8ee..000000000 --- a/web/pageComponents/cards/MagazineCard.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Card } from '@components' -import { CSSProperties } from 'react' -import styled from 'styled-components' -import Image, { Ratios } from '../shared/SanityImage' -import { MagazineCardData } from '../../types/index' - -const { Title, Header, Action, Arrow, Media, CardLink } = Card - -const StyledCard = styled(Card)` - height: var(--height); - --card-gap: var(--space-large); -` - -type MagazineCardProp = { - data: MagazineCardData - fitToContent?: boolean -} - -const getThumbnail = (data: MagazineCardData) => { - const { heroImage } = data - - if (!heroImage?.asset) return false - - return ( - - ) -} - -const MagazineCard = ({ data, fitToContent = false, ...rest }: MagazineCardProp) => { - const { slug, title, tags } = data - const thumbnail = getThumbnail(data) - - if (!thumbnail) return null - - return ( - - - {thumbnail} -
- - <>{title}</> - -
- - - - {tags && tags.length > 0 && ( -
-
- {tags.map((tag: string, index: number) => ( - - {tag} - {index < tags.length - 1 && ', '} - - ))} -
-
- )} -
-
- ) -} - -export default MagazineCard diff --git a/web/pageComponents/hooks/useRouterClearParams.ts b/web/pageComponents/hooks/useRouterClearParams.ts new file mode 100644 index 000000000..851ccbfdd --- /dev/null +++ b/web/pageComponents/hooks/useRouterClearParams.ts @@ -0,0 +1,12 @@ +import { useRouter } from 'next/router' + +const useRouterClearParams = () => { + const router = useRouter() + + return (routerOptions = {}) => { + const href = { pathname: router.pathname } + delete router.query.filter + router.replace(href, undefined, { shallow: true, ...routerOptions }) + } +} +export default useRouterClearParams diff --git a/web/pageComponents/pageTemplates/MagazinePage.tsx b/web/pageComponents/pageTemplates/MagazinePage.tsx deleted file mode 100644 index 4794a9fbb..000000000 --- a/web/pageComponents/pageTemplates/MagazinePage.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import styled from 'styled-components' -import { useRouter } from 'next/router' -import MagazineTagBar from '../shared/MagazineTagBar' -import { PageContent } from './shared/SharedPageContent' -import SharedTitle from './shared/SharedTitle' -import { HeroTypes, MagazinePageSchema } from '../../types/index' -import { SharedBanner } from './shared/SharedBanner' -import Teaser from '../shared/Teaser' -import Seo from '../../pageComponents/shared/Seo' -import useSharedTitleStyles from '../../lib/hooks/useSharedTitleStyles' - -const MagazinePageLayout = styled.main` - /* The neverending spacing story... If two sections with the same background colour - follows each other we want less spacing */ - .background--bg-mid-blue + .background--bg-mid-blue, - .background--bg-default + .background--bg-default, - .background--bg-moss-green + .background--bg-moss-green, - .background--bg-moss-green-light + .background--bg-moss-green-light, - .background--bg-spruce-wood + .background--bg-spruce-wood, - .background--bg-mist-blue + .background--bg-mist-blue, - .background--bg-slate-blue + .background--bg-slate-blue, - .background--bg-mid-yellow + .background--bg-mid-yellow, - .background--bg-mid-orange + .background--bg-mid-orange, - .background--bg-mid-green + .background--bg-mid-green { - /* The teaser component uses an article element, so lets avoid that. - Would be more robust if we add a container for the padding :/ */ - > section, - > figure, - > div:first-child { - /* padding-top: calc(var(--space-3xLarge) / 2); */ - padding-top: 0; - } - } -` -type MagazinePageProps = { - data: MagazinePageSchema -} - -const MagazinePage = ({ data }: MagazinePageProps) => { - const router = useRouter() - const parentSlug = - (router.locale !== router.defaultLocale ? `/${router.locale}` : '') + - router.asPath.substring(router.asPath.indexOf('/'), router.asPath.lastIndexOf('/')) - const magazineTags = data?.magazineTags - const tags = magazineTags?.map((it) => ({ - label: it, - active: false, - })) - - const { hideFooterComponent, footerComponent } = data - - const titleStyles = useSharedTitleStyles(data?.hero?.type, data?.content?.[0]) - - const handleClickTag = (tagValue: string) => { - router.push({ - pathname: parentSlug, - query: { - tag: tagValue === 'ALL' ? '' : tagValue, - }, - }) - } - - return ( - <> - - - - {tags && } - {data.hero.type !== HeroTypes.DEFAULT && ( - - )} - - {!hideFooterComponent && footerComponent?.data && } - - - ) -} - -export default MagazinePage diff --git a/web/pageComponents/pageTemplates/PreviewTemplate.tsx b/web/pageComponents/pageTemplates/PreviewTemplate.tsx index 53d217105..c6eb1f66a 100644 --- a/web/pageComponents/pageTemplates/PreviewTemplate.tsx +++ b/web/pageComponents/pageTemplates/PreviewTemplate.tsx @@ -3,7 +3,7 @@ import { lazy } from 'react' import { usePreview } from '../../lib/sanity' import ErrorPage from 'next/error' -const MagazinePage = lazy(() => import('./MagazinePage')) +const MagazinePage = lazy(() => import('../../templates/magazine/MagazinePage')) const LandingPage = lazy(() => import('./LandingPage')) const EventPage = lazy(() => import('./Event')) const NewsPage = lazy(() => import('./News')) diff --git a/web/pageComponents/pageTemplates/shared/SharedBanner.tsx b/web/pageComponents/pageTemplates/shared/SharedBanner.tsx index daa2d8019..02a7d0d84 100644 --- a/web/pageComponents/pageTemplates/shared/SharedBanner.tsx +++ b/web/pageComponents/pageTemplates/shared/SharedBanner.tsx @@ -10,9 +10,11 @@ type BannerProps = { hero: HeroType hideImageCaption?: boolean captionBg?: BackgroundColours + /* Magazine */ + tags?: string[] } -export const SharedBanner = ({ title, hero, hideImageCaption, captionBg }: BannerProps) => { +export const SharedBanner = ({ title, hero, hideImageCaption, captionBg, tags }: BannerProps) => { switch (hero.type) { case HeroTypes.FULL_WIDTH_IMAGE: return ( @@ -38,6 +40,8 @@ export const SharedBanner = ({ title, hero, hideImageCaption, captionBg }: Banne case HeroTypes.LOOPING_VIDEO: return default: - return + return ( + + ) } } diff --git a/web/pageComponents/searchIndexPages/magazineIndex/Hits.tsx b/web/pageComponents/searchIndexPages/magazineIndex/Hits.tsx deleted file mode 100644 index ff1da1c1d..000000000 --- a/web/pageComponents/searchIndexPages/magazineIndex/Hits.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useHits, UseHitsProps } from 'react-instantsearch' -import { FormattedMessage } from 'react-intl' -import styled from 'styled-components' -import MagazineCard from '../../cards/MagazineCard' -import type { MagazineCardData } from '../../../types/index' -import { forwardRef } from 'react' - -const HitList = styled.div` - width: 100%; - display: grid; - justify-content: center; - grid-gap: var(--space-large); - - --card-minWidth: 200px; - --card-maxWidth: 335px; - grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--card-minWidth)), var(--card-maxWidth))); - - @media (min-width: 1000px) { - --card-maxWidth: 400px; - } -` - -const StyledMagazineCard = styled(MagazineCard)` - min-width: var(--card-minWidth); - max-width: var(--card-maxWidth); - - flex-basis: 0; - flex-grow: 1; -` - -export type HitsProps = React.ComponentProps<'div'> & - UseHitsProps & { - hitComponent: (props: { hit: THit }) => JSX.Element - } - -// @TODO: refactor into our code style -export const Hits = forwardRef(({ ...rest }, ref) => { - const { hits } = useHits() - - if (!hits || hits.length === 0) { - return - } - - return ( - - {hits.map((hit) => { - const data = { - title: hit.pageTitle, - slug: hit.slug, - tags: hit.magazineTags, - heroImage: hit.heroImage, - heroType: hit.heroType, - } - return - })} - - ) -}) diff --git a/web/pageComponents/searchIndexPages/magazineIndex/MagazineTagFilter.tsx b/web/pageComponents/searchIndexPages/magazineIndex/MagazineTagFilter.tsx deleted file mode 100644 index b2d0bee37..000000000 --- a/web/pageComponents/searchIndexPages/magazineIndex/MagazineTagFilter.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { forwardRef } from 'react' -import { useMenu, UseMenuProps, useClearRefinements, useCurrentRefinements } from 'react-instantsearch' -import MagazineTagBar from '../../shared/MagazineTagBar' - -export type RefinementListProps = { tags: string[] } & React.ComponentProps<'div'> & UseMenuProps - -export const MagazineTagFilter = forwardRef(function MagazineTagFilter( - props: RefinementListProps, - ref, -) { - const { refine } = useMenu(props) - const { refine: clear } = useClearRefinements() - const { items: currentItems } = useCurrentRefinements() - const { tags } = props - - const tagLinks = tags.map((e) => ({ - href: '#', - label: e, - active: currentItems[0]?.refinements[0]?.label === e, - })) - - return ( - { - value === 'ALL' ? clear() : refine(value) - }} - ref={ref} - /> - ) -}) diff --git a/web/pageComponents/shared/Hero/DefaultHero.tsx b/web/pageComponents/shared/Hero/DefaultHero.tsx index 6c76afa5f..cc964ea5c 100644 --- a/web/pageComponents/shared/Hero/DefaultHero.tsx +++ b/web/pageComponents/shared/Hero/DefaultHero.tsx @@ -42,9 +42,11 @@ type Props = { image?: ImageWithCaptionData isBigTitle?: boolean bigTitle?: PortableTextBlock[] + /* Magazine */ + tags?: string[] } -export const DefaultHero = ({ title, image, isBigTitle, bigTitle }: Props) => { +export const DefaultHero = ({ title, image, isBigTitle, bigTitle, tags }: Props) => { return ( <> {isBigTitle && ( @@ -54,6 +56,24 @@ export const DefaultHero = ({ title, image, isBigTitle, bigTitle }: Props) => { )} {!isBigTitle && {title && }} + {tags && tags?.length > 0 && ( +
+ {tags && tags?.length > 0 && ( +
    + {tags.map((tag: string) => { + return ( + + {tag} + + ) + })} +
+ )} +
+ )} {image && } ) diff --git a/web/pageComponents/shared/Teaser.tsx b/web/pageComponents/shared/Teaser.tsx index d4a76de75..d48c7e042 100644 --- a/web/pageComponents/shared/Teaser.tsx +++ b/web/pageComponents/shared/Teaser.tsx @@ -114,4 +114,4 @@ const Teaser = ({ data, anchor }: TeaserProps) => { ) } -export default Teaser \ No newline at end of file +export default Teaser diff --git a/web/pages/[[...slug]].tsx b/web/pages/[[...slug]].tsx index 14c59d1db..fc0eeb276 100644 --- a/web/pages/[[...slug]].tsx +++ b/web/pages/[[...slug]].tsx @@ -17,7 +17,7 @@ import { getComponentsData } from '../lib/fetchData' import { useContext, useEffect } from 'react' import { PreviewContext } from '../lib/contexts/PreviewContext' -const MagazinePage = dynamic(() => import('../pageComponents/pageTemplates/MagazinePage')) +const MagazinePage = dynamic(() => import('../templates/magazine/MagazinePage')) const LandingPage = dynamic(() => import('../pageComponents/pageTemplates/LandingPage')) const EventPage = dynamic(() => import('../pageComponents/pageTemplates/Event')) const NewsPage = dynamic(() => import('../pageComponents/pageTemplates/News')) diff --git a/web/pages/magasin/index.global.tsx b/web/pages/magasin/index.global.tsx index 66f165a6c..bd36fee49 100644 --- a/web/pages/magasin/index.global.tsx +++ b/web/pages/magasin/index.global.tsx @@ -1,44 +1,29 @@ import { GetServerSideProps } from 'next' -import { InstantSearchSSRProvider, getServerState } from 'react-instantsearch' import type { AppProps } from 'next/app' import { IntlProvider } from 'react-intl' import Footer from '../../pageComponents/shared/Footer' import Header from '../../pageComponents/shared/Header' -import { magazineIndexQuery } from '../../lib/queries/magazine' +import { allMagazineDocuments, getMagazineArticlesByTag, magazineIndexQuery } from '../../lib/queries/magazine' import getIntl from '../../common/helpers/getIntl' import { getNameFromLocale, getIsoFromLocale } from '../../lib/localization' import { defaultLanguage } from '../../languages' -import MagazineIndexPage from '../../pageComponents/pageTemplates/MagazineIndexPage' import { AlgoliaIndexPageType, MagazineIndexPageType } from '../../types' -import { getComponentsData } from '../../lib/fetchData' -import { renderToString } from 'react-dom/server' +import { getComponentsData, getData, MagazineQueryParams } from '../../lib/fetchData' +import MagazineRoom from '../../templates/magazine/Magazineroom' -export default function MagazineIndexNorwegian({ - isServerRendered = false, - serverState, - data, - url, -}: AlgoliaIndexPageType) { +export default function MagazineIndexNorwegian({ data }: AlgoliaIndexPageType) { const defaultLocale = defaultLanguage.locale const { pageData, slug, intl } = data const locale = intl?.locale || defaultLocale return ( - - - - - + + + ) } @@ -61,6 +46,9 @@ MagazineIndexNorwegian.getLayout = (page: AppProps) => { defaultLocale={getIsoFromLocale(defaultLocale)} messages={data?.intl?.messages} > + {/* + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore */} <>
{page} @@ -70,7 +58,7 @@ MagazineIndexNorwegian.getLayout = (page: AppProps) => { ) } -export const getServerSideProps: GetServerSideProps = async ({ req, preview = false, locale = 'no' }) => { +export const getServerSideProps: GetServerSideProps = async ({ req, preview = false, locale = 'no', query }) => { // For the time being, let's just give 404 for satellites // We will also return 404 if the locale is not Norwegian. // This is a hack and and we should improve this at some point @@ -85,7 +73,7 @@ export const getServerSideProps: GetServerSideProps = async ({ req, preview = fa const lang = getNameFromLocale(locale) const intl = await getIntl(locale, false) - const queryParams = { + let queryParams: MagazineQueryParams = { lang, } @@ -99,29 +87,38 @@ export const getServerSideProps: GetServerSideProps = async ({ req, preview = fa preview, ) - const url = new URL(req.headers.referer || `https://${req.headers.host}${req.url}`).toString() - const serverState = await getServerState( - , - { renderToString }, - ) + let magazineList = [] + if (query?.tag) { + queryParams = { + ...queryParams, + tag: query.tag as string, + } + const magazineGroq = getMagazineArticlesByTag(false, false) + const { data } = await getData({ + query: magazineGroq, + queryParams, + }) + + magazineList = data + } else { + const { data } = await getData({ + query: allMagazineDocuments, + queryParams, + }) + magazineList = data + } return { props: { - serverState, - url, data: { menuData, footerData, intl, - pageData, + pageData: { + ...pageData, + magazineArticles: magazineList, + query, + }, slug, }, }, diff --git a/web/pages/magazine/index.global.tsx b/web/pages/magazine/index.global.tsx index 23a07cbdb..1e9148c7b 100644 --- a/web/pages/magazine/index.global.tsx +++ b/web/pages/magazine/index.global.tsx @@ -1,38 +1,28 @@ import { GetServerSideProps } from 'next' -import { InstantSearchSSRProvider, getServerState } from 'react-instantsearch' import type { AppProps } from 'next/app' import { IntlProvider } from 'react-intl' import Footer from '../../pageComponents/shared/Footer' import Header from '../../pageComponents/shared/Header' -import { magazineIndexQuery } from '../../lib/queries/magazine' +import { allMagazineDocuments, getMagazineArticlesByTag, magazineIndexQuery } from '../../lib/queries/magazine' import getIntl from '../../common/helpers/getIntl' import { getNameFromLocale, getIsoFromLocale } from '../../lib/localization' import { defaultLanguage } from '../../languages' -import MagazineIndexPage from '../../pageComponents/pageTemplates/MagazineIndexPage' import { AlgoliaIndexPageType, MagazineIndexPageType } from '../../types' -import { getComponentsData } from '../../lib/fetchData' -import { renderToString } from 'react-dom/server' +import { getComponentsData, getData, MagazineQueryParams } from '../../lib/fetchData' +import MagazineRoom from '../../templates/magazine/Magazineroom' -export default function MagazineIndex({ isServerRendered = false, serverState, data, url }: AlgoliaIndexPageType) { +export default function MagazineIndex({ data }: AlgoliaIndexPageType) { const defaultLocale = defaultLanguage.locale const { pageData, slug, intl } = data const locale = intl?.locale || defaultLocale return ( - - - - - + + + ) } @@ -56,6 +46,9 @@ MagazineIndex.getLayout = (page: AppProps) => { defaultLocale={getIsoFromLocale(defaultLocale)} messages={data?.intl?.messages} > + {/* + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore */} <>
{page} @@ -65,7 +58,7 @@ MagazineIndex.getLayout = (page: AppProps) => { ) } -export const getServerSideProps: GetServerSideProps = async ({ req, preview = false, locale = 'en' }) => { +export const getServerSideProps: GetServerSideProps = async ({ req, preview = false, locale = 'en', query }) => { if (locale !== 'en') { return { notFound: true, @@ -75,7 +68,7 @@ export const getServerSideProps: GetServerSideProps = async ({ req, preview = fa const lang = getNameFromLocale(locale) const intl = await getIntl(locale, false) - const queryParams = { + let queryParams: MagazineQueryParams = { lang, } @@ -89,29 +82,38 @@ export const getServerSideProps: GetServerSideProps = async ({ req, preview = fa preview, ) - const url = new URL(req.headers.referer || `https://${req.headers.host}${req.url}`).toString() - const serverState = await getServerState( - , - { renderToString }, - ) + let magazineList = [] + if (query?.tag) { + queryParams = { + ...queryParams, + tag: query.tag as string, + } + const magazineGroq = getMagazineArticlesByTag(false, false) + const { data } = await getData({ + query: magazineGroq, + queryParams, + }) + + magazineList = data + } else { + const { data } = await getData({ + query: allMagazineDocuments, + queryParams, + }) + magazineList = data + } return { props: { - serverState, - url, data: { menuData, footerData, intl, - pageData, + pageData: { + ...pageData, + magazineArticles: magazineList, + query, + }, slug, }, }, diff --git a/web/sections/MagazineTags/MagazineTagBar.tsx b/web/sections/MagazineTags/MagazineTagBar.tsx new file mode 100644 index 000000000..3d9f67cc6 --- /dev/null +++ b/web/sections/MagazineTags/MagazineTagBar.tsx @@ -0,0 +1,117 @@ +import { AnchorHTMLAttributes, forwardRef, useMemo } from 'react' +import { Link } from '@core/Link' +import { FormattedMessage, useIntl } from 'react-intl' +import { filter_alt } from '@equinor/eds-icons' +import { TransformableIcon } from '../../icons/TransformableIcon' +import { useRouter } from 'next/router' + +export type MagazineTagBarProps = { + tags: { id: string; title: string; key: string }[] + href: string + onClick?: (value: string) => void +} + +export type TagLink = { + id: string + key: string + label: string + active: boolean +} & AnchorHTMLAttributes + +const allTagLink = { + href: '#', + label: 'All', + active: false, +} + +const MagazineTagBar = forwardRef(function MagazineTagBar( + { tags, onClick, href }, + ref, +) { + const router = useRouter() + const { query } = router + + const formattedTags = useMemo(() => { + return tags?.map((tag) => ({ + id: tag.id, + label: tag.title, + key: tag.key, + active: query?.tag === tag.key, + })) + }, [tags, query]) + + const intl = useIntl() + allTagLink.label = intl.formatMessage({ id: 'magazine_tag_filter_all', defaultMessage: 'All categories' }) + allTagLink.active = !query + const linkClassNames = ` + inline-block + text-base + mx-5 + lg:text-xs + relative + no-underline + hover:underline + hover:underline-offset-4 + whitespace-nowrap` + + return ( +
+

+ + +

+
    +
  • + { + if (onClick) { + event.preventDefault() + onClick('ALL') + allTagLink.active = true + } + }} + > + {allTagLink.label} + +
  • + {formattedTags.map((tag: TagLink) => { + return ( +
  • + { + if (onClick) { + event.preventDefault() + onClick(tag.key) + allTagLink.active = false + } + }} + > + {tag.label} + +
  • + ) + })} +
+
+ ) +}) + +export default MagazineTagBar diff --git a/web/sections/cards/Card/Card.tsx b/web/sections/cards/Card/Card.tsx index 7a82c8968..0d0e30b0d 100644 --- a/web/sections/cards/Card/Card.tsx +++ b/web/sections/cards/Card/Card.tsx @@ -39,13 +39,13 @@ export const Card = forwardRef(function Card( shadow-card rounded-sm active:shadow-card-interact - min-w-[220px] - md:max-w-[400px]` + min-w-card-minWidth + md:max-w-card-maxWidth` const variantClassNames = { primary: `${commonStyling}`, secondary: `${commonStyling} rounded-md overflow-hidden`, - compact: `h-full flex gap-4 min-w-[200px] xl:max-w-[300px] 3xl:max-w-[400px]`, + compact: `h-full flex gap-4 min-w-[200px] xl:max-w-[300px] 3xl:max-w-card-maxWidth`, single: `grid grid-cols-[40%_1fr] min-h-[450px] shadow-card rounded-sm active:shadow-card-interact`, } const variantAspectRatio = { diff --git a/web/sections/cards/Card/CardContent.tsx b/web/sections/cards/Card/CardContent.tsx index 7bcd63ff5..f26372214 100644 --- a/web/sections/cards/Card/CardContent.tsx +++ b/web/sections/cards/Card/CardContent.tsx @@ -41,8 +41,7 @@ export const CardContent = forwardRef(function single: 'self-end mt-auto max-lg:hidden', } const iconClassNames = twMerge( - ` - size-arrow-right + `size-arrow-right text-energy-red-100 mr-2 group-hover/card:translate-x-2 diff --git a/web/sections/cards/Card/CardHeader.tsx b/web/sections/cards/Card/CardHeader.tsx index a25621e7e..5ae9e7b64 100644 --- a/web/sections/cards/Card/CardHeader.tsx +++ b/web/sections/cards/Card/CardHeader.tsx @@ -1,8 +1,8 @@ import { PortableTextBlock } from '@portabletext/types' import { Heading, Typography, TypographyVariants } from '@core/Typography' import { forwardRef, HTMLAttributes } from 'react' -import { twMerge } from 'tailwind-merge' import { Variants } from './Card' +import envisTwMerge from '../../../twMerge' export type CardHeaderProps = { /** Title string content */ @@ -15,9 +15,9 @@ export type CardHeaderProps = { titleLevel?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' /* Use together with title level to set level = as and variant different */ titleVariant?: TypographyVariants - /* Override styling on hgroup if eyebrow else merged with titleClassName */ + /* Override styling on hgroup if eyebrow */ className?: string - /* Override styling on typography element.ClassName can be used if not eyebrow */ + /* Override styling on typography element. */ titleClassName?: string /* Shows a paragraph above title in hgroup */ eyebrow?: React.ReactNode @@ -55,7 +55,7 @@ export const CardHeader = forwardRef(function C single: `text-lg leading-planetary`, } - const titleClassNames = twMerge( + const titleClassNames = envisTwMerge( `group-hover/card:underline group-focus-visible/card:underline ${variantTitle[variant]} @@ -67,8 +67,8 @@ export const CardHeader = forwardRef(function C ) return eyebrow ? ( -
-

{eyebrow}

+
+

{eyebrow}

{title && ( + +const CardSkeleton = forwardRef(function CardSkeleton( + { hideEyebrow = false, hideIngress = false }, + ref, +) { + return ( +
+
+
+ {!hideEyebrow &&
} +
+ {!hideIngress &&
} +
+
+ ) +}) + +export default CardSkeleton diff --git a/web/sections/cards/MagazineCard/MagazineCard.tsx b/web/sections/cards/MagazineCard/MagazineCard.tsx new file mode 100644 index 000000000..348208a61 --- /dev/null +++ b/web/sections/cards/MagazineCard/MagazineCard.tsx @@ -0,0 +1,47 @@ +import Card from '@sections/cards/Card' +import type { MagazineCardData } from '../../../types/index' +import { forwardRef, HTMLAttributes } from 'react' +import { twMerge } from 'tailwind-merge' + +export type MagazineCardProps = { + data: MagazineCardData +} & HTMLAttributes + +/** + * Magazine Card component. + * Remember to wrap in ul and li if in a list. + * */ +const MagazineCard = forwardRef(function MagazineCard( + { data, className = '' }, + ref, +) { + const { slug = '', title, hero, id } = data + + return ( + + +
+ +
+
+
+ ) +}) +export default MagazineCard diff --git a/web/styles/tailwind.css b/web/styles/tailwind.css index c968419f3..8ddd82499 100644 --- a/web/styles/tailwind.css +++ b/web/styles/tailwind.css @@ -4,18 +4,18 @@ @import './components/videojs.css'; @import 'tailwindcss/utilities'; -:root { - /* Image carousel card withs */ - --image-carousel-card-w-sm: 275px; - --image-carousel-card-w-md: 692px; - --image-carousel-card-w-lg: 980px; +@layer base { + :root { + /* Image carousel card withs */ + --image-carousel-card-w-sm: 275px; + --image-carousel-card-w-md: 692px; + --image-carousel-card-w-lg: 980px; - /* Modal */ - --modal-transition-duration: 0.4s; - --modal-transition-easing: cubic-bezier(0.45, 0, 0.55, 1); -} + /* Modal */ + --modal-transition-duration: 0.4s; + --modal-transition-easing: cubic-bezier(0.45, 0, 0.55, 1); + } -@layer base { p, h1, h2, @@ -180,6 +180,10 @@ .black-center-gradient { background-image: linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)); } + .black-blue-center-gradient { + background-image: linear-gradient(theme('colors.slate-blue.95 / 50%'), theme('colors.slate-blue.95 / 50%')), + linear-gradient(rgba(0, 0, 0, 0.36), rgba(0, 0, 0, 0.36)); + } .black-to-top-gradient { background-image: linear-gradient( to top, diff --git a/web/tailwind.config.cjs b/web/tailwind.config.cjs index e3d7eddd2..2f7f09c3b 100644 --- a/web/tailwind.config.cjs +++ b/web/tailwind.config.cjs @@ -22,6 +22,7 @@ module.exports = { './templates/**/*.{js,ts,tsx}', './icons/**/*.{js,ts,tsx}', './pages/**/*.{js,ts,tsx}', + './templates/**/*.{js,ts,tsx}', ], /* Now instead of dark:{class} classes being applied based on prefers-color-scheme, @@ -222,6 +223,8 @@ module.exports = { '2xl': 'calc((40 / 16) * theme(fontSize.base))', '3xl': 'calc((56 / 16) * theme(fontSize.base))', '4xl': 'calc((96 / 16) * theme(fontSize.base))', + 'card-minWidth': '220px', + 'card-maxWidth': '400px', }), fontSize: { //--typeScale-00 @@ -477,8 +480,11 @@ module.exports = { }, }, }), - transitionProperty: ['motion-safe'], + gridTemplateColumns: { + 'auto-fill-fr': `repeat(auto-fill, minmax(80px,1fr))`, + card: `repeat(auto-fill, minmax(min(100%, theme(spacing.card-minWidth)), theme(spacing.card-maxWidth)))`, + }, }, }, variants: { @@ -519,9 +525,6 @@ module.exports = { '.break-word': { wordBreak: 'break-word', }, - '.auto-fill-fr': { - gridTemplateColumns: `repeat(auto-fill, minmax(80px,1fr))`, - }, }) }), ], diff --git a/web/templates/magazine/MagazinePage.tsx b/web/templates/magazine/MagazinePage.tsx new file mode 100644 index 000000000..ec889c91e --- /dev/null +++ b/web/templates/magazine/MagazinePage.tsx @@ -0,0 +1,87 @@ +import { useRouter } from 'next/router' +import { PageContent } from '../../pageComponents/pageTemplates/shared/SharedPageContent' +import SharedTitle from '../../pageComponents/pageTemplates/shared/SharedTitle' +import { HeroTypes, MagazinePageSchema } from '../../types/index' +import { SharedBanner } from '../../pageComponents/pageTemplates/shared/SharedBanner' +import Teaser from '../../pageComponents/shared/Teaser' +import Seo from '../../pageComponents/shared/Seo' +import useSharedTitleStyles from '../../lib/hooks/useSharedTitleStyles' +import MagazineTagBar from '@sections/MagazineTags/MagazineTagBar' + +type MagazinePageProps = { + data: MagazinePageSchema +} + +const MagazinePage = ({ data }: MagazinePageProps) => { + const router = useRouter() + const parentSlug = + (router.locale !== router.defaultLocale ? `/${router.locale}` : '') + + router.asPath.substring(router.asPath.indexOf('/'), router.asPath.lastIndexOf('/')) + + const { hideFooterComponent, footerComponent, tags } = data + + const titleStyles = useSharedTitleStyles(data?.hero?.type, data?.content?.[0]) + + const handleClickTag = (tagValue: string) => { + if (tagValue === 'ALL') { + delete router.query.filter + router.push({ + pathname: parentSlug, + }) + } else { + router.push({ + pathname: parentSlug, + query: { + tag: tagValue, + }, + }) + } + } + + return ( + <> + +
+ + {data?.magazineTags && } + {data.hero.type !== HeroTypes.DEFAULT && ( + + )} + {data.hero.type !== HeroTypes.DEFAULT && ( +
+ {tags && tags?.length > 0 && ( +
    + {tags.map((tag: string) => { + return ( + + {tag} + + ) + })} +
+ )} +
+ )} + + {!hideFooterComponent && footerComponent?.data && } +
+ + ) +} + +export default MagazinePage diff --git a/web/templates/magazine/Magazineroom.tsx b/web/templates/magazine/Magazineroom.tsx new file mode 100644 index 000000000..340bd4ae8 --- /dev/null +++ b/web/templates/magazine/Magazineroom.tsx @@ -0,0 +1,155 @@ +import type { MagazineIndexPageType } from '../../types' +import { useMemo, useRef, useState } from 'react' +import Seo from '../../pageComponents/shared/Seo' +import { HeroTypes } from '../../types/index' +import { BackgroundContainer } from '@components' +import Teaser from '../../pageComponents/shared/Teaser' +import SharedTitle from '../../pageComponents/pageTemplates/shared/SharedTitle' +import { SharedBanner } from '../../pageComponents/pageTemplates/shared/SharedBanner' +import Blocks from '../../pageComponents/shared/portableText/Blocks' +import MagazineTagBar from '@sections/MagazineTags/MagazineTagBar' +import { useRouter } from 'next/router' +import { ImageBackgroundContainer } from '@components/Backgrounds/ImageBackgroundContainer' +import { Heading } from '@core/Typography' +import MagazineCard from '@sections/cards/MagazineCard/MagazineCard' +import { SimplePagination } from '@core/SimplePagination/SimplePagination' +import { Ratios } from '../../pageComponents/shared/SanityImage' +import CardSkeleton from '@sections/cards/CardSkeleton/CardSkeleton' +import { PaginationContextProvider } from '../../common/contexts/PaginationContext' + +type MagazineIndexTemplateProps = { + pageData: MagazineIndexPageType + slug?: string +} + +const chunkArray = (array: any[], chunkSize: number) => { + const chunkedArray = [] + for (let i = 0; i < array.length; i += chunkSize) { + chunkedArray.push(array.slice(i, i + chunkSize)) + } + return chunkedArray +} + +const MagazineRoom = ({ pageData, slug }: MagazineIndexTemplateProps) => { + const { ingress, title, hero, seoAndSome, magazineTags, magazineArticles, footerComponent } = pageData || {} + const resultsRef = useRef(null) + const [isLoading, setIsLoading] = useState(false) + const router = useRouter() + const parentSlug = + (router.locale !== router.defaultLocale ? `/${router.locale}` : '') + + router.asPath.substring(router.asPath.indexOf('/'), router.asPath.lastIndexOf('/')) + + const magazineList = useMemo(() => magazineArticles, [magazineArticles]) + const pagedList = useMemo(() => chunkArray(magazineList, 12), [magazineList]) + const [page, setPage] = useState(0) + + const getNext = async () => { + setIsLoading(true) + setPage(page + 1) + setIsLoading(false) + } + const getPrevious = async () => { + setIsLoading(true) + setPage(page - 1) + setIsLoading(false) + } + + const handleClickTag = (tagValue: string) => { + if (tagValue === 'ALL') { + delete router.query.filter + router.push({ + pathname: parentSlug, + }) + } else { + router.push({ + pathname: parentSlug, + query: { + tag: tagValue, + }, + }) + } + } + + return ( + <> + +
+ {hero.type !== HeroTypes.BACKGROUND_IMAGE && ( + <> + + {pageData?.hero.type !== HeroTypes.DEFAULT && title && ( + + )} + +
{ingress && }
+
+ + )} + {hero.type === HeroTypes.BACKGROUND_IMAGE && ( + <> + {hero?.figure?.image ? ( + +
+ +
{ingress && }
+
+
+ ) : ( +
+ +
{ingress && }
+
+ )} + + )} + {magazineTags && } + +
    + {isLoading && + Array.from({ length: 5 }, (_v, i) => i).map((item) => ( +
  • + +
  • + ))} + {!isLoading && + pagedList[page].map((article) => ( +
  • + +
  • + ))} +
+ {magazineList?.length > 12 && ( + + )} +
+
{footerComponent && }
+
+ + ) +} +export default MagazineRoom diff --git a/web/twMerge/index.ts b/web/twMerge/index.ts index fed643fc1..e74c56bc5 100644 --- a/web/twMerge/index.ts +++ b/web/twMerge/index.ts @@ -13,6 +13,10 @@ const envisTwMerge = extendTailwindMerge({ size: ['arrow-right'], lineHeight: ['text', 'earthy', 'misty', 'cloudy', 'planetary', 'inherit'], }, + // ↓ Add values to existing class groups or define new ones + classGroups: {}, + // ↓ Here you can define additional conflicts across class groups + conflictingClassGroups: {}, }, }) diff --git a/web/types/algoliaIndexPage.ts b/web/types/algoliaIndexPage.ts index 1aa959133..bba1e1380 100644 --- a/web/types/algoliaIndexPage.ts +++ b/web/types/algoliaIndexPage.ts @@ -9,6 +9,7 @@ import type { BackgroundColours, LinkData, MenuData, + MagazineCardData, } from './index' import { PortableTextBlock } from '@portabletext/types' import { SanityImageObject } from '@sanity/image-url/lib/types/types' @@ -54,8 +55,10 @@ export type MagazineIndexPageType = { content: PortableTextBlock[] background: BackgroundColours } + query?: any + magazineArticles: MagazineCardData[] heroImage: ImageWithCaptionData footerComponent?: TeaserData - magazineTags: string[] + magazineTags: { id: string; title: string; key: string }[] background: BackgroundColours } diff --git a/web/types/cardTypes.ts b/web/types/cardTypes.ts index 9d02fce7a..7a03a1eb5 100644 --- a/web/types/cardTypes.ts +++ b/web/types/cardTypes.ts @@ -6,6 +6,7 @@ import { HeroTypes, EventDateType, DesignOptions, + HeroType, } from './index' export type CardTypes = 'news' | 'topics' | 'people' | 'events' @@ -23,10 +24,12 @@ export type CardData = { } export type MagazineCardData = { + id?: string slug: string title: string | PortableTextBlock[] - tags?: string[] - heroImage?: ImageWithAlt + firstPublishedAt?: string + publishDateTime?: string + hero?: HeroType } export type PeopleCardData = { diff --git a/web/types/imageTypes.ts b/web/types/imageTypes.ts index 29aa039fb..06ade2a03 100644 --- a/web/types/imageTypes.ts +++ b/web/types/imageTypes.ts @@ -28,7 +28,7 @@ export type ImageBackground = { image: ImageWithAlt | SanityImageObject useAnimation?: boolean useLight?: boolean - contentAlignment: ContentAlignmentTypes + contentAlignment?: ContentAlignmentTypes } export type FullWidthImageData = { diff --git a/web/types/pageTypes.ts b/web/types/pageTypes.ts index ccc33543b..72daf0f0b 100644 --- a/web/types/pageTypes.ts +++ b/web/types/pageTypes.ts @@ -47,7 +47,8 @@ export type PageSchema = { export type TopicPageSchema = PageSchema export type MagazinePageSchema = PageSchema & { - magazineTags?: string[] + magazineTags?: { id: string; title: string; key: string }[] + tags?: string[] footerComponent?: { data?: TeaserData } diff --git a/web/types/types.ts b/web/types/types.ts index 4e33d0f30..3597b78af 100644 --- a/web/types/types.ts +++ b/web/types/types.ts @@ -54,6 +54,7 @@ export enum HeroTypes { FIFTY_FIFTY = 'fiftyFifty', FULL_WIDTH_IMAGE = 'fullWidthImage', LOOPING_VIDEO = 'loopingVideo', + BACKGROUND_IMAGE = 'backgroundImage', } export type HeroType = {