diff --git a/.storybook/stories/documentation/Sub Blocks.mdx b/.storybook/stories/documentation/Sub Blocks.mdx index 381a6eaf6..e867a93ee 100644 --- a/.storybook/stories/documentation/Sub Blocks.mdx +++ b/.storybook/stories/documentation/Sub Blocks.mdx @@ -31,3 +31,5 @@ _Cards are components that are only used together with the slider or layout:_ ## [Price Detailed(deprecated)](?path=/story/components-cards-pricedetailed--marked-list&viewMode=docs) ## [Divider](?path=/story/components-divider--docs&viewMode=docs) + +## [ImageCard](?path=/story/components-cards-imagecard--docs&viewMode=docs) diff --git a/src/blocks/CardLayout/__stories__/CardLayout.mdx b/src/blocks/CardLayout/__stories__/CardLayout.mdx index cb64fb1da..38435b627 100644 --- a/src/blocks/CardLayout/__stories__/CardLayout.mdx +++ b/src/blocks/CardLayout/__stories__/CardLayout.mdx @@ -28,4 +28,5 @@ The following blocks are currently supported: - [`BackgroundCard` — Background card](?path=/story/components-cards-backgroundcard--default&viewMode=docs) - [`PriceCard` — Price card](?path=/story/components-cards-pricecard--default&viewMode=docs) - [`LayoutItem` — Component part of `Layout` component, consists with `Media` and `Content`](?path=/story/components-cards-layoutitem--default&viewMode=docs) +- [`ImageCard` — Image card](?path=/story/components-cards-imagecard--default&viewMode=docs) diff --git a/src/blocks/CardLayout/__stories__/CardLayout.stories.tsx b/src/blocks/CardLayout/__stories__/CardLayout.stories.tsx index 2a44738fd..de2b76f29 100644 --- a/src/blocks/CardLayout/__stories__/CardLayout.stories.tsx +++ b/src/blocks/CardLayout/__stories__/CardLayout.stories.tsx @@ -55,6 +55,11 @@ const DefaultTemplate: StoryFn = (args) => ( }, ], }, + { + ...args, + title: 'Card layout with image cards', + children: createCardArray(3, data.cards.imageCard), + }, ], }} /> diff --git a/src/blocks/CardLayout/__stories__/data.json b/src/blocks/CardLayout/__stories__/data.json index 1bd2f2899..881b9e560 100644 --- a/src/blocks/CardLayout/__stories__/data.json +++ b/src/blocks/CardLayout/__stories__/data.json @@ -43,6 +43,12 @@ "Ut enim ad minim veniam exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", "Ut enim ad minim veniam exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." ] + }, + "imageCard": { + "type": "image-card", + "title": "Tell a story and build a narrative", + "text": "We are all storytellers. Stories are a powerful way to communicate ideas and share information. The right story can lead to a better understanding of a situation, make us laugh, or even inspire us to do something in the future.", + "image": "/story-assets/img_8-12_light.png" } }, "buttons": { diff --git a/src/constructor-items.ts b/src/constructor-items.ts index 715473933..566441c1e 100644 --- a/src/constructor-items.ts +++ b/src/constructor-items.ts @@ -33,6 +33,7 @@ import { BasicCard, Content, Divider, + ImageCard, LayoutItem, MediaCard, PriceCard, @@ -73,6 +74,7 @@ export const subBlockMap = { [SubBlockType.Content]: Content, [SubBlockType.Quote]: Quote, [SubBlockType.PriceCard]: PriceCard, + [SubBlockType.ImageCard]: ImageCard, }; export const navItemMap = { diff --git a/src/models/constructor-items/common.ts b/src/models/constructor-items/common.ts index b7ca07989..7ffe69821 100644 --- a/src/models/constructor-items/common.ts +++ b/src/models/constructor-items/common.ts @@ -77,6 +77,7 @@ export type ContentSize = 's' | 'l'; export type ContentTextSize = 's' | 'm' | 'l'; export type ContentTheme = 'default' | 'dark' | 'light'; export type FileLinkType = 'vertical' | 'horizontal'; +export type ImageCardMargins = 's' | 'm'; // modifiers export interface Themable { diff --git a/src/models/constructor-items/sub-blocks.ts b/src/models/constructor-items/sub-blocks.ts index 4f47ec3ea..89cbb1f5a 100644 --- a/src/models/constructor-items/sub-blocks.ts +++ b/src/models/constructor-items/sub-blocks.ts @@ -11,6 +11,7 @@ import { CardBaseProps, ContentTheme, DividerSize, + ImageCardMargins, ImageObjectProps, ImageProps, LinkProps, @@ -41,6 +42,7 @@ export enum SubBlockType { */ Card = 'card', PriceCard = 'price-card', + ImageCard = 'image-card', } export enum IconPosition { @@ -57,6 +59,11 @@ export interface IconWrapperProps { icon?: PositionedIcon; } +export enum ImageCardDirection { + Direct = 'direct', + Reverse = 'reverse', +} + export const SubBlockTypes = Object.values(SubBlockType); export interface DividerProps { @@ -180,6 +187,14 @@ export interface LayoutItemProps extends ClassNameProps, AnalyticsEventsBase { icon?: PositionedIcon; } +export interface ImageCardProps extends CardBaseProps, Pick { + image: ImageProps; + enableImageBorderRadius?: boolean; + margins?: ImageCardMargins; + direction?: ImageCardDirection; + backgroundColor?: string; +} + // sub-block models export type DividerModel = { type: SubBlockType.Divider; @@ -221,6 +236,10 @@ export type PriceCardModel = { type: SubBlockType.PriceCard; } & PriceCardProps; +export type ImageCardModel = { + type: SubBlockType.ImageCard; +} & ImageCardProps; + export type SubBlockModels = | DividerModel | QuoteModel @@ -231,6 +250,7 @@ export type SubBlockModels = | BannerCardModel | BasicCardModel | PriceCardModel - | LayoutItemModel; + | LayoutItemModel + | ImageCardModel; export type SubBlock = SubBlockModels; diff --git a/src/schema/constants.ts b/src/schema/constants.ts index 1d413b573..4397380c8 100644 --- a/src/schema/constants.ts +++ b/src/schema/constants.ts @@ -26,6 +26,7 @@ import { BackgroundCard, BasicCard, Divider, + ImageCard, MediaCardBlock, PriceCardBlock, PriceDetailedBlock, @@ -63,6 +64,7 @@ export const cardSchemas = { ...Quote, ...BasicCard, ...PriceCardBlock, + ...ImageCard, }; export const constructorBlockSchemaNames = [ @@ -102,4 +104,5 @@ export const constructorCardSchemaNames = [ 'basic-card', 'layout-item', 'price-card', + 'image-card', ]; diff --git a/src/schema/validators/sub-blocks.ts b/src/schema/validators/sub-blocks.ts index d9ac4d521..b9177959e 100644 --- a/src/schema/validators/sub-blocks.ts +++ b/src/schema/validators/sub-blocks.ts @@ -8,3 +8,4 @@ export * from '../../sub-blocks/Divider/schema'; export * from '../../sub-blocks/BasicCard/schema'; export * from '../../sub-blocks/PriceCard/schema'; export * from '../../sub-blocks/HubspotForm/schema'; +export * from '../../sub-blocks/ImageCard/schema'; diff --git a/src/sub-blocks/ImageCard/ImageCard.scss b/src/sub-blocks/ImageCard/ImageCard.scss new file mode 100644 index 000000000..39494eac1 --- /dev/null +++ b/src/sub-blocks/ImageCard/ImageCard.scss @@ -0,0 +1,71 @@ +@import '../../../styles/variables.scss'; +@import '../../../styles/mixins'; + +$block: '.#{$ns}image-card'; + +#{$block} { + @include card(); + min-height: 1px; + $image: #{&}__image; + $content: #{&}__content; + + #{$content} { + padding: $indentM; + } + + #{$image} { + &_inner { + width: 100%; + display: block; + + &_radius { + border-radius: $borderRadius; + } + } + + &_margins { + &_s { + padding: 4px; + + #{$image}_inner { + border-radius: calc(#{$borderRadius} - #{$imagePadding}); + } + } + + &_m { + padding: $indentM; + + #{$image}_inner { + border-radius: initial; + } + } + } + } + + &_with-content { + display: flex; + flex-direction: column; + + &#{$block}_direction_direct { + #{$image} { + padding-bottom: 0; + } + + #{$content} { + padding-top: $indentSM; + } + } + + &#{$block}_direction_reverse { + flex-direction: column-reverse; + + #{$image} { + padding-top: 0; + } + + #{$content} { + padding-bottom: $indentSM; + } + } + } +} diff --git a/src/sub-blocks/ImageCard/ImageCard.tsx b/src/sub-blocks/ImageCard/ImageCard.tsx new file mode 100644 index 000000000..4461f6d33 --- /dev/null +++ b/src/sub-blocks/ImageCard/ImageCard.tsx @@ -0,0 +1,48 @@ +import React from 'react'; + +import {Image} from '../../components'; +import {getMediaImage} from '../../components/Media/Image/utils'; +import {ImageCardDirection, ImageCardProps} from '../../models'; +import {block} from '../../utils'; +import Content from '../Content/Content'; + +import './ImageCard.scss'; + +const b = block('image-card'); + +const ImageCard = (props: ImageCardProps) => { + const { + border = 'shadow', + title, + text, + image, + enableImageBorderRadius = false, + direction = ImageCardDirection.Direct, + margins, + backgroundColor, + } = props; + + const hasContent = Boolean(text || title); + const imageProps = getMediaImage(image); + + return ( +
+
+ +
+ {hasContent && ( +
+ +
+ )} +
+ ); +}; + +export default ImageCard; diff --git a/src/sub-blocks/ImageCard/__stories__/ImageCard.mdx b/src/sub-blocks/ImageCard/__stories__/ImageCard.mdx new file mode 100644 index 000000000..eb9033b9e --- /dev/null +++ b/src/sub-blocks/ImageCard/__stories__/ImageCard.mdx @@ -0,0 +1,28 @@ +import {Meta} from '@storybook/blocks'; + +import {StoryTemplate} from '../../../demo/StoryTemplate.mdx'; +import * as ImageCardStories from './ImageCard.stories.tsx'; + + + +## Parameters + +`type: "image-card"` + +[`image: string | ImageObjectProps | ReactNode` — ImageProps](?path=/docs/documentation-types--docs#imageobjectprops---image-property). + +`title?: Title | string` — Card title. + +`text?: string` — Card description (with YFM support). + +`border: 'shadow' | 'line' | 'none'` — Select border of the card. + +`backgroundColor?: string` — Card background color. + +`margins?: 's' | 'm'` — Space between the image and the card borders. + +`direction?: 'direct' | 'reverse'` — Image and Content direction. + +`enableImageBorderRadius?: boolean` — Set border-radius for the image. Affects only when `margins='none'`. + + diff --git a/src/sub-blocks/ImageCard/__stories__/ImageCard.stories.tsx b/src/sub-blocks/ImageCard/__stories__/ImageCard.stories.tsx new file mode 100644 index 000000000..d394a9f27 --- /dev/null +++ b/src/sub-blocks/ImageCard/__stories__/ImageCard.stories.tsx @@ -0,0 +1,101 @@ +import React from 'react'; + +import {Meta, StoryFn} from '@storybook/react'; + +import {ImageCardProps} from '../../../models'; +import ImageCard from '../ImageCard'; + +import data from './data.json'; + +export default { + component: ImageCard, + title: 'Components/Cards/ImageCard', + args: data.default.content, + argTypes: { + margins: { + control: {type: 'radio'}, + options: [undefined, 's', 'm'], + }, + backgroundColor: { + control: {type: 'color'}, + }, + }, +} as Meta; + +const DefaultTemplate: StoryFn = (args) => ( +
+ +
+); + +const MultipleTemplate: StoryFn = (args) => ( +
+
+ )} /> +
+
+ )} /> +
+
+ )} /> +
+
+); + +const ContentTemplate: StoryFn = (args) => ( +
+
+ +
+
+ +
+
+ +
+
+); + +const MultipleContentTemplate: StoryFn = (args) => ( +
+ )} /> + )} /> + )} /> +
+); + +const BorderTemplate: StoryFn = (args) => ( +
+
+ )} /> +
+
+ )} /> +
+
+ )} /> +
+
+); + +const BorderRadiusTemplate: StoryFn = (args) => ( +
+

Default

+ +

enableImageBorderRadius: true

+ +
+); + +export const Default = DefaultTemplate.bind({}); +export const Margins = MultipleTemplate.bind({}); +export const DirectionReverse = MultipleTemplate.bind({}); +export const Content = MultipleContentTemplate.bind({}); +export const BackgroundColor = MultipleTemplate.bind({}); +export const Border = BorderTemplate.bind({}); +export const BorderRadius = BorderRadiusTemplate.bind({}); + +DirectionReverse.args = {direction: 'reverse'} as Partial; +BackgroundColor.args = {...data.backgroundColor.content}; diff --git a/src/sub-blocks/ImageCard/__stories__/data.json b/src/sub-blocks/ImageCard/__stories__/data.json new file mode 100644 index 000000000..f3f61019c --- /dev/null +++ b/src/sub-blocks/ImageCard/__stories__/data.json @@ -0,0 +1,48 @@ +{ + "default": { + "content": { + "title": "Tell a story and build a narrative", + "text": "We are all storytellers. Stories are a powerful way to communicate ideas and share information. The right story can lead to a better understanding of a situation, make us laugh, or even inspire us to do something in the future.", + "image": "/story-assets/img_8-12_light.png", + "direction": "direct", + "border": "shadow" + } + }, + "margins": { + "none": { + "title": "Default" + }, + "small": { + "margins": "s", + "title": "margins: 's'" + }, + "medium": { + "margins": "m", + "title": "margins: 'm'" + } + }, + "direction": { + "content": { + "direction": "reverse" + } + }, + "border": { + "shadow": { + "border": "shadow (default)", + "title": "border: 'shadow'" + }, + "line": { + "border": "line", + "title": "border: 'line'" + }, + "none": { + "border": "none", + "title": "border: 'none'" + } + }, + "backgroundColor": { + "content": { + "backgroundColor": "#ccf0d2" + } + } +} diff --git a/src/sub-blocks/ImageCard/schema.ts b/src/sub-blocks/ImageCard/schema.ts new file mode 100644 index 000000000..285a50835 --- /dev/null +++ b/src/sub-blocks/ImageCard/schema.ts @@ -0,0 +1,31 @@ +import pick from 'lodash/pick'; + +import {BaseProps, CardBase} from '../../schema/validators/common'; +import {ImageProps} from '../../schema/validators/components'; +import {ContentBase} from '../Content/schema'; + +const ImageCardBlockContentProps = pick(ContentBase, ['title', 'text']); + +export const ImageCard = { + 'image-card': { + additionalProperties: false, + required: ['image'], + properties: { + ...BaseProps, + ...CardBase, + ...ImageCardBlockContentProps, + image: ImageProps, + direction: { + type: 'string', + enum: ['direct', 'reverse'], + }, + margins: { + type: 'string', + enum: ['s', 'm'], + }, + backgroundColor: { + type: 'string', + }, + }, + }, +}; diff --git a/src/sub-blocks/index.ts b/src/sub-blocks/index.ts index eae436465..75e84e95b 100644 --- a/src/sub-blocks/index.ts +++ b/src/sub-blocks/index.ts @@ -9,3 +9,4 @@ export {default as BasicCard} from './BasicCard/BasicCard'; export {default as Content} from './Content/Content'; export {default as HubspotForm} from './HubspotForm'; export {default as PriceCard} from './PriceCard/PriceCard'; +export {default as ImageCard} from './ImageCard/ImageCard';