mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-01-08 21:02:25 -05:00
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:
@@ -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).",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,5 +4,7 @@ export interface ShareProps {
|
||||
}
|
||||
|
||||
export interface ShareRedirectProps {
|
||||
redirectURL: string;
|
||||
xRedirectURL: string;
|
||||
blueSkyRedirectURL: string;
|
||||
threadsRedirectURL: string;
|
||||
}
|
||||
|
||||
@@ -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}`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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
58
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user