diff --git a/client/src/components/share/index.tsx b/client/src/components/share/index.tsx index da83ca0c713..1c2b2e25689 100644 --- a/client/src/components/share/index.tsx +++ b/client/src/components/share/index.tsx @@ -3,16 +3,22 @@ import { ShareTemplate } from './share-template'; import { ShareProps } from './types'; import { useShare } from './use-share'; -export const Share = ({ superBlock, block }: ShareProps): JSX.Element => { +export const Share = ({ + superBlock, + block, + minified +}: ShareProps): JSX.Element => { const redirectURLs = useShare({ superBlock, - block + block, + minified }); return ( ); }; diff --git a/client/src/components/share/share-template.tsx b/client/src/components/share/share-template.tsx index f40daa13762..b5ae2881dec 100644 --- a/client/src/components/share/share-template.tsx +++ b/client/src/components/share/share-template.tsx @@ -11,42 +11,43 @@ import { ShareRedirectProps } from './types'; export const ShareTemplate: React.ComponentType = ({ xRedirectURL, blueSkyRedirectURL, - threadsRedirectURL + threadsRedirectURL, + minified }) => { const { t } = useTranslation(); return ( <> diff --git a/client/src/components/share/types.ts b/client/src/components/share/types.ts index 517ee7df0e8..da7192f120d 100644 --- a/client/src/components/share/types.ts +++ b/client/src/components/share/types.ts @@ -1,10 +1,12 @@ export interface ShareProps { superBlock: string; block: string; + minified?: boolean; } export interface ShareRedirectProps { xRedirectURL: string; blueSkyRedirectURL: string; threadsRedirectURL: string; + minified?: boolean; } diff --git a/client/src/templates/Challenges/components/independent-lower-jaw.css b/client/src/templates/Challenges/components/independent-lower-jaw.css index 712f89c1e15..91ea2a75d22 100644 --- a/client/src/templates/Challenges/components/independent-lower-jaw.css +++ b/client/src/templates/Challenges/components/independent-lower-jaw.css @@ -119,3 +119,9 @@ .independent-lower-jaw .tooltip .tooltiptext.left-tooltip { left: 0; } + +.share-button-wrapper { + display: flex; + justify-content: start; + gap: 10px; +} diff --git a/client/src/templates/Challenges/components/independent-lower-jaw.test.tsx b/client/src/templates/Challenges/components/independent-lower-jaw.test.tsx new file mode 100644 index 00000000000..acadace0995 --- /dev/null +++ b/client/src/templates/Challenges/components/independent-lower-jaw.test.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import type { ChallengeMeta, Test } from '../../../redux/prop-types'; +import { SuperBlocks } from '@freecodecamp/shared/config/curriculum'; +import { IndependentLowerJaw } from './independent-lower-jaw'; + +const baseChallengeMeta: ChallengeMeta = { + block: 'test-block', + id: 'test-challenge-id', + isFirstStep: false, + superBlock: SuperBlocks.RespWebDesignV9, + helpCategory: 'HTML-CSS', + disableLoopProtectTests: false, + disableLoopProtectPreview: false +}; + +const passingTests: Test[] = [{ pass: true, text: 'test', testString: 'test' }]; + +const baseProps = { + openHelpModal: vi.fn(), + openResetModal: vi.fn(), + executeChallenge: vi.fn(), + submitChallenge: vi.fn(), + tests: passingTests, + isSignedIn: true, + challengeMeta: baseChallengeMeta, + completedPercent: 100, + completedChallengeIds: ['id-1', 'test-challenge-id'], + currentBlockIds: ['id-1', 'test-challenge-id'] +}; + +describe('', () => { + it('shows share buttons when the block is completed on the last step', () => { + render(); + + expect(screen.getByTestId('share-on-x')).toBeInTheDocument(); + expect(screen.getByTestId('share-on-bluesky')).toBeInTheDocument(); + expect(screen.getByTestId('share-on-threads')).toBeInTheDocument(); + }); + + it('does not show share buttons when the block is not completed', () => { + render( + + ); + + expect(screen.queryByTestId('share-on-x')).not.toBeInTheDocument(); + }); + + it('does not show share buttons when it is not the last step', () => { + render( + + ); + + expect(screen.queryByTestId('share-on-x')).not.toBeInTheDocument(); + }); +}); diff --git a/client/src/templates/Challenges/components/independent-lower-jaw.tsx b/client/src/templates/Challenges/components/independent-lower-jaw.tsx index 7c5f87f3205..2a2111fc2a3 100644 --- a/client/src/templates/Challenges/components/independent-lower-jaw.tsx +++ b/client/src/templates/Challenges/components/independent-lower-jaw.tsx @@ -3,13 +3,22 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { useTranslation } from 'react-i18next'; import { Button } from '@freecodecamp/ui'; -import { Test } from '../../../redux/prop-types'; -import { challengeTestsSelector } from '../redux/selectors'; -import { isSignedInSelector } from '../../../redux/selectors'; +import { + completedChallengesIdsSelector, + isSignedInSelector +} from '../../../redux/selectors'; +import { ChallengeMeta, Test } from '../../../redux/prop-types'; +import { + challengeMetaSelector, + challengeTestsSelector, + completedPercentageSelector, + currentBlockIdsSelector +} from '../redux/selectors'; import { apiLocation } from '../../../../config/env.json'; import { openModal, submitChallenge, executeChallenge } from '../redux/actions'; import Help from '../../../assets/icons/help'; import callGA from '../../../analytics/call-ga'; +import { Share } from '../../../components/share'; import './independent-lower-jaw.css'; import Reset from '../../../assets/icons/reset'; @@ -17,9 +26,24 @@ import Reset from '../../../assets/icons/reset'; const mapStateToProps = createSelector( challengeTestsSelector, isSignedInSelector, - (tests: Test[], isSignedIn: boolean) => ({ + challengeMetaSelector, + completedPercentageSelector, + completedChallengesIdsSelector, + currentBlockIdsSelector, + ( + tests: Test[], + isSignedIn: boolean, + challengeMeta: ChallengeMeta, + completedPercent: number, + completedChallengeIds: string[], + currentBlockIds: string[] + ) => ({ tests, - isSignedIn + isSignedIn, + challengeMeta, + completedPercent, + completedChallengeIds, + currentBlockIds }) ); @@ -37,6 +61,10 @@ interface IndependentLowerJawProps { submitChallenge: () => void; tests: Test[]; isSignedIn: boolean; + challengeMeta: ChallengeMeta; + completedPercent: number; + completedChallengeIds: string[]; + currentBlockIds: string[]; } export function IndependentLowerJaw({ openHelpModal, @@ -44,7 +72,11 @@ export function IndependentLowerJaw({ executeChallenge, submitChallenge, tests, - isSignedIn + isSignedIn, + challengeMeta, + completedPercent, + completedChallengeIds, + currentBlockIds }: IndependentLowerJawProps): JSX.Element { const { t } = useTranslation(); const firstFailedTest = tests.find(test => !!test.err); @@ -57,6 +89,20 @@ export function IndependentLowerJaw({ React.useState(false); const isChallengeComplete = tests.every(test => test.pass); + const hasBlockIds = currentBlockIds.length > 0; + const isLastStepInBlock = + hasBlockIds && + currentBlockIds[currentBlockIds.length - 1] === challengeMeta.id; + const isBlockCompletedByIds = + hasBlockIds && + currentBlockIds.every(challengeId => + completedChallengeIds.includes(challengeId) + ); + const hasCompletedPercent = Number.isFinite(completedPercent); + const isBlockCompleted = + isBlockCompletedByIds || (hasCompletedPercent && completedPercent === 100); + const showShareButton = + isChallengeComplete && isLastStepInBlock && isBlockCompleted; React.useEffect(() => { setShowHint(!!hint); @@ -110,6 +156,15 @@ export function IndependentLowerJaw({ >

{t('learn.congratulations-code-passes')}

+ {isSignedIn && showShareButton && ( +
+ +
+ )} {!isSignedIn && (