Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
08b94a5
Audio Layout for DCAR
DanielCliftonGuardian Mar 17, 2026
50fcdf8
Merge branch 'main' into audiolayout-apps
DanielCliftonGuardian Mar 17, 2026
8a155a8
Fix tsc
DanielCliftonGuardian Mar 18, 2026
3dd2f92
Merge branch 'main' into audiolayout-apps
DanielCliftonGuardian Mar 18, 2026
e239140
Add audio article button
DanielCliftonGuardian Mar 18, 2026
77e9447
Listen to this podcast
DanielCliftonGuardian Mar 18, 2026
b0004f3
Merge branch 'main' into play-audio-button
DanielCliftonGuardian Mar 18, 2026
3c2f99d
Create AppsAudioPlayButton.stories.tsx
DanielCliftonGuardian Mar 18, 2026
9b4b47b
Add podcast meta
DanielCliftonGuardian Mar 19, 2026
ac5e454
Merge branch 'main' into audiolayout-apps
DanielCliftonGuardian Mar 19, 2026
e77c0d5
Merge branch 'main' into play-audio-button
DanielCliftonGuardian Mar 19, 2026
0fb9b84
Add audio article button
DanielCliftonGuardian Mar 18, 2026
b8c1a28
Listen to this podcast
DanielCliftonGuardian Mar 18, 2026
a079ee0
Create AppsAudioPlayButton.stories.tsx
DanielCliftonGuardian Mar 18, 2026
c4bed9a
Merge branch 'play-audio-button' of https://github.com/guardian/dotco…
DanielCliftonGuardian Mar 19, 2026
c4036bb
Audio bridget version
DanielCliftonGuardian Mar 23, 2026
5ed4b81
Set relative base url to fix cors issue
DanielCliftonGuardian Mar 24, 2026
00bc3af
Add polling for button
DanielCliftonGuardian Mar 25, 2026
614597d
Merge branch 'main' into play-audio-button
DanielCliftonGuardian Mar 25, 2026
41ada05
Update assets.ts
DanielCliftonGuardian Mar 25, 2026
a0a1d49
Merge branch 'play-audio-button' of https://github.com/guardian/dotco…
DanielCliftonGuardian Mar 25, 2026
d2ad777
Revert dev base url change
DanielCliftonGuardian Mar 25, 2026
1fb5e0c
Merge branch 'main' into play-audio-button
DanielCliftonGuardian Mar 25, 2026
f46bba1
bump bridget
DanielCliftonGuardian Mar 26, 2026
7900306
Merge branch 'main' into play-audio-button
DanielCliftonGuardian Mar 26, 2026
b6e9d85
resolve merge conflicts
DanielCliftonGuardian Mar 26, 2026
6448e13
import shared styles
DanielCliftonGuardian Mar 27, 2026
fa5d33e
Merge branch 'main' into play-audio-button
DanielCliftonGuardian Mar 27, 2026
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
6 changes: 6 additions & 0 deletions dotcom-rendering/.storybook/mocks/bridgetApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ export const getListenToArticleClient: BridgetApi<
isPlaying: async () => false,
});

export const getAudioClient: BridgetApi<'getAudioClient'> = () => ({
isAvailable: async () => true,
isPlaying: async () => false,
});

export const getNativeABTestingClient: BridgetApi<
'getNativeABTestingClient'
> = () => ({
Expand All @@ -127,6 +132,7 @@ export const ensure_all_exports_are_present = {
getInteractionClient,
getInteractivesClient,
getListenToArticleClient,
getAudioClient,
getNativeABTestingClient,
} satisfies {
[Method in keyof BridgeModule]: BridgetApi<Method>;
Expand Down
1,107 changes: 320 additions & 787 deletions dotcom-rendering/fixtures/generated/fe-articles/Audio.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dotcom-rendering/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"@guardian/ab-core": "8.0.0",
"@guardian/ab-testing-config": "workspace:ab-testing-config",
"@guardian/braze-components": "22.2.0",
"@guardian/bridget": "8.7.0",
"@guardian/bridget": "8.9.0",
"@guardian/browserslist-config": "6.1.0",
"@guardian/cdk": "62.6.1",
"@guardian/commercial-core": "29.0.0",
Expand Down
2 changes: 1 addition & 1 deletion dotcom-rendering/scripts/test-data/gen-fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const articles = [
},
{
name: 'Audio',
url: 'https://www.theguardian.com/world/2013/jun/06/nsa-phone-records-verizon-court-order',
url: 'https://www.theguardian.com/news/audio/2026/mar/06/what-i-see-in-clinic-is-never-a-set-of-labels-are-we-in-danger-of-overdiagnosing-mental-illness--podcast',
},
{
name: 'StandardWithVideo',
Expand Down
25 changes: 25 additions & 0 deletions dotcom-rendering/src/components/AppsAudioPlayButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import { AppsAudioPlayButton as AppsAudioPlayButtonComponent } from './AppsAudioPlayButton';

const meta = {
component: AppsAudioPlayButtonComponent,
title: 'Components/Apps Audio Play Button',
} satisfies Meta<typeof AppsAudioPlayButtonComponent>;

export default meta;

type Story = StoryObj<typeof meta>;

export const WithDuration: Story = {
args: {
onClickHandler: () => undefined,
audioDuration: '26:00',
},
};

export const NoDuration: Story = {
args: {
onClickHandler: () => undefined,
audioDuration: undefined,
},
};
88 changes: 88 additions & 0 deletions dotcom-rendering/src/components/AppsAudioPlayButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { css } from '@emotion/react';
import { height, space } from '@guardian/source/foundations';
import type { ThemeIcon } from '@guardian/source/react-components';
import {
Button,
SvgMediaControlsPlay,
} from '@guardian/source/react-components';
import { palette } from '../palette';
import { waveFormContainerCss } from './ListenToArticleButton';
import type { WaveFormTheme } from './WaveForm';
import { WaveForm } from './WaveForm';

const buttonCss = (audioDuration: string | undefined) => css`
display: flex;
align-items: center;
background-color: ${palette('--listen-to-article-button-fill')};
color: ${palette('--listen-to-article-button-background')};
&:active,
&:focus,
&:hover {
background-color: ${palette('--listen-to-article-button-fill')};
}
margin-bottom: ${space[4]}px;
margin-left: ${space[2]}px;
padding-left: ${space[3]}px;
padding-right: ${audioDuration === undefined ? space[4] : space[3]}px;
padding-bottom: 0px;
font-size: 15px;
height: ${height.ctaXsmall}px;
min-height: ${height.ctaXsmall}px;

.src-button-space {
width: 0px;
}
`;

const themeIcon: ThemeIcon = {
fill: palette('--follow-icon-background'),
};

const waveTheme: WaveFormTheme = {
wave: palette('--listen-to-article-waveform'),
};

const dividerCss = css`
width: 0.5px;
height: 100%;
opacity: 0.5;
border-left: 1px solid ${palette('--follow-icon-background')};
margin-left: ${space[2]}px;
`;

type Props = {
onClickHandler: () => void;
audioDuration?: string;
};

export const AppsAudioPlayButton = ({
onClickHandler,
audioDuration,
}: Props) => {
return (
<div css={waveFormContainerCss}>
<WaveForm
seed="apps-audio-play"
height={space[10]}
bars={250}
theme={waveTheme}
gap={1}
barWidth={2}
/>
<Button
onClick={onClickHandler}
size="default"
cssOverrides={buttonCss(audioDuration)}
>
<span>Listen to this podcast</span>
{audioDuration !== undefined && audioDuration !== '' && (
<>
<span css={dividerCss}></span>
<SvgMediaControlsPlay size="small" theme={themeIcon} />
<span>{audioDuration}</span>
</>
)}
</Button>
</div>
);
};
82 changes: 82 additions & 0 deletions dotcom-rendering/src/components/AppsAudioPlayer.importable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { log } from '@guardian/libs';
import { useCallback, useEffect, useRef, useState } from 'react';
import { getAudioClient } from '../lib/bridgetApi';
import { useIsBridgetCompatible } from '../lib/useIsBridgetCompatible';
import { AppsAudioPlayButton } from './AppsAudioPlayButton';

const AUDIO_BRIDGET_VERSION = '8.9.0';
const POLLING_INTERVAL_MS = 3000;

type Props = {
audioDuration?: string;
};

export const AppsAudioPlayer = ({ audioDuration }: Props) => {
const [showButton, setShowButton] = useState<boolean>(false);
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);

const isBridgetCompatible = useIsBridgetCompatible(AUDIO_BRIDGET_VERSION);

const checkIsPlaying = useCallback(() => {
getAudioClient()
.isPlaying()
.then((isPlaying) => {
setShowButton(!isPlaying);
})
.catch((error: Error) => {
log('dotcom', 'Error polling isPlaying: ', error);
});
}, []);

useEffect(() => {
if (isBridgetCompatible) {
Promise.all([
getAudioClient().isAvailable(),
getAudioClient().isPlaying(),
])
.then(([isAvailable, isPlaying]) => {
setShowButton(isAvailable && !isPlaying);

if (isAvailable) {
pollingRef.current = setInterval(
checkIsPlaying,
POLLING_INTERVAL_MS,
);
}
})
.catch((error: Error) => {
log('dotcom', 'Error fetching audio status: ', error);
setShowButton(false);
});
}

return () => {
if (pollingRef.current) {
clearInterval(pollingRef.current);
}
};
}, [isBridgetCompatible, checkIsPlaying]);

const playHandler = () => {
void getAudioClient()
.play()
.then(() => {
// Hide the button once audio is playing
setShowButton(false);
})
.catch((error: Error) => {
window.guardian.modules.sentry.reportError(
error,
'bridget-getAudioClient-play-error',
);
log('dotcom', 'Bridget getAudioClient.play Error:', error);
});
};

return showButton ? (
<AppsAudioPlayButton
onClickHandler={playHandler}
audioDuration={audioDuration}
/>
) : null;
};
43 changes: 42 additions & 1 deletion dotcom-rendering/src/components/ArticleMeta.apps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,19 @@ import {
ArticleDisplay,
type ArticleFormat,
} from '../lib/articleFormat';
import { getAudioData } from '../lib/audio-data';
import { getSoleContributor } from '../lib/byline';
import { palette as themePalette } from '../palette';
import type { Branding as BrandingType } from '../types/branding';
import type { FEElement } from '../types/content';
import type { TagType } from '../types/tag';
import { shouldShowAvatar, shouldShowContributor } from './ArticleMeta.web';
import {
getPodcast,
getRssFeedUrl,
getSeriesTag,
shouldShowAvatar,
shouldShowContributor,
} from './ArticleMeta.web';
import { Avatar } from './Avatar';
import { Branding } from './Branding.island';
import { CommentCount } from './CommentCount.island';
Expand All @@ -24,6 +32,7 @@ import { FollowWrapper } from './FollowWrapper.island';
import { Island } from './Island';
import { ListenToArticle } from './ListenToArticle.island';
import { LiveblogNotifications } from './LiveblogNotifications.island';
import { PodcastMeta } from './PodcastMeta';

type Props = {
format: ArticleFormat;
Expand All @@ -37,6 +46,7 @@ type Props = {
isCommentable: boolean;
pageId?: string;
headline?: string;
mainMediaElements?: FEElement[];
};

const metaGridContainer = css`
Expand All @@ -60,6 +70,17 @@ const metaGridContainer = css`
}
`;

const podcastMetaPadding = css`
${until.phablet} {
padding-left: 20px;
padding-right: 20px;
}
${until.mobileLandscape} {
padding-left: 10px;
padding-right: 10px;
}
`;

const metaContainerMargins = css`
${until.phablet} {
margin-left: -20px;
Expand Down Expand Up @@ -230,6 +251,7 @@ export const ArticleMetaApps = ({
isCommentable,
pageId,
headline,
mainMediaElements,
}: Props) => {
const soleContributor = getSoleContributor(tags, byline);
const authorName = soleContributor?.title ?? 'Author Image';
Expand All @@ -245,6 +267,12 @@ export const ArticleMetaApps = ({
const isLiveBlog = format.design === ArticleDesign.LiveBlog;
const isGallery = format.design === ArticleDesign.Gallery;
const isVideo = format.design === ArticleDesign.Video;
const isAudio = format.design === ArticleDesign.Audio;

const seriesTag = getSeriesTag(tags);
const podcast = getPodcast(tags);
const audioData = getAudioData(mainMediaElements);
const rssFeedUrl = getRssFeedUrl(tags);

const shouldShowFollowButtons = (layoutOrDesignType: boolean) =>
layoutOrDesignType && !!byline && !isUndefined(soleContributor);
Expand All @@ -268,6 +296,19 @@ export const ArticleMetaApps = ({
isGallery ? galleryMetaContainer : undefined,
]}
>
{isAudio && podcast && seriesTag && (
<div css={podcastMetaPadding}>
<PodcastMeta
series={seriesTag}
format={format}
image={podcast.image}
spotifyUrl={podcast.spotifyUrl}
subscriptionUrl={podcast.subscriptionUrl}
audioDownloadUrl={audioData?.audioDownloadUrl}
rssFeedUrl={rssFeedUrl}
/>
</div>
)}
<div
css={[
metaGridContainer,
Expand Down
6 changes: 3 additions & 3 deletions dotcom-rendering/src/components/ArticleMeta.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -310,17 +310,17 @@ const metaNumbersExtrasLiveBlog = css`
}
`;

const getSeriesTag = (tags: TagType[]): TagType | undefined => {
export const getSeriesTag = (tags: TagType[]): TagType | undefined => {
return tags.find((tag) => tag.type === 'Series' && tag.podcast);
};

const getPodcast = (tags: TagType[]): Podcast | undefined => {
export const getPodcast = (tags: TagType[]): Podcast | undefined => {
const seriesTag = getSeriesTag(tags);

return seriesTag?.podcast;
};

const getRssFeedUrl = (tags: TagType[]): string => {
export const getRssFeedUrl = (tags: TagType[]): string => {
const seriesTag = getSeriesTag(tags);

return `/${seriesTag?.id}/podcast.xml`;
Expand Down
2 changes: 1 addition & 1 deletion dotcom-rendering/src/components/ListenToArticleButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const themeIcon: ThemeIcon = {
fill: palette('--follow-icon-background'),
};

const waveFormContainerCss = css`
export const waveFormContainerCss = css`
height: ${space[12]}px;
border-top: 1px solid ${palette('--article-meta-lines')};
position: relative;
Expand Down
Loading
Loading