Skip to content

Commit ae54fa5

Browse files
authored
feat: side card list block (#283)
* feat: add side card list block
1 parent 242df50 commit ae54fa5

19 files changed

+329
-1
lines changed

.mocks/page.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,24 @@
136136
"resetPaddings": true,
137137
"mobileOrder": "reverse",
138138
"children": [
139+
{
140+
"type": "blog-side-card-list-block",
141+
"column": "right",
142+
"resetPaddings": true,
143+
"title": "Side card list",
144+
"items": [
145+
{
146+
"image": "https://storage.yandexcloud.net/cloud-www-assets/constructor/storybook/images/logotype-cube.svg",
147+
"description": "Lorem ipsum dolor sit amet lorem ipsum dolor sit amet dolor sit amet",
148+
"url": "https://example.com"
149+
},
150+
{
151+
"image": "https://storage.yandexcloud.net/cloud-www-assets/constructor/storybook/images/logotype-long.svg",
152+
"description": "Lorem ipsum dolor sit amet lorem ipsum dolor sit amet dolor sit amet",
153+
"url": "https://example.com"
154+
}
155+
]
156+
},
139157
{
140158
"type": "blog-yfm-block",
141159
"column": "right",

.mocks/utils.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,5 +119,31 @@ export const getTakeStoryArgs = () => {
119119
};
120120
};
121121

122+
export const getSideCardListStoryArgs = () => {
123+
return {
124+
title: 'Side card list',
125+
paddingBottom: 'l',
126+
paddingTop: 'l',
127+
items: [
128+
{
129+
image: {
130+
src: 'https://storage.yandexcloud.net/cloud-www-assets/constructor/storybook/images/logotype-cube.svg',
131+
alt: 'card 1',
132+
},
133+
description: 'Lorem ipsum dolor sit amet lorem ipsum dolor sit amet dolor sit amet',
134+
url: 'https://example.com',
135+
},
136+
{
137+
image: {
138+
src: 'https://storage.yandexcloud.net/cloud-www-assets/constructor/storybook/images/logotype-long.svg',
139+
alt: 'card 2',
140+
},
141+
description: 'Lorem ipsum dolor sit amet lorem ipsum dolor sit amet dolor sit amet',
142+
url: 'https://example.com',
143+
},
144+
],
145+
};
146+
};
147+
122148
export const youtubeSrc = 'https://youtu.be/0Qd3T6skprA';
123149
export const dataLensSrc = 'm2bzon9y39lck';

src/blocks/SideCardList/README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
A block component for displaying a list of partner cards.
2+
3+
## Props
4+
5+
| Prop | Type | Description |
6+
| ------------- | ---------- | ----------------------------------------------- |
7+
| className | string | Optional CSS class name |
8+
| title | string | Optional title to display above the card list |
9+
| items | SideCard[] | Array of card items |
10+
| paddingTop | string | Top padding size (xxs, xs, s, m, l, xl, xxl) |
11+
| paddingBottom | string | Bottom padding size (xxs, xs, s, m, l, xl, xxl) |
12+
| qa | string | QA attribute for testing |
13+
14+
## SideCard
15+
16+
| Prop | Type | Description |
17+
| ----------- | ------------------- | -------------------------------- |
18+
| image | MediaProps['image'] | Image media content for the card |
19+
| description | string | Text to display on the card |
20+
| url | string | Link URL for the card |
21+
22+
## Usage
23+
24+
```tsx
25+
import {SideCardList} from './SideCardList';
26+
27+
<SideCardList
28+
className="custom-class"
29+
title="Side card list title"
30+
items={[
31+
{
32+
image: {
33+
src: 'https://example.com/image1.png',
34+
alt: 'Image 1 description',
35+
},
36+
description: 'Card 1',
37+
url: 'https://example.com/card1',
38+
},
39+
{
40+
image: {
41+
src: 'https://example.com/image2.png',
42+
alt: 'Image 2 description',
43+
},
44+
description: 'Card 2',
45+
url: 'https://example.com/card1',
46+
},
47+
]}
48+
/>;
49+
```
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
@import '../../../styles/mixins';
2+
@import '../../../styles/variables';
3+
4+
$block: '.#{$namespace}side-card-list';
5+
6+
$imageWidth: 232px;
7+
8+
#{$block} {
9+
&__container {
10+
display: flex;
11+
flex-direction: column;
12+
gap: $indentSM;
13+
max-width: $imageWidth + 2 * $indentM;
14+
}
15+
16+
&__title {
17+
@include text-size(header-2);
18+
font-weight: 500;
19+
}
20+
21+
&__items {
22+
display: flex;
23+
flex-direction: column;
24+
gap: $indentXS;
25+
}
26+
27+
&__item {
28+
@include add-specificity(&) {
29+
min-height: auto;
30+
}
31+
32+
&-image {
33+
object-fit: contain;
34+
object-position: left;
35+
36+
max-height: 104px;
37+
max-width: $imageWidth;
38+
width: auto;
39+
}
40+
41+
&-media {
42+
max-width: $imageWidth;
43+
padding: 0 0 $indentXS;
44+
}
45+
46+
&-description {
47+
padding-bottom: $indentXXS;
48+
}
49+
}
50+
51+
@media (max-width: map-get($gridBreakpoints, 'lg')) {
52+
&__container {
53+
max-width: none;
54+
width: 50%;
55+
}
56+
}
57+
58+
@media (max-width: map-get($gridBreakpoints, 'sm')) {
59+
&__container {
60+
width: 100%;
61+
}
62+
}
63+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {CardBase, Media as PCMedia, YFMWrapper} from '@gravity-ui/page-constructor';
2+
3+
import {Wrapper} from '../../components/Wrapper/Wrapper';
4+
import {SideCardListProps} from '../../models/blocks';
5+
import {PaddingsDirections} from '../../models/paddings';
6+
import {block} from '../../utils/cn';
7+
8+
import './SideCardList.scss';
9+
10+
const b = block('side-card-list');
11+
12+
export const SideCardList = ({title, items, paddingTop, paddingBottom}: SideCardListProps) => {
13+
return (
14+
<Wrapper
15+
paddings={{
16+
[PaddingsDirections.top]: paddingTop,
17+
[PaddingsDirections.bottom]: paddingBottom,
18+
}}
19+
className={b('container')}
20+
>
21+
{title && <div className={b('title')}>{title}</div>}
22+
<div className={b('items')}>
23+
{items.map(({url, description, image}, index) => (
24+
<CardBase key={index} url={url} className={b('item')}>
25+
<CardBase.Content>
26+
<PCMedia
27+
className={b('item-media')}
28+
imageClassName={b('item-image')}
29+
image={image}
30+
/>
31+
{description && (
32+
<div className={b('item-description')}>
33+
<YFMWrapper
34+
content={description}
35+
modifiers={{
36+
blog: true,
37+
resetPaddings: true,
38+
}}
39+
/>
40+
</div>
41+
)}
42+
</CardBase.Content>
43+
</CardBase>
44+
))}
45+
</div>
46+
</Wrapper>
47+
);
48+
};
49+
50+
export default SideCardList;
Loading
Loading
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {Meta, Markdown} from '@storybook/blocks';
2+
3+
import {StoryTemplate} from '../../../demo/StoryTemplate.mdx';
4+
import * as SideCardListStories from './SideCardList.stories.tsx';
5+
import README from '../README.md?raw';
6+
7+
<Meta of={SideCardListStories} />
8+
<StoryTemplate>
9+
<Markdown>{README}</Markdown>
10+
</StoryTemplate>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {Block, PageConstructor} from '@gravity-ui/page-constructor';
2+
import type {Meta, StoryFn} from '@storybook/react';
3+
4+
import {blockMockData, getSideCardListStoryArgs} from '../../../../.mocks/utils';
5+
import customBlocks from '../../../constructor/blocksMap';
6+
import {PostPageContext} from '../../../contexts/PostPageContext';
7+
import {SideCardListProps} from '../../../models/blocks';
8+
import {BlockType} from '../../../models/common';
9+
import {SideCardList} from '../SideCardList';
10+
11+
export default {
12+
title: 'Blocks/SideCardList',
13+
component: SideCardList,
14+
args: {
15+
theme: 'light',
16+
},
17+
} as Meta;
18+
19+
type SideCardListModel = {
20+
type: BlockType.SideCardList;
21+
} & SideCardListProps;
22+
23+
const DefaultTemplate: StoryFn<SideCardListModel> = (args) => (
24+
<PostPageContext.Provider value={blockMockData}>
25+
<PageConstructor content={{blocks: [args] as unknown as Block[]}} custom={customBlocks} />
26+
</PostPageContext.Provider>
27+
);
28+
29+
export const Default = DefaultTemplate.bind({});
30+
31+
Default.args = {
32+
type: BlockType.SideCardList,
33+
...(getSideCardListStoryArgs() as SideCardListProps),
34+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {test} from '../../../../playwright/core/index';
2+
3+
import {Default} from './helpers';
4+
5+
test.describe('SideCardList', () => {
6+
test('render stories <Default>', async ({mount, expectScreenshot, defaultDelay}) => {
7+
await mount(<Default />);
8+
await defaultDelay();
9+
await expectScreenshot({skipTheme: 'dark'});
10+
});
11+
});

0 commit comments

Comments
 (0)