From 16123014eaf877de2ef6c4e304a33b03d3e7f226 Mon Sep 17 00:00:00 2001 From: Anna Date: Thu, 19 Dec 2024 06:37:27 -0500 Subject: [PATCH] chore(ui): add bluesky button (#57297) Co-authored-by: Naomi Co-authored-by: Naomi Carrigan --- client/i18n/locales/english/translations.json | 10 ++- client/package.json | 8 +- .../client-only-routes/show-certification.tsx | 28 +++++++ client/src/components/share/index.tsx | 10 ++- .../components/share/share-template.test.tsx | 29 ++++++- .../src/components/share/share-template.tsx | 76 ++++++++++++++----- client/src/components/share/types.ts | 4 +- .../src/components/share/use-share.test.tsx | 20 +++-- client/src/components/share/use-share.tsx | 41 ++++++++-- pnpm-lock.yaml | 58 +++----------- 10 files changed, 194 insertions(+), 90 deletions(-) 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 ( - - + ); }; 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