Skip to content

Commit 33901f0

Browse files
committed
feat(PageFeatures): Allow the PageFeatures component to render in a grid
1 parent 9be9fff commit 33901f0

File tree

5 files changed

+185
-59
lines changed

5 files changed

+185
-59
lines changed

packages/gamut-labs/src/landingPage/Feature.tsx

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -36,38 +36,60 @@ const FeatureBlock = styled.div`
3636
}
3737
`;
3838

39-
type FeaturedMediaProps = {
40-
featuresMedia?: 'image' | 'icon' | 'stat' | 'none';
39+
type FeaturedImageProps = {
40+
featuresMedia: 'image';
4141
imgSrc: string;
42-
imgAlt?: string;
43-
statText?: string;
42+
imgAlt: string;
4443
};
4544

46-
const FeaturedMedia: React.FC<FeaturedMediaProps> = ({
47-
featuresMedia = 'none',
48-
...rest
49-
}) => {
50-
if (featuresMedia === 'image') {
45+
type FeaturedIconProps = {
46+
featuresMedia: 'icon';
47+
imgSrc: string;
48+
imgAlt: string;
49+
};
50+
51+
type FeaturedStatProps = {
52+
featuresMedia: 'stat';
53+
statText: string;
54+
};
55+
56+
type FeaturedNoMediaProps = {
57+
featuresMedia: 'none';
58+
};
59+
60+
type FeaturedMediaProps =
61+
| FeaturedImageProps
62+
| FeaturedIconProps
63+
| FeaturedStatProps
64+
| FeaturedNoMediaProps;
65+
66+
const FeaturedMedia: React.FC<FeaturedMediaProps> = (props) => {
67+
if (props.featuresMedia === 'image') {
5168
return (
52-
<Image src={rest.imgSrc} alt={rest.imgAlt} data-testid="feature-image" />
69+
<Image
70+
src={props.imgSrc}
71+
alt={props.imgAlt}
72+
data-testid="feature-image"
73+
/>
5374
);
5475
}
5576

56-
if (featuresMedia === 'icon') {
77+
if (props.featuresMedia === 'icon') {
5778
return (
58-
<Icon src={rest.imgSrc} alt={rest.imgAlt} data-testid="feature-icon" />
79+
<Icon src={props.imgSrc} alt={props.imgAlt} data-testid="feature-icon" />
5980
);
6081
}
6182

62-
if (featuresMedia === 'stat') {
83+
if (props.featuresMedia === 'stat') {
6384
return (
6485
<Text
6586
as="div"
6687
marginTop={48}
6788
fontSize={{ xs: 44, lg: 64 }}
89+
fontWeight="title"
6890
data-testid="feature-stat"
6991
>
70-
{rest.statText}
92+
{props.statText}
7193
</Text>
7294
);
7395
}
@@ -82,24 +104,16 @@ export type FeatureProps = Pick<
82104
FeaturedMediaProps;
83105

84106
export const Feature: React.FC<FeatureProps> = ({
85-
featuresMedia,
86-
imgSrc,
87-
imgAlt = '',
88-
statText,
89107
title,
90108
desc,
91109
onAnchorClick,
92110
testId,
111+
...featuredMediaProps
93112
}) => (
94113
<FeatureBlock data-testid={testId}>
95-
<FeaturedMedia
96-
featuresMedia={featuresMedia}
97-
imgSrc={imgSrc}
98-
imgAlt={imgAlt}
99-
statText={statText}
100-
/>
114+
<FeaturedMedia {...featuredMediaProps} />
101115
{title && (
102-
<Text as="h3" fontSize={{ xs: 22, lg: 26 }}>
116+
<Text as="h3" fontSize={{ xs: 22, lg: 26 }} fontWeight="title">
103117
{title}
104118
</Text>
105119
)}

packages/gamut-labs/src/landingPage/PageFeatures.tsx

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
import { Container } from '@codecademy/gamut';
1+
import {
2+
Column,
3+
ColumnSizes,
4+
Container,
5+
LayoutGrid,
6+
ResponsiveProperty,
7+
} from '@codecademy/gamut';
28
import { mediaQueries } from '@codecademy/gamut-styles';
39
import styled from '@emotion/styled';
4-
import React from 'react';
10+
import React, { ReactNode } from 'react';
511

612
import { CTA, Description, Feature, FeatureProps, Title } from './';
713
import { BaseProps } from './types';
@@ -13,6 +19,8 @@ const FlexContainer = styled(Container)`
1319
`;
1420

1521
export type PageFeaturesProps = BaseProps & {
22+
maxCols?: 1 | 2 | 3 | 4;
23+
1624
/**
1725
* Array of features, which consist of image, image alt, title, and description
1826
*/
@@ -29,10 +37,56 @@ export type PageFeaturesProps = BaseProps & {
2937
featuresMedia?: 'image' | 'icon' | 'stat' | 'none';
3038
};
3139

40+
const rowRenderEach = (
41+
items: FeatureProps[],
42+
itemRenderer: (item: FeatureProps) => ReactNode
43+
): ReactNode => (
44+
<FlexContainer nowrap column>
45+
{items.map(itemRenderer)}
46+
</FlexContainer>
47+
);
48+
49+
const gridRenderEach = (
50+
maxCols: NonNullable<PageFeaturesProps['maxCols']>,
51+
items: FeatureProps[],
52+
itemRenderer: (item: FeatureProps) => ReactNode
53+
): ReactNode => {
54+
const size = { xs: 12, sm: 12 / maxCols } as ResponsiveProperty<ColumnSizes>;
55+
/* eslint-disable react/no-array-index-key */
56+
return (
57+
<LayoutGrid
58+
columnGap={{ lg: 'lg', xs: 'sm' }}
59+
rowGap={{ lg: 'lg', xs: 'sm' }}
60+
>
61+
{items.map((item, i) => (
62+
<Column key={i} size={size}>
63+
{itemRenderer(item)}
64+
</Column>
65+
))}
66+
</LayoutGrid>
67+
);
68+
/* eslint-enable react/no-array-index-key */
69+
};
70+
71+
const renderEach = (
72+
maxCols: PageFeaturesProps['maxCols'],
73+
items: FeatureProps[],
74+
itemRenderer: (item: FeatureProps) => ReactNode
75+
): ReactNode => {
76+
if (maxCols === undefined) {
77+
return rowRenderEach(items, itemRenderer);
78+
}
79+
if (maxCols > 0 && maxCols <= 4) {
80+
return gridRenderEach(maxCols, items, itemRenderer);
81+
}
82+
return null;
83+
};
84+
3285
export const PageFeatures: React.FC<PageFeaturesProps> = ({
3386
title,
3487
desc,
3588
cta,
89+
maxCols,
3690
features,
3791
featuresMedia,
3892
isIcon,
@@ -49,17 +103,20 @@ export const PageFeatures: React.FC<PageFeaturesProps> = ({
49103
</CTA>
50104
)}
51105
</div>
52-
<FlexContainer nowrap column>
53-
{features.map((feature) => (
54-
<Feature
55-
key={feature.title}
56-
{...feature}
57-
featuresMedia={
58-
featuresMedia ? featuresMedia : isIcon ? 'icon' : 'image'
59-
}
60-
onAnchorClick={onAnchorClick}
61-
/>
62-
))}
63-
</FlexContainer>
106+
{renderEach(
107+
maxCols,
108+
features.map((feature) => ({
109+
...feature,
110+
featuresMedia: featuresMedia
111+
? featuresMedia
112+
: isIcon
113+
? 'icon'
114+
: 'image',
115+
onAnchorClick,
116+
})) as FeatureProps[],
117+
(feature) => (
118+
<Feature key={feature.title} {...feature} />
119+
)
120+
)}
64121
</div>
65122
);

packages/gamut-labs/src/landingPage/__tests__/Feature-test.tsx

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,38 @@ import React from 'react';
33

44
import { Feature, FeatureProps } from '..';
55

6-
const renderComponent = (overrides: Partial<FeatureProps> = {}) => {
7-
const props: FeatureProps = {
8-
featuresMedia: 'image',
9-
imgSrc: 'https://content.codecademy.com/courses/free/boba.svg',
10-
imgAlt: 'Codey Boba Tea',
11-
...overrides,
12-
};
13-
14-
return mount(<Feature {...props} />);
15-
};
6+
const renderComponent = (props: FeatureProps) => mount(<Feature {...props} />);
167

178
describe('Feature', () => {
189
it('renders a title when title prop is provided', () => {
19-
const wrapper = renderComponent({ title: 'Test Title' });
10+
const wrapper = renderComponent({
11+
featuresMedia: 'none',
12+
title: 'Test Title',
13+
});
2014
expect(wrapper.find('h3').text()).toEqual('Test Title');
2115
});
2216

2317
it('does not render a title when title prop is not provided', () => {
24-
const wrapper = renderComponent();
18+
const wrapper = renderComponent({
19+
featuresMedia: 'none',
20+
desc: 'Test Description',
21+
});
2522
expect(wrapper.find('h3')).toHaveLength(0);
2623
});
2724

2825
it('renders a description when desc prop is provided', () => {
29-
const wrapper = renderComponent({ desc: 'Test Description' });
26+
const wrapper = renderComponent({
27+
featuresMedia: 'none',
28+
desc: 'Test Description',
29+
});
3030
expect(wrapper.find('p').text()).toEqual('Test Description');
3131
});
3232

3333
it('does not render a description when desc prop is not provided', () => {
34-
const wrapper = renderComponent();
34+
const wrapper = renderComponent({
35+
featuresMedia: 'none',
36+
title: 'Test Title',
37+
});
3538
expect(wrapper.find('p')).toHaveLength(0);
3639
});
3740

@@ -43,14 +46,22 @@ describe('Feature', () => {
4346
});
4447

4548
it('renders an image', () => {
46-
const wrapper = renderComponent();
49+
const wrapper = renderComponent({
50+
featuresMedia: 'image',
51+
imgSrc: 'https://content.codecademy.com/courses/free/boba.svg',
52+
imgAlt: 'Codey Boba Tea',
53+
});
4754
expect(wrapper.find('img[data-testid="feature-image"]')).toHaveLength(1);
4855
expect(wrapper.find('img[data-testid="feature-icon"]')).toHaveLength(0);
4956
expect(wrapper.find('div[data-testid="feature-stat"]')).toHaveLength(0);
5057
});
5158

5259
it('renders an icon', () => {
53-
const wrapper = renderComponent({ featuresMedia: 'icon' });
60+
const wrapper = renderComponent({
61+
featuresMedia: 'icon',
62+
imgSrc: 'https://content.codecademy.com/courses/free/boba.svg',
63+
imgAlt: 'Codey Boba Tea',
64+
});
5465
expect(wrapper.find('img[data-testid="feature-image"]')).toHaveLength(0);
5566
expect(wrapper.find('img[data-testid="feature-icon"]')).toHaveLength(1);
5667
expect(wrapper.find('div[data-testid="feature-stat"]')).toHaveLength(0);

packages/gamut-labs/src/landingPage/__tests__/FeaturesSection-test.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,10 @@ describe('PageFeatures', () => {
6666
const wrapper = renderComponent({
6767
features: [
6868
{
69-
imgSrc: 'https://content.codecademy.com/courses/free/boba.svg',
70-
imgAlt: 'Codey boba tea',
7169
title: 'Software Engineer',
7270
desc: '**Software Engineer**. Example link [here](#).',
7371
},
7472
{
75-
imgSrc: 'https://content.codecademy.com/courses/free/boba.svg',
76-
imgAlt: 'Codey boba tea',
7773
title: 'Data Scientist',
7874
desc: '**Data Scientist**. Example link [here](#).',
7975
},

packages/styleguide/stories/Brand/Organisms/PageFeatures.stories.mdx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,51 @@ Promote a single stat by setting the `featuresMedia` prop to `stat` and providin
136136
{(args) => <PageFeatures {...args} />}
137137
</Story>
138138
</Canvas>
139+
140+
## Wrap the features to create a grid
141+
142+
If you specify a `maxCols` the features at desktop will wrap at that number of columns
143+
144+
<Canvas>
145+
<Story
146+
name="Features Grid"
147+
args={{
148+
features: [
149+
{
150+
imgSrc: 'https://content.codecademy.com/courses/free/boba.svg',
151+
imgAlt: 'Codey boba tea',
152+
title: 'Software Engineer',
153+
desc: '**Software Engineer**. Example link [here](#).',
154+
},
155+
{
156+
imgSrc: 'https://content.codecademy.com/courses/free/boba.svg',
157+
imgAlt: 'Codey boba tea',
158+
title: 'Data Scientist',
159+
desc: '**Data Scientist**. Example link [here](#).',
160+
},
161+
{
162+
imgSrc: 'https://content.codecademy.com/courses/free/boba.svg',
163+
imgAlt: 'Codey boba tea',
164+
title: 'Product Manager',
165+
desc: '**Product Manager**. Example link [here](#).',
166+
},
167+
{
168+
imgSrc: 'https://content.codecademy.com/courses/free/boba.svg',
169+
imgAlt: 'Codey boba tea',
170+
title: 'Product Designer',
171+
desc: '**Product Designer**. Example link [here](#).',
172+
},
173+
{
174+
imgSrc: 'https://content.codecademy.com/courses/free/boba.svg',
175+
imgAlt: 'Codey boba tea',
176+
title: 'Product Designer',
177+
desc: '**Product Designer**. Example link [here](#).',
178+
},
179+
],
180+
featuresMedia: 'icon',
181+
maxCols: 3,
182+
}}
183+
>
184+
{(args) => <PageFeatures {...args} />}
185+
</Story>
186+
</Canvas>

0 commit comments

Comments
 (0)