Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
f3adbf1
move the follow and notification buttons from top to bottom
aracho1 Jan 30, 2026
12fc9f5
add bio and description to tag models
aracho1 Feb 2, 2026
f5feecb
implement new design
aracho1 Feb 2, 2026
5fae777
create a follow button wrapper with a pill variant
aracho1 Feb 2, 2026
c5bbae4
set up contributor profile
aracho1 Feb 2, 2026
006693d
add to commentlayout
aracho1 Feb 2, 2026
8f66db4
rename contributor profile name and component
aracho1 Feb 3, 2026
7648f34
fix storybook config
aracho1 Feb 3, 2026
9c64e50
fix lint issues
aracho1 Feb 3, 2026
b1e3300
create a variant and update code
aracho1 Feb 4, 2026
8e1e5e6
add thick border line below profile
aracho1 Feb 4, 2026
449d884
add notification icon and update copy
aracho1 Feb 5, 2026
4a57c1b
rename files
aracho1 Feb 5, 2026
0f13ff2
update the notification text to be non-button
aracho1 Feb 5, 2026
30ed257
display contributor name in the notification button
aracho1 Feb 5, 2026
20a8272
hide notification when not following
aracho1 Feb 5, 2026
bded4e2
tweak toggle button positioning and add straight line
aracho1 Feb 5, 2026
345a4fc
insert after 4th para
aracho1 Feb 5, 2026
1d65d5e
delete old files and add a new storybook
aracho1 Feb 5, 2026
ba92c92
update use effect
aracho1 Feb 9, 2026
75732f7
separate out the new follow wrapper
aracho1 Feb 9, 2026
7616a08
fix button from stretching
aracho1 Feb 9, 2026
4b27214
use shared blocklist
aracho1 Feb 9, 2026
6d6053b
wrap island around the follow button
aracho1 Feb 10, 2026
c2cf6f8
fix lint issues
aracho1 Feb 10, 2026
45964b5
undo unnecessary changes
aracho1 Feb 10, 2026
12b0cb5
update ContributorProfileFOllow
aracho1 Feb 10, 2026
e733a28
update notification button copy
aracho1 Feb 10, 2026
15a7d72
rename to ContributorFollowCard
aracho1 Feb 10, 2026
b01eb65
move button and notifications to inside the new component
aracho1 Feb 10, 2026
789bc26
tweaks on notification component
aracho1 Feb 10, 2026
aa3ca48
rename to ContributorFollowBlock
aracho1 Feb 11, 2026
5243c96
css tweaks
aracho1 Feb 11, 2026
48bd872
update palette definition names
aracho1 Feb 11, 2026
bde21c8
add dark mode colours
aracho1 Feb 11, 2026
c919d3b
recorder paletteDeclarations
aracho1 Feb 12, 2026
33a2cf3
tweak margin/paddings
aracho1 Feb 12, 2026
af2d06d
remove redundant css rules
aracho1 Feb 12, 2026
7efcbfb
remove a/b test conditions
aracho1 Feb 12, 2026
55e534c
disable until a/b test is set up
aracho1 Feb 12, 2026
e04df2d
update schema
aracho1 Feb 12, 2026
68bd711
add byline to enhanceBlocks
aracho1 Feb 12, 2026
a498531
Merge branch 'main' into new-follow-button-opinion-articles
aracho1 Feb 12, 2026
178fb3e
revert width change in follow wrapper
aracho1 Feb 16, 2026
c0819b6
add comment about repeated code in new block
aracho1 Feb 16, 2026
b5b1668
Merge branch 'main' into new-follow-button-opinion-articles
aracho1 Feb 17, 2026
aa63860
revert article.ts
aracho1 Feb 17, 2026
c40b355
use source toggle switch
aracho1 Feb 19, 2026
d736dd8
use source button
aracho1 Feb 19, 2026
db78d9b
remove unused css
aracho1 Feb 19, 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
3 changes: 3 additions & 0 deletions dotcom-rendering/.storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import { palette as sourcePalette } from '@guardian/source/foundations';
sb.mock(import('../src/lib/useNewsletterSubscription.ts'), { spy: true });
sb.mock(import('../src/lib/useAuthStatus.ts'), { spy: true });
sb.mock(import('../src/lib/fetchEmail.ts'), { spy: true });
sb.mock(import('../src/lib/useIsMyGuardianEnabled.ts'), { spy: true });
sb.mock(import('../src/lib/useIsBridgetCompatible.ts'), { spy: true });
sb.mock(import('../src/lib/bridgetApi.ts'), { spy: true });

// Prevent components being lazy rendered when we're taking Chromatic snapshots
Lazy.disabled = isChromatic();
Expand Down
303 changes: 303 additions & 0 deletions dotcom-rendering/src/components/ContributorFollowBlock.importable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
/**
* There is a lot of repeated code between this component and FollowWrapper.
* This is intentional so that we can implement it as an A/B test.
* We will clean this up once the test is complete.
*/
import { css } from '@emotion/react';
import { Topic } from '@guardian/bridget/Topic';
import { isUndefined, log } from '@guardian/libs';
import { space, textSans12, textSans15 } from '@guardian/source/foundations';
import {
Button,
SvgCheckmark,
SvgNotificationsOn,
SvgPlus,
} from '@guardian/source/react-components';
import {
StraightLines,
ToggleSwitch,
} from '@guardian/source-development-kitchen/react-components';
import { useEffect, useState } from 'react';
import { getNotificationsClient, getTagClient } from '../lib/bridgetApi';
import { useIsBridgetCompatible } from '../lib/useIsBridgetCompatible';
import { useIsMyGuardianEnabled } from '../lib/useIsMyGuardianEnabled';
import { palette } from '../palette';
import { isNotInBlockList } from './FollowWrapper.importable';

// -- Follow button --

type FollowButtonProps = {
isFollowing: boolean;
onClickHandler: () => void;
};

const FollowButtonPillStyle = ({
isFollowing,
onClickHandler,
}: FollowButtonProps) => {
return (
<Button
onClick={onClickHandler}
type="button"
theme={{
backgroundPrimary: palette('--contributor-follow-fill'),
textPrimary: palette('--quiz-atom-button-text'),
}}
iconSide="left"
icon={isFollowing ? <SvgCheckmark /> : <SvgPlus />}
>
<span>{isFollowing ? 'Following in My Guardian' : 'Follow'}</span>
</Button>
);
};

// -- notifications --

const notificationAlertStyles = css`
${textSans15}
color: ${palette('--follow-text')};
min-height: ${space[6]}px;
width: 100%;
`;

const notificationAlertRowStyles = css`
display: flex;
column-gap: ${space[6]}px;
justify-content: space-between;
width: 100%;
`;

const notificationIconStyles = css`
display: flex;
margin-right: ${space[1]}px;

svg {
margin-top: -${space[1] - 1}px;
fill: currentColor;
}
`;
const notificationLabelStyles = css`
display: flex;
align-items: flex-start;
${textSans12}
`;

const NotificationAlert = ({
isFollowing,
onClickHandler,
displayName,
}: FollowButtonProps & { displayName?: string }) => {
return (
<div css={notificationAlertStyles}>
<div css={notificationAlertRowStyles}>
<div css={notificationLabelStyles}>
<div css={notificationIconStyles}>
<SvgNotificationsOn size="small" />
</div>
<span>
Receive an alert when {displayName ?? 'this author'}{' '}
publishes an article
</span>
</div>
<div>
<ToggleSwitch
checked={isFollowing}
onClick={onClickHandler}
/>
</div>
</div>
</div>
);
};

const followBlockStyles = css`
display: flex;
flex-direction: column;
width: 100%;
align-items: flex-start;
`;

type FollowBlockProps = {
contributorId: string;
displayName: string;
};

export const ContributorFollowBlock = ({
contributorId,
displayName,
}: FollowBlockProps) => {
const [isFollowingNotifications, setIsFollowingNotifications] = useState<
boolean | undefined
>(undefined);
const [isFollowingContributor, setIsFollowingContributor] = useState<
boolean | undefined
>(undefined);

const isMyGuardianEnabled = useIsMyGuardianEnabled();
const isBridgetCompatible = useIsBridgetCompatible('2.5.0');

const shouldShowFollow =
isBridgetCompatible &&
isMyGuardianEnabled &&
isNotInBlockList(contributorId);

useEffect(() => {
const topic = new Topic({
id: contributorId,
displayName,
type: 'tag-contributor',
});

void getNotificationsClient()
.isFollowing(topic)
.then(setIsFollowingNotifications)
.catch((error) => {
window.guardian.modules.sentry.reportError(
error,
'bridget-getNotificationsClient-isFollowing-error',
);
log(
'dotcom',
'Bridget getNotificationsClient.isFollowing Error:',
error,
);
});

void getTagClient()
.isFollowing(topic)
.then(setIsFollowingContributor)
.catch((error) => {
window.guardian.modules.sentry.reportError(
error,
'bridget-getTagClient-isFollowing-error',
);
log('dotcom', 'Bridget getTagClient.isFollowing Error:', error);
});
}, [contributorId, displayName]);

const tagHandler = () => {
const topic = new Topic({
id: contributorId,
displayName,
type: 'tag-contributor',
});

if (isFollowingContributor) {
void getTagClient()
.unfollow(topic)
.then((success) => {
if (success) {
setIsFollowingContributor(false);
}
})
.catch((error) => {
window.guardian.modules.sentry.reportError(
error,
'bridget-getTagClient-unfollow-error',
);
log(
'dotcom',
'Bridget getTagClient.unfollow Error:',
error,
);
});
} else {
void getTagClient()
.follow(topic)
.then((success) => {
if (success) {
setIsFollowingContributor(true);
}
})
.catch((error) => {
window.guardian.modules.sentry.reportError(
error,
'bridget-getTagClient-follow-error',
);
log('dotcom', 'Bridget getTagClient.follow Error:', error);
});
}
};

const notificationsHandler = () => {
const topic = new Topic({
id: contributorId,
displayName,
type: 'tag-contributor',
});

if (isFollowingNotifications) {
void getNotificationsClient()
.unfollow(topic)
.then((success) => {
if (success) {
setIsFollowingNotifications(false);
}
})
.catch((error) => {
window.guardian.modules.sentry.reportError(
error,
'briidget-getNotificationsClient-unfollow-error',
);
log(
'dotcom',
'Bridget getNotificationsClient.unfollow Error:',
error,
);
});
} else {
void getNotificationsClient()
.follow(topic)
.then((success) => {
if (success) {
setIsFollowingNotifications(true);
}
})
.catch((error) => {
window.guardian.modules.sentry.reportError(
error,
'bridget-getNotificationsClient-follow-error',
);
log(
'dotcom',
'Bridget getNotificationsClient.follow Error:',
error,
);
});
}
};

if (!shouldShowFollow) {
return null;
}

return (
<div css={followBlockStyles}>
<FollowButtonPillStyle
isFollowing={isFollowingContributor ?? false}
onClickHandler={
!isUndefined(isFollowingContributor)
? tagHandler
: () => undefined
}
/>
{isFollowingContributor && (
<div>
<StraightLines
count={1}
color={palette('--contributor-follow-straight-lines')}
/>
<NotificationAlert
isFollowing={isFollowingNotifications ?? false}
onClickHandler={
!isUndefined(isFollowingNotifications)
? notificationsHandler
: () => undefined
}
displayName={displayName}
/>
</div>
)}
</div>
);
};
Loading
Loading