From bec328745fac80a386a8163eeb3ccf15e86e9318 Mon Sep 17 00:00:00 2001 From: Padmaja Date: Mon, 29 Jan 2024 16:47:33 +0530 Subject: [PATCH] Add Key numbers component. #2048 (#2054) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Key numbers component #2048 * Missing commit #2028 * ♻️ Remove any #2048 * ✨ horizontal scroll option for mobiles #2048 * 💄 Increased width of item #2048 * 💄 Fix padding issue and scroll offset * Remove duplicate item #2048 --- sanityv3/schemas/index.js | 4 + sanityv3/schemas/objects/keyNumberItem.tsx | 44 +++++++ sanityv3/schemas/objects/keyNumbers.tsx | 116 ++++++++++++++++++ web/lib/queries/common/keyNumbersFields.ts | 27 ++++ web/lib/queries/common/pageContentFields.ts | 4 + .../shared/SharedPageContent.tsx | 6 + web/pageComponents/shared/Carousel.tsx | 7 +- web/pageComponents/shared/ReadMoreLink.tsx | 34 +++++ web/pageComponents/shared/Teaser.tsx | 34 +---- .../topicPages/KeyNumbers/KeyNumberItem.tsx | 35 ++++++ .../topicPages/KeyNumbers/KeyNumbers.tsx | 91 ++++++++++++++ web/types/types.ts | 21 ++++ 12 files changed, 392 insertions(+), 31 deletions(-) create mode 100644 sanityv3/schemas/objects/keyNumberItem.tsx create mode 100644 sanityv3/schemas/objects/keyNumbers.tsx create mode 100644 web/lib/queries/common/keyNumbersFields.ts create mode 100644 web/pageComponents/shared/ReadMoreLink.tsx create mode 100644 web/pageComponents/topicPages/KeyNumbers/KeyNumberItem.tsx create mode 100644 web/pageComponents/topicPages/KeyNumbers/KeyNumbers.tsx diff --git a/sanityv3/schemas/index.js b/sanityv3/schemas/index.js index 9ab308ecc..31c85d120 100644 --- a/sanityv3/schemas/index.js +++ b/sanityv3/schemas/index.js @@ -85,6 +85,8 @@ import videoPlayer from './objects/videoPlayer' import videoPlayerCarousel from './objects/videoPlayerCarousel' import videoControls from './objects/videoControls' import hlsVideo from './objects/hlsVideo' +import keyNumbers from './objects/keyNumbers' +import keyNumberItem from './objects/keyNumberItem' const routeSchemas = languages.map(({ name, title }) => { return route(name, title) @@ -164,6 +166,8 @@ const RemainingSchemas = [ videoPlayerCarousel, videoControls, hlsVideo, + keyNumbers, + keyNumberItem, ] // Then we give our schema to the builder and provide the result to Sanity diff --git a/sanityv3/schemas/objects/keyNumberItem.tsx b/sanityv3/schemas/objects/keyNumberItem.tsx new file mode 100644 index 000000000..bfdca9b16 --- /dev/null +++ b/sanityv3/schemas/objects/keyNumberItem.tsx @@ -0,0 +1,44 @@ +import { Rule } from 'sanity' +import { NumberIcon } from '@sanity/icons' + +export default { + name: 'keyNumberItem', + type: 'object', + title: 'Key Number', + fields: [ + { + name: 'keyNumber', + title: 'Key Number', + type: 'number', + validation: (Rule: Rule) => Rule.required(), + }, + { + name: 'unit', + title: 'Unit', + description: 'A short abbreviated text describing the unit of the key number', + type: 'string', + }, + { + name: 'description', + title: 'Description', + type: 'string', + description: 'Short description to show below the key number', + }, + ], + + preview: { + select: { + keyNumber: 'keyNumber', + unit: 'unit', + description: 'description', + }, + prepare(selection: Record) { + const { keyNumber, unit, description } = selection + return { + title: `${keyNumber} ${unit ?? ''}`, + subtitle: description, + media: NumberIcon, + } + }, + }, +} diff --git a/sanityv3/schemas/objects/keyNumbers.tsx b/sanityv3/schemas/objects/keyNumbers.tsx new file mode 100644 index 000000000..67c72b047 --- /dev/null +++ b/sanityv3/schemas/objects/keyNumbers.tsx @@ -0,0 +1,116 @@ +import CompactBlockEditor from '../components/CompactBlockEditor' +import { configureBlockContent, configureTitleBlockContent } from '../editors' +import { PortableTextBlock, Rule } from 'sanity' +import { NumberIcon } from '@sanity/icons' +import blocksToText from '../../helpers/blocksToText' + +const titleContentType = configureTitleBlockContent() +const ingressContentType = configureBlockContent({ + h1: false, + h2: false, + h3: false, + h4: false, + attachment: false, +}) + +const disclaimerContentType = configureBlockContent({ + h1: false, + h2: false, + h3: false, + h4: false, + attachment: false, + smallText: true, +}) + +export default { + name: 'keyNumbers', + title: 'Key Numbers', + type: 'object', + fieldsets: [ + { + name: 'link', + title: 'Link', + description: 'Select either an internal link or external URL.', + }, + { + name: 'design', + title: 'Design options', + }, + ], + + fields: [ + { + title: 'Title', + name: 'title', + type: 'array', + components: { + input: CompactBlockEditor, + }, + of: [titleContentType], + validation: (Rule: Rule) => Rule.required().warning('In most cases you should add a title'), + }, + { + name: 'ingress', + title: 'Ingress', + type: 'array', + of: [ingressContentType], + }, + { + name: 'keyNumberItems', + title: 'Key Number Items', + type: 'array', + of: [{ type: 'keyNumberItem' }], + validation: (Rule: Rule) => Rule.min(2).error('Need minimum 2 key numbers'), + }, + { + name: 'useHorizontalScroll', + title: 'Use horizontal scroll', + description: + 'When this is enabled, the key numbers will use horizontal scroll if the amount of content is greater than the screen size allows. Only for mobiles.', + type: 'boolean', + initialValue: false, + }, + { + name: 'disclaimer', + title: 'Disclaimer', + type: 'array', + components: { + input: CompactBlockEditor, + }, + of: [disclaimerContentType], + }, + { + name: 'action', + title: 'Link/action', + description: 'Select the link or downloadable file for the teaser', + fieldset: 'link', + type: 'array', + of: [ + { type: 'linkSelector', title: 'Link' }, + { type: 'downloadableImage', title: 'Downloadable image' }, + { type: 'downloadableFile', title: 'Downloadable file' }, + ], + validation: (Rule: Rule) => Rule.max(1).error('Only one action is permitted'), + }, + { + title: 'Background', + description: 'Pick a colour for the background. Default is white.', + name: 'background', + type: 'colorlist', + fieldset: 'design', + }, + ], + preview: { + select: { + title: 'title', + items: 'keyNumberItems', + }, + prepare(selection: { title: PortableTextBlock[]; items: Array }) { + return { + title: blocksToText(selection.title), + subtitle: `Showing ${selection.items.length} key numbers`, + media: NumberIcon, + } + }, + }, +} diff --git a/web/lib/queries/common/keyNumbersFields.ts b/web/lib/queries/common/keyNumbersFields.ts new file mode 100644 index 000000000..c450a87a9 --- /dev/null +++ b/web/lib/queries/common/keyNumbersFields.ts @@ -0,0 +1,27 @@ +import downloadableFileFields from './actions/downloadableFileFields' +import downloadableImageFields from './actions/downloadableImageFields' +import linkSelectorFields from './actions/linkSelectorFields' +import markDefs from './blockEditorMarks' + +export const keyNumbersFields = /*groq*/ ` + "type": _type, + "id" : _key, + title, + ingress[]{..., ${markDefs}}, + disclaimer[]{..., ${markDefs}}, + useHorizontalScroll, + "designOptions": { + "background": coalesce(background.title, 'White'), + }, + "action": action[0]{ + ${linkSelectorFields}, + ${downloadableFileFields}, + ${downloadableImageFields}, + }, + "items" : keyNumberItems[]{ + "id": _key, + keyNumber, + unit, + description, + }, + ` diff --git a/web/lib/queries/common/pageContentFields.ts b/web/lib/queries/common/pageContentFields.ts index cc804cccc..4e90cff19 100644 --- a/web/lib/queries/common/pageContentFields.ts +++ b/web/lib/queries/common/pageContentFields.ts @@ -8,11 +8,15 @@ import linkSelectorFields, { linkReferenceFields } from './actions/linkSelectorF import markDefs from './blockEditorMarks' import { eventPromotionFields, futureEventsQuery, pastEventsQuery } from './eventPromotion' import { imageCarouselFields } from './imageCarouselFields' +import { keyNumbersFields } from './keyNumbersFields' import { noDrafts, sameLang } from './langAndDrafts' import promoteMagazine from './promotions/promoteMagazine' import { publishDateTimeQuery } from './publishDateTime' const pageContentFields = /* groq */ ` +_type == "keyNumbers" =>{ + ${keyNumbersFields} + }, _type == "teaser" => { "type": _type, "id": _key, diff --git a/web/pageComponents/pageTemplates/shared/SharedPageContent.tsx b/web/pageComponents/pageTemplates/shared/SharedPageContent.tsx index 30d90d0f7..158f59042 100644 --- a/web/pageComponents/pageTemplates/shared/SharedPageContent.tsx +++ b/web/pageComponents/pageTemplates/shared/SharedPageContent.tsx @@ -19,6 +19,7 @@ import ImageCarousel from '../../shared/ImageCarousel/ImageCarousel' import IframeCarousel from '../../shared/IframeCarousel/IframeCarousel' import VideoPlayer from '../../shared/VideoPlayer' import VideoPlayerCarousel from '../../shared/VideoPlayerCarousel' +import KeyNumbers from '../../topicPages/KeyNumbers/KeyNumbers' import { AnchorLinkData, TopicPageSchema, @@ -45,6 +46,7 @@ import { IframeCarouselData, VideoPlayerData, VideoPlayerCarouselData, + KeyNumbersData, } from '../../../types/types' // How could we do this for several different component types? @@ -68,6 +70,7 @@ type ComponentProps = | VideoPlayerData | VideoPlayerCarouselData | CookieDeclarationData + | KeyNumbersData type PageContentProps = { data: TopicPageSchema | MagazinePageSchema } @@ -121,6 +124,9 @@ export const PageContent = ({ data }: PageContentProps) => { return case 'videoPlayerCarousel': return + case 'keyNumbers': + return + default: return null } diff --git a/web/pageComponents/shared/Carousel.tsx b/web/pageComponents/shared/Carousel.tsx index adb5fd9e4..d781f39c0 100644 --- a/web/pageComponents/shared/Carousel.tsx +++ b/web/pageComponents/shared/Carousel.tsx @@ -120,7 +120,12 @@ export const Carousel = ({ children, scrollOffset, horizontalPadding = false, .. const handleScroll = (scrollType: string) => { const container = scrollRef.current if (container) { - const offset = scrollOffset || Math.max(0.5 * container.offsetWidth, 320) + const noOfItems = container?.childElementCount - 2 // exclude right & left arrow from children count. + const itemWidth = container?.lastElementChild?.clientWidth || 0 + const padding = (container?.scrollWidth - itemWidth * noOfItems) / (noOfItems - 1) + const calculatedOffset = itemWidth + padding / 2 + const offset = scrollOffset || calculatedOffset + container.scrollBy({ left: scrollType === 'forward' ? offset : -offset, behavior: prefersReducedMotion ? 'auto' : 'smooth', diff --git a/web/pageComponents/shared/ReadMoreLink.tsx b/web/pageComponents/shared/ReadMoreLink.tsx new file mode 100644 index 000000000..09bd583da --- /dev/null +++ b/web/pageComponents/shared/ReadMoreLink.tsx @@ -0,0 +1,34 @@ +import { LinkData } from '../../types' +import { getUrlFromAction } from '../../common/helpers' +import { getLocaleFromName } from '../../lib/localization' +import { Link } from '@components/Link' +import styled from 'styled-components' + +const StyledLink = styled(Link)` + font-size: var(--typeScale-1); +` +const ReadMoreLink = ({ action }: { action: LinkData }) => { + const { type, label, extension } = action + const url = getUrlFromAction(action) + if (!url) { + console.warn(`Missing URL on 'TeaserAction' link with type: '${type}' and label: '${label}'`) + return null + } + + if (action.type === 'internalUrl') { + const locale = getLocaleFromName(action.link?.lang) + return ( + + {action.label} + + ) + } + + return ( + + {action.label} {extension && `(${extension.toUpperCase()})`} + + ) +} + +export default ReadMoreLink diff --git a/web/pageComponents/shared/Teaser.tsx b/web/pageComponents/shared/Teaser.tsx index 1fe0865cc..90c8f296a 100644 --- a/web/pageComponents/shared/Teaser.tsx +++ b/web/pageComponents/shared/Teaser.tsx @@ -1,14 +1,12 @@ -import { Teaser as EnvisTeaser, Link, Eyebrow, BackgroundContainer, Text } from '@components' +import { Teaser as EnvisTeaser, Eyebrow, BackgroundContainer, Text } from '@components' import styled from 'styled-components' import IngressText from './portableText/IngressText' import TitleText from './portableText/TitleText' import { urlFor } from '../../common/helpers' import Img from 'next/image' import Image from './SanityImage' -import { getUrlFromAction } from '../../common/helpers/getUrlFromAction' - -import type { TeaserData, ImageWithAlt, LinkData } from '../../types/types' -import { getLocaleFromName } from '../../lib/localization' +import type { TeaserData, ImageWithAlt } from '../../types/types' +import ReadMoreLink from './ReadMoreLink' import { BlockType } from './portableText/helpers/defaultSerializers' const { Content, Media } = EnvisTeaser @@ -57,30 +55,6 @@ const TeaserImage = ({ image }: { image: ImageWithAlt }) => { ) } -const TeaserAction = ({ action }: { action: LinkData }) => { - const { type, label, extension } = action - const url = getUrlFromAction(action) - if (!url) { - console.warn(`Missing URL on 'TeaserAction' link with type: '${type}' and label: '${label}'`) - return null - } - - if (action.type === 'internalUrl') { - const locale = getLocaleFromName(action.link?.lang) - return ( - - {action.label} - - ) - } - - return ( - - {action.label} {extension && `(${extension.toUpperCase()})`} - - ) -} - const Teaser = ({ data, anchor }: TeaserProps) => { const { title, overline, text, image, action, designOptions, isBigText } = data const { background, imageSize, imagePosition } = designOptions @@ -125,7 +99,7 @@ const Teaser = ({ data, anchor }: TeaserProps) => { {text && } )} - {action && } + {action && } diff --git a/web/pageComponents/topicPages/KeyNumbers/KeyNumberItem.tsx b/web/pageComponents/topicPages/KeyNumbers/KeyNumberItem.tsx new file mode 100644 index 000000000..27d8083f7 --- /dev/null +++ b/web/pageComponents/topicPages/KeyNumbers/KeyNumberItem.tsx @@ -0,0 +1,35 @@ +import { Text } from '@components/Text' +import { KeyNumberItemData } from '../../../types' +import styled from 'styled-components' + +const NumberText = styled(Text)` + display: inline; + font-weight: var(--fontWeight-medium); +` +const Wrapper = styled.div<{ $isScrollable: boolean }>` + min-width: var(--card-maxWidth); + ${({ $isScrollable }) => $isScrollable && { padding: '0 0 0 var(--space-medium)' }}; +` + +type KeyNumberItemProps = KeyNumberItemData & { isScrollable?: boolean } +export default function ({ keyNumber, description, unit, isScrollable = false }: KeyNumberItemProps) { + return ( + + <> + + {keyNumber?.toLocaleString()}{' '} + + {unit && ( + + {unit} + + )} + + {description && ( + + {description} + + )} + + ) +} diff --git a/web/pageComponents/topicPages/KeyNumbers/KeyNumbers.tsx b/web/pageComponents/topicPages/KeyNumbers/KeyNumbers.tsx new file mode 100644 index 000000000..a4673d1bf --- /dev/null +++ b/web/pageComponents/topicPages/KeyNumbers/KeyNumbers.tsx @@ -0,0 +1,91 @@ +import { KeyNumbersData } from '../../../types' +import { BackgroundContainer } from '@components/Backgrounds' +import styled from 'styled-components' +import TitleText from '../../shared/portableText/TitleText' +import IngressText from '../../shared/portableText/IngressText' +import KeyNumberItem from './KeyNumberItem' +import ReadMoreLink from '../../../pageComponents/shared/ReadMoreLink' +import RichText from '../../shared/portableText/RichText' +import { Carousel } from '../../shared/Carousel' +import useWindowSize from '../../../lib/hooks/useWindowSize' + +const Disclaimer = styled.div` + @media (min-width: 1300px) { + margin-right: var(--layout-paddingHorizontal-large); + } + margin-bottom: var(--space-large); +` +const StyledHeading = styled(TitleText)` + padding: 0 0 var(--space-large) 0; + text-align: left; +` +const Ingress = styled.div` + @media (min-width: 1300px) { + margin-right: var(--layout-paddingHorizontal-large); + } + margin-bottom: var(--space-xLarge); +` +const StyledBackgroundContainer = styled(BackgroundContainer)` + padding: var(--space-3xLarge) var(--layout-paddingHorizontal-small); ; +` +const HorizontalWrapper = styled.div` + --card-maxWidth: 280px; + padding-bottom: var(--space-large); + + @media (min-width: 800px) { + --card-maxWidth: 400px; + } +` +const Container = styled.div` + display: grid; + gap: var(--space-large); + margin-bottom: var(--space-large); + grid-template-columns: repeat(1, 1fr); + @media (min-width: 768px) { + grid-template-columns: repeat(2, 1fr); + } + @media (min-width: 1200px) { + grid-template-columns: repeat(3, 1fr); + } +` + +type KeyNumbersProps = { + data: KeyNumbersData + anchor?: string +} +export default function ({ data, anchor }: KeyNumbersProps) { + const { title, items, designOptions, ingress, action, disclaimer, useHorizontalScroll } = data + const { width } = useWindowSize() + + const renderScroll = useHorizontalScroll && Boolean(width && width <= 800) + const Wrapper = renderScroll + ? ({ children }: { children: React.ReactNode }) => ( + + {children} + + ) + : Container + return ( + + {title && } + {ingress && ( + + + + )} + + + {items.map((item) => ( + + ))} + + + {disclaimer && ( + + + + )} + {action && } + + ) +} diff --git a/web/types/types.ts b/web/types/types.ts index 75c529956..7bf65df9a 100644 --- a/web/types/types.ts +++ b/web/types/types.ts @@ -771,3 +771,24 @@ export type IframeCarouselData = { } export type ContactFormCatalogType = 'humanRightsInformationRequest' | 'loginIssues' + +export type KeyNumberItemData = { + type: 'keyNumberItem' + id: string + keyNumber: number + description?: string + unit?: string +} +export type KeyNumbersData = { + type: 'keyNumbers' + id: string + ingress?: PortableTextBlock[] + title?: PortableTextBlock[] + disclaimer?: PortableTextBlock[] + items: KeyNumberItemData[] + useHorizontalScroll: boolean + designOptions: { + background: BackgroundColours + } + action?: LinkData +}