diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json index 792309f1bb5..75f72c09bdd 100644 --- a/client/i18n/locales/english/translations.json +++ b/client/i18n/locales/english/translations.json @@ -518,7 +518,7 @@ "ms-link": "Microsoft Link", "submit-and-go": "Submit and go to my next challenge", "congratulations": "Congratulations, your code passes. Submit your code to continue.", - "congratulations-code-passes": "✔ Congratulations. Your code passes.", + "congratulations-code-passes": "Congratulations. Your code passes.", "i-completed": "I've completed this challenge", "example-code": "Example Code", "test-output": "Your test output will go here", @@ -556,7 +556,7 @@ "contact-support-mistake": "If you think there has been a mistake, please contact us at donors@freecodecamp.org", "editor-tabs": { "code": "Code", - "tests": "Tests", + "tests": "Tests:", "restart": "Restart", "restart-step": "Restart Step", "console": "Console", diff --git a/client/src/components/Progress/progress-inner.tsx b/client/src/components/Progress/progress-inner.tsx index dfac6282bf0..663eb7b5b06 100644 --- a/client/src/components/Progress/progress-inner.tsx +++ b/client/src/components/Progress/progress-inner.tsx @@ -7,6 +7,7 @@ interface ProgressInnerProps { completedPercent: number; title: string; meta: string; + minified?: boolean; } const easing = BezierEasing(0.2, 0.5, 0.4, 1); @@ -36,7 +37,8 @@ function useIsInViewport(ref: React.RefObject) { function ProgressInner({ completedPercent, title, - meta + meta, + minified }: ProgressInnerProps): JSX.Element { const [shownPercent, setShownPercent] = useState(0); const [lastShownPercent, setLastShownPercent] = useState(0); @@ -94,6 +96,24 @@ function ProgressInner({ }; }, []); + if (minified) { + return ( + <> +
+
{title}
+
{meta}
+
+ + + ); + } + return ( <>
{title}
diff --git a/client/src/components/Progress/progress.tsx b/client/src/components/Progress/progress.tsx index 95ee67d4d2c..357f1a30198 100644 --- a/client/src/components/Progress/progress.tsx +++ b/client/src/components/Progress/progress.tsx @@ -56,6 +56,7 @@ type PropsFromRedux = ConnectedProps; interface ProgressProps extends PropsFromRedux { t: TFunction; + minified?: boolean; } function Progress({ currentBlockIds, @@ -65,7 +66,8 @@ function Progress({ challengeType, completedChallengesInBlock, completedPercent, - t + t, + minified }: ProgressProps): JSX.Element { useFetchAllCurriculumData(); // needed to compute completedPercent let blockTitle = t(`intro:${superBlock}.blocks.${block}.title`); @@ -103,6 +105,7 @@ function Progress({ title={blockTitle} meta={meta} completedPercent={completedPercent} + minified={minified} /> ); diff --git a/client/src/templates/Challenges/classic/show.tsx b/client/src/templates/Challenges/classic/show.tsx index 192a849a8a4..44884a52789 100644 --- a/client/src/templates/Challenges/classic/show.tsx +++ b/client/src/templates/Challenges/classic/show.tsx @@ -315,8 +315,12 @@ function ShowClassic({ // AB testing Pre-fetch in the Spanish locale const isPreFetchEnabled = useFeature('prefetch_ab_test').on; - // Independent lower jaw is only enabled for desktop workshops. - const showIndependentLowerJaw = hasEditableBoundaries && !isMobile; + // Independent lower jaw is only enabled for desktop. + const showIndependentLowerJaw = !isMobile; + + const showSidePanelTests = isMobile || !hasEditableBoundaries; + + // Show test useEffect(() => { if (isPreFetchEnabled && envData.clientLocale === 'espanol') { @@ -428,7 +432,7 @@ function ShowClassic({ instructionsPanelRef={instructionsPanelRef} toolPanel={toolPanel} hasDemo={hasDemo} - showIndependentLowerJaw={showIndependentLowerJaw} + showSidePanelTests={showSidePanelTests} /> ); }; @@ -470,7 +474,7 @@ function ShowClassic({ > - {isMobile && ( + {isMobile ? ( - )} - {!isMobile && ( + ) : ( , + toolPanel: null, hasDemo: demoType === 'onClick' })} isDailyCodingChallenge={isDailyCodingChallenge} @@ -546,6 +549,8 @@ function ShowClassic({ challengeTitle={title} challengeBlock={block} superBlock={superBlock} + guideUrl={guideUrl} + videoUrl={videoUrl} /> void; } const { forumLocation } = envData; @@ -31,7 +34,11 @@ const mapStateToProps = (state: unknown) => ({ }); const mapDispatchToProps = (dispatch: Dispatch) => bindActionCreators( - { createQuestion, closeHelpModal: () => closeModal('help') }, + { + createQuestion, + closeHelpModal: () => closeModal('help'), + openVideoModal: () => openModal('video') + }, dispatch ); @@ -43,7 +50,6 @@ export const generateSearchLink = ( const titleText = t(`intro:${superBlock}.blocks.${block}.title`); const selector = 'in:title'; const query = encodeURIComponent(`${titleText} - ${title} ${selector}`); - const search = `${forumLocation}/search?q=${query}`; return search; }; @@ -99,7 +105,10 @@ function HelpModal({ isOpen, challengeBlock, superBlock, - challengeTitle + challengeTitle, + guideUrl, + videoUrl, + openVideoModal }: HelpModalProps): JSX.Element { const { t } = useTranslation(); const [showHelpForm, setShowHelpForm] = useState(false); @@ -136,6 +145,11 @@ function HelpModal({ resetFormValues(); }; + const handleOpenVideo = () => { + openVideoModal(); + handleClose(); + }; + const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); @@ -149,13 +163,17 @@ function HelpModal({ closeHelpModal(); }; + const hintUrl = guideUrl + ? guideUrl + : generateSearchLink(challengeTitle, challengeBlock, superBlock); + if (isOpen) { callGA({ event: 'pageview', pagePath: '/help-modal' }); } return ( - {t('buttons.ask-for-help')} + {t('buttons.get-help')} {showHelpForm ? ( @@ -278,12 +296,38 @@ function HelpModal({

- + + + {videoUrl && ( + <> + + + + )} @@ -292,7 +336,7 @@ function HelpModal({ block={true} size='large' variant='primary' - onClick={closeHelpModal} + onClick={handleClose} > {t('buttons.cancel')} diff --git a/client/src/templates/Challenges/components/independent-lower-jaw.css b/client/src/templates/Challenges/components/independent-lower-jaw.css index 91ea2a75d22..766203add7f 100644 --- a/client/src/templates/Challenges/components/independent-lower-jaw.css +++ b/client/src/templates/Challenges/components/independent-lower-jaw.css @@ -8,12 +8,13 @@ .independent-lower-jaw .hint-container { background-color: var(--background-quaternary); display: flex; - flex-direction: row; + flex-direction: column; justify-content: space-between; + gap: 10px; padding: 12px; position: absolute; bottom: 100%; - width: 95%; + width: 97%; left: 50%; transform: translateX(-50%); margin-bottom: 12px; @@ -22,6 +23,21 @@ animation: jaw-hint-fade-in 0.3s ease forwards; } +.independent-lower-jaw .hint-container .hint-header { + display: flex; + justify-content: space-between; + flex-direction: row; +} + +.independent-lower-jaw .hint-container .hint-header svg { + height: 1.4rem; + width: 1.4rem; +} + +.independent-lower-jaw .hint-container .hint-body p:last-child { + margin-bottom: 0; +} + @keyframes jaw-hint-fade-in { from { opacity: 0; @@ -57,7 +73,7 @@ display: flex; justify-content: center; align-items: center; - border: none; + border: 1px solid var(--quaternary-color); } .independent-lower-jaw .action-row-right button { @@ -122,6 +138,12 @@ .share-button-wrapper { display: flex; - justify-content: start; + justify-content: end; + margin-top: 10px; gap: 10px; } + +.share-button-wrapper a { + border: 1px solid var(--quaternary-color); + padding: 2px 6px; +} diff --git a/client/src/templates/Challenges/components/independent-lower-jaw.test.tsx b/client/src/templates/Challenges/components/independent-lower-jaw.test.tsx index 245577e2dbc..027b362bc59 100644 --- a/client/src/templates/Challenges/components/independent-lower-jaw.test.tsx +++ b/client/src/templates/Challenges/components/independent-lower-jaw.test.tsx @@ -10,6 +10,11 @@ import { createStore } from '../../../redux/create-store'; import { mockCurriculumData } from '../utils/__fixtures__/curriculum-data'; import { render } from '../../../../utils/test-utils'; +vi.mock('../../../components/Progress'); +vi.mock('../utils/fetch-all-curriculum-data', () => ({ + useSubmit: () => vi.fn() +})); + const baseChallengeMeta: ChallengeMeta = { block: 'test-block', id: 'test-challenge-id', @@ -21,12 +26,11 @@ const baseChallengeMeta: ChallengeMeta = { }; const passingTests: Test[] = [{ pass: true, text: 'test', testString: 'test' }]; - const baseProps = { openHelpModal: vi.fn(), openResetModal: vi.fn(), executeChallenge: vi.fn(), - submitChallenge: vi.fn(), + saveChallenge: vi.fn(), tests: passingTests, isSignedIn: true, challengeMeta: baseChallengeMeta, diff --git a/client/src/templates/Challenges/components/independent-lower-jaw.tsx b/client/src/templates/Challenges/components/independent-lower-jaw.tsx index 8d6312ab9e3..ee447efb80c 100644 --- a/client/src/templates/Challenges/components/independent-lower-jaw.tsx +++ b/client/src/templates/Challenges/components/independent-lower-jaw.tsx @@ -2,7 +2,17 @@ import React from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { useTranslation } from 'react-i18next'; -import { Button } from '@freecodecamp/ui'; +import { Button, Spacer } from '@freecodecamp/ui'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + faLightbulb, + faClose, + faZap, + faSave, + faClockRotateLeft, + faRotateLeft +} from '@fortawesome/free-solid-svg-icons'; +import Progress from '../../../components/Progress'; import { completedChallengesIdsSelector, isSignedInSelector @@ -16,10 +26,10 @@ import { } from '../redux/selectors'; import { apiLocation } from '../../../../config/env.json'; import { openModal, executeChallenge } from '../redux/actions'; +import { saveChallenge } from '../../../redux/actions'; import Help from '../../../assets/icons/help'; import callGA from '../../../analytics/call-ga'; import { Share } from '../../../components/share'; -import Reset from '../../../assets/icons/reset'; import { useSubmit } from '../utils/fetch-all-curriculum-data'; import './independent-lower-jaw.css'; @@ -51,13 +61,15 @@ const mapStateToProps = createSelector( const mapDispatchToProps = { openHelpModal: () => openModal('help'), openResetModal: () => openModal('reset'), - executeChallenge + executeChallenge, + saveChallenge }; interface IndependentLowerJawProps { openHelpModal: () => void; openResetModal: () => void; executeChallenge: () => void; + saveChallenge: () => void; tests: Test[]; isSignedIn: boolean; challengeMeta: ChallengeMeta; @@ -69,6 +81,7 @@ export function IndependentLowerJaw({ openHelpModal, openResetModal, executeChallenge, + saveChallenge, tests, isSignedIn, challengeMeta, @@ -123,6 +136,7 @@ export function IndependentLowerJaw({ }; const isMacOS = navigator.userAgent.includes('Mac OS'); + const showRevertButton = isSignedIn && challengeMeta.saveSubmissionToDB; const checkButtonText = isMacOS ? t('buttons.command-enter') : t('buttons.ctrl-enter'); @@ -138,14 +152,19 @@ export function IndependentLowerJaw({ className='hint-container' data-playwright-test-label='independentLowerJaw-failing-hint' > +
+ + +
-
)} {isChallengeComplete && showSubmissionHint && ( @@ -153,18 +172,37 @@ export function IndependentLowerJaw({ className='hint-container' data-playwright-test-label='independentLowerJaw-submission-hint' > -
-

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

- {isSignedIn && showShareButton && ( -
- -
- )} - {!isSignedIn && ( +
+ + +
+ {t('learn.congratulations-code-passes')} +
+ +
+ {isSignedIn && showShareButton && ( +
+ +
+ )} + {!isSignedIn && ( + <> + {t('learn.sign-in-save')} - )} -
- + + )} )} @@ -198,6 +229,7 @@ export function IndependentLowerJaw({ className={`${isSignedIn && 'btn-cta'} tooltip`} id='independent-lower-jaw-submit-button' data-playwright-test-label='independentLowerJaw-submit-button' + aria-label={t('buttons.submit-continue')} onClick={() => submitChallenge()} ref={submitButtonRef} > @@ -211,6 +243,7 @@ export function IndependentLowerJaw({ type='button' className='btn-cta tooltip' data-playwright-test-label='independentLowerJaw-check-button' + aria-label={t('buttons.check-code')} onClick={handleCheckButtonClick} > {t('buttons.check-code')} @@ -221,19 +254,46 @@ export function IndependentLowerJaw({ )}
- + {showRevertButton ? ( + <> + + + + ) : ( + + )}