Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions dotcom-rendering/src/components/InlineProductCard.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { css } from '@emotion/react';
import { breakpoints } from '@guardian/source/foundations';
import type { Meta } from '@storybook/react-webpack5';
import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat';
import type { ProductImage } from '../types/content';
import { ArticleContainer } from './ArticleContainer';
import type { InlineProductCardProps } from './InlineProductCard';
import { InlineProductCard } from './InlineProductCard';
import { Section as SectionComponent } from './Section';

const meta = {
component: InlineProductCard,
title: 'Components/InlineProductCard',
parameters: {
chromatic: {
viewports: [
breakpoints.mobile,
breakpoints.tablet,
breakpoints.wide,
],
},

formats: [
{
design: ArticleDesign.Standard,
display: ArticleDisplay.Standard,
theme: Pillar.Lifestyle,
},
],
},
decorators: [
(Story) => (
<SectionComponent
shouldCenter={true}
showSideBorders={true}
centralBorder={'full'}
css={css`
strong {
font-weight: bold;
}
`}
format={{
design: ArticleDesign.Review,
display: ArticleDisplay.Showcase,
theme: Pillar.Lifestyle,
}}
>
<ArticleContainer
format={{
design: ArticleDesign.Review,
display: ArticleDisplay.Showcase,
theme: Pillar.Lifestyle,
}}
>
<Story />
</ArticleContainer>
</SectionComponent>
),
],
} satisfies Meta<typeof InlineProductCard>;

export default meta;

const productImage: ProductImage = {
url: 'https://media.guimcode.co.uk/cb193848ed75d40103eceaf12b448de2330770dc/0_0_725_725/725.jpg',
caption: 'Filter-2 test image for live demo',
height: 1,
width: 1,
alt: 'Bosch Sky kettle',
credit: 'Photograph: Rachel Ogden/The Guardian',
displayCredit: false,
};

const sampleProductCard: InlineProductCardProps = {
format: {
design: ArticleDesign.Standard,
display: ArticleDisplay.Standard,
theme: Pillar.Lifestyle,
},
image: productImage,
productCtas: [
{
url: 'https://www.theguardian.com',
label: '£89.99 at Amazon',
},
{
url: 'https://www.theguardian.com',
label: '£99.99 at John Lewis',
},
],
brandName: 'AirCraft',
productName: 'Lume',
customAttributes: [
{ name: 'What we love', value: 'It packs away pretty small' },
{
name: "What we don't love",
value: 'There’s nowhere to stow the remote control',
},
],
isCardOnly: false,
};

export const Default = () => <InlineProductCard {...sampleProductCard} />;

export const productCardOnly = () => (
<InlineProductCard {...sampleProductCard} isCardOnly={true} />
);

export const productCardOnlyDisplayCredit = () => (
<InlineProductCard
{...sampleProductCard}
image={{
...productImage,
displayCredit: true,
}}
isCardOnly={true}
/>
);
219 changes: 219 additions & 0 deletions dotcom-rendering/src/components/InlineProductCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import { css } from '@emotion/react';
import {
from,
headlineMedium20,
headlineMedium24,
space,
textSans15,
textSans17,
textSans20,
textSansBold17,
textSansBold20,
until,
} from '@guardian/source/foundations';
import type { ArticleFormat } from '../lib/articleFormat';
import { palette } from '../palette';
import type { ProductImage } from '../types/content';
import { ProductCardButtons } from './ProductCardButtons';
import { ProductCardImage } from './ProductCardImage';

export type ProductCardCta = {
label: string;
url: string;
};
export type CustomAttributes = {
name: string;
value: string;
};

export type InlineProductCardProps = {
format: ArticleFormat;
brandName: string;
productName: string;
image?: ProductImage;
productCtas: ProductCardCta[];
customAttributes: CustomAttributes[];
isCardOnly: boolean;
shouldShowLeftColCard?: boolean;
lowestPrice?: string;
};

const baseCard = css`
padding: ${space[2]}px ${space[3]}px ${space[3]}px;
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 10px;
row-gap: ${space[4]}px;
max-width: 100%;
img {
width: 100%;
height: auto;
}
${from.mobileLandscape} {
column-gap: 20px;
}
`;

const hideFromWide = css`
${from.wide} {
display: none;
}
`;

const showcaseCard = css`
${baseCard};
background-color: ${palette('--product-card-background')};
border-top: 1px solid ${palette('--product-card-border')};
`;

const productCard = css`
${baseCard};
padding: ${space[2]}px 0 0;
background-color: transparent;
border-top: 1px solid ${palette('--section-border')};
`;

const productInfoContainer = css`
display: flex;
flex-direction: column;
gap: ${space[1]}px;
${textSans20};

${until.mobileLandscape} {
${textSans17};
}
`;

const primaryHeading = css`
${headlineMedium24};
${until.mobileLandscape} {
${headlineMedium20};
}
`;

const productNameStyle = css`
${textSans17};
> strong {
${textSansBold17}
}

${from.mobileLandscape} {
${textSans20};
> strong {
${textSansBold20}
}
}
`;

const mobileButtonWrapper = css`
grid-column: 1 / span 2;
grid-gap: ${space[1]}px;
${from.mobileLandscape} {
display: none;
}
`;

const desktopButtonWrapper = css`
display: none;
${from.mobileLandscape} {
display: inline;
}
`;

const customAttributesContainer = css`
grid-column: 1 / span 2;
border-top: 1px solid ${palette('--product-card-border-neutral')};
padding-top: ${space[2]}px;
display: grid;
gap: ${space[3]}px;

${from.mobileLandscape} {
grid-template-columns: 1fr 1fr;
gap: ${space[5]}px;
}
`;

const customAttributeItem = css`
${textSans15};
${from.phablet} {
${textSans17};
}
strong {
font-weight: 700;
}
`;

const CustomAttribute = ({ name, value }: CustomAttributes) => (
<div css={customAttributeItem}>
<strong>{name}</strong>
<br />
{value}
</div>
);

export const InlineProductCard = ({
format,
brandName,
productName,
image,
customAttributes,
productCtas,
isCardOnly = false,
shouldShowLeftColCard = false,
lowestPrice,
}: InlineProductCardProps) => {
return (
<div
css={[
isCardOnly && productCard,
!isCardOnly && showcaseCard,
shouldShowLeftColCard && !isCardOnly && hideFromWide,
]}
>
<ProductCardImage
format={format}
image={image}
url={productCtas[0]?.url}
label={productCtas[0]?.label}
/>
<div css={productInfoContainer}>
<div css={primaryHeading}>{brandName}</div>
<div css={productNameStyle}>{productName}</div>
{!!lowestPrice && (
<div css={productNameStyle}>
{productCtas.length > 1 ? (
<>
from <strong>{lowestPrice}</strong>
</>
) : (
<strong>{lowestPrice}</strong>
)}
</div>
)}
<div css={desktopButtonWrapper}>
<ProductCardButtons
productCtas={productCtas}
dataComponent={'inline-product-card-buttons-desktop'}
/>
</div>
</div>
<div css={mobileButtonWrapper}>
<ProductCardButtons
productCtas={productCtas}
dataComponent={'inline-product-card-buttons-mobile'}
/>
</div>
{!isCardOnly && customAttributes.length > 0 && (
<div css={customAttributesContainer}>
{customAttributes.map((customAttribute) => (
<CustomAttribute
key={customAttribute.name}
name={customAttribute.name}
value={customAttribute.value}
/>
))}
</div>
)}
</div>
);
};
Loading
Loading