diff --git a/client/src/components/layouts/default.tsx b/client/src/components/layouts/default.tsx index ad865472aec..a86eae022a8 100644 --- a/client/src/components/layouts/default.tsx +++ b/client/src/components/layouts/default.tsx @@ -4,6 +4,7 @@ import { TFunction, withTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; import { createSelector } from 'reselect'; +import { useStaticQuery, graphql } from 'gatsby'; import latoBoldURL from '../../../static/fonts/lato/Lato-Bold.woff'; import latoLightURL from '../../../static/fonts/lato/Lato-Light.woff'; @@ -16,7 +17,8 @@ import { isBrowser } from '../../../utils'; import { fetchUser, onlineStatusChange, - serverStatusChange + serverStatusChange, + updateAllChallengesInfo } from '../../redux/actions'; import { isSignedInSelector, @@ -26,7 +28,12 @@ import { userFetchStateSelector } from '../../redux/selectors'; -import { UserFetchState, User } from '../../redux/prop-types'; +import { + UserFetchState, + User, + AllChallengeNode, + CertificateNode +} from '../../redux/prop-types'; import BreadCrumb from '../../templates/Challenges/components/bread-crumb'; import Flash from '../Flash'; import { flashMessageSelector, removeFlashMessage } from '../Flash/redux'; @@ -77,7 +84,8 @@ const mapDispatchToProps = (dispatch: Dispatch) => fetchUser, removeFlashMessage, onlineStatusChange, - serverStatusChange + serverStatusChange, + updateAllChallengesInfo }, dispatch ); @@ -117,10 +125,13 @@ function DefaultLayout({ t, theme = Themes.Default, user, - fetchUser + fetchUser, + updateAllChallengesInfo }: DefaultLayoutProps): JSX.Element { + const { challengeEdges, certificateNodes } = useGetAllBlockIds(); useEffect(() => { // componentDidMount + updateAllChallengesInfo({ challengeEdges, certificateNodes }); if (!isSignedIn) { fetchUser(); } @@ -237,6 +248,50 @@ function DefaultLayout({ } } +// TODO: get challenge nodes directly rather than wrapped in edges +const useGetAllBlockIds = () => { + const { + allChallengeNode: { edges: challengeEdges }, + allCertificateNode: { nodes: certificateNodes } + }: { + allChallengeNode: AllChallengeNode; + allCertificateNode: { nodes: CertificateNode[] }; + } = useStaticQuery(graphql` + query getBlockNode { + allChallengeNode( + sort: { + fields: [ + challenge___superOrder + challenge___order + challenge___challengeOrder + ] + } + ) { + edges { + node { + challenge { + block + id + } + } + } + } + allCertificateNode { + nodes { + challenge { + certification + tests { + id + } + } + } + } + } + `); + + return { challengeEdges, certificateNodes }; +}; + DefaultLayout.displayName = 'DefaultLayout'; export default connect( diff --git a/client/src/redux/action-types.js b/client/src/redux/action-types.js index 7aa77c37257..c4c3e7b12c5 100644 --- a/client/src/redux/action-types.js +++ b/client/src/redux/action-types.js @@ -27,6 +27,7 @@ export const actionTypes = createTypes( 'updateDonationFormState', 'updateUserToken', 'postChargeProcessing', + 'updateAllChallengesInfo', ...createAsyncTypes('fetchUser'), ...createAsyncTypes('postCharge'), ...createAsyncTypes('fetchProfileForUser'), diff --git a/client/src/redux/actions.js b/client/src/redux/actions.js index 43c1d3923b9..38608a046bc 100644 --- a/client/src/redux/actions.js +++ b/client/src/redux/actions.js @@ -53,6 +53,10 @@ export const fetchUser = createAction(actionTypes.fetchUser); export const fetchUserComplete = createAction(actionTypes.fetchUserComplete); export const fetchUserError = createAction(actionTypes.fetchUserError); +export const updateAllChallengesInfo = createAction( + actionTypes.updateAllChallengesInfo +); + export const postCharge = createAction(actionTypes.postCharge); export const postChargeProcessing = createAction( actionTypes.postChargeProcessing diff --git a/client/src/redux/index.js b/client/src/redux/index.js index 9dbaf84a81e..a6328ee5980 100644 --- a/client/src/redux/index.js +++ b/client/src/redux/index.js @@ -56,6 +56,10 @@ const initialState = { userFetchState: { ...defaultFetchState }, + allChallengesInfo: { + challengeEdges: [], + certificateNodes: [] + }, userProfileFetchState: { ...defaultFetchState }, @@ -153,7 +157,10 @@ export const reducer = handleActions( ...state, donationFormState: { ...defaultDonationFormState, error: payload } }), - + [actionTypes.updateAllChallengesInfo]: (state, { payload }) => ({ + ...state, + allChallengesInfo: { ...payload } + }), [actionTypes.fetchUser]: state => ({ ...state, userFetchState: { ...defaultFetchState } diff --git a/client/src/redux/prop-types.ts b/client/src/redux/prop-types.ts index e7444fcb018..062b201a3eb 100644 --- a/client/src/redux/prop-types.ts +++ b/client/src/redux/prop-types.ts @@ -142,6 +142,19 @@ export type ChallengeNode = { }; }; +export type CertificateNode = { + challenge: { + // TODO: use enum + certification: string; + tests: { id: string }[]; + }; +}; + +export type AllChallengesInfo = { + challengeEdges: { node: ChallengeNode }[]; + certificateNodes: CertificateNode[]; +}; + export type AllChallengeNode = { edges: [ { diff --git a/client/src/redux/selectors.js b/client/src/redux/selectors.js index 9faca5b8eab..eb6c71697c5 100644 --- a/client/src/redux/selectors.js +++ b/client/src/redux/selectors.js @@ -202,6 +202,8 @@ export const certificatesByNameSelector = username => state => { }; export const userFetchStateSelector = state => state[MainApp].userFetchState; +export const allChallengesInfoSelector = state => + state[MainApp].allChallengesInfo; export const userProfileFetchStateSelector = state => state[MainApp].userProfileFetchState; export const usernameSelector = state => state[MainApp].appUsername; diff --git a/client/src/templates/Challenges/components/completion-modal.tsx b/client/src/templates/Challenges/components/completion-modal.tsx index 95e64be8f98..de8c7076b8c 100644 --- a/client/src/templates/Challenges/components/completion-modal.tsx +++ b/client/src/templates/Challenges/components/completion-modal.tsx @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ import { Button, Modal } from '@freecodecamp/react-bootstrap'; -import { useStaticQuery, graphql } from 'gatsby'; import { noop } from 'lodash-es'; import React, { Component } from 'react'; import { TFunction, withTranslation } from 'react-i18next'; @@ -13,8 +12,11 @@ import { dasherize } from '../../../../../utils/slugs'; import { isFinalProject } from '../../../../utils/challenge-types'; import Login from '../../../components/Header/components/Login'; import { executeGA, allowBlockDonationRequests } from '../../../redux/actions'; -import { isSignedInSelector } from '../../../redux/selectors'; -import { AllChallengeNode, ChallengeFiles } from '../../../redux/prop-types'; +import { + isSignedInSelector, + allChallengesInfoSelector +} from '../../../redux/selectors'; +import { AllChallengesInfo, ChallengeFiles } from '../../../redux/prop-types'; import { closeModal, submitChallenge } from '../redux/actions'; import { @@ -34,6 +36,7 @@ const mapStateToProps = createSelector( completedChallengesIds, isCompletionModalOpenSelector, isSignedInSelector, + allChallengesInfoSelector, successMessageSelector, ( challengeFiles: ChallengeFiles, @@ -45,6 +48,7 @@ const mapStateToProps = createSelector( completedChallengesIds: string[], isOpen: boolean, isSignedIn: boolean, + allChallengesInfo: AllChallengesInfo, message: string ) => ({ challengeFiles, @@ -54,6 +58,7 @@ const mapStateToProps = createSelector( completedChallengesIds, isOpen, isSignedIn, + allChallengesInfo, message }) ); @@ -118,6 +123,7 @@ interface CompletionModalsProps { id: string; isOpen: boolean; isSignedIn: boolean; + allChallengesInfo: AllChallengesInfo; message: string; submitChallenge: () => void; superBlock: string; @@ -324,58 +330,13 @@ interface Options { isFinalProjectBlock: boolean; } -interface CertificateNode { - challenge: { - // TODO: use enum - certification: string; - tests: { id: string }[]; - }; -} - const useCurrentBlockIds = ( + allChallengesInfo: AllChallengesInfo, block: string, certification: string, options?: Options ) => { - const { - allChallengeNode: { edges: challengeEdges }, - allCertificateNode: { nodes: certificateNodes } - }: { - allChallengeNode: AllChallengeNode; - allCertificateNode: { nodes: CertificateNode[] }; - } = useStaticQuery(graphql` - query getCurrentBlockNodes { - allChallengeNode( - sort: { - fields: [ - challenge___superOrder - challenge___order - challenge___challengeOrder - ] - } - ) { - edges { - node { - challenge { - block - id - } - } - } - } - allCertificateNode { - nodes { - challenge { - certification - tests { - id - } - } - } - } - } - `); - + const { challengeEdges, certificateNodes } = allChallengesInfo; const currentCertificateIds = certificateNodes .filter( node => dasherize(node.challenge.certification) === certification @@ -390,6 +351,7 @@ const useCurrentBlockIds = ( const CompletionModal = (props: CompletionModalsProps) => { const currentBlockIds = useCurrentBlockIds( + props.allChallengesInfo, props.block || '', props.certification || '', // eslint-disable-next-line @typescript-eslint/no-unsafe-call