mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
fix(client): pull curriculum data where needed (#65710)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { TFunction } from 'i18next';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { useStaticQuery, graphql } from 'gatsby';
|
||||
|
||||
import {
|
||||
challengeMetaSelector,
|
||||
@@ -12,29 +11,19 @@ import {
|
||||
completedPercentageSelector
|
||||
} from '../../templates/Challenges/redux/selectors';
|
||||
import { liveCerts } from '../../../config/cert-and-project-map';
|
||||
import { updateAllChallengesInfo } from '../../redux/actions';
|
||||
import type {
|
||||
CertificateNode,
|
||||
ChallengeNode,
|
||||
SuperBlockStructure
|
||||
} from '../../redux/prop-types';
|
||||
import {
|
||||
updateSuperBlockStructures,
|
||||
superBlockStructuresSelector
|
||||
} from '../../templates/Introduction/redux';
|
||||
import { getIsDailyCodingChallenge } from '@freecodecamp/shared/config/challenge-types';
|
||||
import {
|
||||
isValidDateString,
|
||||
formatDisplayDate
|
||||
} from '../daily-coding-challenge/helpers';
|
||||
import ProgressInner from './progress-inner';
|
||||
import { useFetchAllCurriculumData } from '../../templates/Challenges/utils/fetch-all-curriculum-data';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
currentBlockIdsSelector,
|
||||
challengeMetaSelector,
|
||||
completedChallengesInBlockSelector,
|
||||
completedPercentageSelector,
|
||||
superBlockStructuresSelector,
|
||||
(
|
||||
currentBlockIds: string[],
|
||||
{
|
||||
@@ -49,8 +38,7 @@ const mapStateToProps = createSelector(
|
||||
superBlock: string;
|
||||
},
|
||||
completedChallengesInBlock: number,
|
||||
completedPercent: number,
|
||||
superBlockStructures: Record<string, SuperBlockStructure>
|
||||
completedPercent: number
|
||||
) => ({
|
||||
currentBlockIds,
|
||||
challengeType,
|
||||
@@ -58,15 +46,11 @@ const mapStateToProps = createSelector(
|
||||
block,
|
||||
superBlock,
|
||||
completedChallengesInBlock,
|
||||
completedPercent,
|
||||
superBlockStructures
|
||||
completedPercent
|
||||
})
|
||||
);
|
||||
|
||||
const mapDispatchToProps = {
|
||||
updateAllChallengesInfo,
|
||||
updateSuperBlockStructures
|
||||
};
|
||||
const mapDispatchToProps = {};
|
||||
|
||||
type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
@@ -81,11 +65,9 @@ function Progress({
|
||||
challengeType,
|
||||
completedChallengesInBlock,
|
||||
completedPercent,
|
||||
t,
|
||||
updateAllChallengesInfo,
|
||||
updateSuperBlockStructures,
|
||||
superBlockStructures: superBlockStructuresFromStore
|
||||
t
|
||||
}: ProgressProps): JSX.Element {
|
||||
useFetchAllCurriculumData(); // needed to compute completedPercent
|
||||
let blockTitle = t(`intro:${superBlock}.blocks.${block}.title`);
|
||||
// Always false for legacy full stack, since it has no projects.
|
||||
const isCertificationProject = liveCerts.some(cert =>
|
||||
@@ -102,32 +84,6 @@ function Progress({
|
||||
}
|
||||
}
|
||||
|
||||
const { challengeNodes, certificateNodes, superBlockStructureNodes } =
|
||||
useGetAllChallengeData();
|
||||
|
||||
useEffect(() => {
|
||||
updateAllChallengesInfo({ challengeNodes, certificateNodes });
|
||||
|
||||
const structuresMap: Record<string, SuperBlockStructure> = {};
|
||||
|
||||
// The super block structures are pretty static, so we only want to
|
||||
// update them if we don't already have them in the store.
|
||||
if (Object.keys(superBlockStructuresFromStore).length === 0) {
|
||||
superBlockStructureNodes.forEach((node: SuperBlockStructure) => {
|
||||
structuresMap[node.superBlock] = node;
|
||||
});
|
||||
|
||||
updateSuperBlockStructures(structuresMap);
|
||||
}
|
||||
}, [
|
||||
challengeNodes,
|
||||
certificateNodes,
|
||||
superBlockStructureNodes,
|
||||
updateAllChallengesInfo,
|
||||
updateSuperBlockStructures,
|
||||
superBlockStructuresFromStore
|
||||
]);
|
||||
|
||||
const totalChallengesInBlock = currentBlockIds?.length ?? 0;
|
||||
const meta =
|
||||
isCertificationProject && totalChallengesInBlock > 0
|
||||
@@ -152,66 +108,6 @@ function Progress({
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: extract this hook and call it when needed (i.e. here, in the lower-jaw
|
||||
// and in completion-modal). Then we don't have to pass the data into redux.
|
||||
// This would mean that we have to memoize any complex calculations in the hook.
|
||||
// Otherwise, this will undo all the recent performance improvements.
|
||||
const useGetAllChallengeData = () => {
|
||||
const {
|
||||
allChallengeNode: { nodes: challengeNodes },
|
||||
allCertificateNode: { nodes: certificateNodes },
|
||||
allSuperBlockStructure: { nodes: superBlockStructureNodes }
|
||||
}: {
|
||||
allChallengeNode: { nodes: ChallengeNode[] };
|
||||
allCertificateNode: { nodes: CertificateNode[] };
|
||||
allSuperBlockStructure: { nodes: SuperBlockStructure[] };
|
||||
} = useStaticQuery(graphql`
|
||||
query getBlockNode {
|
||||
allChallengeNode(
|
||||
sort: [
|
||||
{ challenge: { superOrder: ASC } }
|
||||
{ challenge: { order: ASC } }
|
||||
{ challenge: { challengeOrder: ASC } }
|
||||
]
|
||||
) {
|
||||
nodes {
|
||||
challenge {
|
||||
block
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
allCertificateNode {
|
||||
nodes {
|
||||
challenge {
|
||||
certification
|
||||
tests {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
allSuperBlockStructure {
|
||||
nodes {
|
||||
superBlock
|
||||
chapters {
|
||||
dashedName
|
||||
comingSoon
|
||||
modules {
|
||||
dashedName
|
||||
comingSoon
|
||||
moduleType
|
||||
blocks
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
return { challengeNodes, certificateNodes, superBlockStructureNodes };
|
||||
};
|
||||
|
||||
Progress.displayName = 'Progress';
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
@@ -124,6 +124,18 @@ export const createUserByNameSelector = username => state => {
|
||||
export const userFetchStateSelector = state => state[MainApp].userFetchState;
|
||||
export const allChallengesInfoSelector = state =>
|
||||
state[MainApp].allChallengesInfo;
|
||||
|
||||
export const needsCurriculumDataSelector = createSelector(
|
||||
allChallengesInfoSelector,
|
||||
superBlockStructuresSelector,
|
||||
(allChallengesInfo, superBlockStructures) => {
|
||||
return (
|
||||
!allChallengesInfo?.challengeNodes?.length ||
|
||||
!Object.keys(superBlockStructures).length
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const getSuperBlockStructure = (state, superBlock) =>
|
||||
superBlockStructuresSelector(state)[superBlock];
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ import {
|
||||
saveEditorContent,
|
||||
setEditorFocusability,
|
||||
updateFile,
|
||||
submitChallenge,
|
||||
initTests,
|
||||
stopResetting,
|
||||
openModal,
|
||||
@@ -70,6 +69,7 @@ import LowerJaw from './lower-jaw';
|
||||
import reactTypes from './react-types.json';
|
||||
|
||||
import './editor.css';
|
||||
import { useSubmit } from '../utils/fetch-all-curriculum-data';
|
||||
|
||||
const MonacoEditor = Loadable(() => import('react-monaco-editor'));
|
||||
|
||||
@@ -105,7 +105,6 @@ export interface EditorProps {
|
||||
saveEditorContent: () => void;
|
||||
saveSubmissionToDB?: boolean;
|
||||
setEditorFocusability: (isFocusable: boolean) => void;
|
||||
submitChallenge: () => void;
|
||||
stopResetting: () => void;
|
||||
resetAttempts: () => void;
|
||||
tests: Test[];
|
||||
@@ -199,7 +198,6 @@ const mapDispatchToProps = {
|
||||
saveEditorContent,
|
||||
setEditorFocusability,
|
||||
updateFile,
|
||||
submitChallenge,
|
||||
initTests,
|
||||
stopResetting,
|
||||
resetAttempts,
|
||||
@@ -296,8 +294,10 @@ const Editor = (props: EditorProps): JSX.Element => {
|
||||
const [lowerJawContainer, setLowerJawContainer] =
|
||||
React.useState<HTMLDivElement | null>(null);
|
||||
|
||||
const submitChallenge = useSubmit();
|
||||
|
||||
const submitChallengeDebounceRef = useRef(
|
||||
debounce(props.submitChallenge, 1000, { leading: true, trailing: false })
|
||||
debounce(submitChallenge, 1000, { leading: true, trailing: false })
|
||||
);
|
||||
|
||||
const player = useRef<{
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
} from '../../../redux/selectors';
|
||||
import { getTestRunner } from '../utils/build';
|
||||
import CompletionModal, { combineFileData } from './completion-modal';
|
||||
import { mockCurriculumData } from '../utils/__fixtures__/curriculum-data';
|
||||
import { useStaticQuery } from 'gatsby';
|
||||
vi.mock('../../../analytics');
|
||||
vi.mock('../../../utils/fire-confetti');
|
||||
vi.mock('../../../components/Progress');
|
||||
@@ -60,6 +62,10 @@ const id = '7';
|
||||
const fakeCompletedChallengesIds = ['1', '3', '5', '7', '8'];
|
||||
|
||||
describe('<CompletionModal />', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(useStaticQuery).mockReturnValue(mockCurriculumData);
|
||||
});
|
||||
|
||||
describe('fireConfetti', () => {
|
||||
beforeEach(() => {
|
||||
mockFireConfetti.mockClear();
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
completedChallengesIdsSelector
|
||||
} from '../../../redux/selectors';
|
||||
import { ChallengeFiles } from '../../../redux/prop-types';
|
||||
import { closeModal, submitChallenge } from '../redux/actions';
|
||||
import { closeModal } from '../redux/actions';
|
||||
import {
|
||||
isCompletionModalOpenSelector,
|
||||
successMessageSelector,
|
||||
@@ -24,6 +24,7 @@ import GreenPass from '../../../assets/icons/green-pass';
|
||||
import { MAX_MOBILE_WIDTH } from '../../../../config/misc';
|
||||
import './completion-modal.css';
|
||||
import callGA from '../../../analytics/call-ga';
|
||||
import { useSubmit } from '../utils/fetch-all-curriculum-data';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
challengeFilesSelector,
|
||||
@@ -54,15 +55,13 @@ const mapStateToProps = createSelector(
|
||||
);
|
||||
|
||||
const mapDispatchToProps = {
|
||||
close: () => closeModal('completion'),
|
||||
submitChallenge
|
||||
close: () => closeModal('completion')
|
||||
};
|
||||
|
||||
type StateProps = ReturnType<typeof mapStateToProps>;
|
||||
|
||||
interface CompletionModalProps extends StateProps {
|
||||
close: () => void;
|
||||
submitChallenge: () => void;
|
||||
t: TFunction;
|
||||
}
|
||||
|
||||
@@ -80,10 +79,10 @@ function CompletionModal({
|
||||
isSignedIn,
|
||||
isSubmitting,
|
||||
message,
|
||||
submitChallenge,
|
||||
t
|
||||
}: CompletionModalProps): JSX.Element {
|
||||
const [downloadURL, setDownloadURL] = useState<string>();
|
||||
const submitChallenge = useSubmit();
|
||||
// We can't useMemo here, because it does not guarantee that the URL object
|
||||
// will be revoked when the dependencies change.
|
||||
useEffect(() => {
|
||||
|
||||
@@ -13,7 +13,6 @@ import type {
|
||||
import { userSelector } from '../../../redux/selectors';
|
||||
import {
|
||||
setEditorFocusability,
|
||||
submitChallenge,
|
||||
openModal,
|
||||
setIsAdvancing
|
||||
} from '../redux/actions';
|
||||
@@ -30,6 +29,7 @@ import {
|
||||
import './hotkeys.css';
|
||||
import { isProjectBased } from '../../../utils/curriculum-layout';
|
||||
import type { EditorProps } from '../classic/editor';
|
||||
import { useSubmit } from '../utils/fetch-all-curriculum-data';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
isHelpModalOpenSelector,
|
||||
@@ -67,7 +67,6 @@ const mapStateToProps = createSelector(
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setEditorFocusability,
|
||||
submitChallenge,
|
||||
openShortcutsModal: () => openModal('shortcuts'),
|
||||
setIsAdvancing
|
||||
};
|
||||
@@ -84,11 +83,7 @@ export type HotkeysProps = Pick<
|
||||
> &
|
||||
Pick<
|
||||
EditorProps,
|
||||
| 'containerRef'
|
||||
| 'tests'
|
||||
| 'challengeFiles'
|
||||
| 'submitChallenge'
|
||||
| 'setEditorFocusability'
|
||||
'containerRef' | 'tests' | 'challengeFiles' | 'setEditorFocusability'
|
||||
> & {
|
||||
isHelpModalOpen?: boolean;
|
||||
isResetModalOpen?: boolean;
|
||||
@@ -116,7 +111,6 @@ function Hotkeys({
|
||||
prevChallengePath,
|
||||
setEditorFocusability,
|
||||
setIsAdvancing,
|
||||
submitChallenge,
|
||||
tests,
|
||||
usesMultifileEditor,
|
||||
openShortcutsModal,
|
||||
@@ -127,6 +121,8 @@ function Hotkeys({
|
||||
isShortcutsModalOpen,
|
||||
isProjectPreviewModalOpen
|
||||
}: HotkeysProps): JSX.Element {
|
||||
const submitChallenge = useSubmit();
|
||||
|
||||
const isModalOpen = [
|
||||
isHelpModalOpen,
|
||||
isResetModalOpen,
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { useStaticQuery } from 'gatsby';
|
||||
|
||||
import type { ChallengeMeta, Test } from '../../../redux/prop-types';
|
||||
import { SuperBlocks } from '@freecodecamp/shared/config/curriculum';
|
||||
import { IndependentLowerJaw } from './independent-lower-jaw';
|
||||
import { createStore } from '../../../redux/create-store';
|
||||
import { mockCurriculumData } from '../utils/__fixtures__/curriculum-data';
|
||||
import { render } from '../../../../utils/test-utils';
|
||||
|
||||
const baseChallengeMeta: ChallengeMeta = {
|
||||
block: 'test-block',
|
||||
@@ -30,9 +35,19 @@ const baseProps = {
|
||||
currentBlockIds: ['id-1', 'test-challenge-id']
|
||||
};
|
||||
|
||||
vi.mock('../../../utils/get-words');
|
||||
|
||||
describe('<IndependentLowerJaw />', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(useStaticQuery).mockReturnValue(mockCurriculumData);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it('shows share buttons when the block is completed on the last step', () => {
|
||||
render(<IndependentLowerJaw {...baseProps} />);
|
||||
render(<IndependentLowerJaw {...baseProps} />, createStore());
|
||||
|
||||
expect(screen.getByTestId('share-on-x')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('share-on-bluesky')).toBeInTheDocument();
|
||||
@@ -45,7 +60,8 @@ describe('<IndependentLowerJaw />', () => {
|
||||
{...baseProps}
|
||||
completedPercent={50}
|
||||
completedChallengeIds={['id-1']}
|
||||
/>
|
||||
/>,
|
||||
createStore()
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('share-on-x')).not.toBeInTheDocument();
|
||||
@@ -57,7 +73,8 @@ describe('<IndependentLowerJaw />', () => {
|
||||
{...baseProps}
|
||||
currentBlockIds={[baseChallengeMeta.id, 'id-2']}
|
||||
completedChallengeIds={[baseChallengeMeta.id, 'id-2']}
|
||||
/>
|
||||
/>,
|
||||
createStore()
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('share-on-x')).not.toBeInTheDocument();
|
||||
|
||||
@@ -15,13 +15,14 @@ import {
|
||||
currentBlockIdsSelector
|
||||
} from '../redux/selectors';
|
||||
import { apiLocation } from '../../../../config/env.json';
|
||||
import { openModal, submitChallenge, executeChallenge } from '../redux/actions';
|
||||
import { openModal, executeChallenge } 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';
|
||||
import Reset from '../../../assets/icons/reset';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
challengeTestsSelector,
|
||||
@@ -50,15 +51,13 @@ const mapStateToProps = createSelector(
|
||||
const mapDispatchToProps = {
|
||||
openHelpModal: () => openModal('help'),
|
||||
openResetModal: () => openModal('reset'),
|
||||
executeChallenge,
|
||||
submitChallenge
|
||||
executeChallenge
|
||||
};
|
||||
|
||||
interface IndependentLowerJawProps {
|
||||
openHelpModal: () => void;
|
||||
openResetModal: () => void;
|
||||
executeChallenge: () => void;
|
||||
submitChallenge: () => void;
|
||||
tests: Test[];
|
||||
isSignedIn: boolean;
|
||||
challengeMeta: ChallengeMeta;
|
||||
@@ -70,7 +69,6 @@ export function IndependentLowerJaw({
|
||||
openHelpModal,
|
||||
openResetModal,
|
||||
executeChallenge,
|
||||
submitChallenge,
|
||||
tests,
|
||||
isSignedIn,
|
||||
challengeMeta,
|
||||
@@ -79,6 +77,7 @@ export function IndependentLowerJaw({
|
||||
currentBlockIds
|
||||
}: IndependentLowerJawProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const submitChallenge = useSubmit();
|
||||
const firstFailedTest = tests.find(test => !!test.err);
|
||||
const hint = firstFailedTest?.message;
|
||||
const [showHint, setShowHint] = React.useState(false);
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
updateChallengeMeta,
|
||||
openModal,
|
||||
closeModal,
|
||||
submitChallenge,
|
||||
setUserCompletedExam,
|
||||
updateSolutionFormValues,
|
||||
initTests
|
||||
@@ -60,6 +59,7 @@ import FinishExamModal from './components/finish-exam-modal';
|
||||
import ExamResults from './components/exam-results';
|
||||
import MissingPrerequisites from './components/missing-prerequisites';
|
||||
import FoundationalCSharpSurveyAlert from './components/foundational-c-sharp-survey-alert';
|
||||
import { useSubmit } from '../utils/fetch-all-curriculum-data';
|
||||
|
||||
import './exam.css';
|
||||
|
||||
@@ -101,7 +101,6 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
stopExam,
|
||||
setUserCompletedExam,
|
||||
clearExamResults,
|
||||
submitChallenge,
|
||||
initTests,
|
||||
updateChallengeMeta,
|
||||
updateSolutionFormValues
|
||||
@@ -132,7 +131,6 @@ interface ShowExamProps {
|
||||
t: TFunction;
|
||||
startExam: () => void;
|
||||
stopExam: () => void;
|
||||
submitChallenge: () => void;
|
||||
setUserCompletedExam: (arg0: UserExam) => void;
|
||||
updateChallengeMeta: (arg0: ChallengeMeta) => void;
|
||||
}
|
||||
@@ -171,6 +169,8 @@ function ShowExam(props: ShowExamProps) {
|
||||
|
||||
const container = useRef<HTMLElement>(null);
|
||||
|
||||
const submitChallenge = useSubmit();
|
||||
|
||||
const [examTimeInSeconds, setExamTimeInSeconds] = useState(0);
|
||||
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
|
||||
const [generatedExamQuestions, setGeneratedExamQuestions] = useState<
|
||||
@@ -309,7 +309,7 @@ function ShowExam(props: ShowExamProps) {
|
||||
// TODO: show loader
|
||||
cleanUp();
|
||||
|
||||
const { setUserCompletedExam, submitChallenge } = props;
|
||||
const { setUserCompletedExam } = props;
|
||||
|
||||
setUserCompletedExam({ userExamQuestions, examTimeInSeconds });
|
||||
submitChallenge();
|
||||
|
||||
@@ -107,7 +107,6 @@ const ShowFillInTheBlank = ({
|
||||
}: ShowFillInTheBlankProps) => {
|
||||
const { t } = useTranslation();
|
||||
const emptyArray = fillInTheBlank.blanks.map(() => null);
|
||||
|
||||
const [showWrong, setShowWrong] = useState(false);
|
||||
const [userAnswers, setUserAnswers] = useState<(null | string)[]>(emptyArray);
|
||||
const [answersCorrect, setAnswersCorrect] =
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
updateChallengeMeta,
|
||||
openModal,
|
||||
updateSolutionFormValues,
|
||||
submitChallenge,
|
||||
initTests
|
||||
} from '../redux/actions';
|
||||
import { isChallengeCompletedSelector } from '../redux/selectors';
|
||||
@@ -32,6 +31,7 @@ import {
|
||||
msUsernameSelector
|
||||
} from '../../../redux/selectors';
|
||||
import LinkMsUser from './link-ms-user';
|
||||
import { useSubmit } from '../utils/fetch-all-curriculum-data';
|
||||
|
||||
// Redux Setup
|
||||
const mapStateToProps = createSelector(
|
||||
@@ -58,8 +58,7 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
updateSolutionFormValues,
|
||||
openCompletionModal: () => openModal('completion'),
|
||||
openHelpModal: () => openModal('help'),
|
||||
setIsProcessing,
|
||||
submitChallenge
|
||||
setIsProcessing
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
@@ -78,7 +77,6 @@ interface MsTrophyProps {
|
||||
pageContext: {
|
||||
challengeMeta: ChallengeMeta;
|
||||
};
|
||||
submitChallenge: () => void;
|
||||
t: TFunction;
|
||||
updateChallengeMeta: (arg0: ChallengeMeta) => void;
|
||||
}
|
||||
@@ -92,6 +90,9 @@ function MsTrophy(props: MsTrophyProps) {
|
||||
}
|
||||
}
|
||||
} = props;
|
||||
|
||||
const submitChallenge = useSubmit();
|
||||
|
||||
useEffect(() => {
|
||||
const {
|
||||
challengeMounted,
|
||||
@@ -124,7 +125,7 @@ function MsTrophy(props: MsTrophyProps) {
|
||||
}, []);
|
||||
|
||||
const handleSubmit = () => {
|
||||
const { setIsProcessing, submitChallenge } = props;
|
||||
const { setIsProcessing } = props;
|
||||
|
||||
setIsProcessing(true);
|
||||
submitChallenge();
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Certification } from '@freecodecamp/shared/config/certification-settings';
|
||||
import { SuperBlocks } from '@freecodecamp/shared/config/curriculum';
|
||||
|
||||
// Mock curriculum data for fetch-all-curriculum-data
|
||||
export const mockCurriculumData = {
|
||||
allChallengeNode: {
|
||||
nodes: [
|
||||
{
|
||||
challenge: {
|
||||
block: 'test-block',
|
||||
id: 'test-challenge-id'
|
||||
}
|
||||
},
|
||||
{
|
||||
challenge: {
|
||||
block: 'another-block',
|
||||
id: 'another-challenge-id'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
allCertificateNode: {
|
||||
nodes: [
|
||||
{
|
||||
challenge: {
|
||||
certification: Certification.RespWebDesign,
|
||||
tests: [{ id: 'test-project-1' }, { id: 'test-project-2' }]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
allSuperBlockStructure: {
|
||||
nodes: [
|
||||
{
|
||||
superBlock: SuperBlocks.RespWebDesignV9,
|
||||
chapters: [
|
||||
{
|
||||
dashedName: 'chapter-1',
|
||||
comingSoon: false,
|
||||
modules: [
|
||||
{
|
||||
dashedName: 'module-1',
|
||||
comingSoon: false,
|
||||
moduleType: 'workshop',
|
||||
blocks: ['test-block', 'another-block']
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useStaticQuery, graphql } from 'gatsby';
|
||||
|
||||
import { updateAllChallengesInfo } from '../../../redux/actions';
|
||||
import { submitChallenge } from '../redux/actions';
|
||||
import {
|
||||
updateSuperBlockStructures,
|
||||
superBlockStructuresSelector
|
||||
} from '../../../templates/Introduction/redux';
|
||||
import {
|
||||
allChallengesInfoSelector,
|
||||
needsCurriculumDataSelector
|
||||
} from '../../../redux/selectors';
|
||||
import type {
|
||||
CertificateNode,
|
||||
ChallengeNode,
|
||||
SuperBlockStructure
|
||||
} from '../../../redux/prop-types';
|
||||
|
||||
interface AllCurriculumData {
|
||||
allChallengeNode: { nodes: ChallengeNode[] };
|
||||
allCertificateNode: { nodes: CertificateNode[] };
|
||||
allSuperBlockStructure: { nodes: SuperBlockStructure[] };
|
||||
}
|
||||
|
||||
export function useFetchAllCurriculumData(): void {
|
||||
const dispatch = useDispatch();
|
||||
const needsCurriculumData = useSelector(needsCurriculumDataSelector);
|
||||
const allChallengesInfo = useSelector(allChallengesInfoSelector) as {
|
||||
challengeNodes?: Array<{ challenge: { id: string } }>;
|
||||
} | null;
|
||||
const superBlockStructures = useSelector(
|
||||
superBlockStructuresSelector
|
||||
) as Record<string, SuperBlockStructure>;
|
||||
|
||||
const {
|
||||
allChallengeNode: { nodes: challengeNodes },
|
||||
allCertificateNode: { nodes: certificateNodes },
|
||||
allSuperBlockStructure: { nodes: superBlockStructureNodes }
|
||||
}: AllCurriculumData = useStaticQuery(graphql`
|
||||
query GetAllCurriculumData {
|
||||
allChallengeNode(
|
||||
sort: {
|
||||
fields: [
|
||||
challenge___superOrder
|
||||
challenge___order
|
||||
challenge___challengeOrder
|
||||
]
|
||||
}
|
||||
) {
|
||||
nodes {
|
||||
challenge {
|
||||
block
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
allCertificateNode {
|
||||
nodes {
|
||||
challenge {
|
||||
certification
|
||||
tests {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
allSuperBlockStructure {
|
||||
nodes {
|
||||
superBlock
|
||||
chapters {
|
||||
dashedName
|
||||
comingSoon
|
||||
modules {
|
||||
dashedName
|
||||
comingSoon
|
||||
moduleType
|
||||
blocks
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
useEffect(() => {
|
||||
// Only dispatch if curriculum data is needed
|
||||
if (!needsCurriculumData) return;
|
||||
|
||||
// Update allChallengesInfo if not already loaded
|
||||
if (!allChallengesInfo?.challengeNodes?.length) {
|
||||
dispatch(
|
||||
updateAllChallengesInfo({
|
||||
challengeNodes,
|
||||
certificateNodes
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Update superBlockStructures if not already loaded
|
||||
if (Object.keys(superBlockStructures || {}).length === 0) {
|
||||
const structuresMap: Record<string, SuperBlockStructure> = {};
|
||||
superBlockStructureNodes.forEach(node => {
|
||||
structuresMap[node.superBlock] = node;
|
||||
});
|
||||
dispatch(updateSuperBlockStructures(structuresMap));
|
||||
}
|
||||
}, [
|
||||
dispatch,
|
||||
needsCurriculumData,
|
||||
challengeNodes,
|
||||
certificateNodes,
|
||||
superBlockStructureNodes,
|
||||
allChallengesInfo,
|
||||
superBlockStructures
|
||||
]);
|
||||
}
|
||||
|
||||
export function useSubmit() {
|
||||
// The submitChallenge epic needs the curriculum data to be loaded, so this
|
||||
// useFetchAllCurriculumData call must happen first.
|
||||
useFetchAllCurriculumData();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return () => dispatch(submitChallenge());
|
||||
}
|
||||
Reference in New Issue
Block a user