Skip to content
Draft
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c27fe8b
detect portrait image + add style
emilysaffron Nov 14, 2025
1625b1b
add gradient
emilysaffron Nov 14, 2025
95dffb3
update tests
emilysaffron Nov 14, 2025
f49add0
Merge branch 'latest' into WS-1674-PVdisplayintopicpromo
emilysaffron Nov 14, 2025
4f4886e
undo commented out effect
emilysaffron Nov 14, 2025
7b630d5
Merge branch 'WS-1674-PVdisplayintopicpromo' of github.com:bbc/simorg…
emilysaffron Nov 14, 2025
154c975
update bundle size max
emilysaffron Nov 14, 2025
cae21d3
update integration snapshots
emilysaffron Nov 14, 2025
c35f5a2
update z index for play button
emilysaffron Nov 14, 2025
72bcb8c
add isCurationPromo condition
emilysaffron Nov 17, 2025
6269515
Merge branch 'latest' into WS-1674-PVdisplayintopicpromo
emilysaffron Nov 17, 2025
6182c31
add storybook example
emilysaffron Nov 17, 2025
581ea37
Merge branch 'WS-1674-PVdisplayintopicpromo' of github.com:bbc/simorg…
emilysaffron Nov 17, 2025
f5a3c88
update default state
emilysaffron Nov 17, 2025
5ba9a81
clean up
emilysaffron Nov 17, 2025
4e2d44a
clean up
emilysaffron Nov 17, 2025
2de54b5
snapshots
emilysaffron Nov 17, 2025
899ceb0
Merge branch 'latest' into WS-1674-PVdisplayintopicpromo
emilysaffron Nov 17, 2025
741bda6
remove conditional
emilysaffron Nov 18, 2025
0358f97
conditinal on all promos
emilysaffron Nov 18, 2025
501c66b
Merge branch 'latest' into WS-1674-PVdisplayintopicpromo
emilysaffron Nov 18, 2025
4b06899
fix accidental changes
emilysaffron Nov 18, 2025
d631a25
integration
emilysaffron Nov 18, 2025
693cfd2
Merge branch 'latest' into WS-1674-PVdisplayintopicpromo
emilysaffron Nov 18, 2025
0e0bd24
default to false for portrait
emilysaffron Nov 18, 2025
23164e3
Merge branch 'WS-1674-PVdisplayintopicpromo' of github.com:bbc/simorg…
emilysaffron Nov 18, 2025
58f222c
use white from theme
emilysaffron Nov 18, 2025
8c83113
Merge branch 'latest' into WS-1674-PVdisplayintopicpromo
emilysaffron Nov 18, 2025
eafae85
Merge branch 'latest' into WS-1674-PVdisplayintopicpromo
emilysaffron Nov 19, 2025
a048344
use originalSize to set isPortrait
emilysaffron Nov 20, 2025
e48de6c
Merge branch 'WS-1674-PVdisplayintopicpromo' of github.com:bbc/simorg…
emilysaffron Nov 20, 2025
e68ff73
Merge branch 'latest' into WS-1674-PVdisplayintopicpromo
emilysaffron Nov 20, 2025
74b8bd4
add default
emilysaffron Nov 20, 2025
3a09577
Merge branch 'WS-1674-PVdisplayintopicpromo' of github.com:bbc/simorg…
emilysaffron Nov 20, 2025
3c8f771
extend conditional
emilysaffron Nov 24, 2025
17b1b0d
Merge branch 'latest' into WS-1674-PVdisplayintopicpromo
emilysaffron Nov 24, 2025
3f0d5b2
conditionally call hook
emilysaffron Nov 24, 2025
826c9cb
Merge branch 'WS-1674-PVdisplayintopicpromo' of github.com:bbc/simorg…
emilysaffron Nov 24, 2025
ca0a81d
Merge branch 'latest' into WS-1674-PVdisplayintopicpromo
emilysaffron Nov 24, 2025
fabf938
Merge branch 'latest' into WS-1674-PVdisplayintopicpromo
emilysaffron Nov 25, 2025
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
18 changes: 10 additions & 8 deletions data/persian/homePage/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,17 @@
"id": "c3kr439dnzgo"
},
{
"type": "article",
"type": "video",
"duration": "PT1M56S",
"isLive": false,
"title": "مولوی عبدالحمید به رهبران جمهوری اسلامی: نمی‌توانید فریاد زنان را خاموش کنید ",
"firstPublished": "2023-07-12T13:58:52.043Z",
"lastPublished": "2023-07-12T13:58:52.043Z",
"link": "https://www.bbc.com/persian/articles/czm0vwkr0p6o",
"imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsdevpb/6cbc/test/c6e521d0-0c56-11ee-a81d-07e4fe474984.jpg.webp",
"imageAlt": "مولوی عبدالحمید ",
"id": "czm0vwkr0p6o"
"title": "«واکنش به ربات‌نماهای کیش؛ از «آبروریزی جهانی» تا «بچه رباط کریم",
"firstPublished": "2025-11-16T20:29:46.464Z",
"lastPublished": "2025-11-16T20:29:46.464Z",
"link": "https://www.bbc.com/persian/articles/cn7e3651ydxo",
"imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/dffd/live/936d9340-c32a-11f0-ae46-bd64331f0fd4.jpg.webp",
"description": "- «من یک داده هستم» ویدیوهای زن و مرد ربات‌نما در نمایشگاه فناوری کیش اینوکس در شبکه‌های اجتماعی ایران ترند شده است؛ تصاویری که ابتدا با تیترهای غلط‌انداز منتشر شد و در دو سه روز گذشته واکنش‌های بسیاری برانگیخته است که بیشتر آنها انتقادی یا طنزآلود است.",
"imageAlt": "زن و مرد با گریم ربات در نمایشگاه فناوری کیش",
"id": "cn7e3651ydxo"
}
],
"activePage": 1,
Expand Down
295 changes: 295 additions & 0 deletions data/persian/topics/c6z7mnr559gt.json

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions data/ws/homePage/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@
"curations": [
{
"summaries": [
{
"type": "topic",
"duration": "PT1M56S",
"isLive": false,
"title": "«واکنش به ربات‌نماهای کیش؛ از «آبروریزی جهانی» تا «بچه رباط کریم",
"firstPublished": "2025-11-16T20:29:46.464Z",
"lastPublished": "2025-11-16T20:29:46.464Z",
"link": "https://www.bbc.com/persian/articles/cn7e3651ydxo",
"imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/dffd/live/936d9340-c32a-11f0-ae46-bd64331f0fd4.jpg.webp",
"description": "- «من یک داده هستم» ویدیوهای زن و مرد ربات‌نما در نمایشگاه فناوری کیش اینوکس در شبکه‌های اجتماعی ایران ترند شده است؛ تصاویری که ابتدا با تیترهای غلط‌انداز منتشر شد و در دو سه روز گذشته واکنش‌های بسیاری برانگیخته است که بیشتر آنها انتقادی یا طنزآلود است.",
"imageAlt": "زن و مرد با گریم ربات در نمایشگاه فناوری کیش",
"id": "cn7e3651ydxo",
"visualProminence": "MAXIMUM"
},
{
"type": "topic",
"isLive": false,
Expand Down
2 changes: 1 addition & 1 deletion scripts/bundleSize/bundleSizeConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
export const VARIANCE = 5;

export const MIN_SIZE = 916;
export const MAX_SIZE = 1257;
export const MAX_SIZE = 1269;
2 changes: 1 addition & 1 deletion src/app/components/Billboard/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export const PidginLiveWithFiveSummaries = () => {
);
};

export const PersianBillboard = () => {
export const PersianBillboardWithPortraitVideo = () => {
const summary = persianData.data.curations[2].summaries[0];
return (
<div dir="rtl">
Expand Down
14 changes: 10 additions & 4 deletions src/app/components/Curation/CurationGrid/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import fixture from '#data/pidgin/topics/c95y35941vrt.json';
import highImpactPromoFixture from '#data/ws/homePage/index.json';
import curationWithPVs from '#data/persian/topics/c6z7mnr559gt.json';
import { Summary } from '#app/models/types/curationData';

import CurationGrid from '.';
Expand All @@ -22,14 +23,19 @@ export default {
};

export const Example = () => {
return (
<Component summaries={fixture.data?.curations[0].summaries} />
);
return <Component summaries={fixture.data?.curations[0].summaries} />;
};

export const CurationGridWithPortraitVideos = () => {
return <Component summaries={curationWithPVs.data?.curations[0].summaries} />;
};

export const HighImpactPromo = () => {
return (
<Component summaries={highImpactPromoFixture.data.curations[0].summaries as Summary[]} />
<Component
summaries={
highImpactPromoFixture.data.curations[0].summaries as Summary[]
}
/>
);
};
15 changes: 15 additions & 0 deletions src/app/components/Image/index.styles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ const styles = {
width: '100%',
objectFit: 'cover', // objectFit used in combination with inline style aspectRatio will center the image horizontally and vertically if aspectRatio prop is different from image's intrinsic aspect ratio
}),
portraitImage: css({
position: 'relative',
width: '100%',
objectFit: 'contain',
}),
imageFixedAspectRatio: css({
height: 'auto',
}),
Expand All @@ -38,6 +43,16 @@ const styles = {
portraitOrientation: css({
position: 'absolute',
}),

gradientBackground: css({
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
pointerEvents: 'none',
filter: `blur(15px)`,
}),
};

export default styles;
12 changes: 0 additions & 12 deletions src/app/components/Image/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,6 @@ describe('Image - Canonical', () => {
});
});

it('should render a placeholder image when placeholder is true', () => {
render(<Fixture />);
const imageEl = screen.getByAltText('Test image alt text');
expect(imageEl.parentNode).toHaveStyle({
backgroundImage: `url(${BASE64_PLACEHOLDER_IMAGE})`,
});
});

it('should render a placeholder image when placeholder is true', () => {
render(<Fixture darkPlaceholder />);
const imageEl = screen.getByAltText('Test image alt text');
Expand All @@ -156,7 +148,6 @@ describe('Image - Canonical', () => {

it('should render the container with an aspect ratio based on width and height', () => {
render(<Fixture />);

const imageEl = screen.getByAltText('Test image alt text');
expect(imageEl.parentNode).toHaveStyle({
paddingBottom: '56.2%',
Expand All @@ -167,7 +158,6 @@ describe('Image - Canonical', () => {
render(<Fixture width={undefined} height={undefined} />);

const imageEl = screen.getByAltText('Test image alt text');

expect(imageEl.parentNode).toHaveStyle({
paddingBottom: '0',
});
Expand All @@ -179,7 +169,6 @@ describe('Image - Canonical', () => {
);

const imageEl = screen.getByAltText('Test image alt text');

expect(imageEl.parentNode).toHaveStyle({
paddingBottom: '75%',
});
Expand All @@ -189,7 +178,6 @@ describe('Image - Canonical', () => {
render(<Fixture aspectRatio={[4, 3]} />);

const imageEl = screen.getByAltText('Test image alt text');

expect(imageEl.parentNode).toHaveStyle({
paddingBottom: '75%',
});
Expand Down
67 changes: 63 additions & 4 deletions src/app/components/Image/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
/** @jsx jsx */
/* @jsxFrag React.Fragment */
import React, { Fragment, PropsWithChildren, useState, use } from 'react';
import { Global, jsx } from '@emotion/react';
import React, {
Fragment,
PropsWithChildren,
useState,
use,
useEffect,
} from 'react';
import { Global, jsx, useTheme } from '@emotion/react';
import { Helmet } from 'react-helmet';
import useImageColour from '#app/hooks/useImageColour';
import styles from './index.styles';
import { RequestContext } from '../../contexts/RequestContext';
import { HOME_PAGE } from '../../routes/utils/pageTypes';
Expand All @@ -27,6 +34,7 @@ export type ImageProps = {
fetchPriority?: 'high';
hasCaption?: boolean;
isPortraitOrientation?: boolean;
isPromo?: boolean;
};

const roundNumber = (num: number) => Math.round(num * 100) / 100;
Expand Down Expand Up @@ -56,9 +64,35 @@ const Image = ({
fetchPriority,
hasCaption,
isPortraitOrientation,
isPromo,
}: PropsWithChildren<ImageProps>) => {
const { pageType, isLite, isAmp } = use(RequestContext);
const [isLoaded, setIsLoaded] = useState(false);
const [isPortrait, setIsPortrait] = useState(false);
const imgRef = React.useRef<HTMLImageElement | null>(null);

useEffect(() => {
const img = imgRef.current;
if (img?.complete) {
setIsPortrait(img.naturalHeight > img.naturalWidth);
setIsLoaded(true);
}
}, []);

const {
palette: { GREY_8, WHITE },
isDarkUi,
} = useTheme();

const { isLoading, colour } = useImageColour(src, {
fallbackColour: GREY_8,
minimumContrast: 0,
contrastColour: WHITE,
paletteSize: 10,
});

const gradientColour = isDarkUi ? WHITE : '#180109';

if (isLite) return null;

const showPlaceholder = placeholder && !isLoaded;
Expand Down Expand Up @@ -164,8 +198,25 @@ const Image = ({
/>
</>
)}
{isPromo && isPortrait && (
<div
css={[
styles.gradientBackground,
{
background: colour
? `linear-gradient(200deg,rgba(${colour.rgb.join(',')}, 0.6) 0%, ${gradientColour} 54%, ${gradientColour} 90%)`
: `linear-gradient(200deg, ${GREY_8} 0%, ${gradientColour} 54%, ${gradientColour}90%)`,
},
]}
/>
)}
<img
onLoad={() => setIsLoaded(true)}
ref={imgRef}
onLoad={e => {
const img = e.currentTarget;
setIsPortrait(img.naturalHeight > img.naturalWidth);
setIsLoaded(true);
}}
src={src}
{...(srcSet && { srcSet: imgSrcSet })}
{...(imgSizes && { sizes: imgSizes })}
Expand All @@ -174,10 +225,18 @@ const Image = ({
width={width}
height={height}
css={[
styles.image,
isPortrait && isPromo ? styles.portraitImage : styles.image,
hasFixedAspectRatio
? styles.imageFixedAspectRatio
: styles.imageResponsiveRatio,
isPromo && {
background: `rgb(${colour?.rgb?.join(',')})`,
...(!isLoading && {
[`@supports (filter: blur(15px))`]: {
background: `rgba(${colour?.rgb?.join(',')}, 0.62)`,
},
}),
},
]}
fetchPriority={fetchPriority}
style={{
Expand Down
1 change: 1 addition & 0 deletions src/app/legacy/components/OptimoPromos/Image/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const Image = ({
width={width}
height={height}
lazyLoad
isPromo
/>
);
};
Expand Down
1 change: 1 addition & 0 deletions src/app/legacy/components/Promo/image.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ const Image = props => {
fallbackMediaType="image/jpeg"
sizes={sizes}
aspectRatio={[16, 9]}
isPromo
/>
{children && (
<ChildWrapper className={className}>{children}</ChildWrapper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ exports[`Canonical Home Page Page content First item in the first curation is th
exports[`Canonical Home Page Page content First item in the first curation is the correct image 1`] = `
<img
alt="الرياضه"
class="bbc-139onq"
class="bbc-1nl89yy"
fetchpriority="high"
loading="eager"
src="https://ichef.test.bbci.co.uk/ace/standard/240/cpsdevpb/36C4/test/_63702041_tv000117434.jpg"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ exports[`Canonical Home Page Page content First item in the first curation is th
exports[`Canonical Home Page Page content First item in the first curation is the correct image 1`] = `
<img
alt="पहलगाम"
class="bbc-139onq"
class="bbc-1nl89yy"
loading="lazy"
src="https://ichef.bbci.co.uk/ace/ws/240/cpsprodpb/8b52/live/6c11f130-2c01-11f0-8da1-ad2bbee6cda5.jpg.webp"
style="aspect-ratio:16 / 9"
Expand Down Expand Up @@ -823,7 +823,7 @@ exports[`Canonical Home Page messageBanner has a link 1`] = `"https://whatsapp.c
exports[`Canonical Home Page messageBanner has an image 1`] = `
<img
alt=""
class="bbc-139onq"
class="bbc-1ndjx27"
loading="eager"
sizes="(max-width: 37.4375rem) 184px, 224px"
src="https://ichef.bbci.co.uk/ace/ws/224/cpsprodpb/3bc3/live/e712bd90-225e-11ef-80aa-699d54c46324.png.webp"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ exports[`Canonical Home Page Page content First item in the first curation is th

exports[`Canonical Home Page Page content First item in the first curation is the correct image 1`] = `
<img
class="bbc-139onq"
class="bbc-1nl89yy"
fetchpriority="high"
loading="eager"
src="https://ichef.bbci.co.uk/ace/ws/240/cpsprodpb/36D1/production/_127933041__63970643_bbc-news-world-service-logo-nc.png.webp"
Expand Down Expand Up @@ -748,7 +748,7 @@ exports[`Canonical Home Page messageBanner has a link 1`] = `"https://www.bbc.co
exports[`Canonical Home Page messageBanner has an image 1`] = `
<img
alt=""
class="bbc-139onq"
class="bbc-1ndjx27"
loading="eager"
sizes="(max-width: 37.4375rem) 184px, 224px"
src="https://ichef.test.bbci.co.uk/ace/ws/224/cpsdevpb/0af7/test/5a5151c0-a3aa-11ed-9015-6935ab4fa6ca.png.webp"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ exports[`Canonical Home Page Page content First item in the first curation is th
exports[`Canonical Home Page Page content First item in the first curation is the correct image 1`] = `
<img
alt="Promena kod svake žene"
class="bbc-139onq"
class="bbc-1nl89yy"
fetchpriority="high"
loading="eager"
src="https://ichef.bbci.co.uk/ace/ws/240/cpsdevpb/3d07/test/7ea19630-8d4a-11ef-8e34-ef6bbfe11731.jpg.webp"
Expand Down Expand Up @@ -492,7 +492,7 @@ exports[`Canonical Home Page messageBanner has a link 1`] = `"https://www.whatsa
exports[`Canonical Home Page messageBanner has an image 1`] = `
<img
alt=""
class="bbc-139onq"
class="bbc-1ndjx27"
loading="eager"
sizes="(max-width: 37.4375rem) 184px, 224px"
src="https://ichef.test.bbci.co.uk/ace/ws/224/cpsdevpb/0409/test/6d7272a0-abfc-11ef-b4f6-4568dfb1f4fc.png.webp"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ exports[`Canonical Home Page Page content First item in the first curation is th
exports[`Canonical Home Page Page content First item in the first curation is the correct image 1`] = `
<img
alt="Promena kod svake žene"
class="bbc-139onq"
class="bbc-1nl89yy"
fetchpriority="high"
loading="eager"
src="https://ichef.bbci.co.uk/ace/ws/240/cpsdevpb/3d07/test/7ea19630-8d4a-11ef-8e34-ef6bbfe11731.jpg.webp"
Expand Down Expand Up @@ -492,7 +492,7 @@ exports[`Canonical Home Page messageBanner has a link 1`] = `"https://www.whatsa
exports[`Canonical Home Page messageBanner has an image 1`] = `
<img
alt=""
class="bbc-139onq"
class="bbc-1ndjx27"
loading="eager"
sizes="(max-width: 37.4375rem) 184px, 224px"
src="https://ichef.test.bbci.co.uk/ace/ws/224/cpsdevpb/0409/test/6d7272a0-abfc-11ef-b4f6-4568dfb1f4fc.png.webp"
Expand Down
Loading
Loading