From ba61d33698e762e3ababdf6523072e6a4671641d Mon Sep 17 00:00:00 2001 From: Ahmad Abdolsaheb Date: Fri, 27 Feb 2026 15:23:21 +0300 Subject: [PATCH] fix(client): pull curriculum data where needed (#65710) Co-authored-by: Oliver Eyton-Williams --- client/src/components/Progress/progress.tsx | 118 +--------------- client/src/redux/selectors.js | 12 ++ .../templates/Challenges/classic/editor.tsx | 8 +- .../components/completion-modal.test.tsx | 6 + .../components/completion-modal.tsx | 9 +- .../Challenges/components/hotkeys.tsx | 12 +- .../components/independent-lower-jaw.test.tsx | 27 +++- .../components/independent-lower-jaw.tsx | 11 +- client/src/templates/Challenges/exam/show.tsx | 8 +- .../Challenges/fill-in-the-blank/show.tsx | 1 - .../templates/Challenges/ms-trophy/show.tsx | 11 +- .../utils/__fixtures__/curriculum-data.ts | 53 ++++++++ .../utils/fetch-all-curriculum-data.tsx | 127 ++++++++++++++++++ 13 files changed, 254 insertions(+), 149 deletions(-) create mode 100644 client/src/templates/Challenges/utils/__fixtures__/curriculum-data.ts create mode 100644 client/src/templates/Challenges/utils/fetch-all-curriculum-data.tsx diff --git a/client/src/components/Progress/progress.tsx b/client/src/components/Progress/progress.tsx index ea6313b38a1..95ee67d4d2c 100644 --- a/client/src/components/Progress/progress.tsx +++ b/client/src/components/Progress/progress.tsx @@ -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 + 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; @@ -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 = {}; - - // 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); diff --git a/client/src/redux/selectors.js b/client/src/redux/selectors.js index 15acdfbe5d4..c1517c86366 100644 --- a/client/src/redux/selectors.js +++ b/client/src/redux/selectors.js @@ -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]; diff --git a/client/src/templates/Challenges/classic/editor.tsx b/client/src/templates/Challenges/classic/editor.tsx index 61f4e34297a..e86b0fd8e72 100644 --- a/client/src/templates/Challenges/classic/editor.tsx +++ b/client/src/templates/Challenges/classic/editor.tsx @@ -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(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<{ diff --git a/client/src/templates/Challenges/components/completion-modal.test.tsx b/client/src/templates/Challenges/components/completion-modal.test.tsx index d648d3f80cb..dd89ce57534 100644 --- a/client/src/templates/Challenges/components/completion-modal.test.tsx +++ b/client/src/templates/Challenges/components/completion-modal.test.tsx @@ -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('', () => { + beforeEach(() => { + vi.mocked(useStaticQuery).mockReturnValue(mockCurriculumData); + }); + describe('fireConfetti', () => { beforeEach(() => { mockFireConfetti.mockClear(); diff --git a/client/src/templates/Challenges/components/completion-modal.tsx b/client/src/templates/Challenges/components/completion-modal.tsx index 8d579a3cb77..08794645bf2 100644 --- a/client/src/templates/Challenges/components/completion-modal.tsx +++ b/client/src/templates/Challenges/components/completion-modal.tsx @@ -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; 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(); + 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(() => { diff --git a/client/src/templates/Challenges/components/hotkeys.tsx b/client/src/templates/Challenges/components/hotkeys.tsx index 21688a7a44e..a2525b6a1d3 100644 --- a/client/src/templates/Challenges/components/hotkeys.tsx +++ b/client/src/templates/Challenges/components/hotkeys.tsx @@ -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, 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 acadace0995..245577e2dbc 100644 --- a/client/src/templates/Challenges/components/independent-lower-jaw.test.tsx +++ b/client/src/templates/Challenges/components/independent-lower-jaw.test.tsx @@ -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('', () => { + beforeEach(() => { + vi.mocked(useStaticQuery).mockReturnValue(mockCurriculumData); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + it('shows share buttons when the block is completed on the last step', () => { - render(); + render(, createStore()); expect(screen.getByTestId('share-on-x')).toBeInTheDocument(); expect(screen.getByTestId('share-on-bluesky')).toBeInTheDocument(); @@ -45,7 +60,8 @@ describe('', () => { {...baseProps} completedPercent={50} completedChallengeIds={['id-1']} - /> + />, + createStore() ); expect(screen.queryByTestId('share-on-x')).not.toBeInTheDocument(); @@ -57,7 +73,8 @@ describe('', () => { {...baseProps} currentBlockIds={[baseChallengeMeta.id, 'id-2']} completedChallengeIds={[baseChallengeMeta.id, 'id-2']} - /> + />, + createStore() ); 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 8c11e230b8b..8d6312ab9e3 100644 --- a/client/src/templates/Challenges/components/independent-lower-jaw.tsx +++ b/client/src/templates/Challenges/components/independent-lower-jaw.tsx @@ -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); diff --git a/client/src/templates/Challenges/exam/show.tsx b/client/src/templates/Challenges/exam/show.tsx index ed1e8a25a86..f78f4be001d 100644 --- a/client/src/templates/Challenges/exam/show.tsx +++ b/client/src/templates/Challenges/exam/show.tsx @@ -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(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(); diff --git a/client/src/templates/Challenges/fill-in-the-blank/show.tsx b/client/src/templates/Challenges/fill-in-the-blank/show.tsx index e0ed807bc3a..dd9df3c9155 100644 --- a/client/src/templates/Challenges/fill-in-the-blank/show.tsx +++ b/client/src/templates/Challenges/fill-in-the-blank/show.tsx @@ -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] = diff --git a/client/src/templates/Challenges/ms-trophy/show.tsx b/client/src/templates/Challenges/ms-trophy/show.tsx index 15c7e09bd5e..4e04f7a8ef1 100644 --- a/client/src/templates/Challenges/ms-trophy/show.tsx +++ b/client/src/templates/Challenges/ms-trophy/show.tsx @@ -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(); diff --git a/client/src/templates/Challenges/utils/__fixtures__/curriculum-data.ts b/client/src/templates/Challenges/utils/__fixtures__/curriculum-data.ts new file mode 100644 index 00000000000..28d80e88a91 --- /dev/null +++ b/client/src/templates/Challenges/utils/__fixtures__/curriculum-data.ts @@ -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'] + } + ] + } + ] + } + ] + } +}; diff --git a/client/src/templates/Challenges/utils/fetch-all-curriculum-data.tsx b/client/src/templates/Challenges/utils/fetch-all-curriculum-data.tsx new file mode 100644 index 00000000000..f19cf958cf8 --- /dev/null +++ b/client/src/templates/Challenges/utils/fetch-all-curriculum-data.tsx @@ -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; + + 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 = {}; + 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()); +}