chore(ui): add bluesky button (#57297)

Co-authored-by: Naomi <accounts+github@nhcarrigan.com>
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit is contained in:
Anna
2024-12-19 06:37:27 -05:00
committed by GitHub
parent 1525a67f48
commit 16123014ea
10 changed files with 194 additions and 90 deletions

View File

@@ -106,7 +106,9 @@
"donate-now": "Donate Now",
"confirm-amount": "Confirm amount",
"play-scene": "Press Play",
"closed-caption": "Closed caption"
"closed-caption": "Closed caption",
"share-on-bluesky": "Share on BlueSky",
"share-on-threads": "Share on Threads"
},
"landing": {
"big-heading-1": "Learn to code — for free.",
@@ -357,7 +359,7 @@
"challenge": "Challenge",
"completed": "Completed",
"add-linkedin": "Add this certification to my LinkedIn profile",
"add-twitter": "Share this certification on Twitter",
"add-twitter": "Share this certification on X",
"tweet": "I just earned the {{certTitle}} certification @freeCodeCamp! Check it out here: {{certURL}}",
"avatar": "{{username}}'s avatar",
"joined": "Joined {{date}}",
@@ -366,7 +368,9 @@
"points": "{{count}} point on {{date}}",
"points_plural": "{{count}} points on {{date}}",
"page-number": "{{pageNumber}} of {{totalPages}}",
"edit-my-profile": "Edit My Profile"
"edit-my-profile": "Edit My Profile",
"add-bluesky": "Share this certification on BlueSky",
"add-threads": "Share this certification on Threads"
},
"footer": {
"tax-exempt-status": "freeCodeCamp is a donor-supported tax-exempt 501(c)(3) charitable organization (United States Federal Tax Identification Number: 82-0779546).",

View File

@@ -44,10 +44,10 @@
"@babel/preset-react": "7.23.3",
"@babel/preset-typescript": "7.23.3",
"@babel/standalone": "7.23.7",
"@fortawesome/fontawesome-svg-core": "6.4.2",
"@fortawesome/free-brands-svg-icons": "6.4.2",
"@fortawesome/free-solid-svg-icons": "6.4.2",
"@fortawesome/react-fontawesome": "0.2.0",
"@fortawesome/fontawesome-svg-core": "6.7.1",
"@fortawesome/free-brands-svg-icons": "6.7.1",
"@fortawesome/free-solid-svg-icons": "6.7.1",
"@fortawesome/react-fontawesome": "0.2.2",
"@freecodecamp/loop-protect": "3.0.0",
"@freecodecamp/react-calendar-heatmap": "1.1.0",
"@freecodecamp/ui": "3.1.1",

View File

@@ -321,6 +321,34 @@ const ShowCertification = (props: ShowCertificationProps): JSX.Element => {
>
{t('profile.add-twitter')}
</Button>
<Spacer size='m' />
<Button
block={true}
size='large'
variant='primary'
href={`https://bsky.app/intent/compose?text=${t('profile.tweet', {
certTitle: urlFriendlyCertTitle,
certURL: certURL
})}`}
target='_blank'
data-playwright-test-label='bluesky-share-btn'
>
{t('profile.add-bluesky')}
</Button>
<Spacer size='m' />
<Button
block={true}
size='large'
variant='primary'
href={`https://threads.net/intent/post?text=${t('profile.tweet', {
certTitle: urlFriendlyCertTitle,
certURL: certURL
})}`}
target='_blank'
data-playwright-test-label='thread-share-btn'
>
{t('profile.add-threads')}
</Button>
</Col>
<Spacer size='l' />
</Row>

View File

@@ -4,9 +4,15 @@ import { ShareProps } from './types';
import { useShare } from './use-share';
export const Share = ({ superBlock, block }: ShareProps): JSX.Element => {
const redirectURL = useShare({
const redirectURLs = useShare({
superBlock,
block
});
return <ShareTemplate redirectURL={redirectURL} />;
return (
<ShareTemplate
xRedirectURL={redirectURLs.xUrl}
blueSkyRedirectURL={redirectURLs.blueSkyUrl}
threadsRedirectURL={redirectURLs.threadsURL}
/>
);
};

View File

@@ -5,12 +5,33 @@ import { ShareTemplate } from './share-template';
const redirectURL = 'string';
describe('Share Template Testing', () => {
render(<ShareTemplate redirectURL={redirectURL} />);
render(
<ShareTemplate
xRedirectURL={redirectURL}
blueSkyRedirectURL={redirectURL}
threadsRedirectURL={redirectURL}
/>
);
test('Testing share template Click Redirect Event', () => {
const link = screen.getByRole('link', {
const twitterLink = screen.queryByRole('link', {
name: 'buttons.tweet aria.opens-new-window'
});
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute('href', 'string');
expect(twitterLink).toBeInTheDocument();
expect(twitterLink).toHaveAttribute('href', 'string');
const blueSkyLink = screen.queryByRole('link', {
name: 'buttons.share-on-bluesky aria.opens-new-window'
});
expect(blueSkyLink).toBeInTheDocument();
expect(blueSkyLink).toHaveAttribute('href', 'string');
const threadsLink = screen.queryByRole('link', {
name: 'buttons.share-on-threads aria.opens-new-window'
});
expect(threadsLink).toBeInTheDocument();
expect(threadsLink).toHaveAttribute('href', 'string');
});
});

View File

@@ -1,29 +1,69 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { faXTwitter } from '@fortawesome/free-brands-svg-icons';
import {
faXTwitter,
faBluesky,
faInstagram
} from '@fortawesome/free-brands-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { ShareRedirectProps } from './types';
export const ShareTemplate: React.ComponentType<ShareRedirectProps> = ({
redirectURL
xRedirectURL,
blueSkyRedirectURL,
threadsRedirectURL
}) => {
const { t } = useTranslation();
return (
<a
data-testid='ShareTemplateWrapperTestID'
className='btn fade-in'
href={redirectURL}
target='_blank'
rel='noreferrer'
>
<FontAwesomeIcon
icon={faXTwitter}
size='1x'
aria-label='twitterIcon'
aria-hidden='true'
/>
{t('buttons.tweet')}
<span className='sr-only'>{t('aria.opens-new-window')}</span>
</a>
<div>
<a
data-testid='ShareTemplateWrapperTestID'
className='btn fade-in'
href={xRedirectURL}
target='_blank'
rel='noreferrer'
>
<FontAwesomeIcon
icon={faXTwitter}
size='1x'
aria-label='twitterIcon'
aria-hidden='true'
/>
{t('buttons.tweet')}
<span className='sr-only'>{t('aria.opens-new-window')}</span>
</a>
<a
data-testid='ShareTemplateWrapperTestID'
className='btn fade-in'
href={blueSkyRedirectURL}
target='_blank'
rel='noreferrer'
>
<FontAwesomeIcon
icon={faBluesky}
size='1x'
aria-label='blueSkyIcon'
aria-hidden='true'
/>
{t('buttons.share-on-bluesky')}
<span className='sr-only'>{t('aria.opens-new-window')}</span>
</a>
<a
data-testid='ShareTemplateWrapperTestID'
className='btn fade-in'
href={threadsRedirectURL}
target='_blank'
rel='noreferrer'
>
<FontAwesomeIcon
icon={faInstagram}
size='1x'
aria-label='instagramIcon'
aria-hidden='true'
/>
{t('buttons.share-on-threads')}
<span className='sr-only'>{t('aria.opens-new-window')}</span>
</a>
</div>
);
};

View File

@@ -4,5 +4,7 @@ export interface ShareProps {
}
export interface ShareRedirectProps {
redirectURL: string;
xRedirectURL: string;
blueSkyRedirectURL: string;
threadsRedirectURL: string;
}

View File

@@ -1,12 +1,12 @@
import { useTranslation } from 'react-i18next';
import {
action,
hastag,
nextLine,
space,
twitterDevelpoerDomainURL,
twitterDomain,
useShare
useShare,
twitterData,
blueSkyData,
threadsData
} from './use-share';
test('useShare testing', () => {
@@ -23,7 +23,15 @@ test('useShare testing', () => {
const i18nSupportedBlock = t(`intro:${superBlock}.blocks.${block}.title`);
const tweetMessage = `I${space}have${space}completed${space}${i18nSupportedBlock}${space}%23freecodecamp`;
const redirectFreeCodeCampLearnURL = `https://${freecodecampLearnDomain}/${superBlock}/${hastag}${block}`;
expect(redirectURL).toBe(
`https://${twitterDomain}/${action}?original_referer=${twitterDevelpoerDomainURL}&text=${tweetMessage}${nextLine}&url=${redirectFreeCodeCampLearnURL}`
expect(redirectURL.xUrl).toBe(
`https://${twitterData.domain}/${twitterData.action}?original_referer=${twitterData.developerDomainURL}&text=${tweetMessage}${nextLine}&url=${redirectFreeCodeCampLearnURL}`
);
expect(redirectURL.blueSkyUrl).toBe(
`https://${blueSkyData.domain}/${blueSkyData.action}?original_referer=${blueSkyData.developerDomainURL}&text=${tweetMessage}${nextLine}&url=${redirectFreeCodeCampLearnURL}`
);
expect(redirectURL.threadsURL).toBe(
`https://${threadsData.domain}/${threadsData.action}?original_referer=${threadsData.developerDomainURL}&text=${tweetMessage}${nextLine}&url=${redirectFreeCodeCampLearnURL}`
);
});

View File

@@ -4,19 +4,48 @@ import { ShareProps } from './types';
export const space = '%20';
export const hastag = '%23';
export const nextLine = '%0A';
export const action = 'intent/tweet';
export const twitterDomain = 'twitter.com';
const freecodecampLearnDomainURL = 'www.freecodecamp.org/learn';
export const twitterDevelpoerDomainURL = 'https://developer.twitter.com';
export const useShare = ({ superBlock, block }: ShareProps): string => {
export const twitterData = {
action: 'intent/tweet',
domain: 'twitter.com',
developerDomainURL: 'https://developer.twitter.com'
};
export const blueSkyData = {
action: 'intent/compose',
domain: 'bsky.app',
developerDomainURL: 'https://docs.bsky.app/'
};
export const threadsData = {
action: 'intent/post',
domain: 'threads.net',
developerDomainURL: 'https://developers.facebook.com'
};
interface ShareUrls {
xUrl: string;
blueSkyUrl: string;
threadsURL: string;
}
export const useShare = ({ superBlock, block }: ShareProps): ShareUrls => {
const { t } = useTranslation();
const redirectFreeCodeCampLearnURL = `https://${freecodecampLearnDomainURL}/${superBlock}/${hastag}${block}`;
const i18nSupportedBlock = t(`intro:${superBlock}.blocks.${block}.title`);
const tweetMessage = `I${space}have${space}completed${space}${i18nSupportedBlock}${space}${hastag}freecodecamp`;
const redirectURL = `https://${twitterDomain}/${action}?original_referer=${twitterDevelpoerDomainURL}&text=${tweetMessage}${nextLine}&url=${redirectFreeCodeCampLearnURL}`;
const xRedirectURL = `https://${twitterData.domain}/${twitterData.action}?original_referer=${twitterData.developerDomainURL}&text=${tweetMessage}${nextLine}&url=${redirectFreeCodeCampLearnURL}`;
return redirectURL;
const blueSkyRedirectURL = `https://${blueSkyData.domain}/${blueSkyData.action}?original_referer=${blueSkyData.developerDomainURL}&text=${tweetMessage}${nextLine}&url=${redirectFreeCodeCampLearnURL}`;
const threadRedirectURL = `https://${threadsData.domain}/${threadsData.action}?original_referer=${threadsData.developerDomainURL}&text=${tweetMessage}${nextLine}&url=${redirectFreeCodeCampLearnURL}`;
return {
xUrl: xRedirectURL,
blueSkyUrl: blueSkyRedirectURL,
threadsURL: threadRedirectURL
};
};

58
pnpm-lock.yaml generated
View File

@@ -453,17 +453,17 @@ importers:
specifier: 7.23.7
version: 7.23.7
'@fortawesome/fontawesome-svg-core':
specifier: 6.4.2
version: 6.4.2
specifier: 6.7.1
version: 6.7.1
'@fortawesome/free-brands-svg-icons':
specifier: 6.4.2
version: 6.4.2
specifier: 6.7.1
version: 6.7.1
'@fortawesome/free-solid-svg-icons':
specifier: 6.4.2
version: 6.4.2
specifier: 6.7.1
version: 6.7.1
'@fortawesome/react-fontawesome':
specifier: 0.2.0
version: 0.2.0(@fortawesome/fontawesome-svg-core@6.4.2)(react@16.14.0)
specifier: 0.2.2
version: 0.2.2(@fortawesome/fontawesome-svg-core@6.7.1)(react@16.14.0)
'@freecodecamp/loop-protect':
specifier: 3.0.0
version: 3.0.0
@@ -3034,40 +3034,22 @@ packages:
peerDependencies:
'@sinclair/typebox': '>=0.26 <=0.32'
'@fortawesome/fontawesome-common-types@6.4.2':
resolution: {integrity: sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==}
engines: {node: '>=6'}
'@fortawesome/fontawesome-common-types@6.7.1':
resolution: {integrity: sha512-gbDz3TwRrIPT3i0cDfujhshnXO9z03IT1UKRIVi/VEjpNHtSBIP2o5XSm+e816FzzCFEzAxPw09Z13n20PaQJQ==}
engines: {node: '>=6'}
'@fortawesome/fontawesome-svg-core@6.4.2':
resolution: {integrity: sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==}
engines: {node: '>=6'}
'@fortawesome/fontawesome-svg-core@6.7.1':
resolution: {integrity: sha512-8dBIHbfsKlCk2jHQ9PoRBg2Z+4TwyE3vZICSnoDlnsHA6SiMlTwfmW6yX0lHsRmWJugkeb92sA0hZdkXJhuz+g==}
engines: {node: '>=6'}
'@fortawesome/free-brands-svg-icons@6.4.2':
resolution: {integrity: sha512-LKOwJX0I7+mR/cvvf6qIiqcERbdnY+24zgpUSouySml+5w8B4BJOx8EhDR/FTKAu06W12fmUIcv6lzPSwYKGGg==}
engines: {node: '>=6'}
'@fortawesome/free-solid-svg-icons@6.4.2':
resolution: {integrity: sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA==}
'@fortawesome/free-brands-svg-icons@6.7.1':
resolution: {integrity: sha512-nJR76eqPzCnMyhbiGf6X0aclDirZriTPRcFm1YFvuupyJOGwlNF022w3YBqu+yrHRhnKRpzFX+8wJKqiIjWZkA==}
engines: {node: '>=6'}
'@fortawesome/free-solid-svg-icons@6.7.1':
resolution: {integrity: sha512-BTKc0b0mgjWZ2UDKVgmwaE0qt0cZs6ITcDgjrti5f/ki7aF5zs+N91V6hitGo3TItCFtnKg6cUVGdTmBFICFRg==}
engines: {node: '>=6'}
'@fortawesome/react-fontawesome@0.2.0':
resolution: {integrity: sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==}
peerDependencies:
'@fortawesome/fontawesome-svg-core': ~1 || ~6
react: '>=16.3'
'@fortawesome/react-fontawesome@0.2.2':
resolution: {integrity: sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==}
peerDependencies:
@@ -16955,36 +16937,20 @@ snapshots:
dependencies:
'@sinclair/typebox': 0.32.14
'@fortawesome/fontawesome-common-types@6.4.2': {}
'@fortawesome/fontawesome-common-types@6.7.1': {}
'@fortawesome/fontawesome-svg-core@6.4.2':
dependencies:
'@fortawesome/fontawesome-common-types': 6.4.2
'@fortawesome/fontawesome-svg-core@6.7.1':
dependencies:
'@fortawesome/fontawesome-common-types': 6.7.1
'@fortawesome/free-brands-svg-icons@6.4.2':
'@fortawesome/free-brands-svg-icons@6.7.1':
dependencies:
'@fortawesome/fontawesome-common-types': 6.4.2
'@fortawesome/free-solid-svg-icons@6.4.2':
dependencies:
'@fortawesome/fontawesome-common-types': 6.4.2
'@fortawesome/fontawesome-common-types': 6.7.1
'@fortawesome/free-solid-svg-icons@6.7.1':
dependencies:
'@fortawesome/fontawesome-common-types': 6.7.1
'@fortawesome/react-fontawesome@0.2.0(@fortawesome/fontawesome-svg-core@6.4.2)(react@16.14.0)':
dependencies:
'@fortawesome/fontawesome-svg-core': 6.4.2
prop-types: 15.8.1
react: 16.14.0
'@fortawesome/react-fontawesome@0.2.2(@fortawesome/fontawesome-svg-core@6.7.1)(react@16.14.0)':
dependencies:
'@fortawesome/fontawesome-svg-core': 6.7.1