diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json
index 607c3ab33a1..97c3b60a722 100644
--- a/client/i18n/locales/english/translations.json
+++ b/client/i18n/locales/english/translations.json
@@ -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).",
diff --git a/client/package.json b/client/package.json
index 0c11cabde8e..92992af4d2e 100644
--- a/client/package.json
+++ b/client/package.json
@@ -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",
diff --git a/client/src/client-only-routes/show-certification.tsx b/client/src/client-only-routes/show-certification.tsx
index 286219923af..234c1eb665d 100644
--- a/client/src/client-only-routes/show-certification.tsx
+++ b/client/src/client-only-routes/show-certification.tsx
@@ -321,6 +321,34 @@ const ShowCertification = (props: ShowCertificationProps): JSX.Element => {
>
{t('profile.add-twitter')}
+
+
+
+
diff --git a/client/src/components/share/index.tsx b/client/src/components/share/index.tsx
index 5e143e2a77c..da83ca0c713 100644
--- a/client/src/components/share/index.tsx
+++ b/client/src/components/share/index.tsx
@@ -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 ;
+ return (
+
+ );
};
diff --git a/client/src/components/share/share-template.test.tsx b/client/src/components/share/share-template.test.tsx
index eb723f25fe4..a7fae4ae7f3 100644
--- a/client/src/components/share/share-template.test.tsx
+++ b/client/src/components/share/share-template.test.tsx
@@ -5,12 +5,33 @@ import { ShareTemplate } from './share-template';
const redirectURL = 'string';
describe('Share Template Testing', () => {
- render();
+ render(
+
+ );
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');
});
});
diff --git a/client/src/components/share/share-template.tsx b/client/src/components/share/share-template.tsx
index 21e02eb12d1..7a75c355aca 100644
--- a/client/src/components/share/share-template.tsx
+++ b/client/src/components/share/share-template.tsx
@@ -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 = ({
- redirectURL
+ xRedirectURL,
+ blueSkyRedirectURL,
+ threadsRedirectURL
}) => {
const { t } = useTranslation();
return (
-
-
- {t('buttons.tweet')}
- {t('aria.opens-new-window')}
-
+
);
};
diff --git a/client/src/components/share/types.ts b/client/src/components/share/types.ts
index 1caca6957d4..517ee7df0e8 100644
--- a/client/src/components/share/types.ts
+++ b/client/src/components/share/types.ts
@@ -4,5 +4,7 @@ export interface ShareProps {
}
export interface ShareRedirectProps {
- redirectURL: string;
+ xRedirectURL: string;
+ blueSkyRedirectURL: string;
+ threadsRedirectURL: string;
}
diff --git a/client/src/components/share/use-share.test.tsx b/client/src/components/share/use-share.test.tsx
index 1052eede19e..6d0d07d94f3 100644
--- a/client/src/components/share/use-share.test.tsx
+++ b/client/src/components/share/use-share.test.tsx
@@ -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}`
);
});
diff --git a/client/src/components/share/use-share.tsx b/client/src/components/share/use-share.tsx
index ed7af6d0e5e..6b4dc4f5f85 100644
--- a/client/src/components/share/use-share.tsx
+++ b/client/src/components/share/use-share.tsx
@@ -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
+ };
};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index bbce818b9e2..d50716be4b8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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