diff --git a/dotcom-rendering/fixtures/manual/productBlockElement.ts b/dotcom-rendering/fixtures/manual/productBlockElement.ts index e7831a945aa..413efefa572 100644 --- a/dotcom-rendering/fixtures/manual/productBlockElement.ts +++ b/dotcom-rendering/fixtures/manual/productBlockElement.ts @@ -1,3 +1,4 @@ +import { extractHeadingText } from '../../src/model/enhanceProductElement'; import type { ProductBlockElement } from '../../src/types/content'; import { productImage } from './productImage'; @@ -5,6 +6,7 @@ export const exampleProduct: ProductBlockElement = { _type: 'model.dotcomrendering.pageElements.ProductBlockElement', elementId: 'b1f6e8e2-3f3a-4f0c-8d1e-5f3e3e3e3e3e', primaryHeadingHtml: 'Best overall', + primaryHeadingText: extractHeadingText('Best overall'), secondaryHeadingHtml: 'Bosch Sky Kettle', brandName: 'Bosch', productName: 'Sky Kettle', @@ -240,3 +242,274 @@ export const exampleProduct: ProductBlockElement = { }, ], }; + +export const exampleAtAGlanceProductArray: ProductBlockElement[] = [ + { + _type: 'model.dotcomrendering.pageElements.ProductBlockElement', + elementId: 'b85ec38b-091b-40c2-8902-a9114df3cfe3', + primaryHeadingHtml: 'Best running watch for beginners:', + primaryHeadingText: extractHeadingText( + 'Best running watch for beginners:', + ), + secondaryHeadingHtml: 'Garmin Forerunner 55', + brandName: 'Garmin', + productName: 'Forerunner 55', + image: { + url: 'https://media.guim.co.uk/7bf8bdea17b8d7e3f0b9aef1aa88d94737d4bdf3/0_0_725_725/725.jpg', + caption: + 'Garmin Forerunner 55 GPS 42mm Running Smartwatch, Easy to use, Lightweight, Training Guidance, Safety & Tracking Features, Black', + height: 725, + width: 725, + alt: '', + credit: 'Photograph: PR Image', + displayCredit: false, + }, + displayType: 'InlineWithProductCard', + customAttributes: [], + productCtas: [ + { + url: 'https://www.decathlon.co.uk/p/garmin-forerunner-55-gps-watch-black/341619/m8758300', + text: '', + retailer: 'Decathlon', + price: '£179.99', + }, + { + url: 'https://www.amazon.co.uk/Garmin-Forerunner-Lightweight-Smartwatch-Training/dp/B0953X73TP?th=1', + text: '', + retailer: 'Amazon', + price: '£122.49', + }, + ], + starRating: 'none-selected', + content: [], + }, + { + _type: 'model.dotcomrendering.pageElements.ProductBlockElement', + elementId: '1cb32565-86fa-4d95-a944-de49a065e71e', + primaryHeadingHtml: 'Best budget running watch:', + primaryHeadingText: extractHeadingText( + 'Best budget running watch:', + ), + secondaryHeadingHtml: 'Suunto Run', + brandName: 'Suunto', + productName: 'Run', + image: { + url: 'https://media.guim.co.uk/ecc054be145a6e5b3c6fca3694f9b4cbea5078b1/0_0_725_725/725.jpg', + caption: 'SUUNTO RUN All Black GPS Sport and Running Watch', + height: 725, + width: 725, + alt: '', + credit: 'Photograph: PR Image', + displayCredit: false, + }, + displayType: 'InlineWithProductCard', + customAttributes: [], + productCtas: [ + { + url: 'https://www.sportsshoes.com/product/suu151/suunto-run-gps-watch', + text: '', + retailer: 'SportsShoes', + price: '£174.99', + }, + { + url: 'https://www.suunto.com/en-gb/Products/sports-watches/suunto-run/suunto-run-all-black-with-silicone-strap', + text: '', + retailer: 'Suunto', + price: '£199', + }, + ], + starRating: 'none-selected', + content: [], + }, + { + _type: 'model.dotcomrendering.pageElements.ProductBlockElement', + elementId: '43670bc5-00f2-460d-853e-3e6e0bf205c5', + primaryHeadingHtml: 'Best mid-range running watch:', + primaryHeadingText: extractHeadingText( + 'Best mid-range running watch:', + ), + secondaryHeadingHtml: 'Coros Pace Pro', + brandName: 'Coros', + productName: 'Pace Pro', + image: { + url: 'https://media.guim.co.uk/33599323a25a435d3a5647cc9906d2b011f6763e/0_0_725_725/725.jpg', + caption: 'COROS PACE Pro GPS Sport Watch Black', + height: 725, + width: 725, + alt: '', + credit: 'Photograph: PR Image', + displayCredit: false, + }, + displayType: 'InlineWithProductCard', + customAttributes: [], + productCtas: [ + { + url: 'https://healf.com/en-uk/products/coros-coros-pace-pro-gps-sport-watch-black', + text: '', + retailer: 'Healf', + price: '£299', + }, + { + url: 'https://www.myprotein.com/p/sports-accessories/coros-pace-pro-gps-sport-watch-black-one-size/16889572/', + text: '', + retailer: 'Myprotein', + price: '£299', + }, + ], + starRating: 'none-selected', + content: [], + }, + { + _type: 'model.dotcomrendering.pageElements.ProductBlockElement', + elementId: '830b3256-bd3a-4fc2-a4a3-6d42fcf0467f', + primaryHeadingHtml: 'Best-looking mid-range running watch:', + primaryHeadingText: extractHeadingText( + 'Best-looking mid-range running watch:', + ), + secondaryHeadingHtml: 'Suunto Race 2', + brandName: 'Suunto', + productName: 'Race 2', + image: { + url: 'https://media.guim.co.uk/c9f1e864f353555555af61c83f4fe5acf01be95b/0_0_725_725/725.jpg', + caption: 'Suunto Race 2', + height: 725, + width: 725, + alt: '', + credit: 'Photograph: PR Image', + displayCredit: false, + }, + displayType: 'InlineWithProductCard', + customAttributes: [], + productCtas: [ + { + url: 'https://www.sportsshoes.com/product/suu123/suunto-race-2-gps-watch---ss26', + text: '', + retailer: 'SportsShoes', + price: '£429', + }, + { + url: 'https://www.suunto.com/en-gb/Products/sports-watches/suunto-race-2/suunto-race-2-all-black/', + text: '', + retailer: 'Suunto', + price: '£429', + }, + ], + starRating: 'none-selected', + content: [], + }, + { + _type: 'model.dotcomrendering.pageElements.ProductBlockElement', + elementId: '407575ba-5898-4995-a94b-f7ab624c60de', + primaryHeadingHtml: 'The best running watch money can buy:', + primaryHeadingText: extractHeadingText( + 'The best running watch money can buy:', + ), + secondaryHeadingHtml: 'Garmin Forerunner 970', + brandName: 'Garmin', + productName: 'Forerunner 970', + image: { + url: 'https://media.guim.co.uk/5d6f01a3ac82c8f4208e47f6645034b7155c704b/0_0_725_725/725.jpg', + caption: 'Forerunner 970 GPS Smartwatch', + height: 725, + width: 725, + alt: '', + credit: 'Photograph: PR Image', + displayCredit: false, + }, + displayType: 'InlineWithProductCard', + customAttributes: [], + productCtas: [ + { + url: 'https://www.blacks.co.uk/19695870/garmin-forerunner-970-gps-watch-19695870/6206138', + text: '', + retailer: 'Blacks', + price: '£579', + }, + { + url: 'https://www.cotswoldoutdoor.com/p/garmin-forerunner-970-gps-smartwatch-B3BG3B0054.html', + text: '', + retailer: 'Cotswold Outdoor', + price: '£629.99', + }, + ], + starRating: 'none-selected', + content: [], + }, + { + _type: 'model.dotcomrendering.pageElements.ProductBlockElement', + elementId: 'd7de82bb-fd1c-4efb-a54d-9844156db9e5', + primaryHeadingHtml: 'Best running watch for battery life:', + primaryHeadingText: extractHeadingText( + 'Best running watch for battery life:', + ), + secondaryHeadingHtml: 'Garmin Enduro 3', + brandName: 'Garmin', + productName: 'Enduro 3', + image: { + url: 'https://media.guim.co.uk/4ca5935158967c6b2d1a91fabca27ec20a52ef96/0_0_725_725/725.jpg', + caption: 'Garmin Enduro 3 DLC Titanium GPS Watch', + height: 725, + width: 725, + alt: '', + credit: 'Photograph: PR Image', + displayCredit: false, + }, + displayType: 'InlineWithProductCard', + customAttributes: [], + productCtas: [ + { + url: 'https://www.sportsshoes.com/product/gar340/garmin-enduro-3-sapphire-gps-watch', + text: '', + retailer: 'SportsShoes', + price: '£615.99', + }, + { + url: 'https://www.millets.co.uk/19656651/garmin-enduro-3-gps-smartwatch-19656651/5930679/', + text: '', + retailer: 'Millets', + price: '£649', + }, + ], + starRating: 'none-selected', + content: [], + }, + { + _type: 'model.dotcomrendering.pageElements.ProductBlockElement', + elementId: 'c75a0b5a-929e-4550-a146-ccc080c76655', + primaryHeadingHtml: 'Best running watch with LTE/satellite:', + primaryHeadingText: extractHeadingText( + 'Best running watch with LTE/satellite:', + ), + secondaryHeadingHtml: 'Garmin Fenix 8 Pro', + brandName: 'Garmin', + productName: 'Fenix 8 Pro', + image: { + url: 'https://media.guim.co.uk/719acb5d276ed7d64f889ee5a031cfd8d945db67/0_0_725_725/725.jpg', + caption: + 'Garmin\nfēnix 8 Pro AMOLED GPS Multisport Smartwatch, Graphite, 47mm', + height: 725, + width: 725, + alt: '', + credit: 'Photograph: PR Image', + displayCredit: false, + }, + displayType: 'InlineWithProductCard', + customAttributes: [], + productCtas: [ + { + url: 'https://www.sportsshoes.com/product/gar354/garmin-fenix-8-pro-amoled-sapphire--(47mm)-gps-watch---aw25', + text: '', + retailer: 'SportsShoes', + price: '£875.49', + }, + { + url: 'https://www.johnlewis.com/garmin-fenix-8-pro-amoled-gps-multisport-smartwatch-graphite/p114305025', + text: '', + retailer: 'John Lewis', + price: '£991.57', + }, + ], + starRating: 'none-selected', + content: [], + }, +]; diff --git a/dotcom-rendering/src/components/HorizontalSummaryProductCard.tsx b/dotcom-rendering/src/components/HorizontalSummaryProductCard.tsx index 9c1933ff7f0..341863c5636 100644 --- a/dotcom-rendering/src/components/HorizontalSummaryProductCard.tsx +++ b/dotcom-rendering/src/components/HorizontalSummaryProductCard.tsx @@ -94,12 +94,7 @@ export const HorizontalSummaryProductCard = ({ />
-
+
{product.primaryHeadingText}
{product.secondaryHeadingHtml}
Read more diff --git a/dotcom-rendering/src/components/StackedProducts.stories.tsx b/dotcom-rendering/src/components/StackedProducts.stories.tsx new file mode 100644 index 00000000000..cc91de7d759 --- /dev/null +++ b/dotcom-rendering/src/components/StackedProducts.stories.tsx @@ -0,0 +1,36 @@ +import type { Meta, StoryObj } from '@storybook/react-webpack5'; +import { centreColumnDecorator } from '../../.storybook/decorators/gridDecorators'; +import { exampleAtAGlanceProductArray } from '../../fixtures/manual/productBlockElement'; +import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat'; +import { StackedProducts } from './StackedProducts'; + +const meta = { + title: 'Components/Stacked Horizontal Summary Product Cards', + component: StackedProducts, + args: { + products: exampleAtAGlanceProductArray, + format: { + design: ArticleDesign.Standard, + display: ArticleDisplay.Standard, + theme: Pillar.Lifestyle, + }, + }, + decorators: [centreColumnDecorator], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default = {} satisfies Story; + +export const OnlyThreeProducts = { + args: { + products: exampleAtAGlanceProductArray.slice(0, 3), + format: { + design: ArticleDesign.Standard, + display: ArticleDisplay.Standard, + theme: Pillar.Lifestyle, + }, + }, +} satisfies Story; diff --git a/dotcom-rendering/src/components/StackedProducts.tsx b/dotcom-rendering/src/components/StackedProducts.tsx new file mode 100644 index 00000000000..c21a57df632 --- /dev/null +++ b/dotcom-rendering/src/components/StackedProducts.tsx @@ -0,0 +1,106 @@ +import { css } from '@emotion/react'; +import { space, textSans15 } from '@guardian/source/foundations'; +import { + SvgChevronDownSingle, + SvgChevronUpSingle, +} from '@guardian/source/react-components'; +import { useState } from 'react'; +import type { ArticleFormat } from '../lib/articleFormat'; +import { palette } from '../palette'; +import type { ProductBlockElement } from '../types/content'; +import { HorizontalSummaryProductCard } from './HorizontalSummaryProductCard'; + +const showAllButtonStyles = css` + background-color: transparent; + border: none; + display: flex; +`; + +const showAllTextStyles = css` + ${textSans15}; + color: ${palette('--product-card-read-more')}; + font-weight: 700; + text-decoration-line: underline; + text-decoration-color: ${palette('--product-card-read-more-decoration')}; + text-underline-offset: 20%; + padding-right: ${space[1]}px; +`; + +const cardCounterStyles = css` + ${textSans15}; + color: ${palette('--product-card-count')}; + font-weight: 700; +`; + +export const StackedProducts = ({ + products, + format, +}: { + products: ProductBlockElement[]; + format: ArticleFormat; +}) => { + const [isExpanded, setIsExpanded] = useState(false); + return ( +
+
+ {products.map( + (product: ProductBlockElement, index) => + (index < 3 || isExpanded) && ( + + ), + )} +
+ + {products.length > 3 && ( +
+ + +

+ {isExpanded ? products.length : '3'}/{products.length} +

+
+ )} +
+ ); +}; diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index d21d831a63c..e45c1c50c90 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -5148,6 +5148,8 @@ const productCardReadMoreDark: PaletteFunction = () => sourcePalette.lifestyle[600]; const productCardReadMoreDecoration: PaletteFunction = () => sourcePalette.neutral[86]; +const productCardCountLight: PaletteFunction = () => sourcePalette.neutral[46]; +const productCardCountDark: PaletteFunction = () => sourcePalette.neutral[97]; const privacyTextRegularLight: PaletteFunction = () => sourcePalette.neutral[7]; const privacyTextDark: PaletteFunction = () => sourcePalette.neutral[86]; @@ -7637,6 +7639,10 @@ const paletteColours = { light: productCardBorderNeutralLight, dark: productCardBorderNeutralDark, }, + '--product-card-count': { + light: productCardCountLight, + dark: productCardCountDark, + }, '--product-card-headline': { light: productCardHeadingTextLight, dark: productCardHeadingTextDark,