Skip to content
Merged
38 changes: 15 additions & 23 deletions src/blocks/CardLayout/CardLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,37 @@
import React from 'react';

import {block} from '../../utils';
import {CardLayoutBlockProps as CardLayoutBlockParams} from '../../models';
import {Row, Col} from '../../grid';
import {CardLayoutBlockProps as CardLayoutBlockParams, WithChildren} from '../../models';
import {Col, GridColumnSizesType, Row} from '../../grid';
import {BlockHeader, AnimateBlock} from '../../components';

import './CardLayout.scss';

export interface CardLayoutBlockProps extends Omit<CardLayoutBlockParams, 'children'> {
children?: React.ReactNode;
}

const b = block('card-layout-block');

const DEFAULT_SIZES = {
const DEFAULT_SIZES: GridColumnSizesType = {
all: 12,
sm: 6,
md: 4,
};
export type CardLayoutBlockProps = WithChildren<Omit<CardLayoutBlockParams, 'children'>>;

const b = block('card-layout-block');

const CardLayout = ({
const CardLayout: React.FC<CardLayoutBlockProps> = ({
title,
description,
animated,
colSizes = DEFAULT_SIZES,
children,
}: CardLayoutBlockProps) => (
}) => (
<AnimateBlock className={b()} animate={animated}>
<BlockHeader title={title} description={description} />
<div>
<Row>
{children &&
React.Children.map(children, (child, i) => {
return (
<Col sizes={colSizes} key={i} className={b('item')}>
{child}
</Col>
);
})}
</Row>
</div>
<Row>
{React.Children.map(children, (child, index) => (
<Col key={index} sizes={colSizes} className={b('item')}>
{child}
</Col>
))}
</Row>
</AnimateBlock>
);

Expand Down
6 changes: 5 additions & 1 deletion src/blocks/CardLayout/__stories__/CardLayout.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import CardLayout from '../CardLayout';
import {
CardLayoutBlockModel,
CardLayoutBlockProps,
CardWithImageModel,
CardWithImageProps,
SubBlockModels,
} from '../../../models';
Expand All @@ -17,7 +18,10 @@ export default {
component: CardLayout,
} as Meta;

const createCardArray = (count: number, card: CardWithImageProps) => new Array(count).fill(card);
const createCardArray: (count: number, shared: Partial<CardWithImageProps>) => SubBlockModels[] = (
count,
shared,
) => Array.from({length: count}, () => ({...shared} as CardWithImageModel));

const DefaultTemplate: Story<CardLayoutBlockModel> = (args) => (
<PageConstructor content={{blocks: [args]}} />
Expand Down
26 changes: 26 additions & 0 deletions src/blocks/FilterBlock/FilterBlock.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@import '../../../styles/variables';
@import '../../../styles/mixins';

$block: '.#{$ns}filter-block';
$innerBlock: '.#{$ns}block-base';

#{$block} {
&__title {
margin-bottom: $indentSM;

@include centerable-title();
}

&__tabs {
margin-bottom: 0;

@include tab-panel();
}

.row &__block-container.row {
margin: 0px;
}

--pc-first-block-indent: 0;
--pc-first-block-mobile-indent: 0;
}
85 changes: 85 additions & 0 deletions src/blocks/FilterBlock/FilterBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React, {useMemo, useState} from 'react';

import i18n from './i18n';
import {block} from '../../utils';
import {BlockType, ConstructorItem, FilterBlockProps, FilterItem} from '../../models';
import {Row, Col} from '../../grid';
import {BlockHeader, AnimateBlock} from '../../components';
import ButtonTabs, {ButtonTabsItemProps} from '../../components/ButtonTabs/ButtonTabs';
import {ConstructorBlocks} from '../../containers/PageConstructor/components/ConstructorBlocks';

import './FilterBlock.scss';

const b = block('filter-block');

const FilterBlock: React.FC<FilterBlockProps> = ({
title,
description,
tags,
tagButtonSize,
allTag,
items,
colSizes,
centered,
animated,
}) => {
const tabButtons = useMemo(() => {
const allButton: ButtonTabsItemProps | undefined = allTag
? {id: null, title: typeof allTag === 'boolean' ? i18n('label-all-tag') : allTag}
: undefined;
const otherButtons: ButtonTabsItemProps[] | undefined =
tags && tags.map((tag) => ({id: tag.id, title: tag.label}));
return [...(allButton ? [allButton] : []), ...(otherButtons ? otherButtons : [])];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allButton ?? : []

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately cannot change this line as suggested, because we merge here two arrays to omit nulls in final array. Replacing ternary operator with the ?? operator because in this case type of result wont be an array.

}, [allTag, tags]);

const [selectedTag, setSelectedTag] = useState(tabButtons.length ? tabButtons[0].id : null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we return null if there is no tabButtons to avoid checking it's length below?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we can, but it will make the body of the memo more complex, I believe it's better to leave it as is.


const actualTag: string | null = useMemo(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't catch why do we need this variable? I think selectedTag is enough, isn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's assume that we have 3 ids, 'A', 'B', 'C'. ` We change the value of selected tag to 'C', then remove 'C' from our id list. In this case we should choose a new id to display. The actualTag is this new selected value, or the selectedTag if it still exists.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But how can we reproduce this situation? We don't have multiple tags choice or an opportunity to let no tags be selected

return tabButtons.length && !tabButtons.find((tab) => tab.id === selectedTag)
? tabButtons[0].id
: selectedTag;
}, [tabButtons, selectedTag]);

const container: ConstructorItem[] = useMemo(() => {
const itemsToShow: FilterItem[] = actualTag
? items.filter((item) => item.tags.includes(actualTag))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be easier to create object with tags as keys once at the start?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each item can have several tag values. In this case we should add a card object to array of several keys - this procedure will be more complex in code, and I'm not sure that the performance will change dramatically to make so.

: items;
return [
{
type: BlockType.CardLayoutBlock,
title: '',
colSizes: colSizes,
children: itemsToShow.map((item) => item.card),
},
];
}, [actualTag, items, colSizes]);

return (
<AnimateBlock className={b()} animate={animated}>
{title && (
<BlockHeader
className={b('title', {centered: centered})}
title={title}
description={description}
/>
)}
{tabButtons.length && (
<Row>
<Col>
<ButtonTabs
className={b('tabs', {centered: centered})}
items={tabButtons}
activeTab={selectedTag}
onSelectTab={setSelectedTag}
tabSize={tagButtonSize}
/>
</Col>
</Row>
)}
<Row className={b('block-container')}>
<ConstructorBlocks items={container} />
</Row>
</AnimateBlock>
);
};
export default FilterBlock;
19 changes: 19 additions & 0 deletions src/blocks/FilterBlock/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
`type: "filter-block"`

`title?: Title | string` — Title.

`description?: string` — Text.

`filterTags: []` - Tags by which content can be filtered.

`tagButtonSize: 's' | 'm' | 'l' | 'xl'` - Size of filter tags.

`allTag: boolean | string` - Specifies whether to show the 'All' tag. If the value is a non-falsy string, the block uses the value as label for the `All` tag.

`items:` — Items, the block displays.

- `tags: string[]` - tags assigned to the card.

- `card: SubBlock` - card to show.

`centered?: boolean` - Specifies whether the header and the tab panel are centered.
56 changes: 56 additions & 0 deletions src/blocks/FilterBlock/__stories__/FilterBlock.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';
import {Meta, Story} from '@storybook/react/types-6-0';

import FilterBlock from '../FilterBlock';
import {
FilterBlockModel,
CardWithImageProps,
FilterBlockProps,
FilterItem,
FilterTag,
} from '../../../models';
import {CardWithImageModel} from '../../../models/constructor-items/sub-blocks';
import {PageConstructor} from '../../../containers/PageConstructor';

import data from './data.json';

export default {
title: 'Blocks/Filter Block',
component: FilterBlock,
} as Meta;

const createItemList: (
count: number,
shared: CardWithImageProps,
tagList: FilterTag[],
) => FilterItem[] = (count, shared, tagList) =>
Array.from({length: count}, (_, index) => ({
tags: [tagList[index % tagList.length].id],
card: {
...shared,
title: shared.title ? `${shared.title}&nbsp;${index + 1}` : `${index + 1}`,
} as CardWithImageModel,
}));

const createArgs = (overrides: Partial<FilterBlockProps>) =>
({
type: 'filter-block',
title: data.default.content.title,
description: data.default.content.description,
tags: data.default.filters,
items: createItemList(6, data.default.card, data.default.filters),
...overrides,
} as FilterBlockProps);

const DefaultTemplate: Story<FilterBlockModel> = (args) => (
<PageConstructor content={{blocks: [args]}} />
);

export const Default = DefaultTemplate.bind({});
Default.args = createArgs({allTag: false});

export const WithDefaultAllTag = DefaultTemplate.bind({});
WithDefaultAllTag.args = createArgs({allTag: true});

export const WithCustomAllTag = DefaultTemplate.bind({});
WithCustomAllTag.args = createArgs({allTag: 'Custom All Tag Label'});
29 changes: 29 additions & 0 deletions src/blocks/FilterBlock/__stories__/data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"default": {
"card": {
"type": "card-with-image",
"image": "https://storage.yandexcloud.net/cloud-www-assets/constructor/storybook/images/img-mini_4-12_light.png",
"title": "Lorem&nbsp;ipsum",
"description": "Dolor sit amet"
},
"content": {
"type": "card-layout-block",
"title": "Card Layout",
"description": "Three cards in a row on the desktop, two cards in a row on a tablet, one card in a row on a mobile phone."
},
"filters": [
{
"id": "one",
"label": "First very long label"
},
{
"id": "two",
"label": "Second very long label"
},
{
"id": "three",
"label": "Third very long label"
}
]
}
}
3 changes: 3 additions & 0 deletions src/blocks/FilterBlock/i18n/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"label-all-tag": "All"
}
7 changes: 7 additions & 0 deletions src/blocks/FilterBlock/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {registerKeyset} from '../../../utils/registerKeyset';
import en from './en.json';
import ru from './ru.json';

const COMPONENT = 'FilterBlock';

export default registerKeyset({en, ru}, COMPONENT);
3 changes: 3 additions & 0 deletions src/blocks/FilterBlock/i18n/ru.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"label-all-tag": "Все"
}
56 changes: 56 additions & 0 deletions src/blocks/FilterBlock/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
AnimatableProps,
BlockBaseProps,
BlockHeaderProps,
containerSizesObject,
} from '../../schema/validators/common';
import {filteredArray} from '../../schema/validators/utils';

export const FilterTagProps = {
additionalProperties: false,
required: ['id', 'label'],
properties: {
id: {
type: 'string',
},
label: {
type: 'string',
},
},
};

export const FilterItemProps = {
additionalProperties: false,
required: ['tags', 'card'],
properties: {
tags: {
type: 'array',
items: {
type: 'string',
},
},
card: {$ref: 'self#/definitions/card'},
},
};

export const FilterProps = {
additionalProperties: false,
required: ['filterTags', 'block'],
properties: {
...BlockBaseProps,
...AnimatableProps,
...BlockHeaderProps,
allTag: {oneOf: [{type: 'boolean'}, {type: 'string'}]},
colSizes: containerSizesObject,
tags: filteredArray(FilterTagProps),
items: filteredArray(FilterItemProps),
tagButtonSize: {
type: 'string',
enum: ['s', 'm', 'l', 'xl'],
},
},
};

export const FilterBlock = {
'filterable-block': FilterProps,
};
Loading