feat: use growthbook to determine next and previous challenges (#57435)

This commit is contained in:
Oliver Eyton-Williams
2024-12-12 18:27:41 +01:00
committed by GitHub
parent 3e0b2b914c
commit 827b9e3ecd
14 changed files with 237 additions and 37 deletions
+52 -2
View File
@@ -4,8 +4,12 @@ const { createFilePath } = require('gatsby-source-filesystem');
const uniq = require('lodash/uniq'); const uniq = require('lodash/uniq');
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
const webpack = require('webpack'); const webpack = require('webpack');
const env = require('./config/env.json');
const {
superBlockStages,
SuperBlockStage
} = require('../shared/config/curriculum');
const env = require('./config/env.json');
const { const {
createChallengePages, createChallengePages,
createBlockIntroPages, createBlockIntroPages,
@@ -135,8 +139,54 @@ exports.createPages = async function createPages({
} }
`); `);
const allChallengeNodes = result.data.allChallengeNode.edges.map(
({ node }) => node
);
const inNextCurriculum = superBlock =>
superBlockStages[SuperBlockStage.Next].includes(superBlock);
const currentChallengeNodes = allChallengeNodes.filter(
node => !inNextCurriculum(node.challenge.superBlock)
);
const createIdToNextPathMap = nodes =>
nodes.reduce((map, node, index) => {
const nextNode = nodes[index + 1];
const nextPath = nextNode ? nextNode.challenge.fields.slug : null;
if (nextPath) map[node.id] = nextPath;
return map;
}, {});
const createIdToPrevPathMap = nodes =>
nodes.reduce((map, node, index) => {
const prevNode = nodes[index - 1];
const prevPath = prevNode ? prevNode.challenge.fields.slug : null;
if (prevPath) map[node.id] = prevPath;
return map;
}, {});
const idToNextPathCurrentCurriculum = createIdToNextPathMap(
currentChallengeNodes
);
const idToPrevPathCurrentCurriculum = createIdToPrevPathMap(
currentChallengeNodes
);
const idToNextPathNextCurriculum = createIdToNextPathMap(allChallengeNodes);
const idToPrevPathNextCurriculum = createIdToPrevPathMap(allChallengeNodes);
// Create challenge pages. // Create challenge pages.
result.data.allChallengeNode.edges.forEach(createChallengePages(createPage)); result.data.allChallengeNode.edges.forEach(
createChallengePages(createPage, {
idToNextPathCurrentCurriculum,
idToPrevPathCurrentCurriculum,
idToNextPathNextCurriculum,
idToPrevPathNextCurriculum
})
);
const blocks = uniq( const blocks = uniq(
result.data.allChallengeNode.edges.map( result.data.allChallengeNode.edges.map(
+5 -2
View File
@@ -404,14 +404,17 @@ export type ChallengeMeta = {
id: string; id: string;
introPath: string; introPath: string;
isFirstStep: boolean; isFirstStep: boolean;
nextChallengePath: string | null;
prevChallengePath: string | null;
superBlock: SuperBlocks; superBlock: SuperBlocks;
title?: string; title?: string;
challengeType?: number; challengeType?: number;
helpCategory: string; helpCategory: string;
disableLoopProtectTests: boolean; disableLoopProtectTests: boolean;
disableLoopProtectPreview: boolean; disableLoopProtectPreview: boolean;
} & NavigationPaths;
export type NavigationPaths = {
nextChallengePath?: string;
prevChallengePath?: string;
}; };
export type PortfolioProjectData = { export type PortfolioProjectData = {
@@ -21,6 +21,7 @@ import {
ChallengeMeta, ChallengeMeta,
ChallengeNode, ChallengeNode,
CompletedChallenge, CompletedChallenge,
NavigationPaths,
ResizeProps, ResizeProps,
SavedChallengeFiles, SavedChallengeFiles,
Test Test
@@ -62,6 +63,7 @@ import { getGuideUrl } from '../utils';
import { preloadPage } from '../../../../utils/gatsby/page-loading'; import { preloadPage } from '../../../../utils/gatsby/page-loading';
import envData from '../../../../config/env.json'; import envData from '../../../../config/env.json';
import ToolPanel from '../components/tool-panel'; import ToolPanel from '../components/tool-panel';
import { getChallengePaths } from '../utils/challenge-paths';
import { XtermTerminal } from './xterm'; import { XtermTerminal } from './xterm';
import MultifileEditor from './multifile-editor'; import MultifileEditor from './multifile-editor';
import DesktopLayout from './desktop-layout'; import DesktopLayout from './desktop-layout';
@@ -111,6 +113,7 @@ interface ShowClassicProps extends Pick<PreviewProps, 'previewMounted'> {
output: string[]; output: string[];
pageContext: { pageContext: {
challengeMeta: ChallengeMeta; challengeMeta: ChallengeMeta;
nextCurriculumPaths: NavigationPaths;
projectPreview: { projectPreview: {
challengeData: CompletedChallenge; challengeData: CompletedChallenge;
}; };
@@ -205,6 +208,7 @@ function ShowClassic({
pageContext: { pageContext: {
challengeMeta, challengeMeta,
challengeMeta: { isFirstStep, nextChallengePath }, challengeMeta: { isFirstStep, nextChallengePath },
nextCurriculumPaths,
projectPreview: { challengeData } projectPreview: { challengeData }
}, },
createFiles, createFiles,
@@ -232,6 +236,8 @@ function ShowClassic({
const isMobile = useMediaQuery({ const isMobile = useMediaQuery({
query: `(max-width: ${MAX_MOBILE_WIDTH}px)` query: `(max-width: ${MAX_MOBILE_WIDTH}px)`
}); });
const showNextCurriculum = useFeature('fcc-10').on;
const guideUrl = getGuideUrl({ forumTopicId, title }); const guideUrl = getGuideUrl({ forumTopicId, title });
const blockNameTitle = `${t( const blockNameTitle = `${t(
@@ -370,11 +376,18 @@ function ShowClassic({
// project and is shown (once) automatically. In contrast, labs are more // project and is shown (once) automatically. In contrast, labs are more
// freeform, so the preview is shown on demand. // freeform, so the preview is shown on demand.
if (demoType === 'onLoad') openModal('projectPreview'); if (demoType === 'onLoad') openModal('projectPreview');
const challengePaths = getChallengePaths({
showNextCurriculum,
currentCurriculumPaths: challengeMeta,
nextCurriculumPaths
});
updateChallengeMeta({ updateChallengeMeta({
...challengeMeta, ...challengeMeta,
title, title,
challengeType, challengeType,
helpCategory helpCategory,
...challengePaths
}); });
challengeMounted(challengeMeta.id); challengeMounted(challengeMeta.id);
setIsAdvancing(false); setIsAdvancing(false);
@@ -9,6 +9,7 @@ import { bindActionCreators } from 'redux';
import type { Dispatch } from 'redux'; import type { Dispatch } from 'redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { Container, Col, Row, Alert, Spacer } from '@freecodecamp/ui'; import { Container, Col, Row, Alert, Spacer } from '@freecodecamp/ui';
import { useFeature } from '@growthbook/growthbook-react';
// Local Utilities // Local Utilities
import LearnLayout from '../../../components/layouts/learn'; import LearnLayout from '../../../components/layouts/learn';
@@ -39,9 +40,11 @@ import {
ChallengeNode, ChallengeNode,
ChallengeMeta, ChallengeMeta,
CompletedChallenge, CompletedChallenge,
NavigationPaths,
Test Test
} from '../../../redux/prop-types'; } from '../../../redux/prop-types';
import ProjectToolPanel from '../projects/tool-panel'; import ProjectToolPanel from '../projects/tool-panel';
import { getChallengePaths } from '../utils/challenge-paths';
import SolutionForm from '../projects/solution-form'; import SolutionForm from '../projects/solution-form';
import { FlashMessages } from '../../../components/Flash/redux/flash-messages'; import { FlashMessages } from '../../../components/Flash/redux/flash-messages';
import { SuperBlocks } from '../../../../../shared/config/curriculum'; import { SuperBlocks } from '../../../../../shared/config/curriculum';
@@ -99,6 +102,7 @@ interface ShowCodeAllyProps {
openCompletionModal: () => void; openCompletionModal: () => void;
pageContext: { pageContext: {
challengeMeta: ChallengeMeta; challengeMeta: ChallengeMeta;
nextCurriculumPaths: NavigationPaths;
}; };
partiallyCompletedChallenges: CompletedChallenge[]; partiallyCompletedChallenges: CompletedChallenge[];
t: TFunction; t: TFunction;
@@ -111,6 +115,8 @@ interface ShowCodeAllyProps {
function ShowCodeAlly(props: ShowCodeAllyProps) { function ShowCodeAlly(props: ShowCodeAllyProps) {
const container = useRef<HTMLElement>(null); const container = useRef<HTMLElement>(null);
const showNextCurriculum = useFeature('fcc-10').on;
const { const {
completedChallenges, completedChallenges,
data: { data: {
@@ -130,6 +136,7 @@ function ShowCodeAlly(props: ShowCodeAllyProps) {
}, },
isChallengeCompleted, isChallengeCompleted,
isSignedIn, isSignedIn,
pageContext: { nextCurriculumPaths },
partiallyCompletedChallenges, partiallyCompletedChallenges,
t, t,
updateSolutionFormValues updateSolutionFormValues
@@ -166,11 +173,17 @@ function ShowCodeAlly(props: ShowCodeAllyProps) {
updateChallengeMeta updateChallengeMeta
} = props; } = props;
initTests(tests); initTests(tests);
const challengePaths = getChallengePaths({
showNextCurriculum,
currentCurriculumPaths: challengeMeta,
nextCurriculumPaths
});
updateChallengeMeta({ updateChallengeMeta({
...challengeMeta, ...challengeMeta,
title, title,
challengeType, challengeType,
helpCategory helpCategory,
...challengePaths
}); });
challengeMounted(challengeMeta.id); challengeMounted(challengeMeta.id);
container.current?.focus(); container.current?.focus();
+13 -1
View File
@@ -11,6 +11,7 @@ import type { Dispatch } from 'redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { Container, Col, Alert, Row, Button, Spacer } from '@freecodecamp/ui'; import { Container, Col, Alert, Row, Button, Spacer } from '@freecodecamp/ui';
import { micromark } from 'micromark'; import { micromark } from 'micromark';
import { useFeature } from '@growthbook/growthbook-react';
// Local Utilities // Local Utilities
import LearnLayout from '../../../components/layouts/learn'; import LearnLayout from '../../../components/layouts/learn';
@@ -46,6 +47,7 @@ import {
CompletedChallenge, CompletedChallenge,
UserExamQuestion, UserExamQuestion,
UserExam, UserExam,
NavigationPaths,
GeneratedExamResults, GeneratedExamResults,
GeneratedExamQuestion, GeneratedExamQuestion,
PrerequisiteChallenge, PrerequisiteChallenge,
@@ -54,6 +56,7 @@ import {
} from '../../../redux/prop-types'; } from '../../../redux/prop-types';
import { FlashMessages } from '../../../components/Flash/redux/flash-messages'; import { FlashMessages } from '../../../components/Flash/redux/flash-messages';
import { formatSecondsToTime } from '../../../utils/format-seconds'; import { formatSecondsToTime } from '../../../utils/format-seconds';
import { getChallengePaths } from '../utils/challenge-paths';
import ExitExamModal from './components/exit-exam-modal'; import ExitExamModal from './components/exit-exam-modal';
import FinishExamModal from './components/finish-exam-modal'; import FinishExamModal from './components/finish-exam-modal';
import ExamResults from './components/exam-results'; import ExamResults from './components/exam-results';
@@ -127,6 +130,7 @@ interface ShowExamProps {
closeFinishExamModal: () => void; closeFinishExamModal: () => void;
pageContext: { pageContext: {
challengeMeta: ChallengeMeta; challengeMeta: ChallengeMeta;
nextCurriculumPaths: NavigationPaths;
}; };
t: TFunction; t: TFunction;
startExam: () => void; startExam: () => void;
@@ -164,11 +168,13 @@ function ShowExam(props: ShowExamProps) {
isChallengeCompleted, isChallengeCompleted,
openExitExamModal, openExitExamModal,
openFinishExamModal, openFinishExamModal,
pageContext: { nextCurriculumPaths },
t t
} = props; } = props;
let timerInterval: NodeJS.Timeout; let timerInterval: NodeJS.Timeout;
const showNextCurriculum = useFeature('fcc-10').on;
const container = useRef<HTMLElement>(null); const container = useRef<HTMLElement>(null);
const [examTimeInSeconds, setExamTimeInSeconds] = useState(0); const [examTimeInSeconds, setExamTimeInSeconds] = useState(0);
@@ -198,11 +204,17 @@ function ShowExam(props: ShowExamProps) {
updateChallengeMeta updateChallengeMeta
} = props; } = props;
initTests(tests); initTests(tests);
const challengePaths = getChallengePaths({
showNextCurriculum,
currentCurriculumPaths: challengeMeta,
nextCurriculumPaths
});
updateChallengeMeta({ updateChallengeMeta({
...challengeMeta, ...challengeMeta,
title, title,
challengeType, challengeType,
helpCategory helpCategory,
...challengePaths
}); });
challengeMounted(challengeMeta.id); challengeMounted(challengeMeta.id);
@@ -1,6 +1,7 @@
// Package Utilities // Package Utilities
import { graphql } from 'gatsby'; import { graphql } from 'gatsby';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { useFeature } from '@growthbook/growthbook-react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import { ObserveKeys } from 'react-hotkeys'; import { ObserveKeys } from 'react-hotkeys';
import type { TFunction } from 'i18next'; import type { TFunction } from 'i18next';
@@ -14,7 +15,12 @@ import ShortcutsModal from '../components/shortcuts-modal';
// Local Utilities // Local Utilities
import LearnLayout from '../../../components/layouts/learn'; import LearnLayout from '../../../components/layouts/learn';
import { ChallengeNode, ChallengeMeta, Test } from '../../../redux/prop-types'; import {
ChallengeNode,
ChallengeMeta,
NavigationPaths,
Test
} from '../../../redux/prop-types';
import Hotkeys from '../components/hotkeys'; import Hotkeys from '../components/hotkeys';
import ChallengeTitle from '../components/challenge-title'; import ChallengeTitle from '../components/challenge-title';
import ChallegeExplanation from '../components/challenge-explanation'; import ChallegeExplanation from '../components/challenge-explanation';
@@ -30,6 +36,7 @@ import {
initTests initTests
} from '../redux/actions'; } from '../redux/actions';
import Scene from '../components/scene/scene'; import Scene from '../components/scene/scene';
import { getChallengePaths } from '../utils/challenge-paths';
import { isChallengeCompletedSelector } from '../redux/selectors'; import { isChallengeCompletedSelector } from '../redux/selectors';
import './show.css'; import './show.css';
@@ -64,6 +71,7 @@ interface ShowFillInTheBlankProps {
openHelpModal: () => void; openHelpModal: () => void;
pageContext: { pageContext: {
challengeMeta: ChallengeMeta; challengeMeta: ChallengeMeta;
nextCurriculumPaths: NavigationPaths;
}; };
t: TFunction; t: TFunction;
updateChallengeMeta: (arg0: ChallengeMeta) => void; updateChallengeMeta: (arg0: ChallengeMeta) => void;
@@ -93,7 +101,7 @@ const ShowFillInTheBlank = ({
openHelpModal, openHelpModal,
updateChallengeMeta, updateChallengeMeta,
openCompletionModal, openCompletionModal,
pageContext: { challengeMeta }, pageContext: { challengeMeta, nextCurriculumPaths },
isChallengeCompleted isChallengeCompleted
}: ShowFillInTheBlankProps) => { }: ShowFillInTheBlankProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -109,14 +117,21 @@ const ShowFillInTheBlank = ({
const [isScenePlaying, setIsScenePlaying] = useState(false); const [isScenePlaying, setIsScenePlaying] = useState(false);
const container = useRef<HTMLElement | null>(null); const container = useRef<HTMLElement | null>(null);
const showNextCurriculum = useFeature('fcc-10').on;
useEffect(() => { useEffect(() => {
initTests(tests); initTests(tests);
const challengePaths = getChallengePaths({
showNextCurriculum,
currentCurriculumPaths: challengeMeta,
nextCurriculumPaths
});
updateChallengeMeta({ updateChallengeMeta({
...challengeMeta, ...challengeMeta,
title, title,
challengeType, challengeType,
helpCategory helpCategory,
...challengePaths
}); });
challengeMounted(challengeMeta.id); challengeMounted(challengeMeta.id);
container.current?.focus(); container.current?.focus();
@@ -1,5 +1,6 @@
import { graphql } from 'gatsby'; import { graphql } from 'gatsby';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { useFeature } from '@growthbook/growthbook-react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@@ -8,7 +9,12 @@ import { isEqual } from 'lodash';
// Local Utilities // Local Utilities
import LearnLayout from '../../../components/layouts/learn'; import LearnLayout from '../../../components/layouts/learn';
import { ChallengeNode, ChallengeMeta, Test } from '../../../redux/prop-types'; import {
ChallengeNode,
ChallengeMeta,
NavigationPaths,
Test
} from '../../../redux/prop-types';
import ChallengeDescription from '../components/challenge-description'; import ChallengeDescription from '../components/challenge-description';
import Hotkeys from '../components/hotkeys'; import Hotkeys from '../components/hotkeys';
import ChallengeTitle from '../components/challenge-title'; import ChallengeTitle from '../components/challenge-title';
@@ -24,6 +30,7 @@ import {
} from '../redux/actions'; } from '../redux/actions';
import { isChallengeCompletedSelector } from '../redux/selectors'; import { isChallengeCompletedSelector } from '../redux/selectors';
import { BlockTypes } from '../../../../../shared/config/blocks'; import { BlockTypes } from '../../../../../shared/config/blocks';
import { getChallengePaths } from '../utils/challenge-paths';
import Scene from '../components/scene/scene'; import Scene from '../components/scene/scene';
import MultipleChoiceQuestions from '../components/multiple-choice-questions'; import MultipleChoiceQuestions from '../components/multiple-choice-questions';
import ChallengeExplanation from '../components/challenge-explanation'; import ChallengeExplanation from '../components/challenge-explanation';
@@ -58,6 +65,7 @@ interface ShowQuizProps {
openHelpModal: () => void; openHelpModal: () => void;
pageContext: { pageContext: {
challengeMeta: ChallengeMeta; challengeMeta: ChallengeMeta;
nextCurriculumPaths: NavigationPaths;
}; };
updateChallengeMeta: (arg0: ChallengeMeta) => void; updateChallengeMeta: (arg0: ChallengeMeta) => void;
updateSolutionFormValues: () => void; updateSolutionFormValues: () => void;
@@ -88,7 +96,7 @@ const ShowGeneric = ({
} }
} }
}, },
pageContext: { challengeMeta }, pageContext: { challengeMeta, nextCurriculumPaths },
initTests, initTests,
updateChallengeMeta, updateChallengeMeta,
openCompletionModal, openCompletionModal,
@@ -104,11 +112,17 @@ const ShowGeneric = ({
useEffect(() => { useEffect(() => {
initTests(tests); initTests(tests);
const challengePaths = getChallengePaths({
showNextCurriculum,
currentCurriculumPaths: challengeMeta,
nextCurriculumPaths
});
updateChallengeMeta({ updateChallengeMeta({
...challengeMeta, ...challengeMeta,
title, title,
challengeType, challengeType,
helpCategory helpCategory,
...challengePaths
}); });
challengeMounted(challengeMeta.id); challengeMounted(challengeMeta.id);
container.current?.focus(); container.current?.focus();
@@ -146,6 +160,8 @@ const ShowGeneric = ({
); );
const [showFeedback, setShowFeedback] = useState(false); const [showFeedback, setShowFeedback] = useState(false);
const showNextCurriculum = useFeature('fcc-10').on;
const handleMcqOptionChange = ( const handleMcqOptionChange = (
questionIndex: number, questionIndex: number,
answerIndex: number answerIndex: number
@@ -7,14 +7,21 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import type { Dispatch } from 'redux'; import type { Dispatch } from 'redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { useFeature } from '@growthbook/growthbook-react';
import { Container, Col, Row, Button, Spacer } from '@freecodecamp/ui'; import { Container, Col, Row, Button, Spacer } from '@freecodecamp/ui';
import LearnLayout from '../../../components/layouts/learn'; import LearnLayout from '../../../components/layouts/learn';
import { ChallengeNode, ChallengeMeta, Test } from '../../../redux/prop-types'; import {
ChallengeNode,
ChallengeMeta,
NavigationPaths,
Test
} from '../../../redux/prop-types';
import ChallengeDescription from '../components/challenge-description'; import ChallengeDescription from '../components/challenge-description';
import Hotkeys from '../components/hotkeys'; import Hotkeys from '../components/hotkeys';
import ChallengeTitle from '../components/challenge-title'; import ChallengeTitle from '../components/challenge-title';
import CompletionModal from '../components/completion-modal'; import CompletionModal from '../components/completion-modal';
import { getChallengePaths } from '../utils/challenge-paths';
import HelpModal from '../components/help-modal'; import HelpModal from '../components/help-modal';
import { import {
challengeMounted, challengeMounted,
@@ -76,6 +83,7 @@ interface MsTrophyProps {
openHelpModal: () => void; openHelpModal: () => void;
pageContext: { pageContext: {
challengeMeta: ChallengeMeta; challengeMeta: ChallengeMeta;
nextCurriculumPaths: NavigationPaths;
}; };
submitChallenge: () => void; submitChallenge: () => void;
t: TFunction; t: TFunction;
@@ -83,6 +91,7 @@ interface MsTrophyProps {
} }
function MsTrophy(props: MsTrophyProps) { function MsTrophy(props: MsTrophyProps) {
const showNextCurriculum = useFeature('fcc-10').on;
const container = useRef<HTMLElement>(null); const container = useRef<HTMLElement>(null);
const { const {
data: { data: {
@@ -104,16 +113,22 @@ function MsTrophy(props: MsTrophyProps) {
} }
} }
}, },
pageContext: { challengeMeta }, pageContext: { challengeMeta, nextCurriculumPaths },
initTests, initTests,
updateChallengeMeta updateChallengeMeta
} = props; } = props;
initTests(tests); initTests(tests);
const challengePaths = getChallengePaths({
showNextCurriculum,
currentCurriculumPaths: challengeMeta,
nextCurriculumPaths
});
updateChallengeMeta({ updateChallengeMeta({
...challengeMeta, ...challengeMeta,
title, title,
challengeType, challengeType,
helpCategory helpCategory,
...challengePaths
}); });
challengeMounted(challengeMeta.id); challengeMounted(challengeMeta.id);
container.current?.focus(); container.current?.focus();
@@ -6,12 +6,14 @@ import type { TFunction } from 'i18next';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { Container, Col, Row, Spacer } from '@freecodecamp/ui'; import { Container, Col, Row, Spacer } from '@freecodecamp/ui';
import { useFeature } from '@growthbook/growthbook-react';
import LearnLayout from '../../../../components/layouts/learn'; import LearnLayout from '../../../../components/layouts/learn';
import { isSignedInSelector } from '../../../../redux/selectors'; import { isSignedInSelector } from '../../../../redux/selectors';
import { import {
ChallengeMeta, ChallengeMeta,
ChallengeNode, ChallengeNode,
NavigationPaths,
Test Test
} from '../../../../redux/prop-types'; } from '../../../../redux/prop-types';
import ChallengeDescription from '../../components/challenge-description'; import ChallengeDescription from '../../components/challenge-description';
@@ -34,7 +36,9 @@ import {
consoleOutputSelector, consoleOutputSelector,
isChallengeCompletedSelector isChallengeCompletedSelector
} from '../../redux/selectors'; } from '../../redux/selectors';
import { getGuideUrl } from '../../utils'; import { getGuideUrl } from '../../utils';
import { getChallengePaths } from '../../utils/challenge-paths';
import SolutionForm from '../solution-form'; import SolutionForm from '../solution-form';
import ProjectToolPanel from '../tool-panel'; import ProjectToolPanel from '../tool-panel';
@@ -83,6 +87,7 @@ interface BackEndProps {
output: string[]; output: string[];
pageContext: { pageContext: {
challengeMeta: ChallengeMeta; challengeMeta: ChallengeMeta;
nextCurriculumPaths: NavigationPaths;
}; };
t: TFunction; t: TFunction;
tests: Test[]; tests: Test[];
@@ -92,6 +97,7 @@ interface BackEndProps {
} }
const ShowBackEnd = (props: BackEndProps) => { const ShowBackEnd = (props: BackEndProps) => {
const showNextCurriculum = useFeature('fcc-10').on;
const container = useRef<HTMLElement>(null); const container = useRef<HTMLElement>(null);
const handleSubmit = ({ const handleSubmit = ({
@@ -118,15 +124,21 @@ const ShowBackEnd = (props: BackEndProps) => {
} }
} }
}, },
pageContext: { challengeMeta } pageContext: { challengeMeta, nextCurriculumPaths }
} = props; } = props;
initConsole(); initConsole();
initTests(tests); initTests(tests);
const challengePaths = getChallengePaths({
showNextCurriculum,
currentCurriculumPaths: challengeMeta,
nextCurriculumPaths
});
updateChallengeMeta({ updateChallengeMeta({
...challengeMeta, ...challengeMeta,
title, title,
challengeType, challengeType,
helpCategory helpCategory,
...challengePaths
}); });
challengeMounted(challengeMeta.id); challengeMounted(challengeMeta.id);
container.current?.focus(); container.current?.focus();
@@ -8,11 +8,13 @@ import { bindActionCreators } from 'redux';
import type { Dispatch } from 'redux'; import type { Dispatch } from 'redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { Container, Col, Row, Spacer } from '@freecodecamp/ui'; import { Container, Col, Row, Spacer } from '@freecodecamp/ui';
import { useFeature } from '@growthbook/growthbook-react';
import LearnLayout from '../../../../components/layouts/learn'; import LearnLayout from '../../../../components/layouts/learn';
import { import {
ChallengeNode, ChallengeNode,
ChallengeMeta, ChallengeMeta,
NavigationPaths,
Test Test
} from '../../../../redux/prop-types'; } from '../../../../redux/prop-types';
import ChallengeDescription from '../../components/challenge-description'; import ChallengeDescription from '../../components/challenge-description';
@@ -31,6 +33,7 @@ import { isChallengeCompletedSelector } from '../../redux/selectors';
import { getGuideUrl } from '../../utils'; import { getGuideUrl } from '../../utils';
import SolutionForm from '../solution-form'; import SolutionForm from '../solution-form';
import ProjectToolPanel from '../tool-panel'; import ProjectToolPanel from '../tool-panel';
import { getChallengePaths } from '../../utils/challenge-paths';
// Redux Setup // Redux Setup
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
@@ -61,6 +64,7 @@ interface ProjectProps {
openCompletionModal: () => void; openCompletionModal: () => void;
pageContext: { pageContext: {
challengeMeta: ChallengeMeta; challengeMeta: ChallengeMeta;
nextCurriculumPaths: NavigationPaths;
}; };
t: TFunction; t: TFunction;
updateChallengeMeta: (arg0: ChallengeMeta) => void; updateChallengeMeta: (arg0: ChallengeMeta) => void;
@@ -78,6 +82,7 @@ const ShowFrontEndProject = (props: ProjectProps) => {
} }
}; };
const showNextCurriculum = useFeature('fcc-10').on;
const container = useRef<HTMLElement>(null); const container = useRef<HTMLElement>(null);
useEffect(() => { useEffect(() => {
@@ -88,16 +93,22 @@ const ShowFrontEndProject = (props: ProjectProps) => {
challenge: { fields, title, challengeType, helpCategory } challenge: { fields, title, challengeType, helpCategory }
} }
}, },
pageContext: { challengeMeta }, pageContext: { challengeMeta, nextCurriculumPaths },
initTests, initTests,
updateChallengeMeta updateChallengeMeta
} = props; } = props;
initTests(fields.tests); initTests(fields.tests);
const challengePaths = getChallengePaths({
showNextCurriculum,
currentCurriculumPaths: challengeMeta,
nextCurriculumPaths
});
updateChallengeMeta({ updateChallengeMeta({
...challengeMeta, ...challengeMeta,
title, title,
challengeType, challengeType,
helpCategory helpCategory,
...challengePaths
}); });
challengeMounted(challengeMeta.id); challengeMounted(challengeMeta.id);
container.current?.focus(); container.current?.focus();
+18 -3
View File
@@ -17,15 +17,22 @@ import {
useQuiz, useQuiz,
Spacer Spacer
} from '@freecodecamp/ui'; } from '@freecodecamp/ui';
import { useFeature } from '@growthbook/growthbook-react';
// Local Utilities // Local Utilities
import { shuffleArray } from '../../../../../shared/utils/shuffle-array'; import { shuffleArray } from '../../../../../shared/utils/shuffle-array';
import LearnLayout from '../../../components/layouts/learn'; import LearnLayout from '../../../components/layouts/learn';
import { ChallengeNode, ChallengeMeta, Test } from '../../../redux/prop-types'; import {
ChallengeNode,
ChallengeMeta,
NavigationPaths,
Test
} from '../../../redux/prop-types';
import ChallengeDescription from '../components/challenge-description'; import ChallengeDescription from '../components/challenge-description';
import Hotkeys from '../components/hotkeys'; import Hotkeys from '../components/hotkeys';
import ChallengeTitle from '../components/challenge-title'; import ChallengeTitle from '../components/challenge-title';
import CompletionModal from '../components/completion-modal'; import CompletionModal from '../components/completion-modal';
import { getChallengePaths } from '../utils/challenge-paths';
import { import {
challengeMounted, challengeMounted,
updateChallengeMeta, updateChallengeMeta,
@@ -74,6 +81,7 @@ interface ShowQuizProps {
isChallengeCompleted: boolean; isChallengeCompleted: boolean;
pageContext: { pageContext: {
challengeMeta: ChallengeMeta; challengeMeta: ChallengeMeta;
nextCurriculumPaths: NavigationPaths;
}; };
updateChallengeMeta: (arg0: ChallengeMeta) => void; updateChallengeMeta: (arg0: ChallengeMeta) => void;
updateSolutionFormValues: () => void; updateSolutionFormValues: () => void;
@@ -101,7 +109,7 @@ const ShowQuiz = ({
} }
} }
}, },
pageContext: { challengeMeta }, pageContext: { challengeMeta, nextCurriculumPaths },
initTests, initTests,
updateChallengeMeta, updateChallengeMeta,
isChallengeCompleted, isChallengeCompleted,
@@ -126,6 +134,7 @@ const ShowQuiz = ({
const [showUnanswered, setShowUnanswered] = useState(false); const [showUnanswered, setShowUnanswered] = useState(false);
const [exitConfirmed, setExitConfirmed] = useState(false); const [exitConfirmed, setExitConfirmed] = useState(false);
const showNextCurriculum = useFeature('fcc-10').on;
const blockNameTitle = `${t( const blockNameTitle = `${t(
`intro:${superBlock}.blocks.${block}.title` `intro:${superBlock}.blocks.${block}.title`
@@ -189,11 +198,17 @@ const ShowQuiz = ({
useEffect(() => { useEffect(() => {
initTests(tests); initTests(tests);
const challengePaths = getChallengePaths({
showNextCurriculum,
currentCurriculumPaths: challengeMeta,
nextCurriculumPaths
});
updateChallengeMeta({ updateChallengeMeta({
...challengeMeta, ...challengeMeta,
title, title,
challengeType, challengeType,
helpCategory helpCategory,
...challengePaths
}); });
challengeMounted(challengeMeta.id); challengeMounted(challengeMeta.id);
container.current?.focus(); container.current?.focus();
@@ -0,0 +1,23 @@
import { NavigationPaths } from '../../../redux/prop-types';
export const getChallengePaths = ({
showNextCurriculum,
currentCurriculumPaths,
nextCurriculumPaths
}: {
showNextCurriculum: boolean;
currentCurriculumPaths: NavigationPaths;
nextCurriculumPaths: NavigationPaths;
}): NavigationPaths => {
const nextChallengePath = showNextCurriculum
? nextCurriculumPaths.nextChallengePath
: currentCurriculumPaths.nextChallengePath;
const prevChallengePath = showNextCurriculum
? nextCurriculumPaths.prevChallengePath
: currentCurriculumPaths.prevChallengePath;
return {
nextChallengePath,
prevChallengePath
};
};
+15 -13
View File
@@ -72,21 +72,19 @@ function getIsFirstStepInBlock(id, edges) {
return previous.node.challenge.block !== current.node.challenge.block; return previous.node.challenge.block !== current.node.challenge.block;
} }
function getNextChallengePath(id, edges) {
const next = edges[id + 1];
return next ? next.node.challenge.fields.slug : null;
}
function getPrevChallengePath(id, edges) {
const prev = edges[id - 1];
return prev ? prev.node.challenge.fields.slug : null;
}
function getTemplateComponent(challengeType) { function getTemplateComponent(challengeType) {
return views[viewTypes[challengeType]]; return views[viewTypes[challengeType]];
} }
exports.createChallengePages = function (createPage) { exports.createChallengePages = function (
createPage,
{
idToNextPathCurrentCurriculum,
idToPrevPathCurrentCurriculum,
idToNextPathNextCurriculum,
idToPrevPathNextCurriculum
}
) {
return function ({ node }, index, allChallengeEdges) { return function ({ node }, index, allChallengeEdges) {
const { const {
dashedName, dashedName,
@@ -121,10 +119,14 @@ exports.createChallengePages = function (createPage) {
template, template,
required, required,
isLastChallengeInBlock: isLastChallengeInBlock, isLastChallengeInBlock: isLastChallengeInBlock,
nextChallengePath: getNextChallengePath(index, allChallengeEdges), nextChallengePath: idToNextPathCurrentCurriculum[node.id],
prevChallengePath: getPrevChallengePath(index, allChallengeEdges), prevChallengePath: idToPrevPathCurrentCurriculum[node.id],
id id
}, },
nextCurriculumPaths: {
nextChallengePath: idToNextPathNextCurriculum[node.id],
prevChallengePath: idToPrevPathNextCurriculum[node.id]
},
projectPreview: getProjectPreviewConfig( projectPreview: getProjectPreviewConfig(
node.challenge, node.challenge,
allChallengeEdges allChallengeEdges
+1 -1
View File
@@ -3,7 +3,7 @@
* but the link of the page isn't rendered on the screen. * but the link of the page isn't rendered on the screen.
* For more details, see https://github.com/freeCodeCamp/freeCodeCamp/pull/55472. * For more details, see https://github.com/freeCodeCamp/freeCodeCamp/pull/55472.
*/ */
export const preloadPage = (path: string | null) => { export const preloadPage = (path?: string) => {
if (!window.___loader || !path) return; if (!window.___loader || !path) return;
window.___loader.hovering(path); window.___loader.hovering(path);